/* 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.polyhedron;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collections;
import vmm.actions.ActionList;
import vmm.actions.ToggleAction;
import vmm.core.Animation;
import vmm.core.I18n;
import vmm.core.TimerAnimation;
import vmm.core.VMMSave;
import vmm.core.View;
import vmm.core3D.Exhibit3D;
import vmm.core3D.PhongLighting;
import vmm.core3D.Transform3D;
import vmm.core3D.Vector3D;
import vmm.core3D.View3D;
import vmm.core3D.View3DLit;
/**
* Represents a polyhedron given in the form of a indexed face set. That is,
* the data for the polyhedron consists of a list of vertices plus a list of
* data for the faces of the polyhedron. A face is specified by a list of
* integers, one for each vertex of the face, where each integer is an index
* into the list of vertices. The vertices for a face must be listed in
* counterclockwise order as viewed from the front of face. No public
* method is provided for changing the indexed face set data after the
* object is constructed, but subclasses can do so using the protected
* method {@link #setIFSData(Vector3D[], int[][])}.
*
An IFS has a create animation that shows the faces of the polyhedron
* being drawn in back-to-front order using a simple painter's algorithm.
*/
public class IFS extends Exhibit3D {
/**
* When drawind the polyhedron in wireframe form, each edge is divided
* into this many sections. This prevents incorrect drawing order for
* the edges, which can occur (at least) in the {@link Rhombohedron}.
* The default value is 8; this seems to work in all cases, but the
* value can be changed in a subclass if necessary. Also note that
* dividing the edges into multiple segments in this way means that
* an edges can be partially clipped, since clipping is applied to
* segments rather than to the edge as a whole.
*/
protected int edgeDivisor = 8;
/**
* When this value is set to true, a modified version of the simple painter's
* algorithm is used. In the modified version, all back-facing faces are drawn
* before any front-facing face is drawn. This prevents incorrect draw order
* for faces that sometimes occurs (at least) for the {@link Rhombohedron}
* when the unmodified painter's algorithm is used. The default value is
* false, but it is set to true in the Rhombohedron class.
*/
protected boolean useBackFaceFudge = false;
private Vector3D[] vertices; // A list of all the vertices of the polyhedron.
private int[][] faces; // faces[i] is a list of vertices of the i-th face, given as indices into the vertex array.
private Vector3D[] unitNormals; // Unit normals for the faces; computed in setIFSData().
private ArrayList edges; // A list of edges in the polyhedron. Each entry is an array of two ints that
// give the indices of the endpoints of the edges in the vertex array.
// Although an edge can occur in two faces, the edge is only listed once in this list.
// This list is computed in setIFSData.
private ArrayList clippedFaces; // A list of just those faces that have not been clipped, in bact-to-front order.
private ArrayList clippedEdges; // A list of edge segments that have not been clipped, in back-to-front order;
// (Each edge in the polyhedron becomes several segments in this list.)
/**
* Create a polyhedron, using specifed lists of vertices and faces. The data is not verified.
* @param vertexList an array containing the vertices of the polyhedron
* @param faceData describes the faces of the polyhedron. faceData[i] is a list of vertices
* in the i-th face, listed in counterclockwise order as seen from in front of the face, with
* each vertex specified as an index into the vertex array.
*/
public IFS(Vector3D[] vertexList, int[][] faceData) {
setIFSData(vertexList,faceData);
}
/**
* This protected constructor creates an IFS object with no data for the polyhedron.
* The data must be provided by the subclass before the method
* {@link #computeDrawData3D(View3D, boolean, Transform3D, Transform3D)} is called.
*/
protected IFS() {
}
/**
* Change the data that describes the polyhedron. Note that this is used by
* {@link RegularPolyhedron} to show the various truncated forms of the polyhedron.
* Each truncation has its own truncation.
*/
protected void setIFSData(Vector3D[] vertices, int[][] faces) {
this.vertices = vertices;
this.faces = faces;
edges = new ArrayList();
for (int[] face : faces) {
makeseg: for (int i = 0; i < face.length; i++) {
int v1 = face[i];
int v2 = i == face.length-1 ? face[0] : face[i+1];
for (int[] edge : edges)
if (edge[0] == v1 && edge[1] == v2 || edge[0] == v2 && edge[1] == v1)
continue makeseg;
edges.add(new int[] {v1,v2});
}
}
unitNormals = new Vector3D[faces.length];
for (int i = 0; i < faces.length; i++) {
int[] vertexIndices = faces[i];
Vector3D normal = null;
for (int j = 0; j < vertexIndices.length; j++) {
Vector3D v1 = vertices[vertexIndices[j]];
Vector3D v2 = vertices[ vertexIndices[(j+1) % (vertexIndices.length)] ];
Vector3D v3 = vertices[ vertexIndices[(j+2) % (vertexIndices.length)] ];
normal = v3.minus(v2).cross(v1.minus(v2));
normal.normalize();
if (!Double.isNaN(normal.x))
break; // in case v1,v2,v3 are colinear vertices, go on to try the next set of vertices
}
if (normal == null)
normal = new Vector3D(0,0,1);
unitNormals[i] = normal;
}
clippedFaces = null;
clippedEdges = null;
forceRedraw();
}
protected void computeDrawData3D(View3D view, boolean exhibitNeedsRedraw, Transform3D previousTransform3D, Transform3D newTransform3D) {
if (clippedFaces == null || exhibitNeedsRedraw || ! newTransform3D.hasSameProjection(previousTransform3D))
clip(view);
}
protected void doDraw3D(Graphics2D g, View3D view, Transform3D transform) {
boolean wire = false;
if ( ! (view instanceof View3DLit) )
wire = true;
else if ( ((View3DLit)view).getRenderingStyle() == View3DLit.WIREFRAME_RENDERING)
wire = true;
else if ( ((View3DLit)view).getFastDrawing() && !((View3DLit)view).getDragAsSurface())
wire = true;
if (wire) {
boolean thickWireframe = true;
if (view instanceof IFSView)
thickWireframe = ((IFSView)view).getThickWireframe();
Color saveColor = g.getColor();
Stroke saveStroke = g.getStroke();
Stroke wideStroke = new BasicStroke(transform.getDefaultStrokeSize() * 5, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
Stroke normalStroke = new BasicStroke(transform.getDefaultStrokeSize(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
for (Segment seg : clippedEdges) {
if (seg.wd == true) {
g.setStroke(wideStroke);
g.setColor(thickWireframe ? saveColor : g.getBackground());
}
else {
g.setStroke(normalStroke);
g.setColor(thickWireframe ? g.getBackground() : saveColor);
}
view.drawLine(seg.v1,seg.v2);
}
g.setColor(saveColor);
g.setStroke(saveStroke);
}
else {
View3DLit vl = (View3DLit)view;
int sidesToDraw = -1;
if (view instanceof IFSView)
sidesToDraw = ((IFSView)view).sidesToDraw;
if (sidesToDraw < 0)
sidesToDraw = clippedFaces.size();
int opaqueness = (int)(255*(1-vl.getTransparency()));
for (int f = 0; f < sidesToDraw; f++) {
Face face = clippedFaces.get(f);
if (vl.getLightingEnabled()) {
Color c = PhongLighting.phongLightingColor(face.unitNormal, vl, vl.getTransform3D(),
face.centerPoint, Color.white);
if (opaqueness < 255)
c = new Color(c.getRed(),c.getGreen(),c.getBlue(),opaqueness);
view.setColor(c);
}
else
view.setColor(Color.WHITE);
Vector3D[] v = new Vector3D[face.vertexIndices.length];
for (int i = 0; i < face.vertexIndices.length; i++)
v[i] = vertices[face.vertexIndices[i]];
vl.fillPolygon(v, Color.BLACK);
}
}
}
/**
* Returns a View of type {@link IFS.IFSView}.
*/
public View getDefaultView() {
return new IFSView();
}
/**
* Returns an animation in which the faces of the polyhedron are drawn one at a time,
* in back-to-front order.
*/
public Animation getCreateAnimation(View view) {
if (! (view instanceof IFSView) )
return null;
final IFSView ifsView = (IFSView)view;
return new TimerAnimation(-1,200) {
protected void drawFrame() {
ifsView.sidesToDraw++;
if (clippedFaces != null && ifsView.sidesToDraw >= clippedFaces.size())
cancel();
else {
if (clippedFaces != null && getMillisecondsPerFrame() * clippedFaces.size() > 3000) {
setMillisecondsPerFrame(3000/clippedFaces.size());
}
fireExhibitChangeEvent();
}
}
protected void animationStarting() {
ifsView.sidesToDraw = 0;
fireExhibitChangeEvent();
}
protected void animationEnding() {
ifsView.sidesToDraw = -1;
fireExhibitChangeEvent();
}
};
}
/**
* Adds to the Actions a toggle to control whether thick lines are used for drawing
* the wireframe form of the polyhedron.
*/
public ActionList getActionsForView(View view) {
ActionList actions = super.getActionsForView(view);
if (view instanceof IFSView) {
actions.add(null);
actions.add(((IFSView)view).thickWireframeToggle);
}
return actions;
}
/**
* Creates the lists of faces and of edge segments that are used to draw the
* polyhedron.
*/
private void clip(View3D view) {
Transform3D transform = view.getTransform3D();
clippedFaces = new ArrayList(); // The list of faces that are not clipped from the given View.
makeface: for (int f = 0; f < faces.length; f++) {
int[] faceData = faces[f];
for (int i : faceData)
if (view.clip(vertices[i]))
continue makeface; // one of the vertices has been clipped; do not add this face to the list
Face face = new Face(faceData,unitNormals[f]);
clippedFaces.add(face);
}
for (Face face : clippedFaces)
face.computeZ(transform); // ( must be called befor sorting )
Collections.sort(clippedFaces); // sorts the list of faces into back-to-front order.
clippedEdges = new ArrayList(); // The list of edges that are not clipped from the given View.
for (int[] edge : edges) {
Vector3D v1 = vertices[edge[0]];
Vector3D v2 = vertices[edge[1]];
Vector3D dv = (v2.minus(v1).times(1.0/edgeDivisor));
Vector3D v = v1;
for (int i = 0; i < edgeDivisor; i++) { // the edge is divided into several segments of equal length
Vector3D w = (i == edgeDivisor-1)? v2 : v1.plus(dv.times(i));
if (! (view.clip(v) || view.clip(w)) ) {
clippedEdges.add(new Segment(v,w,true)); // add this segment only if both its endpoint are not clipped
clippedEdges.add(new Segment(v,w,false));
}
v = w;
}
}
for (Segment seg : clippedEdges)
seg.computeZ(transform); // ( must be called before sorting )
Collections.sort(clippedEdges); // sort the segments into back-to-front order
}
/**
* Holds the data for one face of the polyhedron, with the information needed
* to sort the list of faces into back to front order.
*
*/
private class Face implements Comparable{
int[] vertexIndices; // vertices of this face, as indices into the vertex array
Vector3D unitNormal; // used in coputing color of patch
Vector3D centerPoint; // used in computing color of patch
double centerZ; // only valid after computeZ has been called to apply a transform
boolean isBackFace; // This is used only if useBackFaceFudge is true; otherwise it is false.
Face(int[] indices, Vector3D unitNormal) {
vertexIndices = indices;
this.unitNormal = unitNormal;
centerPoint = vertices[vertexIndices[0]];
for (int i = 1; i < vertexIndices.length; i++)
centerPoint = centerPoint.plus(vertices[vertexIndices[i]]);
centerPoint = centerPoint.times(1.0 / vertexIndices.length);
}
void computeZ(Transform3D transform) {
centerZ = 0;
for (int index : vertexIndices)
centerZ += transform.objectToViewZ(vertices[index]);
centerZ /= vertexIndices.length;
if (useBackFaceFudge) {
if (transform.getOrthographicProjection())
isBackFace = unitNormal.dot(transform.getViewDirection()) <= 0;
else {
Vector3D patchCG = vertices[vertexIndices[0]];
for (int i = 1; i < vertexIndices.length; i++)
patchCG = patchCG.plus(vertices[vertexIndices[i]]);
patchCG = patchCG.times(1.0 / vertexIndices.length);
isBackFace = unitNormal.dot(patchCG.minus(transform.getViewPoint())) <= 0;
}
}
else {
isBackFace = false;
}
}
public int compareTo(Face face) { // Note that this only works if computeZ() has been called for all the faces in the list!
if (isBackFace == face.isBackFace) {
if (centerZ == face.centerZ)
return 0;
else if (centerZ < face.centerZ)
return -1;
else
return 1;
}
else if (isBackFace)
return -1; // when only one face is marked as a backface, it always comes before the front face
else
return 1;
}
}
/**
* Contains information about one of the segments that make up the edges of the
* polyhedron, with the information needed to sort the segments into back to
* front order.
*/
private class Segment implements Comparable {
Vector3D v1, v2; // endpoints of this segment
double centerZ; // only valid after computeZ has been called to apply a transform
boolean wd; // true for wide segments, false for narrow
Segment(Vector3D v1, Vector3D v2, boolean wd) {
this.v1 = v1;
this.v2 = v2;
this.wd = wd;
}
void computeZ(Transform3D transform) {
if (wd == true)
//centerZ = (transform.objectToViewZ(v1) + transform.objectToViewZ(v2)) / 2;
centerZ = 1024*Math.min(transform.objectToViewZ(v1),transform.objectToViewZ(v2));
else
centerZ = 1024*Math.max(transform.objectToViewZ(v1),transform.objectToViewZ(v2))+8;
}
public int compareTo(Segment seg) { // Note that this only works if computeZ() has been called for all the segments in the list!
if (centerZ == seg.centerZ)
return 0;
else if (centerZ < seg.centerZ)
return -1;
else
return 1;
}
}
public static class IFSView extends View3DLit {
private int sidesToDraw = -1;
@VMMSave private boolean thickWireframe = true;
public IFSView() {
setAntialiased(true);
setDragAsSurface(true);
}
protected ToggleAction thickWireframeToggle = new ToggleAction(I18n.tr("vmm.polyhedron.IFS.thickWireframe"),true) {
public void actionPerformed(ActionEvent evt) {
setThickWireframe(getState());
}
};
public boolean getThickWireframe() {
return thickWireframe;
}
public void setThickWireframe(boolean thickWireframe) {
if (this.thickWireframe == thickWireframe)
return;
this.thickWireframe = thickWireframe;
thickWireframeToggle.setState(thickWireframe);
forceRedraw();
}
}
}