/* Copyright (C) 1999-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;

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

/** The TriMeshEditorWindow class represents the window for editing TriangleMesh objects. */
public class TriMeshEditorWindow extends MeshEditorWindow
{
  ToolPalette modes;
  boolean topology;

  public TriMeshEditorWindow(EditingWindow parent, String title, ObjectInfo obj, Runnable onClose, boolean allowTopology)
  {
    super(parent, title, obj, onClose);
    topology = allowTopology;
    
    tools = new ToolPalette(1, allowTopology ? 12 : 9);
    //toolsPanel = new BorderContainer();
    toolsPanel.add(tools, BorderContainer.NORTH);  //TODO(MB) Remove this toolsPanel creation
    tools.addTool(defaultTool = new ReshapeMeshTool(this));
    tools.addTool(new ScaleMeshTool(this));
    tools.addTool(new RotateMeshTool(this, false));
    tools.addTool(new SkewMeshTool(this));
    tools.addTool(new TaperMeshTool(this));
    tools.addTool(new ThickenMeshTool(this));
    if (topology)
    {
      tools.addTool(new BevelExtrudeTool(this));
      tools.addTool(new CreateVertexTool(this));
      tools.addTool(new CreateVertexTool_ED(this));
    }
    tools.addTool(new SkeletonTool(this, true));
    tools.addTool(getMetaTool() /* MoveViewTool */);
    tools.addTool(getAltTool() /* RotateViewTool */);
    tools.selectTool(defaultTool);
    toolsPanel.add(modes = new ToolPalette(1, 3), BorderContainer.SOUTH);
    modes.addTool(new GenericTool(this, "point.gif", "selected/point.gif"));
    modes.addTool(new GenericTool(this, "edge.gif", "selected/edge.gif"));
    modes.addTool(new GenericTool(this, "face.gif", "selected/face.gif"));
    
    MenuStructure ms = new MenuStructure();
    ms.loadStructure("TriMesh", false);
    menubar = ms.createMenuBar(this);
    setMenuBar(menubar);
    menuitemsCache = null;  // The menubar was rebuild so invalidate cache
   
    UIUtilities.recursivelyAddKeyPressedListeners(this);
    pack();
    Dimension d1 = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension d2 = new Dimension((d1.width*3)/4, (d1.height*3)/4);
    setBounds(new Rectangle((d1.width-d2.width)/2, (d1.height-d2.height)/2, d2.width, d2.height));
    tools.requestFocus();

    updateMenus();
  }

  /** List of names of menu items which are only allowed if {@link #topology} is true.
    Used by {@link #isMenuItemAllowed()}.*/

  private static final String[] MENUITEMS_ALLOWED_ON_TOPOLOGY=new String[]
    { "clear", "subdivideEdges", "simplify", "bevel", "optimize",
      "closeBoundary", "joinBoundaries", "invertNormals", "createFace"
    };

  /** List of names of menu items which should be CheckboxMenuItem.
    Used by {@link #isMenuItemNameCheckbox()}.*/

  private static final String[] MENUITEMS_CHECKBOXITEMS=new String[]
    { "tolerantSelection", "freehandSelection", "displayAsQuads", "none",
      "shading", "interpolating", "approximating", "detachSkeleton"
    };

  /** Tests if menu item item should be shown in the menus for
    this object editor. */
  public boolean isMenuItemAllowed(MenuWidget item)
  {
    String itemname = ((Widget)item).getName();
    if (Utilities.findIndexEqual(MENUITEMS_ALLOWED_ON_TOPOLOGY, itemname) > -1)
      return topology;   // Some items are only allowed if topology is true
    else if ("renderPreview".equals(itemname))
      return ModellingApp.getPreferences().getObjectPreviewRenderer() != null;
    else
      return super.isMenuItemAllowed(item);
  }

  /** Tests if menu item named itemname should be a checkbox item. */
  protected boolean isMenuItemNameCheckbox(String itemname)
  {
    if (Utilities.findIndexEqual(MENUITEMS_CHECKBOXITEMS, itemname) > -1)
      return true;
    else
      return super.isMenuItemNameCheckbox(itemname);
  }

  /* EditingWindow methods. */

  public void setTool(EditingTool tool)
  {
    for(int i = 0; i < theView.length; ++i)
    {
      if (tool instanceof GenericTool)
      {
        ((TriMeshViewer) theView[i]).setSelectionMode(modes.getSelection()); //TODO(MB) current or theView[i] ?
        theView[i].getCurrentTool().activate();  // TODO(MB) Added by original AoI
        ((TriMeshViewer) theView[i]).informSelectionChanged(this,
            ModelEvent.CHANGEDUR_OBJECTEDITOR);
        updateMenus();
      }
      else
        {
          theView[i].setTool(tool);
          currentTool = tool;
        }
    }
  }

  /** Returns true if the view has at least one selected
    element. */
  public boolean isSomethingSelected()
  {
    int i;
    boolean selected[] = ((TriMeshViewer) getCurrentView()).getSelection();

    for (i = 0; i < selected.length && !selected[i]; i++);

    return i < selected.length;
  }

  /** Updates a single menu item
    (setting enabled/disabled, (un)checked ...). */
  protected void updateMenuItem(MenuWidget item)
  {
    String itemname = ((Widget)item).getName();
    BCheckBoxMenuItem cbitem = null;
    BMenuItem mnitem = null;
    if (item instanceof BCheckBoxMenuItem)
      cbitem = (BCheckBoxMenuItem)item;
    else if (item instanceof BMenuItem)
      mnitem = (BMenuItem)item;

    int selectionMode = ((TriMeshViewer) getCurrentView()).getSelectionMode();
    TriangleMesh obj = (TriangleMesh)getCurrentView().getObject().object;

    // Edit Menu

    if ("clear".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("selectBoundary".equals(itemname))
      mnitem.setEnabled(selectionMode == TriMeshViewer.EDGE_MODE && !getCurrentView().getObject().object.isClosed());
    else if ("extendSelection".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("hideSelection".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());

    // Mesh Menu

    else if ("subdivideEdges".equals(itemname))
    {
      mnitem.setEnabled(true);
      if (!isSomethingSelected())
        mnitem.setEnabled(false);
      else if (selectionMode == TriMeshViewer.EDGE_MODE)
        mnitem.setText(Translate.text("menu.subdivideEdges"));
      else if (selectionMode == TriMeshViewer.FACE_MODE)
        mnitem.setText(Translate.text("menu.subdivideFaces"));
      else
        mnitem.setEnabled(false);
    }
    else if ("simplify".equals(itemname))
    {
      mnitem.setEnabled(true);
      if (isSomethingSelected())
        mnitem.setText(Translate.text("menu.simplify"));
      else
        mnitem.setText(Translate.text("menu.simplifyMesh"));
    }
    else if ("createFace".equals(itemname))
      mnitem.setEnabled(isSomethingSelected() && selectionMode == TriMeshViewer.POINT_MODE);
    else if ("editPoints".equals(itemname))
    {
      mnitem.setEnabled(isSomethingSelected());
    }
    else if ("transformPoints".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("randomize".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("bevel".equals(itemname))
    {
      mnitem.setEnabled(isSomethingSelected());
    }
    else if ("parameters".equals(itemname))
    {                                 // TODO(MB) Check if this is senseful
      if (isSomethingSelected())
      {
        if (selectionMode == TriMeshViewer.EDGE_MODE)
        {
          int boundary[][] = ((TriMeshViewer) getCurrentView()).findSelectedBoundaries();
          mnitem.setEnabled(boundary.length > 0);
        }
        else
          mnitem.setEnabled(true);
      }
      else
        mnitem.setEnabled(false);
    }
    else if ("closeBoundary".equals(itemname))
      mnitem.setEnabled(isSomethingSelected() && selectionMode == TriMeshViewer.EDGE_MODE);
    else if ("joinBoundaries".equals(itemname))
    {
      if (isSomethingSelected() && selectionMode == TriMeshViewer.EDGE_MODE)
      {
        int boundary[][] = ((TriMeshViewer) getCurrentView()).findSelectedBoundaries();
        mnitem.setEnabled(boundary.length == 2);
      }
      else
        mnitem.setEnabled(false);
    }
    else if ("extractCurve".equals(itemname))
      mnitem.setEnabled(isSomethingSelected() && selectionMode == TriMeshViewer.EDGE_MODE);
    else if ("smoothness".equals(itemname))
      mnitem.setEnabled(isSomethingSelected() && selectionMode != TriMeshViewer.FACE_MODE);

    else if ("fourViews".equals(itemname))
    {
      if (numViewsShown == 4)
        mnitem.setText(Translate.text("menu.fourViews"));
      else
        mnitem.setText(Translate.text("menu.oneView"));
    }
    
    // Smooth Menu in Mesh Menu
    else if ("none".equals(itemname))
      cbitem.setState(obj.getSmoothingMethod() == TriangleMesh.NO_SMOOTHING);
    else if ("shading".equals(itemname))
      cbitem.setState(obj.getSmoothingMethod() == TriangleMesh.SMOOTH_SHADING);
    else if ("interpolating".equals(itemname))
      cbitem.setState(obj.getSmoothingMethod() == TriangleMesh.INTERPOLATING);
    else if ("approximating".equals(itemname))
      cbitem.setState(obj.getSmoothingMethod() == TriangleMesh.APPROXIMATING);

    // Skeleton Menu

    else if ("editBone".equals(itemname))
    {
      Skeleton s = obj.getSkeleton();
      Joint selJoint = s.getJoint(getCurrentView().getSelectedJoint());

      mnitem.setEnabled(selJoint != null);
    }
    else if ("deleteBone".equals(itemname))
    {
      Skeleton s = obj.getSkeleton();
      Joint selJoint = s.getJoint(getCurrentView().getSelectedJoint());

      mnitem.setEnabled(selJoint != null && selJoint.children.length == 0);
    }
    else if ("bindSkeleton".equals(itemname))
      mnitem.setEnabled(isSomethingSelected());
    else if ("setParentBone".equals(itemname))
    {
      Skeleton s = obj.getSkeleton();
      Joint selJoint = s.getJoint(getCurrentView().getSelectedJoint());

      mnitem.setEnabled(selJoint != null);
    }
    else super.updateMenuItem(item);

  }

  public void keyPressed(KeyPressedEvent e)
  {
    if (e.getKeyCode() == e.VK_DELETE || e.getKeyCode() == e.VK_BACK_SPACE)   // TODO(MB) Redesign
      {
	if (getCurrentView().getCurrentTool() instanceof SkeletonTool)
	  deleteJointCommand();
	else
	  deleteCommand();
      }
    else
      super.keyPressed(e);
  }

  public void actionPerformed(CommandEvent e)
  {
    String command = e.getActionCommand();

    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

    if (handleMenuAction(command)) // Try to call the command by reflection
      ;  // Do nothing
    else if (command.equals("undo"))
      undoCommand();
    else if (command.equals("clear"))
      deleteCommand();
//    else if (command.equals("selectAll"))
//      selectAllCommand();
//    else if (command.equals("hideSelection"))
//      hideSelectionCommand();
//    else if (command.equals("showAll"))
//      showAllCommand();
    else if (command.equals("subdivideEdges"))
      subdivideCommand();
//    else if (command.equals("simplify"))
//      simplifyCommand();
//    else if (command.equals("optimize"))
//      optimizeCommand();
    else if (command.equals("editPoints"))
      setPointsCommand();
//    else if (command.equals("transformPoints"))
//      transformPointsCommand();
//    else if (command.equals("randomize"))
//      randomizeCommand();
//    else if (command.equals("bevel"))
//      bevelCommand();
    else if (command.equals("parameters"))
      setParametersCommand();
    else if (command.equals("centerMesh"))
      centerCommand();
//    else if (command.equals("closeBoundary"))
//      closeBoundaryCommand();
//    else if (command.equals("joinBoundaries"))
//      joinBoundariesCommand();
//    else if (command.equals("extractCurve"))
//      extractCurveCommand();
    else if (command.equals("smoothness"))
      setSmoothnessCommand();
    else if (command.equals("invertNormals"))
      reverseNormalsCommand();
    else if (command.equals("meshTension"))
      setTensionCommand();
    else if (command.equals("renderPreview"))
      getCurrentView().previewObject();
    else if (command.equals("editBone"))
      editJointCommand();
    else if (command.equals("deleteBone"))
      deleteJointCommand();
//    else if (command.equals("bindSkeleton"))
//      bindSkeletonCommand();
    else if (command.equals("setParentBone"))
      setJointParentCommand();
//    else if (command.equals("importSkeleton"))
//      importSkeletonCommand();
    else if (command.equals("grid"))
      setGridCommand();
    else if (command.equals("fourViews"))
      toggleViewsCommand();
    else if (command.equals("showTemplate"))
      {
        boolean wasShown = getCurrentView().getTemplateShown();
        getCurrentView().setShowTemplate(!wasShown);
        updateMenus();
        updateImage();
      }
    else if (command.equals("setTemplate"))
      setTemplateCommand();
    setCursor(Cursor.getDefaultCursor());
  }

  public void itemStateChanged(CommandEvent e)
  {
    Object source = e.getSource();
    BCheckBoxMenuItem sourceitem = null;
    String itemname = "";
    int i;

    if (source instanceof BCheckBoxMenuItem)
    {
      sourceitem = (BCheckBoxMenuItem)source;
      itemname = sourceitem.getName();
    }

    if ("tolerantSelection".equals(itemname))
      {
        for(i = 0; i < theView.length; ++i)
          ((TriMeshViewer) theView[i]).setTolerant(sourceitem.getState());
	return;
      }
    if ("freehandSelection".equals(itemname))
      {
        for(i = 0; i < theView.length; ++i)
          ((TriMeshViewer) theView[i]).setFreehandSelection(sourceitem.getState());
	return;
      }
    if ("displayAsQuads".equals(itemname))
      {
        for(i = 0; i < theView.length; ++i)
          ((TriMeshViewer) theView[i]).setQuadMode(sourceitem.getState());
        updateImage();
	return;
      }
    if ("detachSkeleton".equals(itemname))
      {
        for(i = 0; i < theView.length; ++i)
          ((TriMeshViewer) theView[i]).setSkeletonDetached(sourceitem.getState());
	return;
      }

    final String[] SMOOTHMETHODS= new String[]{"none", "shading", "interpolating", "approximating"};
    i = Utilities.findIndexEqual(SMOOTHMETHODS, itemname);
    if (i > -1)
    {
      setSmoothingMethod(i);
      return;
    }
    super.itemStateChanged(e);
  }

  /** Creates a new view, containing the same Object3D with a
   * new MeshEditSelection.
   */
  protected MeshEditorWindow createNewViewNewSelection()
  {
    TriMeshEditorWindow w = new TriMeshEditorWindow(parentWindow, getTitle(), objInConstructor, onClose, topology);
    // w.theView.setObject(theView.getObject().getObject3D());  TODO(MB) ??? Something with editGesture ???
    return w;
  }

  /** Create a MeshViewer which is appropriate for a particular editor window.
    Here a TriMeshViewer.*/
  
  public MeshViewer createMeshViewer(ObjectInfo obj, RowContainer p)
  {
    return new TriMeshViewer(obj,  p);
  }
    
  public void initBaseSelectionHolder(ObjectInfo obj)
  {
    baseSelHolder = new MeshSelectionHolder(obj);
    TriangleMesh mesh = (TriangleMesh) obj.object;
    baseSelHolder.setSelection(new boolean [mesh.getVertices().length]);
    baseSelHolder.setSelectionMode(TriMeshViewer.POINT_MODE);
    baseSelHolder.decRefCount();  // Start with ref count 0.
    
    for (int i = 0; i < theView.length; ++i)
      ((TriMeshViewer) theView[i]).setSelectionHolder(baseSelHolder);
  }
  
//  public void initWindowMenus(ObjectInfo obj)
//  {  
//    //dummy
//  }
  
  
  /** Create the toolbar panel on the left of an object editor. */
  
  public BorderContainer createToolbarPanel()
  {
    return new BorderContainer();   // Dummy
  }
  
  /* Select the entire mesh. */

  public void selectAllCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    boolean selected[] = tmv.getSelection();

    for (int i = 0; i < selected.length; i++)
      selected[i] = true;
    if (tmv.getSelectionMode() == TriMeshViewer.EDGE_MODE)
      for (int i = 0; i < selected.length; i++)
        if (tmv.isEdgeHidden(i))
          selected[i] = false;
    
    ((TriMeshViewer) getCurrentView()).setSelection(selected);
    ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
        ModelEvent.CHANGEDUR_OBJECTEDITOR);
  }

  /** Hide the selected part of the mesh. */
  
  public void hideSelectionCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    TriangleMesh theMesh = (TriangleMesh) tmv.getObject().object;
    boolean sel[] = tmv.getSelection();
    boolean hide[] = new boolean [theMesh.getFaces().length];
    if (tmv.getSelectionMode() == TriMeshViewer.FACE_MODE)
      System.arraycopy(sel, 0, hide, 0, sel.length);
    else if (tmv.getSelectionMode() == TriMeshViewer.EDGE_MODE)
    {
      TriangleMesh.Edge edge[] = theMesh.getEdges();
      for (int i = 0; i < sel.length; i++)
        if (sel[i])
          hide[edge[i].f1] = hide[edge[i].f2] = true;
    }
    else
    {
      TriangleMesh.Face face[] = theMesh.getFaces();
      for (int i = 0; i < face.length; i++)
        hide[i] = (sel[face[i].v1] || sel[face[i].v2] || sel[face[i].v3]);
    }
    boolean wasHidden[] = tmv.getHiddenFaces();
    if (wasHidden != null)
      for (int i = 0; i < wasHidden.length; i++)
        if (wasHidden[i])
          hide[i] = true;
    tmv.setHiddenFaces(hide);
    for (int i = 0; i < sel.length; i++)
      sel[i] = false;
    tmv.setSelection(sel);
  }
  
  /** Show all faces of the mesh. */
  
  public void showAllCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    tmv.setHiddenFaces(null);
  }
  
  /* Select the edges which form the boundary of the mesh. */

  public void selectBoundaryCommand()
  {
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    TriangleMesh.Edge edge[] = theMesh.getEdges();
    boolean selected[] = ((TriMeshViewer) getCurrentView()).getSelection();

    for (int i = 0; i < selected.length; i++)
    {
      selected[i] = (edge[i].f2 == -1);
    }
    
    ((TriMeshViewer) getCurrentView()).setSelection(selected);
    ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
        ModelEvent.CHANGEDUR_OBJECTEDITOR);
  }

  /* Extend the selection outward by one edge. */

  public void extendSelectionCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    int dist[] = tmv.getSelectionDistance();
    boolean selectedVert[] = new boolean [dist.length];
    boolean selected[] = tmv.getSelection();
    TriangleMesh.Edge edge[] = theMesh.getEdges();

    for (int i = 0; i < edge.length; i++)
      if ((dist[edge[i].v1] == 0 || dist[edge[i].v2] == 0) && !tmv.isEdgeHidden(i))
	selectedVert[edge[i].v1] = selectedVert[edge[i].v2] = true;
    if (tmv.getSelectionMode() == TriMeshViewer.POINT_MODE)
    {
      ((TriMeshViewer) getCurrentView()).setSelection(selected);
      ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
          ModelEvent.CHANGEDUR_OBJECTEDITOR);
    }
    else if (tmv.getSelectionMode() == TriMeshViewer.EDGE_MODE)
      {
	for (int i = 0; i < edge.length; i++)
	  selected[i] = (selectedVert[edge[i].v1] && selectedVert[edge[i].v2]);
        
        ((TriMeshViewer) getCurrentView()).setSelection(selected);
        ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
            ModelEvent.CHANGEDUR_OBJECTEDITOR);
      }
    else
      {
	TriangleMesh.Face face[] = theMesh.getFaces();
	for (int i = 0; i < face.length; i++)
	  selected[i] = (selectedVert[face[i].v1] && selectedVert[face[i].v2] && selectedVert[face[i].v3]);

        ((TriMeshViewer) getCurrentView()).setSelection(selected);
        ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
            ModelEvent.CHANGEDUR_OBJECTEDITOR);
      }
  }

  /** Delete the selected points, edges, or faces from the mesh. */

  public void deleteCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    TriangleMesh.Vertex vert[] = (Vertex []) theMesh.getVertices();
    TriangleMesh.Edge edge[] = theMesh.getEdges();
    TriangleMesh.Face face[] = theMesh.getFaces();
    boolean selected[] = tmv.getSelection();
    boolean deleteVert[] = new boolean [vert.length];
    boolean deleteEdge[] = new boolean [edge.length];
    boolean deleteFace[] = new boolean [face.length];

    // Determine which parts of the mesh to delete.

    if (tmv.getSelectionMode() == TriMeshViewer.POINT_MODE)
      {
	for (int i = 0; i < deleteVert.length; i++)
	  deleteVert[i] = selected[i];
	for (int i = 0; i < deleteEdge.length; i++)
	  deleteEdge[i] = (deleteVert[edge[i].v1] || deleteVert[edge[i].v2]);
	for (int i = 0; i < deleteFace.length; i++)
	  deleteFace[i] = (deleteVert[face[i].v1] || deleteVert[face[i].v2] || deleteVert[face[i].v3]);
      }
    else if (tmv.getSelectionMode() == TriMeshViewer.EDGE_MODE)
      {
	for (int i = 0; i < deleteFace.length; i++)
	  deleteFace[i] = (selected[face[i].e1] || selected[face[i].e2] || selected[face[i].e3]);
	for (int i = 0; i < deleteEdge.length; i++)
	  deleteEdge[i] = (deleteFace[edge[i].f1] && (edge[i].f2 == -1 || deleteFace[edge[i].f2]));
	for (int i = 0; i < deleteVert.length; i++)
	  deleteVert[i] = vert[i].edges > 0;  // Already "orphaned" vertices are never deleted
	for (int i = 0; i < deleteFace.length; i++)
	  if (!deleteFace[i])
	    deleteVert[face[i].v1] = deleteVert[face[i].v2] = deleteVert[face[i].v3] = false;
      }
    else
      {
	for (int i = 0; i < deleteFace.length; i++)
	  deleteFace[i] = selected[i];
	for (int i = 0; i < deleteEdge.length; i++)
	  deleteEdge[i] = (deleteFace[edge[i].f1] && (edge[i].f2 == -1 || deleteFace[edge[i].f2]));
	for (int i = 0; i < deleteVert.length; i++)
	  deleteVert[i] = vert[i].edges > 0;  // Already "orphaned" vertices are never deleted
	for (int i = 0; i < deleteFace.length; i++)
	  if (!deleteFace[i])
	    deleteVert[face[i].v1] = deleteVert[face[i].v2] = deleteVert[face[i].v3] = false;
      }

    // Make sure this will still be a valid object.
    
    int breaks = 0;
    
    for (int i = 0; i < vert.length; i++)
      {
	int e[] = theMesh.getEdgesForVertex(vert[i]);
        if (e.length == 0)
          continue;

	int f, fprev = edge[e[0]].f1;
        breaks = 0;
	for (int j = 1; j < e.length; j++)
	  {
	    f = (edge[e[j]].f1 == fprev ? edge[e[j]].f2 : edge[e[j]].f1);
	    if (f == -1)
	      break;
	    if (!deleteFace[fprev] && deleteFace[f])
	      breaks++;
	    fprev = f;
	  }
	if (!deleteFace[fprev] && (edge[e[0]].f2 == -1 || deleteFace[edge[e[0]].f1]))
	  breaks++;
	if (breaks > 1)
          break;
    }
    
    if (breaks > 1)
    {
      BStandardDialog dlg = new BStandardDialog("Illegal surface", new String [] {"Deleting the selection will",
        "result in an illegal surface."}, BStandardDialog.QUESTION);
      
      if (dlg.showOptionDialog(this, new String [] {Translate.text("button.ok"),
          Translate.text("button.cancel")}, Translate.text("button.cancel")) == 1)
        return;
    }

    DeleteMarkedItemsListOp vertDeleteOp = new DeleteMarkedItemsListOp(deleteVert);
    DeleteMarkedItemsListOp faceDeleteOp = new DeleteMarkedItemsListOp(deleteFace);
    
    Vertex v[] = (Vertex[])vertDeleteOp.apply(vert);
    Face f[] = (Face[])Utilities.deepArrayCopy((Copieable[])faceDeleteOp.apply(face));
    
    // Adjust vertex indices in face
    int newVertIndex[] = vertDeleteOp.getMappingOldToNew();
    
    for(int i = 0; i < f.length; ++i)
    {
      f[i].v1 = newVertIndex[f[i].v1];
      f[i].v2 = newVertIndex[f[i].v2];
      f[i].v3 = newVertIndex[f[i].v3];
    }    
    
//    // Find the new lists of vertices and faces.
//
//    int newVertCount = 0, newFaceCount = 0;
//    int newVertIndex[] = new int [vert.length];
//    for (int i = 0; i < deleteVert.length; i++)
//      {
//	newVertIndex[i] = -1;
//	if (!deleteVert[i])
//	  newVertCount++;
//      }
//    for (int i = 0; i < deleteFace.length; i++)
//      if (!deleteFace[i])
//	newFaceCount++;
//    Vertex v[] = new Vertex [newVertCount];
//    int f[][] = new int [newFaceCount][];
//    newVertCount = 0;
//    for (int i = 0; i < vert.length; i++)
//      if (!deleteVert[i])
//	{
//	  newVertIndex[i] = newVertCount;
//	  v[newVertCount++] = vert[i];
//	}
//    newFaceCount = 0;
//    for (int i = 0; i < face.length; i++)
//      if (!deleteFace[i])
//	f[newFaceCount++] = new int [] {newVertIndex[face[i].v1], newVertIndex[face[i].v2], newVertIndex[face[i].v3]};



    // Update the texture parameters.

    ParameterValue oldParamVal[] = theMesh.getParameterValues();
    ParameterValue newParamVal[] = new ParameterValue [oldParamVal.length];
    for (int i = 0; i < oldParamVal.length; i++)
    {
      if (oldParamVal[i] instanceof VertexParameterValue)
      {
        double oldval[] = ((VertexParameterValue) oldParamVal[i]).getValue();
        newParamVal[i] = new VertexParameterValue(vertDeleteOp.apply(oldval));
//        double newval[] = new double [newVertCount];
//        for (int j = 0, k = 0; j < oldval.length; j++)
//          if (!deleteVert[j])
//            newval[k++] = oldval[j];
//        newParamVal[i] = new VertexParameterValue(newval);
      }
      else if (oldParamVal[i] instanceof FaceParameterValue)
      {
        double oldval[] = ((FaceParameterValue) oldParamVal[i]).getValue();
        newParamVal[i] = new FaceParameterValue(faceDeleteOp.apply(oldval));        
//        double newval[] = new double [newFaceCount];
//        for (int j = 0, k = 0; j < oldval.length; j++)
//          if (!deleteFace[j])
//            newval[k++] = oldval[j];
//        newParamVal[i] = new FaceParameterValue(newval);
      }
      else if (oldParamVal[i] instanceof FaceVertexParameterValue)
      {
        double oldval[][] = ((FaceVertexParameterValue) oldParamVal[i]).getValue();
        double newval[][] = new double [3][];
        for (int j = 0; j < 3; ++j)
          newval[j] = faceDeleteOp.apply(oldval[j]);
        
//        double newval[][] = new double [3][newFaceCount];
//        for (int j = 0, k = 0; j < oldval[0].length; j++)
//          if (!deleteFace[j])
//          {
//            newval[0][k] = oldval[0][j];
//            newval[1][k] = oldval[1][j];
//            newval[2][k] = oldval[2][j];
//            k++;
//          }
        newParamVal[i] = new FaceVertexParameterValue(newval);
      }
      else
        newParamVal[i] = oldParamVal[i].duplicate();
    }
    
    // Construct the new mesh.
    
    TriangleMesh newmesh = new TriangleMesh(v, f);
    Vertex newvert[] = (Vertex []) newmesh.getVertices();
    Edge newedge[] = newmesh.getEdges();
    newmesh.getSkeleton().copy(theMesh.getSkeleton());
    newmesh.copyTextureAndMaterial(theMesh);
    newmesh.setSmoothingMethod(theMesh.getSmoothingMethod());
    newmesh.setParameterValues(newParamVal);

    // Copy over the smoothness values for edges.

    for (int i = 0; i < edge.length; i++)
      {
        int j = newmesh.findEdgeByVertexSet(newVertIndex[edge[i].v1],
            newVertIndex[edge[i].v2]);
        if (j >= 0)
          newedge[j].smoothness = edge[i].smoothness;
//	for (int j = 0; j < newedge.length; j++)
//	  if ((r1 == newedge[j].v1 && r2 == newedge[j].v2) || (r1 == newedge[j].v2 && r2 == newedge[j].v1))
//	    newedge[j].smoothness = edge[i].smoothness;
      }
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    theMesh.copyObject(newmesh);
    object3DChangedDuringEditor();
  }

  /* Subdivide selected edges or faces of the mesh. */

  public void subdivideCommand()
  {
    int i, j;
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    TriangleMesh theMesh = (TriangleMesh) tmv.getObject().object, newmesh;
    boolean selected[] = tmv.getSelection(), newselection[];
    Edge edges[];
    Face faces[];

    if (tmv.getSelectionMode() != TriMeshViewer.EDGE_MODE &&
        tmv.getSelectionMode() != TriMeshViewer.FACE_MODE)
      return;
    for (i = 0; !selected[i] && i < selected.length; i++);
    if (i == selected.length)
      return;

    if (tmv.getSelectionMode() == TriMeshViewer.EDGE_MODE)
      {
	// Subdivide selected edges, using the appropriate method.

	i = theMesh.getVertices().length;
        newmesh = TriangleMesh.subdivideEdges(theMesh, selected, Double.MAX_VALUE);
//	if (theMesh.getSmoothingMethod() == TriangleMesh.APPROXIMATING)
//	  newmesh = TriangleMesh.subdivideLoop(theMesh, selected, Double.MAX_VALUE);
//	else if (theMesh.getSmoothingMethod() == TriangleMesh.INTERPOLATING)
//	  newmesh = TriangleMesh.subdivideButterfly(theMesh, selected, Double.MAX_VALUE);
//	else
//	  newmesh = TriangleMesh.subdivideLinear(theMesh, selected);
	setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
	theMesh.copyObject(newmesh);
	object3DChangedDuringEditor();

	// Update the selection.

	edges = newmesh.getEdges();
	newselection = new boolean [edges.length];
	for (j = 0; j < edges.length; j++)
	  newselection[j] = (edges[j].v1 >= i || edges[j].v2 >= i);

        ((TriMeshViewer) getCurrentView()).setSelection(newselection);
        ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
            ModelEvent.CHANGEDUR_OBJECTEDITOR);
      }
    else
      {
	// Subdivide selected faces.

	i = theMesh.getVertices().length;
	newmesh = TriangleMesh.subdivideFaces(theMesh, selected);
	setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
	theMesh.copyObject(newmesh);
	object3DChangedDuringEditor();

	// Update the selection.

	faces = newmesh.getFaces();
	newselection = new boolean [faces.length];
	for (j = 0; j < faces.length; j++)
	  newselection[j] = (faces[j].v1 >= i || faces[j].v2 >= i || faces[j].v3 >= i);

        ((TriMeshViewer) getCurrentView()).setSelection(newselection);
        ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
            ModelEvent.CHANGEDUR_OBJECTEDITOR);
      }
  }

  public void simplifyCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    TriangleMesh theMesh = (TriangleMesh) tmv.getObject().object;
    boolean selection[] = tmv.getSelection();
    int selectionMode = tmv.getSelectionMode();
    ValueField errorField = new ValueField(0.01, ValueField.NONNEGATIVE);
    int i;

    ComponentsDialog dlg = new ComponentsDialog(this, "Specify tolerance for simplified mesh:",
    	new Widget[] {errorField}, new String[] {"Max Surface Error"});
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));

    // If we are not in Edge selection mode, convert the selection to edges.

    if (selectionMode == TriMeshViewer.POINT_MODE)
    {
      Edge e[] = theMesh.getEdges();
      boolean newSel[] = new boolean [e.length];
      for (i = 0; i < e.length; i++)
        newSel[i] = (selection[e[i].v1] && selection[e[i].v2]);
      selection = newSel;
    }
    if (selectionMode == TriMeshViewer.FACE_MODE)
    {
      Edge e[] = theMesh.getEdges();
      boolean newSel[] = new boolean [e.length];
      for (i = 0; i < e.length; i++)
        newSel[i] = (selection[e[i].f1] || selection[e[i].f2]);
      selection = newSel;
    }

    // If no edges are selected, then simplify the entire mesh.

    for (i = 0; i < selection.length && !selection[i]; i++);
    if (i == selection.length)
      {
        selection = new boolean [selection.length];
        for (i = 0; i < selection.length; i++)
          selection[i] = true;
      }

    // Generate the simplified mesh.

    new TriMeshSimplifier(theMesh, selection, errorField.getValue(), this);
    object3DChangedDuringEditor();
  }

  public void optimizeCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    MessageDialog dlg = new MessageDialog(this, "Optimize mesh connectivity?  This will rearrange the mesh edges to produce a smoother surface.",
        new String [] {Translate.text("button.ok"), Translate.text("button.cancel")});
    if (dlg.getChoice() == 1)
      return;
    TriangleMesh theMesh = (TriangleMesh) tmv.getObject().object;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    theMesh.copyObject(TriangleMesh.optimizeMesh(theMesh));
    for (int i = 0; i < theView.length; ++i)
      theView[i].setMesh(theMesh);
    
    boolean selection[] = ((TriMeshViewer) getCurrentView()).getSelection();
    for (int i = 0; i < selection.length; i++)
      selection[i] = false;

    ((TriMeshViewer) getCurrentView()).setSelection(selection);
    ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
        ModelEvent.CHANGEDUR_OBJECTEDITOR);
    object3DChangedDuringEditor();
  }

  public void createFaceCommand()
  {
    boolean selection[] = ((TriMeshViewer) getCurrentView()).getSelection();
    int sel[] = Utilities.createIndexList(selection);

    if (sel.length != 3)
      return;

    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    if (theMesh.addNewFace(sel[0], sel[1], sel[2]) == -1)
    {
      MessageDialog dlg = new MessageDialog(this,
        new String [] {"An edge can't be adjacent to more than",
        "two faces. New face not created."},
        new String [] {Translate.text("button.ok")});
      return;
    }
    object3DChangedDuringEditor();
  }
  
  public void repairGeometryCommand()
  {
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    theMesh.repairGeometry();
    object3DChangedDuringEditor();
  }
    

//  public void bevelCommand()
//  {
//    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
//    final TriangleMesh theMesh = (TriangleMesh) tmv.getObject().object;
//    Face face[] = theMesh.getFaces();
//    final boolean selection[] = tmv.getSelection();
//    final ValueField heightField = new ValueField(0.0, ValueField.NONE), widthField = new ValueField(0.0, ValueField.NONE);
//    final Choice applyChoice = new Choice();
//    final ObjectPreviewCanvas preview = new ObjectPreviewCanvas(new ObjectInfo(theMesh, new CoordinateSystem(), ""));
//    final int bevelMode[];
//    int selectionMode = tmv.getSelectionMode();
//
//    if (selectionMode == TriMeshViewer.FACE_MODE)
//      bevelMode = new int [] {TriMeshBeveler.BEVEL_FACE_GROUPS, TriMeshBeveler.BEVEL_FACES};
//    else if (selectionMode == TriMeshViewer.POINT_MODE)
//      bevelMode = new int [] {TriMeshBeveler.BEVEL_VERTICES, TriMeshBeveler.BEVEL_VERTICES};
//    else
//      bevelMode = new int [] {TriMeshBeveler.BEVEL_EDGES, TriMeshBeveler.BEVEL_EDGES};
//    applyChoice.add("Selection as a Whole");
//    applyChoice.add("Individual Faces");
//    applyChoice.addItemListener(new ItemListener() {
//      public void itemStateChanged(ItemEvent ev)
//      {
//        TriMeshBeveler beveler = new TriMeshBeveler(theMesh, selection, bevelMode[applyChoice.getSelectedIndex()]);
//        double height = heightField.getValue();
//        double width = widthField.getValue();
//        preview.setObject(beveler.bevelMesh(height, width));
//        preview.updateImage();
//        preview.repaint();
//      }
//    });
//    TextListener listener = new TextListener() {
//      public void textValueChanged(TextEvent ev)
//      {
//        TriMeshBeveler beveler = new TriMeshBeveler(theMesh, selection, bevelMode[applyChoice.getSelectedIndex()]);
//        double height = heightField.getValue();
//        double width = widthField.getValue();
//        preview.setObject(beveler.bevelMesh(height, width));
//        preview.updateImage();
//        preview.repaint();
//      }
//    };
//    heightField.addTextListener(listener);
//    widthField.addTextListener(listener);
//    preview.setPreferredSize(200, 200);
//    ComponentsDialog dlg;
//    if (selectionMode == TriMeshViewer.FACE_MODE)
//      dlg = new ComponentsDialog(this, "Bevel/Extrude Selected Faces:",
//    	new Component[] {heightField, widthField, applyChoice, preview},
//          new String [] {"Extrude Height", "Bevel Width", "Apply To", ""});
//    else if (selectionMode == TriMeshViewer.POINT_MODE)
//      dlg = new ComponentsDialog(this, "Bevel/Extrude Selected Points:",
//          new Component[] {heightField, widthField, preview}, 
//          new String [] {"Extrude Height", "Bevel Width", ""});
//    else
//      dlg = new ComponentsDialog(this, "Bevel/Extrude Selected Edges:",
//          new Component[] {heightField, widthField, preview}, 
//          new String [] {"Extrude Height", "Bevel Width", ""});
//    if (!dlg.clickedOk())
//      return;
//    double height = heightField.getValue();
//    double width = widthField.getValue();
//    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
//
//    // Generate the new mesh.
//
//    TriMeshBeveler beveler = new TriMeshBeveler(theMesh, selection, bevelMode[applyChoice.getSelectedIndex()]);
//    theMesh.copyObject(beveler.bevelMesh(height, width));
//
//    object3DChangedDuringEditor();
//    ((TriMeshViewer) getCurrentView()).setSelection(beveler.getNewSelection());
//    ((TriMeshViewer) getCurrentView()).informSelectionChanged(this,
//        ModelEvent.CHANGEDUR_OBJECTEDITOR);
//  }

  void bevelCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    final TriangleMesh theMesh = (TriangleMesh)tmv.getObject().object;
    Face face[] = theMesh.getFaces();
    final boolean selection[] = tmv.getSelection();
    final ValueField heightField = new ValueField(0.0, ValueField.NONE), widthField = new ValueField(0.0, ValueField.NONE);
    final ObjectPreviewCanvas preview = new ObjectPreviewCanvas(new ObjectInfo(theMesh, new CoordinateSystem(), ""));
    final int bevelMode[];
    int selectionMode = tmv.getSelectionMode();
    if (selectionMode == TriMeshViewer.FACE_MODE)
      bevelMode = new int [] {TriMeshBeveler.BEVEL_FACE_GROUPS, TriMeshBeveler.BEVEL_FACES};
    else if (selectionMode == TriMeshViewer.POINT_MODE)
      bevelMode = new int [] {TriMeshBeveler.BEVEL_VERTICES, TriMeshBeveler.BEVEL_VERTICES};
    else
      bevelMode = new int [] {TriMeshBeveler.BEVEL_EDGES, TriMeshBeveler.BEVEL_EDGES};
    final BComboBox applyChoice = new BComboBox(new String [] {
      Translate.text("selectionAsWhole"),
      Translate.text("individualFaces")
    });
    applyChoice.addEventLink(ValueChangedEvent.class, new Object() {
      void processEvent()
      {
        TriMeshBeveler beveler = new TriMeshBeveler(theMesh, selection, bevelMode[applyChoice.getSelectedIndex()]);
        double height = heightField.getValue();
        double width = widthField.getValue();
        preview.setObject(beveler.bevelMesh(height, width));
        preview.updateImage();
        preview.repaint();
      }
    });
    Object listener = new Object() {
      void processEvent()
      {
        TriMeshBeveler beveler = new TriMeshBeveler(theMesh, selection, bevelMode[applyChoice.getSelectedIndex()]);
        double height = heightField.getValue();
        double width = widthField.getValue();
        preview.setObject(beveler.bevelMesh(height, width));
        preview.updateImage();
        preview.repaint();
      }
    };
    heightField.addEventLink(ValueChangedEvent.class, listener);
    widthField.addEventLink(ValueChangedEvent.class, listener);
    preview.setPreferredSize(new Dimension(200, 200));
    ComponentsDialog dlg;
    if (selectionMode == TriMeshViewer.FACE_MODE)
      dlg = new ComponentsDialog(this, Translate.text("bevelFacesTitle"),
          new Widget[] {heightField, widthField, applyChoice, preview},
          new String [] {Translate.text("extrudeHeight"), Translate.text("bevelWidth"), Translate.text("applyTo"), ""});
    else if (selectionMode == TriMeshViewer.POINT_MODE)
      dlg = new ComponentsDialog(this, Translate.text("bevelPointsTitle"),
          new Widget[] {heightField, widthField, preview},
          new String [] {Translate.text("extrudeHeight"), Translate.text("bevelWidth"), ""});
    else
      dlg = new ComponentsDialog(this, Translate.text("bevelEdgesTitle"),
          new Widget[] {heightField, widthField, preview},
          new String [] {Translate.text("extrudeHeight"), Translate.text("bevelWidth"), ""});
    if (!dlg.clickedOk())
      return;
    double height = heightField.getValue();
    double width = widthField.getValue();
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));

    // Generate the new mesh.

    TriMeshBeveler beveler = new TriMeshBeveler(theMesh, selection, bevelMode[applyChoice.getSelectedIndex()]);
    theMesh.copyObject(beveler.bevelMesh(height, width));
    object3DChangedDuringEditor();
    tmv.setSelection(beveler.getNewSelection());
    tmv.informSelectionChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITOR);
  }

  
  
  /**
   * Given the ordered list of edges for a selected boundary segment, determine whether they form
   * a closed boundary.
   */
  
  private boolean isBoundaryClosed(int edges[])
  {
    if (edges.length < 3)
      return false;
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    Edge ed[] = theMesh.getEdges();
    Edge first = ed[edges[0]];
    Edge last = ed[edges[edges.length-1]];
    return (first.v1 == last.v1 || first.v1 == last.v2 || first.v2 == last.v1 || first.v2 == last.v2);
  }
  
  public void closeBoundaryCommand()
  {
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    Vertex vt[] = (Vertex []) theMesh.getVertices();
    Edge ed[] = theMesh.getEdges();
    Face fc[] = theMesh.getFaces();
    int boundary[][] = ((TriMeshViewer) getCurrentView()).findSelectedBoundaries();
    
    AppendItemsListOp vertAppendOp = new AppendItemsListOp(boundary.length);
    AppendItemsListOp faceAppendOp = new AppendItemsListOp();
    
//    Vertex newvert[] = new Vertex [vt.length+boundary.length];
//    int newface[][], count = 0;

    // First, add all of the current vertices and faces to the lists.

//    for (int i = 0; i < vt.length; i++)
//      newvert[i] = vt[i].r;
//    for (int i = 0; i < boundary.length; i++)
//      count += boundary[i].length;
//    newface = new int [fc.length+count][];
//    for (int i = 0; i < fc.length; i++)
//      newface[i] = new int [] {fc[i].v1, fc[i].v2, fc[i].v3};
//    count = fc.length;

    // For each boundary, add a single vertex in the center, and faces surrounding it.

    for (int i = 0; i < boundary.length; i++)
      {
        if (boundary[i].length < 2)
          continue;
        Edge ed0 = ed[boundary[i][0]];
        Edge ed1 = ed[boundary[i][1]];
        // j = index of not shared vertex of ed0 with ed1
        int j = (ed0.v1 == ed1.v1 || ed0.v1 == ed1.v2 ? ed0.v2 : ed0.v1);
        Vec3 center = new Vec3();
        for (int k = 0; k < boundary[i].length; k++)
          {
            center.add(vt[j].r);
            Edge e = ed[boundary[i][k]];
            j = (e.v1 == j ? e.v2 : e.v1);
          }
        if (isBoundaryClosed(boundary[i]))
          center.scale(1.0/boundary[i].length);
        else
        {
          center.add(vt[j].r);
          center.scale(1.0/(boundary[i].length+1));
        }
//        newvert[vt.length+i] = center;
        vertAppendOp.add(new Vertex(center));
        for (int k = 0; k < boundary[i].length; k++)
          {
            Edge e = ed[boundary[i][k]];
            Face f = fc[e.f1];
            if ((f.v1 == e.v1 && f.v2 == e.v2) || (f.v2 == e.v1 && f.v3 == e.v2) || (f.v3 == e.v1 && f.v1 == e.v2))
              faceAppendOp.add(new Face(e.v2, e.v1, vt.length+i));
//              newface[count++] = new int [] {e.v2, e.v1, vt.length+i};
            else
              faceAppendOp.add(new Face(e.v2, e.v1, vt.length+i));
//              newface[count++] = new int [] {e.v1, e.v2, vt.length+i};
          }
      }

    // Update the texture parameters.
    
    TextureParameter param[] = theMesh.getParameters();
    ParameterValue oldParamVal[] = theMesh.getParameterValues();
    ParameterValue newParamVal[] = new ParameterValue [oldParamVal.length];
    for (int i = 0; i < oldParamVal.length; i++)
    {
      double defvalue = param[i].defaultVal;

      if (oldParamVal[i] instanceof VertexParameterValue)
      {
        double oldval[] = ((VertexParameterValue) oldParamVal[i]).getValue();
        AppendDefaultItemListOp addop =
            new AppendDefaultItemListOp(defvalue, vertAppendOp.getAddSize());
        newParamVal[i] = new VertexParameterValue(addop.apply(oldval));
        
//        double newval[] = new double [newvert.length];
//        for (int j = 0; j < oldval.length; j++)
//          newval[j] = oldval[j];
//        for (int j = oldval.length; j < newval.length; j++)
//          newval[j] = param[i].defaultVal;
//        newParamVal[i] = new VertexParameterValue(newval);
      }
      else if (oldParamVal[i] instanceof FaceParameterValue)
      {
        double oldval[] = ((FaceParameterValue) oldParamVal[i]).getValue();
        AppendDefaultItemListOp addop =
            new AppendDefaultItemListOp(defvalue, faceAppendOp.getAddSize());
        newParamVal[i] = new FaceParameterValue(addop.apply(oldval));

//        double oldval[] = ((FaceParameterValue) oldParamVal[i]).getValue();
//        double newval[] = new double [newface.length];
//        for (int j = 0; j < oldval.length; j++)
//          newval[j] = oldval[j];
//        for (int j = oldval.length; j < newval.length; j++)
//          newval[j] = param[i].defaultVal;
//        newParamVal[i] = new FaceParameterValue(newval);
      }
      else if (oldParamVal[i] instanceof FaceVertexParameterValue)
      {
        double oldval[][] = ((FaceVertexParameterValue) oldParamVal[i]).getValue();
        double newval[][] = new double [3][];
        AppendDefaultItemListOp addop =
            new AppendDefaultItemListOp(defvalue, faceAppendOp.getAddSize());
        for (int j = 0; j < 3; ++j)
          newval[j] = addop.apply(oldval[j]);

//        double oldval[][] = ((FaceVertexParameterValue) oldParamVal[i]).getValue();
//        double newval[][] = new double [3][newface.length];
//        for (int j = 0; j < 3; j++)
//        {
//          for (int k = 0; k < oldval[0].length; k++)
//            newval[j][k] = oldval[j][k];
//          for (int k = oldval[0].length; k < newface.length; k++)
//            newval[j][k] = param[i].defaultVal;
//        }
        newParamVal[i] = new FaceVertexParameterValue(newval);
      }
      else
        newParamVal[i] = oldParamVal[i].duplicate();
    }
    // Create the new mesh.

    Vertex newvert[] = (Vertex[])vertAppendOp.apply(vt);
    Face newface[] = (Face[])faceAppendOp.apply(fc);
    
    TriangleMesh newmesh = new TriangleMesh(newvert, newface);
    Vertex newvt[] = (Vertex []) newmesh.getVertices();
    Edge newed[] = newmesh.getEdges();
//    newmesh.getSkeleton().copy(theMesh.getSkeleton());  // (MB) Not necessary?
    newmesh.copyTextureAndMaterial(theMesh);
    newmesh.setSmoothingMethod(theMesh.getSmoothingMethod());
    newmesh.setParameterValues(newParamVal);
    
    // Copy over the smoothness values.

    
    // Copy over the smoothness values for edges.

    for (int i = 0; i < vt.length; i++)
      newvt[i].smoothness = vt[i].smoothness;
    for (int i = 0; i < newed.length; i++)
      {
	if (newed[i].v1 >= vt.length || newed[i].v2 >= vt.length)
	  continue;
	for (int j = 0; j < ed.length; j++)
	  if ((newed[i].v1 == ed[j].v1 && newed[i].v2 == ed[j].v2) || (newed[i].v1 == ed[j].v2 && newed[i].v2 == ed[j].v1))
	    newed[i].smoothness = ed[j].smoothness;
      }
    
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    theMesh.copyObject(newmesh);
    object3DChangedDuringEditor();
  }

//  public void joinBoundariesCommand()
//  {
//    final int boundary[][] = ((TriMeshViewer) getCurrentView()).findSelectedBoundaries();  
//    if (boundary.length != 2)
//      return;
//    final int boundaryVert[][] = new int [2][];
//    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
//    Vertex vt[] = (Vertex []) theMesh.getVertices();
//    Edge ed[] = theMesh.getEdges();
//    boolean closed = isBoundaryClosed(boundary[0]);
//    if (closed != isBoundaryClosed(boundary[1]))
//    {
//      new MessageDialog(this, "You cannot join an open boundary curve to a closed one.");
//      return;
//    }
//
//    // Construct a list of the vertices on each boundary;
//
//    for (int j = 0; j < 2; j++)
//    {
//      boundaryVert[j] = new int [boundary[j].length + (closed ? 0 : 1)];
//      if (boundary[j].length == 1 || ed[boundary[j][0]].v1 == ed[boundary[j][1]].v1 || ed[boundary[j][0]].v1 == ed[boundary[j][1]].v2)
//        boundaryVert[j][0] = ed[boundary[j][0]].v2;
//      else
//        boundaryVert[j][0] = ed[boundary[j][0]].v1;
//      for (int i = 0; i < boundaryVert[j].length-1; i++)
//        {
//          Edge e = ed[boundary[j][i]];
//          boundaryVert[j][i+1] = (e.v1 == boundaryVert[j][i] ? e.v2 : e.v1);
//        }
//    }
//
//    // Consider all possible ways of joining the two boundaries, and select the one which
//    // gives the lowest mean squared distance between connected points as the initial guess.
//
//    double offset = 0.0;
//    boolean reverse = false;
//    int maxsteps = (boundary[0].length > boundary[1].length ? boundary[0].length : boundary[1].length);
//    if (closed)
//    {
//      double step0 = boundary[0].length/((double) maxsteps);
//      double step1 = (reverse ? -1.0 : 1.0)*boundary[1].length/((double) maxsteps);
//      double mindist = Double.MAX_VALUE;
//      for (int i = 0; i < maxsteps-1; i++)
//        {
//          double dist = 0.0;
//          for (int j = 0; j < maxsteps; j++)
//            {
//              double p0 = j*step0, p1 = (i+j)*step1;
//              int i0 = ((int) Math.round(p0)+boundary[0].length) % boundary[0].length;
//              int i1 = ((int) Math.round(p1)+boundary[1].length) % boundary[1].length;
//              dist += vt[boundaryVert[0][i0]].r.distance2(vt[boundaryVert[1][i1]].r);
//            }
//          if (dist < mindist)
//            {
//              mindist = dist;
//              offset = i*step1;
//              reverse = false;
//            }
//          dist = 0.0;
//          for (int j = 0; j < maxsteps; j++)
//            {
//              double p0 = j*step0, p1 = (i-j)*step1;
//              int i0 = ((int) Math.round(p0)+boundary[0].length) % boundary[0].length;
//              int i1 = ((int) Math.round(p1)+boundary[1].length) % boundary[1].length;
//              dist += vt[boundaryVert[0][i0]].r.distance2(vt[boundaryVert[1][i1]].r);
//            }
//          if (dist < mindist)
//            {
//              mindist = dist;
//              offset = i*step1;
//              reverse = true;
//            }
//        }
//    }
//    else
//    {
//      double fdist = 0.0, rdist = 0.0;
//      double step0 = boundary[0].length/((double) maxsteps);
//      double step1 = boundary[1].length/((double) maxsteps);
//      int revStart = boundaryVert[1].length-1;
//      for (int i = 0; i < maxsteps; i++)
//      {
//        int i0 = (int) Math.round(i*step0);
//        int i1 = (int) Math.round(i*step1);
//        fdist += vt[boundaryVert[0][i0]].r.distance2(vt[boundaryVert[1][i1]].r);
//        rdist += vt[boundaryVert[0][i0]].r.distance2(vt[boundaryVert[1][revStart-i1]].r);
//      }
//      reverse = (fdist > rdist);
//    }
//
//    // Create a dialog allowing the user to adjust the parameters.
//
//    final ValueSlider offsetSlider = new ValueSlider(-0.5*boundary[1].length, 0.5*boundary[1].length, 2*maxsteps, 0.0);
//    final Checkbox reverseBox = new Checkbox("Reverse Direction", false);
//    final ObjectPreviewCanvas preview = new ObjectPreviewCanvas(new ObjectInfo(doJoinBoundaries(boundary,
//      boundaryVert, offset, reverse), new CoordinateSystem(), ""));
//    final double baseOffset = offset;
//    final boolean baseReverse = reverse;
//    
//    
//    
//    Panel p = new Panel();
//    GridBagConstraints gc = new GridBagConstraints();
//    p.setLayout(new GridBagLayout());
//    if (closed)
//    {
//      gc.gridy = 0;
//      p.add(new Label("Offset: "), gc);
//      gc.fill = gc.HORIZONTAL;
//      gc.anchor = gc.WEST;
//      p.add(offsetSlider, gc);
//    }
//    gc.gridy = 1;
//    gc.gridwidth = 2;
//    gc.fill = gc.NONE;
//    gc.anchor = gc.CENTER;
//    p.add(reverseBox, gc);
//    gc.gridy = 2;
//    gc.fill = gc.BOTH;
//    gc.weightx = gc.weighty = 1.0;
//    p.add(preview, gc);
//    preview.setPreferredSize(200, 200);
//    preview.setRenderMode(preview.RENDER_FLAT);
//    offsetSlider.addAdjustmentListener(new AdjustmentListener() {
//      public void adjustmentValueChanged(AdjustmentEvent ev)
//      {
//        preview.setObject(doJoinBoundaries(boundary, boundaryVert, baseOffset+offsetSlider.getValue(), baseReverse^reverseBox.getState()));
//        preview.updateImage();
//        preview.repaint();
//      }
//    } );
//    reverseBox.addItemListener(new ItemListener() {
//      public void itemStateChanged(ItemEvent ev)
//      {
//        preview.setObject(doJoinBoundaries(boundary, boundaryVert, baseOffset+offsetSlider.getValue(), baseReverse^reverseBox.getState()));
//        preview.updateImage();
//        preview.repaint();
//      }
//    } );
//    PanelDialog dlg = new PanelDialog(this, "Select parameters for joining the boundary curves:", p);
//    if (!dlg.clickedOk())
//      return;
//    
//    
//    
//    
//    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
//    theMesh.copyObject(preview.getObject().object);
//    object3DChangedDuringEditor();
//}

  
  void joinBoundariesCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    final int boundary[][] = tmv.findSelectedBoundaries();
    if (boundary.length != 2)
      return;
    final int boundaryVert[][] = new int [2][];
    TriangleMesh theMesh = (TriangleMesh)tmv.getObject().object;
    Vertex vt[] = (Vertex []) theMesh.getVertices();
    Edge ed[] = theMesh.getEdges();
    boolean closed = isBoundaryClosed(boundary[0]);
    if (closed != isBoundaryClosed(boundary[1]))
    {
      new BStandardDialog("", Translate.text("cannotJoinOpenAndClosed"), BStandardDialog.ERROR).showMessageDialog(this);
      return;
    }

    // Construct a list of the vertices on each boundary;

    for (int j = 0; j < 2; j++)
    {
      boundaryVert[j] = new int [boundary[j].length + (closed ? 0 : 1)];
      if (boundary[j].length == 1 || ed[boundary[j][0]].v1 == ed[boundary[j][1]].v1 || ed[boundary[j][0]].v1 == ed[boundary[j][1]].v2)
        boundaryVert[j][0] = ed[boundary[j][0]].v2;
      else
        boundaryVert[j][0] = ed[boundary[j][0]].v1;
      for (int i = 0; i < boundaryVert[j].length-1; i++)
      {
        Edge e = ed[boundary[j][i]];
        boundaryVert[j][i+1] = (e.v1 == boundaryVert[j][i] ? e.v2 : e.v1);
      }
    }

    // Consider all possible ways of joining the two boundaries, and select the one which
    // gives the lowest mean squared distance between connected points as the initial guess.

    double offset = 0.0;
    boolean reverse = false;
    int maxsteps = (boundary[0].length > boundary[1].length ? boundary[0].length : boundary[1].length);
    if (closed)
    {
      double step0 = boundary[0].length/((double) maxsteps);
      double step1 = (reverse ? -1.0 : 1.0)*boundary[1].length/((double) maxsteps);
      double mindist = Double.MAX_VALUE;
      for (int i = 0; i < maxsteps-1; i++)
      {
        double dist = 0.0;
        for (int j = 0; j < maxsteps; j++)
        {
          double p0 = j*step0, p1 = (i+j)*step1;
          int i0 = ((int) Math.round(p0)+boundary[0].length) % boundary[0].length;
          int i1 = ((int) Math.round(p1)+boundary[1].length) % boundary[1].length;
          dist += vt[boundaryVert[0][i0]].r.distance2(vt[boundaryVert[1][i1]].r);
        }
        if (dist < mindist)
        {
          mindist = dist;
          offset = i*step1;
          reverse = false;
        }
        dist = 0.0;
        for (int j = 0; j < maxsteps; j++)
        {
          double p0 = j*step0, p1 = (i-j)*step1;
          int i0 = ((int) Math.round(p0)+boundary[0].length) % boundary[0].length;
          int i1 = ((int) Math.round(p1)+boundary[1].length) % boundary[1].length;
          dist += vt[boundaryVert[0][i0]].r.distance2(vt[boundaryVert[1][i1]].r);
        }
        if (dist < mindist)
        {
          mindist = dist;
          offset = i*step1;
          reverse = true;
        }
      }
    }
    else
    {
      double fdist = 0.0, rdist = 0.0;
      double step0 = boundary[0].length/((double) maxsteps);
      double step1 = boundary[1].length/((double) maxsteps);
      int revStart = boundaryVert[1].length-1;
      for (int i = 0; i < maxsteps; i++)
      {
        int i0 = (int) Math.round(i*step0);
        int i1 = (int) Math.round(i*step1);
        fdist += vt[boundaryVert[0][i0]].r.distance2(vt[boundaryVert[1][i1]].r);
        rdist += vt[boundaryVert[0][i0]].r.distance2(vt[boundaryVert[1][revStart-i1]].r);
      }
      reverse = (fdist > rdist);
    }

    // Create a dialog allowing the user to adjust the parameters.

    final ValueSlider offsetSlider = new ValueSlider(-0.5*boundary[1].length, 0.5*boundary[1].length, 2*maxsteps, 0.0);
    final BCheckBox reverseBox = new BCheckBox(Translate.text("reverseDirection"), false);
    final ObjectPreviewCanvas preview = new ObjectPreviewCanvas(new ObjectInfo(doJoinBoundaries(boundary,
      boundaryVert, offset, reverse), new CoordinateSystem(), ""));
    final double baseOffset = offset;
    final boolean baseReverse = reverse;
    FormContainer content = new FormContainer(new double [] {1.0}, new double [] {0.0, 0.0, 1.0});
    if (closed)
    {
      RowContainer row = new RowContainer();
      row.add(new BLabel(Translate.text("Offset")+":"));
      row.add(offsetSlider);
      content.add(row, 0, 0);
    }
    content.add(reverseBox, 0, 1);
    content.add(preview, 0, 2, new LayoutInfo(LayoutInfo.CENTER, LayoutInfo.BOTH, null, null));
    preview.setPreferredSize(new Dimension(200, 200));
    preview.setRenderMode(preview.RENDER_FLAT);
    Object listener = new Object() {
      void processEvent()
      {
        preview.setObject(doJoinBoundaries(boundary, boundaryVert, baseOffset+offsetSlider.getValue(), baseReverse^reverseBox.getState()));
        preview.updateImage();
        preview.repaint();
      }
    };
    offsetSlider.addEventLink(ValueChangedEvent.class, listener);
    reverseBox.addEventLink(ValueChangedEvent.class, listener);
    PanelDialog dlg = new PanelDialog(this, Translate.text("joinBoundardiesTitle"), content);
    if (!dlg.clickedOk())
      return;
//    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {preview.getObject().object, theMesh}));
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    theMesh.copyObject(preview.getObject().object);
    object3DChangedDuringEditor();
    tmv.updateImage();
  }

  
  /* This method does the actual work of joining together two boundary curves. */

  TriangleMesh doJoinBoundaries(int boundary[][], int boundaryVert[][], double offset, boolean reverse)
  {
    int maxsteps = (boundary[0].length > boundary[1].length ? boundary[0].length : boundary[1].length);
    double step0 = boundary[0].length/((double) maxsteps);
    double step1 = (reverse ? -1.0 : 1.0)*boundary[1].length/((double) maxsteps);
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    Vertex vt[] = (Vertex []) theMesh.getVertices();
    Edge ed[] = theMesh.getEdges();
    Face fc[] = theMesh.getFaces();
    
    AppendItemsListOp faceAppendOp = new AppendItemsListOp(boundary[0].length+boundary[1].length);

//    Vec3 newvert[] = new Vec3 [vt.length];
//    int newface[][] = new int [fc.length+boundary[0].length+boundary[1].length][];

    // First copy over the old vertices and faces.

//    for (int i = 0; i < vt.length; i++)
//      newvert[i] = vt[i].r;
//    for (int i = 0; i < fc.length; i++)
//      newface[i] = new int [] {fc[i].v1, fc[i].v2, fc[i].v3};
//    int count = fc.length;

    // Go around the boundaries and add new faces.

    if (isBoundaryClosed(boundary[0]))
    {
      // We are connecting two closed boundaries.
      
      double p0 = 0.0, p1 = offset;
      int i0prev = 0, i1prev = ((int) Math.round(p1)+boundary[1].length) % boundary[1].length;
      for (int i = 1; i <= maxsteps; i++)
      {
        p0 += step0;
        p1 += step1;
        while (p1 < 0.0)
          p1 += boundary[1].length;
        int i0 = ((int) Math.round(p0)) % boundary[0].length;
        int i1 = ((int) Math.round(p1)) % boundary[1].length;
        if (i0 != i0prev)
        {
          Edge e = ed[boundary[0][step0 > 0.0 ? i0prev : i0]];
          Face f = fc[e.f1];
          int v1 = boundaryVert[0][i0prev], v2 = boundaryVert[0][i0];
          if ((f.v1 == v1 && f.v2 == v2) || (f.v2 == v1 && f.v3 == v2) || (f.v3 == v1 && f.v1 == v2))
            faceAppendOp.add(new Face(v2, v1, boundaryVert[1][i1prev]));
//            newface[count++] = new int [] {v2, v1, boundaryVert[1][i1prev]};
          else
            faceAppendOp.add(new Face(v1, v2, boundaryVert[1][i1prev]));
//            newface[count++] = new int [] {v1, v2, boundaryVert[1][i1prev]};
        }
        if (i1 != i1prev)
        {
          Edge e = ed[boundary[1][step1 > 0.0 ? i1prev : i1]];
          Face f = fc[e.f1];
          int v1 = boundaryVert[1][i1prev], v2 = boundaryVert[1][i1];
          if ((f.v1 == v1 && f.v2 == v2) || (f.v2 == v1 && f.v3 == v2) || (f.v3 == v1 && f.v1 == v2))
            faceAppendOp.add(new Face(v2, v1, boundaryVert[0][i0]));
//            newface[count++] = new int [] {v2, v1, boundaryVert[0][i0]};
          else
            faceAppendOp.add(new Face(v1, v2, boundaryVert[0][i0]));
//            newface[count++] = new int [] {v1, v2, boundaryVert[0][i0]};
        }
        i0prev = i0;
        i1prev = i1;
      }
    }
    else
    {
      // We are connecting two open boundaries.
      
      double p0 = 0.0, p1 = (reverse ? boundary[1].length : 0.0);
      int i0prev = 0, i1prev = (int) Math.round(p1);
      while (faceAppendOp.getAddSize() < (fc.length+boundary[0].length+boundary[1].length))
      {
        p0 += step0;
        p1 += step1;
        int i0 = (int) Math.round(p0);
        int i1 = (int) Math.round(p1);
        if (i0 < 0)
          i0 = 0;
        if (i1 < 0)
          i1 = 0;
        if (i0 > boundary[0].length)
          i0 = boundary[0].length;
        if (i1 > boundary[1].length)
          i1 = boundary[1].length;
        if (i0 != i0prev)
        {
          Edge e = ed[boundary[0][i0prev < i0 ? i0prev : i0]];
          Face f = fc[e.f1];
          int v1 = boundaryVert[0][i0prev], v2 = boundaryVert[0][i0];
          if ((f.v1 == v1 && f.v2 == v2) || (f.v2 == v1 && f.v3 == v2) || (f.v3 == v1 && f.v1 == v2))
            faceAppendOp.add(new Face(v2, v1, boundaryVert[1][i1prev]));
//            newface[count++] = new int [] {v2, v1, boundaryVert[1][i1prev]};
          else
            faceAppendOp.add(new Face(v1, v2, boundaryVert[1][i1prev]));
//            newface[count++] = new int [] {v1, v2, boundaryVert[1][i1prev]};
        }
        if (i1 != i1prev)
        {
          Edge e = ed[boundary[1][i1prev < i1 ? i1prev : i1]];
          Face f = fc[e.f1];
          int v1 = boundaryVert[1][i1prev], v2 = boundaryVert[1][i1];
          if ((f.v1 == v1 && f.v2 == v2) || (f.v2 == v1 && f.v3 == v2) || (f.v3 == v1 && f.v1 == v2))
            faceAppendOp.add(new Face(v2, v1, boundaryVert[0][i0]));
//            newface[count++] = new int [] {v2, v1, boundaryVert[0][i0]};
          else
            faceAppendOp.add(new Face(v1, v2, boundaryVert[0][i0]));
//            newface[count++] = new int [] {v1, v2, boundaryVert[0][i0]};
        }
        i0prev = i0;
        i1prev = i1;
      }
    }
    
    // Update the texture parameters.
    
    TextureParameter param[] = theMesh.getParameters();
    ParameterValue oldParamVal[] = theMesh.getParameterValues();
    ParameterValue newParamVal[] = new ParameterValue [oldParamVal.length];
    for (int i = 0; i < oldParamVal.length; i++)
    {
      double defvalue = param[i].defaultVal;
      
      if (oldParamVal[i] instanceof FaceParameterValue)
      {
        double oldval[] = ((FaceParameterValue) oldParamVal[i]).getValue();
        AppendDefaultItemListOp addop =
            new AppendDefaultItemListOp(defvalue, faceAppendOp.getAddSize());
        newParamVal[i] = new FaceParameterValue(addop.apply(oldval));

//        double oldval[] = ((FaceParameterValue) oldParamVal[i]).getValue();
//        double newval[] = new double [newface.length];
//        for (int j = 0; j < oldval.length; j++)
//          newval[j] = oldval[j];
//        for (int j = oldval.length; j < newval.length; j++)
//          newval[j] = param[i].defaultVal;
//        newParamVal[i] = new FaceParameterValue(newval);
      }
      else if (oldParamVal[i] instanceof FaceVertexParameterValue)
      {
        double oldval[][] = ((FaceVertexParameterValue) oldParamVal[i]).getValue();
        double newval[][] = new double [3][];
        AppendDefaultItemListOp addop =
            new AppendDefaultItemListOp(defvalue, faceAppendOp.getAddSize());
        for (int j = 0; j < 3; ++j)
          newval[j] = addop.apply(oldval[j]);

//        double oldval[][] = ((FaceVertexParameterValue) oldParamVal[i]).getValue();
//        double newval[][] = new double [3][newface.length];
//        for (int j = 0; j < 3; j++)
//        {
//          for (int k = 0; k < oldval[0].length; k++)
//            newval[j][k] = oldval[j][k];
//          for (int k = oldval[0].length; k < newface.length; k++)
//            newval[j][k] = param[i].defaultVal;
//        }
        newParamVal[i] = new FaceVertexParameterValue(newval);
      }
      else
        newParamVal[i] = oldParamVal[i].duplicate();
    }
    
    // Create the new mesh.

    Face newface[] = (Face[])faceAppendOp.apply(fc);
    TriangleMesh newmesh = new TriangleMesh(vt, newface);
    Vertex newvt[] = (Vertex []) newmesh.getVertices();
    Edge newed[] = newmesh.getEdges();
    newmesh.copyTextureAndMaterial(theMesh);
    newmesh.setSmoothingMethod(theMesh.getSmoothingMethod());
    newmesh.setParameterValues(newParamVal);
    
    // Copy over the smoothness values.

    for (int i = 0; i < vt.length; i++)
      newvt[i].smoothness = vt[i].smoothness;
    for (int i = 0; i < newed.length; i++)
      {
	if (newed[i].v1 >= vt.length || newed[i].v2 >= vt.length)
	  continue;
	for (int j = 0; j < ed.length; j++)
	  if ((newed[i].v1 == ed[j].v1 && newed[i].v2 == ed[j].v2) || (newed[i].v1 == ed[j].v2 && newed[i].v2 == ed[j].v1))
	    newed[i].smoothness = ed[j].smoothness;
      }
    return newmesh;
  }

  public void extractCurveCommand()
  {
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    Vertex vt[] = (Vertex []) theMesh.getVertices();
    Edge ed[] = theMesh.getEdges();
    boolean selected[] = ((TriMeshViewer) getCurrentView()).getSelection();
    Vector edges = new Vector();
    int i;

    if (((TriMeshViewer) getCurrentView()).getSelectionMode() != TriMeshViewer.EDGE_MODE)
      return;

    // Find the select edges, and try to chain them together.

    for (i = 0; i < selected.length; i++)
      if (selected[i])
        edges.addElement(ed[i]);
    if (edges.size() == 0)
      return;
    Edge first = (Edge) edges.elementAt(0), last = first;
    Vector ordered = new Vector();
    ordered.addElement(first);
    edges.removeElementAt(0);
    while (edges.size() > 0)
    {
      for (i = 0; i < edges.size(); i++)
      {
        Edge e = (Edge) edges.elementAt(i);
        if (e.v1 == first.v1 || e.v2 == first.v1 || e.v1 == first.v2 || e.v2 == first.v2)
        {
          ordered.insertElementAt(e, 0);
          first = e;
          break;
        }
        if (e.v1 == last.v1 || e.v2 == last.v1 || e.v1 == last.v2 || e.v2 == last.v2)
        {
          ordered.addElement(e);
          last = e;
          break;
        }
      }
      if (i == edges.size())
      {
        new BStandardDialog("", Translate.text("edgesNotContinuous"), BStandardDialog.ERROR).showMessageDialog(this);
        return;
      }
      edges.removeElementAt(i);
    }

    // Now find the sequence of vertices.

    boolean closed = (ordered.size() > 2 && (last.v1 == first.v1 || last.v2 == first.v1 || last.v1 == first.v2 || last.v2 == first.v2));
    Vec3 v[] = new Vec3 [closed ? ordered.size() : ordered.size()+1];
    float smoothness[] = new float [v.length];
    Edge second = (ordered.size() == 1 ? first : (Edge) ordered.elementAt(1));
    int prev;
    if (first.v1 == second.v1 || first.v1 == second.v2)
      prev = first.v2;
    else
      prev = first.v1;
    for (i = 0; i < ordered.size(); i++)
    {
      Edge e = (Edge) ordered.elementAt(i);
      v[i] = new Vec3(vt[prev].r);
      smoothness[i] = vt[prev].smoothness;
      prev = (e.v1 == prev ? e.v2 : e.v1);
    }
    if (!closed)
    {
      v[i] = new Vec3(vt[prev].r);
      smoothness[i] = vt[prev].smoothness;
    }
    int smoothingMethod = theMesh.getSmoothingMethod();
    if (smoothingMethod == Mesh.SMOOTH_SHADING)
      smoothingMethod = Mesh.NO_SMOOTHING;
    Curve cv = new Curve(v, smoothness, smoothingMethod, closed);
    Widget parent = (Widget) parentWindow;
    while (parent != null && !(parent instanceof LayoutWindow))
      parent = parent.getParent();
    if (parent != null)
      {
        ((LayoutWindow) parent).addObject(cv, getCurrentView().thisObjectInScene.coords.duplicate(), 
            "Extracted Curve", null);
        ((LayoutWindow) parent).updateImage();
      }
  }

  public void setSmoothnessCommand()
  {
    final TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    TriangleMesh oldMesh = (TriangleMesh) theMesh.duplicate();
    final Vertex vt[] = (Vertex []) theMesh.getVertices();
    final Edge ed[] = theMesh.getEdges();
    final boolean selected[] = ((TriMeshViewer) getCurrentView()).getSelection();
    final boolean pointmode = (((TriMeshViewer) getCurrentView()).getSelectionMode() == TriMeshViewer.POINT_MODE);
    final ActionProcessor processor = new ActionProcessor();
    float value;
    final ValueSlider smoothness;

    int i;

    for (i = 0; i < selected.length && !selected[i]; i++);
    if (i == selected.length)
      return;
    if (pointmode)
      value = vt[i].smoothness;
    else
      value = ed[i].smoothness;
    value = 0.001f * (Math.round(value*1000.0f));
    smoothness = new ValueSlider(0.0, 1.0, 100, (double) value);
    smoothness.addEventLink(ValueChangedEvent.class, new Object() {
      void processEvent()
      {
        processor.addEvent(new Runnable() {
          public void run()
          {
            float s = (float) smoothness.getValue();
            for (int i = 0; i < selected.length; i++)
              if (selected[i])
              {
                if (pointmode)
                  vt[i].smoothness = s;
                else
                  ed[i].smoothness = s;
              }
            theMesh.setSmoothingMethod(theMesh.getSmoothingMethod());
            //object3DChangedDuringEditor();   // TODO(MB) During editor?
            getCurrentView().getObject().object.informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITORDRAG);
            getCurrentView().objectChanged();  // TODO(MB) Enhance message mechanism to remove these lines
            getCurrentView().updateImage();
            getCurrentView().repaint();
            
//            for (int i = 0; i < theView.length; ++i)
//            {
//              theView[i].updateImage();  // TODO(MB) Use message mechanism
//              theView[i].repaint();
//            }
          }
        } );
      }
    } );
    ComponentsDialog dlg = new ComponentsDialog(this,
        Translate.text(pointmode ? "setPointSmoothness" : "setEdgeSmoothness"),
        new Widget [] {smoothness}, new String [] {Translate.text("Smoothness")});
    processor.stopProcessing();
    if (dlg.clickedOk())
    {
      setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, oldMesh}));
      object3DChangedDuringEditor();
    }
    else
      {
        theMesh.copyObject(oldMesh);
        object3DChangedDuringEditor();
        for (i = 0; i < theView.length; ++i)
        {
          theView[i].updateImage();
          theView[i].repaint();
        }
      }
  }

  public void reverseNormalsCommand()
  {
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    theMesh.reverseNormals();
    object3DChangedDuringEditor();
  }

  void setSmoothingMethod(int method)
  {
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;

    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    updateMenus();
    theMesh.setSmoothingMethod(method);
    object3DChangedDuringEditor();
  }

  /** Given a list of deltas which will be added to the selected vertices, calculate the
      corresponding deltas for the unselected vertices according to the mesh tension. */

  public void adjustDeltas(Vec3 delta[])
  {
    int dist[] = getCurrentView().getSelectionDistance(), count[] = new int [delta.length];
    TriangleMesh theMesh = (TriangleMesh) getCurrentView().getObject().object;
    TriangleMesh.Edge edge[] = theMesh.getEdges();
    int maxDistance = getTensionDistance();
    double tension = getMeshTension(), scale[] = new double [maxDistance+1];


    for (int i = 0; i < delta.length; i++)
      if (dist[i] != 0)
        delta[i].set(0.0, 0.0, 0.0);
    for (int i = 0; i < maxDistance; i++)
      {
	for (int j = 0; j < count.length; j++)
	  count[j] = 0;
	for (int j = 0; j < edge.length; j++)
	  {
	    if (dist[edge[j].v1] == i && dist[edge[j].v2] == i+1)
	      {
		count[edge[j].v2]++;
		delta[edge[j].v2].add(delta[edge[j].v1]);
	      }
	    else if (dist[edge[j].v2] == i && dist[edge[j].v1] == i+1)
	      {
		count[edge[j].v1]++;
		delta[edge[j].v1].add(delta[edge[j].v2]);
	      }
	  }
	for (int j = 0; j < count.length; j++)
	  if (count[j] > 1)
	    delta[j].scale(1.0/count[j]);
      }
    for (int i = 0; i < scale.length; i++)
      scale[i] = Math.pow((maxDistance-i+1.0)/(maxDistance+1.0), tension);
    for (int i = 0; i < delta.length; i++)
      if (dist[i] > 0)
        delta[i].scale(scale[dist[i]]);
  }
  
  /** Allow the user to set the texture parameters for selected vertices or faces. */
  
  public void setParametersCommand()
  {
    TriMeshViewer tmv = (TriMeshViewer) getCurrentView();
    int selectionMode = tmv.getSelectionMode();
    if (selectionMode == TriMeshViewer.EDGE_MODE)
      return;
    if (selectionMode == TriMeshViewer.POINT_MODE)
    {
      super.setParametersCommand();
      return;
    }
    TriangleMesh theMesh = (TriangleMesh) tmv.getObject().object;
    final MeshVertex vert[] = theMesh.getVertices();
    ObjectInfo info = tmv.getObject();
    TextureParameter param[] = info.object.getParameters();
    final ParameterValue paramValue[] = info.object.getParameterValues();
    int i, j, k, paramIndex[] = null;
    final boolean selected[] = tmv.getSelection();
    double value[][];

    String label[];


    for (j = 0; j < selected.length && !selected[j]; j++);
    if (j == selected.length)
      return;
    if (param != null)
    {
      // Find the list of per-face and per-face/per-vertex parameters.

      int num = 0;
      for (i = 0; i < param.length; i++)
        if (paramValue[i] instanceof FaceParameterValue || paramValue[i] instanceof FaceVertexParameterValue)
          if (param[i] != tmv.getExtraParameter())
            num++;
      paramIndex = new int [num];
      for (i = 0, k = 0; k < param.length; k++)
        if (paramValue[k] instanceof FaceParameterValue || paramValue[k] instanceof FaceVertexParameterValue)
          if (param[i] != tmv.getExtraParameter())
            paramIndex[i++] = k;
    }
    if (paramIndex == null || paramIndex.length == 0)
    {
      new BStandardDialog("", Translate.text("noPerFaceParams"), BStandardDialog.INFORMATION).showMessageDialog(this);
      return;
    }
    value = new double [paramIndex.length][];
    for (i = 0; i < paramIndex.length; i++)
    {
      if (paramValue[paramIndex[i]] instanceof FaceParameterValue)
      {
        double currentVal[] = ((FaceParameterValue) paramValue[paramIndex[i]]).getValue();
        double commonVal = currentVal[j];
        for (k = j; k < selected.length; k++)
          if (selected[k] && currentVal[k] != commonVal)
            commonVal = Double.NaN;
        value[i] = new double [] {commonVal};
      }
      else
      {
        double currentVal[][] = ((FaceVertexParameterValue) paramValue[paramIndex[i]]).getValue();
        double commonVal[] = new double [] {currentVal[0][j], currentVal[1][j], currentVal[2][j]};
        for (k = j; k < selected.length; k++)
          if (selected[k])
          {
            if (currentVal[0][k] != commonVal[0])
              commonVal[0] = Double.NaN;
            if (currentVal[1][k] != commonVal[1])
              commonVal[1] = Double.NaN;
            if (currentVal[2][k] != commonVal[2])
              commonVal[2] = Double.NaN;
          }
        value[i] = commonVal;
      }
    }

    // Build the panel for editing the values.

    Widget editWidget[][] = new Widget [paramIndex.length][3];
    LayoutInfo leftLayout = new LayoutInfo(LayoutInfo.EAST, LayoutInfo.NONE, new Insets(0, 10, 0, 5), null);
    FormContainer content;
    if (info.object.getTexture() instanceof LayeredTexture)
    {
      // This is a layered texture, so we want to group the parameters by layer.

      LayeredMapping map = (LayeredMapping) info.object.getTextureMapping();
      Texture layer[] = map.getLayers();
      content = new FormContainer(2, paramIndex.length+layer.length*3);
      content.setDefaultLayout(new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, null, null));
      int line = 0;
      for (k = 0; k < layer.length; k++)
      {
        content.add(new BLabel(Translate.text("layerLabel", Integer.toString(k+1), layer[k].getName())), 0, line++, 2, 1);
        TextureParameter layerParam[] = map.getLayerParameters(k);
        boolean any = false;
        for (i = 0; i < paramIndex.length; i++)
        {
          // Determine whether this parameter is actually part of this layer.

          int m;
          TextureParameter pm = param[paramIndex[i]];
          for (m = 0; m < layerParam.length; m++)
            if (layerParam[m].equals(pm))
              break;
          if (m == layerParam.length)
            continue;
          any = true;

          // It is, so add it.

          for (m = 0; m < value[i].length; m++)
          {
            Component toAdd;
            editWidget[i][m] = pm.getEditingWidget(value[i][m]);
            content.add(new BLabel(m == 0 ? pm.name : ""), 0, line, leftLayout);
            content.add(editWidget[i][m], 1, line++);
          }
        }
        if (!any)
          content.add(Translate.label("noLayerPerFaceParams"), 0, line++, 2, 1, new LayoutInfo());
      }
    }
    else
    {
      // This is a simple texture, so just list off all the parameters.

      content = new FormContainer(2, paramIndex.length+1);
      content.setDefaultLayout(new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, null, null));
      content.add(new BLabel(Translate.text("Texture")+": "+info.object.getTexture().getName()), 0, 0);
      for (i = 0; i < paramIndex.length; i++)
      {
        TextureParameter pm = param[paramIndex[i]];
        editWidget[i][0] = pm.getEditingWidget(value[i][0]);
        content.add(new BLabel(pm.name), 0, i+1, leftLayout);
        content.add(editWidget[i][0], 1, i+1);
      }

    }
    PanelDialog dlg = new PanelDialog(this, Translate.text("texParamsForSelectedFaces"), content);
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    for (j = 0; j < editWidget.length; j++)
    {
      if (paramValue[paramIndex[j]] instanceof FaceParameterValue)
      {
        double d;
        if (editWidget[j][0] instanceof ValueField)
          d = ((ValueField) editWidget[j][0]).getValue();
        else
          d = ((ValueSlider) editWidget[j][0]).getValue();
        if (!Double.isNaN(d))
        {
          double val[] = ((FaceParameterValue) paramValue[paramIndex[j]]).getValue();
          for (i = 0; i < selected.length; i++)
            if (selected[i])
              val[i] = d;
          ((FaceParameterValue) paramValue[paramIndex[j]]).setValue(val);
        }

      }
      else
      {
        double d[];
        if (editWidget[j][0] instanceof ValueField)
          d = new double [] {((ValueField) editWidget[j][0]).getValue(), ((ValueField) editWidget[j][1]).getValue(), ((ValueField) editWidget[j][2]).getValue()};
        else
          d = new double [] {((ValueSlider) editWidget[j][0]).getValue(), ((ValueSlider) editWidget[j][1]).getValue(), ((ValueSlider) editWidget[j][2]).getValue()};
        double val[][] = ((FaceVertexParameterValue) paramValue[paramIndex[j]]).getValue();
        for (i = 0; i < selected.length; i++)
          if (selected[i])

            for (int m = 0; m < 3; m++)
              if (!Double.isNaN(d[m]))
                val[m][i] = d[m];
        ((FaceVertexParameterValue) paramValue[paramIndex[j]]).setValue(val);
      }
    }
  }

}