/*  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 vmm3d.core3D;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;

import org.freehep.graphics2d.VectorGraphics;

import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;

import javax.swing.event.ChangeEvent;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import vmm3d.actions.AbstractActionVMM;
import vmm3d.actions.ActionList;
import vmm3d.actions.ActionRadioGroup;
import vmm3d.core.Axes2D;
import vmm3d.core.Display;
import vmm3d.core.Exhibit;
import vmm3d.core.I18n;
import vmm3d.core.MouseTask;
import vmm3d.core.Prefs;
import vmm3d.core.SaveAndRestore;
import vmm3d.core.Transform;
import vmm3d.core.Util;
import vmm3d.core.VMMSave;
import vmm3d.core.View;

/**
 * Extends the View class to support three-dimensional viewing.
 */
public class View3D extends View {
	
	/**
	 * One of the possible values for the viewStyle, specifying a basic non-stereo view.  This is the default view style.
	 */
	public static final int MONOCULAR_VIEW = 0;

	/**
	 * One of the possible values for the viewStyle, specifying "anaglyph stereo". That is, left and right eye views
	 * are drawn in green and red, respectively, and the two views are composed into a single image that can be viewed
	 * with red/green or red/blue stereo glasses.  In this view, the background color is forced to black.
	 */
	public static final int RED_GREEN_STEREO_VIEW = 1;
	
	/**
	 * One of the possible values for the viewStyle, specifying a "stereograph" view.  That is, the left and
	 * right eye views are drawn side-by-side.  The user can view a 3D image by looking at a point in back of the
	 * images, to make the left and right eye views fuse into a single image.
	 */
	public static final int STEREOGRAPH_VIEW = 2;
	
	/**
	 * One of the possible values for the viewStyle, specifying a "stereograph" view.  That is, the left and
	 * right eye views are drawn side-by-side.  The user can view a 3D image by crossing his or her eyes to fuse
	 * the left ane right eye views.
	 */
	public static final int CROSS_EYE_STEREO_VIEW = 3;
	
	@VMMSave
	private int viewStyle = MONOCULAR_VIEW;

	private Color savedBackground;
	
	@VMMSave
	private boolean enableThreeD = true;
	
	private Transform saveTransform2DWhileThreeDEnabled = null;
	private Transform saveTransform3DWhileThreeDDisabled = null;
	private int saveViewStyleWhile3DDisabled = -1;

	
	/**
	 * The current transform of this view, if it is a Transform3D.  This is just a reference to
	 * the same object refereced by {@link View#getTransform()}, and is here for convenience only.
	 */
	protected Transform3D transform3D;

	/**
	 * Used when the view style is RED_GREEN_STEREO_VIEW.
	 */
	protected StereoComposite stereoComposite;
	
	private Rectangle leftEyeRect, rightEyeRect;  // for STEROGRAPH_VIEW and CROSS_EYE_STEREO_VIEW
	private Dimension stereographPreferredSize;
	private Rectangle stereographPreferredLeftEyeRect, stereographPreferredRightEyeRect;
	private int saveStereographDisplayWidth, saveStereographDisplayHeight;
	private Dimension savePreferredSizeForStereograph;
	
	private VectorGraphics leftEyeGraphics, rightEyeGraphics, leftEyeUntransformedGraphics, rightEyeUntransformedGraphics;
	private VectorGraphics saveGraphicsDuringStereo, saveUntransformedGraphicsDuringStereo;
	
	private Vector3D viewDirection;  // a copy of transform3D.getViewDirection(), for use for clipping during 3D drawing
	private double clipZ;     // z-value above which points are clipped, for use during 3D drawing
	private double eyeSeparationMultiplier;   // the current eyeSeparationMultiplier, for use during 3D drawing.
	
	/**
	 * Left and right offscreen images, used during stereograph viewing.
	 */
	protected BufferedImage leftStereographOSI, rightStereographOSI;

	/**
	 * A menu item for selecting one of the four possible view styles.  This is used in
	 * {@link #getViewCommands()}.
	 */
	protected ActionRadioGroup viewStyleCommands = new ActionRadioGroup(new String[] {
			I18n.tr("vmm3d.core3D.commands.Monocular"),
			I18n.tr("vmm3d.core3D.commands.RedGreenStereo"),
			I18n.tr("vmm3d.core3D.commands.Stereograph"),
			I18n.tr("vmm3d.core3D.commands.CrossEyeStereo")
	}, 0) {
		public void optionSelected(int selectedIndex) {
			setViewStyle(selectedIndex);
		}
	};

	/**
	 * A set of two radio items for selecting between orthograhic and prespective projection.
	 */
	 protected ActionRadioGroup projectionCommands = new ActionRadioGroup(new String[] {
			I18n.tr("vmm3d.core3D.commands.PerspectiveProjection"),
			I18n.tr("vmm3d.core3D.commands.OrthographicProjection")
	}, 0) {
		public void optionSelected(int selectedIndex) {
			setOrthographicProjection(selectedIndex == 1);
		}
	};
	
	/**
	 * A menu item that will show a Set Viewpoint dialog box.  This is created in
	 * {@link #getSettingsCommands()}
	 */
	protected AbstractActionVMM setViewpointAction;
	
	/**
	 * A menu item that will show a Set Eye Separation Multiplier dialog box.  This is created in
	 * {@link #getSettingsCommands()}
	 */
	protected AbstractActionVMM set3DViewOptionsAction;
	
	/**
	 * Returns the currently selected viewstyle.
	 * @see #setViewStyle(int)
	 */
	public int getViewStyle() {
		return viewStyle;
	}
	
	/**
	 * Set the current view styles.  This must be one of the four constants
	 * {@link #MONOCULAR_VIEW}, {@link #RED_GREEN_STEREO_VIEW}, {@link #CROSS_EYE_STEREO_VIEW},
	 * or {@link #STEREOGRAPH_VIEW}.
	 * @param style the new view style.  This is ignored if it is not one of the four legal options.
	 */
	public void setViewStyle(int style) {
		if (style < MONOCULAR_VIEW || style > CROSS_EYE_STEREO_VIEW)
			return;
		viewStyleCommands.setSelectedIndex(style);
		if (style == viewStyle)
			return;
		fullOSI = leftStereographOSI = rightStereographOSI = null;  // force creation of new offscreen image
		if (style == RED_GREEN_STEREO_VIEW) {
			Color c = getBackground();
			setBackground(Color.BLACK);
			savedBackground = c;  // must be done after setBackground, since that method sets savedBackground to null
			backgroundCommands.setEnabled(false);
		}
		else {
			if (viewStyle == RED_GREEN_STEREO_VIEW) {
				if ( savedBackground != null )
					setBackground(savedBackground);
			}
			backgroundCommands.setEnabled(true);
		}
		if (style == CROSS_EYE_STEREO_VIEW || style == STEREOGRAPH_VIEW) {  // Changing INTO sterograph view
			enterStereographView(viewStyle,style);
		}
		else if (viewStyle == CROSS_EYE_STEREO_VIEW || viewStyle == STEREOGRAPH_VIEW) {  // Changing OUT of stereograph view
			leaveStereographView();
		}
		viewStyle = style;
		if (transform3D != null) {
			if (viewStyle != RED_GREEN_STEREO_VIEW  || ! "yes".equals(Prefs.get("view3d.moveObjectsForwardInAnaglyph")))
				transform3D.setObjectDisplacementNormalToScreen(0);
			else {
				if (getViewStyle() == View3D.RED_GREEN_STEREO_VIEW && getTransform3D() != null) {
					double extent = Math.max(getTransform3D().getXmaxRequested() - getTransform3D().getXminRequested(),
							getTransform3D().getYmaxRequested() - getTransform3D().getYminRequested());
					getTransform3D().setObjectDisplacementNormalToScreen(extent/3);
				}
			}
		}
		forceRedraw();
	}
	
	private void enterStereographView(int oldViewStyle, int viewStyle) {
		int dotsPerInch = Toolkit.getDefaultToolkit().getScreenResolution();  // does not give correct answer on Mac ??-- gives 72
		if (dotsPerInch == 72 && Util.isMacOS())
			dotsPerInch = 100;
		if (oldViewStyle != CROSS_EYE_STEREO_VIEW && oldViewStyle != STEREOGRAPH_VIEW)
			savePreferredSizeForStereograph = getDisplay() == null ? null : new Dimension(getDisplay().getWidth(),getDisplay().getHeight());
		Dimension preferredSize = null;
//		String prefs;
//		if (viewStyle == CROSS_EYE_STEREO_VIEW)
//			prefs = Prefs.get("core3D.cross_eye_stereo.preferredSize");
//		else 
//			prefs = Prefs.get("core3D.parallel_stereo.preferredSize");
//		if (prefs != null) {
//			try {
//				String[] sizeStrings = Util.explode(prefs,",");
//				int[] sizeData = new int[10];
//				for (int i = 0; i < 10; i++) {
//					sizeData[i] = Integer.parseInt(sizeStrings[i]);
//					if (sizeData[i] < 50 || sizeData[i] > 2000)
//						throw new Exception();
//				}
//				leftEyeRect = new Rectangle(sizeData[0],sizeData[1],sizeData[2],sizeData[3]);
//				if (leftEyeRect.width <= 0 || leftEyeRect.height <= 0)
//					throw new Exception();
//				rightEyeRect = new Rectangle(sizeData[4],sizeData[5],sizeData[6],sizeData[7]);
//				if (rightEyeRect.width <= 0 || rightEyeRect.height <= 0)
//					throw new Exception();
//				preferredSize = new Dimension(sizeData[8],sizeData[9]);
//			}
//			catch (Exception e) {
//				preferredSize = null;  // if the data from Preferences is not of the correct form.
//			}
//		}
//		if (preferredSize == null) {
			if (viewStyle == CROSS_EYE_STEREO_VIEW) {
				leftEyeRect = new Rectangle(dotsPerInch-20,+5,7*dotsPerInch/2,7*dotsPerInch/2);
				rightEyeRect = new Rectangle(5*dotsPerInch-20,5,7*dotsPerInch/2,7*dotsPerInch/2);
				preferredSize = new Dimension(19*dotsPerInch/2-40, 12+7*dotsPerInch/2);
			}
			else {
				leftEyeRect = new Rectangle(dotsPerInch-20,5,(5*dotsPerInch)/2,(5*dotsPerInch)/2);
				rightEyeRect = new Rectangle(dotsPerInch+5+(5*dotsPerInch)/2,5,(5*dotsPerInch)/2,(5*dotsPerInch)/2);
				preferredSize = new Dimension(-14+7*dotsPerInch, 12+(5*dotsPerInch)/2);
			}
//		}
		stereographPreferredSize = preferredSize;
		stereographPreferredLeftEyeRect = (Rectangle)leftEyeRect.clone();
		stereographPreferredRightEyeRect = (Rectangle)rightEyeRect.clone();
		saveStereographDisplayWidth = preferredSize.width;
		saveStereographDisplayHeight = preferredSize.height;
		if (getDisplay() != null) {
			getDisplay().setPreferredSize(preferredSize);
			if (preferredSize.width > getDisplay().getWidth() || preferredSize.height > getDisplay().getHeight()) {
				double scale = Math.min((double)getDisplay().getWidth()/preferredSize.width, 
						(double)getDisplay().getHeight()/preferredSize.height);
				leftEyeRect.x = (int)(leftEyeRect.x * scale);
				rightEyeRect.x = (int)(rightEyeRect.x * scale);
				leftEyeRect.width = rightEyeRect.width = (int)(leftEyeRect.width * scale);
				leftEyeRect.height = rightEyeRect.height = (int)(leftEyeRect.height * scale);
			}
			int left = (getDisplay().getWidth() - (rightEyeRect.x + rightEyeRect.width - leftEyeRect.x))/2;
			int top = (getDisplay().getHeight() - leftEyeRect.height)/2;
			rightEyeRect.x = rightEyeRect.x + (left - leftEyeRect.x);
			leftEyeRect.x = left;
			rightEyeRect.y = leftEyeRect.y = top;
			saveStereographDisplayWidth = getDisplay().getWidth();
			saveStereographDisplayHeight = getDisplay().getHeight();
		}
	}
	
	private void adjustStereographToNewSize(int width, int height) { 
		leftEyeRect = (Rectangle)stereographPreferredLeftEyeRect.clone();
		rightEyeRect = (Rectangle)stereographPreferredRightEyeRect.clone();
		double scale = Math.min((double)width/stereographPreferredSize.width, 
				(double)height/stereographPreferredSize.height);
		leftEyeRect.x = (int)(leftEyeRect.x * scale);
		rightEyeRect.x = (int)(rightEyeRect.x * scale);
		leftEyeRect.width = rightEyeRect.width = (int)(leftEyeRect.width * scale);
		leftEyeRect.height = rightEyeRect.height = (int)(leftEyeRect.height * scale);
		int left = (width - (rightEyeRect.x + rightEyeRect.width - leftEyeRect.x))/2;
		int top = (height - leftEyeRect.height)/2;
		rightEyeRect.x = rightEyeRect.x + (left - leftEyeRect.x);
		leftEyeRect.x = left;
		rightEyeRect.y = leftEyeRect.y = top;
		saveStereographDisplayWidth = width;
		saveStereographDisplayHeight = height;
//		String prefs = "" + leftEyeRect.x + ',' +leftEyeRect.y + ',' +leftEyeRect.width + ',' +leftEyeRect.height + ',' +
//		               rightEyeRect.x + ',' +rightEyeRect.y + ',' +rightEyeRect.width + ',' +rightEyeRect.height + ',' +
//		               width + ',' + height;
//		if (viewStyle == CROSS_EYE_STEREO_VIEW)  {
//			Prefs.put("core3D.cross_eye_stereo.preferredSize",prefs);
//			Prefs.save("core3D.cross_eye_stereo.preferredSize");
//		}
//		else if (viewStyle == STEREOGRAPH_VIEW) {
//			Prefs.put("core3D.parallel_stereo.preferredSize",prefs);
//			Prefs.save("core3D.parallel_stereo.preferredSize");
//		}
	}
	
	private void leaveStereographView() {
		if (getDisplay() != null) {
			if (savePreferredSizeForStereograph == null)
				getDisplay().setPreferredSize(new Dimension(800,490));
			else
				getDisplay().setPreferredSize(savePreferredSizeForStereograph);
		}
	}
	
	/**
	 * This method moves the left- and right-eye views of the exhibit closer together or farther apart,
	 * within the limits of available space.  This method is called by {@link BasicMouseTask3D}, and
	 * will probably be used rarely if ever otherwise.
	 * @param offset The number of pixels to move the images.  A positive value moves them closer
	 * together.  A negative value moves them farther apart.  However, they can't be moved closer together
	 * than when they are touching or farther apart than the border of the window permits.
	 */
	public void moveStereographImages(int offset) {
		if (viewStyle != CROSS_EYE_STEREO_VIEW && viewStyle != STEREOGRAPH_VIEW)
			return;
		Display display = getDisplay();
		if (display == null)
			return;
		if (offset < 0) {
			if (leftEyeRect.x + offset < 0)
				offset = -leftEyeRect.x;
			if (offset >= 0)
				return;
		}
		else {
			if (leftEyeRect.x + leftEyeRect.width + offset + 1 > rightEyeRect.x - offset - 1)
				offset = (rightEyeRect.x - (leftEyeRect.x + leftEyeRect.width))/2 - 1;
			if (offset <= 0)
				return;
		}
		leftEyeRect.x += offset;
		rightEyeRect.x -= offset;
		forceRedraw();
//		String prefs = "" + leftEyeRect.x + ',' +leftEyeRect.y + ',' +leftEyeRect.width + ',' +leftEyeRect.height + ',' +
//                       rightEyeRect.x + ',' +rightEyeRect.y + ',' +rightEyeRect.width + ',' +rightEyeRect.height + ',' +
//                       display.getWidth() + ',' + display.getHeight();
//		if (viewStyle == CROSS_EYE_STEREO_VIEW)  {
//			Prefs.put("core3D.cross_eye_stereo.preferredSize",prefs);
//			Prefs.save("core3D.cross_eye_stereo.preferredSize");
//		}
//		else if (viewStyle == STEREOGRAPH_VIEW) {
//			Prefs.put("core3D.parallel_stereo.preferredSize",prefs);
//			Prefs.save("core3D.parallel_stereo.preferredSize");
//		}
	}
	
	
	/**
	 * This method is used in BasicMouseTask3D to determine the rectangle that containts
	 * the left-eye  image of a stereographic view.  It is also used in
	 * {@link vmm3d.ode.ODE_3D} for transforming screen points to object points.
	 */
	public Rectangle stereographLeftEyeRect() { // for use in BasicMouseTask3D
		return leftEyeRect;
	}
	
	/**
	 * This  method is used in BasicMouseTask3D to determine the rectangle that containts
	 * the left-eye  image of a stereographic view.  It is also used in
	 * {@link vmm3d.ode.ODE_3D} for transforming screen points to object points.
	 */
	public Rectangle stereographRightEyeRect() {
		return rightEyeRect;
	}
	
	//-------------------- Implementing hybrid 2D/3D View (July 2006) ---------------------------
	
	/**
	 * Gets the value of the enableThreeD property.
	 * @see #setEnableThreeD(boolean)
	 */
	public boolean getEnableThreeD() {
		return enableThreeD;
	}
	
	/**
	 * Sets the enableThreeD property.  When this property is set to false, three-D related items
	 * in the Settings menu are disabled.  When 3D is disabled, the view style is set to MONOCULAR, the Viewpoint is
	 * set to (1,0,0), and the projection is set to be orthographic, so any 3D drawing that is
	 * done while 3D is disabled will use a simple projection onto the yz-plane by discarding the
	 * x-coordinate.  (Although in general you should not be doing any 3D drawing while 3D is disabled.)
	 * When 3D is enabled after being disabled, the view style and viewpoint settings are restored to
	 * what they were when 3D was entered unless they have been changed in the meantime.
	 * 

Note that if this view has an Axes decoration, they type of Axes decoration (Axes2D or Axes3D) * is set to match the new state of the enableThreeD property. *

WARNING: This method does NOT do anything about mouse tasks, so you might want * to change the mouse task in this View's display at the same time that you call this method. */ public void setEnableThreeD(boolean enable) { if (enable == enableThreeD) return; boolean showAxes = getShowAxes(); if (showAxes) setShowAxes(false); enableThreeD = enable; if (showAxes) setShowAxes(true); if (enableThreeD) { saveTransform2DWhileThreeDEnabled = getTransform(); if (saveTransform3DWhileThreeDDisabled != null) { setTransform(saveTransform3DWhileThreeDDisabled); saveTransform3DWhileThreeDDisabled = null; } else { setTransform(new Transform3D()); } if (saveViewStyleWhile3DDisabled >= 0) { setViewStyle(saveViewStyleWhile3DDisabled); saveViewStyleWhile3DDisabled = -1; } } else { saveTransform3DWhileThreeDDisabled = getTransform(); if (saveTransform2DWhileThreeDEnabled != null) { setTransform(saveTransform2DWhileThreeDEnabled); saveTransform2DWhileThreeDEnabled = null; } else { Transform3D tr = new Transform3D(); if (getTransform() != null) // copy only limits, not viewpoint tr.setLimits(getTransform().getXmin(),getTransform().getXmax(),getTransform().getYmin(),getTransform().getYmax()); setTransform(tr); } saveViewStyleWhile3DDisabled = getViewStyle(); setViewStyle(MONOCULAR_VIEW); setViewPoint( new Vector3D(1,0,0)); setOrthographicProjection(true); } viewStyleCommands.setEnabled(enableThreeD); if (setViewpointAction != null) setViewpointAction.setEnabled(enableThreeD); projectionCommands.setEnabled(enableThreeD); forceRedraw(); // (probably many times redundant!) } /** * Set the x and y ranges to be used when 3D has been disabled by {@link #setEnableThreeD(boolean)}. * This method can be called at any time, even while 3D is enabled -- if it is, the specified * ranges only come in to effect the next time 3D is disabled. */ public void setWindowForUseWhileThreeDDisabled(double xmin, double xmax, double ymin, double ymax) { if (!enableThreeD) setWindow(xmin,xmax,ymin,ymax); else { if (saveTransform2DWhileThreeDEnabled == null) saveTransform2DWhileThreeDEnabled = new Transform3D(); saveTransform2DWhileThreeDEnabled.setLimits(xmin,xmax,ymin,ymax); } } /** * If 3D is enabled, this returns the saved transform that is used when 3D is disabled; * if 3D is disabled, this returns the saved transform that is used when 3D is enabled. * This method was introduced to be used by {@link SaveAndRestore} and will probably not * be used otehrwise. */ protected Transform getSavedAuxiliaryTransformForEnableThreeD() { if (enableThreeD) return saveTransform2DWhileThreeDEnabled; else return saveTransform3DWhileThreeDDisabled; } /** * If 3D is enabled this sets the saved transform that is used when 3D is disabled; * if 3D is disabled, this sets the saved transform that is used when 3D is enabled. * This method was introduced to be used by {@link SaveAndRestore} and will probably not be * used otherwise. */ protected void setSavedAuxiliaryTransformForEnableThreeD(Transform transform) { if (enableThreeD) saveTransform2DWhileThreeDEnabled = transform; else saveTransform3DWhileThreeDDisabled = transform; } //========================================== /** * Returns true if this view is currently set to use an orthographic projection, and false if it is set to * use a perspective projection. */ public boolean getOrthographicProjection() { if (transform3D == null) return true; else return transform3D.getOrthographicProjection(); } /** * Set to true to use an orthographic projection, and to false to use a perspective projection. * The default is to use a perspective projection. */ public void setOrthographicProjection(boolean orthographic) { if (transform3D != null) transform3D.setOrthographicProjection(orthographic); } /** * Returns the current viewpoint for this View's transform. (In the unexpected case that this * view is not using a 3D transform, returns null.) * @see #setViewPoint(Vector3D) */ public Vector3D getViewPoint() { if (transform3D == null) return null; else return transform3D.getViewPoint(); } /** * Sets the viewpoint for this View's transform. (Assuming that this view is using a 3D transform -- * if not, nothing is done.) Note that calling this method will also adjust the imagePlaneYDirection * by projecting the current imagePlaneYDirection onto the new view plane. * Note that when an Exhbit is installed, the Exhibit's default Transform replaces whatever * transform already exists, so there is not much use calling this when no exhibit is installed. * @see #setViewUp(Vector3D) * @see Transform3D#setViewPoint(Vector3D) */ public void setViewPoint(Vector3D viewpoint) { if (transform3D != null && !transform3D.getViewPoint().equals(viewpoint)) transform3D.setViewPoint(viewpoint); } /** * Returns the current imagePlaneYDirection for this View's transform. (In the unexpected case that this * view is not using a 3D transform, returns null.) * @see #setViewUp(Vector3D) */ public Vector3D getViewUp() { if (transform3D == null) return null; else return transform3D.getImagePlaneYDirection(); } /** * Sets the view up vector for this View's transform. (Assuming that this view is using a 3D transform -- * if not, nothing is done.) Note that the specified viewUp vector will have to be projected * onto the image plane to give the actual imagePlaneYDirection. * @see Transform3D#setImagePlaneYDirection(Vector3D) */ public void setViewUp(Vector3D viewUp) { if (transform3D != null) transform3D.setImagePlaneYDirection(viewUp); } // --------------------- override some methods from the View class for 3D ----------------- /** * Set the exhibit shown in this view. It is overridden in this class to set * {@link #transform3D} and to manage stereographic viewing if necessary. */ public void setExhibit(Exhibit exhibit) { if (exhibit == getExhibit()) return; if (!enableThreeD) saveTransform2DWhileThreeDEnabled = getTransform(); super.setExhibit(exhibit); if (getTransform() != null && getTransform() instanceof Transform3D) transform3D = (Transform3D)getTransform(); else transform3D = null; savedBackground = null; if (exhibit != null && (viewStyle == STEREOGRAPH_VIEW || viewStyle == CROSS_EYE_STEREO_VIEW)) enterStereographView(MONOCULAR_VIEW,viewStyle); if (viewStyle == RED_GREEN_STEREO_VIEW && transform3D != null && "yes".equals(Prefs.get("view3d.moveObjectsForwardInAnaglyph"))) { double extent = Math.max(transform3D.getXmaxRequested() - transform3D.getXminRequested(), transform3D.getYmaxRequested() - transform3D.getYminRequested()); transform3D.setObjectDisplacementNormalToScreen(extent/3); } if (!enableThreeD) { saveTransform3DWhileThreeDDisabled = getTransform(); setTransform(saveTransform2DWhileThreeDEnabled); saveTransform2DWhileThreeDEnabled = null; } } public void setTransform(Transform transform) { super.setTransform(transform); if (transform != null && transform instanceof Transform3D) { transform3D = (Transform3D)transform; projectionCommands.setSelectedIndex( transform3D.getOrthographicProjection() ? 1 : 0); if (viewStyle == RED_GREEN_STEREO_VIEW && "yes".equals(Prefs.get("view3d.moveObjectsForwardInAnaglyph"))) { double extent = Math.max(transform3D.getXmaxRequested() - transform3D.getXminRequested(), transform3D.getYmaxRequested() - transform3D.getYminRequested()); transform3D.setObjectDisplacementNormalToScreen(extent/3); } } else transform3D = null; } /** * Set the display where this view draws its exhibit. This is overridden in this class * to put the display into stereograpic view mode, if the view is currently set to * use sterographic viewing. */ public void setDisplay(Display display) { super.setDisplay(display); if (getExhibit() != null && (viewStyle == STEREOGRAPH_VIEW || viewStyle == CROSS_EYE_STEREO_VIEW)) enterStereographView(MONOCULAR_VIEW,viewStyle); else { String anaglyphViewPref = Prefs.get("view3d.initialAnaglyphMode", "default"); if (anaglyphViewPref.equalsIgnoreCase("always")) { if (enableThreeD) setViewStyle(RED_GREEN_STEREO_VIEW); else saveViewStyleWhile3DDisabled = RED_GREEN_STEREO_VIEW; } else if (anaglyphViewPref.equalsIgnoreCase("never")) { if (enableThreeD) setViewStyle(MONOCULAR_VIEW); else saveViewStyleWhile3DDisabled = MONOCULAR_VIEW; } } } /** * Called when a view is removed from its display. In this case, it restores the viewStyle * to MONOCULAR_VIEW to avoid leaving the display set up for stereo viewing. */ public void finish() { // called when a view is removed from the display. setViewStyle(MONOCULAR_VIEW); } /** * Set this View to view the same exhibit as another specififed view. * This is overridden here to copy the 3D transform from the specified * view (if it has one) and to copy its viewStyle if it is a View3D and * to properly handle the enableThreeD propety. * Since a 3D View cannot make effective use of a 2D transform, the * sharedTransform parameter is ignored if the transform in the given * view is not a 3D transform. */ public void takeExhibit(View view, boolean shareTransform) { savedBackground = null; if ( ! shareTransform || view.getTransform() == null || ! (view.getTransform() instanceof Transform3D)) { super.takeExhibit(view,false); // Can't share a 2D transform. return; } if (view instanceof View3D) setEnableThreeD(((View3D)view).getEnableThreeD()); super.takeExhibit(view, true); transform3D = (Transform3D)getTransform(); if (view instanceof View3D) { // Also copy extra transform for implementing enableThreeD proeprty. View3D v3 = (View3D)view; if (enableThreeD) { // Alread set this to match v3.enableThreeD saveTransform2DWhileThreeDEnabled = v3.saveTransform2DWhileThreeDEnabled; setViewStyle(v3.getViewStyle()); } else { saveTransform3DWhileThreeDDisabled = v3.saveTransform3DWhileThreeDDisabled; saveViewStyleWhile3DDisabled = v3.saveViewStyleWhile3DDisabled; } } } /** * If the transform associated with this view is a {@link Transform3D}, then that transform is returned. * Otherwise, the return value is null. */ public Transform3D getTransform3D() { if (getTransform() instanceof Transform3D) return (Transform3D)getTransform(); else return null; } /** * Returns an Axes decoration that is appropriate for this view. If * 3D is enabled, returns an object of type {@link Axes3D} and returns * an {@link Axes2D} if 3D is disabled. * @see #setEnableThreeD(boolean) */ protected Axes2D createAxes() { if (enableThreeD) return new Axes3D(); else return new Axes2D(); } /** * Returns a MouseTase of type {@link BasicMouseTask3D}, which is approriate for most 3D views. */ public MouseTask getDefaultMouseTask() { return new BasicMouseTask3D(); } public void setBackground(Color c) { super.setBackground(c); savedBackground = null; } /** * React to a change by rebuilding the offscreen bit map. This overrides the method in * the View class for the following reason: If the state change comes from a Transform3D, * it might be a change in the "orthographicProjection" property of a Transform3D that * is shared with another view, so the setting of the projectionCommands is changed to * match the setting in the Transform3D. */ public void stateChanged(ChangeEvent evt) { super.stateChanged(evt); if (evt.getSource() instanceof Transform3D) { boolean isOrtho = ((Transform3D)evt.getSource()).getOrthographicProjection(); projectionCommands.setSelectedIndex( isOrtho? 1 : 0); } } // ------------------------- View and Setting Commands ----------------------------------------------------------------------- /** * Returns a list of view commands appropriate for a View3D. This method adds commands for selecting * between orthographic and perspective projection and for selecting the viewStyle to the list * of commands obtained from the superclass. */ public ActionList getViewCommands() { ActionList commands = super.getViewCommands(); commands.add(null); // Add projection selection commands commands.add(projectionCommands); projectionCommands.setEnabled(enableThreeD); commands.add(null); commands.add(viewStyleCommands); return commands; } /** * Adds a "Set Viewpoint" command to any settings commands created by the superclass. */ public ActionList getSettingsCommands() { ActionList commands = super.getSettingsCommands(); setViewpointAction = new AbstractActionVMM(I18n.tr("vmm3d.core3D.commands.SetViewpoint")) { public void actionPerformed(ActionEvent evt) { if (getDisplay() != null) getDisplay().stopAnimation(); SetViewpointDialog.showDialog(View3D.this); } }; set3DViewOptionsAction = new AbstractActionVMM(I18n.tr("vmm3d.core3D.commands.Set3DViewOptions") + "...") { public void actionPerformed(ActionEvent evt) { if (getDisplay() != null) getDisplay().stopAnimation(); Set3DViewOptionsDialog.showDialog(View3D.this); } }; commands.add(set3DViewOptionsAction); commands.add(null); setViewpointAction.setEnabled(enableThreeD); commands.add(setViewpointAction); return commands; } // -------------------- methods used in rendering, overridden in this class to -------------------------------- // -------------------- modify their behavior in the case of stereo viewing. ----------------------------------- /** * This is called by render to check whether a new offscreen image needs to be * created, given the specified width and height of the drawing area. */ protected boolean needsNewOSI(int width, int height) { if (viewStyle == MONOCULAR_VIEW || viewStyle == RED_GREEN_STEREO_VIEW) return super.needsNewOSI(width,height); else return leftStereographOSI == null || width != saveStereographDisplayWidth || height != saveStereographDisplayHeight; } /** * This is called by render to copy the offscreen image to the screen. * The width and height of the drawing area are given as parameters, since they * are needed in the case of stereograph views. */ protected void putOSI(VectorGraphics g, int width, int height) { /* if (viewStyle == MONOCULAR_VIEW || viewStyle == RED_GREEN_STEREO_VIEW) { // g.drawImage(fullOSI,0,0,null); g.repaint(); //repaint(); System.out.println("DEBUG: putOSI from View3D"); //g.dispose(); } else { Color saveColor = g.getColor(); g.setColor(Color.WHITE); g.fillRect(0,0,width,height); g.setColor(Color.GRAY); g.drawRect(leftEyeRect.x-1, leftEyeRect.y-1, leftEyeRect.width+1, leftEyeRect.width+1); g.drawRect(rightEyeRect.x-1, rightEyeRect.y-1, rightEyeRect.width+1, rightEyeRect.width+1); g.drawImage(leftStereographOSI,leftEyeRect.x,leftEyeRect.y,null); g.drawImage(rightStereographOSI,rightEyeRect.x,rightEyeRect.y,null); g.setColor(saveColor); } */ } /** * Create an offscreen image for drawing. This is overridden here to handle the left- and right-eye views that are * needed for anaglyph stereo and stereograph viewing. It is not meant to be called directly. */ protected void createOSI(int width, int height) { fullOSI = leftStereographOSI = rightStereographOSI = null; if (viewStyle == MONOCULAR_VIEW) super.createOSI(width,height); else if (viewStyle == RED_GREEN_STEREO_VIEW) { if (stereoComposite == null) stereoComposite = new StereoComposite(); stereoComposite.setSize(width,height); fullOSI = stereoComposite.getImage(); } else if (viewStyle == STEREOGRAPH_VIEW || viewStyle == CROSS_EYE_STEREO_VIEW) { if (width != saveStereographDisplayWidth || height != saveStereographDisplayHeight) adjustStereographToNewSize(width,height); leftStereographOSI = new BufferedImage(leftEyeRect.width,leftEyeRect.height,offscreenImageType); rightStereographOSI = new BufferedImage(leftEyeRect.width,leftEyeRect.height,offscreenImageType); fullOSI = leftStereographOSI; //so it won't be null } } /** * Fills the offscreen image with the background color of this View. This is overidden here * to handle the left- and right-eye views in the case of anaglyph stereo and stereograph viewing. */ protected void clearOSI() { if (viewStyle == MONOCULAR_VIEW) super.clearOSI(); else if (viewStyle == RED_GREEN_STEREO_VIEW) { Graphics g = stereoComposite.getLeftEyeGraphics(); if (g != null) { VectorGraphics vg = VectorGraphics.create(g); vg.setColor(Color.black); vg.fillRect(0,0,stereoComposite.getWidth(),stereoComposite.getHeight()); vg.setColor(Color.white); } g.dispose(); g = stereoComposite.getRightEyeGraphics(); if (g != null) { VectorGraphics vg = VectorGraphics.create(g); vg.setColor(Color.black); vg.fillRect(0,0,stereoComposite.getWidth(),stereoComposite.getHeight()); vg.setColor(Color.white); } g.dispose(); } else { Graphics g = leftStereographOSI.getGraphics(); VectorGraphics vg = VectorGraphics.create(g); vg.setColor(getBackground()); vg.fillRect(0,0,leftEyeRect.width,leftEyeRect.height); vg.dispose(); g = rightStereographOSI.getGraphics(); vg = VectorGraphics.create(g); vg.setColor(getBackground()); vg.fillRect(0,0,leftEyeRect.width,leftEyeRect.height); vg.dispose(); } } /** * This is called by methods in the View class to prepare the off-screen image for drawing. It is overridden here * to handle th special setup needed for anaglyph stereo and stereograph viewing. It is not meant to be called directly. */ protected VectorGraphics prepareOSIForDrawing() { //System.out.println("DEBUG: Call to prepareOSIForDrawing()"); viewDirection = transform3D.getViewDirection(); viewDirection.negate(); // since the view direction returned above points from the viewpoint towards the origen, and I want the reverse for clipping clipZ = transform3D.getFocalLength() - transform3D.getClipDistance(); eyeSeparationMultiplier = Prefs.getDouble("eyeSeparationMultiplier", 1); if (viewStyle == MONOCULAR_VIEW) return super.prepareOSIForDrawing(); if (viewStyle == RED_GREEN_STEREO_VIEW) { //VectorGraphics g = (VectorGraphics)stereoComposite.getLeftEyeGraphics().create(); VectorGraphics g=getCurrentGraphics(); if (g != null) { g.setColor(Color.white); g.setBackground(Color.black); if (getAntialiased()) g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); getTransform().setUpDrawInfo(g,0,0,stereoComposite.getWidth(),stereoComposite.getHeight(),getPreserveAspect(),getApplyGraphics2DTransform()); leftEyeGraphics = g; leftEyeUntransformedGraphics = getTransform().getUntransformedGraphics(); } //g = (VectorGraphics)stereoComposite.getRightEyeGraphics().create(); g=getCurrentGraphics(); if (g != null) { g.setColor(Color.white); g.setBackground(Color.black); if (getAntialiased()) g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); getTransform().setUpDrawInfo(g,0,0,stereoComposite.getWidth(),stereoComposite.getHeight(),getPreserveAspect(),getApplyGraphics2DTransform()); rightEyeGraphics = g; rightEyeUntransformedGraphics = getTransform().getUntransformedGraphics(); } //g = (VectorGraphics)fullOSI.getGraphics(); g=getCurrentGraphics(); getTransform().setUpDrawInfo(g,0,0,stereoComposite.getWidth(),stereoComposite.getHeight(),getPreserveAspect(),getApplyGraphics2DTransform()); currentGraphics = g; return g; } else { //VectorGraphics g = (VectorGraphics)rightStereographOSI.getGraphics(); VectorGraphics g =getCurrentGraphics(); g.setColor(getForeground()); g.setBackground(getBackground()); getTransform().setUpDrawInfo(g,0,0,leftEyeRect.width,leftEyeRect.height,getPreserveAspect(),getApplyGraphics2DTransform()); if (getAntialiased()) g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); rightEyeGraphics = g; rightEyeUntransformedGraphics = getTransform().getUntransformedGraphics(); g = (org.freehep.graphics2d.VectorGraphics)leftStereographOSI.getGraphics(); if (getAntialiased()) g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(getForeground()); g.setBackground(getBackground()); getTransform().setUpDrawInfo(g,0,0,leftEyeRect.width,leftEyeRect.height,getPreserveAspect(),getApplyGraphics2DTransform()); leftEyeGraphics = g; leftEyeUntransformedGraphics = getTransform().getUntransformedGraphics(); currentGraphics = g; return g; } } /** * This is called by methods in the View class to clean up after drawing to the off-screen image. It is overridden * here to do the cleanup necessary for anaglyph stereo viewing. It is not meant to be called directly. */ protected void finishOSIDraw() { if (viewStyle == MONOCULAR_VIEW) super.finishOSIDraw(); else { leftEyeGraphics.dispose(); rightEyeGraphics.dispose(); if (leftEyeUntransformedGraphics != null) leftEyeUntransformedGraphics.dispose(); if (rightEyeUntransformedGraphics != null) rightEyeUntransformedGraphics.dispose(); if (viewStyle == RED_GREEN_STEREO_VIEW) stereoComposite.compose(); } } //----------------------- XML routines ------------------------------------------- /** * Overridden to add extra transform info. */ public void addExtraXML(Document containingDocument, Element viewElement) { super.addExtraXML(containingDocument, viewElement); Transform savedTransform = getSavedAuxiliaryTransformForEnableThreeD(); if (savedTransform != null) { Element transformElement = SaveAndRestore.makeTransformElement("savedtransform",containingDocument, savedTransform); viewElement.appendChild(transformElement); } } /** * Overridden to read back the extra transform info. */ public void readExtraXML(Element viewInfo) throws IOException { super.readExtraXML(viewInfo); Element savedtransformElement = SaveAndRestore.getChildElement(viewInfo,"savedtransform"); if (savedtransformElement != null) { Transform transform = SaveAndRestore.buildTransformFromElement(savedtransformElement); setSavedAuxiliaryTransformForEnableThreeD(transform); } } //----------------------- 3D drawing routines ------------------------------------- /** * This method can be called during a drawing operation, such as in {@link Exhibit3D#doDraw3D(Graphics2D, View3D, Transform3D)}, * to draw a single pixel. This method is designed to work correctly in stereo views as well as in a standard monocular * view; for stereo viewing, the pixel is drawn in both the left- and the right-eye view. * @param v a non-null vector giving the coordinates of the pixel to be drawn, in world coordinates. * @see #drawPixels(Vector3D[]) */ public void drawPixel(Vector3D v) { Point2D pt = new Point2D.Double(); if (clip(v)) return; if (viewStyle == MONOCULAR_VIEW) { transform3D.objectToXYWindowCoords(v,pt); transform3D.windowToViewport(pt); drawPixelDirect(null,(int)pt.getX(),(int)pt.getY()); } else { setUpForLeftEye(); transform3D.objectToXYWindowCoords(v,pt); transform3D.windowToViewport(pt); drawPixelDirect(null,(int)pt.getX(),(int)pt.getY()); setUpForRightEye(); transform3D.objectToXYWindowCoords(v,pt); transform3D.windowToViewport(pt); drawPixelDirect(null,(int)pt.getX(),(int)pt.getY()); finishStereoView(); } } /** * Draws a dot of specified diameter centered at a specified 3D point. * This is done by calling the org.freehep.graphics2d.VectorGraphics fill() command for an * appropriate Ellipse2D. The diameter is specified in pixels. * Note that if the preserveAspectRatio is off for this View, then * the dot will can be an oval rather than a circle. */ public void drawDot(Vector3D pt, double diameter) { double h = diameter*transform3D.getPixelWidth(); double w = diameter*transform3D.getPixelHeight(); Point2D pt2D = new Point2D.Double(); if (viewStyle == MONOCULAR_VIEW) { transform3D.objectToDrawingCoords(pt, pt2D); currentGraphics.fill(new Ellipse2D.Double(pt2D.getX()-h/2,pt2D.getY()-w/2,h,w)); } else { setUpForLeftEye(); transform3D.objectToDrawingCoords(pt, pt2D); currentGraphics.fill(new Ellipse2D.Double(pt2D.getX()-h/2,pt2D.getY()-w/2,h,w)); setUpForRightEye(); transform3D.objectToDrawingCoords(pt, pt2D); currentGraphics.fill(new Ellipse2D.Double(pt2D.getX()-h/2,pt2D.getY()-w/2,h,w)); finishStereoView(); } } /** * This method can be called during a drawing operation, such as in {@link Exhibit3D#doDraw3D(Graphics2D, View3D, Transform3D)}, * to draw a list of pixels. This method is designed to work correctly in stereo views as well as in a standard monocular * view; for stereo viewing, each pixel is drawn in both the left- and the right-eye view. This method is more efficient * than drawing each pixel individually when drawing in stereo. * @param vlist a non-null array of vectors, where each vector contains the coordinates of the pixel to be drawn, in world coordinates. * The individual vectors in the list can be null; null values in the array are ignored. */ public void drawPixels(Vector3D[] vlist) { Point2D pt = new Point2D.Double(); if (viewStyle == MONOCULAR_VIEW) { for (int i = 0; i < vlist.length; i++) if (vlist[i] != null && !clip(vlist[i])) { transform3D.objectToXYWindowCoords(vlist[i],pt); transform3D.windowToViewport(pt); drawPixelDirect(null,(int)pt.getX(),(int)pt.getY()); } } else { setUpForLeftEye(); for (int i = 0; i < vlist.length; i++) if (vlist[i] != null && !clip(vlist[i])) { transform3D.objectToXYWindowCoords(vlist[i],pt); transform3D.windowToViewport(pt); drawPixelDirect(null,(int)pt.getX(),(int)pt.getY()); } setUpForRightEye(); for (int i = 0; i < vlist.length; i++) if (vlist[i] != null && !clip(vlist[i])) { transform3D.objectToXYWindowCoords(vlist[i],pt); transform3D.windowToViewport(pt); drawPixelDirect(null,(int)pt.getX(),(int)pt.getY()); } finishStereoView(); } } /** * This method can be called during a drawing operation, such as in {@link Exhibit3D#doDraw3D(Graphics2D, View3D, Transform3D)}, * to draw a line segment. This method is designed to work correctly in stereo views as well as in a standard monocular * view; for stereo viewing, the line segment is drawn in both the left- and the right-eye view. * @param v1 a non-null vector giving the coordinates of one endpoint of the line segment, in world coordinates. * @param v2 a non-null vector giving the coordinates of a second endpoint of the line segment, in world coordinates. */ public void drawLine(Vector3D v1, Vector3D v2) { Point2D p1, p2; if (clip(v1) || clip(v2)) return; if (viewStyle == MONOCULAR_VIEW) { p1 = transform3D.objectToDrawingCoords(v1); p2 = transform3D.objectToDrawingCoords(v2); currentGraphics.draw(new Line2D.Float(p1,p2)); } else { setUpForLeftEye(); p1 = transform3D.objectToDrawingCoords(v1); p2 = transform3D.objectToDrawingCoords(v2); currentGraphics.draw(new Line2D.Float(p1,p2)); setUpForRightEye(); p1 = transform3D.objectToDrawingCoords(v1); p2 = transform3D.objectToDrawingCoords(v2); currentGraphics.draw(new Line2D.Float(p1,p2)); finishStereoView(); } } /** * This method can be called during a drawing operation, such as in {@link Exhibit3D#doDraw3D(Graphics2D, View3D, Transform3D)}, * to draw a string. Only the basepoint of the string undergoes transformation and projection. The string itself * is always drawn flat on the screen, as if it is located in the view plane. * This method is designed to work correctly in stereo views as well as in a standard monocular * view; for stereo viewing, the string is drawn in both the left- and the right-eye view. * @param str the non-null string that is to be drawn. * @param basepoint a non-null vector giving the coordinates of the basepoing of the string to be drawn, in world coordinates. */ public void drawString(String str, Vector3D basepoint) { if (clip(basepoint)) return; if (viewStyle == MONOCULAR_VIEW) { Point2D p = transform3D.objectToXYWindowCoords(basepoint); super.drawString(str,p.getX(),p.getY()); // NOTE: Don't call drawString(str,Point2D) -- leads to infinite recursion } else { setUpForLeftEye(); Point2D p = transform3D.objectToXYWindowCoords(basepoint); super.drawString(str,p.getX(),p.getY()); setUpForRightEye(); p = transform3D.objectToXYWindowCoords(basepoint); super.drawString(str,p.getX(),p.getY()); finishStereoView(); } } /** * Draws a curve in three-space. This is a convenience method that just calls drawCurve(points,points.length). * @see #drawCurve(Vector3D[], int) */ public void drawCurve(Vector3D[] points) { if (points != null) drawCurve(points,points.length); } /** * Draws a curve in three-space. The curve is drawn by connecting points in a given array of 3D points with * line segments. This method is designed to work correctly in stereo views as well as in a standard monocular * view; for stereo viewing, the curve is drawn in both the left- and the right-eye view. * @param points A non-null array containing the points on the curve. Null values are allowed in this array. * A null value is treated as a missing point on the curve -- no connecting line segment is drawn from the missing * point to the points on either side. This makes it possible for a single array to define multiple disconnected * curve segments. * @param pointCount The number of points on the curve. Only the points in positions 0 through pointCount-1 in * the array are used when drawing the curve. */ public void drawCurve(Vector3D[] points, int pointCount) { drawCurve(points,0,pointCount-1); } /** * Draws a curve in three-space. The curve is drawn by connecting points in a given array of 3D points with * line segments. This method is designed to work correctly in stereo views as well as in a standard monocular * view; for stereo viewing, the curve is drawn in both the left- and the right-eye view. This version of * drawCurve makes it possible to draw any contiguous sequence of points on the curve. * @param points A non-null array containing the points on the curve. Null values are allowed in this array. * A null value is treated as a missing point on the curve -- no connecting line segment is drawn from the missing * point to the points on either side. This makes it possible for a single array to define multiple disconnected * curve segments. * @param startIndex The number of points in the array that should be used for the curve. A curve is drawn * though points[startIndex], point[startIndex+1], ..., points[endIndex]. The value of startIndex * is clamped to lie in the range 0 to points.length-1. * @param endIndex The number of points in the array that should be used for the curve. A curve is drawn * though points[startIndex], point[startIndex+1], ..., points[endIndex]. The value of endIndex * is clamped to lie in the range 0 to points.length-1. If startIndex is less than or equal to endIndex, * nothing is drawn. */ public void drawCurve(Vector3D[] points, int startIndex, int endIndex) { if (points == null) return; if (startIndex >= points.length) startIndex = points.length - 1; if (startIndex < 0) startIndex = 0; if (endIndex >= points.length) endIndex = points.length - 1; if (endIndex < 0) endIndex = 0; if (endIndex <= startIndex) return; Point2D[] projectedPoints = new Point2D[endIndex - startIndex + 1]; if (viewStyle == MONOCULAR_VIEW) { for (int i = 0; i < projectedPoints.length; i++){ if (points[i - startIndex] == null || clip(points[i - startIndex])) projectedPoints[i] = null; else projectedPoints[i] = transform3D.objectToXYWindowCoords(points[i - startIndex]); } super.drawCurve(projectedPoints); } else { setUpForLeftEye(); for (int i = 0; i < projectedPoints.length; i++){ if (points[i - startIndex] == null || clip(points[i - startIndex])) projectedPoints[i] = null; else projectedPoints[i] = transform3D.objectToXYWindowCoords(points[i - startIndex]); } super.drawCurve(projectedPoints); setUpForRightEye(); for (int i = 0; i < projectedPoints.length; i++){ if (points[i - startIndex] == null || clip(points[i - startIndex])) projectedPoints[i] = null; else projectedPoints[i] = transform3D.objectToXYWindowCoords(points[i - startIndex]); } super.drawCurve(projectedPoints); finishStereoView(); } } /** * A convenience method that simply calls drawCollardCurve(points,0,points.length-1,reversed). * @see #drawCollaredCurve(Vector3D[], int, int, boolean) */ public void drawCollaredCurve(Vector3D[] points, boolean reversed) { if (points != null) drawCollaredCurve(points,0,points.length-1,reversed); } /** * A convenience method that simply calls drawCollardCurve(points,0,pointCount-1,reversed). * @see #drawCollaredCurve(Vector3D[], int, int, boolean) */ public void drawCollaredCurve(Vector3D[] points, int pointCount, boolean reversed) { if (points != null) drawCollaredCurve(points,0,pointCount-1,reversed); } /** * Draws a curve in three-space, possibley using "collars" to produce a more three-dimensional effect. * The collars can be used in two ways, either reversed or not reversed. If reverse is false, then * the curve is drawn in monocular views by connecting points in a given array of 3D points with * line segments, except that where one part of the curve passes over another, the back segment is "broken", * crossing-diagram style, to show which curve is in front. (The "break" is actually the result of drawing a * wide collar around the curve in the background color.) However, this crossing diagram style is only * used in a monocular view, not in stereo views; for stereo viewing, a solid curve is drawn using * the {@link #drawCurve(Vector3D[], int)} method. *

If "reversed" is true, then the curve is drawn with reversed collars in both monocular and stereo views. * For a reversed collar, the curve itself is drawn in the background color, and the collar is drawn in the * foreground color. This gives a nice 3D effect. * @param points A non-null array containing the points on the curve. Null values are allowed in this array. * A null value is treated as a missing point on the curve -- no connecting line segment is drawn from the missing * point to the points on either side. This makes it possible for a single array to define multiple disconnected * curve segments. * @param startIndex The number of points in the array that should be used for the curve. A curve is drawn * though points[startIndex], point[startIndex+1], ..., points[endIndex]. The value of startIndex * is clamped to lie in the range 0 to points.length-1. * @param endIndex The number of points in the array that should be used for the curve. A curve is drawn * though points[startIndex], point[startIndex+1], ..., points[endIndex]. The value of endIndex * is clamped to lie in the range 0 to points.length-1. If startIndex is less than or equal to endIndex, * nothing is drawn. * @param reversed tells whether or not to used reversed colors for drawing the collar. */ public void drawCollaredCurve(Vector3D[] points, int startIndex, int endIndex, boolean reversed) { if (points == null) return; if (!reversed && viewStyle != MONOCULAR_VIEW) { drawCurve(points,startIndex,endIndex); return; } if (startIndex >= points.length) startIndex = points.length - 1; if (startIndex < 0) startIndex = 0; if (endIndex >= points.length) endIndex = points.length - 1; if (endIndex < 0) endIndex = 0; if (endIndex <= startIndex) return; if (viewStyle == MONOCULAR_VIEW) drawCollaredCurveDirect(points,startIndex,endIndex,reversed); else { setUpForLeftEye(); drawCollaredCurveDirect(points,startIndex,endIndex,reversed); setUpForRightEye(); drawCollaredCurveDirect(points,startIndex,endIndex,reversed); finishStereoView(); } } /** * Draws a surface in wireframe. In general, surfaces should be drawn using the * {@link View3DLit#drawSurface(Grid3D)} and {@link View3DLit#drawSurface(Grid3D, double, double)} * methods, which will render the surface either as wireframe or as patches, as appropriate for the settings * in a {@link View3DLit}. However, * this drawWireframeSurface method can be used to render a wireframe surface in a plain View3D * or to draw a surface grid that really is intrinsically a wireframe in any 3D view. * @param surfaceData contains a grid of points on the surface */ public void drawWireframeSurface(Grid3D surfaceData) { if (getViewStyle() == MONOCULAR_VIEW) { surfaceData.applyTransform(transform3D, this); surfaceData.drawCurves(this, currentGraphics); } else { setUpForLeftEye(); surfaceData.applyTransform(transform3D, this); surfaceData.drawCurves(this, currentGraphics); setUpForRightEye(); surfaceData.applyTransform(transform3D, this); surfaceData.drawCurves(this, currentGraphics); finishStereoView(); } } // ---------- Override 2D draw methods so they work for 3D (drawing into the xy-plane in 3-space) -------- /** * Sets the pixel with pixel coordinates (x,y) to be a spelcified color. Note that this by-passes * the support in the View3D class for stereo viewing (that is, x and y are used as untransformed * pixel coordinates). The pixel is set in the current off-screen * image, which can be either the fullOSI for monocular viewing or either the left or right * OSI for stereo viewing. This method is meant mainly for use by lower level drawing methods, * such as the phong lighting. * @see #drawPixel(Vector3D) * @param color the color for the pixel; if null, the current drawing color is used. */ public void drawPixelDirect(Color color, int x, int y) { if (x < 0 || y < 0) return; int rgb; if (color == null) color = currentGraphics.getColor(); rgb = color.getRGB(); BufferedImage image; if (viewStyle == RED_GREEN_STEREO_VIEW) { if (currentGraphics == leftEyeGraphics) image = stereoComposite.getLeftEyeImage(); else image = stereoComposite.getRightEyeImage(); } else if (viewStyle == MONOCULAR_VIEW) image = fullOSI; else { if (currentGraphics == leftEyeGraphics) image = leftStereographOSI; else image = rightStereographOSI; } if (x >= image.getWidth() || y >= image.getHeight()) return; image.setRGB(x,y,rgb); } // ----------- Helper stuff for 3D rendering ----------------------------- private class CurveSegment extends Line2D.Double implements Comparable { Vector3D v1, v2; double midpoint_z; CurveSegment(Vector3D v1, Vector3D v2) { this.v1 = v1; this.v2 = v2; midpoint_z = (v1.z + v2.z)/2; Point2D p1 = new Point2D.Double(v1.x, v1.y); Point2D p2 = new Point2D.Double(v2.x, v2.y); if (!getTransform().appliedTransform2D()) { getTransform().windowToViewport(p1); getTransform().windowToViewport(p2); } setLine(p1,p2); } public int compareTo(CurveSegment c) { if (midpoint_z < c.midpoint_z) return -1; else if (midpoint_z > c.midpoint_z) return 1; else return 0; } } private void drawCollaredCurveDirect(Vector3D[] points, int startIndex, int endIndex, boolean reversed) { Vector3D p1 = points[startIndex]==null? null : transform3D.objectToViewCoords(points[startIndex]); ArrayList segments = new ArrayList(); for (int i = startIndex + 1; i <= endIndex; i++) { Vector3D p2 = points[i]==null || clip(points[i])? null : transform3D.objectToViewCoords(points[i]); if (p1 != null && p2 != null) segments.add(new CurveSegment(p1,p2)); p1 = p2; } java.util.Collections.sort(segments); Color saveColor = currentGraphics.getColor(); Stroke saveStroke = currentGraphics.getStroke(); Stroke wideStroke = new BasicStroke(getTransform().getDefaultStrokeSize() * 5, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); Stroke normalStroke = new BasicStroke(getTransform().getDefaultStrokeSize(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); for (int i = 0; i < segments.size(); i++) { currentGraphics.setStroke(wideStroke); currentGraphics.setColor(reversed ? saveColor : currentGraphics.getBackground()); currentGraphics.draw(segments.get(i)); currentGraphics.setStroke(normalStroke); currentGraphics.setColor(reversed ? currentGraphics.getBackground() : saveColor); currentGraphics.draw(segments.get(i)); } currentGraphics.setColor(saveColor); currentGraphics.setStroke(saveStroke); } // ------------------------- methods to support stereo rendering /** * To be used with the setUpForLeftEye and finishStereoView methods to support * 3D rendering. When rendering in one of the three stereo view styles, the image needs to be rendered * twice, once for the left eye and once for the right eye. The setUpForLeftEye methods * should be called first; this sets up the transform and current graphics context for rendering from * the viewpoint of the left eye into the left eye image. Next comes the code for drawing the object. * then setUpForRightEye, followed by a repeat of the code for drawing the object. * Finally, calling finshStereoView must be called to restore the original drawing * context. These three methods must always be used in this way. Note that most programmers will not * have to worry about this -- these methods will only be used when writing methods for rendering * new graphics primitives such as lines and surfaces in this class and its subclasses. */ protected void setUpForLeftEye() { double separationFactor; if (viewStyle == CROSS_EYE_STEREO_VIEW) separationFactor = 0.04; else if (viewStyle == STEREOGRAPH_VIEW) separationFactor = -0.03; else separationFactor = 0.03; separationFactor *= eyeSeparationMultiplier; transform3D.selectLeftEye(separationFactor); saveGraphicsDuringStereo = currentGraphics; // same as transform.getGraphics() saveUntransformedGraphicsDuringStereo = getTransform().getUntransformedGraphics(); Color color = saveGraphicsDuringStereo.getColor(); // copying color and stroke to left and right views, in case they were changed in currentGraphics leftEyeGraphics.setColor(color); rightEyeGraphics.setColor(color); Stroke stroke = saveGraphicsDuringStereo.getStroke(); leftEyeGraphics.setStroke(stroke); rightEyeGraphics.setStroke(stroke); Font font = saveGraphicsDuringStereo.getFont(); leftEyeGraphics.setFont(font); rightEyeGraphics.setFont(font); transform3D.useGraphics(leftEyeGraphics,leftEyeUntransformedGraphics); currentGraphics = leftEyeGraphics; } /** * To be used with {@link #setUpForLeftEye()} and finishStereoView to support * 3D rendering. */ protected void setUpForRightEye() { double separationFactor; if (viewStyle == CROSS_EYE_STEREO_VIEW) separationFactor = 0.04; else if (viewStyle == STEREOGRAPH_VIEW) separationFactor = -0.03; else separationFactor = 0.03; separationFactor *= eyeSeparationMultiplier; transform3D.selectRightEye(separationFactor); transform3D.useGraphics(rightEyeGraphics,rightEyeUntransformedGraphics); currentGraphics = rightEyeGraphics; } /** * To be used with {@link #setUpForLeftEye()} and {@link #setUpForRightEye()} to support * 3D rendering. */ protected void finishStereoView() { transform3D.selectNoEye(); transform3D.useGraphics(saveGraphicsDuringStereo,saveUntransformedGraphicsDuringStereo); currentGraphics = saveGraphicsDuringStereo; saveGraphicsDuringStereo = null; saveUntransformedGraphicsDuringStereo = null; } /** * This method is used to test whether a point should be clipped. It is valid * only during a 3D rendering operation. It is needed in {@link Grid3D} and in * {@link vmm3d.polyhedron.IFS}. */ final public boolean clip(Vector3D objectPoint) { return objectPoint.dot(viewDirection) > clipZ; } }

Ads help maintain this website.