import java.awt.*;
import java.applet.*;
import java.io.*;
import java.awt.event.*;

interface DataListener {
	public void dataChanged(boolean satisfied);
}

public class Applet1 extends Applet
{
	StrutGraph strutgraph = null;
	StrutViewer strutviewer = null;

	// entry point for frame version
	public static void main(String args[]) {
		Applet ap = new Applet1();
		ap.init();
		ap.start();
	}
	
	// entry point for applet version
	public void init() {
		strutgraph = new StrutGraph(StephenNodeDat, StephenStrutDat, null);
		//strutgraph = new StrutGraph("blob.txt", null);
		strutviewer = new StrutViewer(strutgraph);
		strutgraph.setWatcher(strutviewer);
		
		// For Frame version, (search for "FRAMEVER" for places to change)
		// 1) make class DoubleBufferedFrame below
		// extend the Frame class instead of Panel,
		// 2) comment out these lines:
		// FRAMEVER
		setLayout(new BorderLayout());   
		strutviewer.setLayout(new BorderLayout());
		add("Center", strutviewer);
	
		// FRAMEVER And 3) uncomment this one:
		//strutviewer.show();
		
		strutgraph.start();
	}
	
	public void start() {
		//strutviewer.pack();
		//strutviewer.show();
		//strutgraph.start();
	}
	
	public void stop() {
		strutgraph.stop();
	}	

	public void destroy() {
		strutgraph.stop();
		strutgraph.setWatcher(null);
		strutgraph  = null;
		//strutviewer.dispose();
		strutviewer = null;
	}
	
	/**
	 * Some hardcoded data for use in applet
	 */
	static final double StephenNodeDat[][] = {
		{-8.5, 0, 0}, // 0
		{8.5, 0, 0}, // 1
		{0, -0.66612949304613, 8.4443040860981}, // 2
		{0, -8.4443040860981, 0.66612949304613}, // 3
		{-4.7963116268700, -9.2766868401806, -0.47515650470272}, // 4
		{ 4.7963116268700,  0.47515650470272, 9.2766868401806}, // 5
		{ 0.83494337199663, -0.71106403461286, 3.5147148078182}, // 6
		{-0.83494337199663, -3.5147148078182, 0.71106403461286}, // 7
		{0, -10.460722695081, 10.46072269508}, // 8
	};
	static final int StephenStrutDat[][] = {
		{0, 1, 17},
		{0, 2, 12},
		{1, 2, 12},
		{0, 3, 12},
		{1, 3, 12},
		{0, 4, 10},
		{1, 5, 10},
		{0, 6, 10},
		{1, 7, 10},
		{2, 5, 5},   {5, 2, 5},
		{3, 4, 5},   {4, 3, 5},
		{2, 6, 5},   {6, 2, 5},
		{3, 7, 5},   {7, 3, 5},
		{4, 6, 11},  {6, 4, 11},
		{5, 7, 11},  {7, 5, 11},
		{2, 8, 10},  {8, 2, 10},
		{3, 8, 10},  {8, 3, 10},
		{4, 8, 12},  {8, 4, 12},
		{5, 8, 12},  {8, 5, 12},
		{6, 8, 12},  {8, 6, 12},
		{7, 8, 12},  {8, 7, 12},
	};
}


class Node {
    public double pos[] = new double[3];
    public double newpos[]  = new double[3]; /* temporary for relax() */
    public double scspos[]  = new double[3]; /* temporary for graphics */
    public Strut struts_ending_here;
	public Node(double dat[]) { Vec.SET3(pos, dat); }
}

class Strut {
    public double desiredlength;
    public Node from, to; /* force acts on "to" only */
    public Strut rest; /* rest of struts beginning at from */
	public Strut(double deslen, Node start, Node end) {
		desiredlength = deslen;
		from = start;
		to = end;
		rest = to.struts_ending_here;
	}

	public boolean isSatisfied() {
		double length = Math.sqrt(Vec.DISTSQRD3(from.pos, to.pos));
		double diff = Math.abs(length - desiredlength);
		return diff < Vec.EPS;
	}
}

class StrutGraph extends Thread {
	public int nstruts = 0;
	public Strut[] struts = null;
	public Node[] nodes = null;
	private static double desired_position[] = new double[3];
	DataListener watcher = null;
	
	public void setWatcher(DataListener dl) {
		watcher = dl;
	}
	
	public void run() {
		// so that graphics can have priority over computation:
		Thread.currentThread().setPriority(Thread.currentThread().getPriority() - 2);
		
		// main computational loop
		while(true) {
			relax();
			boolean satisfied = isSatisfied();
			if(watcher != null)
				watcher.dataChanged(satisfied);
			if(satisfied)
				try { sleep(250); } catch (java.lang.InterruptedException e) {}
			//System.out.println("relaxed");
		}
	}
	
	public StrutGraph(double nodedat[][], int strutdat[][], DataListener dl) {
		for(int i=0; i<nodedat.length; i++) { // just to center the stephen model - needs to be generalized
			nodedat[i][1] += 4;
			nodedat[i][2] -= 4;
		}
		watcher = dl;
		nstruts = strutdat.length;
		nodes = new Node[nodedat.length];
		struts = new Strut[nstruts];
		for(int i=0; i<nodedat.length; i++)
			nodes[i] = new Node(nodedat[i]);
		for(int i=0; i<nstruts; i++) {
			int from = strutdat[i][0];
			int to   = strutdat[i][1];
			double deslen = strutdat[i][2];
			struts[i] = new Strut(deslen, nodes[from], nodes[to]);
			nodes[to].struts_ending_here = struts[i];
		}
	}
		
	public StrutGraph(String fname, DataListener dl) {
		watcher = dl;
		int i;
		FileReader fr;
		try {
			fr = new FileReader(fname);
		}
		catch(FileNotFoundException fnfe) {
			return;
		}
		StreamTokenizer st = new StreamTokenizer(fr);
		st.parseNumbers();
		st.slashSlashComments(true);
		st.slashStarComments(true);
		
		// Read in the nodes initial postitions and strut lengths...
		int nnodes;
		if( ! nextNumber(st)) {
			System.out.println("couldn't read number of nodes");
			return;
		}
		nnodes = new Double(st.nval).intValue();
		
		nodes = new Node[nnodes];
		if(nodes == null) {
			System.out.println("couldn't allocate nodes");
			return;
		}
		for(i=0; i<nnodes; i++) {
			double doublebuf[] = new double[3];
			for(int j=0; j<3; j++) {
				if( ! nextNumber(st)) {
					System.out.println("couldn't read node " + i);
					return;
				}
				doublebuf[j] = st.nval;
			}
			nodes[i] = new Node(doublebuf);
			nodes[i].struts_ending_here = null;
		}

		int maxstruts = 2*nnodes*nnodes;
		struts = new Strut[maxstruts];
		nstruts = 0;
		while (true) {
			double doublebuf;
			int from, to;
			if( ! nextNumber(st)) {
				if(StreamTokenizer.TT_EOF == st.ttype)
					break;
				System.out.println("couldn't read strut " + nstruts);
				return;
			}
			from = new Double(st.nval).intValue();
			if( ! nextNumber(st)) {
				System.out.println("couldn't read strut " + nstruts);
				return;
			}
			to = new Double(st.nval).intValue();
			if( ! nextNumber(st)) {
				System.out.println("couldn't read strut " + nstruts);
				return;
			}

			doublebuf = st.nval;

			if(nstruts >= maxstruts) {
				System.out.println("too many struts");
				return;
			}
			struts[nstruts] = new Strut(doublebuf, nodes[from], nodes[to]);
			nodes[to].struts_ending_here = struts[nstruts];
			nstruts++;
		}
	}
	private boolean nextNumber(StreamTokenizer st) {
		try {
			if(StreamTokenizer.TT_NUMBER != st.nextToken())
				return false;
		}
		catch(IOException ioe) {
			return false;
		}
		return true;
	}

	public void relax() {
		int i;
		for(i=0; i<nodes.length; i++) {
			Node node = nodes[i];
			// Set newpos to be a weighted average of all the desired positions
			// together with the old position

			int n_averaged = 1;
			Vec.SET3(node.newpos, node.pos);

			Strut strut;
			for(strut=node.struts_ending_here; strut!=null; strut=strut.rest) {
				double length = Math.sqrt(Vec.DISTSQRD3(node.pos, strut.from.pos));
				double desired_length = strut.desiredlength;
				double t = (length == 0 ? 0 : desired_length/length);
				Vec.LERP3(desired_position, strut.from.pos, node.pos, t);
				Vec.VPV3(node.newpos, node.newpos, desired_position);
				n_averaged++;
			}
			Vec.VXS3(node.newpos, node.newpos, 1./n_averaged);
		}
		for(i=0; i<nodes.length; i++)
			Vec.SET3(nodes[i].pos, nodes[i].newpos);
	}


	public boolean isSatisfied() {
		boolean dissatisfied = false;
		for(int i=0; i<norms.length; i++) {
			double d = dissatisfaction(norms[i].normi);
			if (d != 0) 
				dissatisfied = true;
		}
		return !dissatisfied;
	}

	private double dissatisfaction(double normi)
	{
		int i;
		double sum = 0;
		for(i=0; i<nodes.length; i++) {
			Node node = nodes[i];
			for(Strut strut=node.struts_ending_here; strut!=null; strut=strut.rest) {
				double length = Math.sqrt(Vec.DISTSQRD3(node.pos, strut.from.pos));
				double desired_length = strut.desiredlength;
				double diff = Math.abs(length - desired_length);
				if (diff < Vec.EPS)
					diff = 0.;
				if (normi < 0) /* infinity norm-- just take the max */
					sum = Math.max(sum, diff);
				else if (normi == 0)
					sum += (diff == 0. ? 0. : 1.);
				else    /* zero or finite norm */
					sum += Math.pow(diff, normi);
			}
		}
		if (normi < 0) /* infinity norm-- return the max */
			return sum;
		sum *= .5;  /* counted everything twice */
		if (normi == 0 && Vec.EQ(sum, 0.))
			return 0.; /* zero norm-- don't trust pow to do the rightthing */
		return Math.pow(sum, 1./normi);
	}

	/*
	 * Various metrics for measuring dissatisfaction...
	 */
	static class Dissat {
		public Dissat(double n, String d) { normi = n; description = d; }
	    public double normi;
	    public String description;
	}  
	static final Dissat norms[] = {
		new Dissat(0, "0 (boolean)-norm"),
		new Dissat(1, "1 (sum)-norm"),
		new Dissat(2, "2 (euclidean)-norm"),
		new Dissat(3, "3-norm"),
		new Dissat(4, "4-norm"),
		new Dissat(5, "5-norm"),
		new Dissat(-1, "infinity (max)-norm"),
	};
}


class Vec {
	public static final double EPS = 1e-12;
	public static final double DISTSQRD2(double a[], double b[]) { return DISTSQRDn(a, b, 2); }
	public static final double DISTSQRD3(double a[], double b[]) { return DISTSQRDn(a, b, 3); }
	public static final double DISTSQRDn(double a[], double b[], int n) {
		double sum=0;
		for(int i=0; i<n; i++) {
			double diff = a[i] - b[i];
			sum += diff*diff;
		}
		return sum;
	}
	public static final void LERP3(double to[], double v0[], double v1[], double t) {
		for(int i=0; i<3; i++)
			to[i] = v0[i] + t*(v1[i]-v0[i]);
	}
	public static final void VMVn(double to[], double v0[], double v1[], int n) {
		for(int i=0; i<n; i++)
			to[i] = v0[i] - v1[i];
	}
	public static final void VMV2(double to[], double v0[], double v1[]) { VMVn(to, v0, v1, 2); }
	public static final void VMV3(double to[], double v0[], double v1[]) { VMVn(to, v0, v1, 3); }
	public static final void VPVn(double to[], double v0[], double v1[], int n) {
		for(int i=0; i<n; i++)
			to[i] = v0[i] + v1[i];
	}
	public static final void VPV2(double to[], double v0[], double v1[]) { VPVn(to, v0, v1, 2); }
	public static final void VPV3(double to[], double v0[], double v1[]) { VPVn(to, v0, v1, 3); }
	public static final void VXS2(double to[], double v0[], double s) { VXSn(to, v0, s, 2); }
	public static final void VXS3(double to[], double v0[], double s) { VXSn(to, v0, s, 3); }
	public static final void VXSn(double to[], double v0[], double s, int n) {
		for(int i=0; i<n; i++)
			to[i] = v0[i] * s;
	}
	public static final void XV2(double to[], double v0[]) {
		to[0] = -v0[1];
		to[1] =  v0[0];
	}
	public static final void SET3(double to[], double from[]) {
		for(int i=0; i<3; i++)
			to[i] = from[i];
	}
	public static final boolean LEQ(double a, double b) { return -EPS <= b - a; }
	public static final boolean GEQ(double a, double b) { return b - a <= EPS; }
	public static final boolean EQ(double a, double b) { return LEQ(a, b) && GEQ(a, b); }
	public static final boolean LT(double a, double b) { return !GEQ(a, b); }
	public static final boolean GT(double a, double b) { return !LEQ(a,b); }
	
	public static final double NORMSQRD2(double v0[]) { return NORMSQRDn(v0, 2); }
	public static final double NORMSQRD3(double v0[]) { return NORMSQRDn(v0, 3); }
	public static final double NORMSQRDn(double v0[], int n) {
		double sqlen = 0;
		for(int i=0; i<n; i++)
			sqlen += v0[i] * v0[i];
		return sqlen;
	}
	public static final void SETMAT2(double to[][], double from[][]) { SETMATn(to, from, 2); }
	public static final void SETMAT3(double to[][], double from[][]) { SETMATn(to, from, 3); }
	public static final void SETMATn(double to[][], double from[][], int n) {
		for(int i=0; i<n; i++)
			for(int j=0; j<n; j++)
				to[i][j] = from[i][j];
	}
	public static final void TRANSPOSE2(double to[][], double from[][]) { TRANSPOSEn(to, from, 2); }
	public static final void TRANSPOSE3(double to[][], double from[][]) { TRANSPOSEn(to, from, 3); }
	public static final void TRANSPOSEn(double to[][], double from[][], int n) {
		for(int i=0; i<n; i++)
			for(int j=0; j<n; j++)
				to[i][j] = from[j][i];
	}
	public static final void MXM2(double to[][], double a[][], double b[][]) { MXMn(to, a, b, 2); }
	public static final void MXM3(double to[][], double a[][], double b[][]) { MXMn(to, a, b, 3); }
	public static final void MXMn(double to[][], double a[][], double b[][], int n) {
		for(int i=0; i<n; i++) {
			for(int j=0; j<n; j++) {
				double sum = 0;
				for(int k=0; k<n; k++) 
					sum += a[i][k] * b[k][j];
				to[i][j] = sum;
			}
		}
	}
	public static final void MXV2(double to[], double m[][], double v[]) { MXVn(to, m, v, 2); }
	public static final void MXV3(double to[], double m[][], double v[]) { MXVn(to, m, v, 3); }
	public static final void MXVn(double to[], double m[][], double v[], int n) {
		for(int i=0; i<n; i++) {
			to[i] = 0;
			for(int j=0; j<n; j++)
				to[i] += m[i][j] * v[j];
		}
	}
	public static final void IDENTMAT2(double m[][]) { IDENTMATn(m, 2); }
	public static final void IDENTMAT3(double m[][]) { IDENTMATn(m, 3); }
	public static final void IDENTMATn(double m[][], int n) {
		for(int i=0; i<n; i++)
			for(int j=0; j<n; j++)
				m[i][j] = 0;
		for(int i=0; i<n; i++)
			m[i][i] = 1;
	}
	public static final void NORMALIZE2(double to[], double from[]) { NORMALIZEn(to, from, 2); }
	public static final void NORMALIZE3(double to[], double from[]) { NORMALIZEn(to, from, 3); }
	public static final void NORMALIZEn(double to[], double from[], int n) {
		VXSn(to, from, 1.0/NORMSQRDn(from, n), n);
	}
	public static final void MatFromQuat(double mat[][], double axis[], double rot)
	{
		// initialize quat from axis and rot
		double normvec[] = new double[3];
		double scaledvec[] = new double[3];
		NORMALIZE3(normvec, axis);
		double half_angle = rot * 0.5;
		double sin_half_angle = Math.sin(half_angle);
		double W = Math.cos(half_angle);
		VXS3(scaledvec, normvec, sin_half_angle);
		
		// calculate some temporaries
		double XX = scaledvec[0] + scaledvec[0];
		double YY = scaledvec[1] + scaledvec[1];
		double ZZ = scaledvec[2] + scaledvec[2];

		double WXX = W * XX;
		double XXX = scaledvec[0] * XX;

		double WYY = W * YY;
		double XYY = scaledvec[0] * YY;
		double YYY = scaledvec[1] * YY;

		double WZZ = W * ZZ;
		double XZZ = scaledvec[0] * ZZ;
		double YZZ = scaledvec[1] * ZZ;
		double ZZZ = scaledvec[2] * ZZ;

		// set the output matrix values
		mat[0][0] = 1.0 - (YYY + ZZZ);
		mat[0][1] = XYY - WZZ;
		mat[0][2] = XZZ + WYY;

		mat[1][0] = XYY + WZZ;
		mat[1][1] = 1.0 - (XXX + ZZZ);
		mat[1][2] = YZZ - WXX;

		mat[2][0] = XZZ - WYY;
		mat[2][1] = YZZ + WXX;
		mat[2][2] = 1.0 - (XXX + YYY);
	}

}



class StrutViewer extends DoubleBufferedFrame implements DataListener, MouseListener, MouseMotionListener, KeyListener {
	private StrutGraph mStrutGraph = null;
	private int mNStruts = 0;
	private Strut mStruts[] = null;
	private Node mNodes[] = null;
	private Transform mXfm = new Transform();
	private static double 
		start[] = new double[3],
		end[] = new double[3],
		tmp[] = new double[3];
	private static double mScale = 15;
	private int mPicked = -1; // id of currently selected node
	private Strut mPickedAffectors = null;
	private double mMaxDir = 1;

	private boolean mChanged = true;
	private boolean mSatisfied = false;
	
	// Key Listener functionality watches for the 's' key and
	// appends keyframe data using the current positions of
	// each node and appends them to the file "vertdat.txt"
	// This is to generate animation data for other applications.
	BufferedWriter outfile = null;
	public void keyPressed(KeyEvent e) {}
	public void keyReleased(KeyEvent e) {}
	public void keyTyped(KeyEvent e) {
		if(e.getKeyChar() != 's')
			return;
		try {
			if(outfile == null) { // first time
				outfile = new BufferedWriter(new FileWriter("vertdat.txt"));
				outfile.write("" + mNodes.length);
				outfile.newLine();
			}
			for(int i=0; i<mNodes.length; i++) {
				for(int j=0; j<3; j++)
					outfile.write("" + mNodes[i].pos[j] + " ");
				outfile.newLine();
			}
			outfile.flush();
		}
		catch (IOException ioe) { 
			return;
		}
	}
	
	StrutViewer(StrutGraph sg) {
		mStrutGraph = sg;
		mNStruts = sg.nstruts;
		mStruts = sg.struts;
		mNodes = sg.nodes;
		this.setSize(500, 500);
		addMouseListener(this);
		addMouseMotionListener(this);
		addKeyListener(this);
		mMaxDir = 0;
		for(int i=0; i<mNodes.length; i++)
			for(int j=0; j<3; j++)
				if(Math.abs(mNodes[i].pos[j]) > mMaxDir)
					mMaxDir = Math.abs(mNodes[i].pos[j]);
	}
	
	public void dataChanged(boolean satisfied) {
		if(mChanged)
			return; // already been notified, so don't stack up repaint requests
		mSatisfied = satisfied;
		mChanged = true;
		repaint();
	}
		
	public void paint(Graphics gr) {
		mChanged = false;
		mScale = mOffscreenSize.width / mMaxDir * 0.4;
		Graphics g = startPaint(gr);
		g.setColor(Color.white);
		g.fillRect(0, 0, mOffscreenSize.width, mOffscreenSize.height);
		g.setColor(Color.black);
		double cx = mOffscreenSize.width / 2.0;
		double cy = mOffscreenSize.height / 2.0;
		for(int i=0; i<mNodes.length; i++)
			LcsToScs(mNodes[i].pos, mNodes[i].scspos);
		for(int i=0; i<mNStruts; i++) {
			Strut curstrut = mStruts[i];
			int xs = (int)(curstrut.from.scspos[0] + .5);
			int ys = (int)(curstrut.from.scspos[1] + .5);
			int xe = (int)(curstrut.to.scspos[0] + .5);
			int ye = (int)(curstrut.to.scspos[1] + .5);
			if( ! mSatisfied)
				g.setColor(curstrut.isSatisfied() ? Color.green.darker() : Color.red);
			g.drawLine(xs, ys, xe, ye);
		}
		endPaint();
	}

	private double rotatedlcs[] = new double[3];
	private double transmat[][] = new double[3][3];
	private void ScsToLcs(double scs[/* 3 */], double lcs[/* 3 */]) {
		double cx = mOffscreenSize.width / 2.0;
		double cy = mOffscreenSize.height / 2.0;
		rotatedlcs[0] = (scs[0] - cx) / mScale;
		rotatedlcs[1] = (scs[1] - cy) / -mScale;
		rotatedlcs[2] = scs[2];
		mXfm.inverseXfm(lcs, rotatedlcs);
	}
	
	private double lcstoscstmp[] = new double[3];
	private void LcsToScs(double lcs[/* 3 */], double scs[/* 3 */]) {
		double cx = mOffscreenSize.width / 2.0;
		double cy = mOffscreenSize.height / 2.0;
		mXfm.Xfm(lcstoscstmp, lcs);
		scs[0] = mScale * lcstoscstmp[0] + cx;
		scs[1] = -mScale * lcstoscstmp[1] + cy;
		scs[2] = lcstoscstmp[2]; // storage for possible later inverse
	}
	
	public boolean pick(MouseEvent e) {
		mPicked = -1;
		double scs_pick[] = new double[2];	
		double cx = mOffscreenSize.width / 2.0;
		double cy = mOffscreenSize.height / 2.0;
		scs_pick[0] = e.getPoint().x;
		scs_pick[1] = e.getPoint().y;
		int closest_node_id = -1;
		double min_sqdist = 8888888.8;
		for(int i=0; i<mNodes.length; i++) {
			LcsToScs(mNodes[i].pos, tmp);
			double sqdist = Vec.DISTSQRD2(tmp, scs_pick);
			if(sqdist < min_sqdist) {
				min_sqdist = sqdist;
				closest_node_id = i;
			}
		}
		if(closest_node_id != -1) {
			double closest_dist = Math.sqrt(min_sqdist);
			if(closest_dist < 4) {
				mPicked = closest_node_id;
				return true;
			}
		}
		return false;
	}
	
	// mouse listener events
	public void mousePressed(MouseEvent e) { 
		if(pick(e)) {
			// temporarally disable other struts from moving this node
			// while the user is dragging it.
			mPickedAffectors = mNodes[mPicked].struts_ending_here;
			mNodes[mPicked].struts_ending_here = null;
		}
		else
			mXfm.startDrag(e.getPoint()); 
		e.consume();
	}
	public void mouseReleased(MouseEvent e) {
		if(mPicked != -1) {
			// user is done dragging this node, so let other struts
			// pull it around again.
			mNodes[mPicked].struts_ending_here = mPickedAffectors;
			mPickedAffectors = null;
		}
	}
	public void mouseEntered(MouseEvent e) {}
	public void mouseExited(MouseEvent e) {}
	public void mouseClicked(MouseEvent e) {}
		
	// mouse motion listener methods
	public void mouseDragged(MouseEvent e) { 
		if(mPicked != -1) {
			// dragging picked node
			double scsx = e.getPoint().x;
			double scsy = e.getPoint().y;
			double scspos[] = mNodes[mPicked].scspos;
			scspos[0] = scsx;
			scspos[1] = scsy;
			ScsToLcs(scspos, mNodes[mPicked].pos);
		}
		else
			mXfm.drag(e.getPoint());
		repaint();
		e.consume();
	}
	public void mouseMoved(MouseEvent e) {}
}

// FRAMEVER - For applications, make this class extend Frame instead of Panel:
class DoubleBufferedFrame extends Panel {
    private Image mActiveOffscreenImage = null;
    protected Dimension mOffscreenSize = new Dimension(-1,-1);
    private Graphics mActiveOffscreenGraphics = null;
	private Graphics mSystemGraphics = null;
	
	DoubleBufferedFrame() {
		this.addComponentListener(new ComponentAdapter() {
			public void componentResized(ComponentEvent e) { 
				repaint(); 
			}
		});
	}
	
 	/** 	 * NOTE: when extending applets:
	 * this override update to *not* erase the background before painting
	 */
	public void update(Graphics g) {
		paint(g);
	}	public Graphics startPaint (Graphics sysgraph) {
		mSystemGraphics = sysgraph;
        // Initialize if this is the first pass or the size has changed
        Dimension d = getSize();
        if ((mActiveOffscreenImage == null) ||
            (d.width != mOffscreenSize.width) ||
            (d.height != mOffscreenSize.height)) 
        {
            mActiveOffscreenImage = createImage(d.width, d.height);
            mActiveOffscreenGraphics = mActiveOffscreenImage.getGraphics();
            mOffscreenSize = d;
            mActiveOffscreenGraphics.setFont(getFont());
        }
		//mActiveOffscreenGraphics.clearRect(0, 0, mOffscreenSize.width, mOffscreenSize.height);
		return mActiveOffscreenGraphics;
    }
    
    public void endPaint () {
        // Start copying the offscreen image to this canvas
        // The application will begin drawing into the other one while this happens
        mSystemGraphics.drawImage(mActiveOffscreenImage, 0, 0, null);
	}

}

class Transform {
	private double mLastPoint[] = new double[2];
	private double mEnd[] = new double[2];
	private double mDragDir[] = new double[2];
	private double mAxis[] = new double[3];
	private double mMat[][] = new double[3][3];
	private double mInverseMat[][] = new double[3][3];
	private double mDeltaMat[][] = new double[3][3];
	private double mTmpMat[][] = new double[3][3];
	private boolean mInverseDirty = true;
	
	Transform() {
		Vec.IDENTMAT3(mMat);
	}
	
	public void Xfm(double result[], double src[]) {
		Vec.MXV3(result, mMat, src);
	}
	
	public void inverseXfm(double result[], double src[]) {
		if(mInverseDirty) {
			Vec.TRANSPOSE3(mInverseMat, mMat);
			mInverseDirty = false;
		}
		Vec.MXV3(result, mInverseMat, src);
	}
	
	public void startDrag(Point where) {
		mLastPoint[0] = where.x;
		mLastPoint[1] = where.y;
	}
	public void drag(Point where) {
		mEnd[0] = where.x; 
		mEnd[1] = where.y;
		Vec.VMV2(mDragDir, mLastPoint, mEnd);
		mDragDir[1] *= -1; // in Windows, Y is down, so invert it
		Vec.XV2(mAxis, mDragDir); // rotation axis at right angles to drag dir
		double len = Math.sqrt(Vec.NORMSQRD2(mAxis));
		if(len > .0001) { // do nothing if ended where we started
			Vec.VXS2(mAxis, mAxis, 1.0/len);
			mAxis[2] = 0;
			double rads = len / 200.0;
			Vec.MatFromQuat(mDeltaMat, mAxis, -rads);
			Vec.MXM3(mTmpMat, mDeltaMat, mMat);
			Vec.SETMAT3(mMat, mTmpMat);
		}
		mLastPoint[0] = mEnd[0];
		mLastPoint[1] = mEnd[1];
		mInverseDirty = true;
	}
}


