/* Copyright (C) 2001-2004 by Peter Eastman

   This program is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 2 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful, but WITHOUT ANY
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
   PARTICULAR PURPOSE.  See the GNU General Public License for more details. */

package artofillusion.tools;

import artofillusion.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.object.TriangleMesh.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.util.*;

/** This dialog box allows the user to specify options for creating extruded objects. */

public class ExtrudeDialog extends BDialog
{
  LayoutWindow window;
  BComboBox objChoice, pathChoice;
  RadioButtonGroup pathGroup;
  BRadioButton pathBox, xBox, yBox, zBox, vectorBox;
  BCheckBox orientBox;
  ValueField distField, xField, yField, zField, segField, angleField, tolField;
  BButton okButton, cancelButton;
  ObjectPreviewCanvas preview;
  Vector objects, paths;

  private static int counter = 1;

  public ExtrudeDialog(LayoutWindow window)
  {
    super(window, "Extrude", true);
    this.window = window;
    Scene scene = window.getScene();
    int selection[] = scene.getSelection();

    // Identify the objects that can be extruded, and the paths along which they can be
    // extruded.

    objects = new Vector();
    paths = new Vector();
    for (int i = 0; i < selection.length; i++)
    {
      ObjectInfo obj = scene.getObject(selection[i]);
      if (obj.object instanceof Curve)
      {
        objects.addElement(obj);
        paths.addElement(obj);
      }
      else if ((obj.object instanceof TriangleMesh || 
          obj.object.canConvertToTriangleMesh() != Object3D.CANT_CONVERT) && 
          !obj.object.isClosed())
        objects.addElement(obj);
    }
    if (objects.size() == 1)
      paths.removeAllElements();

    // Layout the window.

    FormContainer content = new FormContainer(4, 10);
    setContent(BOutline.createEmptyBorder(content, ModellingApp.standardDialogInsets));
    content.setDefaultLayout(new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, new Insets(0, 0, 0, 5), null));
    content.add(new BLabel("Object to Extrude:"), 0, 0, 2, 1);
    content.add(objChoice = new BComboBox(), 0, 1, 2, 1);
    for (int i = 0; i < objects.size(); i++)
      objChoice.add(((ObjectInfo) objects.elementAt(i)).name);
    objChoice.addEventLink(ValueChangedEvent.class, this, "stateChanged");
    content.add(new BLabel("Extrude Direction:"), 0, 2, 2, 1);
    pathGroup = new RadioButtonGroup();
    content.add(xBox = new BRadioButton("X", true, pathGroup), 0, 3);
    content.add(yBox = new BRadioButton("Y", true, pathGroup), 0, 4);
    content.add(zBox = new BRadioButton("Z", true, pathGroup), 0, 5);
    content.add(pathBox = new BRadioButton("Curve", true, pathGroup), 0, 6);
    content.add(vectorBox = new BRadioButton("Vector", true, pathGroup), 0, 7);
    pathBox.setEnabled(paths.size() > 0);
    pathGroup.addEventLink(SelectionChangedEvent.class, this, "stateChanged");
    pathGroup.setSelection(zBox);
    RowContainer distanceRow = new RowContainer();
    content.add(distanceRow, 1, 4);
    distanceRow.add(new BLabel("Distance:"));
    distanceRow.add(distField = new ValueField(1.0, ValueField.POSITIVE, 5));
    distField.addEventLink(ValueChangedEvent.class, this, "makeObject");
    content.add(pathChoice = new BComboBox(), 1, 6);
    for (int i = 0; i < paths.size(); i++)
      pathChoice.add(((ObjectInfo) paths.elementAt(i)).name);
    pathChoice.addEventLink(ValueChangedEvent.class, this, "stateChanged");
    RowContainer vectorRow = new RowContainer();
    content.add(vectorRow, 1, 7);
    vectorRow.add(new BLabel("X"));
    vectorRow.add(xField = new ValueField(0.0, ValueField.NONE, 4));
    xField.addEventLink(ValueChangedEvent.class, this, "makeObject");
    vectorRow.add(new BLabel("Y"));
    vectorRow.add(yField = new ValueField(0.0, ValueField.NONE, 4));
    yField.addEventLink(ValueChangedEvent.class, this, "makeObject");
    vectorRow.add(new BLabel("Z"));
    vectorRow.add(zField = new ValueField(1.0, ValueField.NONE, 4));
    zField.addEventLink(ValueChangedEvent.class, this, "makeObject");
    content.add(orientBox = new BCheckBox("Orientation Follows Curve", true), 0, 8, 2, 1);
    orientBox.addEventLink(ValueChangedEvent.class, this, "stateChanged");
    content.add(new BLabel("Number of Segments:"), 2, 0);
    content.add(new BLabel("Twist (degrees):"), 2, 1);
    content.add(new BLabel("Surface Accuracy:"), 2, 2);
    content.add(segField = new ValueField(1.0, ValueField.POSITIVE+ValueField.INTEGER, 5), 3, 0);
    content.add(angleField = new ValueField(0.0, ValueField.NONE, 5), 3, 1);
    content.add(tolField = new ValueField(0.1, ValueField.POSITIVE, 5), 3, 2);
    segField.addEventLink(ValueChangedEvent.class, this, "makeObject");
    angleField.addEventLink(ValueChangedEvent.class, this, "makeObject");
    tolField.addEventLink(ValueChangedEvent.class, this, "makeObject");
    if (paths.size() > 0)
      for (int i = 0; i < scene.getNumObjects(); i++)
	if (scene.getObject(i) != paths.elementAt(0))
        {
          objChoice.setSelectedIndex(i);
          break;
        }
    content.add(preview = new ObjectPreviewCanvas((ObjectInfo) objects.elementAt(0)), 2, 3, 2, 6,
        new LayoutInfo(LayoutInfo.CENTER, LayoutInfo.BOTH, null, null));
    RowContainer buttons = new RowContainer();
    content.add(buttons, 0, 9, 4, 1, new LayoutInfo());
    buttons.add(okButton = Translate.button("ok", this, "doOk"));
    buttons.add(cancelButton = Translate.button("cancel", this, "dispose"));
    makeObject();
    pack();
    UIUtilities.centerDialog(this, window);
    updateComponents();
    setVisible(true);
  }

  /** Deal with changes to the checkboxes and choices. */

  private void stateChanged()
  {
    makeObject();
    updateComponents();
  }

  /** Enable or disable components, based on the current selections. */

  private void updateComponents()
  {
    distField.setEnabled(xBox.getState() || yBox.getState() || zBox.getState());
    xField.setEnabled(vectorBox.getState());
    yField.setEnabled(vectorBox.getState());
    zField.setEnabled(vectorBox.getState());
    pathChoice.setEnabled(pathBox.getState());
    segField.setEnabled(!pathBox.getState());
    orientBox.setEnabled(pathBox.getState());
    Object3D profile = ((ObjectInfo) objects.elementAt(objChoice.getSelectedIndex())).object;
    tolField.setEnabled(!(profile instanceof Curve || profile instanceof TriangleMesh));
    if (pathBox.getState())
      okButton.setEnabled(objects.elementAt(objChoice.getSelectedIndex()) != paths.elementAt(pathChoice.getSelectedIndex()));
    else
      okButton.setEnabled(true);
  }

  private void doOk()
  {
    ObjectInfo profile = (ObjectInfo) objects.elementAt(objChoice.getSelectedIndex());
    window.addObject(preview.getObject().object, new CoordinateSystem(
        new Vec3(profile.coords.getOrigin()), Vec3.vz(), Vec3.vy()),
        "Extruded Object "+(counter++), null);
        
    window.setSelection(window.getScene().getNumObjects()-1);
    window.setUndoRecord(new UndoRecord(window, false, UndoRecord.DELETE_OBJECT, new Object [] {new Integer(window.getScene().getNumObjects()-1)}));
    window.updateImage();
    dispose();
  }

  // Create the extruded object.

  private void makeObject()
  {
    ObjectInfo profile = (ObjectInfo) objects.elementAt(objChoice.getSelectedIndex());
    Curve path;
    CoordinateSystem pathCoords;

    if (pathBox.getState())
      {
	ObjectInfo info = (ObjectInfo) paths.elementAt(pathChoice.getSelectedIndex());
	path = (Curve) info.object;
	pathCoords = info.coords;
      }
    else
      {
	Vec3 dir = new Vec3();
	if (xBox.getState())
	  dir.x = distField.getValue();
	else if (yBox.getState())
	  dir.y = distField.getValue();
	else if (zBox.getState())
	  dir.z = distField.getValue();
	else
	  dir.set(xField.getValue(), yField.getValue(), zField.getValue());
	Vec3 v[] = new Vec3 [(int) segField.getValue()+1];
	float smooth[] = new float [v.length];
	for (int i = 0; i < v.length; i++)
	  {
	    v[i] = new Vec3(dir);
	    v[i].scale(i/segField.getValue());
	    smooth[i] = 1.0f;
	  }
	path = new Curve(v, smooth, Mesh.INTERPOLATING, false);
	pathCoords = new CoordinateSystem(new Vec3(), Vec3.vz(), Vec3.vy());
      }
    Object3D obj;
    if (profile.object == path)
    {
      obj = null;    //TODO(MB) Better behaviour in this case.
      return;   // Fixes bug in original aoi
    }
    else if (profile.object instanceof TriangleMesh)
      obj = extrudeMesh((TriangleMesh) profile.object, path, profile.coords, pathCoords, angleField.getValue()*Math.PI/180.0, orientBox.getState());
    else if (profile.object instanceof Curve)
      obj = extrudeCurve((Curve) profile.object, path, profile.coords, pathCoords, angleField.getValue()*Math.PI/180.0, orientBox.getState());
    else
      obj = extrudeMesh(profile.object.convertToTriangleMesh(tolField.getValue()), path, profile.coords, pathCoords, angleField.getValue()*Math.PI/180.0, orientBox.getState());
    Texture tex = window.getScene().getDefaultTexture();
    obj.setTexture(tex, tex.getDefaultMapping());
    preview.setObject(obj);
    preview.updateImage();
    preview.repaint();
  }

  /** Extrude a curve into a spline mesh.

      @param profile     the curve to extrude
      @param profCoords  the coordinate system of the profile
      @param dir         the direction and distance along which to extrude it
      @param segments    the number of segments to create
      @param angle       the twist angle (in radians)
      @param orient      if true, the orientation of the profile will follow the curve
      @return the extruded object
  */

  public static Object3D extrudeCurve(Curve profile, CoordinateSystem profCoords, Vec3 dir, int segments, double angle, boolean orient)
  {
    Vec3 v[] = new Vec3 [segments+1];
    float smooth[] = new float [v.length];
    double scale = 1.0/segments;
    for (int i = 0; i < v.length; i++)
      {
        v[i] = new Vec3(dir);
        v[i].scale(i*segments);
        smooth[i] = 1.0f;
      }
    Curve path = new Curve(v, smooth, Mesh.INTERPOLATING, false);
    return extrudeCurve(profile, path, profCoords, new CoordinateSystem(), angle, orient);
  }

  /** Extrude a curve into a spline mesh.

      @param profile     the curve to extrude
      @param path        the path along which to extrude it
      @param profCoords  the coordinate system of the profile
      @param pathCoords  the coordinate system of the path
      @param angle       the twist angle (in radians)
      @param orient      if true, the orientation of the profile will follow the curve
      @return the extruded object
  */

  public static Object3D extrudeCurve(Curve profile, Curve path, CoordinateSystem profCoords, CoordinateSystem pathCoords, double angle, boolean orient)
  {
    MeshVertex profVert[] = profile.getVertices(), pathVert[] = path.getVertices();
    Vec3 profv[] = new Vec3 [profVert.length], pathv[] = new Vec3[pathVert.length];
    Vec3 subdiv[], center = new Vec3(), zdir[], updir[], t[], v[][];
    float usmooth[] = new float [pathVert.length], vsmooth[] = new float [profVert.length];
    float profSmooth[] = profile.getSmoothness(), pathSmooth[] = path.getSmoothness();
    CoordinateSystem localCoords = new CoordinateSystem(new Vec3(), Vec3.vz(), Vec3.vy());
    Mat4 rotate;
    int i, j;

    for (i = 0; i < profVert.length; i++)
      profv[i] = profCoords.fromLocal().timesDirection(profVert[i].r);
    for (i = 0; i < pathVert.length; i++)
      pathv[i] = pathCoords.fromLocal().timesDirection(pathVert[i].r);

    // Construct the Minimally Rotating Frame at every point along the path.  First,
    // subdivide the path and determine its direction at the starting point.

    subdiv = new Curve(pathv, pathSmooth, path.getSmoothingMethod(), path.isClosed()).subdivideCurve().getVertexPositions();
    t = new Vec3 [subdiv.length];
    zdir = new Vec3 [subdiv.length];
    updir = new Vec3 [subdiv.length];
    t[0] = subdiv[1].minus(subdiv[0]);
    t[0].normalize();
    zdir[0] = Vec3.vz();
    updir[0] = Vec3.vy();

    // Now find two vectors perpendicular to the path, and determine how much they
    // contribute to the z and up directions.

    Vec3 dir1, dir2;
    double zfrac1, zfrac2, upfrac1, upfrac2;
    zfrac1 = t[0].dot(zdir[0]);
    zfrac2 = Math.sqrt(1.0-zfrac1*zfrac1);
    dir1 = zdir[0].minus(t[0].times(zfrac1));
    dir1.normalize();
    upfrac1 = t[0].dot(updir[0]);
    upfrac2 = Math.sqrt(1.0-upfrac1*upfrac1);
    dir2 = updir[0].minus(t[0].times(upfrac1));
    dir2.normalize();

    // Propagate the vectors along the path.

    for (i = 1; i < subdiv.length; i++)
      {
	if (i == subdiv.length-1)
	  {
	    if (path.isClosed())
	      t[i] = subdiv[0].minus(subdiv[subdiv.length-2]);
	    else
	      t[i] = subdiv[subdiv.length-1].minus(subdiv[subdiv.length-2]);
	  }
	else
	  t[i] = subdiv[i+1].minus(subdiv[i-1]);
	t[i].normalize();
	if (orient)
	  {
	    dir1 = dir1.minus(t[i].times(t[i].dot(dir1)));
	    dir1.normalize();
	    dir2 = dir2.minus(t[i].times(t[i].dot(dir2)));
	    dir2.normalize();
	    zdir[i] = t[i].times(zfrac1).plus(dir1.times(zfrac2));
	    updir[i] = t[i].times(upfrac1).plus(dir2.times(upfrac2));
	  }
	else
	  {
	    zdir[i] = zdir[i-1];
	    updir[i] = updir[i-1];
	  }
      }

    // Set the smoothness values.

    if (path.getSmoothingMethod() != Mesh.NO_SMOOTHING)
      for (i = 0; i < usmooth.length; i++)
	usmooth[i] = pathSmooth[i];
    if (profile.getSmoothingMethod() != Mesh.NO_SMOOTHING)
      for (i = 0; i < vsmooth.length; i++)
	vsmooth[i] = profSmooth[i];

    // If one curve is interpolating and the other is approximating, use a subdivided
    // version of the interpolating one to more accurately get its shape (since the final
    // mesh will be approximating).

    if (profile.getSmoothingMethod() == Mesh.APPROXIMATING && path.getSmoothingMethod() == Mesh.INTERPOLATING)
      {
	pathv = subdiv;
	usmooth = new float [pathv.length];
	for (i = 0; i < usmooth.length; i++)
	  {
	    if (i%2 == 0)
	      usmooth[i] = Math.min(pathSmooth[i/2]*2.0f, 1.0f);
	    else
	      usmooth[i] = 1.0f;
	  }
      }
    if (profile.getSmoothingMethod() == Mesh.INTERPOLATING && path.getSmoothingMethod() == Mesh.APPROXIMATING)
      {
        profv = new Curve(profv, profSmooth, profile.getSmoothingMethod(), profile.isClosed()).subdivideCurve().getVertexPositions();
	vsmooth = new float [profv.length];
	for (i = 0; i < vsmooth.length; i++)
	  {
	    if (i%2 == 0)
	      vsmooth[i] = Math.min(profSmooth[i/2]*2.0f, 1.0f);
	    else
	      vsmooth[i] = 1.0f;
	  }
      }

    // Create the extruded surface.

    v = new Vec3 [pathv.length][profv.length];
    for (i = 0; i < pathv.length; i++)
      {
	localCoords.setOrigin(pathv[i]);
	int k = (pathv.length == subdiv.length ? i : 2*i);
	localCoords.setOrientation(zdir[k], updir[k]);
	if (angle != 0.0)
	  {
	    rotate = Mat4.axisRotation(t[k], i*angle/(pathv.length-1));
	    localCoords.transformAxes(rotate);
	  }
	for (j = 0; j < profv.length; j++)
	  {
	    v[i][j] = localCoords.fromLocal().times(profv[j]);
	    center.add(v[i][j]);
	  }
      }

    // Center it.

    center.scale(1.0/(profv.length*pathv.length));
    for (i = 0; i < pathv.length; i++)
      for (j = 0; j < profv.length; j++)
	v[i][j].subtract(center);
    SplineMesh mesh = new SplineMesh(v, usmooth, vsmooth, Math.max(profile.getSmoothingMethod(),
	path.getSmoothingMethod()), path.isClosed(), profile.isClosed());
    mesh.makeRightSideOut();
    return mesh;
  }

  /** Extrude a triangle mesh into a solid object.

      @param profile     the TriangleMesh to extrude
      @param profCoords  the coordinate system of the profile
      @param dir         the direction and distance along which to extrude it
      @param segments    the number of segments to create
      @param angle       the twist angle (in radians)
      @param orient      if true, the orientation of the profile will follow the curve
      @return the extruded object
  */

  public static Object3D extrudeMesh(TriangleMesh profile, CoordinateSystem profCoords, Vec3 dir, int segments, double angle, boolean orient)
  {
    Vec3 v[] = new Vec3 [segments+1];
    float smooth[] = new float [v.length];
    double scale = 1.0/segments;
    for (int i = 0; i < v.length; i++)
      {
        v[i] = new Vec3(dir);
        v[i].scale(i*segments);
        smooth[i] = 1.0f;
      }
    Curve path = new Curve(v, smooth, Mesh.INTERPOLATING, false);
    return extrudeMesh(profile, path, profCoords, new CoordinateSystem(), angle, orient);
  }

  /** Extrude a triangle mesh into a solid object.

      @param profile     the TriangleMesh to extrude
      @param path        the path along which to extrude it
      @param profCoords  the coordinate system of the profile
      @param pathCoords  the coordinate system of the path
      @param angle       the twist angle (in radians)
      @param orient      if true, the orientation of the profile will follow the curve
      @return the extruded object
  */

  public static Object3D extrudeMesh(TriangleMesh profile, Curve path, CoordinateSystem profCoords, CoordinateSystem pathCoords, double angle, boolean orient)
  {
    Vertex profVert[] = (Vertex [] ) profile.getVertices();
    MeshVertex pathVert[] = path.getVertices();
    Edge profEdge[] = profile.getEdges();
    Face profFace[] = profile.getFaces();
    Vec3 profv[] = new Vec3 [profVert.length], pathv[] = new Vec3[pathVert.length];
    Vec3 subdiv[], center, zdir[], updir[], t[], v[];
    float pathSmooth[] = path.getSmoothness();
    CoordinateSystem localCoords = new CoordinateSystem(new Vec3(), Vec3.vz(), Vec3.vy());
    Mat4 rotate;
    int numBoundaryEdges = 0, numBoundaryPoints = 0, i, j, k;
    int boundaryEdge[], boundaryPoint[];

    for (i = 0; i < profVert.length; i++)
      profv[i] = profCoords.fromLocal().timesDirection(profVert[i].r);
    for (i = 0; i < pathVert.length; i++)
      pathv[i] = pathCoords.fromLocal().timesDirection(pathVert[i].r);
    if (path.getSmoothingMethod() == Mesh.NO_SMOOTHING)
      for (i = 0; i < pathSmooth.length; i++)
	pathSmooth[i] = 0.0f;

    // Make a list of the edges and vertices which are on the boundary of the mesh.

    boolean onBound[] = new boolean [profv.length];
    for (i = 0; i < profEdge.length; i++)
      if (profEdge[i].f2 == -1)
	{
	  numBoundaryEdges++;
	  onBound[profEdge[i].v1] = onBound[profEdge[i].v2] = true;
	}
    for (i = 0; i < onBound.length; i++)
      if (onBound[i])
	numBoundaryPoints++;
    boundaryEdge = new int [numBoundaryEdges];
    boundaryPoint = new int [numBoundaryPoints];
    for (i = 0, j = 0; i < profEdge.length; i++)
      if (profEdge[i].f2 == -1)
	boundaryEdge[j++] = i;
    for (i = 0, j = 0; i < onBound.length; i++)
      if (onBound[i])
	boundaryPoint[j++] = i;

    // Find which direction each boundary edge points in.

    boolean forward[] = new boolean [boundaryEdge.length];
    int edgeVertIndex[][] = new int [boundaryEdge.length][2];
    for (i = 0; i < boundaryEdge.length; i++)
      {
	Edge ed = profEdge[boundaryEdge[i]];
	Face fc = profFace[ed.f1];
	forward[i] = ((fc.v1 == ed.v1 && fc.v2 == ed.v2) ||
	    (fc.v2 == ed.v1 && fc.v3 == ed.v2) || (fc.v3 == ed.v1 && fc.v1 == ed.v2));
	for (j = 0; j < boundaryPoint.length; j++)
	  {
	    if (boundaryPoint[j] == ed.v1)
	      edgeVertIndex[i][0] = j;
	    else if (boundaryPoint[j] == ed.v2)
	      edgeVertIndex[i][1] = j;
	  }
      }

    // Make up a list of the indices for every point on the side of the extruded object.

    int index[][];
    if (path.isClosed())
      {
	index = new int [pathv.length+1][boundaryPoint.length];
	for (i = 0; i < boundaryPoint.length; i++)
	  {
	    for (j = 0; j < pathv.length; j++)
	      index[j][i] = j*boundaryPoint.length+i;
	    index[j][i] = i;
	  }
      }
    else
      {
	index = new int [pathv.length][boundaryPoint.length];
	for (i = 0; i < boundaryPoint.length; i++)
	  {
	    index[0][i] = boundaryPoint[i];
	    index[pathv.length-1][i] = boundaryPoint[i]+profv.length;
	    for (j = 1; j < pathv.length-1; j++)
	      index[j][i] = (j-1)*boundaryPoint.length+i+2*profv.length;
	  }
      }

    // Construct the Minimally Rotating Frame at every point along the path.  First,
    // subdivide the path and determine its direction at the starting point.

    subdiv = new Curve(pathv, pathSmooth, path.getSmoothingMethod(), path.isClosed()).subdivideCurve().getVertexPositions();
    t = new Vec3 [subdiv.length];
    zdir = new Vec3 [subdiv.length];
    updir = new Vec3 [subdiv.length];
    t[0] = subdiv[1].minus(subdiv[0]);
    t[0].normalize();
    zdir[0] = Vec3.vz();
    updir[0] = Vec3.vy();

    // Now find two vectors perpendicular to the path, and determine how much they
    // contribute to the z and up directions.

    Vec3 dir1, dir2;
    double zfrac1, zfrac2, upfrac1, upfrac2;
    zfrac1 = t[0].dot(zdir[0]);
    zfrac2 = Math.sqrt(1.0-zfrac1*zfrac1);
    dir1 = zdir[0].minus(t[0].times(zfrac1));
    dir1.normalize();
    upfrac1 = t[0].dot(updir[0]);
    upfrac2 = Math.sqrt(1.0-upfrac1*upfrac1);
    dir2 = updir[0].minus(t[0].times(upfrac1));
    dir2.normalize();

    // Propagate the vectors along the path.

    for (i = 1; i < subdiv.length; i++)
      {
	if (i == subdiv.length-1)
	  {
	    if (path.isClosed())
	      t[i] = subdiv[0].minus(subdiv[subdiv.length-2]);
	    else
	      t[i] = subdiv[subdiv.length-1].minus(subdiv[subdiv.length-2]);
	  }
	else
	  t[i] = subdiv[i+1].minus(subdiv[i-1]);
	t[i].normalize();
	if (orient)
	  {
	    dir1 = dir1.minus(t[i].times(t[i].dot(dir1)));
	    dir1.normalize();
	    dir2 = dir2.minus(t[i].times(t[i].dot(dir2)));
	    dir2.normalize();
	    zdir[i] = t[i].times(zfrac1).plus(dir1.times(zfrac2));
	    updir[i] = t[i].times(upfrac1).plus(dir2.times(upfrac2));
	  }
	else
	  {
	    zdir[i] = zdir[i-1];
	    updir[i] = updir[i-1];
	  }
      }

    // Create the extruded surface.

    if (path.isClosed())
      v = new Vec3 [numBoundaryPoints*pathv.length];
    else
      v = new Vec3 [2*profv.length+numBoundaryPoints*(pathv.length-2)];
    Vector newEdge = new Vector(), newFace = new Vector();
    boolean angled = (profile.getSmoothingMethod() == Mesh.NO_SMOOTHING && path.getSmoothingMethod() != Mesh.NO_SMOOTHING);
    if (!path.isClosed())
      {
	// Add two copies of the profile mesh, to serve as the ends.

	localCoords.setOrigin(pathv[0]);
	localCoords.setOrientation(zdir[0], updir[0]);
	for (i = 0; i < profv.length; i++)
	  v[i] = localCoords.fromLocal().times(profv[i]);
	k = (pathv.length == subdiv.length ? pathv.length-1 : 2*(pathv.length-1));
	localCoords.setOrigin(pathv[pathv.length-1]);
	localCoords.setOrientation(zdir[k], updir[k]);
	if (angle != 0.0)
	  {
	    rotate = Mat4.axisRotation(t[k], angle);
	    localCoords.transformAxes(rotate);
	  }
	for (i = 0; i < profv.length; i++)
	  v[i+profv.length] = localCoords.fromLocal().times(profv[i]);

	// Add the edges and faces.

	for (i = 0; i < profEdge.length; i++)
	  {
	    float smoothness = profEdge[i].smoothness;
	    if (angled || profEdge[i].f2 == -1)
	      smoothness = 0.0f;
	    newEdge.addElement(new EdgeInfo(profEdge[i].v1, profEdge[i].v2, smoothness));
	    newEdge.addElement(new EdgeInfo(profEdge[i].v1+profv.length, profEdge[i].v2+profv.length, smoothness));
	  }
	for (i = 0; i < profFace.length; i++)
	  {
	    Face f = profFace[i];
	    newFace.addElement(new int [] {f.v1, f.v2, f.v3});
	    newFace.addElement(new int [] {f.v1+profv.length, f.v3+profv.length, f.v2+profv.length});
	  }
      }
    for (i = 0; i < pathv.length; i++)
      {
	if (!path.isClosed() && i == pathv.length-1)
	  break;

	// Add each row of triangles and edges around the side.

	for (j = 0; j < boundaryEdge.length; j++)
	  {
	    Edge ed = profEdge[boundaryEdge[j]];
	    int v1, v2;
	    if (forward[j])

	      {
		v1 = edgeVertIndex[j][0];
		v2 = edgeVertIndex[j][1];
	      }
	    else
	      {
		v1 = edgeVertIndex[j][1];
		v2 = edgeVertIndex[j][0];
	      }
	    newFace.addElement(new int [] {index[i][v1], index[i+1][v1], index[i+1][v2]});
	    newFace.addElement(new int [] {index[i][v2], index[i][v1], index[i+1][v2]});
	    EdgeInfo ed1 = new EdgeInfo(index[i][v1], index[i+1][v1], angled ? 0.0f : profVert[boundaryPoint[v1]].smoothness);
	    newEdge.addElement(ed1);
	    ed1 = new EdgeInfo(index[i][v2], index[i+1][v2], angled ? 0.0f : profVert[boundaryPoint[v2]].smoothness);
	    newEdge.addElement(ed1);
	    ed1 = new EdgeInfo(index[i][v1], index[i+1][v2], 1.0f);
	    newEdge.addElement(ed1);
	    if (path.isClosed() || i > 0)
	      {
		ed1 = new EdgeInfo(index[i][v1], index[i][v2], pathSmooth[i]);
		newEdge.addElement(ed1);
	      }
	  }

	// Add each row of points around the side.

	localCoords.setOrigin(pathv[i]);
	k = (pathv.length == subdiv.length ? i : 2*i);
	localCoords.setOrientation(zdir[k], updir[k]);
	if (angle != 0.0)
	  {
	    rotate = Mat4.axisRotation(t[k], i*angle/(pathv.length-1));
	    localCoords.transformAxes(rotate);
	  }
	for (j = 0; j < boundaryPoint.length; j++)
	  v[index[i][j]] = localCoords.fromLocal().times(profv[boundaryPoint[j]]);
      }

    // Center it.

    center = new Vec3();
    for (i = 0; i < v.length; i++)
      center.add(v[i]);
    center.scale(1.0/v.length);
    for (i = 0; i < v.length; i++)
      v[i].subtract(center);

    // Build the final object.

    int faces[][] = new int [newFace.size()][];
    for (i = 0; i < faces.length; i++)
      faces[i] = (int []) newFace.elementAt(i);
    TriangleMesh mesh = new TriangleMesh(v, faces);
    Edge meshEdge[] = mesh.getEdges();
    for (i = 0; i < newEdge.size(); i++)
      {
	EdgeInfo info = (EdgeInfo) newEdge.elementAt(i);
	if (info.smoothness == 1.0f)
	  continue;
	for (j = 0; j < meshEdge.length; j++)
	  if ((meshEdge[j].v1 == info.v1 && meshEdge[j].v2 == info.v2) ||
	      (meshEdge[j].v1 == info.v2 && meshEdge[j].v2 == info.v1))
	    meshEdge[j].smoothness = info.smoothness;
      }
    mesh.setSmoothingMethod(Math.max(profile.getSmoothingMethod(), path.getSmoothingMethod()));
    mesh.makeRightSideOut();
    return mesh;
  }

  // Inner class used for storing information about new edges being created.

  private static class EdgeInfo
  {
    int v1, v2;
    float smoothness;

    public EdgeInfo(int vert1, int vert2, float smooth)
    {
      v1 = vert1;
      v2 = vert2;
      smoothness = smooth;
    }
  }
}