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

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;

import vmm.core.Prefs;
import vmm.core.Transform;
import vmm.core.View;
import vmm.core.render.ImageRenderer2D;
import vmm.core3D.Grid3D;
import vmm.core3D.StereoComposite;
import vmm.core3D.Transform3D;
import vmm.core3D.View3D;
import vmm.core3D.Vector3D;
import vmm.core3D.View3DLit;

public class ImageRenderer3D extends ImageRenderer2D implements Renderer3D {
	
	protected View3D view3D;
	protected View3DLit view3DLit; // copy of view3D, or null if view3D is not an instance of view3DLit
	protected Transform3D transform3D;
	protected BufferedImage image2;  // right-eye image for stereo viewing
	protected StereoComposite stereoComposite;
	protected Graphics2D leftEyeGraphics, rightEyeGraphics;
	protected Graphics2D leftEyeUntransformedGraphics, rightEyeUntransformedGraphics;
	protected Graphics2D saveGraphicsDuringStereo, saveUntransformedGraphicsDuringStereo;
	protected Rectangle leftEyeRect, rightEyeRect;
	protected int currentWidth, currentHeight;
	protected int viewStyle;
	private Vector3D viewDirection;
	private double clipZ;
	private double eyeSeparationMultiplier;
		
	public void startRender(View view, Transform transform, int width, int height) {
		if (! (view instanceof View3D) || ! (transform instanceof Transform3D) ) {
			view3D = null;   // These will cause an error if any attempt is made to use 3D drawing methods.
			view3DLit = null;
			transform3D = null;
			viewStyle = View3D.MONOCULAR_VIEW;
			super.startRender(view,transform,width,height);
			return;
		}
		view3D = (View3D)view;
		view3DLit = (view instanceof View3DLit)? (View3DLit)view : null;
		this.transform = transform;
		transform3D = (Transform3D)transform;
		viewStyle = view3D.getViewStyle();
		if (viewStyle != View3D.RED_GREEN_STEREO_VIEW && stereoComposite != null) {
			stereoComposite.releaseMemory();
			stereoComposite = null;
		}
		int type = (view3DLit != null && view3DLit.getBlackAndWhite())? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_INT_RGB;
		switch (viewStyle) {
		case View3D.MONOCULAR_VIEW:
			image2 = null;
			if (image == null || image.getWidth() != width || image.getHeight() != height || image.getType() != type) {
				image = null;
				image = new BufferedImage(width,height,type);
			}
			break;
		case View3D.STEREOGRAPH_VIEW:
		case View3D.CROSS_EYE_STEREO_VIEW:
			Rectangle[] rects = view3D.getSterographViewRectangles();
			leftEyeRect = rects[0];
			rightEyeRect = rects[1];
			int w = leftEyeRect.width;
			int h = leftEyeRect.height;
			if (image == null || image2 == null || image.getWidth() != w || image.getHeight() != h
					|| image2.getWidth() != w || image2.getHeight() != h || image.getType() != type || image2.getType() != type) {
				image = image2 = null;
				image = new BufferedImage(w,h,type);
				image2 = new BufferedImage(w,h,type);
			}
			break;
		case View3D.RED_GREEN_STEREO_VIEW:
			image = image2 = null;
			if (stereoComposite == null)
				stereoComposite = new StereoComposite();
			if (stereoComposite.getWidth() != width || stereoComposite.getHeight() != height)
				stereoComposite.setSize(width,height);
			image  = stereoComposite.getLeftEyeImage();
			image2 = stereoComposite.getRightEyeImage();
			break;
		}
		setupRenderInfo(width,height,true);
		currentWidth = width;
		currentHeight = height;
	}
	
	private void setupRenderInfo(int width, int height, boolean clear) {
		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);
		foregroundColor = viewStyle == View3D.RED_GREEN_STEREO_VIEW? Color.WHITE : view3D.getForeground();
		backgroundColor = viewStyle == View3D.RED_GREEN_STEREO_VIEW? Color.BLACK: view3D.getBackground();
		antialiased = view3D.getAntialiased();
		currentColor = foregroundColor;
		if (viewStyle == View3D.MONOCULAR_VIEW) {
			currentGraphics = image.createGraphics();
			currentGraphics.setBackground(backgroundColor);
			if (clear)
				currentGraphics.clearRect(0, 0, width, height);
			currentGraphics.setColor(currentColor);
			if (antialiased)
				currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
			transform.setUpDrawInfo(currentGraphics, 0, 0, width, height, view3D.getPreserveAspect(), view3D.getApplyGraphics2DTransform());
		}
		else {
			leftEyeGraphics = image.createGraphics();
			rightEyeGraphics = image2.createGraphics();
			leftEyeGraphics.setColor(currentColor);
			leftEyeGraphics.setBackground(backgroundColor);
			if (clear)
				leftEyeGraphics.clearRect(0,0,image.getWidth(),image.getHeight());
			rightEyeGraphics.setColor(currentColor);
			rightEyeGraphics.setBackground(backgroundColor);
			if (clear)
				rightEyeGraphics.clearRect(0,0,image.getWidth(),image.getHeight());
			if (antialiased) {
				leftEyeGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
				rightEyeGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
			}
			transform3D.setUpDrawInfo(rightEyeGraphics,0,0,image2.getWidth(),image2.getHeight(),
					view3D.getPreserveAspect(),view3D.getApplyGraphics2DTransform());
			rightEyeUntransformedGraphics = transform3D.getUntransformedGraphics();
			transform3D.setUpDrawInfo(leftEyeGraphics,0,0,image.getWidth(),image.getHeight(),
					view3D.getPreserveAspect(),view3D.getApplyGraphics2DTransform());
			leftEyeUntransformedGraphics = transform3D.getUntransformedGraphics();
			currentGraphics = leftEyeGraphics;  // should not be used -- there should be no 2D drawing in stereo mode ??
		}
	}
	
	public boolean restartRender(View view, Transform transform, int width, int height) { 
		if (! (view instanceof View3D) )
			return super.restartRender(view,transform,width,height); 
		if (((View3D)view).getViewStyle() != viewStyle || width != currentWidth || height != currentHeight)
			return false;
		view3D = (View3D)view;
		view3DLit = (view instanceof View3DLit)? (View3DLit)view : null;
		transform3D = (Transform3D)transform;
		this.transform = transform;
		setupRenderInfo(width,height,false); 
		return true;
	}
		
	public void endRender() {  
		if (view3D == null) {
			super.endRender();
			return;
		}
		if (viewStyle == View3D.RED_GREEN_STEREO_VIEW)
			stereoComposite.compose();
		transform3D.finishDrawing();
		currentGraphics.dispose();
		currentGraphics = null;
		if (viewStyle != View3D.MONOCULAR_VIEW) {
			rightEyeGraphics.dispose();
			if (rightEyeUntransformedGraphics != rightEyeGraphics)
				rightEyeUntransformedGraphics.dispose();
			leftEyeGraphics = rightEyeGraphics = leftEyeUntransformedGraphics = rightEyeUntransformedGraphics = null;
		}
		transform = null;
		view3D = null;
		view3DLit = null;
		transform3D = null;
		
	}
	
	public void dispose() {
		if (view3D == null) {
			super.dispose();
			return;
		}
		image = image2 = null;
		if (stereoComposite != null) {
			stereoComposite.releaseMemory();
			stereoComposite = null;
		}
		currentWidth = currentHeight = 0;
	}
	
	public void draw(Graphics2D g) {
		//System.out.println("Draw " + Math.random());
		if (viewStyle == View3D.MONOCULAR_VIEW)
			g.drawImage(image,0,0,null);
		else if (viewStyle == View3D.RED_GREEN_STEREO_VIEW)
			g.drawImage(stereoComposite.getImage(),0,0,null);
		else {
			Color saveColor = g.getColor();
			g.setColor(Color.WHITE);
			g.fillRect(0,0,currentWidth,currentHeight);
			g.drawImage(image,leftEyeRect.x,leftEyeRect.y,null);
			g.drawImage(image2,rightEyeRect.x,rightEyeRect.y,null);
			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.setColor(saveColor);
		}
	}
	
	public BufferedImage getImage(boolean alwaysCopy) {
		if (image == null)
			return null;
		if (!alwaysCopy) {
			if (viewStyle == View3D.MONOCULAR_VIEW)
				return image;
			if (viewStyle == View3D.RED_GREEN_STEREO_VIEW)
				return stereoComposite.getImage();
		}
		int type;
		if (viewStyle == View3D.RED_GREEN_STEREO_VIEW)
			type = BufferedImage.TYPE_INT_RGB;
		else
			type = image.getType();
		BufferedImage copy = new BufferedImage(currentWidth, currentHeight, type);
		Graphics2D g = copy.createGraphics();
		draw(g);
		g.dispose();
		return copy;
	}
	
	public void drawPixel(Vector3D v) {
		Point2D pt = new Point2D.Double();
		if (clip(v))
			return;
		int rgb = viewStyle == View3D.RED_GREEN_STEREO_VIEW? 0xFFFFFF : currentColor.getRGB();
		if (viewStyle == View3D.MONOCULAR_VIEW) {
			transform3D.objectToXYWindowCoords(v,pt);
			transform3D.windowToViewport(pt);
			try {
				image.setRGB((int)pt.getX(),(int)pt.getY(),rgb);
			}
			catch (Exception e) {
			}
		}
		else {
			setUpForLeftEye();
			transform3D.objectToXYWindowCoords(v,pt);
			transform3D.windowToViewport(pt);
			try {
				image.setRGB((int)pt.getX(),(int)pt.getY(),rgb);
			}
			catch (Exception e) {
			}
			setUpForRightEye();
			transform3D.objectToXYWindowCoords(v,pt);
			transform3D.windowToViewport(pt);
			try {
				image2.setRGB((int)pt.getX(),(int)pt.getY(),rgb);
			}
			catch (Exception e) {
			}
			finishStereoView();
		}
	}

	public void drawDot(Vector3D pt, double diameter) {
		double h = diameter*transform3D.getPixelWidth();
		double w = diameter*transform3D.getPixelHeight();
		Point2D pt2D = new Point2D.Double();
		if (viewStyle == View3D.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();
		}
	}

	public void drawPixels(Vector3D[] vlist) {
		Point2D pt = new Point2D.Double();
		int rgb = viewStyle == View3D.RED_GREEN_STEREO_VIEW? 0xFFFFFF : currentColor.getRGB();
		if (viewStyle == View3D.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);
					try {
						image.setRGB((int)pt.getX(),(int)pt.getY(),rgb);
					}
					catch (Exception e) {
					}
				}
		}
		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);
					try {
						image.setRGB((int)pt.getX(),(int)pt.getY(),rgb);
					}
					catch (Exception e) {
					}
				}
			setUpForRightEye();
			for (int i = 0; i < vlist.length; i++)
				if (vlist[i] != null && !clip(vlist[i])) {
					transform3D.objectToXYWindowCoords(vlist[i],pt);
					transform3D.windowToViewport(pt);
					try {
						image2.setRGB((int)pt.getX(),(int)pt.getY(),rgb);
					}
					catch (Exception e) {
					}
				}
			finishStereoView();
		}
	}

	public void drawLine(Vector3D v1, Vector3D v2) {
		Point2D p1, p2;
		if (clip(v1) || clip(v2))
			return;
		if (viewStyle == View3D.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();
		}
	}

	public void drawString(String str, Vector3D basepoint) {
		if (clip(basepoint))
			return;
		if (viewStyle == View3D.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();
		}
	}

	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 == View3D.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,0,projectedPoints.length-1);
		}
		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,0,projectedPoints.length-1);
			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,0,projectedPoints.length-1);
			finishStereoView();
		}
	}

	public void drawCollaredCurve(Vector3D[] points, int startIndex,
			int endIndex, boolean reversed) {
		if (points == null)
			return;
		if (!reversed && viewStyle != View3D.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 == View3D.MONOCULAR_VIEW)
			drawCollaredCurveDirect(points,startIndex,endIndex,reversed);
		else {
			setUpForLeftEye();
			drawCollaredCurveDirect(points,startIndex,endIndex,reversed);
			setUpForRightEye();
			drawCollaredCurveDirect(points,startIndex,endIndex,reversed);
			finishStereoView();
		}
	}

	public void drawWireframeSurface(Grid3D surfaceData) {
		if (viewStyle == View3D.MONOCULAR_VIEW) {
			surfaceData.applyTransform(transform3D, view3D);
			surfaceData.drawCurves(view3D, currentGraphics);
		}
		else {
			setUpForLeftEye();
			surfaceData.applyTransform(transform3D, view3D);
			surfaceData.drawCurves(view3D, currentGraphics);
			setUpForRightEye();
			surfaceData.applyTransform(transform3D, view3D);
			surfaceData.drawCurves(view3D, currentGraphics);
			finishStereoView();
		}
	}
	
	public void drawSurface(Grid3D surfaceData, double startPercent, double endPercent) {
		if (view3DLit == null || 
				((View3DLit)view3D).getRenderingStyle() == View3DLit.WIREFRAME_RENDERING || 
				(view3D.getFastDrawing() && !view3DLit.getDragAsSurface())) {
			drawWireframeSurface(surfaceData);
		}
		else if (((View3DLit)view3D).getRenderingStyle() == View3DLit.PATCH_RENDERING) {
			if (view3D.getViewStyle() == View3D.MONOCULAR_VIEW) {
				if (view3D.getAntialiased())
					currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
				surfaceData.applyTransform(transform3D, view3D);
				if (view3D.getFastDrawing())
					surfaceData.drawMajorPatches(view3DLit, currentGraphics);
				else
					surfaceData.drawSubPatches(view3DLit, currentGraphics, startPercent, endPercent);
				if (view3D.getAntialiased())
					currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
			}
			else {
				setUpForLeftEye();
				if (view3D.getAntialiased())
					currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
				surfaceData.applyTransform(transform3D, view3D);
				if (view3D.getFastDrawing())
					surfaceData.drawMajorPatches(view3DLit, currentGraphics);
				else
					surfaceData.drawSubPatches(view3DLit, currentGraphics, startPercent, endPercent);
				if (view3D.getAntialiased())
					currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
				setUpForRightEye();
				if (view3D.getAntialiased())
					currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
				surfaceData.applyTransform(transform3D, view3D);
				if (view3D.getFastDrawing())
					surfaceData.drawMajorPatches(view3DLit, currentGraphics);
				else
					surfaceData.drawSubPatches(view3DLit, currentGraphics, startPercent, endPercent);
				if (view3D.getAntialiased())
					currentGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
				finishStereoView();
			}
		}
	}
	
	public void fillPolygon(Vector3D[] vertices, Color outlineColor) {
		if (view3D.getViewStyle() == View3D.MONOCULAR_VIEW) {
			Color saveColor = currentGraphics.getColor();
			GeneralPath path = new GeneralPath();
			Point2D pt = transform3D.objectToDrawingCoords(vertices[0]);
			path.moveTo( (float)pt.getX(), (float)pt.getY() );
			for (int i = 1; i < vertices.length; i++) {
				pt = transform3D.objectToDrawingCoords(vertices[i]);
				path.lineTo( (float)pt.getX(), (float)pt.getY() );
			}
			path.closePath();
			currentGraphics.fill(path);
			if (outlineColor != null)
				currentGraphics.setColor(outlineColor);
			currentGraphics.draw(path);
			currentGraphics.setColor(saveColor);
		}
		else {
			setUpForLeftEye();
			Color saveColor = currentGraphics.getColor();
			GeneralPath path = new GeneralPath();
			Point2D pt = transform3D.objectToDrawingCoords(vertices[0]);
			path.moveTo( (float)pt.getX(), (float)pt.getY() );
			for (int i = 1; i < vertices.length; i++) {
				pt = transform3D.objectToDrawingCoords(vertices[i]);
				path.lineTo( (float)pt.getX(), (float)pt.getY() );
			}
			path.closePath();
			currentGraphics.fill(path);
			if (outlineColor != null)
				currentGraphics.setColor(outlineColor);
			currentGraphics.draw(path);
			currentGraphics.setColor(saveColor);
			setUpForRightEye();
			saveColor = currentGraphics.getColor();
			path.reset();
			pt = transform3D.objectToDrawingCoords(vertices[0]);
			path.moveTo( (float)pt.getX(), (float)pt.getY() );
			for (int i = 1; i < vertices.length; i++) {
				pt = transform3D.objectToDrawingCoords(vertices[i]);
				path.lineTo( (float)pt.getX(), (float)pt.getY() );
			}
			path.closePath();
			currentGraphics.fill(path);
			if (outlineColor != null)
				currentGraphics.setColor(outlineColor);
			currentGraphics.draw(path);
			currentGraphics.setColor(saveColor);
			finishStereoView();
		}
	}

	// ------------------------- methods to support stereo rendering
	
	/**
	 * To be used with the <code>setUpForLeftEye</code> and <code>finishStereoView</code> 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 <code>setUpForLeftEye</code> 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 <code>setUpForRightEye</code>, followed by a repeat of the code for drawing the object.
	 * Finally, calling <code>finshStereoView</code> 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 == View3D.CROSS_EYE_STEREO_VIEW)
			separationFactor = 0.04;
		else if (viewStyle == View3D.STEREOGRAPH_VIEW)
			separationFactor = -0.03;
		else
			separationFactor = 0.03;
		separationFactor *= eyeSeparationMultiplier;
		transform3D.selectLeftEye(separationFactor);
		saveGraphicsDuringStereo = currentGraphics;  
		saveUntransformedGraphicsDuringStereo = transform3D.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 <code>finishStereoView</code> to support
	 * 3D rendering.
	 */
	protected void setUpForRightEye() {
		double separationFactor;
		if (viewStyle == View3D.CROSS_EYE_STEREO_VIEW)
			separationFactor = 0.04;
		else if (viewStyle == View3D.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;
	}
	
	// -----------  Helper stuff for 3D rendering -----------------------------

	private class CurveSegment extends Line2D.Double implements Comparable<CurveSegment> {
		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 (!transform3D.appliedTransform2D()) {
				transform3D.windowToViewport(p1);
				transform3D.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<CurveSegment> segments = new ArrayList<CurveSegment>();
		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(transform3D.getDefaultStrokeSize() * 5, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
		Stroke normalStroke = new BasicStroke(transform3D.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);
	}
		
	/**
	 * 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 vmm.polyhedron.IFS}. 
	 */
	final public boolean clip(Vector3D objectPoint) {
		return objectPoint.dot(viewDirection) > clipZ;
	}



}
