All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jmol.util.MeshCapper Maven / Gradle / Ivy

There is a newer version: 14.31.10
Show newest version
package org.jmol.util;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.Map;

import javajs.util.AU;
import javajs.util.Lst;
import javajs.util.M3;
import javajs.util.P3;
import javajs.util.Quat;
import javajs.util.T3;
import javajs.util.V3;

/**
 * A class to properly cap a convoluted, closed slice of an isosurface
 * 
 * inspired by: Computational Geometry: Algorithms and Applications Mark de
 * Berg, Marc van Kreveld, Mark Overmars, and Otfried Schwarzkopf
 * Springer-Verlag, Berlin Heidelberg 1997 Chapter 3. Polygon Triangulation
 * 
 * Thanks given to Olaf Hall-Holt for pointing me to this reference.
 * 
 * Extensively modified:
 * 
 * - quaternion transform from 3D plane to XY plane for best precision
 * 
 * - using directional edges -- no angle measurements necessary
 * 
 * - continuous splitting off of triangles
 * 
 * - independent dynamic monotonic regions created and processed as one stream
 * 
 * - no need for vertex typing
 * 
 * - no push/pop stacks
 * 
 * INPUT: stream of [a b] ordered-vertex edges such that triangle a-b-c is
 * interior if (ab.cross.ac).dot.planeNormal > 0 (right-hand rule;
 * counter-clockwise edge flow)
 * 
 * Bob Hanson - Jan 11, 2015
 * 
 * @author Bob Hanson, [email protected]
 */
public class MeshCapper {

  public MeshCapper() {
    // for reflection
  }

  /**
   * source of edges; consumer of triangles
   */
  private MeshSlicer slicer;

  /**
   * for debugging
   */
  private boolean dumping;

  /**
   * initialization only
   */
  private Map capMap;
  private Lst vertices;

  /**
   * dynamic region processing. These are just [DESCENDER, ASCENDER, LAST] for
   * each region
   * 
   */
  private Lst lstRegions;

  private static final int DESCENDER = 0;
  private static final int ASCENDER = 1;
  private static final int LAST = 2;

  /**
   * informational only
   */
  private int nTriangles, nRegions;

  private Lst lstTriangles;

  private int nPoints;

  M3 m3, m3inv;

  /////////////// initialization //////////////////

  /**
   * @param slicer
   * @return this
   */
  public MeshCapper set(MeshSlicer slicer) {
    // resolution has not been necessary
    this.slicer = slicer;
    dumping = Logger.debugging;
    return this;
  }

  void clear() {
    capMap = new Hashtable();
    vertices = new Lst();
  }

  /**
   * generic entry for a set of faces
   * 
   * @param faces
   *        array of pointers into points
   * @param vertices
   * @param faceTriangles optional return list by face
   * @return array of triangles [a b c mask]
   */
  public int[][] triangulateFaces(int[][] faces, P3[] vertices,
                                  int[][] faceTriangles) {
    lstTriangles = new Lst();
    P3[] points = new P3[10];
    for (int f = 0, n = faces.length; f < n; f++) {
      int[] face = faces[f];
      int npts = face.length;
      if (points.length < npts)
        points = new P3[npts];
      int n0 = lstTriangles.size();
      for (int i = npts; --i >= 0;)
        points[i] = vertices[face[i]];
      triangulatePolygon(points, npts);
      int n1 = lstTriangles.size();
        int[] ft = new int[n1 - n0];
        if (faceTriangles != null)
          faceTriangles[f] = ft;
        for (int i = n0; i < n1; i++) {
          int[] t = lstTriangles.get(i);
          ft[i - n0] = i;
          for (int j = 3; --j >= 0;)
            t[j] = face[t[j]];
          t[3] = -t[3];
        }
    }
    int[][] triangles = AU.newInt2(lstTriangles.size());
    lstTriangles.toArray(triangles);
    return triangles;
  }


  /**
   * generic entry for a polygon
   * 
   * @param points
   * @param nPoints number of points or -1
   * @return int[][i j k mask]
   */
  public int[][] triangulatePolygon(P3[] points, int nPoints) {
    clear();
    boolean haveList = (nPoints >= 0);
    // this is winding backward
    if (!haveList || lstTriangles == null)
      lstTriangles = new Lst();
    nPoints = this.nPoints = (haveList ? nPoints : points.length);
    CapVertex v0 = null;
    for (int i = 0; i < nPoints; i++) {
      if (points[i] == null)
        return null;
      CapVertex v = new CapVertex(points[i], i);
      vertices.addLast(v);
      if (v0 != null) {
        v0.link(v);
      }
      v0 = v;
    }
    v0.link(vertices.get(0));
    createCap(null);
    if (haveList)
      return null;
    int[][] a = AU.newInt2(lstTriangles.size());
    for (int i = lstTriangles.size(); --i >= 0;)
      a[i] = lstTriangles.get(i);
    return a;
  }

  /**
   * Input method from MeshSlicer. Pointers are into MeshSlicer.m.vs[]
   * 
   * @param ipt1
   * @param ipt2
   * @param thisSet
   */

  void addEdge(int ipt1, int ipt2, int thisSet) {
    CapVertex v1 = addPoint(thisSet, ipt1);
    CapVertex v2 = addPoint(thisSet, ipt2);
    v1.link(v2);
  }

  /**
   * The MeshSlicer class manages all introduction of vertices; we must pass on
   * to it the subset of vertices from the original Jmol isosurface being
   * capped.
   * 
   * @param thisSet
   * @param i
   * @return a CapVertex pointing to this new point in the isosurface or one we
   *         already have
   */
  private CapVertex addPoint(int thisSet, int i) {
    Integer ii = Integer.valueOf(i);
    CapVertex v = capMap.get(ii);
    if (v == null) {
      T3 pt = slicer.m.vs[i];
      // copy this point to a new one to avoid shading across the edge
      i = slicer.addIntersectionVertex(pt, 0, -1, thisSet, null, -1, -1);
      v = new CapVertex(pt, i);
      vertices.addLast(v);
      capMap.put(ii, v);
    }
    if (dumping)
      Logger.info(i + "\t" + slicer.m.vs[i]);
    return v;
  }

  /**
   * for debugging
   * 
   * @param v
   * @return external point or test point
   */
  private T3 getInputPoint(CapVertex v) {
    return (slicer == null ? P3.newP(v) : slicer.m.vs[v.ipt]);
  }

  /**
   * Export triangle to MeshSlicer
   * 
   * @param ipt1
   * @param ipt2
   * @param ipt3
   */
  private void outputTriangle(int ipt1, int ipt2, int ipt3) {
    if (slicer == null) {
      int mask = 0;
      if (isEdge(ipt1, ipt2))
        mask |= 1;
      if (isEdge(ipt2, ipt3))
        mask |= 2;
      if (isEdge(ipt3, ipt1))
        mask |= 4;
      lstTriangles.addLast(new int[] { ipt1, ipt2, ipt3, mask });
    } else {
      slicer.addTriangle(ipt1, ipt2, ipt3);
    }
  }

  private boolean isEdge(int i, int j) {
    return (j == (i + 1) % nPoints);
  }

//  private CapVertex[] test(CapVertex[] vs) {
//    dumping = true;
//    int n = 0;
//    vs = new CapVertex[] { 
//        new CapVertex(P3.new3(0, 10, 0), n++),
//        new CapVertex(P3.new3(0, 0, 0), n++),
//        new CapVertex(P3.new3(1, 0, 0), n++),
//        new CapVertex(P3.new3(2, 0, 0), n++)
//
//    //      new CapVertex(P3.new3(0,  10,  0), n++)
//    //      new CapVertex(P3.new3(-2,  0,  0), n++),
//    //      new CapVertex(P3.new3(-1,  0,  0), n++), 
//    //      new CapVertex(P3.new3(0,  0,  0), n++)
//
//    //      new CapVertex(P3.new3(0,  10,  0), n++),
//    //      new CapVertex(P3.new3(-2,  0,  0), n++),
//    //      new CapVertex(P3.new3(-1,  0,  0), n++), 
//    //      new CapVertex(P3.new3(0,  0,  0), n++), 
//    //      new CapVertex(P3.new3(0,  6,  0), n++), 
//    //      new CapVertex(P3.new3(0,  8,  0), n++) 
//    };
//    return vs;
//  }

  /////////////// processing //////////////////

  /**
   * Entry point when finished generating edges.
   * 
   * @param norm
   */
  void createCap(V3 norm) {

    capMap = null;
    lstRegions = new Lst();

    CapVertex[] vs = new CapVertex[vertices.size()];

    if (vs.length < 3)
      return;

    if (Logger.debugging)
      Logger.info("MeshCapper using " + vs.length + " vertices");

    // transfer to vertex array
    
    vertices.toArray(vs);
    vertices = null;

    // vs = test(vs);
    

    // Transform to xy plane to give best precision.

    V3 vab = V3.newVsub(vs[0], vs[1]);
    V3 vac;
    if (norm == null) {
      vac = V3.newVsub(vs[0], vs[vs.length - 1]);
    } else {
      vac = V3.newV(norm);
      vac.cross(vac, vab);
    }
    Quat q = Quat.getQuaternionFrameV(vab, vac, null, false);
    m3 = q.getMatrix();
    m3inv = M3.newM3(m3);
    m3inv.invert();
    for (int i = vs.length; --i >= 0;)
      m3inv.rotate(vs[i]);

    // Fix loose ends and link by Y,X sort, creating the .yxNext link

    fixEndsAndSortVertices(vs);

    // scan the plane

    CapVertex v0 = vs[0];
    CapVertex v = v0;
    try {
      do {
        v = process(v);
      } while (v != v0);
    } catch (Exception e) {
      System.out.println("MeshCapper exception " + e);
      e.printStackTrace();
    }
    if (slicer != null)
      clear();
    if (Logger.debugging)
      Logger.info("MeshCapper created " + nTriangles + " triangles " + nRegions
          + " regions");
  }

  /**
   * Generate yxNext links based on scanning Y large to small and if Y1==Y2,
   * then X small to large
   * 
   * @param vs
   */
  
  private void fixEndsAndSortVertices(CapVertex[] vs) {
    
    // First we need to make sure that the edges are completely closed. 
    // Start with generating lists of points with missing .next (v0s) and .prev (v1s).
    
    Lst v0s = new Lst();
    Lst v1s = new Lst();
    int n = vs.length;
    for (int i = n; --i >= 0;) {
      if (vs[i].next == null) {
        v0s.addLast(vs[i]);
      } else if (vs[i].prev == null) {
        v1s.addLast(vs[i]);
      }
    }
    
    // For each v0, find the nearest v1 and connect v0 to it. 
    // This will almost certainly be the same point in space.
    // If it is, then unlink v1, as it is not needed.
    
    for (int i = v0s.size(); --i >= 0;) {
      CapVertex v0 = v0s.get(i);
      CapVertex v1 = findNearestVertex(v1s, v0);
      if (v1 == null) {
        System.out.println("MESHCAPPER OHOH");
      } else {
        v0.link(v1);
        if (v0.distanceSquared(v1) < 1e-6)
          v1.link(null);
      }
    }
    
    // Now sort vertices based on X and Y values and make a full loop
    
    Arrays.sort(vs, new MeshCapperSorter());
    for (int i = n; --i >= 0;)
      vs[i].yxNext = vs[(i + 1) % n];
    vs[n - 1].yxNext = vs[0];
    
  }

  /**
   * Find the nearest vertex to v0 in the list v1s.
   * @param v1s
   * @param v0
   * @return nearest vertex or null if we can't match
   */
  private CapVertex findNearestVertex(Lst v1s, CapVertex v0) {
    float min = Float.MAX_VALUE;
    CapVertex vmin = null;
    int imin = -1;
    for (int i = v1s.size(); --i >= 0;) {
      CapVertex v1 = v1s.get(i);
      float d = v1.distanceSquared(v0);
      if (d < min) {
        min = d;
        vmin = v1;
        imin = i;
      }
    }
    if (imin >= 0)
      v1s.removeItemAt(imin);
    return vmin;
  }

  public class MeshCapperSorter implements Comparator {
    
    @Override
    public int compare(CapVertex v1, CapVertex v2) {
      // first HIGHEST Y to LOWEST Y, then LOWEST X to HIGHEST X
      return (v1.y < v2.y ? 1 : v1.y > v2.y || v1.x < v2.x ? -1
          : v1.x > v2.x ? 1 : 0);
    }  
  }
  
  /**
   * Handle the point; mark as processed.
   * 
   * @param v
   * 
   * @return next point to process
   */
  private CapVertex process(CapVertex v) {
    //
    //                    /\
    //                   /  \ascending
    //       descending /    \
    //                 /      \
    //                /        \
    //              -/----------*-<
    //              /            \
    CapVertex q = v.yxNext;
    v.yxNext = null; // indicates already processed
    if (dumping)
      Logger.info(v.toString());
    if (v.prev == v.next)
      return q; // probably removed by fixEndsAndSortVertices
    boolean isDescending = (v.prev.region != null);
    boolean isAscending = (v.next.region != null);

    if (dumping)
      Logger.info("#" + (isAscending ? v.next.id : "    ") + "    "
          + (isDescending ? v.prev.id : "") + "\n#"
          + (isAscending ? "   \\" : "    ")
          + (isDescending ? "    /\n" : "\n") + "#    " + v.id);

    if (!isDescending && !isAscending) {
      CapVertex last = getLastPoint(v);
      if (last == null) {
        // start vertex -- just create a new region
        newRegion(v);
        return q;
      }
      CapVertex p = processSplit(v, last);
      // patch in new point as the next to process
      p.yxNext = q;
      q = p;
      // process left branch
      isAscending = true;
    }

    // note that a point may be both ascending and descending:

    //
    //                    /\         /\
    //                   /  \       /  \
    //                  /    \     /    \
    //                 /   next   prev   \
    //                /        \ /        \
    //              -/----------*----------\<
    //              /                       \

    if (isDescending) {
      processMonotonic(v, true);
    }
    if (isAscending) {
      processMonotonic(v, false);
    }

    if (isDescending && isAscending) {
      if (v.prev.prev == v.next) {
        // end vertex -- draw last triangle
        lstRegions.removeObj(v.region);
        addTriangle(v.prev, v, v.next, "end");
        
        clearV(v.prev);
        clearV(v.next);
      } else {
        // merge vertex -- linking two separate regions
        // just mark as having no region yet
        v.region = null;
      }

    }
    return q;
  }

  private static void clearV(CapVertex v) {
    if (v != null)
      v.clear();
  }

  /**
   * Process a standard monotonic region, cleaving off as many triangles as
   * possible.
   * 
   * @param v
   * @param isDescending
   */
  private void processMonotonic(CapVertex v, boolean isDescending) {
    CapVertex vEdge = (isDescending ? v.prev : v.next);
    v.region = vEdge.region;
    CapVertex last = v.region[LAST];
    if (last == v) {
      // single triangle processed already by descender
      lstRegions.removeObj(v.region);
      return;
    }
    CapVertex v2, v1;

    if (last == vEdge) {

      // same side

      v1 = last;
      v2 = (isDescending ? v1.prev : v1.next);
      while (v2 != v && v2.yxNext == null
          && isDescending == (v.x > v.interpolateX(v2, v1))) {
        if (isDescending) {
          // same side descending
          //
          //                    /\
          //                  v2  \
          //                  /    \
          //         --(last)v-----------
          //                /        \
          //              -*----------\-<
          //              /            \

          addTriangle(v2, v1, v, "same desc " + v.ipt);
          v1 = v2;
          v2 = v2.prev;
        } else {
          // same side ascending
          //
          //                    /\
          //                   /  v2
          //                  /    \
          //              ----------v(last)--
          //                /        \
          //              ------------*-= 0;) {
      CapVertex[] r = lstRegions.get(i);
      // check that the descending edge is to the left
      CapVertex d = r[DESCENDER];
      if (d == r[ASCENDER])
        continue;
      boolean isEdge = (d.region != null);
      boolean isOK = ((isEdge ? v.interpolateX(d, d.next) : d.x) < v.x);
      if (isEdge && closest != null && closest.x != d.x
          && isOK == (closest.x < d.x)) {
        // d is an unfinished edge on the left 
        // and closest is further to the left
        // or
        // d is an unfinished edge on the right
        // and closest is further to the right
        closest = null;
        ymin = Float.MAX_VALUE;
      }
      if (!isOK)
        continue;
      // check that the ascending edge is to the right
      CapVertex a = r[ASCENDER];
      isEdge = (a.region != null);
      isOK = ((isEdge ? v.interpolateX(a, a.prev) : a.x) >= v.x);
      if (isEdge && closest != null && closest.x != a.x
          && isOK == (closest.x > a.x)) {
        // a is an unfinished edge on the left 
        // and closest is further to the left
        // or 
        // a is an unfinished edge on the right 
        // and closest is further to the right
        closest = null;
        ymin = Float.MAX_VALUE;
      }
      if (!isOK)
        continue;
      if (r[LAST].y < ymin) {
        ymin = r[LAST].y;
        closest = r[LAST];
      }
    }
    return closest;
  }

  /**
   * Check for CCW winding.
   * 
   * @param v0
   * @param v1
   * @param v2
   * @return true if properly wound -- (v1-v0).cross.(v2-v0).dot.norm > 0
   */
  private boolean checkWinding(CapVertex v0, CapVertex v1, CapVertex v2) {
    return (v1.x - v0.x) * (v2.y - v0.y) > (v1.y - v0.y) * (v2.x - v0.x);
  }

  /**
   * Add the triangle and remove v1 from the chain.
   * 
   * @param v0
   * @param v1
   * @param v2
   * @param note
   */
  private void addTriangle(CapVertex v0, CapVertex v1, CapVertex v2, String note) {
    ++nTriangles;
    if (checkWinding(v0, v1, v2)) {
      if (dumping)
        drawTriangle(nTriangles, v0, v1, v2, "red");
      outputTriangle(v0.ipt, v1.ipt, v2.ipt);
    } else if (dumping) {
      // probably a 180-degree triangle, which can happen with
      //
      //         0
      //        /|
      //       / 5
      //      /  |
      //     /   4
      //    /    |
      //   1--2--3

      Logger.info("#!!!BAD WINDING " + note);
    }
    v1.link(null);
  }

  /**
   * for debugging
   * 
   * @param index
   * @param v0
   * @param v1
   * @param v2
   * @param color
   */
  private void drawTriangle(int index, CapVertex v0, CapVertex v1,
                            CapVertex v2, String color) {
    T3 p0 = getInputPoint(v0);
    T3 p1 = getInputPoint(v1);
    T3 p2 = getInputPoint(v2);
    Logger.info("draw " + color + index + "/* " + v0.id + " " + v1.id + " "
        + v2.id + " */" + p0 + p1 + p2 + " color " + color);
  }

  /**
   * A class to provide linked vertices for MeshCapper
   * 
   */
  private class CapVertex extends T3 implements Cloneable {

    /**
     * external reference
     */
    int ipt;

    /**
     * for debugging
     */
    String id = "";

    /**
     * Y-X scan queue forward link
     */
    protected CapVertex yxNext;

    /**
     * edge double links
     */
    CapVertex prev;
    CapVertex next;

    /**
     * dynamic region pointers
     */

    CapVertex[] region;

    CapVertex(T3 p, int i) {
      ipt = i;
      id = "" + i;
      setT(p);
    }

    public CapVertex cloneV() {
      try {
        return (CapVertex) clone();
      } catch (Exception e) {
        return null;
      }
    }

    /**
     * Get interpolated x for the scan line intersection with an edge. This
     * method is used both in finding the last point for a split and for
     * checking winding on same-side addition.
     * 
     * determine
     * 
     * @param v1
     * @param v2
     * @return x
     */
    protected float interpolateX(CapVertex v1, CapVertex v2) {
      float dy12 = v2.y - v1.y;
      float dx12 = v2.x - v1.x;
      return (dy12 != 0 ? v1.x + (y - v1.y) * dx12 / dy12
          : dx12 > 0 ? Float.MAX_VALUE : -Float.MAX_VALUE);
    }

    /**
     * Link this vertex with v or remove it from the chain.
     * 
     * @param v
     *        null to remove
     */
    protected void link(CapVertex v) {
      if (v == null) {
        prev.next = next;
        next.prev = prev;
        clear();
      } else {
        next = v;
        v.prev = this;
      }
    }

    /**
     * Free all links.
     */
    protected void clear() {
      yxNext = next = prev = null;
      region = null;
    }

    /**
     * for debugging
     * 
     * @return listing of vertices currently in a region
     * 
     */
    private String dumpRegion() {
      String s = "\n#REGION d=" + region[MeshCapper.DESCENDER].id + " a="
          + region[MeshCapper.ASCENDER].id + " last="
          + region[MeshCapper.LAST].id + "\n# ";
      CapVertex v = region[MeshCapper.ASCENDER];
      while (true) {
        s += v.id + " ";
        if (v == region[DESCENDER])
          break;
        v = v.next;
      }
      return s + "\n";
    }

    @Override
    public String toString() {
      // for debugging only
      T3 c = (m3 == null ? this : new P3());
      if (m3 != null)
        m3.rotate2(this, c);
      return "draw p" + id + " {" + c.x + " " + c.y + " " + c.z + "} # "
          + (prev == null ? "null" : prev.id)
          + (next == null ? " null" : " " + next.id)
          + (region == null ? "" : dumpRegion());
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy