Polygon Editor

From JReality Wiki
Jump to: navigation, search

Source file: SubdividerDemo


Run as Java webstart


In this tutorial we will show how to create an interactive editor for subdivided polygons with jReality, as in the figure below:


PolygonEditor.png


Overview

Our implementation will consist of the three classes:

  • PointSequence: An interface for polygons.
  • DragPointSet: A sequence of draggable points as control points.
  • SubdividedPolygon: A sequence of points obtained by subdivision of the control points.
  • PointSequenceView: A class that displays a PointSequence as a jreality line set.


When the user drags a control point, the <code>SubdividedPolygon
needs to be updated, and after that the PointSequenceView needs also be updated. This is done by ChangeListener: The SubdividedPolygon registers at the DragPointSet to get notified about changes, and similarly the PointSequenceView registers at the SubdividedPolygon.


First, the interface for all the polygons:


PointSequence interface

public interface PointSequence {
	double[][] getPoints();
	boolean isClosed();
	void addChangeListener(ChangeListener cl);
}


By this interface, we can use the subdivided polygon easily as input for other applications, as for instance in the tutorial on Darboux transform.

DragPointSet implementation

Here the implementation of the sequence of dragable control points:


package de.jreality.tutorial.util.polygon;
 
import java.awt.Color;
import java.util.LinkedList;
import java.util.List;
 
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
 
import de.jreality.geometry.PointSetFactory;
import de.jreality.scene.Appearance;
import de.jreality.scene.SceneGraphComponent;
import de.jreality.shader.DefaultGeometryShader;
import de.jreality.shader.DefaultPointShader;
import de.jreality.shader.ShaderUtility;
import de.jreality.tools.DragEventTool;
import de.jreality.tools.PointDragEvent;
import de.jreality.tools.PointDragListener;
 
/**
 * A sequence of points that can be dragged. Reports any changes of the point positions to
 * {@link ChangeListener}s that can be attached via {@link #addChangeListener(ChangeListener)}.
 * 
 * The points are stored in an array of double-arrays with length 3.
 * 
 * To use, create an instance and attach it's base ({@link #getBase()} into the scene. Attach
 * a ChangeListener to handle dragging.
 * 
 * @author Steffen Weissmann.
 *
 */
public class DragPointSet implements PointSequence {
 
	private SceneGraphComponent base = new SceneGraphComponent("DragPointSet");
	private PointSetFactory psf = new PointSetFactory();
	
	/**
	 * A tool that reports on dragging. We will attach a PointDragEventListener to it.
	 */
	private DragEventTool dragTool = new DragEventTool();
	
	double[][] vertices;
	
	private boolean closed=true;
	
	/**
	 * @param initialVertices the initial points.
	 */
	public DragPointSet(double[][] initialVertices) {
		base.setGeometry(psf.getGeometry());
		base.addTool(dragTool);
		
		// Here we attach a listener, then we get notified when the
		// user drags a vertex. Then we will call our pointDragged-Method below.
		dragTool.addPointDragListener(new PointDragListener() {
			public void pointDragEnd(PointDragEvent e) {
			}
			public void pointDragStart(PointDragEvent e) {
			}
			public void pointDragged(PointDragEvent e) {
				DragPointSet.this.pointDragged(e);
			}
		});
		initPoints(initialVertices);
		
		// do some initial appearance settings:
		base.setAppearance(new Appearance());
		DefaultGeometryShader dps = (DefaultGeometryShader)
			ShaderUtility.createDefaultGeometryShader(
					base.getAppearance(), false
			);
		DefaultPointShader ps = (DefaultPointShader) dps.getPointShader();
		ps.setPointRadius(0.05);
		ps.setDiffuseColor(Color.yellow);
	}
 
	private void initPoints(double[][] initialVertices) {
		vertices = new double[initialVertices.length][];
		for (int i=0; i<initialVertices.length; i++) {
			vertices[i]=initialVertices[i].clone();
		}
		psf.setVertexCount(initialVertices.length);
		psf.setVertexCoordinates(vertices);
		psf.update();
	}
	
	public void pointDragged(PointDragEvent e) {
		vertices[e.getIndex()][0]=e.getX();
		vertices[e.getIndex()][1]=e.getY();
		vertices[e.getIndex()][2]=e.getZ();
		transform(vertices[e.getIndex()]);
		psf.setVertexCoordinates(vertices);
		psf.update();
		fireChange();
	}
	
	
	/**Override this method and do a transformation of the drag vertex, 
	 * e.g., projection to the sphere.
	 * 
	 * @param vertex
	 */
	public void transform(double[] vertex) {	
	}
	
	/**
	 * Returns the {@link SceneGraphComponent} containing the
	 * point geometry as well as the tool to drag the points.
	 * @return the base component
	 */
	public SceneGraphComponent getBase() {
		return base;
	}
	
	/**
	 * Returns the points as an array of 3-vectors (double arrays with length 3).
	 * @return the points
	 */
	public double[][] getPoints() {
		return vertices;
	}
 
	/**
	 * true if the point sequence is periodic, false if not. This is for instance
	 * used when rendering the point sequence as a line.
	 */
	public boolean isClosed() {
		return closed;
	}
 
	/**
	 * set if the point sequence should be interpreted as periodic.
	 * @param closed
	 */
	public void setClosed(boolean closed) {
		if (closed == this.closed) return;
		this.closed = closed;
		fireChange();
	}
	
	/******** listener code *********/
	
	private List<ChangeListener> listeners = new LinkedList<ChangeListener>();
 
	private void fireChange() {
		final ChangeEvent ev = new ChangeEvent(this);
		synchronized (listeners) {
			for (ChangeListener cl : listeners) cl.stateChanged(ev);
		}
	}
 
	/**
	 * @param A listener that gets notified whenever the points change.
	 */
	public void addChangeListener(ChangeListener cl) {
		synchronized (listeners) {
			listeners.add(cl);
		}
	}
	
	public void removeChangeListener(ChangeListener cl) {
		synchronized (listeners) {
			listeners.remove(cl);
		}
	}
	
 
}

SubdividedPolygon implementation

Now we implement again a PointSequence that subdivides the sequence of draggable points:


import java.util.LinkedList;
import java.util.List;
 
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
 
import de.jreality.math.Rn;
 
/**
 * A subdivider that uses 4-point subdivision. For closed or open point sequences.
 * It will be updated whenever the control point sequence changes.
 * 
 * @author Steffen Weissmann
 */
public class SubdividedPolygon implements ChangeListener, PointSequence {
 
	int subdivisionSteps = 2;
	
	// the PointSequence we will subdivide...
	private PointSequence controlPoints;
	
	// the resulting points
	private double[][] pts;
	
	/**
	 * Create a SubdividedPolygon for a set of control points.
	 * @param controlPoints the control point sequence used for subdivision.
	 */
	public SubdividedPolygon(PointSequence controlPoints) {
		this.controlPoints=controlPoints;
		// we attach this class as a change listener to the control points.
		controlPoints.addChangeListener(this);
		update();
	}
 
	/**
	 * Set the number of subdivision steps.
	 * @param n number of steps
	 */
	public void setSubdivisionLevel(int n) {
		this.subdivisionSteps=n;
		update();
	}
 
	// compute the subdivided points
	private double[][] computeSpline() {
		double[][] cur = controlPoints.getPoints();
		for (int i=0; i<subdivisionSteps; i++) {
			int n = isClosed() ? cur.length : cur.length-1;
			double[][] sub = new double[cur.length+n][];
			for (int j=0; j<n; j++) {
				sub[2*j] = cur[j];
				sub[2*j+1] = subdivide(
						point(cur, j-1),
						point(cur, j),
						point(cur, j+1),
						point(cur, j+2));
			}
			if (!isClosed()) {
				sub[2*n]=cur[n];
			}
			cur = sub;
		}
		return cur;
	}
	
	private double[] point(double[][] pts, int j) {
		int n=pts.length;
		if (j>=0 && j<n) return pts[j];
		if (controlPoints.isClosed()) return pts[(j+n)%n];
		double[] p0=null, p1=null;
		if (j==-1) {
			p0 = pts[0];
			p1 = pts[1];
		}
		if (j==n) {
			p1 = pts[n-2];
			p0 = pts[n-1];
		}
		double[] ret = Rn.linearCombination(null, 2, p0, -1, p1);
		return ret;
	}
 
	private static double[] subdivide(double[] v1, double[] v2, double[] v3, double[] v4) {
		double[] ret = new double[3];
    	for (int j=0; j<3; j++) ret[j] = (9.0*(v2[j]+v3[j])-v1[j]-v4[j])/16.0;
    	return ret;
	}
 
	// recompute subdivision
	private void update() {
		pts = computeSpline();
		fireChange();
	}
	
	/**
	 * returns the subdivided point sequence.
	 */
	public double[][] getPoints() {
		return pts;
	}
 
	/**
	 * this is called from the control point sequence.
	 */
	public void stateChanged(ChangeEvent e) {
		update();
	}
 
	public boolean isClosed() {
		return controlPoints.isClosed();
	}
	
	/******** listener code ********/
	
	private List<ChangeListener> listeners = new LinkedList<ChangeListener>();
	
	private void fireChange() {
		final ChangeEvent ev = new ChangeEvent(this);
		synchronized (listeners ) {
			for (ChangeListener cl : listeners) cl.stateChanged(ev);
		}
	}
 
	public void addChangeListener(ChangeListener cl) {
		synchronized (listeners) {
			listeners.add(cl);
		}
	}
	
	public void removeChangeListener(ChangeListener cl) {
		synchronized (listeners) {
			listeners.remove(cl);
		}
	}
	
}


PointSequenceView implementation

Now we create a general class that renders a PointSequence. We will use this to display the subdivided Polygon.


import java.awt.Color;
 
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
 
import de.jreality.geometry.IndexedLineSetFactory;
import de.jreality.scene.Appearance;
import de.jreality.scene.SceneGraphComponent;
import de.jreality.shader.DefaultGeometryShader;
import de.jreality.shader.DefaultLineShader;
import de.jreality.shader.DefaultPointShader;
import de.jreality.shader.ShaderUtility;
 
/**
 * A geometry representation for a {@link PointSequence}. Will update to any changes of
 * the sequence.
 * 
 * @author Steffen Weissmann
 */
public class PointSequenceView implements ChangeListener {
 
	// the component containing the polygon geometry
	private SceneGraphComponent base = new SceneGraphComponent("subdivided poly");
	private IndexedLineSetFactory lsf = new IndexedLineSetFactory();
	
	// the point sequence to render as a polygon
	private PointSequence sequence;
	
	// Shaders to set appearance attributes
	private DefaultPointShader ps;
	private DefaultLineShader ls;
	
	/**
	 * Constructs a geometry for a point sequence.
	 * 
	 * @param sequence the sequence to visualize.
	 */
	public PointSequenceView(PointSequence sequence) {
		this.sequence = sequence;
		sequence.addChangeListener(this);
		base.setGeometry(lsf.getGeometry());
		base.setAppearance(new Appearance());
		
		DefaultGeometryShader dps = (DefaultGeometryShader)
			ShaderUtility.createDefaultGeometryShader(base.getAppearance(), false);
		ps = (DefaultPointShader) dps.getPointShader();
		ls = (DefaultLineShader) dps.getLineShader();
		
		setPointRadius(0.04);
		setLineRadius(0.02);
		setLineColor(Color.orange);
		setPointColor(Color.green);
		
		update();
	}
 
	public void setPointRadius(double r) {
		ps.setPointRadius(r);
	}
 
	public void setLineRadius(double r) {
		ls.setTubeRadius(r);
	}
	
	public void setPointColor(Color c) {
		ps.setDiffuseColor(c);
	}
	
	public void setLineColor(Color c) {
		ls.setDiffuseColor(c);
	}
	
	// update the geometry
	private void update() {
		double[][] pts = sequence.getPoints();
		int n = pts.length;
		if (n != lsf.getVertexCount()) {
			int [][] inds = new int[n][2];
			for (int i=0, m=sequence.isClosed() ? n : n-1; i<m; i++) {
				inds[i][0]=i;
				inds[i][1]=(i+1)%n;
			}
			lsf.setVertexCount(n);
			lsf.setEdgeCount(inds.length);
			lsf.setEdgeIndices(inds);
		}
		lsf.setVertexCoordinates(pts);
		lsf.update();
	}
	
	public void stateChanged(ChangeEvent e) {
		update();
	}
 
	public SceneGraphComponent getBase() {
		return base;
	}
	
}


The demo application

Now we will put the pieces together for the simple editor shown at the top:


import de.jreality.plugin.JRViewer;
import de.jreality.scene.SceneGraphComponent;
 
public class SubdividerDemo {
 
	/**
	 * Create a sequence of points, on a circle in the xy-plane.
	 * @param n number of points
	 * @param r the radius
	 * @return the points
	 */
	public static double[][] circle(int n, double r) {
		double[][] verts = new double[n][3];
		double dphi = 2.0*Math.PI/n;
		for (int i=0; i<n; i++) {
			verts[i][0]=r*Math.cos(i*dphi);
			verts[i][1]=r*Math.sin(i*dphi);
		}
		return verts;
	}
	
	public static void main(String[] args) {
		// create control points, subdivider and the view:
		DragPointSet dps = new DragPointSet(circle(5,1));
		//dps.setClosed(false);
		SubdividedPolygon sub = new SubdividedPolygon(dps);
		PointSequenceView subView = new PointSequenceView(sub);
		
		// attach the corresponding components to a common base component:
		SceneGraphComponent root = new SceneGraphComponent();
		root.addChild(dps.getBase());
		root.addChild(subView.getBase());
		
		// display:
		JRViewer.display(root);
	}
	
}