diff --git a/.gitignore b/.gitignore
index 01d29097..51bef3c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# Folders
+.vs/
+
 # Compiled Object files
 *.slo
 *.lo
@@ -32,4 +35,5 @@ geometry3Sharp/obj
 geometry3Sharp.csproj.user
 bin
 obj
+.vs
 *.meta
diff --git a/README.md b/README.md
index 1a92a056..5a5242a5 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,14 @@
+# A Short Note about the future of geometry3Sharp
+
+I have not been able to work on or maintain geometry3Sharp for the past few years, due to some restrictive employment-contract terms. Various forks now exist that have active maintainers, and I would recommend you consider switching to one of those. In particular I would recommend the geometry4Sharp fork being developed by New Wheel Technology (_who also does C# development consulting, if you are looking for that_):
+
+https://github.com/NewWheelTech/geometry4Sharp
+
 # geometry3Sharp
 
 Open-Source (Boost-license) C# library for geometric computing. 
 
-geometry3Sharp only uses C# language features available in .NET 3.5, so it works with the Mono C# runtime used in Unity 5.x (*NOTE: you must configure Unity for this to work, see note at bottom of this file*). 
+geometry3Sharp is compatible with Unity. Set the G3_USING_UNITY Scripting Define and you will have transparent interop between g3 and Unity vector types (*see details at the very bottom of this README*). Although the library is written for C# 4.5, if you are using the .NET 3.5 Unity runtime, it will still work, just with a few missing features.
 
 Currently there is a small amount of unsafe code, however this code is only used in a few fast-buffer-copy routines, which can be deleted if you need a safe version (eg for Unity web player).
 
@@ -10,6 +16,14 @@ Currently there is a small amount of unsafe code, however this code is only used
 
 Questions? Contact Ryan Schmidt [@rms80](http://www.twitter.com/rms80) / [gradientspace](http://www.gradientspace.com)
 
+# Projects using g3Sharp
+
+* [Gradientspace Cotangent](https://www.cotangent.io/) - 3D printing and Mesh Repair/Modeling Tool
+* [Nia Technologies NiaFit](https://niatech.org/technology/niafit/) - 3D-printed prosthetic and orthotic design
+* [OrthoVR Project](https://orthovrproject.org/) - 3D-printed lower-leg prosthetic design in VR
+* [Archform](https://www.archform.co/) - Clear Dental Aligner design/planning app
+* [Your Project Here?](rms@gradientspace.com) - *we are very excited to hear about your project!*
+
 
 # Credits
 
@@ -23,10 +37,14 @@ The **MeshSignedDistanceGrid** class was implemented based on the C++ [SDFGen](h
 
 Several tutorials for using g3Sharp have been posted on the Gradientspace blog:
 
-- [Creating meshes, Mesh File I/O, Ray/Mesh Intersection and Nearest-Point](http://www.gradientspace.com/tutorials/2017/7/20/basic-mesh-creation-with-g3sharp)
-- [Mesh Simplification with Reducer class](http://www.gradientspace.com/tutorials/2017/8/30/mesh-simplification)
-- [Voxelization/Signed Distance Fields and Marching Cubes Remeshing](http://www.gradientspace.com/tutorials/2017/11/21/signed-distance-fields-tutorial)
-
+- [Creating meshes, Mesh File I/O, Ray/Mesh Intersection and Nearest-Point](http://www.gradientspace.com/tutorials/2017/7/20/basic-mesh-creation-with-g3sharp) - Explains DMesh3 basics, StandardMeshReader, DMeshAABBTree3 ray and point queries and custom traversals
+- [Mesh Simplification with Reducer class](http://www.gradientspace.com/tutorials/2017/8/30/mesh-simplification) - Reducer class, DMesh3.CheckValidity, MeshConstraints
+- [Remeshing and Mesh Constraints](http://www.gradientspace.com/tutorials/2018/7/5/remeshing-and-constraints) - Remesher class, projection targets, MeshConstraints, Unity remeshing animations
+- [Voxelization/Signed Distance Fields and Marching Cubes Remeshing](http://www.gradientspace.com/tutorials/2017/11/21/signed-distance-fields-tutorial) - MeshSignedDistanceGrid, MarchingCubes, DenseGridTrilinearImplicit, generating 3D lattices
+- [3D Bitmaps, Minecraft Cubes, and Mesh Winding Numbers](http://www.gradientspace.com/tutorials/2017/12/14/3d-bitmaps-and-minecraft-meshes) - Bitmap3, VoxelSurfaceGenerator, DMeshAABBTree3 Mesh Winding Number, 
+- [Implicit Surface Modeling](http://www.gradientspace.com/tutorials/2018/2/20/implicit-surface-modeling) - Implicit primitives, voxel/levelset/functional booleans, offsets, and blending, lattice/lightweighting demo
+- [DMesh3: A Dynamic Indexed Triangle Mesh](http://www.gradientspace.com/tutorials/dmesh3) - deep dive into the DMesh3 class's internal data structures and operations
+- [Surfacing Point Sets with Fast Winding Numbers](http://www.gradientspace.com/tutorials/2018/9/14/point-set-fast-winding) - tutorial on the Fast Mesh/PointSet Winding Number, and how to use the g3Sharp implementation
 
 
 # Main Classes
@@ -47,6 +65,13 @@ Several tutorials for using g3Sharp have been posted on the Gradientspace blog:
 - **IndexPriorityQueue**: min-heap priority queue for dense situations (ie small or large number of items in queue)
 - **DijkstraGraphDistance**: compute shortest-path distances between nodes in graph, from seed points. Graph is defined externally by iterators and Func's, so this class can easily be applied to many situations.
 - **SmallListSet**: efficient allocation of a large number of small lists, with initial fixed-size buffer and "spilling" into linked list.
+- **BufferUtil**: utilities for working with arrays. Math on float/double arrays, automatic conversions, byte[] conversions, compression
+- **FileSystemUtils**: utilities for filesystem stuff
+- *g3Iterators*: IEnumerable utils **ConstantItr**, **RemapItr**, IList hacks **MappedList**, **IntSequence**
+- **HashUtil**: **HashBuilder** util for constructing FNV hashes of g3 types
+- **MemoryPool**: basic object pool
+- *ProfileUtil*: code profiling utility **LocalProfiler** supports multiple timers, accumulating, etc
+- *SafeCollections*: **SafeListBuilder** multi-threaded List construction and operator-apply
 
 ## Math
 
@@ -164,6 +189,10 @@ Several tutorials for using g3Sharp have been posted on the Gradientspace blog:
 	 - vertices can be pinned to fixed positions
 	 - vertices can be constrained to an IProjectionTarget - eg 3D polylines, smooth curves, surfaces, etc
     - **MeshConstraintUtil** constructs common constraint situations
+- **RemesherPro**: extension of Remesher that can remesh much more quickly
+    - FastestRemesh() uses active-set queue to converge, instead of fixed full-mesh passes
+    - SharpEdgeReprojectionRemesh() tries to remesh while aligning triangle face normals to the projection target, in an attempt to preserve sharp edges
+    - FastSplitIteration() quickly splits edges to increase available vertex resolution
 - **RegionRemesher**: applies *Remesher* to sub-region of a *DMesh3*, via *DSubmesh3*
     - boundary of sub-region automatically preserved
     - *BackPropropagate()* function integrates submesh back into input mesh
@@ -202,8 +231,10 @@ Several tutorials for using g3Sharp have been posted on the Gradientspace blog:
     - **TubeGenerator**: polygon swept along polyline
     - **Curve3Axis3RevolveGenerator**: 3D polyline revolved around 3D axis
     - **Curve3Curve3RevolveGenerator**: 3D polyline revolved around 3D polyline (!)
+    - **TriangulatedPolygonGenerator**: triangulate 2D polygon-with-holes
     - **VoxelSurfaceGenerator**: generates minecraft-y voxel mesh surface
     - **MarchingCubes**: multi-threaded triangulation of implicit functions / scalar fields
+    - **MarchingCubesPro**: continuation-method approach to marching cubes that explores isosurface from seed points (more efficient but may miss things if seed points are insufficient)
     
 
 ## Mesh Selections
@@ -230,24 +261,44 @@ Several tutorials for using g3Sharp have been posted on the Gradientspace blog:
 - **MeshExtrudeMesh**: extrude all faces of mesh and stitch boundaries w/ triangle strips
 - **MeshICP**: basic iterative-closest-point alignment to target surface
 - **MeshInsertUVPolyCurve**: insert a 2D polyline (optionally closed) into a 2D mesh
+- **MeshInsertPolygon**: insert a 2D polygon-with-holes into a 2D mesh and return set of triangles "inside" polygon
+- **MeshInsertProjectedPolygon**: variant of MeshInsertPolygon that inserts 2D polygon onto 3D mesh surface via projection plane
 - **MeshIterativeSmooth**: standard iterative vertex-laplacian smoothing with uniform, cotan, mean-value weights
 - **MeshLocalParam**: calculate Discrete Exponential Map uv-coords around a point on mesh
 - **MeshLoopClosure**: cap open region of mesh with a plane
 - **MeshLoopSmooth**: smooth an embedded *EdgeLoop* of a mesh
 - **MeshPlaneCut**: cut a mesh with a plane, return new **EdgeLoop**s and **EdgeSpans**, and optionally fill holes
 - **RegionOperator**: support class that makes it easy to extract a submesh and safely re-integrate it back into base mesh. IE like RegionRemesher, but you can do arbitrary changes to the submesh (as long as you preserve boundary).
-- **SimpleHoleFiller**: topological filling of an open boundary edge loop. No attempt to preserve shape whatsoever!
+- **MeshStitchLoops**: Stitch together two edge loops without any constraint that they have the same vertex count
+- **MeshTrimLoop**: trim mesh with 3D polyline curve lying on mesh faces (approximately)
 - **MeshIsoCurve**: compute piecewise-linear iso-curves of a function on a mesh, as a **DGraph3**
+- **MeshTopology**: Extract mesh sharp-edge-path topology based on crease angle
+- **MeshAssembly**: Decompose mesh into submeshes based on connected solids and open patches
+- **MeshSpatialSort**: sorts set of mesh components into "solids" (each solid is outer mesh and contained cavity meshes)
+- **MeshMeshCut**: Cut one mesh with another, and optionally remove contained regions
+- **MeshBoolean**: Apply **MeshMeshCut** to each of a pair of meshes, and then try to resample cut boundaries so they have same vertices. **This is not a robust mesh boolean!**
+- **SimpleHoleFiller**: topological filling of an open boundary edge loop. No attempt to preserve shape whatsoever!
+- **SmoothedHoleFill**: fill hole in mesh smoothly, ie with (approximate) boundary tangent continuity
+- **MinimalHoleFill**: construct "minimal" fill that is often developable (recovers sharp edges well)
+- **PlanarHoleFiller**: fill planar holes in mesh by mapping to 2D, handles nested holes (eg from plane cut through torus)
+- **PlanarSpansFiller**: try to fill disconnected set of planar spans, by chaining them (WIP)
+- **MeshRepairOrientation**: make triangle winding order consistent across mesh connected components (if possible), and then assign global orientation via spatial sorting/nesting
+- **MergeCoincidentEdges**: weld coincident open boundary edges of mesh (more robust than weld vertices!)
+- **RemoveDuplicateTriangles**: remove duplicate triangles of mesh
+- **RemoteOccludedTriangles**: remove triangles that are "occluded" under various definitions
+- **MeshAutoRepair**: apply many of the above algorithms in an attempt to automatically "repair" an input mesh, where "repaired" means the mesh is closed and manifold.
 
 
 ## Spatial Data Structures
 
-- **DMeshAABBTree**: triangle mesh axis-aligned bounding box tree
+- **DMeshAABBTree3**: triangle mesh axis-aligned bounding box tree
 	- bottom-up construction using mesh topology to accelerate leaf node layer
 	- generic traversal interface DoTraversal(TreeTraversal)
-	- Queries for NearestTriangle(point), FindNearestHitTriangle(ray) and FindAllHitTriangles(ray)
-	- TestIntersection(triangle), TestIntersection(other_tree), FindIntersections(other_tree)
-	- IsInside(point)
+	- FindNearestTriangle(point), FindNearestHitTriangle(ray) and FindAllHitTriangles(ray), FindNearestVertex(point)
+	- FindNearestTriangles(other_tree)
+	- TestIntersection(triangle), TestIntersection(other_tree), FindAllIntersections(other_tree)
+	- IsInside(point), WindingNumber(point), FastWindingNumber(point)
+- **PointAABBTree3**: point variant of DMeshAABBTree3, with PointSet Fast Winding Number
 - **Polygon2dBoxTree**: 2D segment bbox-tree, distance query
 - **PointHashGrid2d**, **SegmentHashGrid2d**: hash tables for 2D geometry elements
 - **PointHashGrid3d**: hash tables for 3D geometry elements
@@ -256,6 +307,9 @@ Several tutorials for using g3Sharp have been posted on the Gradientspace blog:
 - **Bitmap3**: 3D dense bitmap
 - **BiGrid3**: two-level DSparseGrid3
 - **MeshSignedDistanceGrid**: 3D fast-marching construction of narrow-band level set / voxel-distance-field for mesh
+- **MeshScalarSamplingGrid**: Samples scalar function on 3D grid. Can sample full grid or narrow band around specific iso-contour
+- **MeshWindingNumberGrid**: MeshScalarSamplingGrid variant specifically for computing narrow-band Mesh Winding Number field on meshes with holes (finds narrow-band in hole regions via flood-fill)
+- **CachingMeshSDF**: variant of MeshSignedDistanceGrid that does lazy evaluation of distances (eg for use with continuation-method MarchingCubesPro)
 - **IProjectionTarget** implementations for DCurve3, DMesh3, Plane3, Circle3d, Cylinder3d, etc, for use w/ reprojection in Remesher and other algorithms
 - **IIntersectionTarget** implementations for DMesh3, transformed DMesh3, Plane3
 
@@ -302,6 +356,7 @@ Several tutorials for using g3Sharp have been posted on the Gradientspace blog:
 
 - **Cylinder3d**
 - **DenseGridTrilinearImplicit**: trilinear interpolant of 3D grid
+- **CachingDenseGridTrilinearImplicit**: variant of DenseGridTrilinearImplicit that does lazy evaluation of grid values based on an implicit function
 
 
 ## I/O    
diff --git a/appveyor.yml b/appveyor.yml
index 59d2adc7..9a1be15f 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -25,7 +25,7 @@ deploy:
   provider: NuGet
 #  server:                  # remove to push to NuGet.org
   api_key:
-    secure: NwCvdkjy/Jpu+Cev4PeXnCdyEtf5F5O4bDtBoXisEKkez6R+U5Wk12hdHNa1NTFT
+    secure: DIcqDc7g8mWxhl3/G0MxoGVSnRJzdMvPZdJM7NbYjuy1tFhcF8hWA+nNoGntESX7
   on:
     appveyor_repo_tag: true	
   skip_symbols: false
diff --git a/color/Colorf.cs b/color/Colorf.cs
index 46d2e745..b113bb4e 100644
--- a/color/Colorf.cs
+++ b/color/Colorf.cs
@@ -73,7 +73,10 @@ public void Subtract(Colorf o)
         {
             r -= o.r; g -= o.g; b -= o.b; a -= o.a;
         }
-
+        public Colorf WithAlpha(float newAlpha)
+        {
+            return new Colorf(r, g, b, newAlpha);
+        }
 
 
         public static Colorf operator -(Colorf v)
@@ -222,6 +225,12 @@ public string ToString(string fmt)
 
 
 
+        // default colors
+        static readonly public Colorf StandardBeige = new Colorf(0.75f, 0.75f, 0.5f);
+        static readonly public Colorf SelectionGold = new Colorf(1.0f, 0.6f, 0.05f);
+        static readonly public Colorf PivotYellow = new Colorf(1.0f, 1.0f, 0.05f);
+
+
 
         // allow conversion to/from Vector3f
         public static implicit operator Vector3f(Colorf c)
diff --git a/comp_geom/GraphCells2d.cs b/comp_geom/GraphCells2d.cs
index 9e8f501b..e4c794d6 100644
--- a/comp_geom/GraphCells2d.cs
+++ b/comp_geom/GraphCells2d.cs
@@ -62,7 +62,7 @@ public void FindCells()
 
                 int start_vid = idx.a;
                 int wid = idx.b;
-                int e0 = wedges[start_vid][wid].a;
+                int e0 = wedges[start_vid][wid].a; e0 = e0+1-1;   // get rid of unused variable warning, want to keep this for debugging
                 int e1 = wedges[start_vid][wid].b;
 
                 loopv.Clear();
diff --git a/comp_geom/SphericalFibonacciPointSet.cs b/comp_geom/SphericalFibonacciPointSet.cs
new file mode 100644
index 00000000..fe9e8015
--- /dev/null
+++ b/comp_geom/SphericalFibonacciPointSet.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+
+namespace g3
+{
+    /// <summary>
+    /// A Spherical Fibonacci Point Set is a set of points that are roughly evenly distributed on
+    /// a sphere. Basically the points lie on a spiral, see pdf below.
+    /// The i-th SF point of an N-point set can be calculated directly.
+    /// For a given (normalized) point P, finding the nearest SF point (ie mapping back to i)
+    /// can be done in constant time.
+    /// 
+    /// math from http://lgdv.cs.fau.de/uploads/publications/spherical_fibonacci_mapping_opt.pdf
+    /// </summary>
+    public class SphericalFibonacciPointSet
+    {
+        public int N = 64;
+
+        public SphericalFibonacciPointSet(int n = 64) {
+            N = n;
+        }
+
+
+        public int Count { get { return N; } }
+
+
+        /// <summary>
+        /// Compute i'th spherical point
+        /// </summary>
+        public Vector3d Point(int i)
+        {
+            Util.gDevAssert(i < N);
+            double div = (double)i / PHI;
+            double phi = MathUtil.TwoPI * (div - Math.Floor(div));
+            double cos_phi = Math.Cos(phi), sin_phi = Math.Sin(phi);
+
+            double z = 1.0 - (2.0 * (double)i + 1.0) / (double)N;
+            double theta = Math.Acos(z);
+            double sin_theta = Math.Sin(theta);
+
+            return new Vector3d(cos_phi * sin_theta, sin_phi * sin_theta, z);
+        }
+        public Vector3d this[int i] {
+            get { return Point(i); }
+        }
+
+
+        /// <summary>
+        /// Find index of nearest point-set point for input arbitrary point
+        /// </summary>
+        public int NearestPoint(Vector3d p, bool bIsNormalized = false)
+        {
+            if (bIsNormalized)
+                return inverseSF(ref p);
+            p.Normalize();
+            return inverseSF(ref p);
+        }
+
+
+
+
+        static readonly double PHI = (Math.Sqrt(5.0) + 1.0) / 2.0;
+
+        double madfrac(double a, double b)
+        {
+            //#define madfrac(A,B) mad((A),(B),-floor((A)*(B)))
+            return a * b + -Math.Floor(a * b);
+        }
+
+        /// <summary>
+        /// This computes mapping from p to i. Note that the code in the original PDF is HLSL shader code.
+        /// I have ported here to comparable C# functions. *However* the PDF also explains some assumptions
+        /// made about what certain operators return in different cases (particularly NaN handling).
+        /// I have not yet tested these cases to make sure C# behavior is the same (not sure when they happen).
+        /// </summary>
+        int inverseSF(ref Vector3d p)
+        {
+            double phi = Math.Min(Math.Atan2(p.y, p.x), Math.PI);
+            double cosTheta = p.z;
+            double k = Math.Max(2.0, Math.Floor(
+                Math.Log(N * Math.PI * Math.Sqrt(5.0) * (1.0 - cosTheta*cosTheta)) / Math.Log(PHI*PHI)));
+            double Fk = Math.Pow(PHI, k) / Math.Sqrt(5.0);
+
+            //double F0 = round(Fk), F1 = round(Fk * PHI);
+            double F0 = Math.Round(Fk), F1 = Math.Round(Fk * PHI);
+
+            Matrix2d B = new Matrix2d(
+                2 * Math.PI * madfrac(F0 + 1, PHI - 1) - 2 * Math.PI * (PHI - 1),
+                2 * Math.PI * madfrac(F1 + 1, PHI - 1) - 2 * Math.PI * (PHI - 1),
+                -2 * F0 / N, -2 * F1 / N);
+            Matrix2d invB = B.Inverse();
+
+            //Vector2d c = floor(mul(invB, double2(phi, cosTheta - (1 - 1.0/N))));
+            Vector2d c = new Vector2d(phi, cosTheta - (1 - 1.0 / N));
+            c = invB * c;
+            c.x = Math.Floor(c.x); c.y = Math.Floor(c.y);
+
+            double d = double.PositiveInfinity, j = 0;
+            for (uint s = 0; s < 4; ++s) {
+                Vector2d cosTheta_second = new Vector2d(s%2, s/2) + c;
+                cosTheta = B.Row(1).Dot(cosTheta_second) + (1-1.0/N);
+                cosTheta = MathUtil.Clamp(cosTheta, -1.0, +1.0)*2.0 - cosTheta;
+                double i = Math.Floor(N*0.5 - cosTheta*N*0.5);
+                phi = 2.0 * Math.PI * madfrac(i, PHI - 1);
+                cosTheta = 1.0 - (2.0 * i + 1.0) * (1.0 / N); // rcp(n);
+                double sinTheta = Math.Sqrt(1.0 - cosTheta * cosTheta);
+                Vector3d q = new Vector3d(
+                    Math.Cos(phi) * sinTheta,
+                    Math.Sin(phi) * sinTheta,
+                    cosTheta);
+                double squaredDistance = Vector3d.Dot(q - p, q - p);
+                if (squaredDistance < d) {
+                    d = squaredDistance;
+                    j = i;
+                }
+            }
+
+            // [TODO] should we be clamping this??
+            return (int)j;
+        }

+
+
+
+
+    }
+}
diff --git a/core/BufferUtil.cs b/core/BufferUtil.cs
index b1511875..59bba42b 100644
--- a/core/BufferUtil.cs
+++ b/core/BufferUtil.cs
@@ -1,14 +1,19 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.IO;
+using System.IO.Compression;
 using System.Text;
 
 namespace g3
 {
-    //
-    // convenience functions for setting values in an array in sets of 2/3
-    //   (eg for arrays that are actually a list of vectors)
-    //
+    /// <summary>
+    /// Convenience functions for working with arrays. 
+    ///    - Math functions on arrays of floats/doubles
+    ///    - "automatic" conversion from IEnumerable<T> (via introspection)
+    ///    - byte[] conversions
+    ///    - zlib compress/decompress byte[] buffers
+    /// </summary>
     public class BufferUtil
     {
         static public void SetVertex3(double[] v, int i, double x, double y, double z) {
@@ -302,14 +307,260 @@ static public Index3i[] ToIndex3i<T>(IEnumerable<T> values) {
 
 
 
+        /// <summary>
+        /// convert byte array to int array
+        /// </summary>
+        static public int[] ToInt(byte[] buffer)
+        {
+            int sz = sizeof(int);
+            int Nvals = buffer.Length / sz;
+            int[] v = new int[Nvals];
+            for (int i = 0; i < Nvals; i++) {
+                v[i] = BitConverter.ToInt32(buffer, i * sz);
+            }
+            return v;
+        }
+
+
+        /// <summary>
+        /// convert byte array to short array
+        /// </summary>
+        static public short[] ToShort(byte[] buffer)
+        {
+            int sz = sizeof(short);
+            int Nvals = buffer.Length / sz;
+            short[] v = new short[Nvals];
+            for (int i = 0; i < Nvals; i++) {
+                v[i] = BitConverter.ToInt16(buffer, i * sz);
+            }
+            return v;
+        }
+
+
+        /// <summary>
+        /// convert byte array to double array
+        /// </summary>
+        static public double[] ToDouble(byte[] buffer)
+        {
+            int sz = sizeof(double);
+            int Nvals = buffer.Length / sz;
+            double[] v = new double[Nvals];
+            for (int i = 0; i < Nvals; i++) {
+                v[i] = BitConverter.ToDouble(buffer, i * sz);
+            }
+            return v;
+        }
+
+
+        /// <summary>
+        /// convert byte array to float array
+        /// </summary>
+        static public float[] ToFloat(byte[] buffer)
+        {
+            int sz = sizeof(float);
+            int Nvals = buffer.Length / sz;
+            float[] v = new float[Nvals];
+            for (int i = 0; i < Nvals; i++) {
+                v[i] = BitConverter.ToSingle(buffer, i * sz);
+            }
+            return v;
+        }
+
+
+        /// <summary>
+        /// convert byte array to VectorArray3d
+        /// </summary>
+        static public VectorArray3d ToVectorArray3d(byte[] buffer)
+        {
+            int sz = sizeof(double);
+            int Nvals = buffer.Length / sz;
+            int Nvecs = Nvals / 3;
+            VectorArray3d v = new VectorArray3d(Nvecs);
+            for (int i = 0; i < Nvecs; i++) {
+                double x = BitConverter.ToDouble(buffer, (3 * i) * sz);
+                double y = BitConverter.ToDouble(buffer, (3 * i + 1) * sz);
+                double z = BitConverter.ToDouble(buffer, (3 * i + 2) * sz);
+                v.Set(i, x, y, z);
+            }
+            return v;
+        }
+
+
+
+        /// <summary>
+        /// convert byte array to VectorArray2f
+        /// </summary>
+        static public VectorArray2f ToVectorArray2f(byte[] buffer)
+        {
+            int sz = sizeof(float);
+            int Nvals = buffer.Length / sz;
+            int Nvecs = Nvals / 2;
+            VectorArray2f v = new VectorArray2f(Nvecs);
+            for (int i = 0; i < Nvecs; i++) {
+                float x = BitConverter.ToSingle(buffer, (2 * i) * sz);
+                float y = BitConverter.ToSingle(buffer, (2 * i + 1) * sz);
+                v.Set(i, x, y);
+            }
+            return v;
+        }
+
+        /// <summary>
+        /// convert byte array to VectorArray3f
+        /// </summary>
+        static public VectorArray3f ToVectorArray3f(byte[] buffer)
+        {
+            int sz = sizeof(float);
+            int Nvals = buffer.Length / sz;
+            int Nvecs = Nvals / 3;
+            VectorArray3f v = new VectorArray3f(Nvecs);
+            for (int i = 0; i < Nvecs; i++) {
+                float x = BitConverter.ToSingle(buffer, (3 * i) * sz);
+                float y = BitConverter.ToSingle(buffer, (3 * i + 1) * sz);
+                float z = BitConverter.ToSingle(buffer, (3 * i + 2) * sz);
+                v.Set(i, x, y, z);
+            }
+            return v;
+        }
+
+
+
+
+        /// <summary>
+        /// convert byte array to VectorArray3i
+        /// </summary>
+        static public VectorArray3i ToVectorArray3i(byte[] buffer)
+        {
+            int sz = sizeof(int);
+            int Nvals = buffer.Length / sz;
+            int Nvecs = Nvals / 3;
+            VectorArray3i v = new VectorArray3i(Nvecs);
+            for (int i = 0; i < Nvecs; i++) {
+                int x = BitConverter.ToInt32(buffer, (3 * i) * sz);
+                int y = BitConverter.ToInt32(buffer, (3 * i + 1) * sz);
+                int z = BitConverter.ToInt32(buffer, (3 * i + 2) * sz);
+                v.Set(i, x, y, z);
+            }
+            return v;
+        }
+
+
+        /// <summary>
+        /// convert byte array to IndexArray4i
+        /// </summary>
+        static public IndexArray4i ToIndexArray4i(byte[] buffer)
+        {
+            int sz = sizeof(int);
+            int Nvals = buffer.Length / sz;
+            int Nvecs = Nvals / 4;
+            IndexArray4i v = new IndexArray4i(Nvecs);
+            for (int i = 0; i < Nvecs; i++) {
+                int a = BitConverter.ToInt32(buffer, (4 * i) * sz);
+                int b = BitConverter.ToInt32(buffer, (4 * i + 1) * sz);
+                int c = BitConverter.ToInt32(buffer, (4 * i + 2) * sz);
+                int d = BitConverter.ToInt32(buffer, (4 * i + 3) * sz);
+                v.Set(i, a, b, c, d);
+            }
+            return v;
+        }
+
+
+        /// <summary>
+        /// convert int array to bytes
+        /// </summary>
+        static public byte[] ToBytes(int[] array)
+        {
+            byte[] result = new byte[array.Length * sizeof(int)];
+            Buffer.BlockCopy(array, 0, result, 0, result.Length);
+            return result;
+        }
+
+        /// <summary>
+        /// convert short array to bytes
+        /// </summary>
+        static public byte[] ToBytes(short[] array)
+        {
+            byte[] result = new byte[array.Length * sizeof(short)];
+            Buffer.BlockCopy(array, 0, result, 0, result.Length);
+            return result;
+        }
+
+        /// <summary>
+        /// convert float array to bytes
+        /// </summary>
+        static public byte[] ToBytes(float[] array)
+        {
+            byte[] result = new byte[array.Length * sizeof(float)];
+            Buffer.BlockCopy(array, 0, result, 0, result.Length);
+            return result;
+        }
+
+        /// <summary>
+        /// convert double array to bytes
+        /// </summary>
+        static public byte[] ToBytes(double[] array)
+        {
+            byte[] result = new byte[array.Length * sizeof(double)];
+            Buffer.BlockCopy(array, 0, result, 0, result.Length);
+            return result;
+        }
+
+
+
+
+        /// <summary>
+        /// Compress a byte buffer using Deflate/ZLib compression. 
+        /// </summary>
+        static public byte[] CompressZLib(byte[] buffer, bool bFast)
+        {
+            MemoryStream ms = new MemoryStream();
+#if G3_USING_UNITY && (NET_2_0 || NET_2_0_SUBSET)
+            DeflateStream zip = new DeflateStream(ms, CompressionMode.Compress);
+#else
+            DeflateStream zip = new DeflateStream(ms, (bFast) ? CompressionLevel.Fastest : CompressionLevel.Optimal, true);
+#endif
+            zip.Write(buffer, 0, buffer.Length);
+            zip.Close();
+            ms.Position = 0;
+
+            byte[] compressed = new byte[ms.Length];
+            ms.Read(compressed, 0, compressed.Length);
+
+            byte[] zBuffer = new byte[compressed.Length + 4];
+            Buffer.BlockCopy(compressed, 0, zBuffer, 4, compressed.Length);
+            Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zBuffer, 0, 4);
+            return zBuffer;
+        }
+
+
+        /// <summary>
+        /// Decompress a byte buffer that has been compressed using Deflate/ZLib compression
+        /// </summary>
+        static public byte[] DecompressZLib(byte[] zBuffer)
+        {
+            MemoryStream ms = new MemoryStream();
+            int msgLength = BitConverter.ToInt32(zBuffer, 0);
+            ms.Write(zBuffer, 4, zBuffer.Length - 4);
+
+            byte[] buffer = new byte[msgLength];
+
+            ms.Position = 0;
+            DeflateStream zip = new DeflateStream(ms, CompressionMode.Decompress);
+            zip.Read(buffer, 0, buffer.Length);
+
+            return buffer;
+        }
+
+
     }
 
 
 
 
-    // utility class for porting C++ code that uses this kind of idiom:
-    //    T * ptr = &array[i];
-    //    ptr[k] = value
+    /// <summary>
+    /// utility class for porting C++ code that uses this kind of idiom:
+    ///    T * ptr = &array[i];
+    ///    ptr[k] = value
+    /// </summary>
     public struct ArrayAlias<T>
     {
         public T[] Source;
diff --git a/core/DVector.cs b/core/DVector.cs
index 3c488093..fcda514e 100644
--- a/core/DVector.cs
+++ b/core/DVector.cs
@@ -1,7 +1,6 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
-using System.Linq;
-using System.Text;
 
 namespace g3
 {
@@ -12,7 +11,7 @@ namespace g3
     //   - this[] operator does not check bounds, so it can write to any valid Block
     //   - some fns discard Blocks beyond iCurBlock
     //   - wtf...
-    public class DVector<T>
+    public class DVector<T> : IEnumerable<T>
     {
         List<T[]> Blocks;
         int iCurBlock;
@@ -53,7 +52,6 @@ public DVector(IEnumerable<T> init)
             iCurBlockUsed = 0;
             Blocks = new List<T[]>();
             Blocks.Add(new T[nBlockSize]);
-            // AAAHHH this could be so more efficient...
             foreach (T v in init)
                 Add(v);
         }
@@ -149,6 +147,9 @@ public void insertAt(T value, int index) {
 
 
         public void resize(int count) {
+            if (Length == count)
+                return;
+
             // figure out how many segments we need
             int nNumSegs = 1 + (int)count / nBlockSize;
 
@@ -177,6 +178,24 @@ public void resize(int count) {
         }
 
 
+        public void copy(DVector<T> copyIn)
+        {
+            if (this.Blocks != null && copyIn.Blocks.Count == this.Blocks.Count) {
+                int N = copyIn.Blocks.Count;
+                for (int k = 0; k < N; ++k)
+                    Array.Copy(copyIn.Blocks[k], this.Blocks[k], copyIn.Blocks[k].Length);
+                iCurBlock = copyIn.iCurBlock;
+                iCurBlockUsed = copyIn.iCurBlockUsed;
+            } else {
+                resize(copyIn.size);
+                int N = copyIn.Blocks.Count;
+                for (int k = 0; k < N; ++k)
+                    Array.Copy(copyIn.Blocks[k], this.Blocks[k], copyIn.Blocks[k].Length);
+                iCurBlock = copyIn.iCurBlock;
+                iCurBlockUsed = copyIn.iCurBlockUsed;
+            }
+        }
+
 
 
         public T this[int i]
@@ -372,6 +391,22 @@ public static unsafe void FastGetBuffer(DVector<int> v, int * pBuffer)
 
 
 
+        public IEnumerator<T> GetEnumerator() {
+            for (int bi = 0; bi < iCurBlock; ++bi) {
+                T[] block = Blocks[bi];
+                for (int k = 0; k < nBlockSize; ++k)
+                    yield return block[k];
+            }
+            T[] lastblock = Blocks[iCurBlock];
+            for (int k = 0; k < iCurBlockUsed; ++k)
+                yield return lastblock[k];
+        }
+        IEnumerator IEnumerable.GetEnumerator() {
+            return GetEnumerator();
+        }
+
+
+
         // block iterator
         public struct DBlock
         {
diff --git a/core/DijkstraGraphDistance.cs b/core/DijkstraGraphDistance.cs
index 80a17de6..63bf25b5 100644
--- a/core/DijkstraGraphDistance.cs
+++ b/core/DijkstraGraphDistance.cs
@@ -14,10 +14,12 @@ namespace g3
     /// Construction is somewhat complicated, but see shortcut static
     /// methods at end of file for common construction cases:
     ///   - MeshVertices(mesh) - compute on vertices of mesh
+    ///   - MeshVertices(mesh) - compute on vertices of mesh
     /// 
     /// </summary>
     public class DijkstraGraphDistance
     {
+        public const float InvalidValue = float.MaxValue;
 
         /// <summary>
         /// if you enable this, then you can call GetOrder()
@@ -58,7 +60,7 @@ public bool Equals(GraphNodeStruct other)
             {
                 return id == other.id;
             }
-            public static readonly GraphNodeStruct Zero = new GraphNodeStruct() { id = -1, parent = -1, distance = float.MaxValue, frozen = false };
+            public static readonly GraphNodeStruct Zero = new GraphNodeStruct() { id = -1, parent = -1, distance = InvalidValue, frozen = false };
         }
 
 
@@ -113,10 +115,50 @@ public DijkstraGraphDistance(int nMaxID, bool bSparse,
                 foreach (var v in seeds)
                     AddSeed((int)v.x, (float)v.y);
             }
+        }
+
 
+        /// <summary>
+        /// shortcut to construct graph for mesh vertices
+        /// </summary>
+        public static DijkstraGraphDistance MeshVertices(DMesh3 mesh, bool bSparse = false)
+        {
+            return (bSparse) ?
+                new DijkstraGraphDistance( mesh.MaxVertexID, true,
+                (id) => { return mesh.IsVertex(id); },
+                (a, b) => { return (float)mesh.GetVertex(a).Distance(mesh.GetVertex(b)); },
+                mesh.VtxVerticesItr, null) 
+            :  new DijkstraGraphDistance( mesh.MaxVertexID, false,
+                (id) => { return true; },
+                (a, b) => { return (float)mesh.GetVertex(a).Distance(mesh.GetVertex(b)); },
+                mesh.VtxVerticesItr, null);
         }
 
 
+
+		/// <summary>
+		/// shortcut to construct graph for mesh triangles
+		/// </summary>
+		public static DijkstraGraphDistance MeshTriangles(DMesh3 mesh, bool bSparse = false)
+		{
+			Func<int, int, float> tri_dist_f = (a, b) => {
+				return (float)mesh.GetTriCentroid(a).Distance(mesh.GetTriCentroid(b));
+			};
+
+			return (bSparse) ?
+				new DijkstraGraphDistance(mesh.MaxTriangleID, true,
+				(id) => { return mesh.IsTriangle(id); }, tri_dist_f,
+				mesh.TriTrianglesItr, null)
+			: new DijkstraGraphDistance(mesh.MaxTriangleID, false,
+				(id) => { return true; }, tri_dist_f,
+				mesh.TriTrianglesItr, null);
+		}
+
+
+
+        /// <summary>
+        /// reset internal data structures/etc
+        /// </summary>
         public void Reset()
         {
             if ( SparseNodes != null ) {
@@ -145,7 +187,7 @@ public void AddSeed(int id, float seed_dist)
                 SparseQueue.Enqueue(g, seed_dist);
             } else {
                 Debug.Assert(DenseQueue.Contains(id) == false);
-                enqueue_node_dense(id, seed_dist);
+                enqueue_node_dense(id, seed_dist, -1);
             }
             Seeds.Add(id);
         }
@@ -242,6 +284,121 @@ protected void ComputeToMaxDistance_Dense(float fMaxDistance)
         }
 
 
+
+
+        /// <summary>
+        /// Compute distances until node_id is frozen, or (optional) max distance is reached
+        /// Terminates early, so Queue may not be empty
+        /// [TODO] can reimplement this w/ internal call to ComputeToNode(func) ?
+        /// </summary>
+        public void ComputeToNode(int node_id, float fMaxDistance = InvalidValue)
+        {
+            if (TrackOrder == true)
+                order = new List<int>();
+
+            if (SparseNodes != null)
+                ComputeToNode_Sparse(node_id, fMaxDistance);
+            else
+                ComputeToNode_Dense(node_id, fMaxDistance);
+        }
+        protected void ComputeToNode_Sparse(int node_id, float fMaxDistance)
+        {
+            while (SparseQueue.Count > 0) {
+                GraphNode g = SparseQueue.Dequeue();
+                max_value = Math.Max(g.priority, max_value);
+                if (max_value > fMaxDistance)
+                    return;
+                g.frozen = true;
+                if (TrackOrder)
+                    order.Add(g.id);
+                if (g.id == node_id)
+                    return;
+                update_neighbours_sparse(g);
+            }
+        }
+        protected void ComputeToNode_Dense(int node_id, float fMaxDistance)
+        {
+            while (DenseQueue.Count > 0) {
+                float idx_priority = DenseQueue.FirstPriority;
+                max_value = Math.Max(idx_priority, max_value);
+                if (max_value > fMaxDistance)
+                    return;
+                int idx = DenseQueue.Dequeue();
+                GraphNodeStruct g = DenseNodes[idx];
+                g.frozen = true;
+                if (TrackOrder)
+                    order.Add(g.id);
+                g.distance = max_value;
+                DenseNodes[idx] = g;
+                if (g.id == node_id)
+                    return;
+                update_neighbours_dense(g.id);
+            }
+        }
+
+
+
+
+
+
+
+        /// <summary>
+        /// Compute distances until node_id is frozen, or (optional) max distance is reached
+        /// Terminates early, so Queue may not be empty
+        /// </summary>
+        public int ComputeToNode(Func<int, bool> terminatingNodeF, float fMaxDistance = InvalidValue)
+        {
+            if (TrackOrder == true)
+                order = new List<int>();
+
+            if (SparseNodes != null)
+                return ComputeToNode_Sparse(terminatingNodeF, fMaxDistance);
+            else
+                return ComputeToNode_Dense(terminatingNodeF, fMaxDistance);
+        }
+        protected int ComputeToNode_Sparse(Func<int, bool> terminatingNodeF, float fMaxDistance)
+        {
+            while (SparseQueue.Count > 0) {
+                GraphNode g = SparseQueue.Dequeue();
+                max_value = Math.Max(g.priority, max_value);
+                if (max_value > fMaxDistance)
+                    return -1;
+                g.frozen = true;
+                if (TrackOrder)
+                    order.Add(g.id);
+                if (terminatingNodeF(g.id))
+                    return g.id;
+                update_neighbours_sparse(g);
+            }
+            return -1;
+        }
+        protected int ComputeToNode_Dense(Func<int, bool> terminatingNodeF, float fMaxDistance)
+        {
+            while (DenseQueue.Count > 0) {
+                float idx_priority = DenseQueue.FirstPriority;
+                max_value = Math.Max(idx_priority, max_value);
+                if (max_value > fMaxDistance)
+                    return -1;
+                int idx = DenseQueue.Dequeue();
+                GraphNodeStruct g = DenseNodes[idx];
+                g.frozen = true;
+                if (TrackOrder)
+                    order.Add(g.id);
+                g.distance = max_value;
+                DenseNodes[idx] = g;
+                if (terminatingNodeF(g.id))
+                    return g.id;
+                update_neighbours_dense(g.id);
+            }
+            return -1;
+        }
+
+
+
+
+
+
+
         /// <summary>
         /// Get the maximum distance encountered during the Compute()
         /// </summary>
@@ -251,23 +408,27 @@ public float MaxDistance {
 
 
         /// <summary>
-        /// Get the computed distance at node id. returns float.MaxValue if node was not computed.
+        /// Get the computed distance at node id. returns InvalidValue if node was not computed.
         /// </summary>
         public float GetDistance(int id)
         {
             if (SparseNodes != null) {
                 GraphNode g = SparseNodes[id];
                 if (g == null)
-                    return float.MaxValue;
+                    return InvalidValue;
                 return g.priority;
             } else {
                 GraphNodeStruct g = DenseNodes[id];
-                return (g.frozen) ? g.distance : float.MaxValue;
+                return (g.frozen) ? g.distance : InvalidValue;
             }
         }
 
 
 
+        /// <summary>
+        /// Get (internal) list of frozen nodes in increasing distance-order.
+        /// Requries that TrackOrder=true before Compute call.
+        /// </summary>
         public List<int> GetOrder()
         {
             if (TrackOrder == false)
@@ -277,6 +438,43 @@ public List<int> GetOrder()
 
 
 
+        /// <summary>
+        /// Walk from node fromv back to the (graph-)nearest seed point.
+        /// </summary>
+        public bool GetPathToSeed(int fromv, List<int> path)
+        {
+            if ( SparseNodes != null ) {
+                GraphNode g = get_node(fromv);
+                if (g.frozen == false)
+                    return false;
+                path.Add(fromv);
+                while (g.parent != null) {
+                    path.Add(g.parent.id);
+                    g = g.parent;
+                }
+                return true;
+            } else {
+                GraphNodeStruct g = DenseNodes[fromv];
+                if (g.frozen == false)
+                    return false;
+                path.Add(fromv);
+                while ( g.parent != -1 ) {
+                    path.Add(g.parent);
+                    g = DenseNodes[g.parent];
+                }
+                return true;
+            }
+        }
+
+
+
+
+        /*
+         * Internals below here
+         */
+
+
+
         GraphNode get_node(int id)
         {
             GraphNode g = SparseNodes[id];
@@ -303,12 +501,16 @@ void update_neighbours_sparse(GraphNode parent)
                     continue;
 
                 float nbr_dist = NodeDistanceF(parent.id, nbr_id) + cur_dist;
+                if (nbr_dist == InvalidValue)
+                    continue;
+
                 if (SparseQueue.Contains(nbr)) {
                     if (nbr_dist < nbr.priority) {
                         nbr.parent = parent;
                         SparseQueue.Update(nbr, nbr_dist);
                     }
                 } else {
+                    nbr.parent = parent;
                     SparseQueue.Enqueue(nbr, nbr_dist);
                 }
             }
@@ -317,9 +519,9 @@ void update_neighbours_sparse(GraphNode parent)
 
 
 
-        void enqueue_node_dense(int id, float dist)
+        void enqueue_node_dense(int id, float dist, int parent_id)
         {
-            GraphNodeStruct g = new GraphNodeStruct(id, -1, dist);
+            GraphNodeStruct g = new GraphNodeStruct(id, parent_id, dist);
             DenseNodes[id] = g;
             DenseQueue.Insert(id, dist);
         }
@@ -337,6 +539,9 @@ void update_neighbours_dense(int parent_id)
                     continue;
 
                 float nbr_dist = NodeDistanceF(parent_id, nbr_id) + cur_dist;
+                if (nbr_dist == InvalidValue)
+                    continue;
+
                 if (DenseQueue.Contains(nbr_id)) {
                     if (nbr_dist < nbr.distance) {
                         nbr.parent = parent_id;
@@ -344,24 +549,12 @@ void update_neighbours_dense(int parent_id)
                         DenseNodes[nbr_id] = nbr;
                     }
                 } else {
-                    enqueue_node_dense(nbr_id, nbr_dist);
+                    enqueue_node_dense(nbr_id, nbr_dist, parent_id);
                 }
             }
         }
 
 
 
-        /// <summary>
-        /// shortcut to setup functions for mesh vertices
-        /// </summary>
-        public static DijkstraGraphDistance MeshVertices(DMesh3 mesh)
-        {
-            return new DijkstraGraphDistance(
-                mesh.MaxVertexID, false,
-                (id) => { return true; },
-                (a, b) => { return (float)mesh.GetVertex(a).Distance(mesh.GetVertex(b)); },
-                mesh.VtxVerticesItr);
-        }
-
     }
 }
diff --git a/core/IndexPriorityQueue.cs b/core/IndexPriorityQueue.cs
index 397c79a7..059c965f 100644
--- a/core/IndexPriorityQueue.cs
+++ b/core/IndexPriorityQueue.cs
@@ -163,6 +163,18 @@ public void Update(int id, float priority)
         }
 
 
+        /// <summary>
+        /// Query the priority at node id, assuming it exists in queue
+        /// </summary>
+        public float GetPriority(int id)
+        {
+            if (EnableDebugChecks && Contains(id) == false)
+                throw new Exception("IndexPriorityQueue.Update: tried to get priorty of node that does not exist in queue!");
+            int iNode = id_to_index[id];
+            return nodes[iNode].priority;
+        }
+
+
 
         public IEnumerator<int> GetEnumerator()
         {
diff --git a/core/ProfileUtil.cs b/core/ProfileUtil.cs
index 1db33bc7..87d4e4a6 100644
--- a/core/ProfileUtil.cs
+++ b/core/ProfileUtil.cs
@@ -50,14 +50,23 @@ public void Reset()
 
         public string AccumulatedString
         {
-            get { return string.Format("{0:ss}.{0:fffffff}", Accumulated); }
+            get { return string.Format(TimeFormatString(Accumulated), Accumulated); }
         }
         public override string ToString()
         {
             TimeSpan t = Watch.Elapsed;
-            return string.Format("{0:ss}.{0:fffffff}", Watch.Elapsed);
+            return string.Format(TimeFormatString(Accumulated), Watch.Elapsed);
         }
 
+        public static string TimeFormatString(TimeSpan span)
+        {
+            if (span.Minutes > 0)
+                return minute_format;
+            else
+                return second_format;
+        }
+        const string minute_format = "{0:mm}:{0:ss}.{0:fffffff}";
+        const string second_format = "{0:ss}.{0:fffffff}";
     }
 
 
@@ -104,9 +113,9 @@ public void StopAll()
         }
 
 
-        public void StopAndAccumulate(string label)
+        public void StopAndAccumulate(string label, bool bReset = false)
         {
-            Timers[label].Accumulate();
+            Timers[label].Accumulate(bReset);
         }
 
         public void Reset(string label)
@@ -139,7 +148,8 @@ public string Elapsed(string label)
         }
         public string Accumulated(string label)
         {
-            return string.Format("{0:ss}.{0:fffffff}", Timers[label].Accumulated);
+            TimeSpan accum = Timers[label].Accumulated;
+            return string.Format(BlockTimer.TimeFormatString(accum), accum);
         }
 
         public string AllTicks(string prefix = "Times:")
@@ -169,7 +179,8 @@ public string AllTimes(string prefix = "Times:", string separator = " ")
             StringBuilder b = new StringBuilder();
             b.Append(prefix + " ");
             foreach ( string label in Order ) {
-                b.Append(label + ": " + string.Format("{0:ss}.{0:ffffff}", Timers[label].Watch.Elapsed) + separator);
+                TimeSpan span = Timers[label].Watch.Elapsed;
+                b.Append(label + ": " + string.Format(BlockTimer.TimeFormatString(span), span) + separator);
             }
             return b.ToString();
         }
@@ -179,7 +190,8 @@ public string AllAccumulatedTimes(string prefix = "Times:", string separator = "
             StringBuilder b = new StringBuilder();
             b.Append(prefix + " ");
             foreach ( string label in Order ) {
-                b.Append(label + ": " + string.Format("{0:ss}.{0:ffffff}", Timers[label].Accumulated) + separator);
+                TimeSpan span = Timers[label].Accumulated;
+                b.Append(label + ": " + string.Format(BlockTimer.TimeFormatString(span), span) + separator);
             }
             return b.ToString();
         }
diff --git a/core/ProgressCancel.cs b/core/ProgressCancel.cs
new file mode 100644
index 00000000..0a831229
--- /dev/null
+++ b/core/ProgressCancel.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace g3
+{
+    /// <summary>
+    /// interface that provides a cancel function
+    /// </summary>
+    public interface ICancelSource
+    {
+        bool Cancelled();
+    }
+
+
+    /// <summary>
+    /// Just wraps a func<bool> as an ICancelSource
+    /// </summary>
+    public class CancelFunction : ICancelSource
+    {
+        public Func<bool> CancelF;
+        public CancelFunction(Func<bool> cancelF) {
+            CancelF = cancelF;
+        }
+        public bool Cancelled() { return CancelF(); }
+    }
+
+
+    /// <summary>
+    /// This class is intended to be passed to long-running computes to 
+    ///  1) provide progress info back to caller (not implemented yet)
+    ///  2) allow caller to cancel the computation
+    /// </summary>
+    public class ProgressCancel
+    {
+        public ICancelSource Source;
+
+        bool WasCancelled = false;  // will be set to true if CancelF() ever returns true
+
+        public ProgressCancel(ICancelSource source)
+        {
+            Source = source;
+        }
+        public ProgressCancel(Func<bool> cancelF)
+        {
+            Source = new CancelFunction(cancelF);
+        }
+
+        /// <summary>
+        /// Check if client would like to cancel
+        /// </summary>
+        public bool Cancelled()
+        {
+            if (WasCancelled)
+                return true;
+            WasCancelled = Source.Cancelled();
+            return WasCancelled;
+        }
+    }
+}
diff --git a/core/RefCountVector.cs b/core/RefCountVector.cs
index a51b5746..a8ac5b85 100644
--- a/core/RefCountVector.cs
+++ b/core/RefCountVector.cs
@@ -4,16 +4,20 @@
 namespace g3
 {
 
-    // this class allows you to keep track of refences to indices,
-    // with a free list so unreferenced indices can be re-used.
-    //
-    // the enumerator iterates over valid indices
-    //
+    /// <summary>
+    /// RefCountedVector is used to keep track of which indices in a linear index list are in use/referenced.
+    /// A free list is tracked so that unreferenced indices can be re-used.
+    ///
+    /// The enumerator iterates over valid indices (ie where refcount > 0)
+    /// 
+    /// **refcounts are shorts** so the maximum count is 65536. 
+    /// No overflow checking is done in release builds.
+    /// 
+    /// </summary>
     public class RefCountVector : System.Collections.IEnumerable
     {
         public static readonly short invalid = -1;
 
-
         DVector<short> ref_counts;
         DVector<int> free_indices;
         int used_count;
@@ -71,18 +75,30 @@ public int refCount(int index) {
             int n = ref_counts[index];
             return (n == invalid) ? 0 : n;
         }
+        public int rawRefCount(int index) {
+            return ref_counts[index];
+        }
 
 
         public int allocate() {
             used_count++;
             if (free_indices.empty) {
+                // [RMS] do we need this branch anymore? 
                 ref_counts.push_back(1);
                 return ref_counts.size - 1;
             } else {
-                int iFree = free_indices.back;
-                free_indices.pop_back();
-                ref_counts[iFree] = 1;
-                return iFree;
+                int iFree = invalid;
+                while (iFree == invalid && free_indices.empty == false) {
+                    iFree = free_indices.back;
+                    free_indices.pop_back();
+                }
+                if (iFree != invalid) {
+                    ref_counts[iFree] = 1;
+                    return iFree;
+                } else {
+                    ref_counts.push_back(1);
+                    return ref_counts.size - 1;
+                }
             }
         }
 
@@ -90,6 +106,8 @@ public int allocate() {
 
         public int increment(int index, short increment = 1) {
             Util.gDevAssert( isValid(index)  );
+            // debug check for overflow...
+            Util.gDevAssert(  (short)(ref_counts[index] + increment) > 0 );
             ref_counts[index] += increment;
             return ref_counts[index];       
         }
@@ -106,6 +124,73 @@ public void decrement(int index, short decrement = 1) {
         }
 
 
+
+        /// <summary>
+        /// allocate at specific index, which must either be larger than current max index,
+        /// or on the free list. If larger, all elements up to this one will be pushed onto
+        /// free list. otherwise we have to do a linear search through free list.
+        /// If you are doing many of these, it is likely faster to use 
+        /// allocate_at_unsafe(), and then rebuild_free_list() after you are done.
+        /// </summary>
+        public bool allocate_at(int index)
+        {
+            if (index >= ref_counts.size) {
+                int j = ref_counts.size;
+                while (j < index) {
+                    ref_counts.push_back(invalid);
+                    free_indices.push_back(j);
+                    ++j;
+                }
+                ref_counts.push_back(1);
+                used_count++;
+                return true;
+
+            } else {
+                if (ref_counts[index] > 0)
+                    return false;
+
+                int N = free_indices.size;
+                for (int i = 0; i < N; ++i) {
+                    if ( free_indices[i] == index ) {
+                        free_indices[i] = invalid;
+                        ref_counts[index] = 1;
+                        used_count++;
+                        return true;
+                    }
+                }
+                return false;
+            }
+        }
+
+
+        /// <summary>
+        /// allocate at specific index, which must be free or larger than current max index.
+        /// However, we do not update free list. So, you probably need to do 
+        /// rebuild_free_list() after calling this.
+        /// </summary>
+        public bool allocate_at_unsafe(int index)
+        {
+            if (index >= ref_counts.size) {
+                int j = ref_counts.size;
+                while (j < index) {
+                    ref_counts.push_back(invalid);
+                    ++j;
+                }
+                ref_counts.push_back(1);
+                used_count++;
+                return true;
+
+            } else {
+                if (ref_counts[index] > 0)
+                    return false;
+                ref_counts[index] = 1;
+                used_count++;
+                return true;
+            }
+        }
+
+
+
         // [RMS] really should not use this!!
         public void set_Unsafe(int index, short count)
         {
@@ -113,7 +198,6 @@ public void set_Unsafe(int index, short count)
         }
 
         // todo:
-        //   insert
         //   remove
         //   clear
 
diff --git a/core/SafeCollections.cs b/core/SafeCollections.cs
index ef408fce..947054f3 100644
--- a/core/SafeCollections.cs
+++ b/core/SafeCollections.cs
@@ -33,6 +33,19 @@ public void SafeAdd(T value)
             spinlock.Exit();
         }
 
+
+        public void SafeOperation(Action<List<T>> opF)
+        {
+            bool lockTaken = false;
+            while (lockTaken == false)
+                spinlock.Enter(ref lockTaken);
+
+            opF(List);
+
+            spinlock.Exit();
+        }
+
+
         public List<T> Result {
             get { return List; }
         }
diff --git a/core/SmallListSet.cs b/core/SmallListSet.cs
index 8629fd07..568eb4e0 100644
--- a/core/SmallListSet.cs
+++ b/core/SmallListSet.cs
@@ -83,7 +83,13 @@ public void Resize(int new_size)
         public void AllocateAt(int list_index)
         {
             if (list_index >= list_heads.size) {
+                int j = list_heads.size;
                 list_heads.insert(Null, list_index);
+                // need to set intermediate values to null! 
+                while (j < list_index) {
+                    list_heads[j] = Null;
+                    j++;
+                }
             } else {
                 if (list_heads[list_index] != Null)
                     throw new Exception("SmallListSet: list at " + list_index + " is not empty!");
diff --git a/core/Snapping.cs b/core/Snapping.cs
index 9314f79e..e7c04eb5 100644
--- a/core/Snapping.cs
+++ b/core/Snapping.cs
@@ -5,17 +5,18 @@ namespace g3
     public class Snapping
     {
 
-        public static double SnapToIncrement(double fValue, double fIncrement)
+        public static double SnapToIncrement(double fValue, double fIncrement, double offset = 0)
         {
             if (!MathUtil.IsFinite(fValue))
                 return 0;
+            fValue -= offset;
             double sign = Math.Sign(fValue);
             fValue = Math.Abs(fValue);
             int nInc = (int)(fValue / fIncrement);
             double fRem = fValue % fIncrement;
             if (fRem > fIncrement / 2)
                 ++nInc;
-            return sign * (double)nInc * fIncrement;
+            return sign * (double)nInc * fIncrement + offset;
         }
 
 
@@ -29,5 +30,31 @@ public static double SnapToNearbyIncrement(double fValue, double fIncrement, dou
             return fValue;
         }
 
+        private static double SnapToIncrementSigned(double fValue, double fIncrement, bool low)
+        {
+            if (!MathUtil.IsFinite(fValue))
+                return 0;
+            double sign = Math.Sign(fValue);
+            fValue = Math.Abs(fValue);
+            int nInc = (int)(fValue / fIncrement);
+
+            if (low && sign < 0)
+                ++nInc;
+            else if (!low && sign > 0)
+                ++nInc;
+
+            return sign * (double)nInc * fIncrement;
+
+        }
+
+        public static double SnapToIncrementLow(double fValue, double fIncrement, double offset=0)
+        {
+            return SnapToIncrementSigned(fValue - offset, fIncrement, true) + offset;
+        }
+
+        public static double SnapToIncrementHigh(double fValue, double fIncrement, double offset = 0)
+        {
+            return SnapToIncrementSigned(fValue - offset, fIncrement, false) + offset;
+        }
     }
 }
diff --git a/core/TagSet.cs b/core/TagSet.cs
index 6dda9a22..a9d3b38e 100644
--- a/core/TagSet.cs
+++ b/core/TagSet.cs
@@ -43,6 +43,27 @@ public int Get(T reference)
     }
 
 
+    /// <summary>
+    /// integer type/value pair, packed into 32 bits - 8 for type, 24 for value
+    /// </summary>
+    public struct IntTagPair
+    {
+        public byte type;
+        public int value; 
+        public IntTagPair(byte type, int value) {
+            Util.gDevAssert(value < 1 << 24);
+            this.type = type;
+            this.value = value;
+        }
+        public IntTagPair(int combined)
+        {
+            type = (byte)(combined >> 24);
+            value = combined & 0xFFFFFF;
+        }
+        public int intValue { get { return ((int)type) << 24 | value; } }
+    }
+
+
 
 
     /// <summary>
diff --git a/core/Units.cs b/core/Units.cs
index 5a0fb234..54f48b6f 100644
--- a/core/Units.cs
+++ b/core/Units.cs
@@ -96,7 +96,7 @@ public static double Convert(Linear from, Linear to)
             if ( IsMetric(from) && IsMetric(to) ) {
                 double pfrom = GetMetricPower(from);
                 double pto = GetMetricPower(to);
-                double d = pto - pfrom;
+                double d = pfrom - pto;
                 return Math.Pow(10, d);
             }
 
diff --git a/core/Util.cs b/core/Util.cs
index 8009ded3..02e9121a 100644
--- a/core/Util.cs
+++ b/core/Util.cs
@@ -189,7 +189,7 @@ static public string ToSecMilli(TimeSpan t)
 #if G3_USING_UNITY
             return string.Format("{0}", t.TotalSeconds);
 #else
-            return t.ToString("ss\\.ffff");
+            return string.Format("{0:F5}", t.TotalSeconds);
 #endif
         }
 
diff --git a/core/g3Iterators.cs b/core/g3Iterators.cs
index 800f43ad..6bbc8230 100644
--- a/core/g3Iterators.cs
+++ b/core/g3Iterators.cs
@@ -93,5 +93,63 @@ IEnumerator IEnumerable.GetEnumerator()
     }
 
 
+    
+
+    /// <summary>
+    /// IList wrapper for an Interval1i, ie sequential list of integers
+    /// </summary>
+    public struct IntSequence : IList<int>
+    {
+        Interval1i range;
+
+        public IntSequence(Interval1i ival) {
+            range = ival;
+        }
+        public IntSequence(int iStart, int iEnd) {
+            range = new Interval1i(iStart, iEnd);
+        }
+
+        /// <summary> construct interval [0, N-1] </summary>
+        static public IntSequence Range(int N) { return new IntSequence(0, N - 1); }
+
+        /// <summary> construct interval [0, N-1] </summary>
+        static public IntSequence RangeInclusive(int N) { return new IntSequence(0, N); }
+
+        /// <summary> construct interval [start, start+N-1] </summary>
+        static public IntSequence Range(int start, int N) { return new IntSequence(start, start + N - 1); }
+
+
+        /// <summary> construct interval [a, b] </summary>
+        static public IntSequence FromToInclusive(int a, int b) { return new IntSequence(a, b); }
+
+        public int this[int index] {
+            get { return range.a + index; }
+            set { throw new NotImplementedException(); }
+        }
+        public int Count { get { return range.Length+1; } }
+        public bool IsReadOnly { get { return true; } }
+
+        public void Add(int item) { throw new NotImplementedException(); }
+        public void Clear() { throw new NotImplementedException(); }
+        public void Insert(int index, int item) { throw new NotImplementedException(); }
+        public bool Remove(int item) { throw new NotImplementedException(); }
+        public void RemoveAt(int index) { throw new NotImplementedException(); }
+
+        // could be implemented...
+        public bool Contains(int item) { return range.Contains(item); }
+        public int IndexOf(int item) { throw new NotImplementedException(); }
+        public void CopyTo(int[] array, int arrayIndex) { throw new NotImplementedException(); }
+
+        public IEnumerator<int> GetEnumerator() {
+            return range.GetEnumerator();
+        }
+        IEnumerator IEnumerable.GetEnumerator() {
+            return GetEnumerator();
+        }
+    }
+
+
+
+
 
 }
diff --git a/core/gParallel.cs b/core/gParallel.cs
index 23f02c61..53b4aa6b 100644
--- a/core/gParallel.cs
+++ b/core/gParallel.cs
@@ -4,7 +4,7 @@
 using System.Text;
 using System.Threading;
 
-#if !G3_USING_UNITY
+#if !(NET_2_0 || NET_2_0_SUBSET)
 using System.Threading.Tasks;
 #endif
 
@@ -20,7 +20,7 @@ public static void ForEach_Sequential<T>(IEnumerable<T> source, Action<T> body)
         }
         public static void ForEach<T>( IEnumerable<T> source, Action<T> body )
         {
-#if G3_USING_UNITY
+#if G3_USING_UNITY && (NET_2_0 || NET_2_0_SUBSET)
             for_each<T>(source, body);
 #else
             Parallel.ForEach<T>(source, body);
@@ -42,8 +42,9 @@ public static void Evaluate(params Action[] funcs)
 
 
         /// <summary>
-        /// Process indices [iStart,iEnd], inclusive, by passing sub-intervals [start,end] to blockF.
+        /// Process indices [iStart,iEnd] *inclusive* by passing sub-intervals [start,end] to blockF.
         /// Blocksize is automatically determind unless you specify one.
+        /// Iterate over [start,end] *inclusive* in each block
         /// </summary>
         public static void BlockStartEnd(int iStart, int iEnd, Action<int,int> blockF, int iBlockSize = -1, bool bDisableParallel = false )
         {
diff --git a/curve/ArcLengthParam.cs b/curve/ArcLengthParam.cs
index 5884a5d9..633cb108 100644
--- a/curve/ArcLengthParam.cs
+++ b/curve/ArcLengthParam.cs
@@ -92,4 +92,97 @@ protected Vector3d tangent(int i)
         }
     }
 
+
+
+
+
+
+    public struct CurveSample2d
+    {
+        public Vector2d position;
+        public Vector2d tangent;
+        public CurveSample2d(Vector2d p, Vector2d t)
+        {
+            position = p; tangent = t;
+        }
+    }
+
+
+    public interface IArcLengthParam2d
+    {
+        double ArcLength { get; }
+        CurveSample2d Sample(double fArcLen);
+    }
+
+
+    public class SampledArcLengthParam2d : IArcLengthParam2d
+    {
+        double[] arc_len;
+        Vector2d[] positions;
+
+        public SampledArcLengthParam2d(IEnumerable<Vector2d> samples, int nCountHint = -1)
+        {
+            int N = (nCountHint == -1) ? samples.Count() : nCountHint;
+            arc_len = new double[N];
+            arc_len[0] = 0;
+            positions = new Vector2d[N];
+
+            int i = 0;
+            Vector2d prev = Vector2d.Zero;
+            foreach (Vector2d v in samples) {
+                positions[i] = v;
+                if (i > 0) {
+                    double d = (v - prev).Length;
+                    arc_len[i] = arc_len[i - 1] + d;
+                }
+                i++;
+                prev = v;
+            }
+        }
+
+
+        public double ArcLength {
+            get { return arc_len[arc_len.Length - 1]; }
+        }
+
+        public CurveSample2d Sample(double f)
+        {
+            if (f <= 0)
+                return new CurveSample2d(new Vector2d(positions[0]), tangent(0));
+
+            int N = arc_len.Length;
+            if (f >= arc_len[N - 1])
+                return new CurveSample2d(new Vector2d(positions[N - 1]), tangent(N - 1));
+
+            for (int k = 0; k < N; ++k) {
+                if (f < arc_len[k]) {
+                    int a = k - 1;
+                    int b = k;
+                    if (arc_len[a] == arc_len[b])
+                        return new CurveSample2d(new Vector2d(positions[a]), tangent(a));
+                    double t = (f - arc_len[a]) / (arc_len[b] - arc_len[a]);
+                    return new CurveSample2d(
+                        Vector2d.Lerp(positions[a], positions[b], t),
+                        Vector2d.Lerp(tangent(a), tangent(b), t));
+                }
+            }
+
+            throw new ArgumentException("SampledArcLengthParam2d.Sample: somehow arc len is outside any possible range");
+        }
+
+
+        protected Vector2d tangent(int i)
+        {
+            int N = arc_len.Length;
+            if (i == 0)
+                return (positions[1] - positions[0]).Normalized;
+            else if (i == N - 1)
+                return (positions[N - 1] - positions[N - 2]).Normalized;
+            else
+                return (positions[i + 1] - positions[i - 1]).Normalized;
+        }
+    }
+
+
+
 }
diff --git a/curve/BezierCurve2.cs b/curve/BezierCurve2.cs
new file mode 100644
index 00000000..0444f77b
--- /dev/null
+++ b/curve/BezierCurve2.cs
@@ -0,0 +1,246 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace g3
+{
+    /// <summary>
+    /// 2D Bezier curve of arbitrary degree
+    /// Ported from WildMagic5 Wm5BezierCurve2
+    /// </summary>
+    public class BezierCurve2 : BaseCurve2, IParametricCurve2d
+    {
+        int mDegree;
+        int mNumCtrlPoints;
+        Vector2d[] mCtrlPoint;
+        Vector2d[] mDer1CtrlPoint;
+        Vector2d[] mDer2CtrlPoint;
+        Vector2d[] mDer3CtrlPoint;
+        DenseMatrix mChoose;
+
+
+        public int Degree { get { return mDegree; } }
+        public Vector2d[] ControlPoints { get { return mCtrlPoint; } }
+
+
+        public BezierCurve2(int degree, Vector2d[] ctrlPoint, bool bTakeOwnership = false) : base(0, 1)
+        {
+            if ( degree < 2 )
+                throw new Exception("BezierCurve2() The degree must be three or larger\n");
+
+            int i, j;
+
+            mDegree = degree;
+            mNumCtrlPoints = mDegree + 1;
+            if (bTakeOwnership) {
+                mCtrlPoint = ctrlPoint;
+            } else {
+                mCtrlPoint = new Vector2d[ctrlPoint.Length];
+                Array.Copy(ctrlPoint, mCtrlPoint, ctrlPoint.Length);
+            }
+
+            // Compute first-order differences.
+            mDer1CtrlPoint = new Vector2d[mNumCtrlPoints - 1];
+            for (i = 0; i < mNumCtrlPoints - 1; ++i) {
+                mDer1CtrlPoint[i] = mCtrlPoint[i + 1] - mCtrlPoint[i];
+            }
+
+            // Compute second-order differences.
+            mDer2CtrlPoint = new Vector2d[mNumCtrlPoints - 2];
+            for (i = 0; i < mNumCtrlPoints - 2; ++i) {
+                mDer2CtrlPoint[i] = mDer1CtrlPoint[i + 1] - mDer1CtrlPoint[i];
+            }
+
+            // Compute third-order differences.
+            if (degree >= 3) {
+                mDer3CtrlPoint = new Vector2d[mNumCtrlPoints - 3];
+                for (i = 0; i < mNumCtrlPoints - 3; ++i) {
+                    mDer3CtrlPoint[i] = mDer2CtrlPoint[i + 1] - mDer2CtrlPoint[i];
+                }
+            } else {
+                mDer3CtrlPoint = null;
+            }
+
+            // Compute combinatorial values Choose(N,K), store in mChoose[N,K].
+            // The values mChoose[r,c] are invalid for r < c (use only the
+            // entries for r >= c).
+            mChoose = new DenseMatrix(mNumCtrlPoints, mNumCtrlPoints);
+
+            mChoose[0,0] = 1.0;
+            mChoose[1,0] = 1.0;
+            mChoose[1,1] = 1.0;
+            for (i = 2; i <= mDegree; ++i) {
+                mChoose[i,0] = 1.0;
+                mChoose[i,i] = 1.0;
+                for (j = 1; j < i; ++j) {
+                    mChoose[i,j] = mChoose[i - 1,j - 1] + mChoose[i - 1,j];
+                }
+            }
+        }
+
+
+        // used in Clone()
+        protected BezierCurve2() : base(0, 1)
+        {
+        }
+
+
+        public override Vector2d GetPosition(double t)
+        {
+            double oneMinusT = 1 - t;
+            double powT = t;
+            Vector2d result = oneMinusT * mCtrlPoint[0];
+
+            for (int i = 1; i < mDegree; ++i) {
+                double coeff = mChoose[mDegree,i] * powT;
+                result = (result + mCtrlPoint[i] * coeff) * oneMinusT;
+                powT *= t;
+            }
+
+            result += mCtrlPoint[mDegree] * powT;
+
+            return result;
+        }
+
+
+        public override Vector2d GetFirstDerivative(double t)
+        {
+            double oneMinusT = 1 - t;
+            double powT = t;
+            Vector2d result = oneMinusT * mDer1CtrlPoint[0];
+
+            int degreeM1 = mDegree - 1;
+            for (int i = 1; i < degreeM1; ++i) {
+                double coeff = mChoose[degreeM1,i] * powT;
+                result = (result + mDer1CtrlPoint[i] * coeff) * oneMinusT;
+                powT *= t;
+            }
+
+            result += mDer1CtrlPoint[degreeM1] * powT;
+            result *= (double)mDegree;
+
+            return result;
+        }
+
+
+        public override Vector2d GetSecondDerivative(double t)
+        {
+            double oneMinusT = 1 - t;
+            double powT = t;
+            Vector2d result = oneMinusT * mDer2CtrlPoint[0];
+
+            int degreeM2 = mDegree - 2;
+            for (int i = 1; i < degreeM2; ++i) {
+                double coeff = mChoose[degreeM2,i] * powT;
+                result = (result + mDer2CtrlPoint[i] * coeff) * oneMinusT;
+                powT *= t;
+            }
+
+            result += mDer2CtrlPoint[degreeM2] * powT;
+            result *= (double)(mDegree * (mDegree - 1));
+
+            return result;
+        }
+
+
+        public override Vector2d GetThirdDerivative(double t)
+        {
+            if (mDegree < 3) {
+                return Vector2d.Zero;
+            }
+
+            double oneMinusT = 1 - t;
+            double powT = t;
+            Vector2d result = oneMinusT * mDer3CtrlPoint[0];
+
+            int degreeM3 = mDegree - 3;
+            for (int i = 1; i < degreeM3; ++i) {
+                double coeff = mChoose[degreeM3,i] * powT;
+                result = (result + mDer3CtrlPoint[i] * coeff) * oneMinusT;
+                powT *= t;
+            }
+
+            result += mDer3CtrlPoint[degreeM3] * powT;
+            result *= (double)(mDegree * (mDegree - 1) * (mDegree - 2));
+
+            return result;
+        }
+
+
+
+        /*
+         * IParametricCurve2d implementation
+         */
+
+        // TODO: could support closed bezier?
+        public bool IsClosed {
+            get { return false; }
+        }
+
+        // can call SampleT in range [0,ParamLength]
+        public double ParamLength {
+            get { return mTMax - mTMin; }
+        }
+        public Vector2d SampleT(double t)
+        {
+            return GetPosition(t);
+        }
+
+        public Vector2d TangentT(double t)
+        {
+            return GetFirstDerivative(t).Normalized;
+        }
+
+        public bool HasArcLength {
+            get { return true; }
+        }
+        public double ArcLength {
+            get { return GetTotalLength(); }
+        }
+        public Vector2d SampleArcLength(double a)
+        {
+            double t = GetTime(a);
+            return GetPosition(t);
+        }
+
+        public void Reverse()
+        {
+            throw new NotSupportedException("NURBSCurve2.Reverse: how to reverse?!?");
+        }
+
+        public IParametricCurve2d Clone()
+        {
+            BezierCurve2 c2 = new BezierCurve2();
+            c2.mDegree = this.mDegree;
+            c2.mNumCtrlPoints = this.mNumCtrlPoints;
+
+            c2.mCtrlPoint = (Vector2d[])this.mCtrlPoint.Clone();
+            c2.mDer1CtrlPoint = (Vector2d[])this.mDer1CtrlPoint.Clone();
+            c2.mDer2CtrlPoint = (Vector2d[])this.mDer2CtrlPoint.Clone();
+            c2.mDer3CtrlPoint = (Vector2d[])this.mDer3CtrlPoint.Clone();
+            c2.mChoose = new DenseMatrix(this.mChoose);
+            return c2;
+        }
+
+
+        public bool IsTransformable { get { return true; } }
+        public void Transform(ITransform2 xform)
+        {
+            for (int k = 0; k < mCtrlPoint.Length; ++k)
+                mCtrlPoint[k] = xform.TransformP(mCtrlPoint[k]);
+
+            // update derivatives
+            for (int i = 0; i < mNumCtrlPoints - 1; ++i) 
+                mDer1CtrlPoint[i] = mCtrlPoint[i+1] - mCtrlPoint[i];
+            for (int i = 0; i < mNumCtrlPoints - 2; ++i) 
+                mDer2CtrlPoint[i] = mDer1CtrlPoint[i+1] - mDer1CtrlPoint[i];
+            if (mDegree >= 3) {
+                for (int i = 0; i < mNumCtrlPoints - 3; ++i)
+                    mDer3CtrlPoint[i] = mDer2CtrlPoint[i + 1] - mDer2CtrlPoint[i];
+            }
+        }
+
+
+    }
+}
diff --git a/curve/Circle2.cs b/curve/Circle2.cs
index 41b5a60b..3cab0789 100644
--- a/curve/Circle2.cs
+++ b/curve/Circle2.cs
@@ -8,6 +8,12 @@ public class Circle2d : IParametricCurve2d
 		public double Radius;
 		public bool IsReversed;		// use ccw orientation instead of cw
 
+        public Circle2d(double radius) {
+            IsReversed = false;
+            Center = Vector2d.Zero;
+            Radius = radius;
+        }
+
 		public Circle2d(Vector2d center, double radius)
 		{
 			IsReversed = false;
@@ -137,5 +143,21 @@ public double Distance(Vector2d pt)
             return Math.Abs(d - Radius);
         }
 
+
+
+        public static double RadiusArea(double r) {
+            return Math.PI * r * r;
+        }
+        public static double RadiusCircumference(double r) {
+            return MathUtil.TwoPI * r;
+        }
+
+        /// <summary>
+        /// Radius of n-sided regular polygon that contains circle of radius r
+        /// </summary>
+        public static double BoundingPolygonRadius(double r, int n) {
+            double theta = (MathUtil.TwoPI / (double)n) / 2.0;
+            return r / Math.Cos(theta);
+        }
     }
 }
diff --git a/curve/CurveUtils.cs b/curve/CurveUtils.cs
index 042e8622..bc5a9db5 100644
--- a/curve/CurveUtils.cs
+++ b/curve/CurveUtils.cs
@@ -8,27 +8,40 @@ namespace g3
     public class CurveUtils
     {
 
-        public static Vector3d GetTangent(List<Vector3d> vertices, int i)
+        public static Vector3d GetTangent(List<Vector3d> vertices, int i, bool bLoop = false)
         {
-            if (i == 0)
-                return (vertices[1] - vertices[0]).Normalized;
-            else if (i == vertices.Count - 1)
-                return (vertices[vertices.Count - 1] - vertices[vertices.Count - 2]).Normalized;
-            else
-                return (vertices[i + 1] - vertices[i - 1]).Normalized;
+            if (bLoop) {
+                int NV = vertices.Count;
+                if (i == 0)
+                    return (vertices[1] - vertices[NV-1]).Normalized;
+                else
+                    return (vertices[(i+1)%NV] - vertices[i-1]).Normalized;
+            } else {
+                if (i == 0)
+                    return (vertices[1] - vertices[0]).Normalized;
+                else if (i == vertices.Count - 1)
+                    return (vertices[vertices.Count - 1] - vertices[vertices.Count - 2]).Normalized;
+                else
+                    return (vertices[i + 1] - vertices[i - 1]).Normalized;
+            }
         }
 
 
-        public static double ArcLength(List<Vector3d> vertices) {
+        public static double ArcLength(List<Vector3d> vertices, bool bLoop = false) {
             double sum = 0;
-            for (int i = 1; i < vertices.Count; ++i)
-                sum += (vertices[i] - vertices[i - 1]).Length;
+            int NV = vertices.Count;
+            for (int i = 1; i < NV; ++i)
+                sum += vertices[i].Distance(vertices[i-1]);
+            if (bLoop)
+                sum += vertices[NV-1].Distance(vertices[0]);
             return sum;
         }
-        public static double ArcLength(Vector3d[] vertices) {
+        public static double ArcLength(Vector3d[] vertices, bool bLoop = false) {
             double sum = 0;
             for (int i = 1; i < vertices.Length ; ++i)
-                sum += (vertices[i] - vertices[i - 1]).Length;
+                sum += vertices[i].Distance(vertices[i-1]);
+            if (bLoop)
+                sum += vertices[vertices.Length-1].Distance(vertices[0]);
             return sum;
         }
         public static double ArcLength(IEnumerable<Vector3d> vertices) {
@@ -38,6 +51,7 @@ public static double ArcLength(IEnumerable<Vector3d> vertices) {
             foreach (Vector3d v in vertices) {
                 if (i++ > 0)
                     sum += (v - prev).Length;
+                prev = v;
             }
             return sum;
         }
@@ -61,31 +75,27 @@ public static int FindNearestIndex(ISampledCurve3d c, Vector3d v)
 
 
 
-        public static bool FindClosestRayIntersection(ISampledCurve3d c, double segRadius, Ray3d ray, out double rayT)
+        public static bool FindClosestRayIntersection(ISampledCurve3d c, double segRadius, Ray3d ray, out double minRayT)
         {
-            rayT = double.MaxValue;
+            minRayT = double.MaxValue;
             int nNearSegment = -1;
-            //double fNearSegT = 0.0;
 
-            int N = c.VertexCount;
-            int iStop = (c.Closed) ? N : N - 1;
-            for (int i = 0; i < iStop; ++i) {
-                DistRay3Segment3 dist = new DistRay3Segment3(ray,
-                    new Segment3d(c.GetVertex(i), c.GetVertex( (i + 1) % N )));
+            int nSegs = c.SegmentCount;
+            for (int i = 0; i < nSegs; ++i) {
+                Segment3d seg = c.GetSegment(i);
 
                 // raycast to line bounding-sphere first (is this going ot be faster??)
                 double fSphereHitT;
-                bool bHitBoundSphere = RayIntersection.SphereSigned(ray.Origin, ray.Direction,
-                    dist.Segment.Center, dist.Segment.Extent + segRadius, out fSphereHitT);
+                bool bHitBoundSphere = RayIntersection.SphereSigned(ref ray.Origin, ref ray.Direction,
+                    ref seg.Center, seg.Extent + segRadius, out fSphereHitT);
                 if (bHitBoundSphere == false)
                     continue;
 
-                // find ray/seg min-distance and use ray T
-                double dSqr = dist.GetSquared();
+                double rayt, segt;
+                double dSqr = DistRay3Segment3.SquaredDistance(ref ray, ref seg, out rayt, out segt);
                 if ( dSqr < segRadius*segRadius) {
-                    if (dist.RayParameter < rayT) {
-                        rayT = dist.RayParameter;
-                        //fNearSegT = dist.SegmentParameter;
+                    if (rayt < minRayT) {
+                        minRayT = rayt;
                         nNearSegment = i;
                     }
                 }
@@ -136,6 +146,56 @@ public static void InPlaceSmooth(IList<Vector3d> vertices, int iStart, int iEnd,
 
 
 
+        /// <summary>
+        /// smooth set of vertices using extra buffer
+        /// </summary>
+        public static void IterativeSmooth(IList<Vector3d> vertices, double alpha, int nIterations, bool bClosed)
+        {
+            IterativeSmooth(vertices, 0, vertices.Count, alpha, nIterations, bClosed);
+        }
+        /// <summary>
+        /// smooth set of vertices using extra buffer
+        /// </summary>
+        public static void IterativeSmooth(IList<Vector3d> vertices, int iStart, int iEnd, double alpha, int nIterations, bool bClosed, Vector3d[] buffer = null)
+        {
+            int N = vertices.Count;
+            if (buffer == null || buffer.Length < N)
+                buffer = new Vector3d[N];
+            if (bClosed) {
+                for (int iter = 0; iter < nIterations; ++iter) {
+                    for (int ii = iStart; ii < iEnd; ++ii) {
+                        int i = (ii % N);
+                        int iPrev = (ii == 0) ? N - 1 : ii - 1;
+                        int iNext = (ii + 1) % N;
+                        Vector3d prev = vertices[iPrev], next = vertices[iNext];
+                        Vector3d c = (prev + next) * 0.5f;
+                        buffer[i] = (1 - alpha) * vertices[i] + (alpha) * c;
+                    }
+                    for (int ii = iStart; ii < iEnd; ++ii) {
+                        int i = (ii % N);
+                        vertices[i] = buffer[i];
+                    }
+                }
+            } else {
+                for (int iter = 0; iter < nIterations; ++iter) {
+                    for (int i = iStart; i <= iEnd; ++i) {
+                        if (i == 0 || i >= N - 1)
+                            continue;
+                        Vector3d prev = vertices[i - 1], next = vertices[i + 1];
+                        Vector3d c = (prev + next) * 0.5f;
+                        buffer[i] = (1 - alpha) * vertices[i] + (alpha) * c;
+                    }
+                    for (int ii = iStart; ii < iEnd; ++ii) {
+                        int i = (ii % N);
+                        vertices[i] = buffer[i];
+                    }
+                }
+            }
+        }
+
+
+
+
     }
 
 
@@ -151,7 +211,15 @@ public class IWrappedCurve3d : ISampledCurve3d
         public bool Closed { get; set; }
 
         public int VertexCount { get { return (VertexList == null) ? 0 : VertexList.Count; } }
+        public int SegmentCount { get { return Closed ? VertexCount : VertexCount - 1; } }
+
         public Vector3d GetVertex(int i) { return VertexList[i]; }
+        public Segment3d GetSegment(int iSegment) {
+            return (Closed) ? new Segment3d(VertexList[iSegment], VertexList[(iSegment + 1) % VertexList.Count])
+                : new Segment3d(VertexList[iSegment], VertexList[iSegment + 1]);
+        }
+
+
         public IEnumerable<Vector3d> Vertices { get { return VertexList; } }
     }
 
diff --git a/curve/CurveUtils2.cs b/curve/CurveUtils2.cs
index 7c32733a..ed1bf710 100644
--- a/curve/CurveUtils2.cs
+++ b/curve/CurveUtils2.cs
@@ -173,14 +173,25 @@ public static void LaplacianSmoothConstrained(Polygon2d poly, double alpha, int
                 for (int i = 0; i < N; ++i ) {
                     Vector2d curpos = poly[i];
                     Vector2d smoothpos = (poly[(i + N - 1) % N] + poly[(i + 1) % N]) * 0.5;
-                    bool contained = true;
-                    if (bAllowShrink == false || bAllowGrow == false)
-                        contained = origPoly.Contains(smoothpos);
+
                     bool do_smooth = true;
-                    if (bAllowShrink && contained == false)
-                        do_smooth = false;
-                    if (bAllowGrow && contained == true)
-                        do_smooth = false;
+                    if (bAllowShrink == false || bAllowGrow == false) {
+                        bool is_inside = origPoly.Contains(smoothpos);
+                        if (is_inside == true)
+                            do_smooth = bAllowShrink;
+                        else
+                            do_smooth = bAllowGrow;
+                    }
+
+                    // [RMS] this is old code...I think not correct?
+                    //bool contained = true;
+                    //if (bAllowShrink == false || bAllowGrow == false)
+                    //    contained = origPoly.Contains(smoothpos);
+                    //bool do_smooth = true;
+                    //if (bAllowShrink && contained == false)
+                    //    do_smooth = false;
+                    //if (bAllowGrow && contained == true)
+                    //    do_smooth = false;
 
                     if ( do_smooth ) { 
                         Vector2d newpos = beta * curpos + alpha * smoothpos;
@@ -215,5 +226,99 @@ public static void LaplacianSmoothConstrained(GeneralPolygon2d solid, double alp
 
 
 
+		/// <summary>
+		/// return list of objects for which keepF(obj) returns true
+		/// </summary>
+		public static List<T> Filter<T>(List<T> objects, Func<T, bool> keepF)
+		{
+			List<T> result = new List<T>(objects.Count);
+			foreach (var obj in objects) {
+				if (keepF(obj))
+					result.Add(obj);
+			}
+			return result;
+		}
+
+
+		/// <summary>
+		/// Split the input list into two new lists, based on predicate (set1 == true)
+		/// </summary>
+		public static void Split<T>(List<T> objects, out List<T> set1, out List<T> set2, Func<T, bool> splitF)
+		{
+			set1 = new List<T>();
+			set2 = new List<T>();
+			foreach (var obj in objects) {
+				if (splitF(obj))
+					set1.Add(obj);
+				else
+					set2.Add(obj);
+			}
+		}
+
+
+
+        public static Polygon2d SplitToTargetLength(Polygon2d poly, double length)
+        {
+            Polygon2d result = new Polygon2d();
+            result.AppendVertex(poly[0]);
+            for (int j = 0; j < poly.VertexCount; ++j) {
+                int next = (j + 1) % poly.VertexCount;
+                double len = poly[j].Distance(poly[next]);
+                if (len < length) {
+                    result.AppendVertex(poly[next]);
+                    continue;
+                }
+
+                int steps = (int)Math.Ceiling(len / length);
+                for (int k = 1; k < steps; ++k) {
+                    double t = (double)(k) / (double)steps;
+                    Vector2d v = (1.0 - t) * poly[j] + (t) * poly[next];
+                    result.AppendVertex(v);
+                }
+
+                if (j < poly.VertexCount - 1) {
+                    Util.gDevAssert(poly[j].Distance(result.Vertices[result.VertexCount - 1]) > 0.0001);
+                    result.AppendVertex(poly[next]);
+                }
+            }
+
+            return result;
+        }
+
+
+
+
+        /// <summary>
+        /// Remove polygons and polygon-holes smaller than minArea
+        /// </summary>
+        public static List<GeneralPolygon2d> FilterDegenerate(List<GeneralPolygon2d> polygons, double minArea)
+        {
+            List<GeneralPolygon2d> result = new List<GeneralPolygon2d>(polygons.Count);
+            List<Polygon2d> filteredHoles = new List<Polygon2d>();
+            foreach (var poly in polygons) {
+                if (poly.Outer.Area < minArea)
+                    continue;
+                if (poly.Holes.Count == 0) {
+                    result.Add(poly);
+                    continue;
+                }
+                filteredHoles.Clear();
+                for ( int i = 0; i < poly.Holes.Count; ++i ) {
+                    Polygon2d hole = poly.Holes[i];
+                    if (hole.Area > minArea)
+                        filteredHoles.Add(hole);
+                }
+                if ( filteredHoles.Count != poly.Holes.Count ) {
+                    poly.ClearHoles();
+                    foreach (var h in filteredHoles)
+                        poly.AddHole(h, false, false);
+                }
+                result.Add(poly);
+            }
+            return result;
+        }
+
+
+
     }
 }
diff --git a/curve/DCurve3.cs b/curve/DCurve3.cs
index ff723b5e..2eb8a2a4 100644
--- a/curve/DCurve3.cs
+++ b/curve/DCurve3.cs
@@ -5,6 +5,10 @@
 
 namespace g3
 {
+    /// <summary>
+    /// DCurve3 is a 3D polyline, either open or closed (via .Closed)
+    /// Despite the D prefix, it is *not* dynamic
+    /// </summary>
     public class DCurve3 : ISampledCurve3d
     {
         // [TODO] use dvector? or double-indirection indexing?
@@ -50,6 +54,19 @@ public DCurve3(ISampledCurve3d icurve)
             Timestamp = 1;
         }
 
+        public DCurve3(Polygon2d poly, int ix = 0, int iy = 1)
+        {
+            int NV = poly.VertexCount;
+            this.vertices = new List<Vector3d>(NV);
+            for (int k = 0; k < NV; ++k) {
+                Vector3d v = Vector3d.Zero;
+                v[ix] = poly[k].x; v[iy] = poly[k].y;
+                this.vertices.Add(v);
+            }
+            Closed = true;
+            Timestamp = 1;
+        }
+
         public void AppendVertex(Vector3d v) {
             vertices.Add(v);
             Timestamp++;
@@ -58,6 +75,9 @@ public void AppendVertex(Vector3d v) {
         public int VertexCount {
             get { return vertices.Count; }
         }
+        public int SegmentCount {
+            get { return Closed ? vertices.Count : vertices.Count - 1; }
+        }
 
         public Vector3d GetVertex(int i) {
             return vertices[i];
@@ -103,6 +123,11 @@ public void RemoveVertex(int idx)
             Timestamp++;
         }
 
+        public void Reverse() {
+			vertices.Reverse();
+			Timestamp++;
+		}
+
 
         public Vector3d this[int key]
         {
@@ -114,7 +139,7 @@ public Vector3d Start {
             get { return vertices[0]; }
         }
         public Vector3d End {
-            get { return vertices.Last(); }
+            get { return (Closed) ? vertices[0] : vertices.Last(); }
         }
 
         public IEnumerable<Vector3d> Vertices {
@@ -122,57 +147,107 @@ public IEnumerable<Vector3d> Vertices {
         }
 
 
-        public Segment3d Segment(int iSegment)
+        public Segment3d GetSegment(int iSegment)
         {
-            return new Segment3d(vertices[iSegment], vertices[iSegment + 1]);
+            return (Closed) ? new Segment3d(vertices[iSegment], vertices[(iSegment+1)%vertices.Count])
+                : new Segment3d(vertices[iSegment], vertices[iSegment + 1]);
         }
 
         public IEnumerable<Segment3d> SegmentItr()
         {
-            for (int i = 0; i < vertices.Count - 1; ++i)
-                yield return new Segment3d(vertices[i], vertices[i + 1]);
+            if (Closed) {
+                int NV = vertices.Count;
+                for (int i = 0; i < NV; ++i)
+                    yield return new Segment3d(vertices[i], vertices[(i + 1)%NV]);
+            } else {
+                int NV = vertices.Count - 1;
+                for (int i = 0; i < NV; ++i)
+                    yield return new Segment3d(vertices[i], vertices[i + 1]);
+            }
         }
 
-
-        public AxisAlignedBox3d GetBoundingBox()
+        public Vector3d PointAt(int iSegment, double fSegT)
         {
-            // [RMS] problem w/ readonly because vector is a class...
-            //AxisAlignedBox3d box = AxisAlignedBox3d.Empty;
-            AxisAlignedBox3d box = new AxisAlignedBox3d(false);
+            Segment3d seg = new Segment3d(vertices[iSegment], vertices[(iSegment + 1) % vertices.Count]);
+            return seg.PointAt(fSegT);
+        }
+
+
+        public AxisAlignedBox3d GetBoundingBox() {
+            AxisAlignedBox3d box = AxisAlignedBox3d.Empty;
             foreach (Vector3d v in vertices)
                 box.Contain(v);
             return box;
         }
 
         public double ArcLength {
-            get {
-                double dLen = 0;
-                for (int i = 1; i < vertices.Count; ++i)
-                    dLen += (vertices[i] - vertices[i - 1]).Length;
-                return dLen;
-            }
+            get { return CurveUtils.ArcLength(vertices, Closed); }
         }
 
-        public Vector3d Tangent(int i)
-        {
-            if (i == 0)
-                return (vertices[1] - vertices[0]).Normalized;
-            else if (i == vertices.Count - 1)
-                return (vertices.Last() - vertices[vertices.Count - 2]).Normalized;
-            else
-                return (vertices[i + 1] - vertices[i - 1]).Normalized;
+        public Vector3d Tangent(int i) {
+            return CurveUtils.GetTangent(vertices, i, Closed);
         }
 
         public Vector3d Centroid(int i)
         {
-            if (i == 0 || i == vertices.Count - 1)
-                return vertices[i];
-            else
-                return 0.5 * (vertices[i + 1] + vertices[i - 1]);
+            if (Closed) {
+                int NV = vertices.Count;
+                if (i == 0)
+                    return 0.5 * (vertices[1] + vertices[NV - 1]);
+                else
+                    return 0.5 * (vertices[(i+1)%NV] + vertices[i-1]);
+            } else {
+                if (i == 0 || i == vertices.Count - 1)
+                    return vertices[i];
+                else
+                    return 0.5 * (vertices[i + 1] + vertices[i - 1]);
+            }
         }
 
 
+        public Index2i Neighbours(int i)
+        {
+            int NV = vertices.Count;
+            if (Closed) {
+                if (i == 0)
+                    return new Index2i(NV-1, 1);
+                else
+                    return new Index2i(i-1, (i+1) % NV);
+            } else {
+                if (i == 0)
+                    return new Index2i(-1, 1);
+                else if (i == NV-1)
+                    return new Index2i(NV-2, -1);
+                else
+                    return new Index2i(i-1, i+1);
+            }
+        } 
+
+
+        /// <summary>
+        /// Compute opening angle at vertex i in degrees
+        /// </summary>
+        public double OpeningAngleDeg(int i)
+        {
+            int prev = i - 1, next = i + 1;
+            if ( Closed ) {
+                int NV = vertices.Count;
+                prev = (i == 0) ? NV - 1 : prev;
+                next = next % NV;
+            } else {
+                if (i == 0 || i == vertices.Count - 1)
+                    return 180;
+            }
+            Vector3d e1 = (vertices[prev] - vertices[i]);
+            Vector3d e2 = (vertices[next] - vertices[i]);
+            e1.Normalize(); e2.Normalize();
+            return Vector3d.AngleD(e1, e2);
+        }
+
 
+        /// <summary>
+        /// Find nearest vertex to point p
+        /// </summary>
         public int NearestVertex(Vector3d p)
         {
             double nearSqr = double.MaxValue;
@@ -189,6 +264,9 @@ public int NearestVertex(Vector3d p)
         }
 
 
+        /// <summary>
+        /// find squared distance from p to nearest segment on polyline
+        /// </summary>
         public double DistanceSquared(Vector3d p, out int iNearSeg, out double fNearSegT)
         {
             iNearSeg = -1;
@@ -220,5 +298,37 @@ public double DistanceSquared(Vector3d p) {
             return DistanceSquared(p, out iseg, out segt);
         }
 
+
+
+        /// <summary>
+        /// Resample curve so that:
+        ///   - if opening angle at vertex is > sharp_thresh, we emit two more vertices at +/- corner_t, where the t is used in prev/next lerps
+        ///   - if opening angle is > flat_thresh, we skip the vertex entirely (simplification)
+        /// This is mainly useful to get nicer polylines to use as the basis for (eg) creating 3D tubes, rendering, etc
+        /// 
+        /// [TODO] skip tiny segments?
+        /// </summary>
+        public DCurve3 ResampleSharpTurns(double sharp_thresh = 90, double flat_thresh = 189, double corner_t = 0.01)
+        {
+            int NV = vertices.Count;
+            DCurve3 resampled = new DCurve3() { Closed = this.Closed };
+            double prev_t = 1.0 - corner_t;
+            for (int k = 0; k < NV; ++k) {
+                double open_angle = Math.Abs(OpeningAngleDeg(k));
+                if (open_angle > flat_thresh && k > 0) {
+                    // ignore skip this vertex
+                } else if (open_angle > sharp_thresh) {
+                    resampled.AppendVertex(vertices[k]);
+                } else {
+                    Vector3d n = vertices[(k + 1) % NV];
+                    Vector3d p = vertices[k == 0 ? NV - 1 : k - 1];
+                    resampled.AppendVertex(Vector3d.Lerp(p, vertices[k], prev_t));
+                    resampled.AppendVertex(vertices[k]);
+                    resampled.AppendVertex(Vector3d.Lerp(vertices[k], n, corner_t));
+                }
+            }
+            return resampled;
+        }
+
     }
 }
diff --git a/curve/DGraph.cs b/curve/DGraph.cs
index 60374f66..75e4363e 100644
--- a/curve/DGraph.cs
+++ b/curve/DGraph.cs
@@ -18,7 +18,7 @@ namespace g3
     public abstract class DGraph
     {
         public const int InvalidID = -1;
-        public const int DuplicateEdgeID = -1;
+        public const int DuplicateEdgeID = -2;
 
         public static readonly Index2i InvalidEdgeV = new Index2i(InvalidID, InvalidID);
         public static readonly Index3i InvalidEdge3 = new Index3i(InvalidID, InvalidID, InvalidID);
diff --git a/curve/DGraph2.cs b/curve/DGraph2.cs
index 46ee4fd4..a3c501ed 100644
--- a/curve/DGraph2.cs
+++ b/curve/DGraph2.cs
@@ -154,7 +154,8 @@ public void AppendPolyline(PolyLine2d poly, int gid = -1)
             int N = poly.VertexCount;
             for (int i = 0; i < N; ++i) {
                 int cur = AppendVertex(poly[i]);
-                AppendEdge(prev, cur, gid);
+                if ( i > 0 )
+                    AppendEdge(prev, cur, gid);
                 prev = cur;
             }
         }
@@ -164,7 +165,7 @@ public void AppendGraph(DGraph2 graph, int gid = -1)
         {
             int[] mapV = new int[graph.MaxVertexID];
             foreach ( int vid in graph.VertexIndices()) {
-                    mapV[vid] = this.AppendVertex(graph.GetVertex(vid));
+                mapV[vid] = this.AppendVertex(graph.GetVertex(vid));
             }
             foreach ( int eid in graph.EdgeIndices()) {
                 Index2i ev = graph.GetEdgeV(eid);
diff --git a/curve/DGraph2Util.cs b/curve/DGraph2Util.cs
index a53b2eab..0bb8512d 100644
--- a/curve/DGraph2Util.cs
+++ b/curve/DGraph2Util.cs
@@ -14,7 +14,7 @@ public static class DGraph2Util
     {
 
 
-        public struct Curves
+        public class Curves
         {
             public List<Polygon2d> Loops;
             public List<PolyLine2d> Paths;
@@ -76,16 +76,24 @@ public static Curves ExtractCurves(DGraph2 graph)
 
                     PolyLine2d path = new PolyLine2d();
                     path.AppendVertex(graph.GetVertex(vid));
+                    bool is_loop = false;
                     while (true) {
                         used.Add(eid);
                         Index2i next = NextEdgeAndVtx(eid, vid, graph);
                         eid = next.a;
                         vid = next.b;
+                        if ( vid == start_vid ) {
+                            is_loop = true;
+                            break;
+                        }
                         path.AppendVertex(graph.GetVertex(vid));
                         if (eid == int.MaxValue || junctions.Contains(vid))
-                            break;  // done!
+                            break;
                     }
-                    c.Paths.Add(path);
+                    if (is_loop)
+                        c.Loops.Add(new Polygon2d(path.Vertices));
+                    else
+                        c.Paths.Add(path);
                 }
 
             }
@@ -117,7 +125,6 @@ public static Curves ExtractCurves(DGraph2 graph)
                 c.Loops.Add(poly);
             }
 
-
             return c;
         }
 
@@ -125,6 +132,145 @@ public static Curves ExtractCurves(DGraph2 graph)
 
 
 
+        /// <summary>
+        /// merge members of c.Paths that have unique endpoint pairings.
+        /// Does *not* extract closed loops that contain junction vertices,
+        /// unless the 'other' end of those junctions is dangling.
+        /// Also, horribly innefficient!
+        /// </summary>
+        public static void ChainOpenPaths(Curves c, double epsilon = MathUtil.Epsilon)
+        {
+            List<PolyLine2d> to_process = new List<PolyLine2d>(c.Paths);
+            c.Paths = new List<PolyLine2d>();
+
+            // first we separate out 'dangling' curves that have no match on at least one side
+            List<PolyLine2d> dangling = new List<PolyLine2d>();
+            List<PolyLine2d> remaining = new List<PolyLine2d>();
+
+            bool bContinue = true;
+            while (bContinue && to_process.Count > 0) {
+                bContinue = false;
+                foreach (PolyLine2d p in to_process) {
+                    var matches_start = find_connected_start(p, to_process, epsilon);
+                    var matches_end = find_connected_end(p, to_process, epsilon);
+                    if (matches_start.Count == 0 || matches_end.Count == 0) {
+                        dangling.Add(p);
+                        bContinue = true;
+                    } else
+                        remaining.Add(p);
+                }
+                to_process.Clear(); to_process.AddRange(remaining); remaining.Clear();
+            }
+
+            //to_process.Clear(); to_process.AddRange(remaining); remaining.Clear();
+
+            // now incrementally merge together unique matches
+            // [TODO] this will not match across junctions!
+            bContinue = true;
+            while (bContinue && to_process.Count > 0) {
+                bContinue = false;
+                restart_itr:
+                foreach (PolyLine2d p in to_process) {
+                    var matches_start = find_connected_start(p, to_process, epsilon);
+                    var matches_end = find_connected_end(p, to_process, 2*epsilon);
+                    if (matches_start.Count == 1 && matches_end.Count == 1 &&
+                         matches_start[0] == matches_end[0]) {
+                        c.Loops.Add(to_loop(p, matches_start[0], epsilon));
+                        to_process.Remove(p);
+                        to_process.Remove(matches_start[0]);
+                        remaining.Remove(matches_start[0]);
+                        bContinue = true;
+                        goto restart_itr;
+                    } else if (matches_start.Count == 1 && matches_end.Count < 2) {
+                        remaining.Add(merge_paths(matches_start[0], p, 2*epsilon));
+                        to_process.Remove(p);
+                        to_process.Remove(matches_start[0]);
+                        remaining.Remove(matches_start[0]);
+                        bContinue = true;
+                        goto restart_itr;
+                    } else if (matches_end.Count == 1 && matches_start.Count < 2) {
+                        remaining.Add(merge_paths(p, matches_end[0], 2*epsilon));
+                        to_process.Remove(p);
+                        to_process.Remove(matches_end[0]);
+                        remaining.Remove(matches_end[0]);
+                        bContinue = true;
+                        goto restart_itr;
+                    } else {
+                        remaining.Add(p);
+                    }
+                }
+                to_process.Clear(); to_process.AddRange(remaining); remaining.Clear();
+            }
+
+            c.Paths.AddRange(to_process);
+
+            // [TODO] now that we have found all loops, we can chain in dangling curves
+
+            c.Paths.AddRange(dangling);
+
+        }
+
+
+
+
+
+        static List<PolyLine2d> find_connected_start(PolyLine2d pTest, List<PolyLine2d> potential, double eps = MathUtil.Epsilon)
+        {
+            List<PolyLine2d> result = new List<PolyLine2d>();
+            foreach ( var p in potential ) {
+                if (pTest == p)
+                    continue;
+                if (pTest.Start.Distance(p.Start) < eps ||
+                     pTest.Start.Distance(p.End) < eps)
+                    result.Add(p);
+            }
+            return result;
+        }
+        static List<PolyLine2d> find_connected_end(PolyLine2d pTest, List<PolyLine2d> potential, double eps = MathUtil.Epsilon)
+        {
+            List<PolyLine2d> result = new List<PolyLine2d>();
+            foreach (var p in potential) {
+                if (pTest == p)
+                    continue;
+                if ( pTest.End.Distance(p.Start) < eps ||
+                     pTest.End.Distance(p.End) < eps)
+                    result.Add(p);
+            }
+            return result;
+        }
+        static Polygon2d to_loop(PolyLine2d p1, PolyLine2d p2, double eps = MathUtil.Epsilon)
+        {
+            Polygon2d p = new Polygon2d(p1.Vertices);
+            if (p1.End.Distance(p2.Start) > eps)
+                p2.Reverse();
+            p.AppendVertices(p2);
+            return p;               
+        }
+        static PolyLine2d merge_paths(PolyLine2d p1, PolyLine2d p2, double eps = MathUtil.Epsilon)
+        {
+            PolyLine2d pNew;
+            if (p1.End.Distance(p2.Start) < eps) {
+                pNew = new PolyLine2d(p1);
+                pNew.AppendVertices(p2);
+            } else if (p1.End.Distance(p2.End) < eps) {
+                pNew = new PolyLine2d(p1);
+                p2.Reverse();
+                pNew.AppendVertices(p2);
+            } else if (p1.Start.Distance(p2.Start) < eps) {
+                p2.Reverse();
+                pNew = new PolyLine2d(p2);
+                pNew.AppendVertices(p1);
+            } else if (p1.Start.Distance(p2.End) < eps) {
+                pNew = new PolyLine2d(p2);
+                pNew.AppendVertices(p1);
+            } else
+                throw new Exception("shit");
+            return pNew;
+        }
+
+
+
+
         /// <summary>
         /// Find and remove any junction (ie valence>2) vertices of the graph.
         /// At a junction, the pair of best-aligned (ie straightest) edges are left 
diff --git a/curve/DGraph3Util.cs b/curve/DGraph3Util.cs
index a056a3e7..1bf54209 100644
--- a/curve/DGraph3Util.cs
+++ b/curve/DGraph3Util.cs
@@ -16,17 +16,29 @@ public struct Curves
         {
             public List<DCurve3> Loops;
             public List<DCurve3> Paths;
+
+            public HashSet<int> BoundaryV;
+            public HashSet<int> JunctionV;
+
+            public List<List<int>> LoopEdges;
+            public List<List<int>> PathEdges;
         }
 
 
         /// <summary>
         /// Decompose graph into simple polylines and polygons. 
         /// </summary>
-        public static Curves ExtractCurves(DGraph3 graph)
+        public static Curves ExtractCurves(DGraph3 graph,
+            bool bWantLoopIndices = false,
+            Func<int, bool> CurveOrientationF = null )
         {
             Curves c = new Curves();
             c.Loops = new List<DCurve3>();
             c.Paths = new List<DCurve3>();
+            if (bWantLoopIndices) {
+                c.LoopEdges = new List<List<int>>();
+                c.PathEdges = new List<List<int>>();
+            }
 
             HashSet<int> used = new HashSet<int>();
 
@@ -46,9 +58,13 @@ public static Curves ExtractCurves(DGraph3 graph)
                 int eid = graph.GetVtxEdges(vid)[0];
                 if (used.Contains(eid))
                     continue;
+                bool reverse = (CurveOrientationF != null) ? CurveOrientationF(eid) : false;
 
                 DCurve3 path = new DCurve3() { Closed = false };
+                List<int> pathE = (bWantLoopIndices) ? new List<int>() : null;
                 path.AppendVertex(graph.GetVertex(vid));
+                if (pathE != null)
+                    pathE.Add(eid);
                 while ( true ) {
                     used.Add(eid);
                     Index2i next = NextEdgeAndVtx(eid, vid, graph);
@@ -57,12 +73,24 @@ public static Curves ExtractCurves(DGraph3 graph)
                     path.AppendVertex(graph.GetVertex(vid));
                     if (boundaries.Contains(vid) || junctions.Contains(vid))
                         break;  // done!
+                    if (pathE != null)
+                        pathE.Add(eid);
                 }
+                if (reverse) 
+                    path.Reverse();
                 c.Paths.Add(path);
+
+                if ( pathE != null ) {
+                    Util.gDevAssert(pathE.Count == path.VertexCount - 1);
+                    if (reverse)
+                        pathE.Reverse();
+                    c.PathEdges.Add(pathE);
+                }
             }
 
             // ok we should be done w/ boundary verts now...
-            boundaries.Clear();
+            //boundaries.Clear();
+            c.BoundaryV = boundaries;
 
 
             foreach ( int start_vid in junctions ) {
@@ -72,8 +100,13 @@ public static Curves ExtractCurves(DGraph3 graph)
                     int vid = start_vid;
                     int eid = outgoing_eid;
 
+                    bool reverse = (CurveOrientationF != null) ? CurveOrientationF(eid) : false;
+
                     DCurve3 path = new DCurve3() { Closed = false };
+                    List<int> pathE = (bWantLoopIndices) ? new List<int>() : null;
                     path.AppendVertex(graph.GetVertex(vid));
+                    if (pathE != null)
+                        pathE.Add(eid);
                     while (true) {
                         used.Add(eid);
                         Index2i next = NextEdgeAndVtx(eid, vid, graph);
@@ -82,24 +115,46 @@ public static Curves ExtractCurves(DGraph3 graph)
                         path.AppendVertex(graph.GetVertex(vid));
                         if (eid == int.MaxValue || junctions.Contains(vid))
                             break;  // done!
+                        if (pathE != null)
+                            pathE.Add(eid);
                     }
 
                     // we could end up back at our start junction vertex!
                     if (vid == start_vid) {
                         path.RemoveVertex(path.VertexCount - 1);
                         path.Closed = true;
+                        if (reverse)
+                            path.Reverse();
                         c.Loops.Add(path);
+
+                        if (pathE != null) {
+                            Util.gDevAssert(pathE.Count == path.VertexCount);
+                            if (reverse)
+                                pathE.Reverse();
+                            c.LoopEdges.Add(pathE);
+                        }
+
                         // need to mark incoming edge as used...but is it valid now?
                         //Util.gDevAssert(eid != int.MaxValue);
-                        if ( eid != int.MaxValue )
+                        if (eid != int.MaxValue)
                             used.Add(eid);
 
                     } else {
+                        if (reverse)
+                            path.Reverse();
                         c.Paths.Add(path);
+
+                        if (pathE != null) {
+                            Util.gDevAssert(pathE.Count == path.VertexCount - 1);
+                            if (reverse)
+                                pathE.Reverse();
+                            c.PathEdges.Add(pathE);
+                        }
                     }
                 }
 
             }
+            c.JunctionV = junctions;
 
 
             // all that should be left are continuous loops...
@@ -111,23 +166,39 @@ public static Curves ExtractCurves(DGraph3 graph)
                 Index2i ev = graph.GetEdgeV(eid);
                 int vid = ev.a;
 
+                bool reverse = (CurveOrientationF != null) ? CurveOrientationF(eid) : false;
+
                 DCurve3 poly = new DCurve3() { Closed = true };
+                List<int> polyE = (bWantLoopIndices) ? new List<int>() : null;
                 poly.AppendVertex(graph.GetVertex(vid));
+                if (polyE != null)
+                    polyE.Add(eid);
                 while (true) {
                     used.Add(eid);
                     Index2i next = NextEdgeAndVtx(eid, vid, graph);
                     eid = next.a;
                     vid = next.b;
                     poly.AppendVertex(graph.GetVertex(vid));
+                    if (polyE != null)
+                        polyE.Add(eid);
                     if (eid == int.MaxValue || junctions.Contains(vid))
                         throw new Exception("how did this happen??");
                     if (used.Contains(eid))
                         break;
                 }
                 poly.RemoveVertex(poly.VertexCount - 1);
+                if (reverse)
+                    poly.Reverse();
                 c.Loops.Add(poly);
-            }
 
+                if (polyE != null) {
+                    polyE.RemoveAt(polyE.Count - 1);
+                    Util.gDevAssert(polyE.Count == poly.VertexCount);
+                    if (reverse)
+                        polyE.Reverse();
+                    c.LoopEdges.Add(polyE);
+                }
+            }
 
             return c;
         }
@@ -213,5 +284,58 @@ public static List<int> WalkToNextNonRegularVtx(DGraph3 graph, int fromVtx, int
 
 
 
+
+
+        /// <summary>
+        /// Erode inwards from open boundary vertices of graph (ie vtx with single edge).
+        /// Resulting graph is not compact (!)
+        /// </summary>
+        public static void ErodeOpenSpurs(DGraph3 graph)
+        {
+            HashSet<int> used = new HashSet<int>();     // do we need this?
+
+            // find boundary and junction vertices
+            HashSet<int> boundaries = new HashSet<int>();
+            HashSet<int> junctions = new HashSet<int>();
+            foreach (int vid in graph.VertexIndices()) {
+                if (graph.IsBoundaryVertex(vid))
+                    boundaries.Add(vid);
+                if (graph.IsJunctionVertex(vid))
+                    junctions.Add(vid);
+            }
+
+            // walk paths from boundary vertices
+            foreach (int start_vid in boundaries) {
+                if (graph.IsVertex(start_vid) == false)
+                    continue;
+
+                int vid = start_vid;
+                int eid = graph.GetVtxEdges(vid)[0];
+                if (used.Contains(eid))
+                    continue;
+
+                List<int> pathE = new List<int>();
+                if (pathE != null)
+                    pathE.Add(eid);
+                while (true) {
+                    used.Add(eid);
+                    Index2i next = NextEdgeAndVtx(eid, vid, graph);
+                    eid = next.a;
+                    vid = next.b;
+                    if (boundaries.Contains(vid) || junctions.Contains(vid))
+                        break;  // done!
+                    if (pathE != null)
+                        pathE.Add(eid);
+                }
+
+                // delete this path
+                foreach (int path_eid in pathE)
+                    graph.RemoveEdge(path_eid, true);
+            }
+
+        }
+
+
+
     }
 }
diff --git a/curve/GeneralPolygon2d.cs b/curve/GeneralPolygon2d.cs
index abca2fd4..cc15dffa 100644
--- a/curve/GeneralPolygon2d.cs
+++ b/curve/GeneralPolygon2d.cs
@@ -41,10 +41,10 @@ public Polygon2d Outer {
 		}
 
 
-		public void AddHole(Polygon2d hole, bool bCheck = true) {
+		public void AddHole(Polygon2d hole, bool bCheckContainment = true, bool bCheckOrientation = true) {
 			if ( outer == null )
 				throw new Exception("GeneralPolygon2d.AddHole: outer polygon not set!");
-			if ( bCheck ) {
+			if ( bCheckContainment ) {
 				if ( outer.Contains(hole) == false )
 					throw new Exception("GeneralPolygon2d.AddHole: outer does not contain hole!");
 
@@ -54,13 +54,18 @@ public void AddHole(Polygon2d hole, bool bCheck = true) {
 						throw new Exception("GeneralPolygon2D.AddHole: new hole intersects existing hole!");
 				}
 			}
-
-			if ( (bOuterIsCW && hole.IsClockwise) || (bOuterIsCW == false && hole.IsClockwise == false) )
-				throw new Exception("GeneralPolygon2D.AddHole: new hole has same orientation as outer polygon!");
+            if ( bCheckOrientation ) {
+                if ((bOuterIsCW && hole.IsClockwise) || (bOuterIsCW == false && hole.IsClockwise == false))
+                    throw new Exception("GeneralPolygon2D.AddHole: new hole has same orientation as outer polygon!");
+            }
 
 			holes.Add(hole);
 		}
 
+        public void ClearHoles() {
+            holes.Clear();
+        }
+
 
 		bool HasHoles {
 			get { return holes.Count > 0; }
@@ -183,6 +188,22 @@ public bool Contains(Polygon2d poly) {
             return true;
         }
 
+        /// <summary>
+        /// Checks that all points on a segment are within the area defined by the GeneralPolygon2d;
+        /// holes are included in the calculation.
+        /// </summary>
+        public bool Contains(Segment2d seg)
+        {
+            if (outer.Contains(seg) == false)
+                return false;
+            foreach (var h in holes)
+            {
+                if (h.Intersects(seg))
+                    return false;
+            }
+            return true;
+        }
+
 
         public bool Intersects(Polygon2d poly)
         {
diff --git a/curve/ICurve.cs b/curve/ICurve.cs
index 8e59be2b..eaf106ea 100644
--- a/curve/ICurve.cs
+++ b/curve/ICurve.cs
@@ -31,9 +31,11 @@ public interface IParametricCurve3d
     public interface ISampledCurve3d
     {
         int VertexCount { get; }
+        int SegmentCount { get; }
         bool Closed { get; }
 
         Vector3d GetVertex(int i);
+        Segment3d GetSegment(int i);
 
         IEnumerable<Vector3d> Vertices { get; }
     }
diff --git a/curve/LaplacianCurveDeformer.cs b/curve/LaplacianCurveDeformer.cs
new file mode 100644
index 00000000..4debee3c
--- /dev/null
+++ b/curve/LaplacianCurveDeformer.cs
@@ -0,0 +1,372 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace g3
+{
+    /// <summary>
+    /// Variant of LaplacianMeshDeformer that can be applied to 3D curve.
+    /// 
+    /// Solve in each dimension can be disabled using .SolveX/Y/Z
+    /// 
+    /// Currently only supports uniform weights (in Initialize)
+    /// 
+    /// </summary>
+    public class LaplacianCurveDeformer
+    {
+        public DCurve3 Curve;
+
+
+        public bool SolveX = true;
+        public bool SolveY = true;
+        public bool SolveZ = true;
+
+
+        // indicates that solve did not converge in at least one dimension
+        public bool ConvergeFailed = false;
+
+
+        // info that is fixed based on mesh
+        PackedSparseMatrix PackedM;
+        int N;
+        int[] ToCurveV, ToIndex;
+        double[] Px, Py, Pz;
+        int[] nbr_counts;
+        double[] MLx, MLy, MLz;
+
+        // constraints
+        public struct SoftConstraintV
+        {
+            public Vector3d Position;
+            public double Weight;
+            public bool PostFix;
+        }
+        Dictionary<int, SoftConstraintV> SoftConstraints = new Dictionary<int, SoftConstraintV>();
+        bool HavePostFixedConstraints = false;
+
+
+        // needs to be updated after constraints
+        bool need_solve_update;
+        DiagonalMatrix WeightsM;
+        double[] Cx, Cy, Cz;
+        double[] Bx, By, Bz;
+        DiagonalMatrix Preconditioner;
+
+
+        // Appendix C from http://sites.fas.harvard.edu/~cs277/papers/deformation_survey.pdf
+        public bool UseSoftConstraintNormalEquations = true;
+
+
+        // result
+        double[] Sx, Sy, Sz;
+
+
+        public LaplacianCurveDeformer(DCurve3 curve)
+        {
+            Curve = curve;
+        }
+
+
+        public void SetConstraint(int vID, Vector3d targetPos, double weight, bool bForceToFixedPos = false)
+        {
+            SoftConstraints[vID] = new SoftConstraintV() { Position = targetPos, Weight = weight, PostFix = bForceToFixedPos };
+            HavePostFixedConstraints = HavePostFixedConstraints || bForceToFixedPos;
+            need_solve_update = true;
+        }
+
+        public bool IsConstrained(int vID) {
+            return SoftConstraints.ContainsKey(vID);
+        }
+
+        public void ClearConstraints()
+        {
+            SoftConstraints.Clear();
+            HavePostFixedConstraints = false;
+            need_solve_update = true;
+        }
+
+
+        public void Initialize()
+        {
+            int NV = Curve.VertexCount;
+            ToCurveV = new int[NV];
+            ToIndex = new int[NV];
+
+            N = 0;
+            for ( int k = 0; k < NV; k++) {
+                int vid = k;
+                ToCurveV[N] = vid;
+                ToIndex[vid] = N;
+                N++;
+            }
+
+            Px = new double[N];
+            Py = new double[N];
+            Pz = new double[N];
+            nbr_counts = new int[N];
+            SymmetricSparseMatrix M = new SymmetricSparseMatrix();
+
+            for (int i = 0; i < N; ++i) {
+                int vid = ToCurveV[i];
+                Vector3d v = Curve.GetVertex(vid);
+                Px[i] = v.x; Py[i] = v.y; Pz[i] = v.z;
+                nbr_counts[i] = (i == 0 || i == N-1) ? 1 : 2;
+            }
+
+            // construct laplacian matrix
+            for (int i = 0; i < N; ++i) {
+                int vid = ToCurveV[i];
+                int n = nbr_counts[i];
+
+                Index2i nbrs = Curve.Neighbours(vid);
+                
+                double sum_w = 0;
+                for ( int k = 0; k < 2; ++k ) {
+                    int nbrvid = nbrs[k];
+                    if (nbrvid == -1)
+                        continue;
+                    int j = ToIndex[nbrvid];
+                    int n2 = nbr_counts[j];
+
+                    // weight options
+                    double w = -1;
+                    //double w = -1.0 / Math.Sqrt(n + n2);
+                    //double w = -1.0 / n;
+
+                    M.Set(i, j, w);
+                    sum_w += w;
+                }
+                sum_w = -sum_w;
+                M.Set(vid, vid, sum_w);
+            }
+
+            // transpose(L) * L, but matrix is symmetric...
+            if (UseSoftConstraintNormalEquations) {
+                //M = M.Multiply(M);
+                // only works if M is symmetric!!
+                PackedM = M.SquarePackedParallel();
+            } else {
+                PackedM = new PackedSparseMatrix(M);
+            }
+
+            // compute laplacian vectors of initial mesh positions
+            MLx = new double[N];
+            MLy = new double[N];
+            MLz = new double[N];
+            PackedM.Multiply(Px, MLx);
+            PackedM.Multiply(Py, MLy);
+            PackedM.Multiply(Pz, MLz);
+
+            // allocate memory for internal buffers
+            Preconditioner = new DiagonalMatrix(N);
+            WeightsM = new DiagonalMatrix(N);
+            Cx = new double[N]; Cy = new double[N]; Cz = new double[N];
+            Bx = new double[N]; By = new double[N]; Bz = new double[N];
+            Sx = new double[N]; Sy = new double[N]; Sz = new double[N];
+
+            need_solve_update = true;
+            UpdateForSolve();
+        }
+
+
+
+
+        void UpdateForSolve()
+        {
+            if (need_solve_update == false)
+                return;
+
+            // construct constraints matrix and RHS
+            WeightsM.Clear();
+            Array.Clear(Cx, 0, N);
+            Array.Clear(Cy, 0, N);
+            Array.Clear(Cz, 0, N);
+            foreach ( var constraint in SoftConstraints ) {
+                int vid = constraint.Key;
+                int i = ToIndex[vid];
+                double w = constraint.Value.Weight;
+
+                if (UseSoftConstraintNormalEquations)
+                    w = w * w;
+
+                WeightsM.Set(i, i, w);
+                Vector3d pos = constraint.Value.Position;
+                Cx[i] = w * pos.x;
+                Cy[i] = w * pos.y;
+                Cz[i] = w * pos.z;
+            }
+
+            // add RHS vectors
+            for (int i = 0; i < N; ++i) {
+                Bx[i] = MLx[i] + Cx[i];
+                By[i] = MLy[i] + Cy[i];
+                Bz[i] = MLz[i] + Cz[i];
+            }
+
+            // update basic preconditioner
+            // [RMS] currently not using this...it actually seems to make things worse!! 
+            for ( int i = 0; i < N; i++ ) {
+                double diag_value = PackedM[i, i] + WeightsM[i, i];
+                Preconditioner.Set(i, i, 1.0 / diag_value);
+            }
+
+            need_solve_update = false;
+        }
+
+
+
+        // Result must be as large as Mesh.MaxVertexID
+        public bool SolveMultipleCG(Vector3d[] Result)
+        {
+            if (WeightsM == null)
+                Initialize();       // force initialize...
+
+            UpdateForSolve();
+
+            // use initial positions as initial solution. 
+            Array.Copy(Px, Sx, N);
+            Array.Copy(Py, Sy, N);
+            Array.Copy(Pz, Sz, N);
+
+
+            Action<double[], double[]> CombinedMultiply = (X, B) => {
+                //PackedM.Multiply(X, B);
+                PackedM.Multiply_Parallel(X, B);
+
+                for (int i = 0; i < N; ++i)
+                    B[i] += WeightsM[i, i] * X[i];
+            };
+
+            List<SparseSymmetricCG> Solvers = new List<SparseSymmetricCG>();
+            if (SolveX) {
+                Solvers.Add(new SparseSymmetricCG() { B = Bx, X = Sx,
+                    MultiplyF = CombinedMultiply, PreconditionMultiplyF = Preconditioner.Multiply,
+                    UseXAsInitialGuess = true
+                });
+            }
+            if (SolveY) {
+                Solvers.Add(new SparseSymmetricCG() { B = By, X = Sy,
+                    MultiplyF = CombinedMultiply, PreconditionMultiplyF = Preconditioner.Multiply,
+                    UseXAsInitialGuess = true
+                });
+            }
+            if (SolveZ) {
+                Solvers.Add(new SparseSymmetricCG() { B = Bz, X = Sz,
+                    MultiplyF = CombinedMultiply, PreconditionMultiplyF = Preconditioner.Multiply,
+                    UseXAsInitialGuess = true
+                });
+            }
+            bool[] ok = new bool[Solvers.Count];
+
+            gParallel.ForEach(Interval1i.Range(Solvers.Count), (i) => {
+                ok[i] = Solvers[i].Solve();
+                // preconditioned solve is slower =\
+                //ok[i] = solvers[i].SolvePreconditioned();
+            });
+
+            ConvergeFailed = false;
+            foreach ( bool b in ok ) {
+                if (b == false)
+                    ConvergeFailed = true;
+            }
+
+            for ( int i = 0; i < N; ++i ) {
+                int vid = ToCurveV[i];
+                Result[vid] = new Vector3d(Sx[i], Sy[i], Sz[i]);
+            }
+
+            // apply post-fixed constraints
+            if (HavePostFixedConstraints) {
+                foreach (var constraint in SoftConstraints) {
+                    if (constraint.Value.PostFix) {
+                        int vid = constraint.Key;
+                        Result[vid] = constraint.Value.Position;
+                    }
+                }
+            }
+
+            return true;
+        }
+
+
+
+
+        // Result must be as large as Mesh.MaxVertexID
+        public bool SolveMultipleRHS(Vector3d[] Result)
+        {
+            if (WeightsM == null)
+                Initialize();       // force initialize...
+
+            UpdateForSolve();
+
+            // use initial positions as initial solution. 
+            double[][] B = BufferUtil.InitNxM(3, N, new double[][] { Bx, By, Bz });
+            double[][] X = BufferUtil.InitNxM(3, N, new double[][] { Px, Py, Pz });
+
+            Action<double[][], double[][]> CombinedMultiply = (Xt, Bt) => {
+                PackedM.Multiply_Parallel_3(Xt, Bt);
+                gParallel.ForEach(Interval1i.Range(3), (j) => {
+                    BufferUtil.MultiplyAdd(Bt[j], WeightsM.D, Xt[j]);
+                });
+            };
+
+            SparseSymmetricCGMultipleRHS Solver = new SparseSymmetricCGMultipleRHS() {
+                B = B, X = X,
+                MultiplyF = CombinedMultiply, PreconditionMultiplyF = null,
+                UseXAsInitialGuess = true
+            };
+
+            bool ok = Solver.Solve();
+
+            if (ok == false)
+                return false;
+
+            for (int i = 0; i < N; ++i) {
+                int vid = ToCurveV[i];
+                Result[vid] = new Vector3d(X[0][i], X[1][i], X[2][i]);
+            }
+
+            // apply post-fixed constraints
+            if (HavePostFixedConstraints) {
+                foreach (var constraint in SoftConstraints) {
+                    if (constraint.Value.PostFix) {
+                        int vid = constraint.Key;
+                        Result[vid] = constraint.Value.Position;
+                    }
+                }
+            }
+
+            return true;
+        }
+
+
+
+
+
+        public bool Solve(Vector3d[] Result)
+        {
+            // for small problems, faster to use separate CGs?
+            if ( Curve.VertexCount < 10000 )
+                return SolveMultipleCG(Result);
+            else
+                return SolveMultipleRHS(Result);
+        }
+
+
+
+        public bool SolveAndUpdateCurve()
+        {
+            int N = Curve.VertexCount;
+            Vector3d[] Result = new Vector3d[N];
+            if ( Solve(Result) == false )
+                return false;
+            for (int i = 0; i < N; ++i) {
+                Curve[i] = Result[i];
+            }
+            return true;
+        }
+
+
+
+    }
+}
diff --git a/curve/PolyLine2d.cs b/curve/PolyLine2d.cs
index 5364974c..213ff496 100644
--- a/curve/PolyLine2d.cs
+++ b/curve/PolyLine2d.cs
@@ -34,6 +34,12 @@ public PolyLine2d(IList<Vector2d> copy)
             Timestamp = 0;
         }
 
+        public PolyLine2d(IEnumerable<Vector2d> copy)
+        {
+            vertices = new List<Vector2d>(copy);
+            Timestamp = 0;
+        }
+
         public PolyLine2d(Vector2d[] v)
 		{
 			vertices = new List<Vector2d>(v);
@@ -334,6 +340,42 @@ public PolyLine2d Transform(ITransform2 xform)
         }
 
 
+
+        static public PolyLine2d MakeBoxSpiral(Vector2d center, double len, double spacing)
+        {
+            PolyLine2d pline = new PolyLine2d();
+            pline.AppendVertex(center);
+
+            Vector2d c = center;
+            c.x += spacing / 2;
+            pline.AppendVertex(c);
+            c.y += spacing;
+            pline.AppendVertex(c);
+            double accum = spacing / 2 + spacing;
+
+            double w = spacing / 2;
+            double h = spacing;
+
+            double sign = -1.0;
+            while (accum < len) {
+                w += spacing;
+                c.x += sign * w;
+                pline.AppendVertex(c);
+                accum += w;
+
+                h += spacing;
+                c.y += sign * h;
+                pline.AppendVertex(c);
+                accum += h;
+
+                sign *= -1.0;
+            }
+
+            return pline;
+        }
+
+
+
     }
 
 
diff --git a/curve/PolySimplification2.cs b/curve/PolySimplification2.cs
index 9836ed3d..acce4bdb 100644
--- a/curve/PolySimplification2.cs
+++ b/curve/PolySimplification2.cs
@@ -13,7 +13,7 @@ namespace g3
     /// which is not ideal in many contexts (eg manufacturing).
     /// 
     /// Strategy here is :
-    ///   1) runs of vertices that are very close to straight lines (default 0.01mm deviation tol)
+    ///   1) find runs of vertices that are very close to straight lines (default 0.01mm deviation tol)
     ///   2) find all straight segments longer than threshold distance (default 2mm)
     ///   3) discard vertices that deviate less than tolerance (default = 0.2mm)
     ///      from sequential-points-segment, unless they are required to preserve
@@ -64,6 +64,27 @@ public PolySimplification2(PolyLine2d polycurve)
         }
 
 
+
+        /// <summary>
+        /// simplify outer and holes of a polygon solid with same thresholds
+        /// </summary>
+        public static void Simplify(GeneralPolygon2d solid, double deviationThresh)
+        {
+            PolySimplification2 simp = new PolySimplification2(solid.Outer);
+            simp.SimplifyDeviationThreshold = deviationThresh;
+            simp.Simplify();
+            solid.Outer.SetVertices(simp.Result, true);
+
+            foreach (var hole in solid.Holes) {
+                PolySimplification2 holesimp = new PolySimplification2(hole);
+                holesimp.SimplifyDeviationThreshold = deviationThresh;
+                holesimp.Simplify();
+                hole.SetVertices(holesimp.Result, true);
+            }
+        }
+
+
+
         public void Simplify()
         {
             bool[] keep_seg = new bool[Vertices.Count];
@@ -117,13 +138,19 @@ List<Vector2d> collapse_by_deviation_tol(List<Vector2d> input, bool[] keep_segme
 
                 if ( keep_segments[i0] ) {
                     if (last_i != i0) {
-                        Util.gDevAssert(input[i0].Distance(result[result.Count - 1]) > MathUtil.Epsilonf);
-                        result.Add(input[i0]);
+                        // skip join segment if it is degenerate
+                        double join_dist = input[i0].Distance(result[result.Count - 1]);
+                        if ( join_dist > MathUtil.Epsilon)
+                            result.Add(input[i0]);
                     }
                     result.Add(input[i1]);
                     last_i = i1;
-                    cur_i = i1;
                     skip_count = 0;
+                    if (i1 == 0) {
+                        cur_i = NStop;
+                    } else {
+                        cur_i = i1;
+                    }
                     continue;
                 }
 
@@ -152,12 +179,16 @@ List<Vector2d> collapse_by_deviation_tol(List<Vector2d> input, bool[] keep_segme
             }
 
             
-            if ( IsLoop ) { 
+            if ( IsLoop ) {
+                // if we skipped everything, rest of code doesn't work
+                if (result.Count < 3)
+                    return handle_tiny_case(result, input, keep_segments, offset_threshold);
+
                 Line2d last_line = Line2d.FromPoints(input[last_i], input[cur_i % N]);
                 bool collinear_startv = last_line.DistanceSquared(result[0]) < thresh_sqr;
                 bool collinear_starts = last_line.DistanceSquared(result[1]) < thresh_sqr;
-                if (collinear_startv && collinear_starts) {
-                    // last seg is collinaer w/ start seg, merge them
+                if (collinear_startv && collinear_starts && result.Count > 3) {
+                    // last seg is collinear w/ start seg, merge them
                     result[0] = input[last_i];
                     result.RemoveAt(result.Count - 1);
 
@@ -177,5 +208,20 @@ List<Vector2d> collapse_by_deviation_tol(List<Vector2d> input, bool[] keep_segme
         }
 
 
+
+        List<Vector2d> handle_tiny_case(List<Vector2d> result, List<Vector2d> input, bool[] keep_segments, double offset_threshold)
+        {
+            int N = input.Count;
+            if (N == 3)
+                return input;       // not much we can really do here...
+
+            result.Clear();
+            result.Add(input[0]);
+            result.Add(input[N/3]);
+            result.Add(input[N-N/3]);
+            return result;
+        }
+
+
     }
 }
diff --git a/curve/Polygon2d.cs b/curve/Polygon2d.cs
index 81ad24fc..b74287ca 100644
--- a/curve/Polygon2d.cs
+++ b/curve/Polygon2d.cs
@@ -29,6 +29,12 @@ public Polygon2d(IList<Vector2d> copy)
 			Timestamp = 0;
         }
 
+        public Polygon2d(IEnumerable<Vector2d> copy)
+        {
+            vertices = new List<Vector2d>(copy);
+            Timestamp = 0;
+        }
+
         public Polygon2d(Vector2d[] v)
         {
             vertices = new List<Vector2d>(v);
@@ -89,6 +95,11 @@ public void AppendVertex(Vector2d v)
             vertices.Add(v);
 			Timestamp++; 
         }
+        public void AppendVertices(IEnumerable<Vector2d> v)
+        {
+            vertices.AddRange(v);
+            Timestamp++;
+        }
 
         public void RemoveVertex(int idx)
         {
@@ -96,6 +107,20 @@ public void RemoveVertex(int idx)
             Timestamp++;
         }
 
+
+        public void SetVertices(List<Vector2d> newVertices, bool bTakeOwnership)
+        {
+            if ( bTakeOwnership) {
+                vertices = newVertices;
+            } else {
+                vertices.Clear();
+                int N = newVertices.Count;
+                for (int i = 0; i < N; ++i)
+                    vertices.Add(newVertices[i]);
+            }
+        }
+
+
         public void Reverse()
 		{
 			vertices.Reverse();
@@ -187,6 +212,8 @@ public double SignedArea {
 			get {
 				double fArea = 0;
 				int N = vertices.Count;
+				if (N == 0)
+					return 0;
 				Vector2d v1 = vertices[0], v2 = Vector2d.Zero;
 				for (int i = 0; i < N; ++i) {
 					v2 = vertices[(i + 1) % N];
@@ -196,6 +223,9 @@ public double SignedArea {
 				return fArea * 0.5;	
 			}
 		}
+        public double Area {
+            get { return Math.Abs(SignedArea); }
+        }
 
 
 
@@ -303,8 +333,24 @@ public bool Contains(Polygon2d o) {
 			return true;
 		}
 
+        /// <summary>
+        /// Checks that all points on a segment are within the area defined by the Polygon2d.
+        /// </summary>
+        public bool Contains(Segment2d o)
+        {
+            // [TODO] Add bbox check
+            if (Contains(o.P0) == false || Contains(o.P1) == false)
+                return false;
+
+            foreach (Segment2d seg in SegmentItr())
+            {
+                if (seg.Intersects(o))
+                    return false;
+            }
+            return true;
+        }
 
-		public bool Intersects(Polygon2d o) {
+        public bool Intersects(Polygon2d o) {
 			if ( ! this.GetBounds().Intersects( o.GetBounds() ) )
 				return false;
 
@@ -317,8 +363,27 @@ public bool Intersects(Polygon2d o) {
 			return false;
 		}
 
+        /// <summary>
+        /// Checks if any point on a segment is within the area defined by the Polygon2d.
+        /// </summary>
+        public bool Intersects(Segment2d o)
+        {
+            // [TODO] Add bbox check
+            if (Contains(o.P0) == true || Contains(o.P1) == true)
+                return true;
+
+            // [TODO] Add bbox check
+            foreach (Segment2d seg in SegmentItr())
+            {
+                if (seg.Intersects(o))
+                    return true;
+            }
+            return false;
+        }
+
 
-		public List<Vector2d> FindIntersections(Polygon2d o) {
+
+        public List<Vector2d> FindIntersections(Polygon2d o) {
 			List<Vector2d> v = new List<Vector2d>();
 			if ( ! this.GetBounds().Intersects( o.GetBounds() ) )
 				return v;
@@ -684,6 +749,17 @@ public void Chamfer(double chamfer_dist, double minConvexAngleDeg = 30, double m
 
 
 
+		/// <summary>
+		/// Return minimal bounding box of vertices, computed to epsilon tolerance
+		/// </summary>
+		public Box2d MinimalBoundingBox(double epsilon)
+		{
+			ContMinBox2 box2 = new ContMinBox2(vertices, epsilon, QueryNumberType.QT_DOUBLE, false);
+			return box2.MinBox;
+		}
+
+
+
         static public Polygon2d MakeRectangle(Vector2d center, double width, double height)
         {
             VectorArray2d vertices = new VectorArray2d(4);
diff --git a/distance/DistPoint3Triangle3.cs b/distance/DistPoint3Triangle3.cs
index f54725d5..3ba058a7 100644
--- a/distance/DistPoint3Triangle3.cs
+++ b/distance/DistPoint3Triangle3.cs
@@ -52,14 +52,21 @@ public double GetSquared()
             if (DistanceSquared >= 0)
                 return DistanceSquared;
 
+            DistanceSquared = DistanceSqr(ref point, ref triangle, out TriangleClosest, out TriangleBaryCoords);
+            return DistanceSquared;
+        }
+
+
+        public static double DistanceSqr(ref Vector3d point, ref Triangle3d triangle, out Vector3d closestPoint, out Vector3d baryCoords )
+        {
             Vector3d diff = triangle.V0 - point;
             Vector3d edge0 = triangle.V1 - triangle.V0;
             Vector3d edge1 = triangle.V2 - triangle.V0;
             double a00 = edge0.LengthSquared;
-            double a01 = edge0.Dot(edge1);
+            double a01 = edge0.Dot(ref edge1);
             double a11 = edge1.LengthSquared;
-            double b0 = diff.Dot(edge0);
-            double b1 = diff.Dot(edge1);
+            double b0 = diff.Dot(ref edge0);
+            double b1 = diff.Dot(ref edge1);
             double c = diff.LengthSquared;
             double det = Math.Abs(a00 * a11 - a01 * a01);
             double s = a01 * b1 - a11 * b0;
@@ -213,16 +220,17 @@ public double GetSquared()
                     }
                 }
             }
+            closestPoint = triangle.V0 + s * edge0 + t * edge1;
+            baryCoords = new Vector3d(1 - s - t, s, t);
 
             // Account for numerical round-off error.
-            if (sqrDistance < 0) {
-                sqrDistance = 0;
-            }
-            DistanceSquared = sqrDistance;
-
-            TriangleClosest = triangle.V0 + s * edge0 + t * edge1;
-            TriangleBaryCoords = new Vector3d(1 - s - t, s, t);
-            return sqrDistance;
+            return Math.Max(sqrDistance, 0);
         }
+
+
+
+
+
+
     }
 }
diff --git a/distance/DistRay3Segment3.cs b/distance/DistRay3Segment3.cs
index b5a0697c..a6dc4b6a 100644
--- a/distance/DistRay3Segment3.cs
+++ b/distance/DistRay3Segment3.cs
@@ -5,10 +5,11 @@
 
 namespace g3
 {
-    // ported from WildMagic 5 
-    // https://www.geometrictools.com/Downloads/Downloads.html
-
-    class DistRay3Segment3
+    /// <summary>
+    /// Distance between ray and segment
+    /// ported from WildMagic5
+    /// </summary>
+    public class DistRay3Segment3
     {
         Ray3d ray;
         public Ray3d Ray
@@ -39,10 +40,14 @@ public DistRay3Segment3(Ray3d rayIn, Segment3d segmentIn)
 
 
         static public double MinDistance(Ray3d r, Segment3d s) {
-            return new DistRay3Segment3(r, s).Get();
+            double rayt, segt;
+            double dsqr = SquaredDistance(ref r, ref s, out rayt, out segt);
+            return Math.Sqrt(dsqr);
         }
         static public double MinDistanceSegmentParam(Ray3d r, Segment3d s) {
-            return new DistRay3Segment3(r, s).Compute().SegmentParameter;
+            double rayt, segt;
+            /*double dsqr = */SquaredDistance(ref r, ref s, out rayt, out segt);
+            return segt;
         }
 
 
@@ -57,7 +62,7 @@ public double Get() {
 
         public double GetSquared()
         {
-            if (DistanceSquared > 0)
+            if (DistanceSquared >= 0)
                 return DistanceSquared;
 
             Vector3d diff = ray.Origin - segment.Center;
@@ -184,5 +189,136 @@ public double GetSquared()
         }
 
 
+
+
+
+
+        /// <summary>
+        /// compute w/o allocating temporaries/etc
+        /// </summary>
+        public static double SquaredDistance(ref Ray3d ray, ref Segment3d segment, 
+            out double rayT, out double segT)
+        {
+            Vector3d diff = ray.Origin - segment.Center;
+            double a01 = -ray.Direction.Dot(segment.Direction);
+            double b0 = diff.Dot(ray.Direction);
+            double b1 = -diff.Dot(segment.Direction);
+            double c = diff.LengthSquared;
+            double det = Math.Abs(1 - a01 * a01);
+            double s0, s1, sqrDist, extDet;
+
+            if (det >= MathUtil.ZeroTolerance) {
+                // The Ray and Segment are not parallel.
+                s0 = a01 * b1 - b0;
+                s1 = a01 * b0 - b1;
+                extDet = segment.Extent * det;
+
+                if (s0 >= 0) {
+                    if (s1 >= -extDet) {
+                        if (s1 <= extDet)  // region 0
+                        {
+                            // Minimum at interior points of Ray and Segment.
+                            double invDet = (1) / det;
+                            s0 *= invDet;
+                            s1 *= invDet;
+                            sqrDist = s0 * (s0 + a01 * s1 + (2) * b0) +
+                                s1 * (a01 * s0 + s1 + (2) * b1) + c;
+                        } else  // region 1
+                          {
+                            s1 = segment.Extent;
+                            s0 = -(a01 * s1 + b0);
+                            if (s0 > 0) {
+                                sqrDist = -s0 * s0 + s1 * (s1 + (2) * b1) + c;
+                            } else {
+                                s0 = 0;
+                                sqrDist = s1 * (s1 + (2) * b1) + c;
+                            }
+                        }
+                    } else  // region 5
+                      {
+                        s1 = -segment.Extent;
+                        s0 = -(a01 * s1 + b0);
+                        if (s0 > 0) {
+                            sqrDist = -s0 * s0 + s1 * (s1 + (2) * b1) + c;
+                        } else {
+                            s0 = 0;
+                            sqrDist = s1 * (s1 + (2) * b1) + c;
+                        }
+                    }
+                } else {
+                    if (s1 <= -extDet)  // region 4
+                    {
+                        s0 = -(-a01 * segment.Extent + b0);
+                        if (s0 > 0) {
+                            s1 = -segment.Extent;
+                            sqrDist = -s0 * s0 + s1 * (s1 + (2) * b1) + c;
+                        } else {
+                            s0 = 0;
+                            s1 = -b1;
+                            if (s1 < -segment.Extent) {
+                                s1 = -segment.Extent;
+                            } else if (s1 > segment.Extent) {
+                                s1 = segment.Extent;
+                            }
+                            sqrDist = s1 * (s1 + (2) * b1) + c;
+                        }
+                    } else if (s1 <= extDet)  // region 3
+                      {
+                        s0 = 0;
+                        s1 = -b1;
+                        if (s1 < -segment.Extent) {
+                            s1 = -segment.Extent;
+                        } else if (s1 > segment.Extent) {
+                            s1 = segment.Extent;
+                        }
+                        sqrDist = s1 * (s1 + (2) * b1) + c;
+                    } else  // region 2
+                      {
+                        s0 = -(a01 * segment.Extent + b0);
+                        if (s0 > 0) {
+                            s1 = segment.Extent;
+                            sqrDist = -s0 * s0 + s1 * (s1 + (2) * b1) + c;
+                        } else {
+                            s0 = 0;
+                            s1 = -b1;
+                            if (s1 < -segment.Extent) {
+                                s1 = -segment.Extent;
+                            } else if (s1 > segment.Extent) {
+                                s1 = segment.Extent;
+                            }
+                            sqrDist = s1 * (s1 + (2) * b1) + c;
+                        }
+                    }
+                }
+            } else {
+                // Ray and Segment are parallel.
+                if (a01 > 0) {
+                    // Opposite direction vectors.
+                    s1 = -segment.Extent;
+                } else {
+                    // Same direction vectors.
+                    s1 = segment.Extent;
+                }
+
+                s0 = -(a01 * s1 + b0);
+                if (s0 > 0) {
+                    sqrDist = -s0 * s0 + s1 * (s1 + (2) * b1) + c;
+                } else {
+                    s0 = 0;
+                    sqrDist = s1 * (s1 + (2) * b1) + c;
+                }
+            }
+
+            rayT = s0;
+            segT = s1;
+
+            // Account for numerical round-off errors.
+            if (sqrDist < 0) 
+                sqrDist = 0;
+            return sqrDist;
+        }
+
+
+
     }
 }
diff --git a/geometry3Sharp.asmdef b/geometry3Sharp.asmdef
new file mode 100644
index 00000000..20e83fc1
--- /dev/null
+++ b/geometry3Sharp.asmdef
@@ -0,0 +1,8 @@
+{
+    "name": "geometry3Sharp",
+    "references": [],
+    "optionalUnityReferences": [],
+    "includePlatforms": [],
+    "excludePlatforms": [],
+    "allowUnsafeCode": true
+}
\ No newline at end of file
diff --git a/geometry3Sharp.csproj b/geometry3Sharp.csproj
index 76901f63..a43ff4a0 100644
--- a/geometry3Sharp.csproj
+++ b/geometry3Sharp.csproj
@@ -22,6 +22,7 @@
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <PlatformTarget>AnyCPU</PlatformTarget>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugType>pdbonly</DebugType>
@@ -31,6 +32,7 @@
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <PlatformTarget>AnyCPU</PlatformTarget>
   </PropertyGroup>
   <ItemGroup>
     <Reference Include="System" />
@@ -53,6 +55,7 @@
     <Compile Include="color\ColorMixer.cs" />
     <Compile Include="comp_geom\GraphCells2d.cs" />
     <Compile Include="comp_geom\GraphSplitter2d.cs" />
+    <Compile Include="comp_geom\SphericalFibonacciPointSet.cs" />
     <Compile Include="containment\ContMinBox2.cs" />
     <Compile Include="containment\ContMinCircle2.cs" />
     <Compile Include="comp_geom\ConvexHull2.cs" />
@@ -77,7 +80,9 @@
     <Compile Include="core\Units.cs" />
     <Compile Include="core\DVectorArray.cs" />
     <Compile Include="core\VectorArray.cs" />
+    <Compile Include="core\ProgressCancel.cs" />
     <Compile Include="curve\BaseCurve2.cs" />
+    <Compile Include="curve\BezierCurve2.cs" />
     <Compile Include="curve\BSplineBasis.cs" />
     <Compile Include="curve\Circle2.cs" />
     <Compile Include="curve\CurveResampler.cs" />
@@ -88,6 +93,7 @@
     <Compile Include="curve\DGraph3.cs" />
     <Compile Include="curve\DGraph3Util.cs" />
     <Compile Include="curve\Ellipse2.cs" />
+    <Compile Include="curve\LaplacianCurveDeformer.cs" />
     <Compile Include="curve\PlanarSolid2d.cs" />
     <Compile Include="curve\NURBSCurve2.cs" />
     <Compile Include="curve\PolygonFont2d.cs" />
@@ -100,9 +106,13 @@
     <Compile Include="distance\DistPoint2Circle2.cs" />
     <Compile Include="distance\DistPoint3Cylinder3.cs" />
     <Compile Include="distance\DistPoint3Circle3.cs" />
+    <Compile Include="implicit\CachingGridImplicit3d.cs" />
+    <Compile Include="implicit\CachingMeshSDF.cs" />
+    <Compile Include="implicit\FalloffFunctions.cs" />
     <Compile Include="implicit\GridImplicits3d.cs" />
     <Compile Include="implicit\Implicit2d.cs" />
     <Compile Include="implicit\Implicit3d.cs" />
+    <Compile Include="implicit\ImplicitFieldSampler3d.cs" />
     <Compile Include="implicit\ImplicitOperators.cs" />
     <Compile Include="implicit\MarchingQuads.cs" />
     <Compile Include="intersection\IntrLine3AxisAlignedBox3.cs" />
@@ -119,6 +129,7 @@
     <Compile Include="math\AxisAlignedBox2i.cs" />
     <Compile Include="math\AxisAlignedBox3i.cs" />
     <Compile Include="math\BoundsUtil.cs" />
+    <Compile Include="math\FastWindingMath.cs" />
     <Compile Include="math\Frame3f.cs" />
     <Compile Include="math\IndexTypes.cs" />
     <Compile Include="math\IndexUtil.cs" />
@@ -144,7 +155,11 @@
     <Compile Include="math\Vector2f.cs" />
     <Compile Include="math\Vector2i.cs" />
     <Compile Include="math\Vector4d.cs" />
+    <Compile Include="math\Vector4f.cs" />
     <Compile Include="math\VectorTuple.cs" />
+    <Compile Include="mesh\DMesh3Changes.cs" />
+    <Compile Include="mesh\DSubmesh3Set.cs" />
+    <Compile Include="mesh\MeshCaches.cs" />
     <Compile Include="mesh\MeshPointSets.cs" />
     <Compile Include="mesh\MeshRefinerBase.cs" />
     <Compile Include="mesh\NTMesh3.cs" />
@@ -163,6 +178,7 @@
     <Compile Include="mesh\RegionRemesher.cs" />
     <Compile Include="mesh\SimpleQuadMesh.cs" />
     <Compile Include="mesh_generators\ArrowGenerators.cs" />
+    <Compile Include="mesh_generators\PointsMeshGenerators.cs" />
     <Compile Include="mesh_generators\SphereGenerators.cs" />
     <Compile Include="mesh_generators\BoxGenerators.cs" />
     <Compile Include="mesh_generators\CylinderGenerators.cs" />
@@ -178,9 +194,11 @@
     <Compile Include="io\StandardMeshReader.cs" />
     <Compile Include="io\StandardMeshWriter.cs" />
     <Compile Include="mesh_generators\MarchingCubes.cs" />
+    <Compile Include="mesh_generators\TriangulatedPolygonGenerator.cs" />
     <Compile Include="mesh_generators\VoxelSurfaceGenerator.cs" />
     <Compile Include="mesh_ops\MeshExtrudeMesh.cs" />
     <Compile Include="mesh_ops\MeshExtrudeFaces.cs" />
+    <Compile Include="mesh_ops\MeshInsertPolygon.cs" />
     <Compile Include="mesh_ops\MeshInsertUVPolyCurve.cs" />
     <Compile Include="mesh_ops\MeshIsoCurves.cs" />
     <Compile Include="mesh_ops\MeshLocalParam.cs" />
@@ -285,8 +303,13 @@
     <Compile Include="shapes3\Circle3.cs" />
     <Compile Include="spatial\BiGrid3.cs" />
     <Compile Include="spatial\Bitmap3.cs" />
+    <Compile Include="spatial\DCurveBoxTree.cs" />
     <Compile Include="spatial\DCurveProjection.cs" />
+    <Compile Include="spatial\DenseGrid2.cs" />
     <Compile Include="spatial\DenseGrid3.cs" />
+    <Compile Include="spatial\EditMeshSpatial.cs" />
+    <Compile Include="spatial\MeshScalarSamplingGrid.cs" />
+    <Compile Include="spatial\MeshWindingNumberGrid.cs" />
     <Compile Include="spatial\PointAABBTree3.cs" />
     <Compile Include="spatial\DMeshAABBTree.cs" />
     <Compile Include="spatial\DSparseGrid3.cs" />
@@ -294,6 +317,8 @@
     <Compile Include="spatial\GridIndexing.cs" />
     <Compile Include="spatial\MeshSignedDistanceGrid.cs" />
     <Compile Include="spatial\NormalHistogram.cs" />
+    <Compile Include="spatial\PointSetHashtable.cs" />
+    <Compile Include="spatial\TriangleBinsGrid2d.cs" />
     <Compile Include="spatial\PointHashGrid2d.cs" />
     <Compile Include="spatial\PointHashGrid3d.cs" />
     <Compile Include="spatial\Polygon2dBoxTree.cs" />
diff --git a/implicit/CachingGridImplicit3d.cs b/implicit/CachingGridImplicit3d.cs
new file mode 100644
index 00000000..e04ffdb5
--- /dev/null
+++ b/implicit/CachingGridImplicit3d.cs
@@ -0,0 +1,208 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+
+
+namespace g3
+{
+
+    /// <summary>
+    /// [RMS] variant of DenseGridTrilinearImplicit that does lazy evaluation
+    /// of Grid values.
+    /// 
+    /// Tri-linear interpolant for a 3D dense grid. Supports grid translation
+    /// via GridOrigin, but does not support scaling or rotation. If you need those,
+    /// you can wrap this in something that does the xform.
+    /// </summary>
+	public class CachingDenseGridTrilinearImplicit : BoundedImplicitFunction3d
+    {
+        public DenseGrid3f Grid;
+        public double CellSize;
+        public Vector3d GridOrigin;
+
+        public ImplicitFunction3d AnalyticF;
+
+        // value to return if query point is outside grid (in an SDF
+        // outside is usually positive). Need to do math with this value,
+        // so don't use double.MaxValue or square will overflow
+        public double Outside = Math.Sqrt(Math.Sqrt(double.MaxValue));
+
+        public float Invalid = float.MaxValue;
+
+        public CachingDenseGridTrilinearImplicit(Vector3d gridOrigin, double cellSize, Vector3i gridDimensions)
+        {
+            Grid = new DenseGrid3f(gridDimensions.x, gridDimensions.y, gridDimensions.z, Invalid);
+            GridOrigin = gridOrigin;
+            CellSize = cellSize;
+        }
+
+        public AxisAlignedBox3d Bounds()
+		{
+			return new AxisAlignedBox3d(
+				GridOrigin.x, GridOrigin.y, GridOrigin.z,
+				GridOrigin.x + CellSize * Grid.ni, 
+				GridOrigin.y + CellSize * Grid.nj, 
+				GridOrigin.z + CellSize * Grid.nk);
+		}
+
+
+        public double Value(ref Vector3d pt)
+        {
+            Vector3d gridPt = new Vector3d(
+                ((pt.x - GridOrigin.x) / CellSize),
+                ((pt.y - GridOrigin.y) / CellSize),
+                ((pt.z - GridOrigin.z) / CellSize));
+
+            // compute integer coordinates
+            int x0 = (int)gridPt.x;
+            int y0 = (int)gridPt.y, y1 = y0 + 1;
+            int z0 = (int)gridPt.z, z1 = z0 + 1;
+
+            // clamp to grid
+            if (x0 < 0 || (x0+1) >= Grid.ni ||
+                y0 < 0 || y1 >= Grid.nj ||
+                z0 < 0 || z1 >= Grid.nk)
+                return Outside;
+
+            // convert double coords to [0,1] range
+            double fAx = gridPt.x - (double)x0;
+            double fAy = gridPt.y - (double)y0;
+            double fAz = gridPt.z - (double)z0;
+            double OneMinusfAx = 1.0 - fAx;
+
+            // compute trilinear interpolant. The code below tries to do this with the fewest 
+            // number of variables, in hopes that optimizer will be clever about re-using registers, etc.
+            // Commented code at bottom is fully-expanded version.
+            // [TODO] it is possible to implement lerps here as a+(b-a)*t, saving a multiply and a variable.
+            //   This is numerically worse, but since the grid values are floats and
+            //   we are computing in doubles, does it matter?
+            double xa, xb;
+
+            get_value_pair(x0, y0, z0, out xa, out xb);
+            double yz = (1 - fAy) * (1 - fAz);
+            double sum = (OneMinusfAx * xa + fAx * xb) * yz;
+
+            get_value_pair(x0, y0, z1, out xa, out xb);
+            yz = (1 - fAy) * (fAz);
+            sum += (OneMinusfAx * xa + fAx * xb) * yz;
+
+            get_value_pair(x0, y1, z0, out xa, out xb);
+            yz = (fAy) * (1 - fAz);
+            sum += (OneMinusfAx * xa + fAx * xb) * yz;
+
+            get_value_pair(x0, y1, z1, out xa, out xb);
+            yz = (fAy) * (fAz);
+            sum += (OneMinusfAx * xa + fAx * xb) * yz;
+
+            return sum;
+
+            // fV### is grid cell corner index
+            //return
+            //    fV000 * (1 - fAx) * (1 - fAy) * (1 - fAz) +
+            //    fV001 * (1 - fAx) * (1 - fAy) * (fAz) +
+            //    fV010 * (1 - fAx) * (fAy) * (1 - fAz) +
+            //    fV011 * (1 - fAx) * (fAy) * (fAz) +
+            //    fV100 * (fAx) * (1 - fAy) * (1 - fAz) +
+            //    fV101 * (fAx) * (1 - fAy) * (fAz) +
+            //    fV110 * (fAx) * (fAy) * (1 - fAz) +
+            //    fV111 * (fAx) * (fAy) * (fAz);
+        }
+
+
+
+        void get_value_pair(int i, int j, int k, out double a, out double b)
+        {
+            float fa, fb;
+            Grid.get_x_pair(i, j, k, out fa, out fb);
+
+            if (fa == Invalid) {
+                Vector3d p = grid_position(i, j, k);
+                a = AnalyticF.Value(ref p);
+                Grid[i, j, k] = (float)a;
+            } else
+                a = fa;
+
+            if (fb == Invalid) {
+                Vector3d p = grid_position(i+1, j, k);
+                b = AnalyticF.Value(ref p);
+                Grid[i+1, j, k] = (float)b;
+            } else
+                b = fb;
+        }
+
+
+        Vector3d grid_position(int i, int j, int k) {
+            return new Vector3d( GridOrigin.x + CellSize * i, GridOrigin.y + CellSize * j, GridOrigin.z + CellSize*k );
+        }
+
+
+        public Vector3d Gradient(ref Vector3d pt)
+        {
+            Vector3d gridPt = new Vector3d(
+                ((pt.x - GridOrigin.x) / CellSize),
+                ((pt.y - GridOrigin.y) / CellSize),
+                ((pt.z - GridOrigin.z) / CellSize));
+
+            // clamp to grid
+            if (gridPt.x < 0 || gridPt.x >= Grid.ni - 1 ||
+                gridPt.y < 0 || gridPt.y >= Grid.nj - 1 ||
+                gridPt.z < 0 || gridPt.z >= Grid.nk - 1)
+                return Vector3d.Zero;
+
+            // compute integer coordinates
+            int x0 = (int)gridPt.x;
+            int y0 = (int)gridPt.y, y1 = y0 + 1;
+            int z0 = (int)gridPt.z, z1 = z0 + 1;
+
+            // convert double coords to [0,1] range
+            double fAx = gridPt.x - (double)x0;
+            double fAy = gridPt.y - (double)y0;
+            double fAz = gridPt.z - (double)z0;
+
+            double fV000, fV100;
+            get_value_pair(x0, y0, z0, out fV000, out fV100);
+            double fV010, fV110;
+            get_value_pair(x0, y1, z0, out fV010, out fV110);
+            double fV001, fV101;
+            get_value_pair(x0, y0, z1, out fV001, out fV101);
+            double fV011, fV111;
+            get_value_pair(x0, y1, z1, out fV011, out fV111);
+
+            // [TODO] can re-order this to vastly reduce number of ops!
+            double gradX =
+                -fV000 * (1 - fAy) * (1 - fAz) +
+                -fV001 * (1 - fAy) * (fAz) +
+                -fV010 * (fAy) * (1 - fAz) +
+                -fV011 * (fAy) * (fAz) +
+                 fV100 * (1 - fAy) * (1 - fAz) +
+                 fV101 * (1 - fAy) * (fAz) +
+                 fV110 * (fAy) * (1 - fAz) +
+                 fV111 * (fAy) * (fAz);
+
+            double gradY =
+                -fV000 * (1 - fAx) * (1 - fAz) +
+                -fV001 * (1 - fAx) * (fAz) +
+                 fV010 * (1 - fAx) * (1 - fAz) +
+                 fV011 * (1 - fAx) * (fAz) +
+                -fV100 * (fAx) * (1 - fAz) +
+                -fV101 * (fAx) * (fAz) +
+                 fV110 * (fAx) * (1 - fAz) +
+                 fV111 * (fAx) * (fAz);
+
+            double gradZ =
+                -fV000 * (1 - fAx) * (1 - fAy) +
+                 fV001 * (1 - fAx) * (1 - fAy) +
+                -fV010 * (1 - fAx) * (fAy) +
+                 fV011 * (1 - fAx) * (fAy) +
+                -fV100 * (fAx) * (1 - fAy) +
+                 fV101 * (fAx) * (1 - fAy) +
+                -fV110 * (fAx) * (fAy) +
+                 fV111 * (fAx) * (fAy);
+
+            return new Vector3d(gradX, gradY, gradZ);
+        }
+
+    }
+
+}
diff --git a/implicit/CachingMeshSDF.cs b/implicit/CachingMeshSDF.cs
new file mode 100644
index 00000000..e2931bb9
--- /dev/null
+++ b/implicit/CachingMeshSDF.cs
@@ -0,0 +1,587 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+
+
+namespace g3
+{
+
+    /// <summary>
+    /// [RMS] this is variant of MeshSignedDistanceGrid that does lazy evaluation of actual distances,
+    /// using mesh spatial data structure. This is much faster if we are doing continuation-method
+    /// marching cubes as only values on surface will be computed!
+    /// 
+    /// 
+    /// 
+    /// Compute discretely-sampled (ie gridded) signed distance field for a mesh
+    /// The basic approach is, first compute exact distances in a narrow band, and then
+    /// extend out to rest of grid using fast "sweeping" (ie like a distance transform).
+    /// The resulting unsigned grid is then signed using ray-intersection counting, which
+    /// is also computed on the grid, so no BVH is necessary
+    /// 
+    /// If you set ComputeMode to NarrowBandOnly, result is a narrow-band signed distance field.
+    /// This is quite a bit faster as the sweeping is the most computationally-intensive step.
+    /// 
+    /// Caveats:
+    ///  - the "narrow band" is based on triangle bounding boxes, so it is not necessarily
+    ///    that "narrow" if you have large triangles on a diagonal to grid axes
+    /// 
+    /// 
+    /// Potential optimizations:
+    ///  - Often we have a spatial data structure that would allow faster computation of the
+    ///    narrow-band distances (which become quite expensive if we want a wider band!)
+    ///    Not clear how to take advantage of this though. Perhaps we could have a binary
+    ///    grid that, in first pass, we set bits inside triangle bboxes to 1? Or perhaps
+    ///    same as current code, but we use spatial-dist, and so for each ijk we only compute once?
+    ///    (then have to test for computed value at each cell of each triangle...)
+    ///    
+    /// 
+    /// This code is based on the C++ implementation found at https://github.com/christopherbatty/SDFGen
+    /// Original license was public domain. 
+    /// Permission granted by Christopher Batty to include C# port under Boost license.
+    /// </summary>
+    public class CachingMeshSDF
+    {
+        public DMesh3 Mesh;
+        public DMeshAABBTree3 Spatial;
+        public float CellSize;
+
+        // Bounds of grid will be expanded this much in positive and negative directions.
+        // Useful for if you want field to extend outwards.
+        public Vector3d ExpandBounds = Vector3d.Zero;
+
+        // max distance away from surface that we might need to evaluate
+        public float MaxOffsetDistance = 0;
+
+        // Most of this parallelizes very well, makes a huge speed difference
+        public bool UseParallel = true;
+
+        // should we try to compute signs? if not, grid remains unsigned
+        public bool ComputeSigns = true;
+
+        // What counts as "inside" the mesh. Crossing count does not use triangle
+        // orientation, so inverted faces are fine, but overlapping shells or self intersections
+        // will be filled using even/odd rules (as seen along X axis...)
+        // Parity count is basically mesh winding number, handles overlap shells and
+        // self-intersections, but inverted shells are 'subtracted', and inverted faces are a disaster.
+        // Both modes handle internal cavities, neither handles open sheets.
+        public enum InsideModes
+        {
+            CrossingCount = 0,
+            ParityCount = 1
+        }
+        public InsideModes InsideMode = InsideModes.ParityCount;
+
+        // Implementation computes the triangle closest to each grid cell, can
+        // return this grid if desired (only reason not to is avoid hanging onto memory)
+        public bool WantClosestTriGrid = false;
+
+        // grid of per-cell crossing or parity counts
+        public bool WantIntersectionsGrid = false;
+
+        /// <summary> if this function returns true, we should abort calculation </summary>
+        public Func<bool> CancelF = () => { return false; };
+
+
+        public bool DebugPrint = false;
+
+
+        // computed results
+        Vector3f grid_origin;
+        DenseGrid3f grid;
+        DenseGrid3i closest_tri_grid;
+        DenseGrid3i intersections_grid;
+
+        public CachingMeshSDF(DMesh3 mesh, double cellSize, DMeshAABBTree3 spatial)
+        {
+            Mesh = mesh;
+            CellSize = (float)cellSize;
+            Spatial = spatial;
+        }
+
+
+        float UpperBoundDistance;
+        double MaxDistQueryDist;
+
+
+        public void Initialize()
+        {
+            // figure out origin & dimensions
+            AxisAlignedBox3d bounds = Mesh.CachedBounds;
+
+            float fBufferWidth = (float)Math.Max(4*CellSize, 2*MaxOffsetDistance + 2*CellSize);
+            grid_origin = (Vector3f)bounds.Min - fBufferWidth * Vector3f.One - (Vector3f)ExpandBounds;
+            Vector3f max = (Vector3f)bounds.Max + fBufferWidth * Vector3f.One + (Vector3f)ExpandBounds;
+            int ni = (int)((max.x - grid_origin.x) / CellSize) + 1;
+            int nj = (int)((max.y - grid_origin.y) / CellSize) + 1;
+            int nk = (int)((max.z - grid_origin.z) / CellSize) + 1;
+
+            UpperBoundDistance = (float)((ni+nj+nk) * CellSize);
+            grid = new DenseGrid3f(ni, nj, nk, UpperBoundDistance);
+
+            MaxDistQueryDist = MaxOffsetDistance + (2*CellSize*MathUtil.SqrtTwo);
+
+            // closest triangle id for each grid cell
+            if ( WantClosestTriGrid )
+                closest_tri_grid = new DenseGrid3i(ni, nj, nk, -1);
+
+            // intersection_count(i,j,k) is # of tri intersections in (i-1,i]x{j}x{k}
+            DenseGrid3i intersection_count = new DenseGrid3i(ni, nj, nk, 0);
+
+
+            if (ComputeSigns == true) {
+                compute_intersections(grid_origin, CellSize, ni, nj, nk, intersection_count);
+                if (CancelF())
+                    return;
+
+                // then figure out signs (inside/outside) from intersection counts
+                compute_signs(ni, nj, nk, grid, intersection_count);
+                if (CancelF())
+                    return;
+
+                if (WantIntersectionsGrid)
+                    intersections_grid = intersection_count;
+            }
+        }
+
+
+        public float GetValue(Vector3i idx)
+        {
+            float f = grid[idx];
+            if ( f == UpperBoundDistance || f == -UpperBoundDistance ) {
+                Vector3d p = cell_center(idx);
+
+                float sign = Math.Sign(f);
+
+                double dsqr;
+                int near_tid = Spatial.FindNearestTriangle(p, out dsqr, MaxDistQueryDist);
+                //int near_tid = Spatial.FindNearestTriangle(p, out dsqr);
+                if ( near_tid == DMesh3.InvalidID ) {
+                    f += 0.0001f;
+                } else {
+                    f = sign * (float)Math.Sqrt(dsqr);
+                }
+
+                grid[idx] = f;
+                if (closest_tri_grid != null)
+                    closest_tri_grid[idx] = near_tid;
+            }
+            return f;
+        }
+
+
+
+
+
+
+
+
+        public Vector3i Dimensions {
+            get { return new Vector3i(grid.ni, grid.nj, grid.nk); }
+        }
+
+        /// <summary>
+        /// SDF grid available after calling Compute()
+        /// </summary>
+        public DenseGrid3f Grid {
+            get { return grid; }
+        }
+
+        /// <summary>
+        /// Origin of the SDF grid, in same coordinates as mesh
+        /// </summary>
+        public Vector3f GridOrigin {
+            get { return grid_origin; }
+        }
+
+
+        public DenseGrid3i ClosestTriGrid {
+            get {
+                if ( WantClosestTriGrid == false)
+                    throw new Exception("Set WantClosestTriGrid=true to return this value");
+                return closest_tri_grid;
+            }
+        }
+        public DenseGrid3i IntersectionsGrid {
+            get {
+                if (WantIntersectionsGrid == false)
+                    throw new Exception("Set WantIntersectionsGrid=true to return this value");
+                return intersections_grid;
+            }
+        }
+
+
+        public float this[int i, int j, int k] {
+            get { return grid[i, j, k]; }
+        }
+        public float this[Vector3i idx] {
+            get { return grid[idx.x, idx.y, idx.z]; }
+        }
+
+        public Vector3f CellCenter(int i, int j, int k) {
+            return cell_center(new Vector3i(i, j, k));
+        }
+        Vector3f cell_center(Vector3i ijk)
+        {
+            return new Vector3f((float)ijk.x * CellSize + grid_origin[0],
+                                (float)ijk.y * CellSize + grid_origin[1],
+                                (float)ijk.z * CellSize + grid_origin[2]);
+        }
+
+
+        
+
+
+
+
+        // fill the intersection grid w/ number of intersections in each cell
+        void compute_intersections(Vector3f origin, float dx, int ni, int nj, int nk, DenseGrid3i intersection_count)
+        {
+            double ox = (double)origin[0], oy = (double)origin[1], oz = (double)origin[2];
+            double invdx = 1.0 / dx;
+
+            bool cancelled = false;
+
+            // this is what we will do for each triangle. There are no grid-reads, only grid-writes, 
+            // since we use atomic_increment, it is always thread-safe
+            Action<int> ProcessTriangleF = (tid) => {
+                if (tid % 100 == 0 && CancelF() == true)
+                    cancelled = true;
+                if (cancelled) return;
+
+                Vector3d xp = Vector3d.Zero, xq = Vector3d.Zero, xr = Vector3d.Zero;
+                Mesh.GetTriVertices(tid, ref xp, ref xq, ref xr);
+
+
+                bool neg_x = false;
+                if (InsideMode == InsideModes.ParityCount) {
+                    Vector3d n = MathUtil.FastNormalDirection(ref xp, ref xq, ref xr);
+                    neg_x = n.x > 0;
+                }
+
+                // real ijk coordinates of xp/xq/xr
+                double fip = (xp[0] - ox) * invdx, fjp = (xp[1] - oy) * invdx, fkp = (xp[2] - oz) * invdx;
+                double fiq = (xq[0] - ox) * invdx, fjq = (xq[1] - oy) * invdx, fkq = (xq[2] - oz) * invdx;
+                double fir = (xr[0] - ox) * invdx, fjr = (xr[1] - oy) * invdx, fkr = (xr[2] - oz) * invdx;
+
+                // recompute j/k integer bounds of triangle w/o exact band
+                int j0 = MathUtil.Clamp((int)Math.Ceiling(MathUtil.Min(fjp, fjq, fjr)), 0, nj - 1);
+                int j1 = MathUtil.Clamp((int)Math.Floor(MathUtil.Max(fjp, fjq, fjr)), 0, nj - 1);
+                int k0 = MathUtil.Clamp((int)Math.Ceiling(MathUtil.Min(fkp, fkq, fkr)), 0, nk - 1);
+                int k1 = MathUtil.Clamp((int)Math.Floor(MathUtil.Max(fkp, fkq, fkr)), 0, nk - 1);
+
+                // and do intersection counts
+                for (int k = k0; k <= k1; ++k) {
+                    for (int j = j0; j <= j1; ++j) {
+                        double a, b, c;
+                        if (point_in_triangle_2d(j, k, fjp, fkp, fjq, fkq, fjr, fkr, out a, out b, out c)) {
+                            double fi = a * fip + b * fiq + c * fir; // intersection i coordinate
+                            int i_interval = (int)(Math.Ceiling(fi)); // intersection is in (i_interval-1,i_interval]
+                            if (i_interval < 0) {
+                                intersection_count.atomic_incdec(0, j, k, neg_x);
+                            } else if (i_interval < ni) {
+                                intersection_count.atomic_incdec(i_interval, j, k, neg_x);
+                            } else {
+                                // we ignore intersections that are beyond the +x side of the grid
+                            }
+                        }
+                    }
+                }
+            };
+
+            if (UseParallel) {
+                gParallel.ForEach(Mesh.TriangleIndices(), ProcessTriangleF);
+            } else {
+                foreach (int tid in Mesh.TriangleIndices()) {
+                    ProcessTriangleF(tid);
+                }
+            }
+
+        }
+
+
+
+
+
+        // iterate through each x-row of grid and set unsigned distances to be negative
+        // inside the mesh, based on the intersection_counts
+        void compute_signs(int ni, int nj, int nk, DenseGrid3f distances, DenseGrid3i intersection_counts)
+        {
+            Func<int, bool> isInsideF = (count) => { return count % 2 == 1; };
+            if (InsideMode == InsideModes.ParityCount)
+                isInsideF = (count) => { return count > 0; };
+
+            if (UseParallel) {
+                // can process each x-row in parallel
+                AxisAlignedBox2i box = new AxisAlignedBox2i(0, 0, nj, nk);
+                gParallel.ForEach(box.IndicesExclusive(), (vi) => {
+                    if (CancelF())
+                        return;
+
+                    int j = vi.x, k = vi.y;
+                    int total_count = 0;
+                    for (int i = 0; i < ni; ++i) {
+                        total_count += intersection_counts[i, j, k];
+                        if (isInsideF(total_count)) { // if parity of intersections so far is odd,
+                            distances[i, j, k] = -distances[i, j, k]; // we are inside the mesh
+                        }
+                    }
+                });
+
+            } else {
+
+                for (int k = 0; k < nk; ++k) {
+                    if (CancelF())
+                        return;
+
+                    for (int j = 0; j < nj; ++j) {
+                        int total_count = 0;
+                        for (int i = 0; i < ni; ++i) {
+                            total_count += intersection_counts[i, j, k];
+                            if (isInsideF(total_count)) { // if parity of intersections so far is odd,
+                                distances[i, j, k] = -distances[i, j, k]; // we are inside the mesh
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        
+
+
+
+        // calculate twice signed area of triangle (0,0)-(x1,y1)-(x2,y2)
+        // return an SOS-determined sign (-1, +1, or 0 only if it's a truly degenerate triangle)
+        static public int orientation(double x1, double y1, double x2, double y2, out double twice_signed_area)
+        {
+            twice_signed_area = y1 * x2 - x1 * y2;
+            if (twice_signed_area > 0) return 1;
+            else if (twice_signed_area < 0) return -1;
+            else if (y2 > y1) return 1;
+            else if (y2 < y1) return -1;
+            else if (x1 > x2) return 1;
+            else if (x1 < x2) return -1;
+            else return 0; // only true when x1==x2 and y1==y2
+        }
+
+
+        // robust test of (x0,y0) in the triangle (x1,y1)-(x2,y2)-(x3,y3)
+        // if true is returned, the barycentric coordinates are set in a,b,c.
+        static public bool point_in_triangle_2d(double x0, double y0,
+                                         double x1, double y1, double x2, double y2, double x3, double y3,
+                                         out double a, out double b, out double c)
+        {
+            a = b = c = 0;
+            x1 -= x0; x2 -= x0; x3 -= x0;
+            y1 -= y0; y2 -= y0; y3 -= y0;
+            int signa = orientation(x2, y2, x3, y3, out a);
+            if (signa == 0) return false;
+            int signb = orientation(x3, y3, x1, y1, out b);
+            if (signb != signa) return false;
+            int signc = orientation(x1, y1, x2, y2, out c);
+            if (signc != signa) return false;
+            double sum = a + b + c;
+            // if the SOS signs match and are nonzero, there's no way all of a, b, and c are zero.
+            if (sum == 0)
+                throw new Exception("MakeNarrowBandLevelSet.point_in_triangle_2d: badness!");
+            a /= sum;
+            b /= sum;
+            c /= sum;
+            return true;
+        }
+
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+    /// <summary>
+    /// Tri-linear interpolant for a 3D dense grid. Supports grid translation
+    /// via GridOrigin, but does not support scaling or rotation. If you need those,
+    /// you can wrap this in something that does the xform.
+    /// </summary>
+	public class CachingMeshSDFImplicit : BoundedImplicitFunction3d
+    {
+        public CachingMeshSDF SDF;
+        public double CellSize;
+        public Vector3d GridOrigin;
+
+        // value to return if query point is outside grid (in an SDF
+        // outside is usually positive). Need to do math with this value,
+        // so don't use double.MaxValue or square will overflow
+        public double Outside = Math.Sqrt(Math.Sqrt(double.MaxValue));
+
+        public CachingMeshSDFImplicit(CachingMeshSDF sdf)
+        {
+            SDF = sdf;
+            GridOrigin = sdf.GridOrigin;
+            CellSize = sdf.CellSize;
+        }
+
+        public AxisAlignedBox3d Bounds()
+        {
+            return new AxisAlignedBox3d(
+                GridOrigin.x, GridOrigin.y, GridOrigin.z,
+                GridOrigin.x + CellSize * SDF.Grid.ni,
+                GridOrigin.y + CellSize * SDF.Grid.nj,
+                GridOrigin.z + CellSize * SDF.Grid.nk);
+        }
+
+
+        public double Value(ref Vector3d pt)
+        {
+            Vector3d gridPt = new Vector3d(
+                ((pt.x - GridOrigin.x) / CellSize),
+                ((pt.y - GridOrigin.y) / CellSize),
+                ((pt.z - GridOrigin.z) / CellSize));
+
+            // compute integer coordinates
+            int x0 = (int)gridPt.x;
+            int y0 = (int)gridPt.y, y1 = y0 + 1;
+            int z0 = (int)gridPt.z, z1 = z0 + 1;
+
+            // clamp to grid
+            if (x0 < 0 || (x0 + 1) >= SDF.Grid.ni ||
+                y0 < 0 || y1 >= SDF.Grid.nj ||
+                z0 < 0 || z1 >= SDF.Grid.nk)
+                return Outside;
+
+            // convert double coords to [0,1] range
+            double fAx = gridPt.x - (double)x0;
+            double fAy = gridPt.y - (double)y0;
+            double fAz = gridPt.z - (double)z0;
+            double OneMinusfAx = 1.0 - fAx;
+
+            // compute trilinear interpolant. The code below tries to do this with the fewest 
+            // number of variables, in hopes that optimizer will be clever about re-using registers, etc.
+            // Commented code at bottom is fully-expanded version.
+            // [TODO] it is possible to implement lerps here as a+(b-a)*t, saving a multiply and a variable.
+            //   This is numerically worse, but since the grid values are floats and
+            //   we are computing in doubles, does it matter?
+            double xa, xb;
+
+            get_value_pair(x0, y0, z0, out xa, out xb);
+            double yz = (1 - fAy) * (1 - fAz);
+            double sum = (OneMinusfAx * xa + fAx * xb) * yz;
+
+            get_value_pair(x0, y0, z1, out xa, out xb);
+            yz = (1 - fAy) * (fAz);
+            sum += (OneMinusfAx * xa + fAx * xb) * yz;
+
+            get_value_pair(x0, y1, z0, out xa, out xb);
+            yz = (fAy) * (1 - fAz);
+            sum += (OneMinusfAx * xa + fAx * xb) * yz;
+
+            get_value_pair(x0, y1, z1, out xa, out xb);
+            yz = (fAy) * (fAz);
+            sum += (OneMinusfAx * xa + fAx * xb) * yz;
+
+            return sum;
+
+            // fV### is grid cell corner index
+            //return
+            //    fV000 * (1 - fAx) * (1 - fAy) * (1 - fAz) +
+            //    fV001 * (1 - fAx) * (1 - fAy) * (fAz) +
+            //    fV010 * (1 - fAx) * (fAy) * (1 - fAz) +
+            //    fV011 * (1 - fAx) * (fAy) * (fAz) +
+            //    fV100 * (fAx) * (1 - fAy) * (1 - fAz) +
+            //    fV101 * (fAx) * (1 - fAy) * (fAz) +
+            //    fV110 * (fAx) * (fAy) * (1 - fAz) +
+            //    fV111 * (fAx) * (fAy) * (fAz);
+        }
+
+
+
+        void get_value_pair(int i, int j, int k, out double a, out double b)
+        {
+            a = SDF.GetValue(new Vector3i(i,j,k));
+            b = SDF.GetValue(new Vector3i(i+1,j,k));
+        }
+
+
+
+        public Vector3d Gradient(ref Vector3d pt)
+        {
+            Vector3d gridPt = new Vector3d(
+                ((pt.x - GridOrigin.x) / CellSize),
+                ((pt.y - GridOrigin.y) / CellSize),
+                ((pt.z - GridOrigin.z) / CellSize));
+
+            // clamp to grid
+            if (gridPt.x < 0 || gridPt.x >= SDF.Grid.ni - 1 ||
+                gridPt.y < 0 || gridPt.y >= SDF.Grid.nj - 1 ||
+                gridPt.z < 0 || gridPt.z >= SDF.Grid.nk - 1)
+                return Vector3d.Zero;
+
+            // compute integer coordinates
+            int x0 = (int)gridPt.x;
+            int y0 = (int)gridPt.y, y1 = y0 + 1;
+            int z0 = (int)gridPt.z, z1 = z0 + 1;
+
+            // convert double coords to [0,1] range
+            double fAx = gridPt.x - (double)x0;
+            double fAy = gridPt.y - (double)y0;
+            double fAz = gridPt.z - (double)z0;
+
+            double fV000, fV100;
+            get_value_pair(x0, y0, z0, out fV000, out fV100);
+            double fV010, fV110;
+            get_value_pair(x0, y1, z0, out fV010, out fV110);
+            double fV001, fV101;
+            get_value_pair(x0, y0, z1, out fV001, out fV101);
+            double fV011, fV111;
+            get_value_pair(x0, y1, z1, out fV011, out fV111);
+
+            // [TODO] can re-order this to vastly reduce number of ops!
+            double gradX =
+                -fV000 * (1 - fAy) * (1 - fAz) +
+                -fV001 * (1 - fAy) * (fAz) +
+                -fV010 * (fAy) * (1 - fAz) +
+                -fV011 * (fAy) * (fAz) +
+                 fV100 * (1 - fAy) * (1 - fAz) +
+                 fV101 * (1 - fAy) * (fAz) +
+                 fV110 * (fAy) * (1 - fAz) +
+                 fV111 * (fAy) * (fAz);
+
+            double gradY =
+                -fV000 * (1 - fAx) * (1 - fAz) +
+                -fV001 * (1 - fAx) * (fAz) +
+                 fV010 * (1 - fAx) * (1 - fAz) +
+                 fV011 * (1 - fAx) * (fAz) +
+                -fV100 * (fAx) * (1 - fAz) +
+                -fV101 * (fAx) * (fAz) +
+                 fV110 * (fAx) * (1 - fAz) +
+                 fV111 * (fAx) * (fAz);
+
+            double gradZ =
+                -fV000 * (1 - fAx) * (1 - fAy) +
+                 fV001 * (1 - fAx) * (1 - fAy) +
+                -fV010 * (1 - fAx) * (fAy) +
+                 fV011 * (1 - fAx) * (fAy) +
+                -fV100 * (fAx) * (1 - fAy) +
+                 fV101 * (fAx) * (1 - fAy) +
+                -fV110 * (fAx) * (fAy) +
+                 fV111 * (fAx) * (fAy);
+
+            return new Vector3d(gradX, gradY, gradZ);
+        }
+
+    }
+
+
+
+}
diff --git a/implicit/FalloffFunctions.cs b/implicit/FalloffFunctions.cs
new file mode 100644
index 00000000..27b7e0ac
--- /dev/null
+++ b/implicit/FalloffFunctions.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using g3;
+
+namespace gs
+{
+    public interface IFalloffFunction
+    {
+        /// <summary>
+        /// t is value in range [0,1], returns value in range [0,1]
+        /// </summary>
+        double FalloffT(double t);
+
+        /// <summary>
+        /// In most cases, users of IFalloffFunction will make a local copy
+        /// </summary>
+        IFalloffFunction Duplicate();
+    }
+
+
+
+    /// <summary>
+    /// returns 1 in range [0,ConstantRange], and then falls off to 0 in range [ConstantRange,1]
+    /// </summary>
+    public class LinearFalloff : IFalloffFunction
+    {
+        public double ConstantRange = 0;
+
+        public double FalloffT(double t)
+        {
+            t = MathUtil.Clamp(t, 0.0, 1.0);
+            if (ConstantRange <= 0)
+                return 1.0 - t;
+            else
+                return (t < ConstantRange) ? 1.0 : 1.0 - ((t - ConstantRange) / (1 - ConstantRange));
+        }
+
+
+        public IFalloffFunction Duplicate()
+        {
+            return new WyvillFalloff() {
+                ConstantRange = this.ConstantRange
+            };
+        }
+    }
+
+
+
+    /// <summary>
+    /// returns 1 in range [0,ConstantRange], and then falls off to 0 in range [ConstantRange,1]
+    /// </summary>
+    public class WyvillFalloff : IFalloffFunction
+    {
+        public double ConstantRange = 0;
+
+        public double FalloffT(double t)
+        {
+            t = MathUtil.Clamp(t, 0.0, 1.0);
+            if (ConstantRange <= 0)
+                return MathUtil.WyvillFalloff01(t);
+            else
+                return MathUtil.WyvillFalloff(t, ConstantRange, 1.0);
+        }
+
+
+        public IFalloffFunction Duplicate()
+        {
+            return new WyvillFalloff() {
+                ConstantRange = this.ConstantRange
+            };
+        }
+
+    }
+
+
+
+}
diff --git a/implicit/GridImplicits3d.cs b/implicit/GridImplicits3d.cs
index 70d72c75..6e2d1fe8 100644
--- a/implicit/GridImplicits3d.cs
+++ b/implicit/GridImplicits3d.cs
@@ -10,15 +10,16 @@ namespace g3
     /// via GridOrigin, but does not support scaling or rotation. If you need those,
     /// you can wrap this in something that does the xform.
     /// </summary>
-    public class DenseGridTrilinearImplicit : ImplicitFunction3d
+	public class DenseGridTrilinearImplicit : BoundedImplicitFunction3d
     {
         public DenseGrid3f Grid;
         public double CellSize;
         public Vector3d GridOrigin;
 
         // value to return if query point is outside grid (in an SDF
-        // outside is usually positive...)
-        public double Outside = double.MaxValue;
+        // outside is usually positive). Need to do math with this value,
+        // so don't use double.MaxValue or square will overflow
+        public double Outside = Math.Sqrt(Math.Sqrt(double.MaxValue));
 
         public DenseGridTrilinearImplicit(DenseGrid3f grid, Vector3d gridOrigin, double cellSize)
         {
@@ -26,6 +27,22 @@ public DenseGridTrilinearImplicit(DenseGrid3f grid, Vector3d gridOrigin, double
             GridOrigin = gridOrigin;
             CellSize = cellSize;
         }
+        public DenseGridTrilinearImplicit(MeshSignedDistanceGrid sdf_grid)
+        {
+            Grid = sdf_grid.Grid;
+            GridOrigin = sdf_grid.GridOrigin;
+            CellSize = sdf_grid.CellSize;
+        }
+
+
+        public AxisAlignedBox3d Bounds()
+		{
+			return new AxisAlignedBox3d(
+				GridOrigin.x, GridOrigin.y, GridOrigin.z,
+				GridOrigin.x + CellSize * Grid.ni, 
+				GridOrigin.y + CellSize * Grid.nj, 
+				GridOrigin.z + CellSize * Grid.nk);
+		}
 
 
         public double Value(ref Vector3d pt)
diff --git a/implicit/Implicit3d.cs b/implicit/Implicit3d.cs
index dcbb28b1..ea6d70cc 100644
--- a/implicit/Implicit3d.cs
+++ b/implicit/Implicit3d.cs
@@ -1,24 +1,679 @@
 using System;
 using System.Collections.Generic;
 
-
 namespace g3
 {
-
+	/// <summary>
+	/// Minimalist implicit function interface
+	/// </summary>
     public interface ImplicitFunction3d
     {
         double Value(ref Vector3d pt);
     }
 
 
+	/// <summary>
+	/// Bounded implicit function has a bounding box within which
+	/// the "interesting" part of the function is contained 
+	/// (eg the surface)
+	/// </summary>
+	public interface BoundedImplicitFunction3d : ImplicitFunction3d
+	{
+		AxisAlignedBox3d Bounds();
+	}
+
+
+	/// <summary>
+	/// Implicit sphere, where zero isocontour is at Radius
+	/// </summary>
+	public class ImplicitSphere3d : BoundedImplicitFunction3d
+    {
+		public Vector3d Origin;
+		public double Radius;
+
+        public double Value(ref Vector3d pt)
+        {
+			return pt.Distance(ref Origin) - Radius;
+        }
+
+		public AxisAlignedBox3d Bounds()
+		{
+			return new AxisAlignedBox3d(Origin, Radius);
+		}
+    }
+
+
+	/// <summary>
+	/// Implicit half-space. "Inside" is opposite of Normal direction.
+	/// </summary>
+	public class ImplicitHalfSpace3d : BoundedImplicitFunction3d
+	{
+		public Vector3d Origin;
+		public Vector3d Normal;
+
+		public double Value(ref Vector3d pt)
+		{
+			return (pt - Origin).Dot(Normal);
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			return new AxisAlignedBox3d(Origin, MathUtil.Epsilon);
+		}
+	}
+
+
+
+	/// <summary>
+	/// Implicit axis-aligned box
+	/// </summary>
+	public class ImplicitAxisAlignedBox3d : BoundedImplicitFunction3d
+	{
+		public AxisAlignedBox3d AABox;
+
+		public double Value(ref Vector3d pt)
+		{
+			return AABox.SignedDistance(pt);
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			return AABox;
+		}
+	}
+
+
+
+	/// <summary>
+	/// Implicit oriented box
+	/// </summary>
+	public class ImplicitBox3d : BoundedImplicitFunction3d
+	{
+		Box3d box;
+		AxisAlignedBox3d local_aabb;
+		AxisAlignedBox3d bounds_aabb;
+		public Box3d Box {
+			get { return box; }
+			set {
+				box = value;
+				local_aabb = new AxisAlignedBox3d(
+					-Box.Extent.x, -Box.Extent.y, -Box.Extent.z,
+					Box.Extent.x, Box.Extent.y, Box.Extent.z);
+				bounds_aabb = box.ToAABB();
+			}
+		}
+
+
+		public double Value(ref Vector3d pt)
+		{
+			double dx = (pt - Box.Center).Dot(Box.AxisX);
+			double dy = (pt - Box.Center).Dot(Box.AxisY);
+			double dz = (pt - Box.Center).Dot(Box.AxisZ);
+			return local_aabb.SignedDistance(new Vector3d(dx, dy, dz));
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			return bounds_aabb;
+		}
+	}
+
+
+
+	/// <summary>
+	/// Implicit tube around line segment
+	/// </summary>
+	public class ImplicitLine3d : BoundedImplicitFunction3d
+	{
+		public Segment3d Segment;
+		public double Radius;
+
+		public double Value(ref Vector3d pt)
+		{
+			double d = Math.Sqrt(Segment.DistanceSquared(pt));
+			return d - Radius;
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			Vector3d o = Radius * Vector3d.One, p0 = Segment.P0, p1 = Segment.P1;
+			AxisAlignedBox3d box = new AxisAlignedBox3d(p0 - o, p0 + o);
+			box.Contain(p1 - o);
+			box.Contain(p1 + o);
+			return box;
+		}
+	}
+
+
+
+
+	/// <summary>
+	/// Offset the zero-isocontour of an implicit function.
+	/// Assumes that negative is inside, if not, reverse offset.
+	/// </summary>
+	public class ImplicitOffset3d : BoundedImplicitFunction3d
+	{
+		public BoundedImplicitFunction3d A;
+		public double Offset;
+
+		public double Value(ref Vector3d pt)
+		{
+			return A.Value(ref pt) - Offset;
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			AxisAlignedBox3d box = A.Bounds();
+			box.Expand(Offset);
+			return box;
+		}
+	}
+
+
+
+    /// <summary>
+    /// remaps values so that values within given interval are negative,
+    /// and values outside this interval are positive. So, for a distance
+    /// field, this converts single isocontour into two nested isocontours
+    /// with zeros at interval a and b, with 'inside' in interval
+    /// </summary>
+    public class ImplicitShell3d : BoundedImplicitFunction3d
+    {
+        public BoundedImplicitFunction3d A;
+        public Interval1d Inside;
+
+        public double Value(ref Vector3d pt)
+        {
+            double f = A.Value(ref pt);
+            if (f < Inside.a)
+                f = Inside.a - f;
+            else if (f > Inside.b)
+                f = f - Inside.b;
+            else f = -Math.Min(Math.Abs(f - Inside.a), Math.Abs(f - Inside.b));
+            return f;
+        }
+
+        public AxisAlignedBox3d Bounds()
+        {
+            AxisAlignedBox3d box = A.Bounds();
+            box.Expand(Math.Max(0, Inside.b));
+            return box;
+        }
+    }
+
+
+
+
+    /// <summary>
+    /// Boolean Union of two implicit functions, A OR B.
+    /// Assumption is that both have surface at zero isocontour and 
+    /// negative is inside.
+    /// </summary>
+    public class ImplicitUnion3d : BoundedImplicitFunction3d
+	{
+		public BoundedImplicitFunction3d A;
+		public BoundedImplicitFunction3d B;
+
+		public double Value(ref Vector3d pt)
+		{
+			return Math.Min(A.Value(ref pt), B.Value(ref pt));
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			var box = A.Bounds();
+			box.Contain(B.Bounds());
+			return box;
+		}
+	}
+
+
+
+	/// <summary>
+	/// Boolean Intersection of two implicit functions, A AND B
+	/// Assumption is that both have surface at zero isocontour and 
+	/// negative is inside.
+	/// </summary>
+	public class ImplicitIntersection3d : BoundedImplicitFunction3d
+	{
+		public BoundedImplicitFunction3d A;
+		public BoundedImplicitFunction3d B;
+
+		public double Value(ref Vector3d pt)
+		{
+			return Math.Max(A.Value(ref pt), B.Value(ref pt));
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+            // [TODO] intersect boxes
+			var box = A.Bounds();
+			box.Contain(B.Bounds());
+			return box;
+		}
+	}
+
+
+
+	/// <summary>
+	/// Boolean Difference/Subtraction of two implicit functions A-B = A AND (NOT B)
+	/// Assumption is that both have surface at zero isocontour and 
+	/// negative is inside.
+	/// </summary>
+	public class ImplicitDifference3d : BoundedImplicitFunction3d
+	{
+		public BoundedImplicitFunction3d A;
+		public BoundedImplicitFunction3d B;
+
+		public double Value(ref Vector3d pt)
+		{
+			return Math.Max(A.Value(ref pt), -B.Value(ref pt));
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			// [TODO] can actually subtract B.Bounds() here...
+			return A.Bounds();
+		}
+	}
+
+
+
+
+	/// <summary>
+	/// Boolean Union of N implicit functions, A OR B.
+	/// Assumption is that both have surface at zero isocontour and 
+	/// negative is inside.
+	/// </summary>
+	public class ImplicitNaryUnion3d : BoundedImplicitFunction3d
+	{
+		public List<BoundedImplicitFunction3d> Children;
+
+		public double Value(ref Vector3d pt)
+		{
+			double f = Children[0].Value(ref pt);
+			int N = Children.Count;
+			for (int k = 1; k < N; ++k)
+				f = Math.Min(f, Children[k].Value(ref pt));
+			return f;
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			var box = Children[0].Bounds();
+			int N = Children.Count;
+			for (int k = 1; k < N; ++k)
+				box.Contain(Children[k].Bounds());
+			return box;
+		}
+	}
+
+
+
+
+    /// <summary>
+    /// Boolean Intersection of N implicit functions, A AND B.
+    /// Assumption is that both have surface at zero isocontour and 
+    /// negative is inside.
+    /// </summary>
+    public class ImplicitNaryIntersection3d : BoundedImplicitFunction3d
+    {
+        public List<BoundedImplicitFunction3d> Children;
+
+        public double Value(ref Vector3d pt)
+        {
+            double f = Children[0].Value(ref pt);
+            int N = Children.Count;
+            for (int k = 1; k < N; ++k)
+                f = Math.Max(f, Children[k].Value(ref pt));
+            return f;
+        }
+
+        public AxisAlignedBox3d Bounds()
+        {
+            var box = Children[0].Bounds();
+            int N = Children.Count;
+            for (int k = 1; k < N; ++k) {
+                box = box.Intersect(Children[k].Bounds());
+            }
+            return box;
+        }
+    }
+
+
+
+
+
+    /// <summary>
+    /// Boolean Difference of N implicit functions, A - Union(B1..BN)
+    /// Assumption is that both have surface at zero isocontour and 
+    /// negative is inside.
+    /// </summary>
+    public class ImplicitNaryDifference3d : BoundedImplicitFunction3d
+	{
+		public BoundedImplicitFunction3d A;
+		public List<BoundedImplicitFunction3d> BSet;
+
+		public double Value(ref Vector3d pt)
+		{
+			double fA = A.Value(ref pt);
+			int N = BSet.Count;
+			if (N == 0)
+				return fA;
+			double fB = BSet[0].Value(ref pt);
+			for (int k = 1; k < N; ++k)
+				fB = Math.Min(fB, BSet[k].Value(ref pt));
+			return Math.Max(fA, -fB);
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			// [TODO] could actually subtract other bounds here...
+			return A.Bounds();
+		}
+	}
+
+
+
+
+    /// <summary>
+    /// Continuous R-Function Boolean Union of two implicit functions, A OR B.
+    /// Assumption is that both have surface at zero isocontour and 
+    /// negative is inside.
+    /// </summary>
+    public class ImplicitSmoothUnion3d : BoundedImplicitFunction3d
+    {
+        public BoundedImplicitFunction3d A;
+        public BoundedImplicitFunction3d B;
+
+        const double mul = 1.0 / 1.5;
+
+        public double Value(ref Vector3d pt) {
+			double fA = A.Value(ref pt);
+			double fB = B.Value(ref pt);
+			return mul * (fA + fB - Math.Sqrt(fA*fA + fB*fB - fA*fB));
+        }
+
+        public AxisAlignedBox3d Bounds() {
+            var box = A.Bounds();
+            box.Contain(B.Bounds());
+            return box;
+        }
+    }
+
+
+
+    /// <summary>
+    /// Continuous R-Function Boolean Intersection of two implicit functions, A-B = A AND (NOT B)
+    /// Assumption is that both have surface at zero isocontour and 
+    /// negative is inside.
+    /// </summary>
+    public class ImplicitSmoothIntersection3d : BoundedImplicitFunction3d
+    {
+        public BoundedImplicitFunction3d A;
+        public BoundedImplicitFunction3d B;
+
+        const double mul = 1.0 / 1.5;
+
+        public double Value(ref Vector3d pt) {
+            double fA = A.Value(ref pt);
+            double fB = B.Value(ref pt);
+            return mul * (fA + fB + Math.Sqrt(fA*fA + fB*fB - fA*fB));
+        }
+
+        public AxisAlignedBox3d Bounds() {
+            var box = A.Bounds();
+            box.Contain(B.Bounds());
+            return box;
+        }
+    }
+
+
+
+
+    /// <summary>
+    /// Continuous R-Function Boolean Difference of two implicit functions, A AND B
+    /// Assumption is that both have surface at zero isocontour and 
+    /// negative is inside.
+    /// </summary>
+    public class ImplicitSmoothDifference3d : BoundedImplicitFunction3d
+    {
+        public BoundedImplicitFunction3d A;
+        public BoundedImplicitFunction3d B;
+
+        const double mul = 1.0 / 1.5;
+
+        public double Value(ref Vector3d pt) {
+            double fA = A.Value(ref pt);
+            double fB = -B.Value(ref pt);
+            return mul * (fA + fB + Math.Sqrt(fA*fA + fB*fB - fA*fB));
+        }
+
+        public AxisAlignedBox3d Bounds() {
+            var box = A.Bounds();
+            box.Contain(B.Bounds());
+            return box;
+        }
+    }
+
+
+
+
+    /// <summary>
+    /// Blend of two implicit surfaces. Assumes surface is at zero iscontour.
+    /// Uses Pasko blend from http://www.hyperfun.org/F-rep.pdf
+    /// </summary>
+    public class ImplicitBlend3d : BoundedImplicitFunction3d
+	{
+		public BoundedImplicitFunction3d A;
+		public BoundedImplicitFunction3d B;
+
+
+        /// <summary>Weight on implicit A</summary>
+        public double WeightA {
+            get { return weightA; }
+            set { weightA = MathUtil.Clamp(value, 0.00001, 100000); }
+        }
+        double weightA = 0.01;
+
+        /// <summary>Weight on implicit B</summary>
+        public double WeightB {
+            get { return weightB; }
+            set { weightB = MathUtil.Clamp(value, 0.00001, 100000); }
+        }
+        double weightB = 0.01;
+
+        /// <summary>Blending power</summary>
+        public double Blend {
+			get { return blend; }
+			set { blend = MathUtil.Clamp(value, 0.0, 100000); }
+		}
+		double blend = 2.0;
+
+
+        public double ExpandBounds = 0.25;
+
+
+		public double Value(ref Vector3d pt)
+		{
+			double fA = A.Value(ref pt);
+			double fB = B.Value(ref pt);
+			double sqr_sum = fA*fA + fB*fB;
+            if (sqr_sum > 1e12)
+                return Math.Min(fA, fB);
+            double wa = fA/weightA, wb = fB/weightB;
+            double b = blend / (1.0 + wa*wa + wb*wb);
+            //double a = 0.5;
+            //return (1.0/(1.0+a)) * (fA + fB - Math.Sqrt(fA*fA + fB*fB - 2*a*fA*fB)) - b;
+            return 0.666666 * (fA + fB - Math.Sqrt(sqr_sum - fA*fB)) - b;
+		}
+
+		public AxisAlignedBox3d Bounds()
+		{
+			var box = A.Bounds();
+			box.Contain(B.Bounds());
+            box.Expand(ExpandBounds * box.MaxDim );
+			return box;
+		}
+	}
+
+
+
+
+
+
+
+
+    /*
+     *  Skeletal implicit ops
+     */
+
+
+
+    /// <summary>
+    /// This class converts the interval [-falloff,falloff] to [0,1],
+    /// Then applies Wyvill falloff function (1-t^2)^3.
+    /// The result is a skeletal-primitive-like shape with 
+    /// the distance=0 isocontour lying just before midway in
+    /// the range (at the .ZeroIsocontour constant)
+    /// </summary>
+    public class DistanceFieldToSkeletalField : BoundedImplicitFunction3d
+    {
+        public BoundedImplicitFunction3d DistanceField;
+        public double FalloffDistance;
+        public const double ZeroIsocontour = 0.421875;
+
+        public AxisAlignedBox3d Bounds()
+        {
+            AxisAlignedBox3d bounds = DistanceField.Bounds();
+            bounds.Expand(FalloffDistance);
+            return bounds;
+        }
+
+        public double Value(ref Vector3d pt)
+        {
+            double d = DistanceField.Value(ref pt);
+            if (d > FalloffDistance)
+                return 0;
+            else if (d < -FalloffDistance)
+                return 1.0;
+            double a = (d + FalloffDistance) / (2 * FalloffDistance);
+            double t = 1 - (a * a);
+            return t * t * t;
+        }
+    }
+
+
 
-    public class ImplicitSphere3d : ImplicitFunction3d
+
+
+
+
+    /// <summary>
+    /// sum-blend
+    /// </summary>
+    public class SkeletalBlend3d : BoundedImplicitFunction3d
     {
+        public BoundedImplicitFunction3d A;
+        public BoundedImplicitFunction3d B;
+
         public double Value(ref Vector3d pt)
         {
-            return pt.Length - 5.0f;
+            return A.Value(ref pt) + B.Value(ref pt);
+        }
+
+        public AxisAlignedBox3d Bounds()
+        {
+            AxisAlignedBox3d box = A.Bounds();
+            box.Contain(B.Bounds());
+            box.Expand(0.25 * box.MaxDim);
+            return box;
         }
     }
 
 
+
+    /// <summary>
+    /// Ricci blend
+    /// </summary>
+    public class SkeletalRicciBlend3d : BoundedImplicitFunction3d
+    {
+        public BoundedImplicitFunction3d A;
+        public BoundedImplicitFunction3d B;
+        public double BlendPower = 2.0;
+
+        public double Value(ref Vector3d pt)
+        {
+            double a = A.Value(ref pt);
+            double b = B.Value(ref pt);
+            if ( BlendPower == 1.0 ) {
+                return a + b;
+            } else if (BlendPower == 2.0) {
+                return Math.Sqrt(a*a + b*b);
+            } else {
+                return Math.Pow( Math.Pow(a,BlendPower) + Math.Pow(b,BlendPower), 1.0/BlendPower);
+            }
+        }
+
+        public AxisAlignedBox3d Bounds()
+        {
+            AxisAlignedBox3d box = A.Bounds();
+            box.Contain(B.Bounds());
+            box.Expand(0.25 * box.MaxDim);
+            return box;
+        }
+    }
+
+
+
+
+    /// <summary>
+    /// Boolean Union of N implicit functions, A OR B.
+    /// Assumption is that both have surface at zero isocontour and 
+    /// negative is inside.
+    /// </summary>
+    public class SkeletalRicciNaryBlend3d : BoundedImplicitFunction3d
+    {
+        public List<BoundedImplicitFunction3d> Children;
+        public double BlendPower = 2.0;
+        public double FieldShift = 0;
+
+        public double Value(ref Vector3d pt)
+        {
+            int N = Children.Count;
+            double f = 0;
+            if (BlendPower == 1.0) {
+                for (int k = 0; k < N; ++k)
+                    f += Children[k].Value(ref pt);
+            } else if (BlendPower == 2.0) {
+                for (int k = 0; k < N; ++k) {
+                    double v = Children[k].Value(ref pt);
+                    f += v * v;
+                }
+                f = Math.Sqrt(f);
+            } else {
+                for (int k = 0; k < N; ++k) {
+                    double v = Children[k].Value(ref pt);
+                    f += Math.Pow(v, BlendPower);
+                }
+                f = Math.Pow(f, 1.0 / BlendPower);
+            }
+            return f + FieldShift;
+        }
+
+        public AxisAlignedBox3d Bounds()
+        {
+            var box = Children[0].Bounds();
+            int N = Children.Count;
+            for (int k = 1; k < N; ++k)
+                box.Contain(Children[k].Bounds());
+            box.Expand(0.25 * box.MaxDim);
+            return box;
+        }
+    }
+
+
+
+
+
 }
diff --git a/implicit/ImplicitFieldSampler3d.cs b/implicit/ImplicitFieldSampler3d.cs
new file mode 100644
index 00000000..8f01b3fa
--- /dev/null
+++ b/implicit/ImplicitFieldSampler3d.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace g3
+{
+    /// <summary>
+    /// Sample implicit fields into a dense grid
+    /// </summary>
+    public class ImplicitFieldSampler3d
+    {
+        public DenseGrid3f Grid;
+        public double CellSize;
+        public Vector3d GridOrigin;
+        public ShiftGridIndexer3 Indexer;
+        public AxisAlignedBox3i GridBounds;
+
+        public float BackgroundValue;
+
+
+        public enum CombineModes
+        {
+            DistanceMinUnion = 0
+        }
+        public CombineModes CombineMode = CombineModes.DistanceMinUnion;
+
+
+        public ImplicitFieldSampler3d(AxisAlignedBox3d fieldBounds, double cellSize)
+        {
+            CellSize = cellSize;
+            GridOrigin = fieldBounds.Min;
+            Indexer = new ShiftGridIndexer3(GridOrigin, CellSize);
+
+            Vector3d max = fieldBounds.Max; max += cellSize;
+            int ni = (int)((max.x - GridOrigin.x) / CellSize) + 1;
+            int nj = (int)((max.y - GridOrigin.y) / CellSize) + 1;
+            int nk = (int)((max.z - GridOrigin.z) / CellSize) + 1;
+
+            GridBounds = new AxisAlignedBox3i(0, 0, 0, ni, nj, nk);
+
+            BackgroundValue = (float)((ni + nj + nk) * CellSize);
+            Grid = new DenseGrid3f(ni, nj, nk, BackgroundValue);
+        }
+
+
+
+        public DenseGridTrilinearImplicit ToImplicit() {
+            return new DenseGridTrilinearImplicit(Grid, GridOrigin, CellSize);
+        }
+
+
+
+        public void Clear(float f)
+        {
+            BackgroundValue = f;
+            Grid.assign(BackgroundValue);
+        }
+
+
+
+        public void Sample(BoundedImplicitFunction3d f, double expandRadius = 0)
+        {
+            AxisAlignedBox3d bounds = f.Bounds();
+
+            Vector3d expand = expandRadius * Vector3d.One;
+            Vector3i gridMin = Indexer.ToGrid(bounds.Min-expand), 
+                     gridMax = Indexer.ToGrid(bounds.Max+expand) + Vector3i.One;
+            gridMin = GridBounds.ClampExclusive(gridMin);
+            gridMax = GridBounds.ClampExclusive(gridMax);
+
+            AxisAlignedBox3i gridbox = new AxisAlignedBox3i(gridMin, gridMax);
+            switch (CombineMode) {
+                case CombineModes.DistanceMinUnion:
+                    sample_min(f, gridbox.IndicesInclusive());
+                    break;
+            }
+        }
+
+
+        void sample_min(BoundedImplicitFunction3d f, IEnumerable<Vector3i> indices)
+        {
+            gParallel.ForEach(indices, (idx) => {
+                Vector3d v = Indexer.FromGrid(idx);
+                double d = f.Value(ref v);
+                Grid.set_min(ref idx, (float)d);
+            });
+        }
+
+    }
+}
diff --git a/implicit/MarchingQuads.cs b/implicit/MarchingQuads.cs
index 9c1a0c65..03a324ba 100644
--- a/implicit/MarchingQuads.cs
+++ b/implicit/MarchingQuads.cs
@@ -5,7 +5,9 @@
 namespace g3
 {
 	/// <summary>
-	/// Summary description for MarchingQuads.
+	/// 2D MarchingQuads polyline extraction from scalar field
+    /// [TODO] this is very, very old code. Should at minimum rewrite using current
+    /// vector classes/etc.
 	/// </summary>
 	public class MarchingQuads
 	{
@@ -111,7 +113,6 @@ public 	AxisAlignedBox2f GetBounds() {
 
 
 		public void AddSeedPoint( float x, float y ) {
-			// [RMS TODO] does this memleak... ??
 			m_seedPoints.Add( new SeedPoint(x - m_fXShift, y - m_fYShift) );
 		}
 
@@ -165,39 +166,6 @@ public void Polygonize( ImplicitField2d field ) {
 		}
 
 
-		void LerpStep(ref float fValue1, ref float fValue2, ref float fX1, ref float fY1, ref float fX2, ref float fY2, 
-						bool bVerticalEdge) {
-
-	        float fAlpha = 0.0f;
-	        if ( Math.Abs(fValue1-fValue2) < 0.001 ) {
-		        fAlpha = 0.5f;
-	        } else {
-		        fAlpha = (m_fIsoValue - fValue2) / (fValue1 - fValue2);
-	        }
-
-	        float fX = 0.0f, fY = 0.0f;
-	        if (bVerticalEdge) {
-		        fX = fX1;
-		        fY = fAlpha*fY1 + (1.0f-fAlpha)*fY2;
-	        } else {
-		        fX = fAlpha*fX1	+ (1.0f-fAlpha)*fX2;
-		        fY = fY1;
-	        }
-
-	        float fValue = (float)m_field.Value(fX, fY);
-	        if (fValue < m_fIsoValue) {
-		        fValue1 = fValue;
-		        fX1 = fX;
-		        fY1 = fY;
-	        } else {
-		        fValue2 = fValue;
-		        fX2 = fX;
-		        fY2 = fY;
-	        }
-
-		}
-
-
 		void SubdivideStep(ref float fValue1, ref float fValue2, ref float fX1, ref float fY1, ref float fX2, ref float fY2, 
 						bool bVerticalEdge) {
 
@@ -249,7 +217,7 @@ int LerpAndAddStrokeVertex( float fValue1, float fValue2, int x1, int y1, int x2
 			float fX2 = (float)x2 * m_fCellSize + m_fXShift;
 			float fY2 = (float)y2 * m_fCellSize + m_fYShift;
 
-            for (int i = 0; i < 5; ++i)
+            for (int i = 0; i < 10; ++i)
                 SubdivideStep(ref fRefValue1, ref fRefValue2, ref fX1, ref fY1, ref fX2, ref fY2, bVerticalEdge);
 
 			if ( Math.Abs(fRefValue1) < Math.Abs(fRefValue2) ) {
@@ -449,30 +417,5 @@ void SetBounds( AxisAlignedBox2f bounds ) {
 		}
 
 
-/*
-		public void DrawTouchedCells( Graphics g, Pen pen ) {
-
-			for (int yi = 0; yi < m_nCells; ++yi) {
-				for (int xi = 0; xi < m_nCells; ++xi) {
-					Cell cell = m_cells[yi][xi];
-					int x = (int)((float)xi*m_fCellSize + m_fXShift);
-					int y = (int)((float)yi*m_fCellSize + m_fYShift);
-					if (cell.bTouched) {
-						g.DrawRectangle( pen, x, y, (int)m_fCellSize, (int)m_fCellSize );
-					}
-					//if (cell.fValue == s_fValueSentinel)
-						//g.FillRectangle( Brushes.DarkOrange, x-2, y-2, 4, 4 );
-					if (cell.fValue != s_fValueSentinel) {
-						if (cell.fValue < 0.0)
-							g.FillRectangle( Brushes.Magenta, x-2, y-2, 4, 4 );
-						else
-							g.FillRectangle( Brushes.Cyan, x-2, y-2, 4, 4 );
-					}
-				}
-			}
-
-		}
-*/
-
 	}
 }
diff --git a/intersection/IntrLine3AxisAlignedBox3.cs b/intersection/IntrLine3AxisAlignedBox3.cs
index eb66f66b..16c71bf0 100644
--- a/intersection/IntrLine3AxisAlignedBox3.cs
+++ b/intersection/IntrLine3AxisAlignedBox3.cs
@@ -61,7 +61,7 @@ public bool Find()
 
 			LineParam0 = -double.MaxValue;
 			LineParam1 = double.MaxValue;
-			DoClipping(ref LineParam0, ref LineParam1, line.Origin, line.Direction, box,
+			DoClipping(ref LineParam0, ref LineParam1, ref line.Origin, ref line.Direction, ref box,
 			          true, ref Quantity, ref Point0, ref Point1, ref Type);
 
 			Result = (Type != IntersectionType.Empty) ?
@@ -111,8 +111,8 @@ public bool Test ()
 
 
 		static public bool DoClipping (ref double t0, ref double t1,
-		                 Vector3d origin, Vector3d direction,
-		                 AxisAlignedBox3d box, bool solid, ref int quantity, 
+		                 ref Vector3d origin, ref Vector3d direction,
+		                 ref AxisAlignedBox3d box, bool solid, ref int quantity, 
                          ref Vector3d point0, ref Vector3d point1,
 		                 ref IntersectionType  intrType)
 		{
diff --git a/intersection/IntrRay3AxisAlignedBox3.cs b/intersection/IntrRay3AxisAlignedBox3.cs
index e8241e6f..1c2ddb5d 100644
--- a/intersection/IntrRay3AxisAlignedBox3.cs
+++ b/intersection/IntrRay3AxisAlignedBox3.cs
@@ -61,7 +61,7 @@ public bool Find()
 
 			RayParam0 = 0.0;
 			RayParam1 = double.MaxValue;
-			IntrLine3AxisAlignedBox3.DoClipping(ref RayParam0, ref RayParam1, ray.Origin, ray.Direction, box,
+			IntrLine3AxisAlignedBox3.DoClipping(ref RayParam0, ref RayParam1, ref ray.Origin, ref ray.Direction, ref box,
 			          true, ref Quantity, ref Point0, ref Point1, ref Type);
 
 			Result = (Type != IntersectionType.Empty) ?
@@ -71,73 +71,102 @@ public bool Find()
 
 
 
-        // [RMS TODO: lots of useless dot products below!! left over from obox conversion]
 		public bool Test ()
 		{
-			Vector3d WdU = Vector3d.Zero;
-			Vector3d AWdU = Vector3d.Zero;
-			Vector3d DdU = Vector3d.Zero;
-			Vector3d ADdU = Vector3d.Zero;
-			Vector3d AWxDdU = Vector3d.Zero;
-			double RHS;
-
-			Vector3d diff = ray.Origin - box.Center;
-            Vector3d extent = box.Extents;
-
-			WdU[0] = ray.Direction.Dot(Vector3d.AxisX);
-			AWdU[0] = Math.Abs(WdU[0]);
-			DdU[0] = diff.Dot(Vector3d.AxisX);
-			ADdU[0] = Math.Abs(DdU[0]);
-			if (ADdU[0] > extent.x && DdU[0]*WdU[0] >= (double)0)
-			{
-				return false;
-			}
-
-			WdU[1] = ray.Direction.Dot(Vector3d.AxisY);
-			AWdU[1] = Math.Abs(WdU[1]);
-			DdU[1] = diff.Dot(Vector3d.AxisY);
-			ADdU[1] = Math.Abs(DdU[1]);
-			if (ADdU[1] > extent.y && DdU[1]*WdU[1] >= (double)0)
-			{
-				return false;
-			}
-
-			WdU[2] = ray.Direction.Dot(Vector3d.AxisZ);
-			AWdU[2] = Math.Abs(WdU[2]);
-			DdU[2] = diff.Dot(Vector3d.AxisZ);
-			ADdU[2] = Math.Abs(DdU[2]);
-			if (ADdU[2] > extent.z && DdU[2]*WdU[2] >= (double)0)
-			{
-				return false;
-			}
-
-			Vector3d WxD = ray.Direction.Cross(diff);
-
-			AWxDdU[0] = Math.Abs(WxD.Dot(Vector3d.AxisX));
-			RHS = extent.y*AWdU[2] + extent.z*AWdU[1];
-			if (AWxDdU[0] > RHS)
-			{
-				return false;
-			}
-
-			AWxDdU[1] = Math.Abs(WxD.Dot(Vector3d.AxisY));
-			RHS = extent.x*AWdU[2] + extent.z*AWdU[0];
-			if (AWxDdU[1] > RHS)
-			{
-				return false;
-			}
-
-			AWxDdU[2] = Math.Abs(WxD.Dot(Vector3d.AxisZ));
-			RHS = extent.x*AWdU[1] + extent.y*AWdU[0];
-			if (AWxDdU[2] > RHS)
-			{
-				return false;
-			}
-
-			return true;
-		}
-
-
-
-	}
+            return Intersects(ref ray, ref box);
+        }
+
+
+        /// <summary>
+        /// test if ray intersects box.
+        /// expandExtents allows you to scale box for hit-testing purposes. 
+        /// </summary>
+        public static bool Intersects(ref Ray3d ray, ref AxisAlignedBox3d box, double expandExtents = 0)
+        {
+            Vector3d WdU = Vector3d.Zero;
+            Vector3d AWdU = Vector3d.Zero;
+            Vector3d DdU = Vector3d.Zero;
+            Vector3d ADdU = Vector3d.Zero;
+            double RHS;
+
+            Vector3d diff = ray.Origin - box.Center;
+            Vector3d extent = box.Extents + expandExtents;
+
+            WdU.x = ray.Direction.x; // ray.Direction.Dot(Vector3d.AxisX);
+            AWdU.x = Math.Abs(WdU.x);
+            DdU.x = diff.x; // diff.Dot(Vector3d.AxisX);
+            ADdU.x = Math.Abs(DdU.x);
+            if (ADdU.x > extent.x && DdU.x * WdU.x >= (double)0) {
+                return false;
+            }
+
+            WdU.y = ray.Direction.y; // ray.Direction.Dot(Vector3d.AxisY);
+            AWdU.y = Math.Abs(WdU.y);
+            DdU.y = diff.y; // diff.Dot(Vector3d.AxisY);
+            ADdU.y = Math.Abs(DdU.y);
+            if (ADdU.y > extent.y && DdU.y * WdU.y >= (double)0) {
+                return false;
+            }
+
+            WdU.z = ray.Direction.z; // ray.Direction.Dot(Vector3d.AxisZ);
+            AWdU.z = Math.Abs(WdU.z);
+            DdU.z = diff.z; // diff.Dot(Vector3d.AxisZ);
+            ADdU.z = Math.Abs(DdU.z);
+            if (ADdU.z > extent.z && DdU.z * WdU.z >= (double)0) {
+                return false;
+            }
+
+            Vector3d WxD = ray.Direction.Cross(diff);
+            Vector3d AWxDdU = Vector3d.Zero;
+
+            AWxDdU.x = Math.Abs(WxD.x); // Math.Abs(WxD.Dot(Vector3d.AxisX));
+            RHS = extent.y * AWdU.z + extent.z * AWdU.y;
+            if (AWxDdU.x > RHS) {
+                return false;
+            }
+
+            AWxDdU.y = Math.Abs(WxD.y); // Math.Abs(WxD.Dot(Vector3d.AxisY));
+            RHS = extent.x * AWdU.z + extent.z * AWdU.x;
+            if (AWxDdU.y > RHS) {
+                return false;
+            }
+
+            AWxDdU.z = Math.Abs(WxD.z); // Math.Abs(WxD.Dot(Vector3d.AxisZ));
+            RHS = extent.x * AWdU.y + extent.y * AWdU.x;
+            if (AWxDdU.z > RHS) {
+                return false;
+            }
+
+            return true;
+        }
+
+
+        /// <summary>
+        /// Find intersection of ray with AABB, without having to construct any new classes.
+        /// Returns ray T-value of first intersection (or double.MaxVlaue on miss)
+        /// </summary>
+        public static bool FindRayIntersectT(ref Ray3d ray, ref AxisAlignedBox3d box, out double RayParam)
+        {
+            double RayParam0 = 0.0;
+            double RayParam1 = double.MaxValue;
+            int Quantity = 0;
+            Vector3d Point0 = Vector3d.Zero;
+            Vector3d Point1 = Vector3d.Zero;
+            IntersectionType Type = IntersectionType.Empty;
+            IntrLine3AxisAlignedBox3.DoClipping(ref RayParam0, ref RayParam1, ref ray.Origin, ref ray.Direction, ref box,
+                      true, ref Quantity, ref Point0, ref Point1, ref Type);
+
+            if (Type != IntersectionType.Empty) {
+                RayParam = RayParam0;
+                return true;
+            } else {
+                RayParam = double.MaxValue;
+                return false;
+            }
+        }
+
+
+
+
+    }
 }
diff --git a/intersection/IntrRay3Box3.cs b/intersection/IntrRay3Box3.cs
index 22420b03..278a7f5f 100644
--- a/intersection/IntrRay3Box3.cs
+++ b/intersection/IntrRay3Box3.cs
@@ -74,69 +74,77 @@ public bool Find()
 
 		public bool Test ()
 		{
-			Vector3d WdU = Vector3d.Zero;
-			Vector3d AWdU = Vector3d.Zero;
-			Vector3d DdU = Vector3d.Zero;
-			Vector3d ADdU = Vector3d.Zero;
-			Vector3d AWxDdU = Vector3d.Zero;
-			double RHS;
-
-			Vector3d diff = ray.Origin - box.Center;
-
-			WdU[0] = ray.Direction.Dot(box.AxisX);
-			AWdU[0] = Math.Abs(WdU[0]);
-			DdU[0] = diff.Dot(box.AxisX);
-			ADdU[0] = Math.Abs(DdU[0]);
-			if (ADdU[0] > box.Extent.x && DdU[0]*WdU[0] >= (double)0)
-			{
-				return false;
-			}
+            return Intersects(ref ray, ref box);
+		}
 
-			WdU[1] = ray.Direction.Dot(box.AxisY);
-			AWdU[1] = Math.Abs(WdU[1]);
-			DdU[1] = diff.Dot(box.AxisY);
-			ADdU[1] = Math.Abs(DdU[1]);
-			if (ADdU[1] > box.Extent.y && DdU[1]*WdU[1] >= (double)0)
-			{
-				return false;
-			}
 
-			WdU[2] = ray.Direction.Dot(box.AxisZ);
-			AWdU[2] = Math.Abs(WdU[2]);
-			DdU[2] = diff.Dot(box.AxisZ);
-			ADdU[2] = Math.Abs(DdU[2]);
-			if (ADdU[2] > box.Extent.z && DdU[2]*WdU[2] >= (double)0)
-			{
-				return false;
-			}
 
-			Vector3d WxD = ray.Direction.Cross(diff);
+        /// <summary>
+        /// test if ray intersects box.
+        /// expandExtents allows you to scale box for hit-testing purposes.
+        /// </summary>
+        public static bool Intersects(ref Ray3d ray, ref Box3d box, double expandExtents = 0)
+        {
+            Vector3d WdU = Vector3d.Zero;
+            Vector3d AWdU = Vector3d.Zero;
+            Vector3d DdU = Vector3d.Zero;
+            Vector3d ADdU = Vector3d.Zero;
+            Vector3d AWxDdU = Vector3d.Zero;
+            double RHS;
 
-			AWxDdU[0] = Math.Abs(WxD.Dot(box.AxisX));
-			RHS = box.Extent.y*AWdU[2] + box.Extent.z*AWdU[1];
-			if (AWxDdU[0] > RHS)
-			{
-				return false;
-			}
+            Vector3d diff = ray.Origin - box.Center;
+            Vector3d extent = box.Extent + expandExtents;
 
-			AWxDdU[1] = Math.Abs(WxD.Dot(box.AxisY));
-			RHS = box.Extent.x*AWdU[2] + box.Extent.z*AWdU[0];
-			if (AWxDdU[1] > RHS)
-			{
-				return false;
-			}
+            WdU[0] = ray.Direction.Dot(ref box.AxisX);
+            AWdU[0] = Math.Abs(WdU[0]);
+            DdU[0] = diff.Dot(ref box.AxisX);
+            ADdU[0] = Math.Abs(DdU[0]);
+            if (ADdU[0] > extent.x && DdU[0] * WdU[0] >= (double)0) {
+                return false;
+            }
+
+            WdU[1] = ray.Direction.Dot(ref box.AxisY);
+            AWdU[1] = Math.Abs(WdU[1]);
+            DdU[1] = diff.Dot(ref box.AxisY);
+            ADdU[1] = Math.Abs(DdU[1]);
+            if (ADdU[1] > extent.y && DdU[1] * WdU[1] >= (double)0) {
+                return false;
+            }
+
+            WdU[2] = ray.Direction.Dot(ref box.AxisZ);
+            AWdU[2] = Math.Abs(WdU[2]);
+            DdU[2] = diff.Dot(ref box.AxisZ);
+            ADdU[2] = Math.Abs(DdU[2]);
+            if (ADdU[2] > extent.z && DdU[2] * WdU[2] >= (double)0) {
+                return false;
+            }
+
+            Vector3d WxD = ray.Direction.Cross(diff);
+
+            AWxDdU[0] = Math.Abs(WxD.Dot(ref box.AxisX));
+            RHS = extent.y * AWdU[2] + extent.z * AWdU[1];
+            if (AWxDdU[0] > RHS) {
+                return false;
+            }
+
+            AWxDdU[1] = Math.Abs(WxD.Dot(ref box.AxisY));
+            RHS = extent.x * AWdU[2] + extent.z * AWdU[0];
+            if (AWxDdU[1] > RHS) {
+                return false;
+            }
+
+            AWxDdU[2] = Math.Abs(WxD.Dot(ref box.AxisZ));
+            RHS = extent.x * AWdU[1] + extent.y * AWdU[0];
+            if (AWxDdU[2] > RHS) {
+                return false;
+            }
+
+            return true;
+        }
 
-			AWxDdU[2] = Math.Abs(WxD.Dot(box.AxisZ));
-			RHS = box.Extent.x*AWdU[1] + box.Extent.y*AWdU[0];
-			if (AWxDdU[2] > RHS)
-			{
-				return false;
-			}
 
-			return true;
-		}
 
 
 
-	}
+    }
 }
diff --git a/intersection/IntrRay3Triangle3.cs b/intersection/IntrRay3Triangle3.cs
index e303d5b7..611c907f 100644
--- a/intersection/IntrRay3Triangle3.cs
+++ b/intersection/IntrRay3Triangle3.cs
@@ -107,5 +107,65 @@ public bool Find()
             Result = IntersectionResult.NoIntersection;
             return false;
         }
+
+
+
+        /// <summary>
+        /// minimal intersection test, computes ray-t
+        /// </summary>
+        public static bool Intersects(ref Ray3d ray, ref Vector3d V0, ref Vector3d V1, ref Vector3d V2, out double rayT)
+        {
+            // Compute the offset origin, edges, and normal.
+            Vector3d diff = ray.Origin - V0;
+            Vector3d edge1 = V1 - V0;
+            Vector3d edge2 = V2 - V0;
+            Vector3d normal = edge1.Cross(ref edge2);
+
+            rayT = double.MaxValue;
+
+            // Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction,
+            // E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by
+            //   |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))
+            //   |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))
+            //   |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)
+            double DdN = ray.Direction.Dot(ref normal);
+            double sign;
+            if (DdN > MathUtil.ZeroTolerance) {
+                sign = 1;
+            } else if (DdN < -MathUtil.ZeroTolerance) {
+                sign = -1;
+                DdN = -DdN;
+            } else {
+                // Ray and triangle are parallel, call it a "no intersection"
+                // even if the ray does intersect.
+                return false;
+            }
+
+            Vector3d cross = diff.Cross(ref edge2);
+            double DdQxE2 = sign * ray.Direction.Dot(ref cross);
+            if (DdQxE2 >= 0) {
+                cross = edge1.Cross(ref diff);
+                double DdE1xQ = sign * ray.Direction.Dot(ref cross);
+                if (DdE1xQ >= 0) {
+                    if (DdQxE2 + DdE1xQ <= DdN) {
+                        // Line intersects triangle, check if ray does.
+                        double QdN = -sign * diff.Dot(ref normal);
+                        if (QdN >= 0) {
+                            // Ray intersects triangle.
+                            double inv = (1) / DdN;
+                            rayT = QdN * inv;
+                            return true;
+                        }
+                        // else: t < 0, no intersection
+                    }
+                    // else: b1+b2 > 1, no intersection
+                }
+                // else: b2 < 0, no intersection
+            }
+            // else: b1 < 0, no intersection
+
+            return false;
+        }
+
     }
 }
diff --git a/intersection/IntrTriangle3Triangle3.cs b/intersection/IntrTriangle3Triangle3.cs
index 5a8ac887..1fda8145 100644
--- a/intersection/IntrTriangle3Triangle3.cs
+++ b/intersection/IntrTriangle3Triangle3.cs
@@ -5,7 +5,7 @@ namespace g3
 {
     // ported from WildMagic5 IntrTriangle3Triangle3
     // use Test() for fast boolean query, does not compute intersection info
-    // use Find() to for full information
+    // use Find() to compute full information
     // By default fully-contained co-planar triangles are not reported as intersecting.
     // set ReportCoplanarIntersection=true to handle this case (more expensive)
     public class IntrTriangle3Triangle3
@@ -242,12 +242,100 @@ public bool Test()
 
 
 
+        public static bool Intersects(ref Triangle3d triangle0, ref Triangle3d triangle1)
+        {
+            // Get edge vectors for triangle0.
+            Vector3dTuple3 E0;
+            E0.V0 = triangle0.V1 - triangle0.V0;
+            E0.V1 = triangle0.V2 - triangle0.V1;
+            E0.V2 = triangle0.V0 - triangle0.V2;
+
+            // Get normal vector of triangle0.
+            Vector3d N0 = E0.V0.UnitCross(ref E0.V1);
+
+            // Project triangle1 onto normal line of triangle0, test for separation.
+            double N0dT0V0 = N0.Dot(ref triangle0.V0);
+            double min1, max1;
+            ProjectOntoAxis(ref triangle1, ref N0, out min1, out max1);
+            if (N0dT0V0 < min1 || N0dT0V0 > max1) {
+                return false;
+            }
+
+            // Get edge vectors for triangle1.
+            Vector3dTuple3 E1;
+            E1.V0 = triangle1.V1 - triangle1.V0;
+            E1.V1 = triangle1.V2 - triangle1.V1;
+            E1.V2 = triangle1.V0 - triangle1.V2;
+
+            // Get normal vector of triangle1.
+            Vector3d N1 = E1.V0.UnitCross(ref E1.V1);
+
+            Vector3d dir;
+            double min0, max0;
+            int i0, i1;
+
+            Vector3d N0xN1 = N0.UnitCross(ref N1);
+            if (N0xN1.Dot(ref N0xN1) >= MathUtil.ZeroTolerance) {
+                // Triangles are not parallel.
+
+                // Project triangle0 onto normal line of triangle1, test for
+                // separation.
+                double N1dT1V0 = N1.Dot(ref triangle1.V0);
+                ProjectOntoAxis(ref triangle0, ref N1, out min0, out max0);
+                if (N1dT1V0 < min0 || N1dT1V0 > max0) {
+                    return false;
+                }
+
+                // Directions E0[i0]xE1[i1].
+                for (i1 = 0; i1 < 3; ++i1) {
+                    for (i0 = 0; i0 < 3; ++i0) {
+                        dir = E0[i0].UnitCross(E1[i1]);  // could pass ref if we reversed these...need to negate?
+                        ProjectOntoAxis(ref triangle0, ref dir, out min0, out max0);
+                        ProjectOntoAxis(ref triangle1, ref dir, out min1, out max1);
+                        if (max0 < min1 || max1 < min0) {
+                            return false;
+                        }
+                    }
+                }
+
+            } else { // Triangles are parallel (and, in fact, coplanar).
+                // Directions N0xE0[i0].
+                for (i0 = 0; i0 < 3; ++i0) {
+                    dir = N0.UnitCross(E0[i0]);
+                    ProjectOntoAxis(ref triangle0, ref dir, out min0, out max0);
+                    ProjectOntoAxis(ref triangle1, ref dir, out min1, out max1);
+                    if (max0 < min1 || max1 < min0) {
+                        return false;
+                    }
+                }
+
+                // Directions N1xE1[i1].
+                for (i1 = 0; i1 < 3; ++i1) {
+                    dir = N1.UnitCross(E1[i1]);
+                    ProjectOntoAxis(ref triangle0, ref dir, out min0, out max0);
+                    ProjectOntoAxis(ref triangle1, ref dir, out min1, out max1);
+                    if (max0 < min1 || max1 < min0) {
+                        return false;
+                    }
+                }
+            }
+
+            return true;
+        }
+
+
+
+
+
+
+
+
 
 
 
 
 
-        void ProjectOntoAxis ( ref Triangle3d triangle, ref Vector3d axis, out double fmin, out double fmax)
+        static public void ProjectOntoAxis ( ref Triangle3d triangle, ref Vector3d axis, out double fmin, out double fmax)
         {
             double dot0 = axis.Dot(triangle.V0);
             double dot1 = axis.Dot(triangle.V1);
@@ -277,7 +365,7 @@ void ProjectOntoAxis ( ref Triangle3d triangle, ref Vector3d axis, out double fm
 
 
 
-        void TrianglePlaneRelations ( ref Triangle3d triangle, ref Plane3d plane,
+        static public void TrianglePlaneRelations ( ref Triangle3d triangle, ref Plane3d plane,
             out Vector3d distance, out Index3i sign, out int positive, out int negative, out int zero)
         {
             // Compute the signed distances of triangle vertices to the plane.  Use
diff --git a/io/OBJWriter.cs b/io/OBJWriter.cs
index a22c0b4e..59708df7 100644
--- a/io/OBJWriter.cs
+++ b/io/OBJWriter.cs
@@ -15,6 +15,14 @@ namespace g3
 	/// </summary>
     public class OBJWriter : IMeshWriter
     {
+        // stream-opener. Override to write to something other than a file.
+        public Func<string, Stream> OpenStreamF = (sFilename) => {
+            return File.Open(sFilename, FileMode.Create);
+        };
+        public Action<Stream> CloseStreamF = (stream) => {
+            stream.Dispose();
+        };
+
         public string GroupNamePrefix = "mmGroup";   // default, compatible w/ meshmixer
         public Func<int, string> GroupNameF = null;  // use this to replace standard group names w/ your own
 
@@ -57,7 +65,7 @@ public IOWriteResult Write(TextWriter writer, List<WriteMesh> vMeshes, WriteOpti
                 IMesh mesh = vMeshes[mi].Mesh;
 
                 if (options.ProgressFunc != null)
-                    options.ProgressFunc(mi, vMeshes.Count - 1);
+                    options.ProgressFunc(mi, vMeshes.Count);
 
                 bool bVtxColors = options.bPerVertexColors && mesh.HasVertexColors;
                 bool bNormals = options.bPerVertexNormals && mesh.HasVertexNormals;
@@ -117,6 +125,8 @@ public IOWriteResult Write(TextWriter writer, List<WriteMesh> vMeshes, WriteOpti
                 else
 					write_triangles_flat(writer, vMeshes[mi], mapV, uvSet, mapUV, bNormals, bWriteMaterials);
 
+                if (options.ProgressFunc != null)
+                    options.ProgressFunc(mi+1, vMeshes.Count);
             }
 
 
@@ -233,61 +243,67 @@ void write_tri(TextWriter writer, ref Index3i t, bool bNormals, bool bUVs, ref I
 		// write .mtl file
 		IOWriteResult write_materials(List<GenericMaterial> vMaterials, WriteOptions options) 
 		{
-			StreamWriter w = new StreamWriter(options.MaterialFilePath);
-			if (w.BaseStream == null)
-				return new IOWriteResult(IOCode.FileAccessError, "Could not open file " + options.MaterialFilePath + " for writing");
-
-			foreach ( GenericMaterial gmat in vMaterials ) {
-				if ( gmat is OBJMaterial == false )
-					continue;
-				OBJMaterial mat = gmat as OBJMaterial;
-
-				w.WriteLine("newmtl {0}", mat.name);
-				if ( mat.Ka != GenericMaterial.Invalid )
-					w.WriteLine("Ka {0} {1} {2}", mat.Ka.x, mat.Ka.y, mat.Ka.z);
-				if ( mat.Kd != GenericMaterial.Invalid)
-					w.WriteLine("Kd {0} {1} {2}", mat.Kd.x, mat.Kd.y, mat.Kd.z);
-				if ( mat.Ks != GenericMaterial.Invalid )
-					w.WriteLine("Ks {0} {1} {2}", mat.Ks.x, mat.Ks.y, mat.Ks.z);
-				if ( mat.Ke != GenericMaterial.Invalid )
-					w.WriteLine("Ke {0} {1} {2}", mat.Ke.x, mat.Ke.y, mat.Ke.z);
-				if ( mat.Tf != GenericMaterial.Invalid )
-					w.WriteLine("Tf {0} {1} {2}", mat.Tf.x, mat.Tf.y, mat.Tf.z);
-				if ( mat.d != Single.MinValue )
-					w.WriteLine("d {0}", mat.d);
-				if ( mat.Ns != Single.MinValue )
-					w.WriteLine("Ns {0}", mat.Ns);
-				if ( mat.Ni != Single.MinValue )
-					w.WriteLine("Ni {0}", mat.Ni);
-				if ( mat.sharpness != Single.MinValue )
-					w.WriteLine("sharpness {0}", mat.sharpness);
-				if ( mat.illum != -1 )
-					w.WriteLine("illum {0}", mat.illum);
-
-				if ( mat.map_Ka != null && mat.map_Ka != "" )
-					w.WriteLine("map_Ka {0}", mat.map_Ka);
-				if ( mat.map_Kd != null && mat.map_Kd != "" )
-					w.WriteLine("map_Kd {0}", mat.map_Kd);
-				if ( mat.map_Ks != null && mat.map_Ks != "" )
-					w.WriteLine("map_Ks {0}", mat.map_Ks);
-				if ( mat.map_Ke != null && mat.map_Ke != "" )
-					w.WriteLine("map_Ke {0}", mat.map_Ke);
-				if ( mat.map_d != null && mat.map_d != "" )
-					w.WriteLine("map_d {0}", mat.map_d);
-				if ( mat.map_Ns != null && mat.map_Ns != "" )
-					w.WriteLine("map_Ns {0}", mat.map_Ns);
+            Stream stream = OpenStreamF(options.MaterialFilePath);
+            if (stream == null)
+                return new IOWriteResult(IOCode.FileAccessError, "Could not open file " + options.MaterialFilePath + " for writing");
+
+
+            try { 
+                StreamWriter w = new StreamWriter(stream);
+
+			    foreach ( GenericMaterial gmat in vMaterials ) {
+				    if ( gmat is OBJMaterial == false )
+					    continue;
+				    OBJMaterial mat = gmat as OBJMaterial;
+
+				    w.WriteLine("newmtl {0}", mat.name);
+				    if ( mat.Ka != GenericMaterial.Invalid )
+					    w.WriteLine("Ka {0} {1} {2}", mat.Ka.x, mat.Ka.y, mat.Ka.z);
+				    if ( mat.Kd != GenericMaterial.Invalid)
+					    w.WriteLine("Kd {0} {1} {2}", mat.Kd.x, mat.Kd.y, mat.Kd.z);
+				    if ( mat.Ks != GenericMaterial.Invalid )
+					    w.WriteLine("Ks {0} {1} {2}", mat.Ks.x, mat.Ks.y, mat.Ks.z);
+				    if ( mat.Ke != GenericMaterial.Invalid )
+					    w.WriteLine("Ke {0} {1} {2}", mat.Ke.x, mat.Ke.y, mat.Ke.z);
+				    if ( mat.Tf != GenericMaterial.Invalid )
+					    w.WriteLine("Tf {0} {1} {2}", mat.Tf.x, mat.Tf.y, mat.Tf.z);
+				    if ( mat.d != Single.MinValue )
+					    w.WriteLine("d {0}", mat.d);
+				    if ( mat.Ns != Single.MinValue )
+					    w.WriteLine("Ns {0}", mat.Ns);
+				    if ( mat.Ni != Single.MinValue )
+					    w.WriteLine("Ni {0}", mat.Ni);
+				    if ( mat.sharpness != Single.MinValue )
+					    w.WriteLine("sharpness {0}", mat.sharpness);
+				    if ( mat.illum != -1 )
+					    w.WriteLine("illum {0}", mat.illum);
+
+				    if ( mat.map_Ka != null && mat.map_Ka != "" )
+					    w.WriteLine("map_Ka {0}", mat.map_Ka);
+				    if ( mat.map_Kd != null && mat.map_Kd != "" )
+					    w.WriteLine("map_Kd {0}", mat.map_Kd);
+				    if ( mat.map_Ks != null && mat.map_Ks != "" )
+					    w.WriteLine("map_Ks {0}", mat.map_Ks);
+				    if ( mat.map_Ke != null && mat.map_Ke != "" )
+					    w.WriteLine("map_Ke {0}", mat.map_Ke);
+				    if ( mat.map_d != null && mat.map_d != "" )
+					    w.WriteLine("map_d {0}", mat.map_d);
+				    if ( mat.map_Ns != null && mat.map_Ns != "" )
+					    w.WriteLine("map_Ns {0}", mat.map_Ns);
 				
-				if ( mat.bump != null && mat.bump != "" )
-					w.WriteLine("bump {0}", mat.bump);
-				if ( mat.disp != null && mat.disp != "" )
-					w.WriteLine("disp {0}", mat.disp);				
-				if ( mat.decal != null && mat.decal != "" )
-					w.WriteLine("decal {0}", mat.decal);
-				if ( mat.refl != null && mat.refl != "" )
-					w.WriteLine("refl {0}", mat.refl);
-			}
-
-			w.Close();
+				    if ( mat.bump != null && mat.bump != "" )
+					    w.WriteLine("bump {0}", mat.bump);
+				    if ( mat.disp != null && mat.disp != "" )
+					    w.WriteLine("disp {0}", mat.disp);				
+				    if ( mat.decal != null && mat.decal != "" )
+					    w.WriteLine("decal {0}", mat.decal);
+				    if ( mat.refl != null && mat.refl != "" )
+					    w.WriteLine("refl {0}", mat.refl);
+			    }
+
+            } finally {
+                CloseStreamF(stream);
+            }
 
 			return IOWriteResult.Ok;
 		}
diff --git a/io/STLReader.cs b/io/STLReader.cs
index bacc6966..091561b6 100644
--- a/io/STLReader.cs
+++ b/io/STLReader.cs
@@ -8,23 +8,63 @@
 
 namespace g3
 {
+    /// <summary>
+    /// Read ASCII/Binary STL file and produce set of meshes.
+    /// 
+    /// Since STL is just a list of disconnected triangles, by default we try to
+    /// merge vertices together. Use .RebuildStrategy to disable this and/or configure
+    /// which algorithm is used. If you are using via StandardMeshReader, you can add
+    /// .StrategyFlag to ReadOptions.CustomFlags to set this flag.
+    /// 
+    /// TODO: document welding strategies. There is no "best" one, they all fail
+    /// in some cases, because STL is a stupid and horrible format.
+    /// 
+    /// STL Binary supports a per-triangle short-int that is usually used to specify color.
+    /// However since we do not support per-triangle color in DMesh3, this color
+    /// cannot be directly used. Instead of hardcoding behavior, we return the list of shorts
+    /// if requested via IMeshBuilder Metadata. Set .WantPerTriAttribs=true or attach flag .PerTriAttribFlag.
+    /// After the read finishes you can get the face color list via:
+    ///    DVector<short> colors = Builder.Metadata[0][STLReader.PerTriAttribMetadataName] as DVector<short>;
+    /// (for DMesh3Builder, which is the only builder that supports Metadata)
+    /// </summary>
     public class STLReader : IMeshReader
     {
 
         public enum Strategy
         {
-            NoProcessing = 0,
-            IdenticalVertexWeld = 1,
-            TolerantVertexWeld = 2,
+            NoProcessing = 0,           // return triangle soup
+            IdenticalVertexWeld = 1,    // merge identical vertices. Logically sensible but doesn't always work on ASCII STL.
+            TolerantVertexWeld = 2,     // merge vertices within .WeldTolerance
 
-            AutoBestResult = 3
+            AutoBestResult = 3          // try identical weld first, if there are holes then try tolerant weld, and return "best" result
+                                        // ("best" is not well-defined...)
         }
+
+        /// <summary>
+        /// Which algorithm is used to try to reconstruct mesh topology from STL triangle soup
+        /// </summary>
         public Strategy RebuildStrategy = Strategy.AutoBestResult;
 
+        /// <summary>
+        /// Vertices within this distance are considered "the same" by welding strategies.
+        /// </summary>
         public double WeldTolerance = MathUtil.ZeroTolerancef;
 
 
-        // connect to this to get warning messages
+        /// <summary>
+        /// Binary STL supports per-triangle integer attribute, which is often used
+        /// to store face colors. If this flag is true, we will attach these face
+        /// colors to the returned mesh via IMeshBuilder.AppendMetaData
+        /// </summary>
+        public bool WantPerTriAttribs = false;
+
+        /// <summary>
+        /// name argument passed to IMeshBuilder.AppendMetaData
+        /// </summary>
+        public static string PerTriAttribMetadataName = "tri_attrib";
+
+
+        /// <summary> connect to this event to get warning messages </summary>
 		public event ParsingMessagesHandler warningEvent;
 
 
@@ -33,12 +73,21 @@ public enum Strategy
 
 
 
+        /// <summary> ReadOptions.CustomFlags flag for configuring .RebuildStrategy </summary>
         public const string StrategyFlag = "-stl-weld-strategy";
+
+        /// <summary> ReadOptions.CustomFlags flag for configuring .WantPerTriAttribs </summary>
+        public const string PerTriAttribFlag = "-want-tri-attrib";
+
+
         void ParseArguments(CommandArgumentSet args)
         {
             if ( args.Integers.ContainsKey(StrategyFlag) ) {
                 RebuildStrategy = (Strategy)args.Integers[StrategyFlag];
             }
+            if (args.Flags.ContainsKey(PerTriAttribFlag)) {
+                WantPerTriAttribs = true;
+            }
         }
 
 
@@ -48,6 +97,7 @@ protected class STLSolid
         {
             public string Name;
             public DVectorArray3f Vertices = new DVectorArray3f();
+            public DVector<short> TriAttribs = null;
         }
 
 
@@ -88,6 +138,8 @@ public IOReadResult Read(BinaryReader reader, ReadOptions options, IMeshBuilder
             stl_triangle tmp = new stl_triangle();
             Type tri_type = tmp.GetType();
 
+            DVector<short> tri_attribs = new DVector<short>();
+
             try {
                 for (int i = 0; i < totalTris; ++i) {
                     byte[] tri_bytes = reader.ReadBytes(50);
@@ -100,6 +152,7 @@ public IOReadResult Read(BinaryReader reader, ReadOptions options, IMeshBuilder
                     append_vertex(tri.ax, tri.ay, tri.az);
                     append_vertex(tri.bx, tri.by, tri.bz);
                     append_vertex(tri.cx, tri.cy, tri.cz);
+                    tri_attribs.Add(tri.attrib);
                 }
 
             } catch (Exception e) {
@@ -108,6 +161,9 @@ public IOReadResult Read(BinaryReader reader, ReadOptions options, IMeshBuilder
 
             Marshal.FreeHGlobal(bufptr);
 
+            if (Objects.Count == 1)
+                Objects[0].TriAttribs = tri_attribs;
+
             foreach (STLSolid solid in Objects)
                 BuildMesh(solid, builder);
 
@@ -219,6 +275,9 @@ protected virtual void BuildMesh(STLSolid solid, IMeshBuilder builder)
             } else {
                 BuildMesh_NoMerge(solid, builder);
             }
+
+            if (WantPerTriAttribs && solid.TriAttribs != null && builder.SupportsMetaData)
+                builder.AppendMetaData(PerTriAttribMetadataName, solid.TriAttribs);
         }
 
 
diff --git a/io/SVGWriter.cs b/io/SVGWriter.cs
index 8fe0f15d..ed6a0115 100644
--- a/io/SVGWriter.cs
+++ b/io/SVGWriter.cs
@@ -244,6 +244,36 @@ public static void QuickWrite(List<GeneralPolygon2d> polygons, string sPath, dou
         }
 
 
+        public static void QuickWrite(DGraph2 graph, string sPath, double line_width = 1)
+        {
+            SVGWriter writer = new SVGWriter();
+            Style style = SVGWriter.Style.Outline("black", (float)line_width);
+            writer.AddGraph(graph, style);
+            writer.Write(sPath);
+        }
+
+        public static void QuickWrite(List<GeneralPolygon2d> polygons1, string color1, float width1,
+		                              List<GeneralPolygon2d> polygons2, string color2, float width2,
+		                              string sPath)
+		{
+			SVGWriter writer = new SVGWriter();
+			Style style1 = SVGWriter.Style.Outline(color1, width1);
+			Style style1_holes = SVGWriter.Style.Outline(color1, width1/2);
+			foreach (GeneralPolygon2d poly in polygons1) {
+				writer.AddPolygon(poly.Outer, style1);
+				foreach (var hole in poly.Holes)
+					writer.AddPolygon(hole, style1_holes);
+			}
+			Style style2 = SVGWriter.Style.Outline(color2, width2);
+			Style style2_holes = SVGWriter.Style.Outline(color2, width2 / 2);
+			foreach (GeneralPolygon2d poly in polygons2) {
+				writer.AddPolygon(poly.Outer, style2);
+				foreach (var hole in poly.Holes)
+					writer.AddPolygon(hole, style2_holes);
+			}
+			writer.Write(sPath);
+		}
+
 
 
 
diff --git a/io/StandardMeshReader.cs b/io/StandardMeshReader.cs
index 3b903e2d..d1e5b258 100644
--- a/io/StandardMeshReader.cs
+++ b/io/StandardMeshReader.cs
@@ -305,11 +305,9 @@ public IOReadResult ReadFile(string sFilename, IMeshBuilder builder, ReadOptions
         public IOReadResult ReadFile(Stream stream, IMeshBuilder builder, ReadOptions options, ParsingMessagesHandler messages)
         {
             // detect binary STL
-            BinaryReader binReader = new BinaryReader(stream);
-            byte[] header = binReader.ReadBytes(80);
-            bool bIsBinary = false;
-
-            bIsBinary = Util.IsBinaryStream(stream, 500);
+            //BinaryReader binReader = new BinaryReader(stream);
+            //byte[] header = binReader.ReadBytes(80);
+            bool bIsBinary = Util.IsBinaryStream(stream, 500);
 
             // [RMS] Thingi10k includes some files w/ unicode string in ascii header...
             //   How can we detect this? can we check that each character is a character?
diff --git a/io/StandardMeshWriter.cs b/io/StandardMeshWriter.cs
index 70db0f75..088fcc38 100644
--- a/io/StandardMeshWriter.cs
+++ b/io/StandardMeshWriter.cs
@@ -6,8 +6,19 @@
 
 namespace g3
 {
+    /// <summary>
+    /// Writes various mesh file formats. Format is determined from extension. Currently supports:
+    ///   * .obj : Wavefront OBJ Format https://en.wikipedia.org/wiki/Wavefront_.obj_file
+    ///   * .stl : ascii and binary STL formats https://en.wikipedia.org/wiki/STL_(file_format) 
+    ///   * .off : OFF format https://en.wikipedia.org/wiki/OFF_(file_format)
+    ///   * .g3mesh : internal binary format for packed DMesh3 objects
+    ///   
+    /// Each of these is implemented in a separate Writer class, eg OBJWriter, STLWriter, etc
+    /// 
+    /// </summary>
     public class StandardMeshWriter : IDisposable
     {
+
         /// <summary>
         /// If the mesh format we are writing is text, then the OS will write in the number style
         /// of the current language. So in Germany, numbers are written 1,00 instead of 1.00, for example.
@@ -16,6 +27,28 @@ public class StandardMeshWriter : IDisposable
         public bool WriteInvariantCulture = true;
 
 
+
+        /// <summary>
+        /// By default we write to files, but if you would like to write to some other
+        /// Stream type (eg MemoryStream), you can replace this function.  
+        /// We also pass this function down into the XYZWriter classes
+        /// that need to write additional files (eg OBJ mesh)
+        /// </summary>
+        public Func<string, Stream> OpenStreamF = (sFilename) => {
+            return File.Open(sFilename, FileMode.Create);
+        };
+
+        /// <summary>
+        /// called on Streams returned by OpenStreamF when we are done with them.
+        /// </summary>
+        public Action<Stream> CloseStreamF = (stream) => {
+            stream.Close();
+            stream.Dispose();
+        };
+
+
+
+
         public void Dispose()
         {
         }
@@ -45,7 +78,6 @@ public IOWriteResult Write(string sFilename, List<WriteMesh> vMeshes, WriteOptio
         {
             Func<string, List<WriteMesh>, WriteOptions, IOWriteResult> writeFunc = null;
 
-
             string sExtension = Path.GetExtension(sFilename);
             if (sExtension.Equals(".obj", StringComparison.OrdinalIgnoreCase))
                 writeFunc = Write_OBJ;
@@ -89,64 +121,84 @@ public IOWriteResult Write(string sFilename, List<WriteMesh> vMeshes, WriteOptio
 
         IOWriteResult Write_OBJ(string sFilename, List<WriteMesh> vMeshes, WriteOptions options)
         {
-            StreamWriter w = new StreamWriter(sFilename);
-            if (w.BaseStream == null)
+            Stream stream = OpenStreamF(sFilename);
+            if ( stream == null )
                 return new IOWriteResult(IOCode.FileAccessError, "Could not open file " + sFilename + " for writing");
 
-            OBJWriter writer = new OBJWriter();
-            var result = writer.Write(w, vMeshes, options);
-            w.Close();
-            return result;
+            try {
+                StreamWriter w = new StreamWriter(stream);
+                OBJWriter writer = new OBJWriter() {
+                    OpenStreamF = this.OpenStreamF,
+                    CloseStreamF = this.CloseStreamF
+                };
+                var result = writer.Write(w, vMeshes, options);
+                w.Flush();
+                return result;
+            } finally {
+                CloseStreamF(stream);
+            }
         }
 
 
         IOWriteResult Write_OFF(string sFilename, List<WriteMesh> vMeshes, WriteOptions options)
         {
-            StreamWriter w = new StreamWriter(sFilename);
-            if (w.BaseStream == null)
+            Stream stream = OpenStreamF(sFilename);
+            if (stream == null)
                 return new IOWriteResult(IOCode.FileAccessError, "Could not open file " + sFilename + " for writing");
 
-            OFFWriter writer = new OFFWriter();
-            var result = writer.Write(w, vMeshes, options);
-            w.Close();
-            return result;
+            try {
+                StreamWriter w = new StreamWriter(stream);
+                OFFWriter writer = new OFFWriter();
+                var result = writer.Write(w, vMeshes, options);
+                w.Flush();
+                return result;
+            } finally {
+                CloseStreamF(stream);
+            }
         }
 
 
         IOWriteResult Write_STL(string sFilename, List<WriteMesh> vMeshes, WriteOptions options)
         {
-            if (options.bWriteBinary) {
-                FileStream file_stream = File.Open(sFilename, FileMode.Create);
-                BinaryWriter w = new BinaryWriter(file_stream);
-                if (w.BaseStream == null)
-                    return new IOWriteResult(IOCode.FileAccessError, "Could not open file " + sFilename + " for writing");
-                STLWriter writer = new STLWriter();
-                var result = writer.Write(w, vMeshes, options);
-                w.Close();
-                return result;
+            Stream stream = OpenStreamF(sFilename);
+            if (stream == null)
+                return new IOWriteResult(IOCode.FileAccessError, "Could not open file " + sFilename + " for writing");
 
-            } else {
-                StreamWriter w = new StreamWriter(sFilename);
-                if (w.BaseStream == null)
-                    return new IOWriteResult(IOCode.FileAccessError, "Could not open file " + sFilename + " for writing");
-                STLWriter writer = new STLWriter();
-                var result = writer.Write(w, vMeshes, options);
-                w.Close();
-                return result;
+            try { 
+                if (options.bWriteBinary) {
+                    BinaryWriter w = new BinaryWriter(stream);
+                    STLWriter writer = new STLWriter();
+                    var result = writer.Write(w, vMeshes, options);
+                    w.Flush();
+                    return result;
+                } else {
+                    StreamWriter w = new StreamWriter(stream);
+                    STLWriter writer = new STLWriter();
+                    var result = writer.Write(w, vMeshes, options);
+                    w.Flush();
+                    return result;
+                }
+            } finally {
+                CloseStreamF(stream);
             }
         }
 
 
         IOWriteResult Write_G3Mesh(string sFilename, List<WriteMesh> vMeshes, WriteOptions options)
         {
-            FileStream file_stream = File.Open(sFilename, FileMode.Create);
-            BinaryWriter w = new BinaryWriter(file_stream);
-            if (w.BaseStream == null)
+            Stream stream = OpenStreamF(sFilename);
+            if (stream == null)
                 return new IOWriteResult(IOCode.FileAccessError, "Could not open file " + sFilename + " for writing");
-            BinaryG3Writer writer = new BinaryG3Writer();
-            var result = writer.Write(w, vMeshes, options);
-            w.Close();
-            return result;
+
+            try {
+                BinaryWriter w = new BinaryWriter(stream);
+                BinaryG3Writer writer = new BinaryG3Writer();
+                var result = writer.Write(w, vMeshes, options);
+                w.Flush();
+                return result;
+            } finally {
+                CloseStreamF(stream);
+            }
         }
 
 
diff --git a/io/gSerialization.cs b/io/gSerialization.cs
index 5337371c..eaac6169 100644
--- a/io/gSerialization.cs
+++ b/io/gSerialization.cs
@@ -561,6 +561,109 @@ public static void Restore(ref string[] s, BinaryReader reader)
             for (int i = 0; i < N; ++i)
                 Restore(ref s[i], reader);
         }
+    }
+
+
+
+    /// <summary>
+    /// Utility class that is intended to support things like writing and reading
+    /// test cases, etc. You can write out a test case in a single line, eg
+    ///    SimpleStore.Store(path, new object[] { TestMesh, VertexList, PlaneNormal, ... })
+    /// The object list will be binned into the relevant sublists automatically.
+    /// Then you can load this data via:
+    ///    SimpleStore s = SimpleStore.Restore(path)
+    /// </summary>
+    public class SimpleStore
+    {
+        // only ever append to this list!
+        public List<DMesh3> Meshes = new List<DMesh3>();
+        public List<Vector3d> Points = new List<Vector3d>();
+        public List<string> Strings = new List<string>();
+        public List<List<int>> IntLists = new List<List<int>>();
+
+        public SimpleStore()
+        {
+        }
+
+        public SimpleStore(object[] objs)
+        {
+            Add(objs);
+        }
+
+        public void Add(object[] objs)
+        {
+            foreach (object o in objs) {
+                if (o is DMesh3)
+                    Meshes.Add(o as DMesh3);
+                else if (o is string)
+                    Strings.Add(o as String);
+                else if (o is List<int>)
+                    IntLists.Add(o as List<int>);
+                else if (o is IEnumerable<int>)
+                    IntLists.Add(new List<int>(o as IEnumerable<int>));
+                else if (o is Vector3d)
+                    Points.Add((Vector3d)o);
+                else
+                    throw new Exception("SimpleStore: unknown type " + o.GetType().ToString());
+            }
+        }
+
+
+        public static void Store(string sPath, object[] objs)
+        {
+            SimpleStore s = new SimpleStore(objs);
+            Store(sPath, s);
+        }
+
+        public static void Store(string sPath, SimpleStore s)
+        {
+            using (FileStream stream = new FileStream(sPath, FileMode.Create)) {
+                using (BinaryWriter w = new BinaryWriter(stream)) {
+                    w.Write(s.Meshes.Count);
+                    for (int k = 0; k < s.Meshes.Count; ++k)
+                        gSerialization.Store(s.Meshes[k], w);
+                    w.Write(s.Points.Count);
+                    for (int k = 0; k < s.Points.Count; ++k)
+                        gSerialization.Store(s.Points[k], w);
+                    w.Write(s.Strings.Count);
+                    for (int k = 0; k < s.Strings.Count; ++k)
+                        gSerialization.Store(s.Strings[k], w);
+
+                    w.Write(s.IntLists.Count);
+                    for (int k = 0; k < s.IntLists.Count; ++k)
+                        gSerialization.Store(s.IntLists[k], w);
+                }
+            }
+        }
+
+
+        public static SimpleStore Restore(string sPath)
+        {
+            SimpleStore s = new SimpleStore();
+            using (FileStream stream = new FileStream(sPath, FileMode.Open)) {
+                using (BinaryReader r = new BinaryReader(stream)) {
+                    int nMeshes = r.ReadInt32();
+                    for ( int k = 0; k < nMeshes; ++k ) {
+                        DMesh3 m = new DMesh3(); gSerialization.Restore(m, r); s.Meshes.Add(m);
+                    }
+                    int nPoints = r.ReadInt32();
+                    for (int k = 0; k < nPoints; ++k) {
+                        Vector3d v = Vector3d.Zero; gSerialization.Restore(ref v, r); s.Points.Add(v);
+                    }
+                    int nStrings = r.ReadInt32();
+                    for (int k = 0; k < nStrings; ++k) {
+                        string str = null; gSerialization.Restore(ref str, r); s.Strings.Add(str);
+                    }
+                    int nIntLists = r.ReadInt32();
+                    for (int k = 0; k < nIntLists; ++k) {
+                        List<int> l = new List<int>(); gSerialization.Restore(l, r); s.IntLists.Add(l);
+                    }
+                }
+            }
+            return s;
+        }
 
     }
+
+
 }
diff --git a/math/AxisAlignedBox2f.cs b/math/AxisAlignedBox2f.cs
index b35104e6..def2f86f 100644
--- a/math/AxisAlignedBox2f.cs
+++ b/math/AxisAlignedBox2f.cs
@@ -1,5 +1,9 @@
 using System;
 
+#if G3_USING_UNITY
+using UnityEngine;
+#endif
+
 namespace g3
 {
     public struct AxisAlignedBox2f
@@ -256,5 +260,18 @@ public override string ToString() {
         }
 
 
+#if G3_USING_UNITY
+        public static implicit operator AxisAlignedBox2f(UnityEngine.Rect b)
+        {
+            return new AxisAlignedBox2f(b.min, b.max);
+        }
+        public static implicit operator UnityEngine.Rect(AxisAlignedBox2f b)
+        {
+            Rect ub = new Rect();
+            ub.min = b.Min; ub.max = b.Max;
+            return ub;
+        }
+#endif
+
     }
 }
diff --git a/math/AxisAlignedBox3d.cs b/math/AxisAlignedBox3d.cs
index 8644f1cd..021bdd60 100644
--- a/math/AxisAlignedBox3d.cs
+++ b/math/AxisAlignedBox3d.cs
@@ -24,11 +24,17 @@ public AxisAlignedBox3d(double xmin, double ymin, double zmin, double xmax, doub
             Max = new Vector3d(xmax, ymax, zmax);
         }
 
+		/// <summary>
+		/// init box [0,size] x [0,size] x [0,size]
+		/// </summary>
         public AxisAlignedBox3d(double fCubeSize) {
             Min = new Vector3d(0, 0, 0);
             Max = new Vector3d(fCubeSize, fCubeSize, fCubeSize);
         }
 
+		/// <summary>
+		/// Init box [0,width] x [0,height] x [0,depth]
+		/// </summary>
         public AxisAlignedBox3d(double fWidth, double fHeight, double fDepth) {
             Min = new Vector3d(0, 0, 0);
             Max = new Vector3d(fWidth, fHeight, fDepth);
@@ -94,7 +100,6 @@ public Vector3d Center {
             get { return new Vector3d(0.5 * (Min.x + Max.x), 0.5 * (Min.y + Max.y), 0.5 * (Min.z + Max.z)); }
         }
 
-
         public static bool operator ==(AxisAlignedBox3d a, AxisAlignedBox3d b) {
             return a.Min == b.Min && a.Max == b.Max;
         }
@@ -132,6 +137,17 @@ public Vector3d Corner(int i)
             return new Vector3d(x, y, z);
         }
 
+        /// <summary>
+        /// Returns point on face/edge/corner. For each coord value neg==min, 0==center, pos==max
+        /// </summary>
+        public Vector3d Point(int xi, int yi, int zi)
+        {
+            double x = (xi < 0) ? Min.x : ((xi == 0) ? (0.5*(Min.x + Max.x)) : Max.x);
+            double y = (yi < 0) ? Min.y : ((yi == 0) ? (0.5*(Min.y + Max.y)) : Max.y);
+            double z = (zi < 0) ? Min.z : ((zi == 0) ? (0.5*(Min.z + Max.z)) : Max.z);
+            return new Vector3d(x, y, z);
+        }
+
 
         // TODO
         ////! 0 == bottom-left, 1 = bottom-right, 2 == top-right, 3 == top-left
@@ -144,13 +160,38 @@ public void Expand(double fRadius) {
             Min.x -= fRadius; Min.y -= fRadius; Min.z -= fRadius;
             Max.x += fRadius; Max.y += fRadius; Max.z += fRadius;
         }
+
+        //! return this box expanded by radius
+        public AxisAlignedBox3d Expanded(double fRadius) {
+            return new AxisAlignedBox3d(
+                Min.x - fRadius, Min.y - fRadius, Min.z - fRadius,
+                Max.x + fRadius, Max.y + fRadius, Max.z + fRadius);
+        }
+
         //! value is added to min and subtracted from max
         public void Contract(double fRadius) {
-            Min.x += fRadius; Min.y += fRadius; Min.z += fRadius;
-            Max.x -= fRadius; Max.y -= fRadius; Max.z -= fRadius;
+            double w = 2 * fRadius;
+            if ( w > Max.x-Min.x ) { Min.x = Max.x = 0.5 * (Min.x + Max.x); }
+                else { Min.x += fRadius; Max.x -= fRadius; }
+            if ( w > Max.y-Min.y ) { Min.y = Max.y = 0.5 * (Min.y + Max.y); }
+                else { Min.y += fRadius; Max.y -= fRadius; }
+            if ( w > Max.z-Min.z ) { Min.z = Max.z = 0.5 * (Min.z + Max.z); }
+                else { Min.z += fRadius; Max.z -= fRadius; }
+        }
+
+        //! return this box expanded by radius
+        public AxisAlignedBox3d Contracted(double fRadius) {
+            AxisAlignedBox3d result = new AxisAlignedBox3d(
+                Min.x + fRadius, Min.y + fRadius, Min.z + fRadius,
+                Max.x - fRadius, Max.y - fRadius, Max.z - fRadius);
+            if (result.Min.x > result.Max.x) { result.Min.x = result.Max.x = 0.5 * (Min.x + Max.x); }
+            if (result.Min.y > result.Max.y) { result.Min.y = result.Max.y = 0.5 * (Min.y + Max.y); }
+            if (result.Min.z > result.Max.z) { result.Min.z = result.Max.z = 0.5 * (Min.z + Max.z); }
+            return result;
         }
 
-       public void Scale(double sx, double sy, double sz)
+
+        public void Scale(double sx, double sy, double sz)
         {
             Vector3d c = Center;
             Vector3d e = Extents; e.x *= sx; e.y *= sy; e.z *= sz;
@@ -167,6 +208,15 @@ public void Contain(Vector3d v) {
             Max.z = Math.Max(Max.z, v.z);
         }
 
+        public void Contain(ref Vector3d v) {
+            Min.x = Math.Min(Min.x, v.x);
+            Min.y = Math.Min(Min.y, v.y);
+            Min.z = Math.Min(Min.z, v.z);
+            Max.x = Math.Max(Max.x, v.x);
+            Max.y = Math.Max(Max.y, v.y);
+            Max.z = Math.Max(Max.z, v.z);
+        }
+
         public void Contain(AxisAlignedBox3d box) {
             Min.x = Math.Min(Min.x, box.Min.x);
             Min.y = Math.Min(Min.y, box.Min.y);
@@ -176,6 +226,15 @@ public void Contain(AxisAlignedBox3d box) {
             Max.z = Math.Max(Max.z, box.Max.z);
         }
 
+        public void Contain(ref AxisAlignedBox3d box) {
+            Min.x = Math.Min(Min.x, box.Min.x);
+            Min.y = Math.Min(Min.y, box.Min.y);
+            Min.z = Math.Min(Min.z, box.Min.z);
+            Max.x = Math.Max(Max.x, box.Max.x);
+            Max.y = Math.Max(Max.y, box.Max.y);
+            Max.z = Math.Max(Max.z, box.Max.z);
+        }
+
         public AxisAlignedBox3d Intersect(AxisAlignedBox3d box) {
             AxisAlignedBox3d intersect = new AxisAlignedBox3d(
                 Math.Max(Min.x, box.Min.x), Math.Max(Min.y, box.Min.y), Math.Max(Min.z, box.Min.z),
@@ -192,6 +251,19 @@ public bool Contains(Vector3d v) {
             return (Min.x <= v.x) && (Min.y <= v.y) && (Min.z <= v.z)
                 && (Max.x >= v.x) && (Max.y >= v.y) && (Max.z >= v.z);
         }
+        public bool Contains(ref Vector3d v) {
+            return (Min.x <= v.x) && (Min.y <= v.y) && (Min.z <= v.z)
+                && (Max.x >= v.x) && (Max.y >= v.y) && (Max.z >= v.z);
+        }
+
+        public bool Contains(AxisAlignedBox3d box2) {
+            return Contains(ref box2.Min) && Contains(ref box2.Max);
+        }
+        public bool Contains(ref AxisAlignedBox3d box2) {
+            return Contains(ref box2.Min) && Contains(ref box2.Max);
+        }
+
+
         public bool Intersects(AxisAlignedBox3d box) {
             return !((box.Max.x <= Min.x) || (box.Min.x >= Max.x) 
                 || (box.Max.y <= Min.y) || (box.Min.y >= Max.y)
diff --git a/math/AxisAlignedBox3f.cs b/math/AxisAlignedBox3f.cs
index c3f8b7ee..356483ac 100644
--- a/math/AxisAlignedBox3f.cs
+++ b/math/AxisAlignedBox3f.cs
@@ -150,6 +150,18 @@ public Vector3f Corner(int i)
         }
 
 
+        /// <summary>
+        /// Returns point on face/edge/corner. For each coord value neg==min, 0==center, pos==max
+        /// </summary>
+        public Vector3f Point(int xi, int yi, int zi)
+        {
+            float x = (xi < 0) ? Min.x : ((xi == 0) ? (0.5f * (Min.x + Max.x)) : Max.x);
+            float y = (yi < 0) ? Min.y : ((yi == 0) ? (0.5f * (Min.y + Max.y)) : Max.y);
+            float z = (zi < 0) ? Min.z : ((zi == 0) ? (0.5f * (Min.z + Max.z)) : Max.z);
+            return new Vector3f(x, y, z);
+        }
+
+
         //! value is subtracted from min and added to max
         public void Expand(float fRadius)
         {
diff --git a/math/AxisAlignedBox3i.cs b/math/AxisAlignedBox3i.cs
index d60cb05a..16ee2656 100644
--- a/math/AxisAlignedBox3i.cs
+++ b/math/AxisAlignedBox3i.cs
@@ -246,6 +246,27 @@ public Vector3i NearestPoint(Vector3i v)
         }
 
 
+        /// <summary>
+        /// Clamp v to grid bounds [min, max]
+        /// </summary>
+        public Vector3i ClampInclusive(Vector3i v) {
+            return new Vector3i(
+                MathUtil.Clamp(v.x, Min.x, Max.x),
+                MathUtil.Clamp(v.y, Min.y, Max.y),
+                MathUtil.Clamp(v.z, Min.z, Max.z));
+        }
+
+        /// <summary>
+        /// clamp v to grid bounds [min,max)
+        /// </summary>
+        public Vector3i ClampExclusive(Vector3i v) {
+            return new Vector3i(
+                MathUtil.Clamp(v.x, Min.x, Max.x-1),
+                MathUtil.Clamp(v.y, Min.y, Max.y-1),
+                MathUtil.Clamp(v.z, Min.z, Max.z-1));
+        }
+
+
 
         //! relative translation
         public void Translate(Vector3i vTranslate)
diff --git a/math/BoundsUtil.cs b/math/BoundsUtil.cs
index d471f91c..5ed01001 100644
--- a/math/BoundsUtil.cs
+++ b/math/BoundsUtil.cs
@@ -7,6 +7,14 @@ namespace g3
     public static class BoundsUtil
     {
 
+        public static AxisAlignedBox3d Bounds(IEnumerable<DMesh3> meshes) {
+            AxisAlignedBox3d bounds = AxisAlignedBox3d.Empty;
+            foreach (DMesh3 mesh in meshes)
+                bounds.Contain(mesh.CachedBounds);
+            return bounds;
+        }
+
+
         public static AxisAlignedBox3d Bounds(IPointSet source) {
             AxisAlignedBox3d bounds = AxisAlignedBox3d.Empty;
             foreach (int vid in source.VertexIndices())
@@ -28,7 +36,13 @@ public static AxisAlignedBox3d Bounds(ref Vector3d v0, ref Vector3d v1, ref Vect
             return box;
         }
 
-
+        public static AxisAlignedBox2d Bounds(ref Vector2d v0, ref Vector2d v1, ref Vector2d v2)
+        {
+            AxisAlignedBox2d box;
+            MathUtil.MinMax(v0.x, v1.x, v2.x, out box.Min.x, out box.Max.x);
+            MathUtil.MinMax(v0.y, v1.y, v2.y, out box.Min.y, out box.Max.y);
+            return box;
+        }
 
         // AABB of transformed AABB (corners)
         public static AxisAlignedBox3d Bounds(ref AxisAlignedBox3d boxIn, Func<Vector3d,Vector3d> TransformF)
@@ -43,7 +57,39 @@ public static AxisAlignedBox3d Bounds(ref AxisAlignedBox3d boxIn, Func<Vector3d,
         }
 
 
-		public static AxisAlignedBox3d Bounds<T>(IEnumerable<T> values, Func<T, Vector3d> PositionF)
+        public static AxisAlignedBox3d Bounds(IEnumerable<Vector3d> positions)
+        {
+            AxisAlignedBox3d box = AxisAlignedBox3d.Empty;
+            foreach (Vector3d v in positions)
+                box.Contain(v);
+            return box;
+        }
+        public static AxisAlignedBox3f Bounds(IEnumerable<Vector3f> positions)
+        {
+            AxisAlignedBox3f box = AxisAlignedBox3f.Empty;
+            foreach (Vector3f v in positions)
+                box.Contain(v);
+            return box;
+        }
+
+
+        public static AxisAlignedBox2d Bounds(IEnumerable<Vector2d> positions)
+        {
+            AxisAlignedBox2d box = AxisAlignedBox2d.Empty;
+            foreach (Vector2d v in positions)
+                box.Contain(v);
+            return box;
+        }
+        public static AxisAlignedBox2f Bounds(IEnumerable<Vector2f> positions)
+        {
+            AxisAlignedBox2f box = AxisAlignedBox2f.Empty;
+            foreach (Vector2f v in positions)
+                box.Contain(v);
+            return box;
+        }
+
+
+        public static AxisAlignedBox3d Bounds<T>(IEnumerable<T> values, Func<T, Vector3d> PositionF)
 		{
 			AxisAlignedBox3d box = AxisAlignedBox3d.Empty;
 			foreach ( T t in values )
@@ -59,6 +105,18 @@ public static AxisAlignedBox3f Bounds<T>(IEnumerable<T> values, Func<T, Vector3f
 		}
 
 
+        /// <summary>
+        /// compute axis-aligned bounds of set of points after transforming 
+        /// </summary>
+        public static AxisAlignedBox3d Bounds(IEnumerable<Vector3d> values, TransformSequence xform)
+        {
+            AxisAlignedBox3d box = AxisAlignedBox3d.Empty;
+            foreach (Vector3d v in values)
+                box.Contain(xform.TransformP(v));
+            return box;
+        }
+
+
         /// <summary>
         /// compute axis-aligned bounds of set of points after transforming into frame f
         /// </summary>
diff --git a/math/Box2.cs b/math/Box2.cs
index fdb95a31..08b59779 100644
--- a/math/Box2.cs
+++ b/math/Box2.cs
@@ -197,7 +197,7 @@ public Vector2d ClosestPoint(Vector2d v)
                 }
             }
 
-            return closest.x * AxisX + closest.y * AxisY;
+            return Center + closest.x*AxisX + closest.y*AxisY;
         }
 
 
diff --git a/math/Box3.cs b/math/Box3.cs
index 80fb62d2..f0ab9ad3 100644
--- a/math/Box3.cs
+++ b/math/Box3.cs
@@ -53,6 +53,13 @@ public Box3d(Frame3f frame, Vector3d extent)
             AxisZ = frame.Z;
             Extent = extent;
         }
+        public Box3d(Segment3d seg)
+        {
+            Center = seg.Center;
+            AxisZ = seg.Direction;
+            Vector3d.MakePerpVectors(ref AxisZ, out AxisX, out AxisY);
+            Extent = new Vector3d(0, 0, seg.Extent);
+        }
 
         public static readonly Box3d Empty = new Box3d(Vector3d.Zero);
         public static readonly Box3d UnitZeroCentered = new Box3d(Vector3d.Zero, 0.5 * Vector3d.One);
@@ -252,6 +259,168 @@ public void ScaleExtents(Vector3d s)
             Extent *= s;
         }
 
+
+
+
+        /// <summary>
+        /// Returns distance to box, or 0 if point is inside box.
+        /// Ported from WildMagic5 Wm5DistPoint3Box3.cpp
+        /// </summary>
+        public double DistanceSquared(Vector3d v)
+        {
+            // Work in the box's coordinate system.
+            v -= this.Center;
+
+            // Compute squared distance and closest point on box.
+            double sqrDistance = 0;
+            double delta;
+            Vector3d closest = new Vector3d();
+            int i;
+            for (i = 0; i < 3; ++i) {
+                closest[i] = Axis(i).Dot(ref v);
+                if (closest[i] < -Extent[i]) {
+                    delta = closest[i] + Extent[i];
+                    sqrDistance += delta * delta;
+                    closest[i] = -Extent[i];
+                } else if (closest[i] > Extent[i]) {
+                    delta = closest[i] - Extent[i];
+                    sqrDistance += delta * delta;
+                    closest[i] = Extent[i];
+                }
+            }
+
+            return sqrDistance;
+        }
+
+
+
+        /// <summary>
+        /// Returns distance to box, or 0 if point is inside box.
+        /// Ported from WildMagic5 Wm5DistPoint3Box3.cpp
+        /// </summary>
+        public Vector3d ClosestPoint(Vector3d v)
+        {
+            // Work in the box's coordinate system.
+            v -= this.Center;
+
+            // Compute squared distance and closest point on box.
+            double sqrDistance = 0;
+            double delta;
+            Vector3d closest = new Vector3d();
+            for (int i = 0; i < 3; ++i) {
+                closest[i] = Axis(i).Dot(ref v);
+                double extent = Extent[i];
+                if (closest[i] < -extent) {
+                    delta = closest[i] + extent;
+                    sqrDistance += delta * delta;
+                    closest[i] = -extent;
+                } else if (closest[i] > extent) {
+                    delta = closest[i] - extent;
+                    sqrDistance += delta * delta;
+                    closest[i] = extent;
+                }
+            }
+
+            return Center + closest.x*AxisX + closest.y*AxisY + closest.z*AxisZ;
+        }
+
+
+
+
+
+        // ported from WildMagic5 Wm5ContBox3.cpp::MergeBoxes
+        public static Box3d Merge(ref Box3d box0, ref Box3d box1)
+        {
+            // Construct a box that contains the input boxes.
+            Box3d box = new Box3d();
+
+            // The first guess at the box center.  This value will be updated later
+            // after the input box vertices are projected onto axes determined by an
+            // average of box axes.
+            box.Center = 0.5 * (box0.Center + box1.Center);
+
+            // A box's axes, when viewed as the columns of a matrix, form a rotation
+            // matrix.  The input box axes are converted to quaternions.  The average
+            // quaternion is computed, then normalized to unit length.  The result is
+            // the slerp of the two input quaternions with t-value of 1/2.  The result
+            // is converted back to a rotation matrix and its columns are selected as
+            // the merged box axes.
+            Quaterniond q0 = new Quaterniond(), q1 = new Quaterniond();
+            Matrix3d rot0 = new Matrix3d(ref box0.AxisX, ref box0.AxisY, ref box0.AxisZ, false);
+            q0.SetFromRotationMatrix(ref rot0);
+            Matrix3d rot1 = new Matrix3d(ref box1.AxisX, ref box1.AxisY, ref box1.AxisZ, false);
+            q1.SetFromRotationMatrix(ref rot1);
+            if (q0.Dot(q1) < 0) {
+                q1 = -q1;
+            }
+
+            Quaterniond q = q0 + q1;
+            double invLength = 1.0 / Math.Sqrt(q.Dot(q));
+            q = q * invLength;
+            Matrix3d q_mat = q.ToRotationMatrix();
+            box.AxisX = q_mat.Column(0); box.AxisY = q_mat.Column(1); box.AxisZ = q_mat.Column(2);  //q.ToRotationMatrix(box.Axis); 
+
+            // Project the input box vertices onto the merged-box axes.  Each axis
+            // D[i] containing the current center C has a minimum projected value
+            // min[i] and a maximum projected value max[i].  The corresponding end
+            // points on the axes are C+min[i]*D[i] and C+max[i]*D[i].  The point C
+            // is not necessarily the midpoint for any of the intervals.  The actual
+            // box center will be adjusted from C to a point C' that is the midpoint
+            // of each interval,
+            //   C' = C + sum_{i=0}^2 0.5*(min[i]+max[i])*D[i]
+            // The box extents are
+            //   e[i] = 0.5*(max[i]-min[i])
+
+            int i, j;
+            double dot;
+            Vector3d[] vertex = new Vector3d[8];
+            Vector3d pmin = Vector3d.Zero;
+            Vector3d pmax = Vector3d.Zero;
+
+            box0.ComputeVertices(vertex);
+            for (i = 0; i < 8; ++i) {
+                Vector3d diff = vertex[i] - box.Center;
+                for (j = 0; j < 3; ++j) {
+                    dot = box.Axis(j).Dot(ref diff);
+                    if (dot > pmax[j]) {
+                        pmax[j] = dot;
+                    } else if (dot < pmin[j]) {
+                        pmin[j] = dot;
+                    }
+                }
+            }
+
+            box1.ComputeVertices(vertex);
+            for (i = 0; i < 8; ++i) {
+                Vector3d diff = vertex[i] - box.Center;
+                for (j = 0; j < 3; ++j) {
+                    dot = box.Axis(j).Dot(ref diff);
+                    if (dot > pmax[j]) {
+                        pmax[j] = dot;
+                    } else if (dot < pmin[j]) {
+                        pmin[j] = dot;
+                    }
+                }
+            }
+
+            // [min,max] is the axis-aligned box in the coordinate system of the
+            // merged box axes.  Update the current box center to be the center of
+            // the new box.  Compute the extents based on the new center.
+            for (j = 0; j < 3; ++j) {
+                box.Center += (0.5*(pmax[j] + pmin[j])) * box.Axis(j);
+                box.Extent[j] = 0.5*(pmax[j] - pmin[j]);
+            }
+
+            return box;
+        }
+
+
+
+
+
+
+
+
         public static implicit operator Box3d(Box3f v)
         {
             return new Box3d(v.Center, v.AxisX, v.AxisY, v.AxisZ, v.Extent);
diff --git a/math/FastWindingMath.cs b/math/FastWindingMath.cs
new file mode 100644
index 00000000..1ff678f6
--- /dev/null
+++ b/math/FastWindingMath.cs
@@ -0,0 +1,306 @@
+using System;
+using System.Collections.Generic;
+
+namespace g3
+{
+ 
+
+    /// <summary>
+    /// Formulas for triangle winding number approximation
+    /// </summary>
+    public static class FastTriWinding
+    {
+        /// <summary>
+        /// precompute constant coefficients of triangle winding number approximation
+        /// p: 'center' of expansion for triangles (area-weighted centroid avg)
+        /// r: max distance from p to triangles
+        /// order1: first-order vector coeff
+        /// order2: second-order matrix coeff
+        /// triCache: optional precomputed triangle centroid/normal/area
+        /// </summary>
+        public static void ComputeCoeffs(DMesh3 mesh, IEnumerable<int> triangles, 
+            ref Vector3d p, ref double r, 
+            ref Vector3d order1, ref Matrix3d order2,
+            MeshTriInfoCache triCache = null )
+        {
+            p = Vector3d.Zero;
+            order1 = Vector3d.Zero;
+            order2 = Matrix3d.Zero;
+            r = 0;
+
+            // compute area-weighted centroid of triangles, we use this as the expansion point
+            Vector3d P0 = Vector3d.Zero, P1 = Vector3d.Zero, P2 = Vector3d.Zero;
+            double sum_area = 0;
+            foreach (int tid in triangles) {
+                if (triCache != null) {
+                    double area = triCache.Areas[tid];
+                    sum_area += area;
+                    p += area * triCache.Centroids[tid];
+                } else {
+                    mesh.GetTriVertices(tid, ref P0, ref P1, ref P2);
+                    double area = MathUtil.Area(ref P0, ref P1, ref P2);
+                    sum_area += area;
+                    p += area * ((P0 + P1 + P2) / 3.0);
+                }
+            }
+            p /= sum_area;
+
+            // compute first and second-order coefficients of FWN taylor expansion, as well as
+            // 'radius' value r, which is max dist from any tri vertex to p  
+            Vector3d n = Vector3d.Zero, c = Vector3d.Zero; double a = 0;
+            foreach ( int tid in triangles ) {
+                mesh.GetTriVertices(tid, ref P0, ref P1, ref P2);
+
+                if (triCache == null) {
+                    c = (1.0 / 3.0) * (P0 + P1 + P2);
+                    n = MathUtil.FastNormalArea(ref P0, ref P1, ref P2, out a);
+                } else {
+                    triCache.GetTriInfo(tid, ref n, ref a, ref c);
+                }
+
+                order1 += a * n;
+
+                Vector3d dcp = c - p;
+                order2 += a * new Matrix3d(ref dcp, ref n);
+
+                // this is just for return value...
+                double maxdist = MathUtil.Max(P0.DistanceSquared(ref p), P1.DistanceSquared(ref p), P2.DistanceSquared(ref p));
+                r = Math.Max(r, Math.Sqrt(maxdist));
+            }
+        }
+
+
+        /// <summary>
+        /// Evaluate first-order FWN approximation at point q, relative to center c
+        /// </summary>
+        public static double EvaluateOrder1Approx(ref Vector3d center, ref Vector3d order1Coeff, ref Vector3d q)
+        {
+            Vector3d dpq = (center - q);
+            double len = dpq.Length;
+
+            return (1.0 / MathUtil.FourPI) * order1Coeff.Dot(dpq / (len * len * len));
+        }
+
+
+        /// <summary>
+        /// Evaluate second-order FWN approximation at point q, relative to center c
+        /// </summary>
+        public static double EvaluateOrder2Approx(ref Vector3d center, ref Vector3d order1Coeff, ref Matrix3d order2Coeff, ref Vector3d q)
+        {
+            Vector3d dpq = (center - q);
+            double len = dpq.Length;
+            double len3 = len * len * len;
+            double fourPi_len3 = 1.0 / (MathUtil.FourPI * len3);
+
+            double order1 = fourPi_len3 * order1Coeff.Dot(ref dpq);
+
+            // second-order hessian \grad^2(G)
+            double c = - 3.0 / (MathUtil.FourPI * len3 * len * len);
+
+            // expanded-out version below avoids extra constructors
+            //Matrix3d xqxq = new Matrix3d(ref dpq, ref dpq);
+            //Matrix3d hessian = new Matrix3d(fourPi_len3, fourPi_len3, fourPi_len3) - c * xqxq;
+            Matrix3d hessian = new Matrix3d(
+                fourPi_len3 + c*dpq.x*dpq.x, c*dpq.x*dpq.y, c*dpq.x*dpq.z,
+                c*dpq.y*dpq.x, fourPi_len3 + c*dpq.y*dpq.y, c*dpq.y*dpq.z,
+                c*dpq.z*dpq.x, c*dpq.z*dpq.y, fourPi_len3 + c*dpq.z*dpq.z);
+
+            double order2 = order2Coeff.InnerProduct(ref hessian);
+
+            return order1 + order2;
+        }
+
+
+
+
+        // triangle-winding-number first-order approximation. 
+        // t is triangle, p is 'center' of cluster of dipoles, q is evaluation point
+        // (This is really just for testing)
+        public static double Order1Approx(ref Triangle3d t, ref Vector3d p, ref Vector3d xn, ref double xA, ref Vector3d q)
+        {
+            Vector3d at0 = xA * xn;
+
+            Vector3d dpq = (p - q);
+            double len = dpq.Length;
+            double len3 = len * len * len;
+
+            return (1.0 / MathUtil.FourPI) * at0.Dot(dpq / (len * len * len));
+        }
+
+
+        // triangle-winding-number second-order approximation
+        // t is triangle, p is 'center' of cluster of dipoles, q is evaluation point
+        // (This is really just for testing)
+        public static double Order2Approx(ref Triangle3d t, ref Vector3d p, ref Vector3d xn, ref double xA, ref Vector3d q)
+        {
+            Vector3d dpq = (p - q);
+
+            double len = dpq.Length;
+            double len3 = len * len * len;
+
+            // first-order approximation - integrated_normal_area * \grad(G)
+            double order1 = (xA / MathUtil.FourPI) * xn.Dot(dpq / len3);
+
+            // second-order hessian \grad^2(G)
+            Matrix3d xqxq = new Matrix3d(ref dpq, ref dpq);
+            xqxq *= 3.0 / (MathUtil.FourPI * len3 * len * len);
+            double diag = 1 / (MathUtil.FourPI * len3);
+            Matrix3d hessian = new Matrix3d(diag, diag, diag) - xqxq;
+
+            // second-order LHS - integrated second-order area matrix (formula 26)
+            Vector3d centroid = new Vector3d(
+                (t.V0.x + t.V1.x + t.V2.x) / 3.0, (t.V0.y + t.V1.y + t.V2.y) / 3.0, (t.V0.z + t.V1.z + t.V2.z) / 3.0);
+            Vector3d dcp = centroid - p;
+            Matrix3d o2_lhs = new Matrix3d(ref dcp, ref xn);
+            double order2 = xA * o2_lhs.InnerProduct(ref hessian);
+
+            return order1 + order2;
+        }
+    }
+
+
+
+
+    /// <summary>
+    /// Formulas for point-set winding number approximation
+    /// </summary>
+    public static class FastPointWinding
+    {
+        /// <summary>
+        /// precompute constant coefficients of point winding number approximation
+        /// pointAreas must be provided, and pointSet must have vertex normals!
+        /// p: 'center' of expansion for points (area-weighted point avg)
+        /// r: max distance from p to points
+        /// order1: first-order vector coeff
+        /// order2: second-order matrix coeff
+        /// </summary>
+        public static void ComputeCoeffs(
+            IPointSet pointSet, IEnumerable<int> points, double[] pointAreas,
+            ref Vector3d p, ref double r,
+            ref Vector3d order1, ref Matrix3d order2 )
+        {
+            if (pointSet.HasVertexNormals == false)
+                throw new Exception("FastPointWinding.ComputeCoeffs: point set does not have normals!");
+
+            p = Vector3d.Zero;
+            order1 = Vector3d.Zero;
+            order2 = Matrix3d.Zero;
+            r = 0;
+
+            // compute area-weighted centroid of points, we use this as the expansion point
+            double sum_area = 0;
+            foreach (int vid in points) {
+                sum_area += pointAreas[vid];
+                p += pointAreas[vid] * pointSet.GetVertex(vid);
+            }
+            p /= sum_area;
+
+            // compute first and second-order coefficients of FWN taylor expansion, as well as
+            // 'radius' value r, which is max dist from any tri vertex to p  
+            foreach (int vid in points) {
+                Vector3d p_i = pointSet.GetVertex(vid);
+                Vector3d n_i = pointSet.GetVertexNormal(vid);
+                double a_i = pointAreas[vid];
+
+                order1 += a_i * n_i;
+
+                Vector3d dcp = p_i - p;
+                order2 += a_i * new Matrix3d(ref dcp, ref n_i);
+
+                // this is just for return value...
+                r = Math.Max(r, p_i.Distance(p));
+            }
+        }
+
+
+        /// <summary>
+        /// Evaluate first-order FWN approximation at point q, relative to center c
+        /// </summary>
+        public static double EvaluateOrder1Approx(ref Vector3d center, ref Vector3d order1Coeff, ref Vector3d q)
+        {
+            Vector3d dpq = (center - q);
+            double len = dpq.Length;
+
+            return (1.0 / MathUtil.FourPI) * order1Coeff.Dot(dpq / (len * len * len));
+        }
+
+
+
+        /// <summary>
+        /// Evaluate second-order FWN approximation at point q, relative to center c
+        /// </summary>
+        public static double EvaluateOrder2Approx(ref Vector3d center, ref Vector3d order1Coeff, ref Matrix3d order2Coeff, ref Vector3d q)
+        {
+            Vector3d dpq = (center - q);
+            double len = dpq.Length;
+            double len3 = len * len * len;
+            double fourPi_len3 = 1.0 / (MathUtil.FourPI * len3);
+
+            double order1 = fourPi_len3 * order1Coeff.Dot(ref dpq);
+
+            // second-order hessian \grad^2(G)
+            double c = -3.0 / (MathUtil.FourPI * len3 * len * len);
+
+            // expanded-out version below avoids extra constructors
+            //Matrix3d xqxq = new Matrix3d(ref dpq, ref dpq);
+            //Matrix3d hessian = new Matrix3d(fourPi_len3, fourPi_len3, fourPi_len3) - c * xqxq;
+            Matrix3d hessian = new Matrix3d(
+                fourPi_len3 + c * dpq.x * dpq.x, c * dpq.x * dpq.y, c * dpq.x * dpq.z,
+                c * dpq.y * dpq.x, fourPi_len3 + c * dpq.y * dpq.y, c * dpq.y * dpq.z,
+                c * dpq.z * dpq.x, c * dpq.z * dpq.y, fourPi_len3 + c * dpq.z * dpq.z);
+
+            double order2 = order2Coeff.InnerProduct(ref hessian);
+
+            return order1 + order2;
+        }
+
+
+
+        public static double ExactEval(ref Vector3d x, ref Vector3d xn, double xA, ref Vector3d q)
+        {
+            Vector3d dv = (x - q);
+            double len = dv.Length;
+            return (xA / MathUtil.FourPI) * xn.Dot(dv / (len * len * len));
+        }
+
+        // point-winding-number first-order approximation. 
+        // x is dipole point, p is 'center' of cluster of dipoles, q is evaluation point
+        public static double Order1Approx(ref Vector3d x, ref Vector3d p, ref Vector3d xn, double xA, ref Vector3d q)
+        {
+            Vector3d dpq = (p - q);
+            double len = dpq.Length;
+            double len3 = len * len * len;
+
+            return (xA / MathUtil.FourPI) * xn.Dot(dpq / (len * len * len));
+        }
+
+
+        // point-winding-number second-order approximation
+        // x is dipole point, p is 'center' of cluster of dipoles, q is evaluation point
+        public static double Order2Approx(ref Vector3d x, ref Vector3d p, ref Vector3d xn, double xA, ref Vector3d q)
+        {
+            Vector3d dpq = (p - q);
+            Vector3d dxp = (x - p);
+
+            double len = dpq.Length;
+            double len3 = len * len * len;
+
+            // first-order approximation - area*normal*\grad(G)
+            double order1 = (xA / MathUtil.FourPI) * xn.Dot(dpq / len3);
+
+            // second-order hessian \grad^2(G)
+            Matrix3d xqxq = new Matrix3d(ref dpq, ref dpq);
+            xqxq *= 3.0 / (MathUtil.FourPI * len3 * len * len);
+            double diag = 1 / (MathUtil.FourPI * len3);
+            Matrix3d hessian = new Matrix3d(diag, diag, diag) - xqxq;
+
+            // second-order LHS area * \outer(x-p, normal)
+            Matrix3d o2_lhs = new Matrix3d(ref dxp, ref xn);
+            double order2 = xA * o2_lhs.InnerProduct(ref hessian);
+
+            return order1 + order2;
+        }
+    }
+
+
+}
diff --git a/math/Frame3f.cs b/math/Frame3f.cs
index c8fea33b..8d9e1732 100644
--- a/math/Frame3f.cs
+++ b/math/Frame3f.cs
@@ -184,6 +184,9 @@ public void ConstrainedAlignAxis(int nAxis, Vector3f vTo, Vector3f vAround)
             Rotate(rot);
         }
 
+        /// <summary>
+        /// 3D projection of point p onto frame-axis plane orthogonal to normal axis
+        /// </summary>
         public Vector3f ProjectToPlane(Vector3f p, int nNormal)
         {
             Vector3f d = p - origin;
@@ -191,6 +194,10 @@ public Vector3f ProjectToPlane(Vector3f p, int nNormal)
             return origin + (d - d.Dot(n) * n);
         }
 
+        /// <summary>
+        /// map from 2D coordinates in frame-axes plane perpendicular to normal axis, to 3D
+        /// [TODO] check that mapping preserves orientation?
+        /// </summary>
         public Vector3f FromPlaneUV(Vector2f v, int nPlaneNormalAxis)
         {
             Vector3f dv = new Vector3f(v[0], v[1], 0);
@@ -206,19 +213,38 @@ public Vector3f FromFrameP(Vector2f v, int nPlaneNormalAxis) {
             return FromPlaneUV(v, nPlaneNormalAxis);
         }
 
-        public Vector2f ToPlaneUV(Vector3f p, int nNormal = 2, int nAxis0 = 0, int nAxis1 = 1)
+
+        /// <summary>
+        /// Project p onto plane axes
+        /// [TODO] check that mapping preserves orientation?
+        /// </summary>
+        public Vector2f ToPlaneUV(Vector3f p, int nNormal)
         {
+            int nAxis0 = 0, nAxis1 = 1;
+            if (nNormal == 0)
+                nAxis0 = 2;
+            else if (nNormal == 1)
+                nAxis1 = 2;
             Vector3f d = p - origin;
             float fu = d.Dot(GetAxis(nAxis0));
             float fv = d.Dot(GetAxis(nAxis1));
             return new Vector2f(fu, fv);
         }
+        [System.Obsolete("Use explicit ToPlaneUV instead")]
+        public Vector2f ToPlaneUV(Vector3f p, int nNormal, int nAxis0 = -1, int nAxis1 = -1)
+        {
+            if (nAxis0 != -1 || nAxis1 != -1)
+                throw new Exception("[RMS] was this being used?");
+            return ToPlaneUV(p, nNormal);
+        }
 
 
+        ///<summary> distance from p to frame-axes-plane perpendicular to normal axis </summary>
         public float DistanceToPlane(Vector3f p, int nNormal)
         {
             return Math.Abs((p - origin).Dot(GetAxis(nNormal)));
         }
+        ///<summary> signed distance from p to frame-axes-plane perpendicular to normal axis </summary>
 		public float DistanceToPlaneSigned(Vector3f p, int nNormal)
 		{
 			return (p - origin).Dot(GetAxis(nNormal));
@@ -226,116 +252,162 @@ public float DistanceToPlaneSigned(Vector3f p, int nNormal)
 
 
         ///<summary> Map point *into* local coordinates of Frame </summary>
-		public Vector3f ToFrameP(Vector3f v)
-        {
-            v = v - this.origin;
-            v = Quaternionf.Inverse(this.rotation) * v;
-            return v;
+		public Vector3f ToFrameP(Vector3f v) {
+            v.x -= origin.x; v.y -= origin.y; v.z -= origin.z;
+            return rotation.InverseMultiply(ref v);
         }
         ///<summary> Map point *into* local coordinates of Frame </summary>
-        public Vector3d ToFrameP(Vector3d v)
-        {
-            v = v - this.origin;
-            v = Quaternionf.Inverse(this.rotation) * v;
-            return v;
+		public Vector3f ToFrameP(ref Vector3f v) {
+            Vector3f x = new Vector3f(v.x-origin.x, v.y-origin.y, v.z-origin.z);
+            return rotation.InverseMultiply(ref x);
+        }
+        ///<summary> Map point *into* local coordinates of Frame </summary>
+        public Vector3d ToFrameP(Vector3d v) {
+            v.x -= origin.x; v.y -= origin.y; v.z -= origin.z;
+            return rotation.InverseMultiply(ref v);
+        }
+        ///<summary> Map point *into* local coordinates of Frame </summary>
+        public Vector3d ToFrameP(ref Vector3d v) {
+            Vector3d x = new Vector3d(v.x - origin.x, v.y - origin.y, v.z - origin.z);
+            return rotation.InverseMultiply(ref x);
         }
         /// <summary> Map point *from* local frame coordinates into "world" coordinates </summary>
-        public Vector3f FromFrameP(Vector3f v)
-        {
+        public Vector3f FromFrameP(Vector3f v) {
             return this.rotation * v + this.origin;
         }
         /// <summary> Map point *from* local frame coordinates into "world" coordinates </summary>
-        public Vector3d FromFrameP(Vector3d v)
-        {
+        public Vector3f FromFrameP(ref Vector3f v) {
+            return this.rotation * v + this.origin;
+        }
+        /// <summary> Map point *from* local frame coordinates into "world" coordinates </summary>
+        public Vector3d FromFrameP(Vector3d v) {
+            return this.rotation * v + this.origin;
+        }
+        /// <summary> Map point *from* local frame coordinates into "world" coordinates </summary>
+        public Vector3d FromFrameP(ref Vector3d v) {
             return this.rotation * v + this.origin;
         }
 
 
         ///<summary> Map vector *into* local coordinates of Frame </summary>
-        public Vector3f ToFrameV(Vector3f v)
-        {
-            return Quaternionf.Inverse(this.rotation) * v;
+        public Vector3f ToFrameV(Vector3f v) {
+            return rotation.InverseMultiply(ref v);
         }
         ///<summary> Map vector *into* local coordinates of Frame </summary>
-        public Vector3d ToFrameV(Vector3d v)
-        {
-            return Quaternionf.Inverse(this.rotation) * v;
+        public Vector3f ToFrameV(ref Vector3f v) {
+            return rotation.InverseMultiply(ref v);
+        }
+        ///<summary> Map vector *into* local coordinates of Frame </summary>
+        public Vector3d ToFrameV(Vector3d v) {
+            return rotation.InverseMultiply(ref v);
+        }
+        ///<summary> Map vector *into* local coordinates of Frame </summary>
+        public Vector3d ToFrameV(ref Vector3d v) {
+            return rotation.InverseMultiply(ref v);
         }
         /// <summary> Map vector *from* local frame coordinates into "world" coordinates </summary>
-        public Vector3f FromFrameV(Vector3f v)
-        {
+        public Vector3f FromFrameV(Vector3f v) {
             return this.rotation * v;
         }
         /// <summary> Map vector *from* local frame coordinates into "world" coordinates </summary>
-        public Vector3d FromFrameV(Vector3d v)
-        {
+        public Vector3f FromFrameV(ref Vector3f v) {
+            return this.rotation * v;
+        }
+        /// <summary> Map vector *from* local frame coordinates into "world" coordinates </summary>
+        public Vector3d FromFrameV(ref Vector3d v) {
+            return this.rotation * v;
+        }
+        /// <summary> Map vector *from* local frame coordinates into "world" coordinates </summary>
+        public Vector3d FromFrameV(Vector3d v) {
             return this.rotation * v;
         }
 
 
 
         ///<summary> Map quaternion *into* local coordinates of Frame </summary>
-        public Quaternionf ToFrame(Quaternionf q)
-        {
+        public Quaternionf ToFrame(Quaternionf q) {
+            return Quaternionf.Inverse(this.rotation) * q;
+        }
+        ///<summary> Map quaternion *into* local coordinates of Frame </summary>
+        public Quaternionf ToFrame(ref Quaternionf q) {
             return Quaternionf.Inverse(this.rotation) * q;
         }
         /// <summary> Map quaternion *from* local frame coordinates into "world" coordinates </summary>
-        public Quaternionf FromFrame(Quaternionf q)
-        {
+        public Quaternionf FromFrame(Quaternionf q) {
+            return this.rotation * q;
+        }
+        /// <summary> Map quaternion *from* local frame coordinates into "world" coordinates </summary>
+        public Quaternionf FromFrame(ref Quaternionf q) {
             return this.rotation * q;
         }
 
 
         ///<summary> Map ray *into* local coordinates of Frame </summary>
-        public Ray3f ToFrame(Ray3f r)
-        {
-            return new Ray3f(ToFrameP(r.Origin), ToFrameV(r.Direction));
+        public Ray3f ToFrame(Ray3f r) {
+            return new Ray3f(ToFrameP(ref r.Origin), ToFrameV(ref r.Direction));
+        }
+        ///<summary> Map ray *into* local coordinates of Frame </summary>
+        public Ray3f ToFrame(ref Ray3f r) {
+            return new Ray3f(ToFrameP(ref r.Origin), ToFrameV(ref r.Direction));
         }
         /// <summary> Map ray *from* local frame coordinates into "world" coordinates </summary>
-        public Ray3f FromFrame(Ray3f r)
-        {
-            return new Ray3f(FromFrameP(r.Origin), FromFrameV(r.Direction));
+        public Ray3f FromFrame(Ray3f r) {
+            return new Ray3f(FromFrameP(ref r.Origin), FromFrameV(ref r.Direction));
+        }
+        /// <summary> Map ray *from* local frame coordinates into "world" coordinates </summary>
+        public Ray3f FromFrame(ref Ray3f r) {
+            return new Ray3f(FromFrameP(ref r.Origin), FromFrameV(ref r.Direction));
         }
 
+
         ///<summary> Map frame *into* local coordinates of Frame </summary>
-        public Frame3f ToFrame(Frame3f f)
-        {
-            return new Frame3f(ToFrameP(f.origin), ToFrame(f.rotation));
+        public Frame3f ToFrame(Frame3f f) {
+            return new Frame3f(ToFrameP(ref f.origin), ToFrame(ref f.rotation));
+        }
+        ///<summary> Map frame *into* local coordinates of Frame </summary>
+        public Frame3f ToFrame(ref Frame3f f) {
+            return new Frame3f(ToFrameP(ref f.origin), ToFrame(ref f.rotation));
         }
         /// <summary> Map frame *from* local frame coordinates into "world" coordinates </summary>
-        public Frame3f FromFrame(Frame3f f)
-        {
-            return new Frame3f(FromFrameP(f.origin), FromFrame(f.rotation));
+        public Frame3f FromFrame(Frame3f f) {
+            return new Frame3f(FromFrameP(ref f.origin), FromFrame(ref f.rotation));
+        }
+        /// <summary> Map frame *from* local frame coordinates into "world" coordinates </summary>
+        public Frame3f FromFrame(ref Frame3f f) {
+            return new Frame3f(FromFrameP(ref f.origin), FromFrame(ref f.rotation));
         }
 
 
-
-        public Box3f ToFrame(Box3f box) {
-            box.Center = ToFrameP(box.Center);
-            box.AxisX = ToFrameV(box.AxisX);
-            box.AxisY = ToFrameV(box.AxisY);
-            box.AxisZ = ToFrameV(box.AxisZ);
+        ///<summary> Map box *into* local coordinates of Frame </summary>
+        public Box3f ToFrame(ref Box3f box) {
+            box.Center = ToFrameP(ref box.Center);
+            box.AxisX = ToFrameV(ref box.AxisX);
+            box.AxisY = ToFrameV(ref box.AxisY);
+            box.AxisZ = ToFrameV(ref box.AxisZ);
             return box;
         }
-        public Box3f FromFrame(Box3f box) {
-            box.Center = FromFrameP(box.Center);
-            box.AxisX = FromFrameV(box.AxisX);
-            box.AxisY = FromFrameV(box.AxisY);
-            box.AxisZ = FromFrameV(box.AxisZ);
+        /// <summary> Map box *from* local frame coordinates into "world" coordinates </summary>
+        public Box3f FromFrame(ref Box3f box) {
+            box.Center = FromFrameP(ref box.Center);
+            box.AxisX = FromFrameV(ref box.AxisX);
+            box.AxisY = FromFrameV(ref box.AxisY);
+            box.AxisZ = FromFrameV(ref box.AxisZ);
             return box;
         }
-        public Box3d ToFrame(Box3d box) {
-            box.Center = ToFrameP(box.Center);
-            box.AxisX = ToFrameV(box.AxisX);
-            box.AxisY = ToFrameV(box.AxisY);
-            box.AxisZ = ToFrameV(box.AxisZ);
+        ///<summary> Map box *into* local coordinates of Frame </summary>
+        public Box3d ToFrame(ref Box3d box) {
+            box.Center = ToFrameP(ref box.Center);
+            box.AxisX = ToFrameV(ref box.AxisX);
+            box.AxisY = ToFrameV(ref box.AxisY);
+            box.AxisZ = ToFrameV(ref box.AxisZ);
             return box;
         }
-        public Box3d FromFrame(Box3d box) {
-            box.Center = FromFrameP(box.Center);
-            box.AxisX = FromFrameV(box.AxisX);
-            box.AxisY = FromFrameV(box.AxisY);
-            box.AxisZ = FromFrameV(box.AxisZ);
+        /// <summary> Map box *from* local frame coordinates into "world" coordinates </summary>
+        public Box3d FromFrame(ref Box3d box) {
+            box.Center = FromFrameP(ref box.Center);
+            box.AxisX = FromFrameV(ref box.AxisX);
+            box.AxisY = FromFrameV(ref box.AxisY);
+            box.AxisZ = FromFrameV(ref box.AxisZ);
             return box;
         }
 
@@ -358,12 +430,14 @@ public Vector3f RayPlaneIntersection(Vector3f ray_origin, Vector3f ray_direction
         }
 
 
-
-        public static Frame3f Interpolate(Frame3f f1, Frame3f f2, float alpha)
+        /// <summary>
+        /// Interpolate between two frames - Lerp for origin, Slerp for rotation
+        /// </summary>
+        public static Frame3f Interpolate(Frame3f f1, Frame3f f2, float t)
         {
             return new Frame3f(
-                Vector3f.Lerp(f1.origin, f2.origin, alpha),
-                Quaternionf.Slerp(f1.rotation, f2.rotation, alpha) );
+                Vector3f.Lerp(f1.origin, f2.origin, t),
+                Quaternionf.Slerp(f1.rotation, f2.rotation, t) );
         }
 
 
diff --git a/math/IndexUtil.cs b/math/IndexUtil.cs
index bff9dcb8..8891eb2d 100644
--- a/math/IndexUtil.cs
+++ b/math/IndexUtil.cs
@@ -54,6 +54,13 @@ public static int find_tri_index(int a, Index3i tri_verts)
 			if (tri_verts.c == a) return 2;
 			return DMesh3.InvalidID;
 		}
+        public static int find_tri_index(int a, ref Index3i tri_verts)
+        {
+            if (tri_verts.a == a) return 0;
+            if (tri_verts.b == a) return 1;
+            if (tri_verts.c == a) return 2;
+            return DMesh3.InvalidID;
+        }
 
         // return index of a in tri_verts, or InvalidID if not found
         public static int find_edge_index_in_tri(int a, int b, int[] tri_verts )
@@ -80,6 +87,21 @@ public static int find_tri_ordered_edge(int a, int b, int[] tri_verts)
             return DMesh3.InvalidID;
         }
 
+        /// <summary>
+        ///  find sequence [a,b] in tri_verts (mod3) and return index of a, or InvalidID if not found
+        /// </summary>
+        public static int find_tri_ordered_edge(int a, int b, ref Index3i tri_verts)
+        {
+            if (tri_verts.a == a && tri_verts.b == b) return 0;
+            if (tri_verts.b == a && tri_verts.c == b) return 1;
+            if (tri_verts.c == a && tri_verts.a == b) return 2;
+            return DMesh3.InvalidID;
+        }
+        public static int find_tri_ordered_edge(int a, int b, Index3i tri_verts)
+        {
+            return find_tri_ordered_edge(a, b, ref tri_verts);
+        }
+
         // find sequence [a,b] in tri_verts (mod3) then return the third **value**, or InvalidID if not found
         public static int find_tri_other_vtx(int a, int b, int[] tri_verts)
         {
@@ -107,6 +129,21 @@ public static int find_tri_other_vtx(int a, int b, DVector<int> tri_array, int t
 			return DMesh3.InvalidID;
 		}
 
+
+        /// <summary>
+        /// assuming a is in tri-verts, returns other two vertices, in correct order (or Index2i.Max if not found)
+        /// </summary>
+        public static Index2i find_tri_other_verts(int a, ref Index3i tri_verts)
+        {
+            if (tri_verts.a == a)
+                return new Index2i(tri_verts.b, tri_verts.c);
+            else if (tri_verts.b == a)
+                return new Index2i(tri_verts.c, tri_verts.a);
+            else if (tri_verts.c == a)
+                return new Index2i(tri_verts.a, tri_verts.b);
+            return Index2i.Max;
+        }
+
         // find sequence [a,b] in tri_verts (mod3) then return the third **index**, or InvalidID if not found
         public static int find_tri_other_index(int a, int b, int[] tri_verts)
         {
@@ -231,6 +268,25 @@ public static void sort_indices(ref Index3i tri)
 
 
 
+        public static Vector3i ToGrid3Index(int idx, int nx, int ny)
+        {
+            int x = idx % nx;
+            int y = (idx / nx) % ny;
+            int z = idx / (nx * ny);
+            return new Vector3i(x, y, z);
+        }
+
+        public static int ToGrid3Linear(int i, int j, int k, int nx, int ny) {
+            return i + nx * (j + ny * k);
+        }
+        public static int ToGrid3Linear(Vector3i ijk, int nx, int ny) {
+            return ijk.x + nx * (ijk.y + ny * ijk.z);
+        }
+        public static int ToGrid3Linear(ref Vector3i ijk, int nx, int ny) {
+            return ijk.x + nx * (ijk.y + ny * ijk.z);
+        }
+
+
 
         /// <summary>
         /// Filter out invalid entries in indices[] list. Will return indices itself if 
@@ -294,6 +350,50 @@ public static void Apply(int[] indices, IList<int> map)
                 indices[i] = map[indices[i]];
         }
 
+
+
+        public static void TrianglesToVertices(DMesh3 mesh, IEnumerable<int> triangles, HashSet<int> vertices) {
+            foreach ( int tid in triangles ) {
+                Index3i tv = mesh.GetTriangle(tid);
+                vertices.Add(tv.a); vertices.Add(tv.b); vertices.Add(tv.c);
+            }
+        }        
+        public static void TrianglesToVertices(DMesh3 mesh, HashSet<int> triangles, HashSet<int> vertices) {
+            foreach ( int tid in triangles ) {
+                Index3i tv = mesh.GetTriangle(tid);
+                vertices.Add(tv.a); vertices.Add(tv.b); vertices.Add(tv.c);
+            }
+        }
+
+
+        public static void TrianglesToEdges(DMesh3 mesh, IEnumerable<int> triangles, HashSet<int> edges) {
+            foreach ( int tid in triangles ) {
+                Index3i te = mesh.GetTriEdges(tid);
+                edges.Add(te.a); edges.Add(te.b); edges.Add(te.c);
+            }
+        }
+        public static void TrianglesToEdges(DMesh3 mesh, HashSet<int> triangles, HashSet<int> edges) {
+            foreach ( int tid in triangles ) {
+                Index3i te = mesh.GetTriEdges(tid);
+                edges.Add(te.a); edges.Add(te.b); edges.Add(te.c);
+            }
+        }
+
+
+
+        public static void EdgesToVertices(DMesh3 mesh, IEnumerable<int> edges, HashSet<int> vertices) {
+            foreach (int eid in edges) { 
+                Index2i ev = mesh.GetEdgeV(eid);
+                vertices.Add(ev.a); vertices.Add(ev.b);
+            }
+        }
+        public static void EdgesToVertices(DMesh3 mesh, HashSet<int> edges, HashSet<int> vertices) {
+            foreach (int eid in edges) { 
+                Index2i ev = mesh.GetEdgeV(eid);
+                vertices.Add(ev.a); vertices.Add(ev.b);
+            }
+        }
+
     }
 
 
@@ -302,6 +402,22 @@ public static void Apply(int[] indices, IList<int> map)
 
     public static class gIndices
     {
+        // integer indices offsets in x/y directions
+        public static readonly Vector2i[] GridOffsets4 = new Vector2i[] {
+            new Vector2i( -1, 0), new Vector2i( 1, 0),
+            new Vector2i( 0, -1), new Vector2i( 0, 1)
+        };
+
+        // integer indices offsets in x/y directions and diagonals
+        public static readonly Vector2i[] GridOffsets8 = new Vector2i[] {
+            new Vector2i( -1, 0), new Vector2i( 1, 0),
+            new Vector2i( 0, -1), new Vector2i( 0, 1),
+            new Vector2i( -1, 1), new Vector2i( 1, 1),
+            new Vector2i( -1, -1), new Vector2i( 1, -1)
+        };
+
+
+
         // Corner vertices of box faces  -  see Box.Corner for points associated w/ indexing
         // Note that 
         public static readonly int[,] BoxFaces = new int[6, 4] {
diff --git a/math/Interval1d.cs b/math/Interval1d.cs
index b7c12b95..f9ebb729 100644
--- a/math/Interval1d.cs
+++ b/math/Interval1d.cs
@@ -91,6 +91,30 @@ public Interval1d IntersectionWith(ref Interval1d o)
             return new Interval1d(Math.Max(a, o.a), Math.Min(b, o.b));
         }
 
+        /// <summary>
+        /// clamp value f to interval [a,b]
+        /// </summary>
+        public double Clamp(double f) {
+            return (f < a) ? a : (f > b) ? b : f;
+        }
+
+        /// <summary>
+        /// interpolate between a and b using value t in range [0,1]
+        /// </summary>
+        public double Interpolate(double t) {
+            return (1 - t) * a + (t) * b;
+        }
+
+        /// <summary>
+        /// Convert value into (clamped) t value in range [0,1]
+        /// </summary>
+        public double GetT(double value)
+        {
+            if (value <= a) return 0;
+            else if (value >= b) return 1;
+            else if (a == b) return 0.5;
+            else return (value-a) / (b-a);
+        }
 
         public void Set(Interval1d o) {
             a = o.a; b = o.b;
diff --git a/math/MathUtil.cs b/math/MathUtil.cs
index 85afb68e..d49d34c0 100644
--- a/math/MathUtil.cs
+++ b/math/MathUtil.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 
 
 namespace g3
@@ -10,6 +11,7 @@ public static class MathUtil
         public const double Deg2Rad = (Math.PI / 180.0);
         public const double Rad2Deg = (180.0 / Math.PI);
         public const double TwoPI = 2.0 * Math.PI;
+        public const double FourPI = 4.0 * Math.PI;
         public const double HalfPI = 0.5 * Math.PI;
         public const double ZeroTolerance = 1e-08;
         public const double Epsilon = 2.2204460492503131e-016;
@@ -370,12 +372,12 @@ public static float SmoothRise0To1(float fX, float yshift, float xZero, float sp
         }
 
         public static float WyvillRise01(float fX) {
-            float d = 1 - fX * fX;
-            return (d >= 0) ? 1 - (d * d * d) : 0;
+            float d = MathUtil.Clamp(1.0f - fX*fX, 0.0f, 1.0f);
+            return 1 - (d * d * d);
         }
         public static double WyvillRise01(double fX) {
-            double d = 1 - fX * fX;
-            return (d >= 0) ? 1 - (d * d * d) : 0;
+            double d = MathUtil.Clamp(1.0 - fX*fX, 0.0, 1.0);
+            return 1 - (d * d * d);
         }
 
         public static float WyvillFalloff01(float fX) {
@@ -483,6 +485,19 @@ public static Vector3d FastNormalArea(ref Vector3d v1, ref Vector3d v2, ref Vect
 		}
 
 
+        /// <summary>
+        /// aspect ratio of triangle 
+        /// </summary>
+        public static double AspectRatio(ref Vector3d v1, ref Vector3d v2, ref Vector3d v3)
+        {
+            double a = v1.Distance(ref v2), b = v2.Distance(ref v3), c = v3.Distance(ref v1);
+            double s = (a + b + c) / 2.0;
+            return (a * b * c) / (8.0 * (s - a) * (s - b) * (s - c));
+        }
+        public static double AspectRatio(Vector3d v1, Vector3d v2, Vector3d v3) {
+            return AspectRatio(ref v1, ref v2, ref v3);
+        }
+
 
 		//! fast cotangent between two normalized vectors 
 		//! cot = cos/sin, both of which can be computed from vector identities
@@ -629,5 +644,20 @@ public static int PowerOf10(int n) {
         }
 
 
+        /// <summary>
+        /// Iterate from 0 to (nMax-1) using prime-modulo, so we see every index once, but not in-order
+        /// </summary>
+        public static IEnumerable<int> ModuloIteration(int nMaxExclusive, int nPrime = 31337)
+        {
+            int i = 0;
+            bool done = false;
+            while (done == false) {
+                yield return i;
+                i = (i + nPrime) % nMaxExclusive;
+                done = (i == 0);
+            }
+        }
+
+
     }
 }
diff --git a/math/Matrix2d.cs b/math/Matrix2d.cs
index e35a6426..76c9c0ca 100644
--- a/math/Matrix2d.cs
+++ b/math/Matrix2d.cs
@@ -112,6 +112,14 @@ public double ExtractAngle () {
         }
 
 
+        public Vector2d Row(int i) {
+            return (i == 0) ? new Vector2d(m00, m01) : new Vector2d(m10, m11);
+        }
+        public Vector2d Column(int i) {
+            return (i == 0) ? new Vector2d(m00, m10) : new Vector2d(m01, m11);
+        }
+
+
         public void Orthonormalize ()
         {
             // Algorithm uses Gram-Schmidt orthogonalization.  If 'this' matrix is
diff --git a/math/Matrix3d.cs b/math/Matrix3d.cs
index 95f38acb..f0fd6395 100644
--- a/math/Matrix3d.cs
+++ b/math/Matrix3d.cs
@@ -68,13 +68,35 @@ public Matrix3d(Vector3d v1, Vector3d v2, Vector3d v3, bool bRows)
                 Row2 = new Vector3d(v1.z, v2.z, v3.z);
             }
         }
-		public Matrix3d(double m00, double m01, double m02, double m10, double m11, double m12, double m20, double m21, double m22) {
+        public Matrix3d(ref Vector3d v1, ref Vector3d v2, ref Vector3d v3, bool bRows)
+        {
+            if (bRows) {
+                Row0 = v1; Row1 = v2; Row2 = v3;
+            } else {
+                Row0 = new Vector3d(v1.x, v2.x, v3.x);
+                Row1 = new Vector3d(v1.y, v2.y, v3.y);
+                Row2 = new Vector3d(v1.z, v2.z, v3.z);
+            }
+        }
+        public Matrix3d(double m00, double m01, double m02, double m10, double m11, double m12, double m20, double m21, double m22) {
             Row0 = new Vector3d(m00, m01, m02);
             Row1 = new Vector3d(m10, m11, m12);
             Row2 = new Vector3d(m20, m21, m22);
         }
 
 
+        /// <summary>
+        /// Construct outer-product of u*transpose(v) of u and v
+        /// result is that Mij = u_i * v_j
+        /// </summary>
+        public Matrix3d(ref Vector3d u, ref Vector3d v)
+        {
+            Row0 = new Vector3d(u.x*v.x, u.x*v.y, u.x*v.z);
+            Row1 = new Vector3d(u.y*v.x, u.y*v.y, u.y*v.z);
+            Row2 = new Vector3d(u.z*v.x, u.z*v.y, u.z*v.z);
+        }
+
+
         public static readonly Matrix3d Identity = new Matrix3d(true);
         public static readonly Matrix3d Zero = new Matrix3d(false);
 
@@ -150,6 +172,20 @@ public void ToBuffer(double[] buf) {
                 mat.Row1.x * v.x + mat.Row1.y * v.y + mat.Row1.z * v.z,
                 mat.Row2.x * v.x + mat.Row2.y * v.y + mat.Row2.z * v.z);
         }
+
+        public Vector3d Multiply(ref Vector3d v) {
+            return new Vector3d(
+                Row0.x * v.x + Row0.y * v.y + Row0.z * v.z,
+                Row1.x * v.x + Row1.y * v.y + Row1.z * v.z,
+                Row2.x * v.x + Row2.y * v.y + Row2.z * v.z);
+        }
+
+        public void Multiply(ref Vector3d v, ref Vector3d vOut) {
+            vOut.x = Row0.x * v.x + Row0.y * v.y + Row0.z * v.z;
+            vOut.y = Row1.x * v.x + Row1.y * v.y + Row1.z * v.z;
+            vOut.z = Row2.x * v.x + Row2.y * v.y + Row2.z * v.z;
+        }
+
 		public static Matrix3d operator *(Matrix3d mat1, Matrix3d mat2)
 		{
             double m00 = mat1.Row0.x * mat2.Row0.x + mat1.Row0.y * mat2.Row1.x + mat1.Row0.z * mat2.Row2.x;
@@ -177,6 +213,14 @@ public void ToBuffer(double[] buf) {
         }
 
 
+
+        public double InnerProduct(ref Matrix3d m2)
+        {
+            return Row0.Dot(ref m2.Row0) + Row1.Dot(ref m2.Row1) + Row2.Dot(ref m2.Row2);
+        }
+
+
+
         public double Determinant {
 			get {
 				double a11 = Row0.x, a12 = Row0.y, a13 = Row0.z, a21 = Row1.x, a22 = Row1.y, a23 = Row1.z, a31 = Row2.x, a32 = Row2.y, a33 = Row2.z;
diff --git a/math/Matrix3f.cs b/math/Matrix3f.cs
index d384574b..a14b7e44 100644
--- a/math/Matrix3f.cs
+++ b/math/Matrix3f.cs
@@ -150,6 +150,20 @@ public void ToBuffer(float[] buf) {
                 mat.Row1.x * v.x + mat.Row1.y * v.y + mat.Row1.z * v.z,
                 mat.Row2.x * v.x + mat.Row2.y * v.y + mat.Row2.z * v.z);
         }
+
+        public Vector3f Multiply(ref Vector3f v) {
+            return new Vector3f(
+                Row0.x * v.x + Row0.y * v.y + Row0.z * v.z,
+                Row1.x * v.x + Row1.y * v.y + Row1.z * v.z,
+                Row2.x * v.x + Row2.y * v.y + Row2.z * v.z);
+        }
+
+        public void Multiply(ref Vector3f v, ref Vector3f vOut) {
+            vOut.x = Row0.x * v.x + Row0.y * v.y + Row0.z * v.z;
+            vOut.y = Row1.x * v.x + Row1.y * v.y + Row1.z * v.z;
+            vOut.z = Row2.x * v.x + Row2.y * v.y + Row2.z * v.z;
+        }
+
 		public static Matrix3f operator *(Matrix3f mat1, Matrix3f mat2)
 		{
             float m00 = mat1.Row0.x * mat2.Row0.x + mat1.Row0.y * mat2.Row1.x + mat1.Row0.z * mat2.Row2.x;
diff --git a/math/Quaterniond.cs b/math/Quaterniond.cs
index d608338a..555d49cb 100644
--- a/math/Quaterniond.cs
+++ b/math/Quaterniond.cs
@@ -74,7 +74,9 @@ public double Dot(Quaterniond q2) {
         }
 
 
-
+        public static Quaterniond operator -(Quaterniond q2) {
+            return new Quaterniond(-q2.x, -q2.y, -q2.z, -q2.w);
+        }
 
         public static Quaterniond operator*(Quaterniond a, Quaterniond b) {
             double w = a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z;
@@ -83,12 +85,19 @@ public double Dot(Quaterniond q2) {
             double z = a.w * b.z + a.z * b.w + a.x * b.y - a.y * b.x;
             return new Quaterniond(x, y, z, w);
         }
-
-
+        public static Quaterniond operator *(Quaterniond q1, double d) {
+            return new Quaterniond(d * q1.x, d * q1.y, d * q1.z, d * q1.w);
+        }
+        public static Quaterniond operator *(double d, Quaterniond q1) {
+            return new Quaterniond(d * q1.x, d * q1.y, d * q1.z, d * q1.w);
+        }
 
         public static Quaterniond operator -(Quaterniond q1, Quaterniond q2) {
             return new Quaterniond(q1.x - q2.x, q1.y - q2.y, q1.z - q2.z, q1.w - q2.w);
         }
+        public static Quaterniond operator +(Quaterniond q1, Quaterniond q2) {
+            return new Quaterniond(q1.x + q2.x, q1.y + q2.y, q1.z + q2.z, q1.w + q2.w);
+        }
 
         public static Vector3d operator *(Quaterniond q, Vector3d v) {
             //return q.ToRotationMatrix() * v;
@@ -256,8 +265,10 @@ public static Quaterniond Slerp(Quaterniond p, Quaterniond q, double t) {
         }
 
 
-
-        public void SetFromRotationMatrix(Matrix3d rot)
+        public void SetFromRotationMatrix(Matrix3d rot) {
+            SetFromRotationMatrix(ref rot);
+        }
+        public void SetFromRotationMatrix(ref Matrix3d rot)
         {
             // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes
             // article "Quaternion Calculus and Fast Animation".
@@ -312,6 +323,15 @@ public bool EpsilonEqual(Quaterniond q2, double epsilon) {
         }
 
 
+        // [TODO] should we be normalizing in these casts??
+        public static implicit operator Quaterniond(Quaternionf q) {
+            return new Quaterniond(q.x, q.y, q.z, q.w);
+        }
+        public static explicit operator Quaternionf(Quaterniond q) {
+            return new Quaternionf((float)q.x, (float)q.y, (float)q.z, (float)q.w);
+        }
+
+
         public override string ToString() {
             return string.Format("{0:F8} {1:F8} {2:F8} {3:F8}", x, y, z, w);
         }
diff --git a/math/Quaternionf.cs b/math/Quaternionf.cs
index a36e06e3..7e6c7872 100644
--- a/math/Quaternionf.cs
+++ b/math/Quaternionf.cs
@@ -8,7 +8,7 @@
 namespace g3
 {
     // mostly ported from WildMagic5 Wm5Quaternion, from geometrictools.com
-    public struct Quaternionf
+    public struct Quaternionf : IComparable<Quaternionf>, IEquatable<Quaternionf>
     {
         // note: in Wm5 version, this is a 4-element array stored in order (w,x,y,z).
         public float x, y, z, w;
@@ -118,6 +118,48 @@ public float Dot(Quaternionf q2) {
         }
 
 
+
+        /// <summary> Inverse() * v </summary>
+        public Vector3f InverseMultiply(ref Vector3f v)
+        {
+            float norm = LengthSquared;
+            if (norm > 0) {
+                float invNorm = 1.0f / norm;
+                float qx = -x*invNorm, qy = -y*invNorm, qz = -z*invNorm, qw = w*invNorm;
+                float twoX = 2 * qx; float twoY = 2 * qy; float twoZ = 2 * qz;
+                float twoWX = twoX * qw; float twoWY = twoY * qw; float twoWZ = twoZ * qw;
+                float twoXX = twoX * qx; float twoXY = twoY * qx; float twoXZ = twoZ * qx;
+                float twoYY = twoY * qy; float twoYZ = twoZ * qy; float twoZZ = twoZ * qz;
+                return new Vector3f(
+                    v.x * (1 - (twoYY + twoZZ)) + v.y * (twoXY - twoWZ) + v.z * (twoXZ + twoWY),
+                    v.x * (twoXY + twoWZ) + v.y * (1 - (twoXX + twoZZ)) + v.z * (twoYZ - twoWX),
+                    v.x * (twoXZ - twoWY) + v.y * (twoYZ + twoWX) + v.z * (1 - (twoXX + twoYY))); 
+            } else
+                return Vector3f.Zero;
+        }
+
+
+        /// <summary> Inverse() * v </summary>
+        public Vector3d InverseMultiply(ref Vector3d v)
+        {
+            float norm = LengthSquared;
+            if (norm > 0) {
+                float invNorm = 1.0f / norm;
+                float qx = -x * invNorm, qy = -y * invNorm, qz = -z * invNorm, qw = w * invNorm;
+                double twoX = 2 * qx; double twoY = 2 * qy; double twoZ = 2 * qz;
+                double twoWX = twoX * qw; double twoWY = twoY * qw; double twoWZ = twoZ * qw;
+                double twoXX = twoX * qx; double twoXY = twoY * qx; double twoXZ = twoZ * qx;
+                double twoYY = twoY * qy; double twoYZ = twoZ * qy; double twoZZ = twoZ * qz;
+                return new Vector3d(
+                    v.x * (1 - (twoYY + twoZZ)) + v.y * (twoXY - twoWZ) + v.z * (twoXZ + twoWY),
+                    v.x * (twoXY + twoWZ) + v.y * (1 - (twoXX + twoZZ)) + v.z * (twoYZ - twoWX),
+                    v.x * (twoXZ - twoWY) + v.y * (twoYZ + twoWX) + v.z * (1 - (twoXX + twoYY))); ;
+            } else
+                return Vector3f.Zero;
+        }
+
+
+
         // these multiply quaternion by (1,0,0), (0,1,0), (0,0,1), respectively.
         // faster than full multiply, because of all the zeros
         public Vector3f AxisX {
@@ -311,6 +353,51 @@ public void SetFromRotationMatrix(Matrix3f rot)
 
 
 
+        public static bool operator ==(Quaternionf a, Quaternionf b)
+        {
+            return (a.x == b.x && a.y == b.y && a.z == b.z && a.w == b.w);
+        }
+        public static bool operator !=(Quaternionf a, Quaternionf b)
+        {
+            return (a.x != b.x || a.y != b.y || a.z != b.z || a.w != b.w);
+        }
+        public override bool Equals(object obj)
+        {
+            return this == (Quaternionf)obj;
+        }
+        public override int GetHashCode()
+        {
+            unchecked // Overflow is fine, just wrap
+            {
+                int hash = (int)2166136261;
+                // Suitable nullity checks etc, of course :)
+                hash = (hash * 16777619) ^ x.GetHashCode();
+                hash = (hash * 16777619) ^ y.GetHashCode();
+                hash = (hash * 16777619) ^ z.GetHashCode();
+                hash = (hash * 16777619) ^ w.GetHashCode();
+                return hash;
+            }
+        }
+        public int CompareTo(Quaternionf other)
+        {
+            if (x != other.x)
+                return x < other.x ? -1 : 1;
+            else if (y != other.y)
+                return y < other.y ? -1 : 1;
+            else if (z != other.z)
+                return z < other.z ? -1 : 1;
+            else if (w != other.w)
+                return w < other.w ? -1 : 1;
+            return 0;
+        }
+        public bool Equals(Quaternionf other)
+        {
+            return (x == other.x && y == other.y && z == other.z && w == other.w);
+        }
+
+
+
+
 
         public bool EpsilonEqual(Quaternionf q2, float epsilon) {
             return (float)Math.Abs(x - q2.x) <= epsilon && 
diff --git a/math/Ray3.cs b/math/Ray3.cs
index 97e2ec73..55f1eb61 100644
--- a/math/Ray3.cs
+++ b/math/Ray3.cs
@@ -14,10 +14,12 @@ public struct Ray3d
         public Vector3d Origin;
         public Vector3d Direction;
 
-        public Ray3d(Vector3d origin, Vector3d direction)
+        public Ray3d(Vector3d origin, Vector3d direction, bool bIsNormalized = false)
         {
             this.Origin = origin;
             this.Direction = direction;
+            if (bIsNormalized == false && Direction.IsNormalized == false)
+                Direction.Normalize();
         }
 
         public Ray3d(Vector3f origin, Vector3f direction)
@@ -91,10 +93,12 @@ public struct Ray3f
         public Vector3f Origin;
         public Vector3f Direction;
 
-        public Ray3f(Vector3f origin, Vector3f direction)
+        public Ray3f(Vector3f origin, Vector3f direction, bool bIsNormalized = false)
         {
             this.Origin = origin;
             this.Direction = direction;
+            if (bIsNormalized == false && Direction.IsNormalized == false)
+                Direction.Normalize();
         }
 
         // parameter is distance along ray
diff --git a/math/Segment3.cs b/math/Segment3.cs
index f96947ab..4583ef90 100644
--- a/math/Segment3.cs
+++ b/math/Segment3.cs
@@ -61,6 +61,20 @@ public double DistanceSquared(Vector3d p)
 			Vector3d proj = Center + t * Direction;
 			return (proj - p).LengthSquared;
 		}
+        public double DistanceSquared(Vector3d p, out double t)
+        {
+            t = (p - Center).Dot(Direction);
+            if (t >= Extent) {
+                t = Extent;
+                return P1.DistanceSquared(p);
+            } else if (t <= -Extent) {
+                t = -Extent;
+                return P0.DistanceSquared(p);
+            }
+            Vector3d proj = Center + t * Direction;
+            return (proj - p).LengthSquared;
+        }
+
 
         public Vector3d NearestPoint(Vector3d p)
         {
diff --git a/math/TransformSequence.cs b/math/TransformSequence.cs
index 2c92a73d..a8197e69 100644
--- a/math/TransformSequence.cs
+++ b/math/TransformSequence.cs
@@ -22,14 +22,16 @@ enum XFormType
             QuaterionRotation = 1,
             QuaternionRotateAroundPoint = 2,
             Scale = 3,
-            ScaleAroundPoint = 4
+            ScaleAroundPoint = 4,
+            ToFrame = 5,
+            FromFrame = 6
         }
 
         struct XForm
         {
             public XFormType type;
             public Vector3dTuple3 data;
-
+            
             // may need to update these to handle other types...
             public Vector3d Translation {
                 get { return data.V0; }
@@ -43,6 +45,9 @@ public Quaternionf Quaternion {
             public Vector3d RotateOrigin {
                 get { return data.V2; }
             }
+            public Frame3f Frame {
+                get { return new Frame3f((Vector3f)RotateOrigin, Quaternion); }
+            }
         }
 
         List<XForm> Operations;
@@ -54,6 +59,17 @@ public TransformSequence()
             Operations = new List<XForm>();
         }
 
+        public TransformSequence(TransformSequence copy)
+        {
+            Operations = new List<XForm>(copy.Operations);
+        }
+
+
+
+        public void Append(TransformSequence sequence)
+        {
+            Operations.AddRange(sequence.Operations);
+        }
 
 
         public void AppendTranslation(Vector3d dv)
@@ -103,6 +119,23 @@ public void AppendScale(Vector3d s, Vector3d aroundPt)
             });
         }
 
+        public void AppendToFrame(Frame3f frame)
+        {
+            Quaternionf q = frame.Rotation; 
+            Operations.Add(new XForm() {
+                type = XFormType.ToFrame,
+                data = new Vector3dTuple3(new Vector3d(q.x, q.y, q.z), new Vector3d(q.w, 0, 0), frame.Origin)
+            });
+        }
+
+        public void AppendFromFrame(Frame3f frame)
+        {
+            Quaternionf q = frame.Rotation;
+            Operations.Add(new XForm() {
+                type = XFormType.FromFrame,
+                data = new Vector3dTuple3(new Vector3d(q.x, q.y, q.z), new Vector3d(q.w, 0, 0), frame.Origin)
+            });
+        }
 
 
         /// <summary>
@@ -137,6 +170,14 @@ public Vector3d TransformP(Vector3d p)
                         p += Operations[i].RotateOrigin;
                         break;
 
+                    case XFormType.ToFrame:
+                        p = Operations[i].Frame.ToFrameP(ref p);
+                        break;
+
+                    case XFormType.FromFrame:
+                        p = Operations[i].Frame.FromFrameP(ref p);
+                        break;
+
                     default:
                         throw new NotImplementedException("TransformSequence.TransformP: unhandled type!");
                 }
@@ -148,6 +189,98 @@ public Vector3d TransformP(Vector3d p)
 
 
 
+        /// <summary>
+        /// Apply transforms to vector. Includes scaling.
+        /// </summary>
+        public Vector3d TransformV(Vector3d v)
+        {
+            int N = Operations.Count;
+            for (int i = 0; i < N; ++i) {
+                switch (Operations[i].type) {
+                    case XFormType.Translation:
+                        break;
+
+                    case XFormType.QuaternionRotateAroundPoint:
+                    case XFormType.QuaterionRotation:
+                        v = Operations[i].Quaternion * v;
+                        break;
+
+                    case XFormType.ScaleAroundPoint:
+                    case XFormType.Scale:
+                        v *= Operations[i].Scale;
+                        break;
+
+                    case XFormType.ToFrame:
+                        v = Operations[i].Frame.ToFrameV(ref v);
+                        break;
+
+                    case XFormType.FromFrame:
+                        v = Operations[i].Frame.FromFrameV(ref v);
+                        break;
+
+                    default:
+                        throw new NotImplementedException("TransformSequence.TransformV: unhandled type!");
+                }
+            }
+
+            return v;
+        }
+
+
+
+
+        /// <summary>
+        /// Apply transforms to point
+        /// </summary>
+        public Vector3f TransformP(Vector3f p) {
+            return (Vector3f)TransformP((Vector3d)p);
+        }
+
+
+        /// <summary>
+        /// construct inverse transformation sequence
+        /// </summary>
+        public TransformSequence MakeInverse()
+        {
+            TransformSequence reverse = new TransformSequence();
+            int N = Operations.Count;
+            for (int i = N-1; i >= 0; --i) {
+                switch (Operations[i].type) {
+                    case XFormType.Translation:
+                        reverse.AppendTranslation(-Operations[i].Translation);
+                        break;
+
+                    case XFormType.QuaterionRotation:
+                        reverse.AppendRotation(Operations[i].Quaternion.Inverse());
+                        break;
+
+                    case XFormType.QuaternionRotateAroundPoint:
+                        reverse.AppendRotation(Operations[i].Quaternion.Inverse(), Operations[i].RotateOrigin);
+                        break;
+
+                    case XFormType.Scale:
+                        reverse.AppendScale(1.0 / Operations[i].Scale);
+                        break;
+
+                    case XFormType.ScaleAroundPoint:
+                        reverse.AppendScale(1.0 / Operations[i].Scale, Operations[i].RotateOrigin);
+                        break;
+
+                    case XFormType.ToFrame:
+                        reverse.AppendFromFrame(Operations[i].Frame);
+                        break;
+
+                    case XFormType.FromFrame:
+                        reverse.AppendToFrame(Operations[i].Frame);
+                        break;
+
+                    default:
+                        throw new NotImplementedException("TransformSequence.MakeInverse: unhandled type!");
+                }
+            }
+            return reverse;
+        }
+
 
 
         public void Store(BinaryWriter writer)
diff --git a/math/Triangle3.cs b/math/Triangle3.cs
index c918870e..5500c98a 100644
--- a/math/Triangle3.cs
+++ b/math/Triangle3.cs
@@ -20,6 +20,16 @@ public Vector3d this[int key]
             set { if (key == 0) V0 = value; else if (key == 1) V1 = value; else V2 = value; }
         }
 
+        public Vector3d Normal {
+            get { return MathUtil.Normal(ref V0, ref V1, ref V2); }
+        }
+        public double Area {
+            get { return MathUtil.Area(ref V0, ref V1, ref V2); }
+        }
+        public double AspectRatio {
+            get { return MathUtil.AspectRatio(ref V0, ref V1, ref V2); }
+        }
+
         public Vector3d PointAt(double bary0, double bary1, double bary2)
         {
             return bary0 * V0 + bary1 * V1 + bary2 * V2;
diff --git a/math/Vector2i.cs b/math/Vector2i.cs
index 5fc0e83c..72d22f30 100644
--- a/math/Vector2i.cs
+++ b/math/Vector2i.cs
@@ -30,6 +30,7 @@ public int[] array
         public void Add(int s) { x += s; y += s; }
 
 
+        public int LengthSquared { get { return x * x + y * y; } }
 
 
         public static Vector2i operator -(Vector2i v)
diff --git a/math/Vector3d.cs b/math/Vector3d.cs
index 0c2ca2da..044ef93b 100644
--- a/math/Vector3d.cs
+++ b/math/Vector3d.cs
@@ -127,25 +127,27 @@ public double Dot(ref Vector3d v2) {
             return x * v2.x + y * v2.y + z * v2.z;
         }
 
-        public static double Dot(Vector3d v1, Vector3d v2)
-        {
-            return v1.Dot(v2);
+        public static double Dot(Vector3d v1, Vector3d v2) {
+            return v1.Dot(ref v2);
         }
 
-        public Vector3d Cross(Vector3d v2)
-        {
+        public Vector3d Cross(Vector3d v2) {
             return new Vector3d(
                 y * v2.z - z * v2.y,
                 z * v2.x - x * v2.z,
                 x * v2.y - y * v2.x);
         }
-        public static Vector3d Cross(Vector3d v1, Vector3d v2)
-        {
-            return v1.Cross(v2);
+        public Vector3d Cross(ref Vector3d v2) {
+            return new Vector3d(
+                y * v2.z - z * v2.y,
+                z * v2.x - x * v2.z,
+                x * v2.y - y * v2.x);
+        }
+        public static Vector3d Cross(Vector3d v1, Vector3d v2) {
+            return v1.Cross(ref v2);
         }
 
-        public Vector3d UnitCross(Vector3d v2)
-        {
+        public Vector3d UnitCross(ref Vector3d v2) {
             Vector3d n = new Vector3d(
                 y * v2.z - z * v2.y,
                 z * v2.x - x * v2.z,
@@ -153,6 +155,10 @@ public Vector3d UnitCross(Vector3d v2)
             n.Normalize();
             return n;
         }
+        public Vector3d UnitCross(Vector3d v2) {
+            return UnitCross(ref v2);
+        }
+
 
         public double AngleD(Vector3d v2)
         {
@@ -356,9 +362,12 @@ public static explicit operator Vector3(Vector3d v)
         // complicated functions go down here...
 
 
-        // [RMS] this is from WildMagic5, but I added returning the minLength value
-        //   from GTEngine, because I use this in place of GTEngine's Orthonormalize in
-        //   ComputeOrthogonalComplement below
+        /// <summary>
+        /// Gram-Schmidt orthonormalization of the input vectors.
+        /// [RMS] this is from WildMagic5, but I added returning the minLength value
+        /// from GTEngine, because I use this in place of GTEngine's Orthonormalize in
+        /// ComputeOrthogonalComplement below
+        /// </summary>
         public static double Orthonormalize(ref Vector3d u, ref Vector3d v, ref Vector3d w)
         {
             // If the input vectors are v0, v1, and v2, then the Gram-Schmidt
@@ -393,9 +402,11 @@ public static double Orthonormalize(ref Vector3d u, ref Vector3d v, ref Vector3d
         }
 
 
-        // Input W must be a unit-length vector.  The output vectors {U,V} are
-        // unit length and mutually perpendicular, and {U,V,W} is an orthonormal basis.
-        // ported from WildMagic5
+        /// <summary>
+        /// Input W must be a unit-length vector.  The output vectors {U,V} are
+        /// unit length and mutually perpendicular, and {U,V,W} is an orthonormal basis.
+        /// ported from WildMagic5
+        /// </summary>
         public static void GenerateComplementBasis(ref Vector3d u, ref Vector3d v, Vector3d w)
         {
             double invLength;
@@ -421,14 +432,16 @@ public static void GenerateComplementBasis(ref Vector3d u, ref Vector3d v, Vecto
             }
         }
 
-        // this function is from GTEngine
-        // Compute a right-handed orthonormal basis for the orthogonal complement
-        // of the input vectors.  The function returns the smallest length of the
-        // unnormalized vectors computed during the process.  If this value is nearly
-        // zero, it is possible that the inputs are linearly dependent (within
-        // numerical round-off errors).  On input, numInputs must be 1 or 2 and
-        // v0 through v(numInputs-1) must be initialized.  On output, the
-        // vectors v0 through v2 form an orthonormal set.
+        /// <summary>
+        /// this function is ported from GTEngine.
+        /// Compute a right-handed orthonormal basis for the orthogonal complement
+        /// of the input vectors.  The function returns the smallest length of the
+        /// unnormalized vectors computed during the process.  If this value is nearly
+        /// zero, it is possible that the inputs are linearly dependent (within
+        /// numerical round-off errors).  On input, numInputs must be 1 or 2 and
+        /// v0 through v(numInputs-1) must be initialized.  On output, the
+        /// vectors v0 through v2 form an orthonormal set.
+        /// </summary>
         public static double ComputeOrthogonalComplement(int numInputs, Vector3d v0, ref Vector3d v1, ref Vector3d v2 /*, bool robust = false*/)
         {
             if (numInputs == 1) {
@@ -451,5 +464,38 @@ public static double ComputeOrthogonalComplement(int numInputs, Vector3d v0, ref
             return 0;
         }
 
+
+
+        /// <summary>
+        /// Returns two vectors perpendicular to n, as efficiently as possible.
+        /// Duff et all method, from https://graphics.pixar.com/library/OrthonormalB/paper.pdf
+        /// </summary>
+        public static void MakePerpVectors(ref Vector3d n, out Vector3d b1, out Vector3d b2)
+        {
+            if (n.z < 0.0) {
+                double a = 1.0 / (1.0 - n.z);
+                double b = n.x * n.y * a;
+                //b1 = Vec3f(1.0f - n.x * n.x * a, -b, n.x);
+                //b2 = Vec3f(b, n.y * n.y * a - 1.0f, -n.y);
+                b1.x = 1.0f - n.x * n.x * a;
+                b1.y = -b;
+                b1.z = n.x;
+                b2.x = b;
+                b2.y = n.y * n.y * a - 1.0f;
+                b2.z = -n.y;
+            } else {
+                double a = 1.0 / (1.0 + n.z);
+                double b = -n.x * n.y * a;
+                //b1 = Vec3f(1.0 - n.x * n.x * a, b, -n.x);
+                //b2 = Vec3f(b, 1.0 - n.y * n.y * a, -n.y);
+                b1.x = 1.0 - n.x * n.x * a;
+                b1.y = b;
+                b1.z = -n.x;
+                b2.x = b;
+                b2.y = 1.0 - n.y * n.y * a;
+                b2.z = -n.y;
+            }
+        }
+
     }
 }
diff --git a/math/Vector3i.cs b/math/Vector3i.cs
index 0d605586..26e4f6b5 100644
--- a/math/Vector3i.cs
+++ b/math/Vector3i.cs
@@ -57,6 +57,8 @@ public void Subtract(Vector3i o)
         public void Add(int s) { x += s;  y += s;  z += s; }
 
 
+        public int LengthSquared { get { return x * x + y * y + z * z; } }
+
 
         public static Vector3i operator -(Vector3i v)
         {
diff --git a/math/Vector4f.cs b/math/Vector4f.cs
new file mode 100644
index 00000000..777d8049
--- /dev/null
+++ b/math/Vector4f.cs
@@ -0,0 +1,278 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+#if G3_USING_UNITY
+using UnityEngine;
+#endif
+
+namespace g3
+{
+    public struct Vector4f : IComparable<Vector4f>, IEquatable<Vector4f>
+    {
+        public float x;
+        public float y;
+        public float z;
+        public float w;
+
+        public Vector4f(float f) { x = y = z = w = f; }
+        public Vector4f(float x, float y, float z, float w) { this.x = x; this.y = y; this.z = z; this.w = w; }
+        public Vector4f(float[] v2) { x = v2[0]; y = v2[1]; z = v2[2]; w = v2[3]; }
+        public Vector4f(Vector4f copy) { x = copy.x; y = copy.y; z = copy.z; w = copy.w; }
+
+        static public readonly Vector4f Zero = new Vector4f(0.0f, 0.0f, 0.0f, 0.0f);
+        static public readonly Vector4f One = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
+
+        public float this[int key]
+        {
+            get { return (key < 2) ? ((key == 0) ? x : y) : ((key == 2) ? z : w); }
+            set {
+                if (key < 2) { if (key == 0) x = value; else y = value; }
+                else { if (key == 2) z = value; else w = value; }
+            }
+        }
+
+        public float LengthSquared
+        {
+            get { return x * x + y * y + z * z + w * w; }
+        }
+        public float Length
+        {
+            get { return (float)Math.Sqrt(LengthSquared); }
+        }
+
+        public float LengthL1
+        {
+            get { return Math.Abs(x) + Math.Abs(y) + Math.Abs(z) + Math.Abs(w); }
+        }
+
+
+        public float Normalize(float epsilon = MathUtil.Epsilonf)
+        {
+            float length = Length;
+            if (length > epsilon) {
+                float invLength = 1.0f / length;
+                x *= invLength;
+                y *= invLength;
+                z *= invLength;
+                w *= invLength;
+            } else {
+                length = 0;
+                x = y = z = w = 0;
+            }
+            return length;
+        }
+        public Vector4f Normalized {
+            get {
+                float length = Length;
+                if (length > MathUtil.Epsilon) {
+                    float invLength = 1.0f / length;
+                    return new Vector4f(x * invLength, y * invLength, z * invLength, w * invLength);
+                } else
+                    return Vector4f.Zero;
+            }
+        }
+
+        public bool IsNormalized {
+            get { return Math.Abs((x * x + y * y + z * z + w * w) - 1) < MathUtil.ZeroTolerance; }
+        }
+
+
+        public bool IsFinite
+        {
+            get { float f = x + y + z + w; return float.IsNaN(f) == false && float.IsInfinity(f) == false; }
+        }
+
+        public void Round(int nDecimals) {
+            x = (float)Math.Round(x, nDecimals);
+            y = (float)Math.Round(y, nDecimals);
+            z = (float)Math.Round(z, nDecimals);
+            w = (float)Math.Round(w, nDecimals);
+        }
+
+
+        public float Dot(Vector4f v2) {
+            return x * v2.x + y * v2.y + z * v2.z + w * v2.w;
+        }
+        public float Dot(ref Vector4f v2) {
+            return x * v2.x + y * v2.y + z * v2.z + w * v2.w;
+        }
+
+        public static float Dot(Vector4f v1, Vector4f v2) {
+            return v1.Dot(v2);
+        }
+
+
+        public float AngleD(Vector4f v2)
+        {
+            float fDot = MathUtil.Clamp(Dot(v2), -1, 1);
+            return (float)Math.Acos(fDot) * MathUtil.Rad2Degf;
+        }
+        public static float AngleD(Vector4f v1, Vector4f v2)
+        {
+            return v1.AngleD(v2);
+        }
+        public float AngleR(Vector4f v2)
+        {
+            float fDot = MathUtil.Clamp(Dot(v2), -1, 1);
+            return (float)Math.Acos(fDot);
+        }
+        public static float AngleR(Vector4f v1, Vector4f v2)
+        {
+            return v1.AngleR(v2);
+        }
+
+		public float DistanceSquared(Vector4f v2) {
+			float dx = v2.x-x, dy = v2.y-y, dz = v2.z-z, dw = v2.w-w;
+			return dx*dx + dy*dy + dz*dz + dw*dw;
+		}
+		public float DistanceSquared(ref Vector4f v2) {
+			float dx = v2.x-x, dy = v2.y-y, dz = v2.z-z, dw = v2.w-w;
+			return dx*dx + dy*dy + dz*dz + dw*dw;
+		}
+
+        public float Distance(Vector4f v2) {
+            float dx = v2.x-x, dy = v2.y-y, dz = v2.z-z, dw = v2.w - w;
+			return (float)Math.Sqrt(dx*dx + dy*dy + dz*dz + dw*dw);
+		}
+        public float Distance(ref Vector4f v2) {
+            float dx = v2.x-x, dy = v2.y-y, dz = v2.z-z, dw = v2.w - w;
+			return (float)Math.Sqrt(dx*dx + dy*dy + dz*dz + dw*dw);
+		}
+
+
+        public static Vector4f operator -(Vector4f v)
+        {
+            return new Vector4f(-v.x, -v.y, -v.z, -v.w);
+        }
+
+        public static Vector4f operator *(float f, Vector4f v)
+        {
+            return new Vector4f(f * v.x, f * v.y, f * v.z, f * v.w);
+        }
+        public static Vector4f operator *(Vector4f v, float f)
+        {
+            return new Vector4f(f * v.x, f * v.y, f * v.z, f * v.w);
+        }
+        public static Vector4f operator /(Vector4f v, float f)
+        {
+            return new Vector4f(v.x / f, v.y / f, v.z / f, v.w / f);
+        }
+        public static Vector4f operator /(float f, Vector4f v)
+        {
+            return new Vector4f(f / v.x, f / v.y, f / v.z, f / v.w);
+        }
+
+        public static Vector4f operator *(Vector4f a, Vector4f b)
+        {
+            return new Vector4f(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
+        }
+        public static Vector4f operator /(Vector4f a, Vector4f b)
+        {
+            return new Vector4f(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w);
+        }
+
+
+        public static Vector4f operator +(Vector4f v0, Vector4f v1)
+        {
+            return new Vector4f(v0.x + v1.x, v0.y + v1.y, v0.z + v1.z, v0.w + v1.w);
+        }
+        public static Vector4f operator +(Vector4f v0, float f)
+        {
+            return new Vector4f(v0.x + f, v0.y + f, v0.z + f, v0.w + f);
+        }
+
+        public static Vector4f operator -(Vector4f v0, Vector4f v1)
+        {
+            return new Vector4f(v0.x - v1.x, v0.y - v1.y, v0.z - v1.z, v0.w - v1.w);
+        }
+        public static Vector4f operator -(Vector4f v0, float f)
+        {
+            return new Vector4f(v0.x - f, v0.y - f, v0.z - f, v0.w - f);
+        }
+
+
+
+        public static bool operator ==(Vector4f a, Vector4f b)
+        {
+            return (a.x == b.x && a.y == b.y && a.z == b.z && a.w == b.w);
+        }
+        public static bool operator !=(Vector4f a, Vector4f b)
+        {
+            return (a.x != b.x || a.y != b.y || a.z != b.z || a.w != b.w);
+        }
+        public override bool Equals(object obj)
+        {
+            return this == (Vector4f)obj;
+        }
+        public override int GetHashCode()
+        {
+            unchecked // Overflow is fine, just wrap
+            {
+                int hash = (int) 2166136261;
+                // Suitable nullity checks etc, of course :)
+                hash = (hash * 16777619) ^ x.GetHashCode();
+                hash = (hash * 16777619) ^ y.GetHashCode();
+                hash = (hash * 16777619) ^ z.GetHashCode();
+                hash = (hash * 16777619) ^ w.GetHashCode();
+                return hash;
+            }
+        }
+        public int CompareTo(Vector4f other)
+        {
+            if (x != other.x)
+                return x < other.x ? -1 : 1;
+            else if (y != other.y)
+                return y < other.y ? -1 : 1;
+            else if (z != other.z)
+                return z < other.z ? -1 : 1;
+            else if (w != other.w)
+                return w < other.w ? -1 : 1;
+            return 0;
+        }
+        public bool Equals(Vector4f other)
+        {
+            return (x == other.x && y == other.y && z == other.z && w == other.w);
+        }
+
+
+        public bool EpsilonEqual(Vector4f v2, float epsilon) {
+            return Math.Abs(x - v2.x) <= epsilon && 
+                   Math.Abs(y - v2.y) <= epsilon &&
+                   Math.Abs(z - v2.z) <= epsilon &&
+                   Math.Abs(w - v2.w) <= epsilon;
+        }
+
+
+
+        public override string ToString() {
+            return string.Format("{0:F8} {1:F8} {2:F8} {3:F8}", x, y, z, w);
+        }
+        public string ToString(string fmt) {
+            return string.Format("{0} {1} {2} {3}", x.ToString(fmt), y.ToString(fmt), z.ToString(fmt), w.ToString(fmt));
+        }
+
+
+
+
+#if G3_USING_UNITY
+        public static implicit operator Vector4f(Vector4 v)
+        {
+            return new Vector4f(v.x, v.y, v.z, v.w);
+        }
+        public static implicit operator Vector4(Vector4f v)
+        {
+            return new Vector4(v.x, v.y, v.z, v.w);
+        }
+        public static implicit operator Color(Vector4f v)
+        {
+            return new Color(v.x, v.y, v.z, v.w);
+        }
+        public static implicit operator Vector4f(Color c)
+        {
+            return new Vector4f(c.r, c.g, c.b, c.a);
+        }
+#endif
+
+    }
+}
diff --git a/math/VectorTuple.cs b/math/VectorTuple.cs
index 043265e0..993646b2 100644
--- a/math/VectorTuple.cs
+++ b/math/VectorTuple.cs
@@ -6,6 +6,22 @@ namespace g3
     // (which C# does not support, but is common in C++ code)
 
 
+    public struct Vector3dTuple2
+    {
+        public Vector3d V0, V1;
+
+        public Vector3dTuple2(Vector3d v0, Vector3d v1)
+        {
+            V0 = v0; V1 = v1;
+        }
+
+        public Vector3d this[int key] {
+            get { return (key == 0) ? V0 : V1; }
+            set { if (key == 0) V0 = value; else V1 = value; }
+        }
+    }
+
+
     public struct Vector3dTuple3
     {
         public Vector3d V0, V1, V2;
diff --git a/mesh/DMesh3.cs b/mesh/DMesh3.cs
index 19b90a31..794b7dda 100644
--- a/mesh/DMesh3.cs
+++ b/mesh/DMesh3.cs
@@ -15,6 +15,7 @@ public enum MeshResult
         Failed_NotAnEdge = 3,
 
         Failed_BrokenTopology = 10,
+        Failed_HitValenceLimit = 11,
 
         Failed_IsBoundaryEdge = 20,
         Failed_FlippedEdgeExists = 21,
@@ -27,6 +28,12 @@ public enum MeshResult
 		Failed_SameOrientation = 28,
 
         Failed_WouldCreateBowtie = 30,
+        Failed_VertexAlreadyExists = 31,
+        Failed_CannotAllocateVertex = 32,
+
+        Failed_WouldCreateNonmanifoldEdge = 50,
+        Failed_TriangleAlreadyExists = 51,
+        Failed_CannotAllocateTriangle = 52
 
     };
 
@@ -321,9 +328,17 @@ void updateTimeStamp(bool bShapeChange) {
             if (bShapeChange)
                 shape_timestamp++;
 		}
+
+        /// <summary>
+        /// Timestamp is incremented any time any change is made to the mesh
+        /// </summary>
         public int Timestamp {
             get { return timestamp; }
         }
+
+        /// <summary>
+        /// ShapeTimestamp is incremented any time any vertex position is changed or the mesh topology is modified
+        /// </summary>
         public int ShapeTimestamp {
             get { return shape_timestamp; }
         }
@@ -470,7 +485,7 @@ public bool GetVertex(int vID, ref NewVertexInfo vinfo, bool bWantNormals, bool
                 return false;
             vinfo.v.Set(vertices[3 * vID], vertices[3 * vID + 1], vertices[3 * vID + 2]);
             vinfo.bHaveN = vinfo.bHaveUV = vinfo.bHaveC = false;
-            if (HasVertexColors && bWantNormals) {
+            if (HasVertexNormals && bWantNormals) {
                 vinfo.bHaveN = true;
                 vinfo.n.Set(normals[3 * vID], normals[3 * vID + 1], normals[3 * vID + 2]);
             }
@@ -817,6 +832,27 @@ public double GetTriSolidAngle(int tID, ref Vector3d p)
 
 
 
+        /// <summary>
+        /// compute internal angle at vertex i of triangle (where i is 0,1,2);
+        /// TODO can be more efficient here, probably...
+        /// </summary>
+        public double GetTriInternalAngleR(int tID, int i)
+        {
+            int ti = 3 * tID;
+            int ta = 3 * triangles[ti];
+            Vector3d a = new Vector3d(vertices[ta], vertices[ta + 1], vertices[ta + 2]);
+            int tb = 3 * triangles[ti + 1];
+            Vector3d b = new Vector3d(vertices[tb], vertices[tb + 1], vertices[tb + 2]);
+            int tc = 3 * triangles[ti + 2];
+            Vector3d c = new Vector3d(vertices[tc], vertices[tc + 1], vertices[tc + 2]);
+            if ( i == 0 )
+                return (b-a).Normalized.AngleR((c-a).Normalized);
+            else if ( i == 1 )
+                return (a-b).Normalized.AngleR((c-b).Normalized);
+            else
+                return (a-c).Normalized.AngleR((b-c).Normalized);
+        }
+
 
 
         public Index2i GetEdgeV(int eID) {
@@ -909,12 +945,19 @@ public Vector3d GetEdgePoint(int eID, double t)
         // mesh-building
 
 
+        /// <summary>
+        /// Append new vertex at position, returns new vid
+        /// </summary>
         public int AppendVertex(Vector3d v) {
             return AppendVertex(new NewVertexInfo() {
                 v = v, bHaveC = false, bHaveUV = false, bHaveN = false
             });
         }
-        public int AppendVertex(NewVertexInfo info)
+
+        /// <summary>
+        /// Append new vertex at position and other fields, returns new vid
+        /// </summary>
+        public int AppendVertex(ref NewVertexInfo info)
         {
             int vid = vertices_refcount.allocate();
 			int i = 3*vid;
@@ -948,8 +991,13 @@ public int AppendVertex(NewVertexInfo info)
             updateTimeStamp(true);
             return vid;
         }
+        public int AppendVertex(NewVertexInfo info) {
+            return AppendVertex(ref info);
+        }
 
-        // direct copy from source mesh
+        /// <summary>
+        /// copy vertex fromVID from existing source mesh, returns new vid
+        /// </summary>
         public int AppendVertex(DMesh3 from, int fromVID)
         {
             int bi = 3 * fromVID;
@@ -1002,6 +1050,67 @@ public int AppendVertex(DMesh3 from, int fromVID)
         }
 
 
+
+        /// <summary>
+        /// insert vertex at given index, assuming it is unused
+        /// If bUnsafe, we use fast id allocation that does not update free list.
+        /// You should only be using this between BeginUnsafeVerticesInsert() / EndUnsafeVerticesInsert() calls
+        /// </summary>
+        public MeshResult InsertVertex(int vid, ref NewVertexInfo info, bool bUnsafe = false)
+        {
+            if (vertices_refcount.isValid(vid))
+                return MeshResult.Failed_VertexAlreadyExists;
+
+            bool bOK = (bUnsafe) ? vertices_refcount.allocate_at_unsafe(vid) :
+                                   vertices_refcount.allocate_at(vid);
+            if (bOK == false)
+                return MeshResult.Failed_CannotAllocateVertex;
+
+            int i = 3 * vid;
+            vertices.insert(info.v[2], i + 2);
+            vertices.insert(info.v[1], i + 1);
+            vertices.insert(info.v[0], i);
+
+            if (normals != null) {
+                Vector3f n = (info.bHaveN) ? info.n : Vector3f.AxisY;
+                normals.insert(n[2], i + 2);
+                normals.insert(n[1], i + 1);
+                normals.insert(n[0], i);
+            }
+
+            if (colors != null) {
+                Vector3f c = (info.bHaveC) ? info.c : Vector3f.One;
+                colors.insert(c[2], i + 2);
+                colors.insert(c[1], i + 1);
+                colors.insert(c[0], i);
+            }
+
+            if (uv != null) {
+                Vector2f u = (info.bHaveUV) ? info.uv : Vector2f.Zero;
+                int j = 2 * vid;
+                uv.insert(u[1], j + 1);
+                uv.insert(u[0], j);
+            }
+
+            allocate_edges_list(vid);
+
+            updateTimeStamp(true);
+            return MeshResult.Ok;
+        }
+        public MeshResult InsertVertex(int vid, NewVertexInfo info) {
+            return InsertVertex(vid, ref info);
+        }
+
+
+        public virtual void BeginUnsafeVerticesInsert() {
+            // do nothing...
+        }
+        public virtual void EndUnsafeVerticesInsert() {
+            vertices_refcount.rebuild_free_list();
+        }
+
+
+
         public int AppendTriangle(int v0, int v1, int v2, int gid = -1) {
             return AppendTriangle(new Index3i(v0, v1, v2), gid);
         }
@@ -1063,6 +1172,76 @@ void add_tri_edge(int tid, int v0, int v1, int j, int eid)
 
 
 
+        /// <summary>
+        /// Insert triangle at given index, assuming it is unused.
+        /// If bUnsafe, we use fast id allocation that does not update free list.
+        /// You should only be using this between BeginUnsafeTrianglesInsert() / EndUnsafeTrianglesInsert() calls
+        /// </summary>
+        public MeshResult InsertTriangle(int tid, Index3i tv, int gid = -1, bool bUnsafe = false)
+        {
+            if (triangles_refcount.isValid(tid))
+                return MeshResult.Failed_TriangleAlreadyExists;
+
+            if (IsVertex(tv[0]) == false || IsVertex(tv[1]) == false || IsVertex(tv[2]) == false) {
+                Util.gDevAssert(false);
+                return MeshResult.Failed_NotAVertex;
+            }
+            if (tv[0] == tv[1] || tv[0] == tv[2] || tv[1] == tv[2]) {
+                Util.gDevAssert(false);
+                return MeshResult.Failed_InvalidNeighbourhood;
+            }
+
+            // look up edges. if any already have two triangles, this would 
+            // create non-manifold geometry and so we do not allow it
+            int e0 = find_edge(tv[0], tv[1]);
+            int e1 = find_edge(tv[1], tv[2]);
+            int e2 = find_edge(tv[2], tv[0]);
+            if ((e0 != InvalidID && IsBoundaryEdge(e0) == false)
+                 || (e1 != InvalidID && IsBoundaryEdge(e1) == false)
+                 || (e2 != InvalidID && IsBoundaryEdge(e2) == false)) {
+                return MeshResult.Failed_WouldCreateNonmanifoldEdge;
+            }
+
+            bool bOK = (bUnsafe) ? triangles_refcount.allocate_at_unsafe(tid) :
+                                   triangles_refcount.allocate_at(tid);
+            if (bOK == false)
+                return MeshResult.Failed_CannotAllocateTriangle;
+
+            // now safe to insert triangle
+            int i = 3 * tid;
+            triangles.insert(tv[2], i + 2);
+            triangles.insert(tv[1], i + 1);
+            triangles.insert(tv[0], i);
+            if (triangle_groups != null) {
+                triangle_groups.insert(gid, tid);
+                max_group_id = Math.Max(max_group_id, gid + 1);
+            }
+
+            // increment ref counts and update/create edges
+            vertices_refcount.increment(tv[0]);
+            vertices_refcount.increment(tv[1]);
+            vertices_refcount.increment(tv[2]);
+
+            add_tri_edge(tid, tv[0], tv[1], 0, e0);
+            add_tri_edge(tid, tv[1], tv[2], 1, e1);
+            add_tri_edge(tid, tv[2], tv[0], 2, e2);
+
+            updateTimeStamp(true);
+            return MeshResult.Ok;
+        }
+
+
+        public virtual void BeginUnsafeTrianglesInsert() {
+            // do nothing...
+        }
+        public virtual void EndUnsafeTrianglesInsert() {
+            triangles_refcount.rebuild_free_list();
+        }
+
+
+
+
+
         public void EnableVertexNormals(Vector3f initial_normal)
         {
             if (HasVertexNormals)
@@ -1156,6 +1335,9 @@ public IEnumerable<int> EdgeIndices() {
         }
 
 
+        /// <summary>
+        /// Enumerate ids of boundary edges
+        /// </summary>
         public IEnumerable<int> BoundaryEdgeIndices() {
             foreach ( int eid in edges_refcount ) {
                 if (edges[4 * eid + 3] == InvalidID)
@@ -1164,12 +1346,19 @@ public IEnumerable<int> BoundaryEdgeIndices() {
         }
 
 
+        /// <summary>
+        /// Enumerate vertices
+        /// </summary>
         public IEnumerable<Vector3d> Vertices() {
             foreach (int vid in vertices_refcount) {
                 int i = 3 * vid;
                 yield return new Vector3d(vertices[i], vertices[i + 1], vertices[i + 2]);
             }
         }
+
+        /// <summary>
+        /// Enumerate triangles
+        /// </summary>
         public IEnumerable<Index3i> Triangles() {
             foreach (int tid in triangles_refcount) {
                 int i = 3 * tid;
@@ -1177,7 +1366,9 @@ public IEnumerable<Index3i> Triangles() {
             }
         }
 
-        // return value is [v0,v1,t0,t1], where t1 will be InvalidID if this is a boundary edge
+        /// <summary>
+        /// Enumerage edges. return value is [v0,v1,t0,t1], where t1 will be InvalidID if this is a boundary edge
+        /// </summary>
         public IEnumerable<Index4i> Edges() {
             foreach (int eid in edges_refcount) {
                 int i = 4 * eid;
@@ -1188,21 +1379,30 @@ public IEnumerable<Index4i> Edges() {
 
         // queries
 
-        // linear search through edges of vA
+        /// <summary>
+        /// Find edgeid for edge [a,b]
+        /// </summary>
         public int FindEdge(int vA, int vB) {
             debug_check_is_vertex(vA);
             debug_check_is_vertex(vB);
             return find_edge(vA, vB);
         }
 
-        // faster than FindEdge
-        public int FindEdgeFromTri(int vA, int vB, int t) {
-            return find_edge_from_tri(vA, vB, t);
+        /// <summary>
+        /// Find edgeid for edge [a,b] from triangle that contains the edge.
+        /// This is faster than FindEdge() because it is constant-time
+        /// </summary>
+        public int FindEdgeFromTri(int vA, int vB, int tID) {
+            return find_edge_from_tri(vA, vB, tID);
         }
 
-		// [RMS] this does more work than necessary, see (??? comment never finished...)
+		/// <summary>
+        /// If edge has vertices [a,b], and is connected two triangles [a,b,c] and [a,b,d],
+        /// this returns [c,d], or [c,InvalidID] for a boundary edge
+        /// </summary>
         public Index2i GetEdgeOpposingV(int eID)
         {
+            // [TODO] there was a comment here saying this does more work than necessary??
 			// ** it is important that verts returned maintain [c,d] order!!
 			int i = 4*eID;
             int a = edges[i], b = edges[i + 1];
@@ -1216,6 +1416,9 @@ public Index2i GetEdgeOpposingV(int eID)
         }
 
 
+        /// <summary>
+        /// Find triangle made up of any permutation of vertices [a,b,c]
+        /// </summary>
         public int FindTriangle(int a, int b, int c)
         {
             int eid = find_edge(a, b);
@@ -1238,6 +1441,9 @@ public int FindTriangle(int a, int b, int c)
 
 
 
+        /// <summary>
+        /// Enumerate "other" vertices of edges connected to vertex (ie vertex one-ring)
+        /// </summary>
 		public IEnumerable<int> VtxVerticesItr(int vID) {
 			if ( vertices_refcount.isValid(vID) ) {
                 foreach ( int eid in vertex_edges.ValueItr(vID) )
@@ -1246,6 +1452,9 @@ public IEnumerable<int> VtxVerticesItr(int vID) {
 		}
 
 
+        /// <summary>
+        /// Enumerate edge ids connected to vertex (ie edge one-ring)
+        /// </summary>
 		public IEnumerable<int> VtxEdgesItr(int vID) {
 			if ( vertices_refcount.isValid(vID) ) {
                 return vertex_edges.ValueItr(vID);
@@ -1280,6 +1489,7 @@ public int VtxBoundaryEdges(int vID, ref int e0, ref int e1)
         }
 
         /// <summary>
+        /// Find edge ids of boundary edges connected to vertex.
         /// e needs to be large enough (ie call VtxBoundaryEdges, or as large as max one-ring)
         /// returns count, ie number of elements of e that were filled
         /// </summary>
@@ -1299,7 +1509,10 @@ public int VtxAllBoundaryEdges(int vID, int[] e)
         }
 
 
-
+        /// <summary>
+        /// Get triangle one-ring at vertex. 
+        /// bUseOrientation is more efficient but returns incorrect result if vertex is a bowtie
+        /// </summary>
         public MeshResult GetVtxTriangles(int vID, List<int> vTriangles, bool bUseOrientation)
         {
             if (!IsVertex(vID))
@@ -1363,6 +1576,9 @@ public int GetVtxTriangleCount(int vID, bool bBruteForce = false)
         }
 
 
+        /// <summary>
+        /// iterate over triangle IDs of vertex one-ring
+        /// </summary>
 		public IEnumerable<int> VtxTrianglesItr(int vID) {
 			if ( IsVertex(vID) ) {
 				foreach (int eid in vertex_edges.ValueItr(vID)) {
@@ -1379,9 +1595,10 @@ public IEnumerable<int> VtxTrianglesItr(int vID) {
 		}
 
 
-
-		// from edge and vert, returns other vert, two opposing verts, and two triangles
-		public void GetVtxNbrhood(int eID, int vID, ref int vOther, ref int oppV1, ref int oppV2, ref int t1, ref int t2)
+        /// <summary>
+        ///  from edge and vert, returns other vert, two opposing verts, and two triangles
+        /// </summary>
+        public void GetVtxNbrhood(int eID, int vID, ref int vOther, ref int oppV1, ref int oppV2, ref int t1, ref int t2)
 		{
 			int i = 4*eID;
 			vOther = (edges[i] == vID) ? edges[i+1] : edges[i];
@@ -1395,6 +1612,31 @@ public void GetVtxNbrhood(int eID, int vID, ref int vOther, ref int oppV1, ref i
 		}
 
 
+        /// <summary>
+        /// Fastest possible one-ring centroid. This is used inside many other algorithms
+        /// so it helps to have it be maximally efficient
+        /// </summary>
+        public void VtxOneRingCentroid(int vID, ref Vector3d centroid)
+        {
+            centroid = Vector3d.Zero;
+            if (vertices_refcount.isValid(vID)) {
+                int n = 0;
+                foreach (int eid in vertex_edges.ValueItr(vID)) {
+                    int other_idx = 3 * edge_other_v(eid, vID);
+                    centroid.x += vertices[other_idx];
+                    centroid.y += vertices[other_idx + 1];
+                    centroid.z += vertices[other_idx + 2];
+                    n++;
+                }
+                if (n > 0) {
+                    double d = 1.0 / n;
+                    centroid.x *= d; centroid.y *= d; centroid.z *= d;
+                }
+            }
+        }
+
+
+
         public bool tri_has_v(int tID, int vID) {
 			int i = 3*tID;
             return triangles[i] == vID 
@@ -1493,6 +1735,9 @@ public bool IsBoundaryVertex(int vID) {
         }
 
 
+        /// <summary>
+        /// Returns true if any edge of triangle is a boundary edge
+        /// </summary>
         public bool IsBoundaryTriangle(int tID)
         {
             debug_check_is_triangle(tID);
@@ -1541,6 +1786,9 @@ int find_edge_from_tri(int vA, int vB, int tID)
         // queries
 
 
+        /// <summary>
+        /// Returns true if the two triangles connected to edge have different group IDs
+        /// </summary>
         public bool IsGroupBoundaryEdge(int eID)
         {
             if ( IsEdge(eID) == false )
@@ -1557,7 +1805,9 @@ public bool IsGroupBoundaryEdge(int eID)
         }
 
 
-        // returns true if vertex has more than one tri groups in its tri nbrhood
+        /// <summary>
+        /// returns true if vertex has more than one tri group in its tri nbrhood
+        /// </summary>
         public bool IsGroupBoundaryVertex(int vID)
         {
             if (IsVertex(vID) == false)
@@ -1586,7 +1836,9 @@ public bool IsGroupBoundaryVertex(int vID)
 
 
 
-        // returns true if more than two group border edges meet at vertex
+        /// <summary>
+        /// returns true if more than two group boundary edges meet at vertex (ie 3+ groups meet at this vertex)
+        /// </summary>
         public bool IsGroupJunctionVertex(int vID)
         {
             if (IsVertex(vID) == false)
@@ -1615,7 +1867,7 @@ public bool IsGroupJunctionVertex(int vID)
 
 
         /// <summary>
-        /// returns up to 4 group IDs at input vid. Returns false if > 4 encountered
+        /// returns up to 4 group IDs at vertex. Returns false if > 4 encountered
         /// </summary>
         public bool GetVertexGroups(int vID, out Index4i groups)
         {
@@ -1648,7 +1900,7 @@ public bool GetVertexGroups(int vID, out Index4i groups)
 
 
         /// <summary>
-        /// returns up to 4 group IDs at input vid. Returns false if > 4 encountered
+        /// returns all group IDs at vertex
         /// </summary>
         public bool GetAllVertexGroups(int vID, ref List<int> groups)
         {
@@ -1684,17 +1936,64 @@ public List<int> GetAllVertexGroups(int vID) {
         public bool IsBowtieVertex(int vID)
         {
             if (vertices_refcount.isValid(vID)) {
-                int nTris = GetVtxTriangleCount(vID);
-                int vtx_edge_count = GetVtxEdgeCount(vID);
-                if (!(nTris == vtx_edge_count || nTris == vtx_edge_count - 1))
-                    return true;
-                return false;
+				int nEdges = vertex_edges.Count(vID);
+				if (nEdges == 0)
+					return false;
+
+                // find a boundary edge to start at
+                int start_eid = -1;
+                bool start_at_boundary = false;
+                foreach (int eid in vertex_edges.ValueItr(vID)) {
+                    if (edges[4 * eid + 3] == DMesh3.InvalidID) {
+                        start_at_boundary = true;
+                        start_eid = eid;
+                        break;
+                    }
+                }
+                // if no boundary edge, start at arbitrary edge
+                if (start_eid == -1)
+                    start_eid = vertex_edges.First(vID);
+                // initial triangle
+                int start_tid = edges[4 * start_eid + 2];
+
+                int prev_tid = start_tid;
+                int prev_eid = start_eid;
+
+                // walk forward to next edge. if we hit start edge or boundary edge,
+                // we are done the walk. count number of edges as we go.
+                int count = 1;
+                while (true) {
+                    int i = 3 * prev_tid;
+                    Index3i tv = new Index3i(triangles[i], triangles[i+1], triangles[i+2]);
+                    Index3i te = new Index3i(triangle_edges[i], triangle_edges[i+1], triangle_edges[i+2]);
+                    int vert_idx = IndexUtil.find_tri_index(vID, ref tv);
+                    int e1 = te[vert_idx], e2 = te[(vert_idx+2) % 3];
+                    int next_eid = (e1 == prev_eid) ? e2 : e1;
+                    if (next_eid == start_eid)
+                        break;
+                    Index2i next_eid_tris = GetEdgeT(next_eid);
+                    int next_tid = (next_eid_tris.a == prev_tid) ? next_eid_tris.b : next_eid_tris.a;
+                    if (next_tid == DMesh3.InvalidID) {
+                        break;
+                    }
+                    prev_eid = next_eid;
+                    prev_tid = next_tid;
+                    count++;
+                }
+
+                // if we did not see all edges at vertex, we have a bowtie
+                int target_count = (start_at_boundary) ? nEdges - 1 : nEdges;
+                bool is_bowtie = (target_count != count);
+                return is_bowtie;
+
             } else
                 throw new Exception("DMesh3.IsBowtieVertex: " + vID + " is not a valid vertex");
         }
 
 
-        // compute vertex bounding box
+        /// <summary>
+        /// Computes bounding box of all vertices.
+        /// </summary>
         public AxisAlignedBox3d GetBounds()
         {
             double x = 0, y = 0, z = 0;
@@ -1715,7 +2014,9 @@ public AxisAlignedBox3d GetBounds()
         AxisAlignedBox3d cached_bounds;
         int cached_bounds_timestamp = -1;
 
-        //! cached bounding box, lazily re-computed on access if mesh has changed
+        /// <summary>
+        /// cached bounding box, lazily re-computed on access if mesh has changed
+        /// </summary>
         public AxisAlignedBox3d CachedBounds
         {
             get {
@@ -1731,7 +2032,7 @@ public AxisAlignedBox3d CachedBounds
 
 
         bool cached_is_closed = false;
-        int cached_is_closed_timstamp = -1;
+        int cached_is_closed_timestamp = -1;
 
         public bool IsClosed() {
             if (TriangleCount == 0)
@@ -1752,9 +2053,9 @@ public bool IsClosed() {
 
         public bool CachedIsClosed {
             get {
-                if (cached_is_closed_timstamp != Timestamp) {
+                if (cached_is_closed_timestamp != Timestamp) {
                     cached_is_closed = IsClosed();
-                    cached_is_closed_timstamp = Timestamp;
+                    cached_is_closed_timestamp = Timestamp;
                 }
                 return cached_is_closed;
             }
@@ -1763,16 +2064,26 @@ public bool CachedIsClosed {
 
 
 
+        /// <summary> returns true if vertices, edges, and triangles are all "dense" (Count == MaxID) </summary>
         public bool IsCompact {
             get { return vertices_refcount.is_dense && edges_refcount.is_dense && triangles_refcount.is_dense; }
         }
+
+        /// <summary> Returns true if vertex count == max vertex id </summary>
         public bool IsCompactV {
             get { return vertices_refcount.is_dense; }
         }
+
+        /// <summary> returns true if triangle count == max triangle id </summary>
         public bool IsCompactT {
             get { return triangles_refcount.is_dense; }
         }
 
+        /// <summary> returns measure of compactness in range [0,1], where 1 is fully compacted </summary>
+        public double CompactMetric {
+            get { return ((double)VertexCount / (double)MaxVertexID + (double)TriangleCount / (double)MaxTriangleID) * 0.5; }
+        }
+
 
 
         /// <summary>
@@ -1882,9 +2193,11 @@ public SmallListSet VertexEdges {
 
 
 
-        // assumes that we have initialized vertices, triangles, and edges buffers,
-        // and edges refcounts. Rebuilds vertex and tri refcounts, triangle edges,
-        // vertex edges
+        /// <summary>
+        /// Rebuild mesh topology.
+        /// assumes that we have initialized vertices, triangles, and edges buffers,
+        /// and edges refcounts. Rebuilds vertex and tri refcounts, triangle edges, vertex edges.
+        /// </summary>
         public void RebuildFromEdgeRefcounts()
         {
             int MaxVID = vertices.Length / 3;
diff --git a/mesh/DMesh3Changes.cs b/mesh/DMesh3Changes.cs
new file mode 100644
index 00000000..3f673d57
--- /dev/null
+++ b/mesh/DMesh3Changes.cs
@@ -0,0 +1,495 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace g3
+{
+    /// <summary>
+    /// Mesh change for vertex deformations. Currently minimal support for initializing buffers.
+    /// AppendNewVertex() can be used to accumulate modified vertices and their initial positions.
+    /// </summary>
+    public class ModifyVerticesMeshChange
+    {
+        public DVector<int> ModifiedV;
+        public DVector<Vector3d> OldPositions, NewPositions;
+        public DVector<Vector3f> OldNormals, NewNormals;
+        public DVector<Vector3f> OldColors, NewColors;
+        public DVector<Vector2f> OldUVs, NewUVs;
+
+        public Action<ModifyVerticesMeshChange> OnApplyF;
+        public Action<ModifyVerticesMeshChange> OnRevertF;
+
+        public ModifyVerticesMeshChange(DMesh3 mesh, MeshComponents wantComponents = MeshComponents.All)
+        {
+            initialize_buffers(mesh, wantComponents);
+        }
+
+
+        public int AppendNewVertex(DMesh3 mesh, int vid)
+        {
+            int idx = ModifiedV.Length;
+            ModifiedV.Add(vid);
+            OldPositions.Add(mesh.GetVertex(vid));
+            NewPositions.Add(OldPositions[idx]);
+            if (NewNormals != null) {
+                OldNormals.Add(mesh.GetVertexNormal(vid));
+                NewNormals.Add(OldNormals[idx]);
+            }
+            if (NewColors != null) {
+                OldColors.Add(mesh.GetVertexColor(vid));
+                NewColors.Add(OldColors[idx]);
+            }
+            if (NewUVs != null) {
+                OldUVs.Add(mesh.GetVertexUV(vid));
+                NewUVs.Add(OldUVs[idx]);
+            }
+            return idx;
+        }
+
+        public void Apply(DMesh3 mesh)
+        {
+            int N = ModifiedV.size;
+            for (int i = 0; i < N; ++i) {
+                int vid = ModifiedV[i];
+                mesh.SetVertex(vid, NewPositions[i]);
+                if (NewNormals != null)
+                    mesh.SetVertexNormal(vid, NewNormals[i]);
+                if (NewColors != null)
+                    mesh.SetVertexColor(vid, NewColors[i]);
+                if (NewUVs != null)
+                    mesh.SetVertexUV(vid, NewUVs[i]);
+            }
+            if (OnApplyF != null)
+                OnApplyF(this);
+        }
+
+
+        public void Revert(DMesh3 mesh)
+        {
+            int N = ModifiedV.size;
+            for (int i = 0; i < N; ++i) {
+                int vid = ModifiedV[i];
+                mesh.SetVertex(vid, OldPositions[i]);
+                if (NewNormals != null)
+                    mesh.SetVertexNormal(vid, OldNormals[i]);
+                if (NewColors != null)
+                    mesh.SetVertexColor(vid, OldColors[i]);
+                if (NewUVs != null)
+                    mesh.SetVertexUV(vid, OldUVs[i]);
+            }
+            if (OnRevertF != null)
+                OnRevertF(this);
+        }
+
+
+        void initialize_buffers(DMesh3 mesh, MeshComponents components)
+        {
+            ModifiedV = new DVector<int>();
+            NewPositions = new DVector<Vector3d>();
+            OldPositions = new DVector<Vector3d>();
+            if (mesh.HasVertexNormals && (components & MeshComponents.VertexNormals) != 0) {
+                NewNormals = new DVector<Vector3f>();
+                OldNormals = new DVector<Vector3f>();
+            }
+            if (mesh.HasVertexColors && (components & MeshComponents.VertexColors) != 0) {
+                NewColors = new DVector<Vector3f>();
+                OldColors = new DVector<Vector3f>();
+            }
+            if (mesh.HasVertexUVs && (components & MeshComponents.VertexUVs) != 0) {
+                NewUVs = new DVector<Vector2f>();
+                OldUVs = new DVector<Vector2f>();
+            }
+        }
+
+    }
+
+
+
+
+
+
+
+
+    /// <summary>
+    /// Mesh change for full-mesh vertex deformations - more efficient than ModifyVerticesMeshChange.
+    /// Note that this does not enforce that vertex count does not change!
+    /// </summary>
+    public class SetVerticesMeshChange
+    {
+        public DVector<double> OldPositions, NewPositions;
+        public DVector<float> OldNormals, NewNormals;
+        public DVector<float> OldColors, NewColors;
+        public DVector<float> OldUVs, NewUVs;
+
+        public Action<SetVerticesMeshChange> OnApplyF;
+        public Action<SetVerticesMeshChange> OnRevertF;
+
+        public SetVerticesMeshChange()
+        {
+        }
+
+        public void Apply(DMesh3 mesh)
+        {
+            if ( NewPositions != null )
+                mesh.VerticesBuffer.copy(NewPositions);
+            if (mesh.HasVertexNormals && NewNormals != null)
+                mesh.NormalsBuffer.copy(NewNormals);
+            if (mesh.HasVertexColors&& NewColors != null)
+                mesh.ColorsBuffer.copy(NewColors);
+            if (mesh.HasVertexUVs && NewUVs != null)
+                mesh.UVBuffer.copy(NewUVs);
+            if (OnApplyF != null)
+                OnApplyF(this);
+        }
+
+
+        public void Revert(DMesh3 mesh)
+        {
+            if ( OldPositions != null )
+                mesh.VerticesBuffer.copy(OldPositions);
+            if (mesh.HasVertexNormals && OldNormals != null)
+                mesh.NormalsBuffer.copy(OldNormals);
+            if (mesh.HasVertexColors && OldColors != null)
+                mesh.ColorsBuffer.copy(OldColors);
+            if (mesh.HasVertexUVs && OldUVs != null)
+                mesh.UVBuffer.copy(OldUVs);
+            if (OnRevertF != null)
+                OnRevertF(this);
+        }
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+    /// <summary>
+    /// Remove triangles from mesh and store necessary data to be able to reverse the change.
+    /// Vertex and Triangle IDs will be restored on Revert()
+    /// Currently does *not* restore the same EdgeIDs
+    /// </summary>
+    public class RemoveTrianglesMeshChange
+    {
+        protected DVector<int> RemovedV;
+        protected DVector<Vector3d> Positions;
+        protected DVector<Vector3f> Normals;
+        protected DVector<Vector3f> Colors;
+        protected DVector<Vector2f> UVs;
+
+        protected DVector<int> RemovedT;
+        protected DVector<Index4i> Triangles;
+
+        public Action<IEnumerable<int>,IEnumerable<int>> OnApplyF;
+        public Action<IEnumerable<int>, IEnumerable<int>> OnRevertF;
+
+        public RemoveTrianglesMeshChange()
+        {
+        }
+
+
+        public void InitializeFromApply(DMesh3 mesh, IEnumerable<int> triangles)
+        {
+            initialize_buffers(mesh);
+            bool has_groups = mesh.HasTriangleGroups;
+
+            foreach ( int tid in triangles ) { 
+                if (!mesh.IsTriangle(tid))
+                    continue;
+
+                Index3i tv = mesh.GetTriangle(tid);
+                bool va = save_vertex(mesh, tv.a);
+                bool vb = save_vertex(mesh, tv.b);
+                bool vc = save_vertex(mesh, tv.c);
+
+                Index4i tri = new Index4i(tv.a, tv.b, tv.c,
+                    has_groups ? mesh.GetTriangleGroup(tid) : DMesh3.InvalidID);
+                RemovedT.Add(tid);
+                Triangles.Add(tri);
+
+                MeshResult result = mesh.RemoveTriangle(tid, true, false);
+                if (result != MeshResult.Ok)
+                    throw new Exception("RemoveTrianglesMeshChange.Initialize: exception in RemoveTriangle(" + tid.ToString() + "): " + result.ToString());
+                Util.gDevAssert(mesh.IsVertex(tv.a) == va && mesh.IsVertex(tv.b) == vb && mesh.IsVertex(tv.c) == vc);
+            }
+        }
+
+
+
+        public void InitializeFromExisting(DMesh3 mesh, IEnumerable<int> remove_t)
+        {
+            initialize_buffers(mesh);
+            bool has_groups = mesh.HasTriangleGroups;
+
+            HashSet<int> triangles = new HashSet<int>(remove_t);
+            HashSet<int> vertices = new HashSet<int>();
+            IndexUtil.TrianglesToVertices(mesh, remove_t, vertices);
+            List<int> save_v = new List<int>();
+            foreach ( int vid in vertices ) {
+                bool all_contained = true;
+                foreach ( int tid in mesh.VtxTrianglesItr(vid) ) {
+                    if (triangles.Contains(tid) == false) {
+                        all_contained = false;
+                        break;
+                    }
+                }
+                if (all_contained)
+                    save_v.Add(vid);
+            }
+
+            foreach (int vid in save_v) {
+                save_vertex(mesh, vid, true);
+            }
+
+            foreach (int tid in remove_t) {
+                Util.gDevAssert(mesh.IsTriangle(tid));
+                Index3i tv = mesh.GetTriangle(tid);
+                Index4i tri = new Index4i(tv.a, tv.b, tv.c,
+                    has_groups ? mesh.GetTriangleGroup(tid) : DMesh3.InvalidID);
+                RemovedT.Add(tid);
+                Triangles.Add(tri);
+            }
+        }
+
+
+
+        public void Apply(DMesh3 mesh)
+        {
+            int N = RemovedT.size;
+            for ( int i = 0; i< N; ++i) {
+                int tid = RemovedT[i];
+                MeshResult result = mesh.RemoveTriangle(RemovedT[i], true, false);
+                if (result != MeshResult.Ok)
+                    throw new Exception("RemoveTrianglesMeshChange.Apply: error in RemoveTriangle(" + tid.ToString() + "): " + result.ToString());
+            }
+
+            if ( OnApplyF != null )
+                OnApplyF(RemovedV, RemovedT);
+        }
+
+
+        public void Revert(DMesh3 mesh)
+        {
+            int NV = RemovedV.size;
+            if (NV > 0) {
+                NewVertexInfo vinfo = new NewVertexInfo(Positions[0]);
+                mesh.BeginUnsafeVerticesInsert();
+                for (int i = 0; i < NV; ++i) {
+                    int vid = RemovedV[i];
+                    vinfo.v = Positions[i];
+                    if (Normals != null) { vinfo.bHaveN = true; vinfo.n = Normals[i]; }
+                    if (Colors != null) { vinfo.bHaveC = true; vinfo.c = Colors[i]; }
+                    if (UVs != null) { vinfo.bHaveUV = true; vinfo.uv = UVs[i]; }
+                    MeshResult result = mesh.InsertVertex(vid, ref vinfo, true);
+                    if (result != MeshResult.Ok)
+                        throw new Exception("RemoveTrianglesMeshChange.Revert: error in InsertVertex(" + vid.ToString() + "): " + result.ToString());
+                }
+                mesh.EndUnsafeVerticesInsert();
+            }
+
+            int NT = RemovedT.size;
+            if (NT > 0) {
+                mesh.BeginUnsafeTrianglesInsert();
+                for (int i = 0; i < NT; ++i) {
+                    int tid = RemovedT[i];
+                    Index4i tdata = Triangles[i];
+                    Index3i tri = new Index3i(tdata.a, tdata.b, tdata.c);
+                    MeshResult result = mesh.InsertTriangle(tid, tri, tdata.d, true);
+                    if (result != MeshResult.Ok)
+                        throw new Exception("RemoveTrianglesMeshChange.Revert: error in InsertTriangle(" + tid.ToString() + "): " + result.ToString());
+                }
+                mesh.EndUnsafeTrianglesInsert();
+            }
+
+            if (OnRevertF != null)
+                OnRevertF(RemovedV, RemovedT);
+        }
+
+
+
+
+        bool save_vertex(DMesh3 mesh, int vid, bool force = false)
+        {
+            if ( force || mesh.VerticesRefCounts.refCount(vid) == 2 ) {
+                RemovedV.Add(vid);
+                Positions.Add(mesh.GetVertex(vid));
+                if (Normals != null)
+                    Normals.Add(mesh.GetVertexNormal(vid));
+                if (Colors != null)
+                    Colors.Add(mesh.GetVertexColor(vid));
+                if (UVs != null)
+                    UVs.Add(mesh.GetVertexUV(vid));
+                return false;
+            }
+            return true;
+        }
+
+
+        void initialize_buffers(DMesh3 mesh)
+        {
+            RemovedV = new DVector<int>();
+            Positions = new DVector<Vector3d>();
+            if (mesh.HasVertexNormals)
+                Normals = new DVector<Vector3f>();
+            if (mesh.HasVertexColors)
+                Colors = new DVector<Vector3f>();
+            if (mesh.HasVertexUVs)
+                UVs = new DVector<Vector2f>();
+
+            RemovedT = new DVector<int>();
+            Triangles = new DVector<Index4i>();
+        }
+
+    }
+
+
+
+
+
+
+
+
+    /// <summary>
+    /// Add triangles from mesh and store necessary data to be able to reverse the change.
+    /// Vertex and Triangle IDs will be restored on Revert()
+    /// Currently does *not* restore the same EdgeIDs
+    /// </summary>
+    public class AddTrianglesMeshChange
+    {
+        protected DVector<int> AddedV;
+        protected DVector<Vector3d> Positions;
+        protected DVector<Vector3f> Normals;
+        protected DVector<Vector3f> Colors;
+        protected DVector<Vector2f> UVs;
+
+        protected DVector<int> AddedT;
+        protected DVector<Index4i> Triangles;
+
+        public Action<IEnumerable<int>, IEnumerable<int>> OnApplyF;
+        public Action<IEnumerable<int>, IEnumerable<int>> OnRevertF;
+
+
+        public AddTrianglesMeshChange()
+        {
+        }
+
+
+        public void InitializeFromExisting(DMesh3 mesh, IEnumerable<int> added_v, IEnumerable<int> added_t)
+        {
+            initialize_buffers(mesh);
+            bool has_groups = mesh.HasTriangleGroups;
+
+            if (added_v != null) {
+                foreach (int vid in added_v) {
+                    Util.gDevAssert(mesh.IsVertex(vid));
+                    append_vertex(mesh, vid);
+                }
+            }
+
+            foreach (int tid in added_t) {
+                Util.gDevAssert(mesh.IsTriangle(tid));
+
+                Index3i tv = mesh.GetTriangle(tid);
+                Index4i tri = new Index4i(tv.a, tv.b, tv.c,
+                    has_groups ? mesh.GetTriangleGroup(tid) : DMesh3.InvalidID);
+                AddedT.Add(tid);
+                Triangles.Add(tri);
+            }
+        }
+
+
+        public void Apply(DMesh3 mesh)
+        {
+            int NV = AddedV.size;
+            if (NV > 0) {
+                NewVertexInfo vinfo = new NewVertexInfo(Positions[0]);
+                mesh.BeginUnsafeVerticesInsert();
+                for (int i = 0; i < NV; ++i) {
+                    int vid = AddedV[i];
+                    vinfo.v = Positions[i];
+                    if (Normals != null) { vinfo.bHaveN = true; vinfo.n = Normals[i]; }
+                    if (Colors != null) { vinfo.bHaveC = true; vinfo.c = Colors[i]; }
+                    if (UVs != null) { vinfo.bHaveUV = true; vinfo.uv = UVs[i]; }
+                    MeshResult result = mesh.InsertVertex(vid, ref vinfo, true);
+                    if (result != MeshResult.Ok)
+                        throw new Exception("AddTrianglesMeshChange.Revert: error in InsertVertex(" + vid.ToString() + "): " + result.ToString());
+                }
+                mesh.EndUnsafeVerticesInsert();
+            }
+
+            int NT = AddedT.size;
+            if (NT > 0) {
+                mesh.BeginUnsafeTrianglesInsert();
+                for (int i = 0; i < NT; ++i) {
+                    int tid = AddedT[i];
+                    Index4i tdata = Triangles[i];
+                    Index3i tri = new Index3i(tdata.a, tdata.b, tdata.c);
+                    MeshResult result = mesh.InsertTriangle(tid, tri, tdata.d, true);
+                    if (result != MeshResult.Ok)
+                        throw new Exception("AddTrianglesMeshChange.Revert: error in InsertTriangle(" + tid.ToString() + "): " + result.ToString());
+                }
+                mesh.EndUnsafeTrianglesInsert();
+            }
+
+            if (OnApplyF != null)
+                OnApplyF(AddedV, AddedT);
+        }
+
+
+        public void Revert(DMesh3 mesh)
+        {
+            int N = AddedT.size;
+            for (int i = 0; i < N; ++i) {
+                int tid = AddedT[i];
+                MeshResult result = mesh.RemoveTriangle(AddedT[i], true, false);
+                if (result != MeshResult.Ok)
+                    throw new Exception("AddTrianglesMeshChange.Apply: error in RemoveTriangle(" + tid.ToString() + "): " + result.ToString());
+            }
+
+            if ( OnRevertF != null )
+                OnRevertF(AddedV, AddedT);
+        }
+
+
+
+
+        void append_vertex(DMesh3 mesh, int vid)
+        {
+            AddedV.Add(vid);
+            Positions.Add(mesh.GetVertex(vid));
+            if (Normals != null)
+                Normals.Add(mesh.GetVertexNormal(vid));
+            if (Colors != null)
+                Colors.Add(mesh.GetVertexColor(vid));
+            if (UVs != null)
+                UVs.Add(mesh.GetVertexUV(vid));
+        }
+
+
+        void initialize_buffers(DMesh3 mesh)
+        {
+            AddedV = new DVector<int>();
+            Positions = new DVector<Vector3d>();
+            if (mesh.HasVertexNormals)
+                Normals = new DVector<Vector3f>();
+            if (mesh.HasVertexColors)
+                Colors = new DVector<Vector3f>();
+            if (mesh.HasVertexUVs)
+                UVs = new DVector<Vector2f>();
+
+            AddedT = new DVector<int>();
+            Triangles = new DVector<Index4i>();
+        }
+
+    }
+
+
+
+
+}
diff --git a/mesh/DMesh3_edge_operators.cs b/mesh/DMesh3_edge_operators.cs
index d7862393..1df7b540 100644
--- a/mesh/DMesh3_edge_operators.cs
+++ b/mesh/DMesh3_edge_operators.cs
@@ -47,7 +47,7 @@ public void ReverseOrientation(bool bFlipNormals = true) {
         /// (if false, them throws exception if there are still any triangles!)
         /// if bPreserveManifold, checks that we will not create a bowtie vertex first
         /// </summary>
-        public MeshResult RemoveVertex(int vID, bool bRemoveAllTriangles = true, bool bPreserveManifold = true)
+        public MeshResult RemoveVertex(int vID, bool bRemoveAllTriangles = true, bool bPreserveManifold = false)
         {
             if (vertices_refcount.isValid(vID) == false)
                 return MeshResult.Failed_NotAVertex;
@@ -59,7 +59,7 @@ public MeshResult RemoveVertex(int vID, bool bRemoveAllTriangles = true, bool bP
                 if ( bPreserveManifold ) {
                     foreach ( int tid in VtxTrianglesItr(vID) ) {
                         Index3i tri = GetTriangle(tid);
-                        int j = IndexUtil.find_tri_index(vID, tri);
+                        int j = IndexUtil.find_tri_index(vID, ref tri);
                         int oa = tri[(j + 1) % 3], ob = tri[(j + 2) % 3];
                         int eid = find_edge(oa,ob);
                         if (IsBoundaryEdge(eid))
@@ -98,7 +98,7 @@ public MeshResult RemoveVertex(int vID, bool bRemoveAllTriangles = true, bool bP
         ///   If this check is not done, you have to make sure you don't create a bowtie, because other
         ///   code assumes we don't have bowties, and will not handle it properly
         /// </summary>
-        public MeshResult RemoveTriangle(int tID, bool bRemoveIsolatedVertices = true, bool bPreserveManifold = true)
+        public MeshResult RemoveTriangle(int tID, bool bRemoveIsolatedVertices = true, bool bPreserveManifold = false)
         {
             if ( ! triangles_refcount.isValid(tID) ) {
                 Debug.Assert(false);
@@ -262,6 +262,8 @@ public struct EdgeSplitInfo {
 			public int eNewBN;      // new edge [vNew,vB] (original was AB)
 			public int eNewCN;      // new edge [vNew,vC] (C is "first" other vtx in ring)
 			public int eNewDN;		// new edge [vNew,vD] (D is "second" other, which doesn't exist on bdry)
+            public int eNewT2;
+            public int eNewT3;
 		}
 		public MeshResult SplitEdge(int vA, int vB, out EdgeSplitInfo split)
 		{
@@ -272,7 +274,11 @@ public MeshResult SplitEdge(int vA, int vB, out EdgeSplitInfo split)
 			}
 			return SplitEdge(eid, out split);
 		}
-		public MeshResult SplitEdge(int eab, out EdgeSplitInfo split)
+        /// <summary>
+        /// Split edge eab. 
+        /// split_t defines position along edge, and is assumed to be based on order of vertices returned by GetEdgeV()
+        /// </summary>
+		public MeshResult SplitEdge(int eab, out EdgeSplitInfo split, double split_t = 0.5)
 		{
 			split = new EdgeSplitInfo();
 			if (! IsEdge(eab) )
@@ -287,24 +293,27 @@ public MeshResult SplitEdge(int eab, out EdgeSplitInfo split)
 			Index3i T0tv = GetTriangle(t0);
 			int[] T0tv_array = T0tv.array;
 			int c = IndexUtil.orient_tri_edge_and_find_other_vtx(ref a, ref b, T0tv_array);
-
-			// create new vertex
-			Vector3d vNew = 0.5 * ( GetVertex(a) + GetVertex(b) );
-			int f = AppendVertex( vNew );
-            if (HasVertexNormals) 
-                SetVertexNormal(f, (GetVertexNormal(a) + GetVertexNormal(b)).Normalized);
-            if (HasVertexColors)
-                SetVertexColor(f, 0.5f * (GetVertexColor(a) + GetVertexColor(b)) );
-            if (HasVertexUVs)
-                SetVertexUV(f, 0.5f * (GetVertexUV(a) + GetVertexUV(b)));
-
+            if (vertices_refcount.rawRefCount(c) > 32764)
+                return MeshResult.Failed_HitValenceLimit;
+            if (a != edges[eab_i])
+                split_t = 1.0 - split_t;    // if we flipped a/b order we need to reverse t
 
             // quite a bit of code is duplicated between boundary and non-boundary case, but it
             //  is too hard to follow later if we factor it out...
             if ( IsBoundaryEdge(eab) ) {
 
-				// look up edge bc, which needs to be modified
-				Index3i T0te = GetTriEdges(t0);
+                // create new vertex
+                Vector3d vNew = Vector3d.Lerp(GetVertex(a), GetVertex(b), split_t);
+                int f = AppendVertex(vNew);
+                if (HasVertexNormals)
+                    SetVertexNormal(f, Vector3f.Lerp(GetVertexNormal(a), GetVertexNormal(b), (float)split_t).Normalized);
+                if (HasVertexColors)
+                    SetVertexColor(f, Colorf.Lerp(GetVertexColor(a), GetVertexColor(b), (float)split_t));
+                if (HasVertexUVs)
+                    SetVertexUV(f, Vector2f.Lerp(GetVertexUV(a), GetVertexUV(b), (float)split_t));
+
+                // look up edge bc, which needs to be modified
+                Index3i T0te = GetTriEdges(t0);
 				int ebc = T0te[ IndexUtil.find_edge_index_in_tri(b, c, T0tv_array) ];
 
 				// rewrite existing triangle
@@ -339,6 +348,8 @@ public MeshResult SplitEdge(int eab, out EdgeSplitInfo split)
                 split.eNewBN = efb;
 				split.eNewCN = efc;
 				split.eNewDN = InvalidID;
+                split.eNewT2 = t2;
+                split.eNewT3 = InvalidID;
 
 				updateTimeStamp(true);
 				return MeshResult.Ok;
@@ -350,10 +361,22 @@ public MeshResult SplitEdge(int eab, out EdgeSplitInfo split)
 				Index3i T1tv = GetTriangle(t1);
 				int[] T1tv_array = T1tv.array;
 				int d = IndexUtil.find_tri_other_vtx( a, b, T1tv_array );
-
-				// look up edges that we are going to need to update
-				// [TODO OPT] could use ordering to reduce # of compares here
-				Index3i T0te = GetTriEdges(t0);
+                if (vertices_refcount.rawRefCount(d) > 32764) 
+                    return MeshResult.Failed_HitValenceLimit;
+
+                // create new vertex
+                Vector3d vNew = Vector3d.Lerp(GetVertex(a), GetVertex(b), split_t);
+                int f = AppendVertex(vNew);
+                if (HasVertexNormals)
+                    SetVertexNormal(f, Vector3f.Lerp(GetVertexNormal(a), GetVertexNormal(b), (float)split_t).Normalized);
+                if (HasVertexColors)
+                    SetVertexColor(f, Colorf.Lerp(GetVertexColor(a), GetVertexColor(b), (float)split_t));
+                if (HasVertexUVs)
+                    SetVertexUV(f, Vector2f.Lerp(GetVertexUV(a), GetVertexUV(b), (float)split_t));
+
+                // look up edges that we are going to need to update
+                // [TODO OPT] could use ordering to reduce # of compares here
+                Index3i T0te = GetTriEdges(t0);
 				int ebc = T0te[IndexUtil.find_edge_index_in_tri( b, c, T0tv_array )];
 				Index3i T1te = GetTriEdges(t1);
 				int edb = T1te[IndexUtil.find_edge_index_in_tri( d, b, T1tv_array )];
@@ -403,8 +426,10 @@ public MeshResult SplitEdge(int eab, out EdgeSplitInfo split)
                 split.eNewBN = efb;
 				split.eNewCN = efc;
 				split.eNewDN = edf;
+                split.eNewT2 = t2;
+                split.eNewT3 = t3;
 
-				updateTimeStamp(true);
+                updateTimeStamp(true);
 				return MeshResult.Ok;
 			}
 
@@ -819,9 +844,41 @@ public MeshResult MergeEdges(int eKeep, int eDiscard, out MergeEdgesInfo merge_i
 			merge_info.eKept = eab;
 			merge_info.eRemoved = ecd;
 
-			// [TODO] this acts on each interior tri twice. could avoid using vtx-tri iterator?
+            // if a/c or b/d are connected by an existing edge, we can't merge
+            if (a != c && find_edge(a,c) != DMesh3.InvalidID )
+                return MeshResult.Failed_InvalidNeighbourhood;
+            if (b != d && find_edge(b, d) != DMesh3.InvalidID)
+                return MeshResult.Failed_InvalidNeighbourhood;
+
+            // if vertices at either end already share a common neighbour vertex, and we 
+            // do the merge, that would create duplicate edges. This is something like the
+            // 'link condition' in edge collapses. 
+            // Note that we have to catch cases where both edges to the shared vertex are
+            // boundary edges, in that case we will also merge this edge later on
+            if ( a != c ) {
+                int ea = 0, ec = 0, other_v = (b == d) ? b : -1;
+                foreach ( int cnbr in VtxVerticesItr(c) ) {
+                    if (cnbr != other_v && (ea = find_edge(a, cnbr)) != DMesh3.InvalidID) {
+                        ec = find_edge(c, cnbr);
+                        if (IsBoundaryEdge(ea) == false || IsBoundaryEdge(ec) == false)
+                            return MeshResult.Failed_InvalidNeighbourhood;
+                    }
+                }
+            }
+            if ( b != d ) {
+                int eb = 0, ed = 0, other_v = (a == c) ? a : -1;
+                foreach ( int dnbr in VtxVerticesItr(d)) {
+                    if (dnbr != other_v && (eb = find_edge(b, dnbr)) != DMesh3.InvalidID) {
+                        ed = find_edge(d, dnbr);
+                        if (IsBoundaryEdge(eb) == false || IsBoundaryEdge(ed) == false)
+                            return MeshResult.Failed_InvalidNeighbourhood;
+                    }
+                }
+            }
+            
 
-			if (a != c) {
+            // [TODO] this acts on each interior tri twice. could avoid using vtx-tri iterator?
+            if (a != c) {
 				// replace c w/ a in edges and tris connected to c, and move edges to a
                 foreach ( int eid in vertex_edges.ValueItr(c)) { 
 					if (eid == eDiscard)
@@ -905,6 +962,7 @@ public MeshResult MergeEdges(int eKeep, int eDiscard, out MergeEdgesInfo merge_i
 				bool found = false;
 				// in this loop, we compare 'other' vert_1 and vert_2 of edges around v1.
 				// problem case is when vert_1 == vert_2  (ie two edges w/ same other vtx).
+                //restart_merge_loop:
 				for (int i = 0; i < Nedges && found == false; ++i) {
 					int edge_1 = edges_v[i];
 					if ( IsBoundaryEdge(edge_1) == false)
@@ -925,11 +983,12 @@ public MeshResult MergeEdges(int eKeep, int eDiscard, out MergeEdgesInfo merge_i
 							merge_info.eRemovedExtra[vi] = edge_2;
 							merge_info.eKeptExtra[vi] = edge_1;
 
-							//Nedges = edges_v.Count; // this code allows us to continue checking, ie in case we had
-							//i--;					  // multiple such edges. but I don't think it's possible.
-							found = true;			  // exit outer i loop
-							break;					  // exit inner j loop
-						}
+                            //edges_v = vertex_edges_list(v1);      // this code allows us to continue checking, ie in case we had
+                            //Nedges = edges_v.Count;               // multiple such edges. but I don't think it's possible.
+                            //goto restart_merge_loop;
+                            found = true;			  // exit outer i loop
+                            break;					  // exit inner j loop
+                        }
 					}
 				}
 			}
@@ -1081,7 +1140,13 @@ List<int> vertex_edges_list(int vid)
         {
             return new List<int>( vertex_edges.ValueItr(vid) );
         }
-
+        List<int> vertex_vertices_list(int vid)
+        {
+            List<int> vnbrs = new List<int>();
+            foreach (int eid in vertex_edges.ValueItr(vid))
+                vnbrs.Add(edge_other_v(eid, vid));
+            return vnbrs;
+        }
 
 
         void set_edge_vertices(int eID, int a, int b) {
diff --git a/mesh/DSubmesh3.cs b/mesh/DSubmesh3.cs
index c6e4980a..b3ec35af 100644
--- a/mesh/DSubmesh3.cs
+++ b/mesh/DSubmesh3.cs
@@ -27,9 +27,9 @@ public class DSubmesh3
         public DVector<int> SubToBaseT;         // triangle index map from submesh to base mesh. Only computed if ComputeTriMaps = true.
 
         // boundary info
-        public IndexHashSet BaseBorderE;        // list of internal border edge indices on base mesh
+        public IndexHashSet BaseBorderE;        // list of internal border edge indices on base mesh. Does not include mesh boundary edges.
         public IndexHashSet BaseBoundaryE;      // list of mesh-boundary edges on base mesh that are in submesh
-        public IndexHashSet BaseBorderV;        // list of border vertex indices on base mesh (ie verts of BaseBorderE)
+        public IndexHashSet BaseBorderV;        // list of border vertex indices on base mesh (ie verts of BaseBorderE - does not include mesh boundary vertices)
 
 
         public DSubmesh3(DMesh3 mesh, int[] subTriangles)
@@ -66,6 +66,10 @@ public int MapVertexToBaseMesh(int sub_vID) {
         public Index2i MapVerticesToSubmesh(Index2i v) {
             return new Index2i(BaseToSubV[v.a], BaseToSubV[v.b]);
         }
+        public Index2i MapVerticesToBaseMesh(Index2i v) {
+            return new Index2i(MapVertexToBaseMesh(v.a), MapVertexToBaseMesh(v.b));
+        }
+
         public void MapVerticesToSubmesh(int[] vertices)
         {
             for (int i = 0; i < vertices.Length; ++i)
@@ -85,6 +89,13 @@ public void MapEdgesToSubmesh(int[] edges)
                 edges[i] = MapEdgeToSubmesh(edges[i]);
         }
 
+        public int MapEdgeToBaseMesh(int sub_eid)
+        {
+            Index2i sub_ev = SubMesh.GetEdgeV(sub_eid);
+            Index2i base_ev = MapVerticesToBaseMesh(sub_ev);
+            return BaseMesh.FindEdge(base_ev.a, base_ev.b);
+        }
+
 
         public int MapTriangleToSubmesh(int base_tID)
         {
diff --git a/mesh/DSubmesh3Set.cs b/mesh/DSubmesh3Set.cs
new file mode 100644
index 00000000..fd845f3b
--- /dev/null
+++ b/mesh/DSubmesh3Set.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+
+namespace g3
+{
+
+    /// <summary>
+    /// A set of submeshes of a base mesh. You provide a set of keys, and a Func
+    /// that returns the triangle index list for a given key. The set of DSubmesh3
+    /// objects are computed on construction.
+    /// </summary>
+    public class DSubmesh3Set : IEnumerable<DSubmesh3>
+    {
+        public DMesh3 Mesh;
+
+        public IEnumerable<object> TriangleSetKeys;
+        public Func<object, IEnumerable<int>> TriangleSetF;
+
+        // outputs
+
+        /// <summary> List of computed submeshes </summary>
+        public List<DSubmesh3> Submeshes;
+
+        /// <summary> Mapping from keys to submeshes </summary>
+        public Dictionary<object, DSubmesh3> KeyToMesh;
+
+
+        /// <summary>
+        /// Construct submesh set from given keys and key-to-indices Func
+        /// </summary>
+        public DSubmesh3Set(DMesh3 mesh, IEnumerable<object> keys, Func<object,IEnumerable<int>> indexSetsF)
+        {
+            Mesh = mesh;
+            TriangleSetKeys = keys;
+            TriangleSetF = indexSetsF;
+
+            ComputeSubMeshes();
+        }
+
+
+        /// <summary>
+        /// Construct submesh set for an already-computed MeshConnectedComponents instance
+        /// </summary>
+        public DSubmesh3Set(DMesh3 mesh, MeshConnectedComponents components)
+        {
+            Mesh = mesh;
+
+            TriangleSetF = (idx) => {
+                return components.Components[(int)idx].Indices;
+            };
+            List<object> keys = new List<object>();
+            for (int k = 0; k < components.Count; ++k)
+                keys.Add(k);
+            TriangleSetKeys = keys;
+
+            ComputeSubMeshes();
+        }
+
+
+        public IEnumerator<DSubmesh3> GetEnumerator() {
+            return Submeshes.GetEnumerator();
+        }
+        IEnumerator IEnumerable.GetEnumerator() {
+            return Submeshes.GetEnumerator();
+        }
+
+
+
+        virtual protected void ComputeSubMeshes()
+        {
+            Submeshes = new List<DSubmesh3>();
+            KeyToMesh = new Dictionary<object, DSubmesh3>();
+
+            SpinLock data_lock = new SpinLock();
+
+            gParallel.ForEach(TriangleSetKeys, (obj) => {
+                DSubmesh3 submesh = new DSubmesh3(Mesh, TriangleSetF(obj), 0);
+
+                bool taken = false;
+                data_lock.Enter(ref taken);
+                Submeshes.Add(submesh);
+                KeyToMesh[obj] = submesh;
+                data_lock.Exit();
+            });
+        }
+
+    }
+}
diff --git a/mesh/EdgeLoop.cs b/mesh/EdgeLoop.cs
index 88abbfb4..e83d51e8 100644
--- a/mesh/EdgeLoop.cs
+++ b/mesh/EdgeLoop.cs
@@ -74,6 +74,27 @@ public static EdgeLoop FromEdges(DMesh3 mesh, IList<int> edges)
         }
 
 
+        /// <summary>
+        /// construct EdgeLoop from a list of vertices of mesh
+        /// </summary>
+        public static EdgeLoop FromVertices(DMesh3 mesh, IList<int> vertices)
+        {
+            int NV = vertices.Count;
+            int[] Vertices = new int[NV];
+            for (int i = 0; i < NV; ++i)
+                Vertices[i] = vertices[i];
+            int NE = NV;
+            int[] Edges = new int[NE];
+            for (int i = 0; i < NE; ++i) {
+                Edges[i] = mesh.FindEdge(Vertices[i], Vertices[(i + 1)%NE]);
+                if (Edges[i] == DMesh3.InvalidID)
+                    throw new Exception("EdgeLoop.FromVertices: vertices are not connected by edge!");
+            }
+            return new EdgeLoop(mesh, Vertices, Edges, false);
+        }
+
+
+
         /// <summary>
         /// construct EdgeLoop from a list of vertices of mesh
         /// if loop is a boundary edge, we can correct orientation if requested
@@ -127,6 +148,15 @@ public AxisAlignedBox3d GetBounds()
         }
 
 
+        public DCurve3 ToCurve(DMesh3 sourceMesh = null)
+        {
+            if (sourceMesh == null)
+                sourceMesh = Mesh;
+            DCurve3 curve = MeshUtil.ExtractLoopV(sourceMesh, Vertices);
+            curve.Closed = true;
+            return curve;
+        }
+
 
         /// <summary>
         /// if this is a border edge-loop, we can check that it is oriented correctly, and
@@ -173,15 +203,18 @@ public bool IsInternalLoop()
 
 
         /// <summary>
-        /// Check if all edges of this loop are boundary edges
+        /// Check if all edges of this loop are boundary edges.
+        /// If testMesh != null, will check that mesh instead of internal Mesh
         /// </summary>
-        public bool IsBoundaryLoop()
+        public bool IsBoundaryLoop(DMesh3 testMesh = null)
         {
+            DMesh3 useMesh = (testMesh != null) ? testMesh : Mesh;
+
             int NV = Vertices.Length;
             for (int i = 0; i < NV; ++i ) {
-                int eid = Mesh.FindEdge(Vertices[i], Vertices[(i + 1) % NV]);
+                int eid = useMesh.FindEdge(Vertices[i], Vertices[(i + 1) % NV]);
                 Debug.Assert(eid != DMesh3.InvalidID);
-                if (Mesh.IsBoundaryEdge(eid) == false)
+                if (useMesh.IsBoundaryEdge(eid) == false)
                     return false;
             }
             return true;
diff --git a/mesh/EdgeSpan.cs b/mesh/EdgeSpan.cs
index 567c07ba..f55e4835 100644
--- a/mesh/EdgeSpan.cs
+++ b/mesh/EdgeSpan.cs
@@ -63,6 +63,27 @@ public static EdgeSpan FromEdges(DMesh3 mesh, IList<int> edges)
         }
 
 
+        /// <summary>
+        /// construct EdgeSpan from a list of vertices of mesh
+        /// </summary>
+        public static EdgeSpan FromVertices(DMesh3 mesh, IList<int> vertices)
+        {
+            int NV = vertices.Count;
+            int[] Vertices = new int[NV];
+            for (int i = 0; i < NV; ++i)
+                Vertices[i] = vertices[i];
+            int NE = NV - 1;
+            int[] Edges = new int[NE];
+            for ( int i = 0; i < NE; ++i ) {
+                Edges[i] = mesh.FindEdge(Vertices[i], Vertices[i + 1]);
+                if (Edges[i] == DMesh3.InvalidID)
+                    throw new Exception("EdgeSpan.FromVertices: vertices are not connected by edge!");
+            }
+            return new EdgeSpan(mesh, Vertices, Edges, false);
+        }
+
+
+
         public int VertexCount {
             get { return Vertices.Length; }
         }
@@ -84,6 +105,16 @@ public AxisAlignedBox3d GetBounds()
         }
 
 
+        public DCurve3 ToCurve(DMesh3 sourceMesh = null)
+        {
+            if (sourceMesh == null)
+                sourceMesh = Mesh;
+            DCurve3 curve = MeshUtil.ExtractLoopV(sourceMesh, Vertices);
+            curve.Closed = false;
+            return curve;
+        }
+
+
         public bool IsInternalSpan()
         {
             int NV = Vertices.Length;
@@ -97,13 +128,15 @@ public bool IsInternalSpan()
         }
 
 
-        public bool IsBoundarySpan()
+        public bool IsBoundarySpan(DMesh3 testMesh = null)
         {
+            DMesh3 useMesh = (testMesh != null) ? testMesh : Mesh;
+
             int NV = Vertices.Length;
             for (int i = 0; i < NV-1; ++i ) {
-                int eid = Mesh.FindEdge(Vertices[i], Vertices[i + 1]);
+                int eid = useMesh.FindEdge(Vertices[i], Vertices[i + 1]);
                 Debug.Assert(eid != DMesh3.InvalidID);
-                if (Mesh.IsBoundaryEdge(eid) == false)
+                if (useMesh.IsBoundaryEdge(eid) == false)
                     return false;
             }
             return true;
diff --git a/mesh/FaceGroupUtil.cs b/mesh/FaceGroupUtil.cs
index 6d2f5257..8ee72e58 100644
--- a/mesh/FaceGroupUtil.cs
+++ b/mesh/FaceGroupUtil.cs
@@ -157,7 +157,7 @@ public static List<int> FindTrianglesByGroup(IMesh mesh, int findGroupID)
         /// split input mesh into submeshes based on group ID
         /// **does not** separate disconnected components w/ same group ID
         /// </summary>
-        public static DMesh3[] SeparateMeshByGroups(DMesh3 mesh)
+        public static DMesh3[] SeparateMeshByGroups(DMesh3 mesh, out int[] groupIDs)
         {
             Dictionary<int, List<int>> meshes = new Dictionary<int, List<int>>();
             foreach ( int tid in mesh.TriangleIndices() ) {
@@ -167,18 +167,23 @@ public static DMesh3[] SeparateMeshByGroups(DMesh3 mesh)
                     tris = new List<int>();
                     meshes[gid] = tris;
                 }
-                tris.Add(gid);
+                tris.Add(tid);
             }
 
             DMesh3[] result = new DMesh3[meshes.Count];
+            groupIDs = new int[meshes.Count];
             int k = 0;
-            foreach ( var tri_list in meshes.Values) {
+            foreach ( var pair in meshes ) {
+                groupIDs[k] = pair.Key;
+                List<int> tri_list = pair.Value;
                 result[k++] = DSubmesh3.QuickSubmesh(mesh, tri_list);
             }
 
             return result;
         }
-
+        public static DMesh3[] SeparateMeshByGroups(DMesh3 mesh) {
+            int[] ids; return SeparateMeshByGroups(mesh, out ids);
+        }
 
 
     }
diff --git a/mesh/IMesh.cs b/mesh/IMesh.cs
index 08d0ce55..11b913f1 100644
--- a/mesh/IMesh.cs
+++ b/mesh/IMesh.cs
@@ -22,6 +22,8 @@ public interface IPointSet
 
         // iterators allow us to work with gaps in index space
         System.Collections.Generic.IEnumerable<int> VertexIndices();
+
+        int Timestamp { get; }
     }
 
 
diff --git a/mesh/MeshCaches.cs b/mesh/MeshCaches.cs
new file mode 100644
index 00000000..25375a3d
--- /dev/null
+++ b/mesh/MeshCaches.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace g3
+{
+    /*
+     * Basic cache of per-triangle information for a DMesh3
+     */
+    public class MeshTriInfoCache
+    {
+        public DVector<Vector3d> Centroids;
+        public DVector<Vector3d> Normals;
+        public DVector<double> Areas;
+
+        public MeshTriInfoCache(DMesh3 mesh)
+        {
+            int NT = mesh.TriangleCount;
+            Centroids = new DVector<Vector3d>(); Centroids.resize(NT);
+            Normals = new DVector<Vector3d>(); Normals.resize(NT);
+            Areas = new DVector<double>(); Areas.resize(NT);
+            gParallel.ForEach(mesh.TriangleIndices(), (tid) => {
+                Vector3d c, n; double a;
+                mesh.GetTriInfo(tid, out n, out a, out c);
+                Centroids[tid] = c;
+                Normals[tid] = n;
+                Areas[tid] = a;
+            });
+        }
+
+        public void GetTriInfo(int tid, ref Vector3d n, ref double a, ref Vector3d c)
+        {
+            c = Centroids[tid];
+            n = Normals[tid];
+            a = Areas[tid];
+        }
+    }
+}
diff --git a/mesh/MeshConstraintUtil.cs b/mesh/MeshConstraintUtil.cs
index 45e8aac8..7fe7b433 100644
--- a/mesh/MeshConstraintUtil.cs
+++ b/mesh/MeshConstraintUtil.cs
@@ -8,6 +8,22 @@ namespace g3
 {
     public static class MeshConstraintUtil
     {
+
+        // for all edges, disable flip/split/collapse
+        // for all vertices, pin in current position
+        public static void FixEdges(MeshConstraints cons, DMesh3 mesh, IEnumerable<int> edges)
+        {
+            foreach ( int ei in edges ) { 
+                if (mesh.IsEdge(ei)) {
+                    cons.SetOrUpdateEdgeConstraint(ei, EdgeConstraint.FullyConstrained);
+                    Index2i ev = mesh.GetEdgeV(ei);
+                    cons.SetOrUpdateVertexConstraint(ev.a, VertexConstraint.Pinned);
+                    cons.SetOrUpdateVertexConstraint(ev.b, VertexConstraint.Pinned);
+                }
+            }
+        }
+
+
         // for all mesh boundary edges, disable flip/split/collapse
         // for all mesh boundary vertices, pin in current position
         public static void FixAllBoundaryEdges(MeshConstraints cons, DMesh3 mesh)
@@ -50,6 +66,26 @@ public static void FixAllBoundaryEdges_AllowCollapse(MeshConstraints cons, DMesh
         }
 
 
+
+        // for all mesh boundary vertices, pin in current position, but allow splits
+        public static void FixAllBoundaryEdges_AllowSplit(MeshConstraints cons, DMesh3 mesh, int setID)
+        {
+            EdgeConstraint edgeCons = new EdgeConstraint(EdgeRefineFlags.NoFlip | EdgeRefineFlags.NoCollapse);
+            VertexConstraint vertCons = new VertexConstraint(true, setID);
+
+            int NE = mesh.MaxEdgeID;
+            for (int ei = 0; ei < NE; ++ei) {
+                if (mesh.IsEdge(ei) && mesh.IsBoundaryEdge(ei)) {
+                    cons.SetOrUpdateEdgeConstraint(ei, edgeCons);
+
+                    Index2i ev = mesh.GetEdgeV(ei);
+                    cons.SetOrUpdateVertexConstraint(ev.a, vertCons);
+                    cons.SetOrUpdateVertexConstraint(ev.b, vertCons);
+                }
+            }
+        }
+
+
         // loop through submesh border edges on basemesh, map to submesh, and
         // pin those edges / vertices
         public static void FixSubmeshBoundaryEdges(MeshConstraints cons, DSubmesh3 sub)
@@ -99,17 +135,18 @@ public static void FixAllGroupBoundaryEdges(Remesher r, bool bPinVertices)
 
         // for all vertices in loopV, constrain to target
         // for all edges in loopV, disable flips and constrain to target
-        public static void ConstrainVtxLoopTo(MeshConstraints cons, DMesh3 mesh, int[] loopV, IProjectionTarget target, int setID = -1)
+        public static void ConstrainVtxLoopTo(MeshConstraints cons, DMesh3 mesh, IList<int> loopV, IProjectionTarget target, int setID = -1)
         {
             VertexConstraint vc = new VertexConstraint(target);
-            for (int i = 0; i < loopV.Length; ++i)
+            int N = loopV.Count;
+            for (int i = 0; i < N; ++i)
                 cons.SetOrUpdateVertexConstraint(loopV[i], vc);
 
             EdgeConstraint ec = new EdgeConstraint(EdgeRefineFlags.NoFlip, target);
             ec.TrackingSetID = setID;
-            for ( int i = 0; i < loopV.Length; ++i ) {
+            for ( int i = 0; i < N; ++i ) {
                 int v0 = loopV[i];
-                int v1 = loopV[(i + 1) % loopV.Length];
+                int v1 = loopV[(i + 1) % N];
 
                 int eid = mesh.FindEdge(v0, v1);
                 Debug.Assert(eid != DMesh3.InvalidID);
@@ -127,6 +164,44 @@ public static void ConstrainVtxLoopTo(Remesher r, int[] loopV, IProjectionTarget
 
 
 
+
+
+        // for all vertices in loopV, constrain to target
+        // for all edges in loopV, disable flips and constrain to target
+        public static void ConstrainVtxSpanTo(MeshConstraints cons, DMesh3 mesh, IList<int> spanV, IProjectionTarget target, int setID = -1)
+        {
+            VertexConstraint vc = new VertexConstraint(target);
+            int N = spanV.Count;
+            for (int i = 1; i < N-1; ++i)
+                cons.SetOrUpdateVertexConstraint(spanV[i], vc);
+            cons.SetOrUpdateVertexConstraint(spanV[0], VertexConstraint.Pinned);
+            cons.SetOrUpdateVertexConstraint(spanV[N-1], VertexConstraint.Pinned);
+
+            EdgeConstraint ec = new EdgeConstraint(EdgeRefineFlags.NoFlip, target);
+            ec.TrackingSetID = setID;
+            for (int i = 0; i < N-1; ++i) {
+                int v0 = spanV[i];
+                int v1 = spanV[i + 1];
+
+                int eid = mesh.FindEdge(v0, v1);
+                Debug.Assert(eid != DMesh3.InvalidID);
+                if (eid != DMesh3.InvalidID)
+                    cons.SetOrUpdateEdgeConstraint(eid, ec);
+            }
+
+        }
+        public static void ConstrainVtxSpanTo(Remesher r, int[] spanV, IProjectionTarget target, int setID = -1)
+        {
+            if (r.Constraints == null)
+                r.SetExternalConstraints(new MeshConstraints());
+            ConstrainVtxSpanTo(r.Constraints, r.Mesh, spanV, target);
+        }
+
+
+
+
+
+
         public static void PreserveBoundaryLoops(MeshConstraints cons, DMesh3 mesh) {
             MeshBoundaryLoops loops = new MeshBoundaryLoops(mesh);
             foreach ( EdgeLoop loop in loops ) {
diff --git a/mesh/MeshConstraints.cs b/mesh/MeshConstraints.cs
index 490afd3e..79a856b1 100644
--- a/mesh/MeshConstraints.cs
+++ b/mesh/MeshConstraints.cs
@@ -170,6 +170,11 @@ public VertexConstraint GetVertexConstraint(int vid)
             return VertexConstraint.Unconstrained;
         }
 
+        public bool GetVertexConstraint(int vid, ref VertexConstraint vc)
+        {
+            return Vertices.TryGetValue(vid, out vc);
+        }
+
         public void SetOrUpdateVertexConstraint(int vid, VertexConstraint vc)
         {
             Vertices[vid] = vc;
diff --git a/mesh/MeshEditor.cs b/mesh/MeshEditor.cs
index 0d4c1a88..939c7cc3 100644
--- a/mesh/MeshEditor.cs
+++ b/mesh/MeshEditor.cs
@@ -2,6 +2,7 @@
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Linq;
 
 
 namespace g3
@@ -24,6 +25,42 @@ public MeshEditor(DMesh3 mesh)
 
 
 
+        public virtual int[] AddTriangleStrip(IList<Frame3f> frames, IList<Interval1d> spans, int group_id = -1)
+        {
+            int N = frames.Count;
+            if (N != spans.Count)
+                throw new Exception("MeshEditor.AddTriangleStrip: spans list is not the same size!");
+            int[] new_tris = new int[2*(N-1)];
+
+            int prev_a = -1, prev_b = -1;
+            int i = 0, ti = 0;
+            for (i = 0; i < N; ++i) {
+                Frame3f f = frames[i];
+                Interval1d span = spans[i];
+
+                Vector3d va = f.Origin + (float)span.a * f.Y;
+                Vector3d vb = f.Origin + (float)span.b * f.Y;
+
+                // [TODO] could compute normals here...
+
+                int a = Mesh.AppendVertex(va);
+                int b = Mesh.AppendVertex(vb);
+
+                if ( prev_a != -1 ) {
+                    new_tris[ti++] = Mesh.AppendTriangle(prev_a, b, prev_b);
+                    new_tris[ti++] = Mesh.AppendTriangle(prev_a, a, b);
+
+                }
+                prev_a = a; prev_b = b;
+            }
+
+            return new_tris;
+        }
+
+
+
+
+
         public virtual int[] AddTriangleFan_OrderedVertexLoop(int center, int[] vertex_loop, int group_id = -1)
         {
             int N = vertex_loop.Length;
@@ -36,7 +73,7 @@ public virtual int[] AddTriangleFan_OrderedVertexLoop(int center, int[] vertex_l
 
                 Index3i newT = new Index3i(center, b, a);
                 int new_tid = Mesh.AppendTriangle(newT, group_id);
-                if (new_tid == DMesh3.InvalidID)
+                if (new_tid < 0)
                     goto operation_failed;
 
                 new_tris[i] = new_tid;
@@ -49,7 +86,7 @@ public virtual int[] AddTriangleFan_OrderedVertexLoop(int center, int[] vertex_l
                 // remove what we added so far
                 if (i > 0) {
                     if (remove_triangles(new_tris, i) == false)
-                        throw new Exception("MeshConstructor.AddTriangleFan_OrderedVertexLoop: failed to add fan, and also falied to back out changes.");
+                        throw new Exception("MeshEditor.AddTriangleFan_OrderedVertexLoop: failed to add fan, and also falied to back out changes.");
                 }
                 return null;
         }
@@ -73,7 +110,7 @@ public virtual int[] AddTriangleFan_OrderedEdgeLoop(int center, int[] edge_loop,
 
                 Index3i newT = new Index3i(center, b, a);
                 int new_tid = Mesh.AppendTriangle(newT, group_id);
-                if (new_tid == DMesh3.InvalidID)
+                if (new_tid < 0)
                     goto operation_failed;
 
                 new_tris[i] = new_tid;
@@ -86,7 +123,7 @@ public virtual int[] AddTriangleFan_OrderedEdgeLoop(int center, int[] edge_loop,
                 // remove what we added so far
                 if (i > 0) {
                     if (remove_triangles(new_tris, i-1) == false)
-                        throw new Exception("MeshConstructor.AddTriangleFan_OrderedEdgeLoop: failed to add fan, and also failed to back out changes.");
+                        throw new Exception("MeshEditor.AddTriangleFan_OrderedEdgeLoop: failed to add fan, and also failed to back out changes.");
                 }
                 return null;
         }
@@ -119,12 +156,11 @@ public virtual int[] StitchLoop(int[] vloop1, int[] vloop2, int group_id = -1)
 
                 int tid1 = Mesh.AppendTriangle(t1, group_id);
                 int tid2 = Mesh.AppendTriangle(t2, group_id);
-
-                if (tid1 == DMesh3.InvalidID || tid2 == DMesh3.InvalidID)
-                    goto operation_failed;
-
                 new_tris[2 * i] = tid1;
                 new_tris[2 * i + 1] = tid2;
+
+                if (tid1 < 0 || tid2 < 0)
+                    goto operation_failed;
             }
 
             return new_tris;
@@ -133,8 +169,8 @@ public virtual int[] StitchLoop(int[] vloop1, int[] vloop2, int group_id = -1)
             operation_failed:
                 // remove what we added so far
                 if (i > 0) {
-                    if (remove_triangles(new_tris, 2*(i-1)) == false)
-                        throw new Exception("MeshConstructor.StitchLoop: failed to add all triangles, and also failed to back out changes.");
+                    if (remove_triangles(new_tris, 2*i+1) == false)
+                        throw new Exception("MeshEditor.StitchLoop: failed to add all triangles, and also failed to back out changes.");
                 }
                 return null;
         }
@@ -143,15 +179,63 @@ public virtual int[] StitchLoop(int[] vloop1, int[] vloop2, int group_id = -1)
 
 
 
+
+
+        /// <summary>
+        /// Trivial back-and-forth stitch between two vertex loops with same length. 
+        /// If nearest vertices of input loops would not be matched, cycles loops so
+        /// that this is the case. 
+        /// Loops must have appropriate orientation.
+        /// </summary>
+        public virtual int[] StitchVertexLoops_NearestV(int[] loop0, int[] loop1, int group_id = -1)
+        {
+            int N = loop0.Length;
+            Index2i iBestPair = Index2i.Zero;
+            double best_dist = double.MaxValue;
+            for (int i = 0; i < N; ++i) {
+                Vector3d v0 = Mesh.GetVertex(loop0[i]);
+                for (int j = 0; j < N; ++j) {
+                    double dist_sqr = v0.DistanceSquared(Mesh.GetVertex(loop1[j]));
+                    if (dist_sqr < best_dist) {
+                        best_dist = dist_sqr;
+                        iBestPair = new Index2i(i, j);
+                    }
+                }
+            }
+            if (iBestPair.a != iBestPair.b) {
+                int[] newLoop0 = new int[N];
+                int[] newLoop1 = new int[N];
+                for (int i = 0; i < N; ++i) {
+                    newLoop0[i] = loop0[(iBestPair.a + i) % N];
+                    newLoop1[i] = loop1[(iBestPair.b + i) % N];
+                }
+                return StitchLoop(newLoop0, newLoop1, group_id);
+            } else {
+                return StitchLoop(loop0, loop1, group_id);
+            }
+
+        }
+
+
+
+
+
+
         /// <summary>
         /// Stitch two sets of boundary edges that are provided as unordered pairs of edges, by
         /// adding triangulated quads between each edge pair. 
-        /// If a failure is encountered during stitching, the triangles added up to that point are removed.
+        /// If bAbortOnFailure==true and a failure is encountered during stitching, the triangles added up to that point are removed.
+        /// If bAbortOnFailure==false, failures are ignored and the returned triangle list may contain invalid values!
         /// </summary>
-        public virtual int[] StitchUnorderedEdges(List<Index2i> EdgePairs, int group_id = -1)
+        public virtual int[] StitchUnorderedEdges(List<Index2i> EdgePairs, int group_id, bool bAbortOnFailure, out bool stitch_incomplete)
         {
             int N = EdgePairs.Count;
             int[] new_tris = new int[N * 2];
+            if (bAbortOnFailure == false) {
+                for (int k = 0; k < new_tris.Length; ++k)
+                    new_tris[k] = DMesh3.InvalidID;
+            }
+            stitch_incomplete = false;
 
             int i = 0;
             for (; i < N; ++i) {
@@ -159,16 +243,20 @@ public virtual int[] StitchUnorderedEdges(List<Index2i> EdgePairs, int group_id
 
                 // look up and orient the first edge
                 Index4i edge_a = Mesh.GetEdge(edges.a);
-                if ( edge_a.d != DMesh3.InvalidID )
-                    goto operation_failed;
+                if (edge_a.d != DMesh3.InvalidID) {
+                    if (bAbortOnFailure) goto operation_failed;
+                    else { stitch_incomplete = true; continue; }
+                }
                 Index3i edge_a_tri = Mesh.GetTriangle(edge_a.c);
                 int a = edge_a.a, b = edge_a.b;
                 IndexUtil.orient_tri_edge(ref a, ref b, edge_a_tri);
 
                 // look up and orient the second edge
                 Index4i edge_b = Mesh.GetEdge(edges.b);
-                if (edge_b.d != DMesh3.InvalidID)
-                    goto operation_failed;
+                if (edge_b.d != DMesh3.InvalidID) {
+                    if (bAbortOnFailure) goto operation_failed;
+                    else { stitch_incomplete = true; continue; }
+                }
                 Index3i edge_b_tri = Mesh.GetTriangle(edge_b.c);
                 int c = edge_b.a, d = edge_b.b;
                 IndexUtil.orient_tri_edge(ref c, ref d, edge_b_tri);
@@ -182,8 +270,10 @@ public virtual int[] StitchUnorderedEdges(List<Index2i> EdgePairs, int group_id
                 int tid1 = Mesh.AppendTriangle(t1, group_id);
                 int tid2 = Mesh.AppendTriangle(t2, group_id);
 
-                if (tid1 == DMesh3.InvalidID || tid2 == DMesh3.InvalidID)
-                    goto operation_failed;
+                if (tid1 < 0 || tid2 < 0) {
+                    if (bAbortOnFailure) goto operation_failed;
+                    else { stitch_incomplete = true; continue; }
+                }
 
                 new_tris[2 * i] = tid1;
                 new_tris[2 * i + 1] = tid2;
@@ -195,10 +285,15 @@ public virtual int[] StitchUnorderedEdges(List<Index2i> EdgePairs, int group_id
             // remove what we added so far
             if (i > 0) {
                 if (remove_triangles(new_tris, 2 * (i - 1)) == false)
-                    throw new Exception("MeshConstructor.StitchLoop: failed to add all triangles, and also failed to back out changes.");
+                    throw new Exception("MeshEditor.StitchLoop: failed to add all triangles, and also failed to back out changes.");
             }
             return null;
         }
+        public virtual int[] StitchUnorderedEdges(List<Index2i> EdgePairs, int group_id = -1, bool bAbortOnFailure = true)
+        {
+            bool incomplete = false;
+            return StitchUnorderedEdges(EdgePairs, group_id, bAbortOnFailure, out incomplete);
+        }
 
 
 
@@ -210,10 +305,10 @@ public virtual int[] StitchUnorderedEdges(List<Index2i> EdgePairs, int group_id
         /// vertex ordering must reslut in appropriate orientation (which is...??)
         /// [TODO] check and fail on bad orientation
         /// </summary>
-        public virtual int[] StitchSpan(int[] vspan1, int[] vspan2, int group_id = -1)
+        public virtual int[] StitchSpan(IList<int> vspan1, IList<int> vspan2, int group_id = -1)
         {
-            int N = vspan1.Length;
-            if (N != vspan2.Length)
+            int N = vspan1.Count;
+            if (N != vspan2.Count)
                 throw new Exception("MeshEditor.StitchSpan: spans are not the same length!!");
             N--;
 
@@ -232,7 +327,7 @@ public virtual int[] StitchSpan(int[] vspan1, int[] vspan2, int group_id = -1)
                 int tid1 = Mesh.AppendTriangle(t1, group_id);
                 int tid2 = Mesh.AppendTriangle(t2, group_id);
 
-                if (tid1 == DMesh3.InvalidID || tid2 == DMesh3.InvalidID)
+                if (tid1 < 0 || tid2 < 0)
                     goto operation_failed;
 
                 new_tris[2 * i] = tid1;
@@ -246,7 +341,7 @@ public virtual int[] StitchSpan(int[] vspan1, int[] vspan2, int group_id = -1)
                 // remove what we added so far
                 if (i > 0) {
                     if (remove_triangles(new_tris, 2*(i-1)) == false)
-                        throw new Exception("MeshConstructor.StitchLoop: failed to add all triangles, and also failed to back out changes.");
+                        throw new Exception("MeshEditor.StitchLoop: failed to add all triangles, and also failed to back out changes.");
                 }
                 return null;
         }
@@ -260,10 +355,10 @@ public virtual int[] StitchSpan(int[] vspan1, int[] vspan2, int group_id = -1)
         // [TODO] cannot back-out this operation right now
         //
         // Remove list of triangles. Values of triangles[] set to InvalidID are ignored.
-        public bool RemoveTriangles(int[] triangles, bool bRemoveIsolatedVerts)
+        public bool RemoveTriangles(IList<int> triangles, bool bRemoveIsolatedVerts)
         {
             bool bAllOK = true;
-            for (int i = 0; i < triangles.Length; ++i ) {
+            for (int i = 0; i < triangles.Count; ++i ) {
                 if (triangles[i] == DMesh3.InvalidID)
                     continue;
 
@@ -309,10 +404,61 @@ public bool RemoveTriangles(Func<int,bool> selectorF, bool bRemoveIsolatedVerts)
             return bAllOK;
         }
 
+        public static bool RemoveTriangles(DMesh3 Mesh, IList<int> triangles, bool bRemoveIsolatedVerts = true) {
+            MeshEditor editor = new MeshEditor(Mesh);
+            return editor.RemoveTriangles(triangles, bRemoveIsolatedVerts);
+        }
+        public static bool RemoveTriangles(DMesh3 Mesh, IEnumerable<int> triangles, bool bRemoveIsolatedVerts = true) {
+            MeshEditor editor = new MeshEditor(Mesh);
+            return editor.RemoveTriangles(triangles, bRemoveIsolatedVerts);
+        }
 
 
+        /// <summary>
+        /// Remove 'loner' triangles that have no connected neighbours. 
+        /// </summary>
+        public static bool RemoveIsolatedTriangles(DMesh3 mesh)
+        {
+            MeshEditor editor = new MeshEditor(mesh);
+            return editor.RemoveTriangles((tid) => {
+                Index3i tnbrs = mesh.GetTriNeighbourTris(tid);
+                return (tnbrs.a == DMesh3.InvalidID && tnbrs.b == DMesh3.InvalidID && tnbrs.c == DMesh3.InvalidID);
+            }, true);
+        }
+
 
 
+        /// <summary>
+        /// Remove 'fin' triangles that have only one connected triangle.
+        /// Removing one fin can create another, by default will keep iterating
+        /// until all fins removed (in a not very efficient way!).
+        /// Pass bRepeatToConvergence=false to only do one pass.
+        /// [TODO] if we are repeating, construct face selection from nbrs of first list and iterate over that on future passes!
+        /// </summary>
+        public static int RemoveFinTriangles(DMesh3 mesh, Func<DMesh3, int, bool> removeF = null, bool bRepeatToConvergence = true)
+        {
+            MeshEditor editor = new MeshEditor(mesh);
+
+            int nRemoved = 0;
+            List<int> to_remove = new List<int>();
+            repeat:
+            foreach ( int tid in mesh.TriangleIndices()) {
+                Index3i nbrs = mesh.GetTriNeighbourTris(tid);
+                int c = ((nbrs.a != DMesh3.InvalidID)?1:0) + ((nbrs.b != DMesh3.InvalidID)?1:0) + ((nbrs.c != DMesh3.InvalidID)?1:0);
+                if (c <= 1) {
+                    if (removeF == null || removeF(mesh, tid) == true )
+                        to_remove.Add(tid);
+                }
+            }
+            if (to_remove.Count == 0)
+                return nRemoved;
+            nRemoved += to_remove.Count;
+            RemoveTriangles(mesh, to_remove, true);
+            to_remove.Clear();
+            if (bRepeatToConvergence)
+                goto repeat;
+            return nRemoved;
+        }
 
 
 
@@ -451,6 +597,68 @@ public void ReverseTriangles(IEnumerable<int> triangles, bool bFlipVtxNormals =
 
 
 
+        /// <summary>
+        /// separate triangle one-ring at vertex into connected components, and
+        /// then duplicate vertex once for each component
+        /// </summary>
+        public void DisconnectBowtie(int vid)
+        {
+            List<List<int>> sets = new List<List<int>>();
+            foreach ( int tid in Mesh.VtxTrianglesItr(vid)) {
+                Index3i nbrs = Mesh.GetTriNeighbourTris(tid);
+                bool found = false;
+                foreach ( List<int> set in sets ) {
+                    if ( set.Contains(nbrs.a) || set.Contains(nbrs.b) || set.Contains(nbrs.c) ) {
+                        set.Add(tid);
+                        found = true;
+                        break;
+                    }
+                }
+                if ( found == false ) {
+                    List<int> set = new List<int>() { tid };
+                    sets.Add(set);
+                }
+            }
+            if (sets.Count == 1)
+                return;  // not a bowtie!
+            sets.Sort(bowtie_sorter);
+            for ( int k = 1; k < sets.Count; ++k ) {
+                int copy_vid = Mesh.AppendVertex(Mesh, vid);
+                List<int> tris = sets[k];
+                foreach ( int tid in tris ) {
+                    Index3i t = Mesh.GetTriangle(tid);
+                    if (t.a == vid) t.a = copy_vid;
+                    else if (t.b == vid) t.b = copy_vid;
+                    else t.c = copy_vid;
+                    Mesh.SetTriangle(tid, t, false);
+                }
+            }
+        }
+        static int bowtie_sorter(List<int> l1, List<int> l2) {
+            if (l1.Count == l2.Count) return 0;
+            return (l1.Count > l2.Count) ? -1 : 1;
+        }
+
+
+
+        /// <summary>
+        /// Disconnect all bowtie vertices in mesh. Iterates because sometimes
+		/// disconnecting a bowtie creates new bowties (how??).
+		/// Returns number of remaining bowties after iterations.
+        /// </summary>
+        public int DisconnectAllBowties(int nMaxIters = 10)
+        {
+            List<int> bowties = new List<int>(MeshIterators.BowtieVertices(Mesh));
+            int iter = 0;
+            while (bowties.Count > 0 && iter++ < nMaxIters) {
+                foreach (int vid in bowties) 
+                    DisconnectBowtie(vid);
+                bowties = new List<int>(MeshIterators.BowtieVertices(Mesh));
+            }
+			return bowties.Count;
+        }
+
+
 
 
         // in ReinsertSubmesh, a problem can arise where the mesh we are inserting has duplicate triangles of
@@ -646,6 +854,10 @@ public void AppendBox(Frame3f frame, float size)
             AppendBox(frame, size * Vector3f.One);
         }
         public void AppendBox(Frame3f frame, Vector3f size)
+        {
+            AppendBox(frame, size, Colorf.White);
+        }
+        public void AppendBox(Frame3f frame, Vector3f size, Colorf color)
         {
             TrivialBox3Generator boxgen = new TrivialBox3Generator() {
                 Box = new Box3d(frame, size),
@@ -654,6 +866,69 @@ public void AppendBox(Frame3f frame, Vector3f size)
             boxgen.Generate();
             DMesh3 mesh = new DMesh3();
             boxgen.MakeMesh(mesh);
+            if (Mesh.HasVertexColors)
+                mesh.EnableVertexColors(color);
+            AppendMesh(mesh, Mesh.AllocateTriangleGroup());
+        }
+        public void AppendLine(Segment3d seg, float size)
+        {
+            Frame3f f = new Frame3f(seg.Center);
+            f.AlignAxis(2, (Vector3f)seg.Direction);
+            AppendBox(f, new Vector3f(size, size, seg.Extent));
+        }
+        public void AppendLine(Segment3d seg, float size, Colorf color)
+        {
+            Frame3f f = new Frame3f(seg.Center);
+            f.AlignAxis(2, (Vector3f)seg.Direction);
+            AppendBox(f, new Vector3f(size, size, seg.Extent), color);
+        }
+        public static void AppendBox(DMesh3 mesh, Vector3d pos, float size)
+        {
+            MeshEditor editor = new MeshEditor(mesh);
+            editor.AppendBox(new Frame3f(pos), size);
+        }
+        public static void AppendBox(DMesh3 mesh, Vector3d pos, float size, Colorf color)
+        {
+            MeshEditor editor = new MeshEditor(mesh);
+            editor.AppendBox(new Frame3f(pos), size*Vector3f.One, color);
+        }
+        public static void AppendBox(DMesh3 mesh, Vector3d pos, Vector3d normal, float size)
+        {
+            MeshEditor editor = new MeshEditor(mesh);
+            editor.AppendBox(new Frame3f(pos, normal), size);
+        }
+        public static void AppendBox(DMesh3 mesh, Vector3d pos, Vector3d normal, float size, Colorf color)
+        {
+            MeshEditor editor = new MeshEditor(mesh);
+            editor.AppendBox(new Frame3f(pos, normal), size*Vector3f.One, color);
+        }
+        public static void AppendBox(DMesh3 mesh, Frame3f frame, Vector3f size, Colorf color)
+        {
+            MeshEditor editor = new MeshEditor(mesh);
+            editor.AppendBox(frame, size, color);
+        }
+
+        public static void AppendLine(DMesh3 mesh, Segment3d seg, float size)
+        {
+            Frame3f f = new Frame3f(seg.Center);
+            f.AlignAxis(2, (Vector3f)seg.Direction);
+            MeshEditor editor = new MeshEditor(mesh);
+            editor.AppendBox(f, new Vector3f(size, size, seg.Extent));
+        }
+
+
+
+
+        public void AppendPathSolid(IEnumerable<Vector3d> vertices, double radius, Colorf color)
+        {
+            TubeGenerator tubegen = new TubeGenerator() {
+                Vertices = new List<Vector3d>(vertices),
+                Polygon = Polygon2d.MakeCircle(radius, 6),
+                NoSharedVertices = false
+            };
+            DMesh3 mesh = tubegen.Generate().MakeDMesh();
+            if (Mesh.HasVertexColors)
+                mesh.EnableVertexColors(color);
             AppendMesh(mesh, Mesh.AllocateTriangleGroup());
         }
 
@@ -694,10 +969,67 @@ public bool RemoveAllBowtieVertices(bool bRepeatUntilClean)
 
 
 
+
+        /// <summary>
+        /// Remove any unused vertices in mesh, ie vertices with no edges.
+        /// Returns number of removed vertices.
+        /// </summary>
+        public int RemoveUnusedVertices()
+        {
+            int nRemoved = 0;
+            int NV = Mesh.MaxVertexID;
+            for ( int vid = 0; vid < NV; ++vid) {
+                if (Mesh.IsVertex(vid) && Mesh.GetVtxEdgeCount(vid) == 0) {
+                    Mesh.RemoveVertex(vid);
+                    ++nRemoved;
+                }
+            }
+            return nRemoved;
+        }
+        public static int RemoveUnusedVertices(DMesh3 mesh) {
+            MeshEditor e = new MeshEditor(mesh); return e.RemoveUnusedVertices();
+        }
+
+
+
+
+
+
+        /// <summary>
+        /// Remove any connected components with volume &lt; min_volume area lt; min_area
+        /// </summary>
+        public int RemoveSmallComponents(double min_volume, double min_area)
+        {
+            MeshConnectedComponents C = new MeshConnectedComponents(Mesh);
+            C.FindConnectedT();
+            if (C.Count == 1)
+                return 0;
+            int nRemoved = 0;
+            foreach (var comp in C.Components) {
+                Vector2d vol_area = MeshMeasurements.VolumeArea(Mesh, comp.Indices, Mesh.GetVertex);
+                if (vol_area.x < min_volume || vol_area.y < min_area) {
+                    MeshEditor.RemoveTriangles(Mesh, comp.Indices);
+                    nRemoved++;
+                }
+            }
+            return nRemoved;
+        }
+        public static int RemoveSmallComponents(DMesh3 mesh, double min_volume, double min_area) {
+            MeshEditor e = new MeshEditor(mesh); return e.RemoveSmallComponents(min_volume, min_area);
+        }
+
+
+
+
+
+
+
         // this is for backing out changes we have made...
         bool remove_triangles(int[] tri_list, int count)
         {
             for (int i = 0; i < count; ++i) {
+                if (Mesh.IsTriangle(tri_list[i]) == false)
+                    continue;
                 MeshResult result = Mesh.RemoveTriangle(tri_list[i], false, false);
                 if (result != MeshResult.Ok)
                     return false;
diff --git a/mesh/MeshIterators.cs b/mesh/MeshIterators.cs
index e3a775d9..6f9ce32d 100644
--- a/mesh/MeshIterators.cs
+++ b/mesh/MeshIterators.cs
@@ -124,6 +124,18 @@ public static IEnumerable<int> BoundaryEdges(DMesh3 mesh)
         }
 
 
+		public static IEnumerable<int> InteriorEdges(DMesh3 mesh)
+		{
+			int N = mesh.MaxEdgeID;
+			for (int i = 0; i < N; ++i) {
+				if (mesh.IsEdge(i)) {
+					if (mesh.IsBoundaryEdge(i) == false)
+						yield return i;
+				}
+			}
+		}
+
+
         public static IEnumerable<int> GroupBoundaryEdges(DMesh3 mesh)
         {
             int N = mesh.MaxEdgeID;
diff --git a/mesh/MeshMeasurements.cs b/mesh/MeshMeasurements.cs
index 91dcfe96..4a7cf0c5 100644
--- a/mesh/MeshMeasurements.cs
+++ b/mesh/MeshMeasurements.cs
@@ -149,6 +149,54 @@ public static void MassProperties(
 
 
 
+        /// <summary>
+        /// Compute volume and surface area of triangles of mesh.
+        /// Return value is (volume,area)
+        /// Note that if triangles don't define closed region, volume is probably nonsense...
+        /// </summary>
+        public static Vector2d VolumeArea( DMesh3 mesh, IEnumerable<int> triangles,
+            Func<int, Vector3d> getVertexF)
+        {
+            double mass_integral = 0.0;
+            double area_sum = 0;
+            foreach (int tid in triangles) {
+                Index3i tri = mesh.GetTriangle(tid);
+                // Get vertices of triangle i.
+                Vector3d v0 = getVertexF(tri.a);
+                Vector3d v1 = getVertexF(tri.b);
+                Vector3d v2 = getVertexF(tri.c);
+
+                // Get cross product of edges and (un-normalized) normal vector.
+                Vector3d V1mV0 = v1 - v0;
+                Vector3d V2mV0 = v2 - v0;
+                Vector3d N = V1mV0.Cross(V2mV0);
+
+                area_sum += 0.5 * N.Length;
+
+                double tmp0 = v0.x + v1.x;
+                double f1x = tmp0 + v2.x;
+                mass_integral += N.x * f1x;
+            }
+
+            return new Vector2d(mass_integral * (1.0/6.0), area_sum);
+        }
+
+
+
+        /// <summary>
+        /// Compute area of one-ring of mesh vertex by summing triangle areas.
+        /// If bDisjoint = true, we multiple each triangle area by 1/3
+        /// </summary>
+        public static double VertexOneRingArea( DMesh3 mesh, int vid, bool bDisjoint = true )
+        {
+            double sum = 0;
+            double mul = (bDisjoint) ? (1.0/3.0) : 1.0;
+            foreach (int tid in mesh.VtxTrianglesItr(vid))
+                sum += mesh.GetTriArea(tid) * mul;
+            return sum;
+        }
+
+
 
         public static Vector3d Centroid(IEnumerable<Vector3d> vertices)
         {
@@ -200,7 +248,7 @@ public static AxisAlignedBox3d Bounds(DMesh3 mesh, Func<Vector3d, Vector3d> Tran
             } else {
                 foreach (Vector3d v in mesh.Vertices()) {
                     Vector3d vT = TransformF(v);
-                    bounds.Contain(vT);
+                    bounds.Contain(ref vT);
                 }
             }
             return bounds;
@@ -214,7 +262,7 @@ public static AxisAlignedBox3d Bounds(IMesh mesh, Func<Vector3d, Vector3d> Trans
             } else {
                 foreach (int vID in mesh.VertexIndices()) {
                     Vector3d vT = TransformF(mesh.GetVertex(vID));
-                    bounds.Contain(vT);
+                    bounds.Contain(ref vT);
                 }
             }
             return bounds;
@@ -268,19 +316,60 @@ public static double AreaT(DMesh3 mesh, IEnumerable<int> triangleIndices)
         }
 
 
+        /// <summary>
+        /// calculate extents of mesh along axes of frame, with optional transform
+        /// </summary>
+        public static AxisAlignedBox3d BoundsInFrame(DMesh3 mesh, Frame3f frame, Func<Vector3d, Vector3d> TransformF = null)
+        {
+            AxisAlignedBox3d bounds = AxisAlignedBox3d.Empty;
+            if (TransformF == null) {
+                foreach (Vector3d v in mesh.Vertices()) {
+                    Vector3d fv = frame.ToFrameP(v);
+                    bounds.Contain(ref fv);
+                }
+            } else {
+                foreach (Vector3d v in mesh.Vertices()) {
+                    Vector3d vT = TransformF(v);
+                    Vector3d fv = frame.ToFrameP(ref vT);
+                    bounds.Contain(ref fv);
+                }
+            }
+            return bounds;
+        }
 
 
-
+        /// <summary>
+        /// Calculate extents of mesh along an axis, with optional transform
+        /// </summary>
         public static Interval1d ExtentsOnAxis(DMesh3 mesh, Vector3d axis, Func<Vector3d, Vector3d> TransformF = null)
         {
             Interval1d extent = Interval1d.Empty;
             if (TransformF == null) {
                 foreach (Vector3d v in mesh.Vertices()) 
-                    extent.Contain(v.Dot(axis));
+                    extent.Contain(v.Dot(ref axis));
             } else {
                 foreach (Vector3d v in mesh.Vertices()) {
                     Vector3d vT = TransformF(v);
-                    extent.Contain(vT.Dot(axis));
+                    extent.Contain(vT.Dot(ref axis));
+                }
+            }
+            return extent;
+        }
+
+
+        /// <summary>
+        /// Calculate extents of mesh along an axis, with optional transform
+        /// </summary>
+        public static Interval1d ExtentsOnAxis(IMesh mesh, Vector3d axis, Func<Vector3d, Vector3d> TransformF = null)
+        {
+            Interval1d extent = Interval1d.Empty;
+            if (TransformF == null) {
+                foreach (int vid in mesh.VertexIndices())
+                    extent.Contain(mesh.GetVertex(vid).Dot(ref axis));
+            } else {
+                foreach (int vid in mesh.VertexIndices()) {
+                    Vector3d vT = TransformF(mesh.GetVertex(vid));
+                    extent.Contain(vT.Dot(ref axis));
                 }
             }
             return extent;
@@ -289,6 +378,44 @@ public static Interval1d ExtentsOnAxis(DMesh3 mesh, Vector3d axis, Func<Vector3d
 
 
 
+
+        /// <summary>
+        /// Calculate the two most extreme vertices along an axis, with optional transform
+        /// </summary>
+        public static Interval1i ExtremeVertices(DMesh3 mesh, Vector3d axis, Func<Vector3d, Vector3d> TransformF = null)
+        {
+            Interval1d extent = Interval1d.Empty;
+            Interval1i extreme = new Interval1i(DMesh3.InvalidID, DMesh3.InvalidID);
+            if (TransformF == null) {
+                foreach (int vid in mesh.VertexIndices()) {
+                    double t = mesh.GetVertex(vid).Dot(ref axis);
+                    if ( t < extent.a ) {
+                        extent.a = t;
+                        extreme.a = vid;
+                    } else if ( t > extent.b ) {
+                        extent.b = t;
+                        extreme.b = vid;
+                    }
+                }
+            } else {
+                foreach (int vid in mesh.VertexIndices()) {
+                    double t = TransformF(mesh.GetVertex(vid)).Dot(ref axis);
+                    if (t < extent.a) {
+                        extent.a = t;
+                        extreme.a = vid;
+                    } else if (t > extent.b) {
+                        extent.b = t;
+                        extreme.b = vid;
+                    }
+                }
+            }
+            return extreme;
+        }
+
+
+
+
+
         public struct GenusResult
         {
             public bool Valid;
diff --git a/mesh/MeshNormals.cs b/mesh/MeshNormals.cs
index 23727fde..af20cf7b 100644
--- a/mesh/MeshNormals.cs
+++ b/mesh/MeshNormals.cs
@@ -40,6 +40,11 @@ public void Compute()
         }
 
 
+        public Vector3d this[int vid] {
+            get { return Normals[vid]; }
+        }
+
+
         public void CopyTo(DMesh3 SetMesh)
         {
             if (SetMesh.MaxVertexID < Mesh.MaxVertexID)
diff --git a/mesh/MeshPointSets.cs b/mesh/MeshPointSets.cs
index 80139a55..bc866095 100644
--- a/mesh/MeshPointSets.cs
+++ b/mesh/MeshPointSets.cs
@@ -35,6 +35,13 @@ public IEnumerable<int> VertexIndices()
         {
             return Mesh.EdgeIndices();
         }
+
+        /// <summary>
+        /// Timestamp is incremented any time any change is made to the mesh
+        /// </summary>
+        public int Timestamp {
+            get { return Mesh.Timestamp; }
+        }
     }
 
 
@@ -75,6 +82,13 @@ public IEnumerable<int> VertexIndices()
         {
             return Mesh.BoundaryEdgeIndices();
         }
+
+        /// <summary>
+        /// Timestamp is incremented any time any change is made to the mesh
+        /// </summary>
+        public int Timestamp {
+            get { return Mesh.Timestamp; }
+        }
     }
 
 
diff --git a/mesh/MeshRefinerBase.cs b/mesh/MeshRefinerBase.cs
index a0a8f8a9..96ea5075 100644
--- a/mesh/MeshRefinerBase.cs
+++ b/mesh/MeshRefinerBase.cs
@@ -16,6 +16,14 @@ public class MeshRefinerBase
         public bool AllowCollapseFixedVertsWithSameSetID = true;
 
 
+        /// <summary>
+        /// If normals dot product is less than this, we consider it a normal flip. default = 0
+        /// </summary>
+        public double EdgeFlipTolerance {
+            get { return edge_flip_tol; }
+            set { edge_flip_tol = MathUtil.Clamp(value, -1.0, 1.0); }
+        }
+        protected double edge_flip_tol = 0.0f;
 
 
         public MeshRefinerBase(DMesh3 mesh) {
@@ -42,6 +50,28 @@ public void SetExternalConstraints(MeshConstraints cons)
         }
 
 
+        /// <summary>
+        /// Set this to be able to cancel running remesher
+        /// </summary>
+        public ProgressCancel Progress = null;
+
+        /// <summary>
+        /// if this returns true, abort computation. 
+        /// </summary>
+        protected virtual bool Cancelled() {
+            return (Progress == null) ? false : Progress.Cancelled();
+        }
+
+
+        protected double edge_flip_metric(ref Vector3d n0, ref Vector3d n1)
+        {
+            if (edge_flip_tol == 0) {
+                return n0.Dot(n1);
+            } else {
+                return n0.Normalized.Dot(n1.Normalized);
+            }
+        }
+
 
         /// <summary>
         /// check if edge collapse will create a face-normal flip. 
@@ -65,16 +95,16 @@ protected bool collapse_creates_flip_or_invalid(int vid, int vother, ref Vector3
                 double sign = 0;
                 if (curt.a == vid) {
                     Vector3d nnew = (vb - newv).Cross(vc - newv);
-                    sign = ncur.Dot(ref nnew);
+                    sign = edge_flip_metric(ref ncur, ref nnew);
                 } else if (curt.b == vid) {
                     Vector3d nnew = (newv - va).Cross(vc - va);
-                    sign = ncur.Dot(ref nnew);
+                    sign = edge_flip_metric(ref ncur, ref nnew);
                 } else if (curt.c == vid) {
                     Vector3d nnew = (vb - va).Cross(newv - va);
-                    sign = ncur.Dot(ref nnew);
+                    sign = edge_flip_metric(ref ncur, ref nnew);
                 } else
                     throw new Exception("should never be here!");
-                if (sign <= 0.0)
+                if (sign <= edge_flip_tol)
                     return true;
             }
             return false;
@@ -98,10 +128,10 @@ protected bool flip_inverts_normals(int a, int b, int c, int d, int t0)
             Vector3d n0 = MathUtil.FastNormalDirection(ref vOA, ref vOB, ref vC);
             Vector3d n1 = MathUtil.FastNormalDirection(ref vOB, ref vOA, ref vD);
             Vector3d f0 = MathUtil.FastNormalDirection(ref vC, ref vD, ref vOB);
-            if (n0.Dot(f0) < 0 || n1.Dot(f0) < 0)
+            if ( edge_flip_metric(ref n0, ref f0) <= edge_flip_tol || edge_flip_metric(ref n1, ref f0) <= edge_flip_tol)
                 return true;
             Vector3d f1 = MathUtil.FastNormalDirection(ref vD, ref vC, ref vOA);
-            if ( n0.Dot(f1) < 0 || n1.Dot(f1) < 0 )
+            if (edge_flip_metric(ref n0, ref f1) <= edge_flip_tol || edge_flip_metric(ref n1, ref f1) <= edge_flip_tol)
                 return true;
 
             // this only checks if output faces are pointing towards eachother, which seems 
@@ -173,10 +203,15 @@ protected bool can_collapse_vtx(int eid, int a, int b, out int collapse_to)
 
             // handle a or b fixed
             if (ca.Fixed == true && cb.Fixed == false) {
+                // if b is fixed to a target, and it is different than a's target, we can't collapse
+                if (cb.Target != null && cb.Target != ca.Target)
+                    return false;
                 collapse_to = a;
                 return true;
             }
             if (cb.Fixed == true && ca.Fixed == false) {
+                if (ca.Target != null && ca.Target != cb.Target)
+                    return false;
                 collapse_to = b;
                 return true;
             }
@@ -235,7 +270,11 @@ protected VertexConstraint get_vertex_constraint(int vid)
                 return constraints.GetVertexConstraint(vid);
             return VertexConstraint.Unconstrained;
         }
-
+        protected bool get_vertex_constraint(int vid, ref VertexConstraint  vc)
+        {
+            return (constraints == null) ? false :
+                constraints.GetVertexConstraint(vid, ref vc);
+        }
 
     }
 }
diff --git a/mesh/MeshTransforms.cs b/mesh/MeshTransforms.cs
index 6f4f744d..99cfe776 100644
--- a/mesh/MeshTransforms.cs
+++ b/mesh/MeshTransforms.cs
@@ -37,6 +37,12 @@ public static Frame3f Rotate(Frame3f f, Vector3d origin, Quaternionf rotation)
             f.Origin = (Vector3f)Rotate(f.Origin, origin, rotation);
             return f;
         }
+        public static Frame3f Rotate(Frame3f f, Vector3d origin, Quaterniond rotation)
+        {
+            f.Rotate((Quaternionf)rotation);
+            f.Origin = (Vector3f)Rotate(f.Origin, origin, rotation);
+            return f;
+        }
         public static void Rotate(IDeformableMesh mesh, Vector3d origin, Quaternionf rotation)
         {
             int NV = mesh.MaxVertexID;
@@ -57,33 +63,42 @@ public static Vector3d Rotate(Vector3d pos, Vector3d origin, Quaterniond rotatio
         }
         public static void Rotate(IDeformableMesh mesh, Vector3d origin, Quaterniond rotation)
         {
+            bool bHasNormals = mesh.HasVertexNormals;
             int NV = mesh.MaxVertexID;
             for (int vid = 0; vid < NV; ++vid) {
                 if (mesh.IsVertex(vid)) {
                     Vector3d v = rotation * (mesh.GetVertex(vid) - origin) + origin;
                     mesh.SetVertex(vid, v);
+                    if ( bHasNormals )
+                        mesh.SetVertexNormal(vid, (Vector3f)(rotation * mesh.GetVertexNormal(vid)) );
                 }
             }
         }
 
 
-        public static void Scale(IDeformableMesh mesh, double sx, double sy, double sz)
+        public static void Scale(IDeformableMesh mesh, Vector3d scale, Vector3d origin)
         {
             int NV = mesh.MaxVertexID;
-            for ( int vid = 0; vid < NV; ++vid ) {
+            for (int vid = 0; vid < NV; ++vid) {
                 if (mesh.IsVertex(vid)) {
                     Vector3d v = mesh.GetVertex(vid);
-                    v.x *= sx; v.y *= sy; v.z *= sz;
+                    v.x -= origin.x; v.y -= origin.y; v.z -= origin.z;
+                    v.x *= scale.x; v.y *= scale.y; v.z *= scale.z;
+                    v.x += origin.x; v.y += origin.y; v.z += origin.z;
                     mesh.SetVertex(vid, v);
                 }
             }
         }
+        public static void Scale(IDeformableMesh mesh, double sx, double sy, double sz)
+        {
+            Scale(mesh, new Vector3d(sx, sy, sz), Vector3d.Zero);
+        }
         public static void Scale(IDeformableMesh mesh, double s)
         {
             Scale(mesh, s, s, s);
         }
 
-
+        ///<summary>Map mesh *into* local coordinates of Frame </summary>
         public static void ToFrame(IDeformableMesh mesh, Frame3f f)
         {
             int NV = mesh.MaxVertexID;
@@ -91,16 +106,18 @@ public static void ToFrame(IDeformableMesh mesh, Frame3f f)
             for ( int vid = 0; vid < NV; ++vid ) {
                 if (mesh.IsVertex(vid)) {
                     Vector3d v = mesh.GetVertex(vid);
-                    Vector3d vf = f.ToFrameP((Vector3f)v);
+                    Vector3d vf = f.ToFrameP(ref v);
                     mesh.SetVertex(vid, vf);
                     if ( bHasNormals ) {
                         Vector3f n = mesh.GetVertexNormal(vid);
-                        Vector3f nf = f.ToFrameV(n);
+                        Vector3f nf = f.ToFrameV(ref n);
                         mesh.SetVertexNormal(vid, nf);
                     }
                 }
             }
         }
+
+        /// <summary> Map mesh *from* local frame coordinates into "world" coordinates </summary>
         public static void FromFrame(IDeformableMesh mesh, Frame3f f)
         {
             int NV = mesh.MaxVertexID;
@@ -108,11 +125,11 @@ public static void FromFrame(IDeformableMesh mesh, Frame3f f)
             for ( int vid = 0; vid < NV; ++vid ) {
                 if (mesh.IsVertex(vid)) {
                     Vector3d vf = mesh.GetVertex(vid);
-                    Vector3d v = f.FromFrameP((Vector3f)vf);
+                    Vector3d v = f.FromFrameP(ref vf);
                     mesh.SetVertex(vid, v);
                     if ( bHasNormals ) {
                         Vector3f n = mesh.GetVertexNormal(vid);
-                        Vector3f nf = f.FromFrameV(n);
+                        Vector3f nf = f.FromFrameV(ref n);
                         mesh.SetVertexNormal(vid, nf);
                     }
                 }
@@ -268,6 +285,45 @@ public static void PerVertexTransform(IDeformableMesh mesh, Func<Vector3d, Vecto
         }
 
 
+        /// <summary>
+        /// Apply TransformF to vertices and normals of mesh
+        /// </summary>
+        public static void PerVertexTransform(IDeformableMesh mesh, Func<Vector3d, Vector3f, Vector3dTuple2> TransformF)
+        {
+            int NV = mesh.MaxVertexID;
+            for (int vid = 0; vid < NV; ++vid) {
+                if (mesh.IsVertex(vid)) {
+                    Vector3dTuple2 newPN = TransformF(mesh.GetVertex(vid), mesh.GetVertexNormal(vid));
+                    mesh.SetVertex(vid, newPN.V0);
+                    mesh.SetVertexNormal(vid, (Vector3f)newPN.V1);
+                }
+            }
+        }
+
+
+        /// <summary>
+        /// Apply Transform to vertices and normals of mesh
+        /// </summary>
+        public static void PerVertexTransform(IDeformableMesh mesh, TransformSequence xform)
+        {
+            int NV = mesh.MaxVertexID;
+            if (mesh.HasVertexNormals) {
+                for (int vid = 0; vid < NV; ++vid) {
+                    if (mesh.IsVertex(vid)) {
+                        mesh.SetVertex(vid, xform.TransformP(mesh.GetVertex(vid)));
+                        mesh.SetVertexNormal(vid, (Vector3f)xform.TransformV(mesh.GetVertexNormal(vid)));
+                    }
+                }
+            } else {
+                for (int vid = 0; vid < NV; ++vid) {
+                    if (mesh.IsVertex(vid)) 
+                        mesh.SetVertex(vid, xform.TransformP(mesh.GetVertex(vid)));
+                }
+            }
+        }
+
+
+
         /// <summary>
         /// Apply TransformF to subset of vertices of mesh
         /// </summary>
@@ -295,5 +351,22 @@ public static void PerVertexTransform(IDeformableMesh mesh, IEnumerable<int> ver
             }
         }
 
+
+        /// <summary>
+        /// Apply TransformF to subset of mesh vertices defined by MapV[vertices] 
+        /// </summary>
+        public static void PerVertexTransform(IDeformableMesh targetMesh, IDeformableMesh sourceMesh, int[] mapV, Func<Vector3d, int, int, Vector3d> TransformF)
+        {
+            foreach (int vid in sourceMesh.VertexIndices()) {
+                int map_vid = mapV[vid];
+                if (targetMesh.IsVertex(map_vid)) {
+                    Vector3d newPos = TransformF(targetMesh.GetVertex(map_vid), vid, map_vid);
+                    targetMesh.SetVertex(map_vid, newPos);
+                }
+            }
+        }
+
+
+
     }
 }
diff --git a/mesh/MeshUtil.cs b/mesh/MeshUtil.cs
index d378ea9c..251203fa 100644
--- a/mesh/MeshUtil.cs
+++ b/mesh/MeshUtil.cs
@@ -13,9 +13,16 @@ public static class MeshUtil {
 		public static Vector3d UniformSmooth(DMesh3 mesh, int vID, double t) 
 		{
 			Vector3d v = mesh.GetVertex(vID);
-			Vector3d c = MeshWeights.OneRingCentroid(mesh, vID);
-			return (1-t)*v + (t)*c;
-		}
+            //Vector3d c = MeshWeights.OneRingCentroid(mesh, vID);
+            //return (1 - t) * v + (t) * c;
+            Vector3d c = Vector3d.Zero;
+            mesh.VtxOneRingCentroid(vID, ref c);
+            double s = 1.0 - t;
+            v.x = s * v.x + t * c.x;
+            v.y = s * v.y + t * c.y;
+            v.z = s * v.z + t * c.z;
+            return v;
+        }
 
 		// t in range [0,1]
 		public static Vector3d MeanValueSmooth(DMesh3 mesh, int vID, double t) 
@@ -36,9 +43,9 @@ public static Vector3d CotanSmooth(DMesh3 mesh, int vID, double t)
 
 		public static void ScaleMesh(DMesh3 mesh, Frame3f f, Vector3f vScale) {
 			foreach ( int vid in mesh.VertexIndices() ) {
-				Vector3d v = mesh.GetVertex(vid);
-				Vector3f vScaledInF = f.ToFrameP((Vector3f)v) * vScale;
-				Vector3d vNew = f.FromFrameP(vScaledInF);
+				Vector3f v = (Vector3f)mesh.GetVertex(vid);
+				Vector3f vScaledInF = f.ToFrameP(ref v) * vScale;
+				Vector3d vNew = f.FromFrameP(ref vScaledInF);
 				mesh.SetVertex(vid, vNew);
 
 				// TODO: normals
@@ -47,7 +54,9 @@ public static void ScaleMesh(DMesh3 mesh, Frame3f f, Vector3f vScale) {
 
 
 
-
+        /// <summary>
+        /// computes opening angle between the two triangles connected to edge
+        /// </summary>
         public static double OpeningAngleD(DMesh3 mesh, int eid)
         {
             Index2i et = mesh.GetEdgeT(eid);
@@ -60,6 +69,22 @@ public static double OpeningAngleD(DMesh3 mesh, int eid)
         }
 
 
+        /// <summary>
+        /// computes sum of opening-angles in triangles around vid, minus 2pi.
+        /// This is zero on flat areas.
+        /// </summary>
+        public static double DiscreteGaussCurvature(DMesh3 mesh, int vid)
+        {
+            double angle_sum = 0;
+            foreach (int tid in mesh.VtxTrianglesItr(vid)) {
+                Index3i et = mesh.GetTriangle(tid);
+                int idx = IndexUtil.find_tri_index(vid, ref et);
+                angle_sum += mesh.GetTriInternalAngleR(tid, idx);
+            }
+            return angle_sum - MathUtil.TwoPI;
+        }
+
+
 
 
         /// <summary>
@@ -106,6 +131,91 @@ public static bool CheckIfCollapseCreatesFlip(DMesh3 mesh, int edgeID, Vector3d
 
 
 
+        /// <summary>
+        /// if before a flip we have normals (n1,n2) and after we have (m1,m2), check if
+        /// the dot between any of the 4 pairs changes sign after the flip, or is
+        /// less than the dot-product tolerance (ie angle tolerance)
+        /// </summary>
+        public static bool CheckIfEdgeFlipCreatesFlip(DMesh3 mesh, int eID, double flip_dot_tol = 0.0)
+        {
+            Util.gDevAssert(mesh.IsBoundaryEdge(eID) == false);
+            Index4i einfo = mesh.GetEdge(eID);
+            Index2i ov = mesh.GetEdgeOpposingV(eID);
+
+            int a = einfo.a, b = einfo.b, c = ov.a, d = ov.b;
+            int t0 = einfo.c;
+
+            Vector3d vC = mesh.GetVertex(c), vD = mesh.GetVertex(d);
+            Index3i tri_v = mesh.GetTriangle(t0);
+            int oa = a, ob = b;
+            IndexUtil.orient_tri_edge(ref oa, ref ob, ref tri_v);
+            Vector3d vOA = mesh.GetVertex(oa), vOB = mesh.GetVertex(ob);
+            Vector3d n0 = MathUtil.FastNormalDirection(ref vOA, ref vOB, ref vC);
+            Vector3d n1 = MathUtil.FastNormalDirection(ref vOB, ref vOA, ref vD);
+            Vector3d f0 = MathUtil.FastNormalDirection(ref vC, ref vD, ref vOB);
+            if (edge_flip_metric(ref n0, ref f0, flip_dot_tol) <= flip_dot_tol 
+                || edge_flip_metric(ref n1, ref f0, flip_dot_tol) <= flip_dot_tol)
+                return true;
+            Vector3d f1 = MathUtil.FastNormalDirection(ref vD, ref vC, ref vOA);
+            if (edge_flip_metric(ref n0, ref f1, flip_dot_tol) <= flip_dot_tol 
+                || edge_flip_metric(ref n1, ref f1, flip_dot_tol) <= flip_dot_tol)
+                return true;
+            return false;
+        }
+        static double edge_flip_metric(ref Vector3d n0, ref Vector3d n1, double flip_dot_tol) {
+            return (flip_dot_tol == 0) ? n0.Dot(n1) : n0.Normalized.Dot(n1.Normalized);
+        }
+
+
+
+        /// <summary>
+        /// For given edge, return it's triangles and the triangles that would
+        /// be created if it was flipped (used in edge-flip optimizers)
+        /// </summary>
+        public static void GetEdgeFlipTris(DMesh3 mesh, int eID,
+            out Index3i orig_t0, out Index3i orig_t1,
+            out Index3i flip_t0, out Index3i flip_t1)
+        {
+            Index4i einfo = mesh.GetEdge(eID);
+            Index2i ov = mesh.GetEdgeOpposingV(eID);
+            int a = einfo.a, b = einfo.b, c = ov.a, d = ov.b;
+            int t0 = einfo.c;
+            Index3i tri_v = mesh.GetTriangle(t0);
+            int oa = a, ob = b;
+            IndexUtil.orient_tri_edge(ref oa, ref ob, ref tri_v);
+            orig_t0 = new Index3i(oa, ob, c);
+            orig_t1 = new Index3i(ob, oa, d);
+            flip_t0 = new Index3i(c, d, ob);
+            flip_t1 = new Index3i(d, c, oa);
+        }
+
+
+        /// <summary>
+        /// For given edge, return normals of it's two triangles, and normals
+        /// of the triangles created if edge is flipped (used in edge-flip optimizers)
+        /// </summary>
+        public static void GetEdgeFlipNormals(DMesh3 mesh, int eID, 
+            out Vector3d n1, out Vector3d n2,
+            out Vector3d on1, out Vector3d on2)
+        {
+            Index4i einfo = mesh.GetEdge(eID);
+            Index2i ov = mesh.GetEdgeOpposingV(eID);
+            int a = einfo.a, b = einfo.b, c = ov.a, d = ov.b;
+            int t0 = einfo.c;
+            Vector3d vC = mesh.GetVertex(c), vD = mesh.GetVertex(d);
+            Index3i tri_v = mesh.GetTriangle(t0);
+            int oa = a, ob = b;
+            IndexUtil.orient_tri_edge(ref oa, ref ob, ref tri_v);
+            Vector3d vOA = mesh.GetVertex(oa), vOB = mesh.GetVertex(ob);
+            n1 = MathUtil.Normal(ref vOA, ref vOB, ref vC);
+            n2 = MathUtil.Normal(ref vOB, ref vOA, ref vD);
+            on1 = MathUtil.Normal(ref vC, ref vD, ref vOB);
+            on2 = MathUtil.Normal(ref vD, ref vC, ref vOA);
+        }
+
+
+
+
         public static DCurve3 ExtractLoopV(IMesh mesh, IEnumerable<int> vertices) {
             DCurve3 curve = new DCurve3();
             foreach (int vid in vertices)
diff --git a/mesh/MeshWeights.cs b/mesh/MeshWeights.cs
index 354e1807..a16b1d06 100644
--- a/mesh/MeshWeights.cs
+++ b/mesh/MeshWeights.cs
@@ -141,6 +141,8 @@ public static Vector3d MeanValueCentroid(DMesh3 mesh, int v_i)
 				vSum += w_ij * Vj;
 				wSum += w_ij;
 			}
+            if ( wSum < MathUtil.ZeroTolerance )
+                return Vi;
 			return vSum / wSum;
 		}
 		// tan(theta/2) = +/- sqrt( (1-cos(theta)) / (1+cos(theta)) )
diff --git a/mesh/NTMesh3.cs b/mesh/NTMesh3.cs
index e24b9f01..145b585e 100644
--- a/mesh/NTMesh3.cs
+++ b/mesh/NTMesh3.cs
@@ -1617,7 +1617,7 @@ public bool CheckValidity(FailMode eFailMode = FailMode.Throw)
                     CheckOrFailF(IsEdge(edge));
                 }
 
-                List<int> vTris = new List<int>(), vTris2 = new List<int>();
+                List<int> vTris = new List<int>();
                 GetVtxTriangles(vID, vTris);
 
                 CheckOrFailF(vertices_refcount.refCount(vID) == vTris.Count + 1);
diff --git a/mesh/Reducer.cs b/mesh/Reducer.cs
index b65ec6f0..99757e6f 100644
--- a/mesh/Reducer.cs
+++ b/mesh/Reducer.cs
@@ -24,7 +24,7 @@ public class Reducer : MeshRefinerBase
 		public bool MinimizeQuadricPositionError = true;
 
         // if true, we try to keep boundary vertices on boundary. You probably want this.
-        public bool PreserveBoundary = true;
+        public bool PreserveBoundaryShape = true;
 
 		// [RMS] this is a debugging aid, will break to debugger if these edges are touched, in debug builds
 		public List<int> DebugEdges = new List<int>();
@@ -81,8 +81,14 @@ public virtual void DoReduce()
 
             begin_setup();
             Precompute();
+            if (Cancelled())
+                return;
             InitializeVertexQuadrics();
+            if (Cancelled())
+                return;
             InitializeQueue();
+            if (Cancelled())
+                return;
             end_setup();
 
             begin_ops();
@@ -103,6 +109,8 @@ public virtual void DoReduce()
                 int eid = EdgeQueue.Dequeue();
                 if (!mesh.IsEdge(eid))
                     continue;
+                if (Cancelled())
+                    return;
 
                 int vKept;
                 ProcessResult result = CollapseEdge(eid, EdgeQuadrics[eid].collapse_pt, out vKept);
@@ -114,6 +122,9 @@ public virtual void DoReduce()
             end_collapse();
             end_ops();
 
+            if (Cancelled())
+                return;
+
             Reproject();
 
             end_pass();
@@ -155,7 +166,7 @@ public virtual void ReduceToEdgeLength(double minEdgeLen)
 
 
 
-        public virtual void FastCollapsePass(double fMinEdgeLength)
+        public virtual void FastCollapsePass(double fMinEdgeLength, int nRounds = 1, bool MeshIsClosedHint = false)
         {
             if (mesh.TriangleCount == 0)    // badness if we don't catch this...
                 return;
@@ -169,7 +180,9 @@ public virtual void FastCollapsePass(double fMinEdgeLength)
             begin_pass();
 
             begin_setup();
-            Precompute();
+            Precompute(MeshIsClosedHint);
+            if (Cancelled())
+                return;
             end_setup();
 
             begin_ops();
@@ -177,29 +190,42 @@ public virtual void FastCollapsePass(double fMinEdgeLength)
             begin_collapse();
 
             int N = mesh.MaxEdgeID;
-            Vector3d va = Vector3d.Zero, vb = Vector3d.Zero;
-            for ( int eid = 0; eid < N; ++eid) {
-                if (!mesh.IsEdge(eid))
-                    continue;
-                if (mesh.IsBoundaryEdge(eid))
-                    continue;
-
-                mesh.GetEdgeV(eid, ref va, ref vb);
-                if (va.DistanceSquared(ref vb) > min_sqr)
-                    continue;
-
-                COUNT_ITERATIONS++;
-
-                Vector3d midpoint = (va + vb) * 0.5;
-                int vKept;
-                ProcessResult result = CollapseEdge(eid, midpoint, out vKept);
-                if (result == ProcessResult.Ok_Collapsed) {
-                    // do nothing?
+            int num_last_pass = 0;
+            for (int ri = 0; ri < nRounds; ++ri) {
+                num_last_pass = 0;
+
+                Vector3d va = Vector3d.Zero, vb = Vector3d.Zero;
+                for (int eid = 0; eid < N; ++eid) {
+                    if (!mesh.IsEdge(eid))
+                        continue;
+                    if (mesh.IsBoundaryEdge(eid))
+                        continue;
+                    if (Cancelled())
+                        return;
+
+                    mesh.GetEdgeV(eid, ref va, ref vb);
+                    if (va.DistanceSquared(ref vb) > min_sqr)
+                        continue;
+
+                    COUNT_ITERATIONS++;
+
+                    Vector3d midpoint = (va + vb) * 0.5;
+                    int vKept;
+                    ProcessResult result = CollapseEdge(eid, midpoint, out vKept);
+                    if (result == ProcessResult.Ok_Collapsed) {
+                        ++num_last_pass;
+                    }
                 }
+
+                if (num_last_pass == 0)     // converged
+                    break;
             }
             end_collapse();
             end_ops();
 
+            if (Cancelled())
+                return;
+
             Reproject();
 
             end_pass();
@@ -326,7 +352,7 @@ protected Vector3d OptimalPoint(int eid, ref QuadricError q, int ea, int eb) {
 
             // if we would like to preserve boundary, we need to know that here
             // so that we properly score these edges
-            if (HaveBoundary && PreserveBoundary) {
+            if (HaveBoundary && PreserveBoundaryShape) {
                 if (mesh.IsBoundaryEdge(eid)) {
                     return (mesh.GetVertex(ea) + mesh.GetVertex(eb)) * 0.5;
                 } else {
@@ -402,15 +428,17 @@ protected virtual void Reproject() {
 
         protected bool HaveBoundary;
         protected bool[] IsBoundaryVtxCache;
-        protected virtual void Precompute()
+        protected virtual void Precompute(bool bMeshIsClosed = false)
         {
             HaveBoundary = false;
             IsBoundaryVtxCache = new bool[mesh.MaxVertexID];
-            foreach ( int eid in mesh.BoundaryEdgeIndices()) {
-                Index2i ev = mesh.GetEdgeV(eid);
-                IsBoundaryVtxCache[ev.a] = true;
-                IsBoundaryVtxCache[ev.b] = true;
-                HaveBoundary = true;
+            if (bMeshIsClosed == false) {
+                foreach (int eid in mesh.BoundaryEdgeIndices()) {
+                    Index2i ev = mesh.GetEdgeV(eid);
+                    IsBoundaryVtxCache[ev.a] = true;
+                    IsBoundaryVtxCache[ev.b] = true;
+                    HaveBoundary = true;
+                }
             }
         }
         protected bool IsBoundaryV(int vid)
@@ -513,7 +541,7 @@ protected virtual ProcessResult CollapseEdge(int edgeID, Vector3d vNewPos, out i
 				return ProcessResult.Ignored_Constrained;
 
             // if we have a boundary, we want to collapse to boundary
-            if (PreserveBoundary && HaveBoundary) {
+            if (PreserveBoundaryShape && HaveBoundary) {
                 if (collapse_to != -1) {
                     if (( IsBoundaryV(b) && collapse_to != b) ||
                          ( IsBoundaryV(a) && collapse_to != a))
diff --git a/mesh/Remesher.cs b/mesh/Remesher.cs
index 4c06fbd5..4f784eba 100644
--- a/mesh/Remesher.cs
+++ b/mesh/Remesher.cs
@@ -171,10 +171,15 @@ public virtual void BasicRemeshPass() {
                     if (result == ProcessResult.Ok_Collapsed || result == ProcessResult.Ok_Flipped || result == ProcessResult.Ok_Split)
                         ModifiedEdgesLastPass++;
                 }
+                if (Cancelled())        // expensive to check every iter?
+                    return;
                 cur_eid = next_edge(cur_eid, out done);
             } while (done == false);
             end_ops();
 
+            if (Cancelled())
+                return;
+
             begin_smooth();
             if (EnableSmoothing && SmoothSpeedT > 0) {
                 if (EnableSmoothInPlace)
@@ -185,6 +190,9 @@ public virtual void BasicRemeshPass() {
             }
             end_smooth();
 
+            if (Cancelled())
+                return;
+
             begin_project();
             if (target != null && ProjectionMode == TargetProjectionMode.AfterRefinement) {
                 FullProjectionPass();
@@ -192,6 +200,9 @@ public virtual void BasicRemeshPass() {
             }
             end_project();
 
+            if (Cancelled())
+                return;
+
             end_pass();
 		}
 
@@ -451,13 +462,24 @@ protected virtual void update_after_split(int edgeID, int va, int vb, ref DMesh3
                     bPositionFixed = true;
                 }
 
-                // vert inherits Target if both source verts and edge have same Target
-                if ( ca.Target != null && ca.Target == cb.Target 
-                     && constraints.GetEdgeConstraint(edgeID).Target == ca.Target ) {
-                    constraints.SetOrUpdateVertexConstraint(splitInfo.vNew,
-                        new VertexConstraint(ca.Target));
-                    project_vertex(splitInfo.vNew, ca.Target);
-                    bPositionFixed = true;
+                // vert inherits Target if:
+                //  1) both source verts and edge have same Target, and is same as edge target
+                //  2) either vert has same target as edge, and other vert is fixed
+                if ( ca.Target != null || cb.Target != null ) {
+                    IProjectionTarget edge_target = constraints.GetEdgeConstraint(edgeID).Target;
+                    IProjectionTarget set_target = null;
+                    if (ca.Target == cb.Target && ca.Target == edge_target)
+                        set_target = edge_target;
+                    else if (ca.Target == edge_target && cb.Fixed)
+                        set_target = edge_target;
+                    else if (cb.Target == edge_target && ca.Fixed)
+                        set_target = edge_target;
+                    if ( set_target != null ) {
+                        constraints.SetOrUpdateVertexConstraint(splitInfo.vNew,
+                            new VertexConstraint(set_target));
+                        project_vertex(splitInfo.vNew, set_target);
+                        bPositionFixed = true;
+                    }
                 }
             }
 
@@ -604,7 +626,8 @@ protected virtual void ApplyVertexBuffer(bool bParallel)
         protected virtual Vector3d ComputeSmoothedVertexPos(int vID, Func<DMesh3, int, double, Vector3d> smoothFunc, out bool bModified)
         {
             bModified = false;
-            VertexConstraint vConstraint = get_vertex_constraint(vID);
+            VertexConstraint vConstraint = VertexConstraint.Unconstrained;
+            get_vertex_constraint(vID, ref vConstraint);
             if (vConstraint.Fixed)
                 return Mesh.GetVertex(vID);
             VertexControl vControl = (VertexControlF == null) ? VertexControl.AllowAll : VertexControlF(vID);
@@ -653,56 +676,6 @@ protected virtual void FullProjectionPass()
         }
 
 
-        // Project vertices towards projection target by input alpha, and optionally, don't project vertices too far away
-        // We can do projection in parallel if we have .net 
-        // [TODO] this code is currently not called
-        protected virtual void FullProjectionPass(double projectionAlpha, double maxProjectDistance)
-        {
-            projectionAlpha = MathUtil.Clamp(projectionAlpha, 0, 1);
-
-            Action<int> project;
-
-            if (maxProjectDistance < double.MaxValue && maxProjectDistance > 0) {
-                project = (vID) => {
-                    if (vertex_is_constrained(vID))
-                        return;
-                    if (VertexControlF != null && (VertexControlF(vID) & VertexControl.NoProject) != 0)
-                        return;
-                    Vector3d curpos = mesh.GetVertex(vID);
-                    Vector3d projected = target.Project(curpos, vID);
-
-                    var distance = curpos.Distance(projected);
-                    if (distance < maxProjectDistance) {
-                        projected = Vector3d.Lerp(curpos, projected, projectionAlpha);
-                        double distanceAlpha = distance / maxProjectDistance;
-                        projected = Vector3d.Lerp(projected, curpos, distanceAlpha);
-                        mesh.SetVertex(vID, projected);
-                    }
-                };
-            } else {
-                project = (vID) => {
-                    if (vertex_is_constrained(vID))
-                        return;
-                    if (VertexControlF != null && (VertexControlF(vID) & VertexControl.NoProject) != 0)
-                        return;
-                    Vector3d curpos = mesh.GetVertex(vID);
-                    Vector3d projected = target.Project(curpos, vID);
-                    projected = Vector3d.Lerp(curpos, projected, projectionAlpha);
-                    mesh.SetVertex(vID, projected);
-                };
-            }
-
-            if (EnableParallelProjection) {
-                gParallel.ForEach<int>(project_vertices(), project);
-            } else {
-                foreach (int vid in project_vertices())
-                    project(vid);
-            }
-        }
-
-
-
-
         [Conditional("DEBUG")] 
         void RuntimeDebugCheck(int eid)
         {
diff --git a/mesh/RemesherPro.cs b/mesh/RemesherPro.cs
new file mode 100644
index 00000000..53cf6f51
--- /dev/null
+++ b/mesh/RemesherPro.cs
@@ -0,0 +1,731 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Threading;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// Extension to Remesher that is smarter about which edges/vertices to touch:
+    ///  - queue tracks edges that were affected on last pass, and hence might need to be updated
+    ///  - FastSplitIteration() just does splits, to reach target edge length as quickly as possible
+    ///  - RemeshIteration() applies remesh pass for modified edges
+    ///  - TrackedSmoothPass() smooths all vertices but only adds to queue if edge changes enough
+    ///  - TrackedProjectionPass() same
+    /// 
+    /// </summary>
+    public class RemesherPro : Remesher
+    {
+
+        public bool UseFaceAlignedProjection = false;
+        public int FaceProjectionPassesPerIteration = 1;
+
+
+
+        public RemesherPro(DMesh3 m) : base(m)
+        {
+        }
+
+
+        HashSet<int> modified_edges;
+        SpinLock modified_edges_lock = new SpinLock();
+
+
+        protected IEnumerable<int> EdgesIterator()
+        {
+            int cur_eid = start_edges();
+            bool done = false;
+            do {
+                yield return cur_eid;
+                cur_eid = next_edge(cur_eid, out done);
+            } while (done == false);
+        }
+
+
+        void queue_one_ring_safe(int vid) {
+            if ( mesh.IsVertex(vid) ) {
+                bool taken = false;
+                modified_edges_lock.Enter(ref taken);
+
+                foreach (int eid in mesh.VtxEdgesItr(vid))
+                    modified_edges.Add(eid);
+
+                modified_edges_lock.Exit();
+            }
+        }
+
+        void queue_one_ring(int vid) {
+            if ( mesh.IsVertex(vid) ) {
+                foreach (int eid in mesh.VtxEdgesItr(vid))
+                    modified_edges.Add(eid);
+            }
+        }
+
+        void queue_edge_safe(int eid) {
+            bool taken = false;
+            modified_edges_lock.Enter(ref taken);
+
+            modified_edges.Add(eid);
+
+            modified_edges_lock.Exit();
+        }
+
+        void queue_edge(int eid) {
+            modified_edges.Add(eid);
+        }
+
+
+
+        Action<int, int, int, int> SplitF = null;
+
+
+        protected override void OnEdgeSplit(int edgeID, int va, int vb, DMesh3.EdgeSplitInfo splitInfo)
+        {
+            if (SplitF != null)
+                SplitF(edgeID, va, vb, splitInfo.vNew);
+        }
+
+
+
+        /// <summary>
+        /// Converge on remeshed result as quickly as possible
+        /// </summary>
+        public void FastestRemesh(int nMaxIterations = 25, bool bDoFastSplits = true)
+        {
+            ResetQueue();
+
+            // first we do fast splits to hit edge length target
+            // ?? should we do project in fastsplit? will result in more splits, and
+            //    we are going to project in first remesh pass anyway...
+            //    (but that might result in larger queue in first remesh pass?)
+            int fastsplit_i = 0;
+            int max_fastsplits = nMaxIterations;
+            if (bDoFastSplits) {
+                if (Cancelled())
+                    return;
+
+                bool bContinue = true;
+                while ( bContinue ) {
+                    int nSplits = FastSplitIteration();
+                    if (fastsplit_i++ > max_fastsplits)
+                        bContinue = false;
+                    if ((double)nSplits / (double)mesh.EdgeCount < 0.01)
+                        bContinue = false;
+                    if (Cancelled())
+                        return;
+                };
+                ResetQueue();
+            }
+
+            // should we do a fast collapse pass? more dangerous...
+
+            // now do queued remesh iterations. 
+            // disable projection every other iteration to improve speed
+            var saveMode = this.ProjectionMode;
+            for (int k = 0; k < nMaxIterations - 1; ++k) {
+                if (Cancelled())
+                    break;
+                ProjectionMode = (k % 2 == 0) ? TargetProjectionMode.NoProjection : saveMode;
+                RemeshIteration();
+            }
+
+            // final pass w/ full projection
+            ProjectionMode = saveMode;
+
+            if (Cancelled())
+                return;
+
+            RemeshIteration();
+        }
+
+
+
+
+
+
+        /// <summary>
+        /// This is a remesh that tries to recover sharp edges by aligning triangles to face normals
+        /// of our projection target (similar to Ohtake RZN-flow). 
+        /// </summary>
+        public void SharpEdgeReprojectionRemesh(int nRemeshIterations, int nTuneIterations, bool bDoFastSplits = true)
+        {
+            if (ProjectionTarget == null || ProjectionTarget is IOrientedProjectionTarget == false)
+                throw new Exception("RemesherPro.SharpEdgeReprojectionRemesh: cannot call this without a ProjectionTarget that has normals");
+
+            ResetQueue();
+
+            // first we do fast splits to hit edge length target
+            // ?? should we do project in fastsplit? will result in more splits, and
+            //    we are going to project in first remesh pass anyway...
+            //    (but that might result in larger queue in first remesh pass?)
+            int fastsplit_i = 0;
+            int max_fastsplits = nRemeshIterations;
+            if (bDoFastSplits) {
+                if (Cancelled())
+                    return;
+
+                bool bContinue = true;
+                while (bContinue) {
+                    int nSplits = FastSplitIteration();
+                    if (fastsplit_i++ > max_fastsplits)
+                        bContinue = false;
+                    if ((double)nSplits / (double)mesh.EdgeCount < 0.01)
+                        bContinue = false;
+                    if (Cancelled())
+                        return;
+                };
+                ResetQueue();
+            }
+
+            bool save_use_face_aligned = UseFaceAlignedProjection;
+            UseFaceAlignedProjection = true;
+            FaceProjectionPassesPerIteration = 1;
+
+            // should we do a fast collapse pass? more dangerous but would get rid of all the tiny
+            // edges we might have just created, and/or get us closer to target resolution
+
+            // now do queued remesh iterations. As we proceed we slowly step
+            // down the smoothing factor, this helps us get triangles closer
+            // to where they will ultimately want to go
+            double smooth_speed = SmoothSpeedT;
+            for (int k = 0; k < nRemeshIterations; ++k) {
+                if (Cancelled())
+                    break;
+                RemeshIteration();
+                if ( k > nRemeshIterations/2 )
+                    SmoothSpeedT *= 0.9f;
+            }
+
+            // [TODO] would like to still do splits and maybe sometimes flips here. 
+            // Perhaps this could be something more combinatorial? Like, test all the
+            // edges we queued in the projection pass, if we can get better alignment
+            // after a with flip or split, do it
+            //SmoothSpeedT = 0;
+            //MinEdgeLength = MinEdgeLength * 0.1;
+            //EnableFlips = false;
+            for (int k = 0; k < nTuneIterations; ++k) {
+                if (Cancelled())
+                    break;
+                TrackedFaceProjectionPass();
+                //RemeshIteration();
+            }
+
+            SmoothSpeedT = smooth_speed;
+            UseFaceAlignedProjection = save_use_face_aligned;
+
+            //TrackedProjectionPass(true);
+        }
+
+
+
+
+
+
+
+
+
+
+
+        /// <summary>
+        /// Reset tracked-edges queue. Should be called if mesh is modified by external functions
+        /// between passes, and also between different types of passes (eg FastSplitIteration vs RemeshIteration)
+        /// </summary>
+        public void ResetQueue()
+        {
+            if (modified_edges != null) {
+                modified_edges.Clear();
+                modified_edges = null;
+            }
+        }
+
+        List<int> edges_buffer = new List<int>();
+
+
+        /// <summary>
+        /// This pass only does edge splits. Returns number of split edges.
+        /// Tracks previously-split 
+        /// </summary>
+        public int FastSplitIteration()
+        {
+            if (mesh.TriangleCount == 0)    // badness if we don't catch this...
+                return 0;
+
+            PushState();
+            EnableFlips = EnableCollapses = EnableSmoothing = false;
+            ProjectionMode = TargetProjectionMode.NoProjection;
+
+            begin_pass();
+
+            // Iterate over all edges in the mesh at start of pass.
+            // Some may be removed, so we skip those.
+            // However, some old eid's may also be re-used, so we will touch
+            // some new edges. Can't see how we could efficiently prevent this.
+            //
+            begin_ops();
+
+            IEnumerable<int> edgesItr = EdgesIterator();
+            if (modified_edges == null) {
+                modified_edges = new HashSet<int>();
+            } else {
+                edges_buffer.Clear(); edges_buffer.AddRange(modified_edges);
+                edgesItr = edges_buffer;
+                modified_edges.Clear();
+            }
+
+            int startEdges = Mesh.EdgeCount;
+            int splitEdges = 0;
+            
+            // When we split an edge, we need to check it and the adjacent ones we added.
+            // Because of overhead in ProcessEdge, it is worth it to do a distance-check here
+            double max_edge_len_sqr = MaxEdgeLength * MaxEdgeLength;
+            SplitF = (edgeID, a, b, vNew) => {
+                Vector3d v = Mesh.GetVertex(vNew);
+                foreach (int eid in Mesh.VtxEdgesItr(vNew)) {
+                    Index2i ev = Mesh.GetEdgeV(eid);
+                    int othervid = (ev.a == vNew) ? ev.b : ev.a;
+                    if (mesh.GetVertex(othervid).DistanceSquared(ref v) > max_edge_len_sqr)
+                        queue_edge(eid);
+                }
+                //queue_one_ring(vNew);
+            };
+
+
+            ModifiedEdgesLastPass = 0;
+            int processedLastPass = 0;
+            foreach (int cur_eid in edgesItr) {
+                if (Cancelled())
+                    goto abort_compute;
+
+                if (mesh.IsEdge(cur_eid)) {
+                    Index2i ev = mesh.GetEdgeV(cur_eid);
+                    Index2i ov = mesh.GetEdgeOpposingV(cur_eid);
+
+                    processedLastPass++;
+                    ProcessResult result = ProcessEdge(cur_eid);
+                    if (result == ProcessResult.Ok_Split) {
+                        // new edges queued by SplitF
+                        ModifiedEdgesLastPass++;
+                        splitEdges++;
+                    } 
+                }
+            }
+            end_ops();
+
+            //System.Console.WriteLine("FastSplitIteration: start {0}  end {1}  processed: {2}   modified: {3}  queue: {4}",
+            //    startEdges, Mesh.EdgeCount, processedLastPass, ModifiedEdgesLastPass, modified_edges.Count);
+
+            abort_compute:
+            SplitF = null;
+            PopState();
+
+            end_pass();
+
+            return splitEdges;
+        }
+
+
+
+
+
+
+        public virtual void RemeshIteration()
+        {
+            if (mesh.TriangleCount == 0)    // badness if we don't catch this...
+                return;
+
+            begin_pass();
+
+            // Iterate over all edges in the mesh at start of pass.
+            // Some may be removed, so we skip those.
+            // However, some old eid's may also be re-used, so we will touch
+            // some new edges. Can't see how we could efficiently prevent this.
+            //
+            begin_ops();
+
+            IEnumerable<int> edgesItr = EdgesIterator();
+            if (modified_edges == null) {
+                modified_edges = new HashSet<int>();
+            } else {
+                edges_buffer.Clear(); edges_buffer.AddRange(modified_edges);
+                edgesItr = edges_buffer;
+                modified_edges.Clear();
+            }
+
+            int startEdges = Mesh.EdgeCount;
+            int flips = 0, splits = 0, collapes = 0;
+
+            ModifiedEdgesLastPass = 0;
+            int processedLastPass = 0;
+            foreach (int cur_eid in edgesItr) {
+                if (Cancelled())
+                    return;
+
+                if (mesh.IsEdge(cur_eid)) {
+                    Index2i ev = mesh.GetEdgeV(cur_eid);
+                    Index2i ov = mesh.GetEdgeOpposingV(cur_eid);
+
+                    // TODO: optimize the queuing here, are over-doing it!
+                    // TODO: be able to queue w/o flip (eg queue from smooth never requires flip check)
+
+                    processedLastPass++;
+                    ProcessResult result = ProcessEdge(cur_eid);
+                    if (result == ProcessResult.Ok_Collapsed) {
+                        queue_one_ring(ev.a); queue_one_ring(ev.b);
+                        queue_one_ring(ov.a); queue_one_ring(ov.b);
+                        ModifiedEdgesLastPass++;
+                        collapes++;
+                    } else if (result == ProcessResult.Ok_Split) {
+                        queue_one_ring(ev.a); queue_one_ring(ev.b);
+                        queue_one_ring(ov.a); queue_one_ring(ov.b);
+                        ModifiedEdgesLastPass++;
+                        splits++;
+                    } else if (result == ProcessResult.Ok_Flipped) {
+                        queue_one_ring(ev.a); queue_one_ring(ev.b);
+                        queue_one_ring(ov.a); queue_one_ring(ov.b);
+                        ModifiedEdgesLastPass++;
+                        flips++;
+                    }
+                }
+            }
+            end_ops();
+
+            //System.Console.WriteLine("RemeshIteration: start {0}  end {1}  processed: {2}   modified: {3}  queue: {4}",
+            //    startEdges, Mesh.EdgeCount, processedLastPass, ModifiedEdgesLastPass, modified_edges.Count);
+            //System.Console.WriteLine("   flips {0}  splits {1}  collapses {2}", flips, splits, collapes);
+
+            if (Cancelled())
+                return;
+
+            begin_smooth();
+            if (EnableSmoothing && SmoothSpeedT > 0) {
+                TrackedSmoothPass(EnableParallelSmooth);
+                DoDebugChecks();
+            }
+            end_smooth();
+
+            if (Cancelled())
+                return;
+
+            begin_project();
+            if (ProjectionTarget != null && ProjectionMode == TargetProjectionMode.AfterRefinement) {
+                //FullProjectionPass();
+
+                if (UseFaceAlignedProjection) {
+                    for ( int i = 0; i < FaceProjectionPassesPerIteration; ++i )
+                        TrackedFaceProjectionPass();
+                } else {
+                    TrackedProjectionPass(EnableParallelProjection);
+                }
+                DoDebugChecks();
+            }
+            end_project();
+
+            end_pass();
+        }
+
+
+        protected virtual void TrackedSmoothPass(bool bParallel)
+        {
+            InitializeVertexBufferForPass();
+
+            Func<DMesh3, int, double, Vector3d> smoothFunc = MeshUtil.UniformSmooth;
+            if (CustomSmoothF != null) {
+                smoothFunc = CustomSmoothF;
+            } else {
+                if (SmoothType == SmoothTypes.MeanValue)
+                    smoothFunc = MeshUtil.MeanValueSmooth;
+                else if (SmoothType == SmoothTypes.Cotan)
+                    smoothFunc = MeshUtil.CotanSmooth;
+            }
+
+            Action<int> smooth = (vID) => {
+                Vector3d vCur = Mesh.GetVertex(vID);
+                bool bModified = false;
+                Vector3d vSmoothed = ComputeSmoothedVertexPos(vID, smoothFunc, out bModified);
+                //if (vCur.EpsilonEqual(vSmoothed, MathUtil.ZeroTolerancef))
+                //    bModified = false;
+                if (bModified) {
+                    vModifiedV[vID] = true;
+                    vBufferV[vID] = vSmoothed;
+
+                    foreach (int eid in mesh.VtxEdgesItr(vID)) {
+                        Index2i ev = Mesh.GetEdgeV(eid);
+                        int othervid = (ev.a == vID) ? ev.b : ev.a;
+                        Vector3d otherv = mesh.GetVertex(othervid);
+                        double old_len = vCur.Distance(otherv);
+                        double new_len = vSmoothed.Distance(otherv);
+                        if (new_len < MinEdgeLength || new_len > MaxEdgeLength)
+                            queue_edge_safe(eid);
+                    }
+                }
+            };
+
+
+            if (bParallel) {
+                gParallel.ForEach<int>(smooth_vertices(), smooth);
+            } else {
+                foreach (int vID in smooth_vertices())
+                    smooth(vID);
+            }
+
+            ApplyVertexBuffer(bParallel);
+            //System.Console.WriteLine("Smooth Pass: queue: {0}", modified_edges.Count);
+        }
+
+
+
+
+
+        // [TODO] projection pass
+        //   - only project vertices modified by smooth pass?
+        //   - and/or verts in set of modified edges? 
+        protected virtual void TrackedProjectionPass(bool bParallel)
+        {
+            InitializeVertexBufferForPass();
+
+            Action<int> project = (vID) => {
+                Vector3d vCur = Mesh.GetVertex(vID);
+                bool bModified = false;
+                Vector3d vProjected = ComputeProjectedVertexPos(vID, out bModified);
+                if (vCur.EpsilonEqual(vProjected, MathUtil.ZeroTolerancef))
+                    bModified = false;
+                if (bModified) {
+                    vModifiedV[vID] = true;
+                    vBufferV[vID] = vProjected;
+
+                    foreach (int eid in mesh.VtxEdgesItr(vID)) {
+                        Index2i ev = Mesh.GetEdgeV(eid);
+                        int othervid = (ev.a == vID) ? ev.b : ev.a;
+                        Vector3d otherv = mesh.GetVertex(othervid);
+                        double old_len = vCur.Distance(otherv);
+                        double new_len = vProjected.Distance(otherv);
+                        if (new_len < MinEdgeLength || new_len > MaxEdgeLength)
+                            queue_edge_safe(eid);
+                    }
+                }
+            };
+
+
+            if (bParallel) {
+                gParallel.ForEach<int>(smooth_vertices(), project);
+            } else {
+                foreach (int vID in smooth_vertices())
+                    project(vID);
+            }
+
+            ApplyVertexBuffer(bParallel);
+            //System.Console.WriteLine("Projection Pass: queue: {0}", modified_edges.Count);
+        }
+
+
+
+
+        /// <summary>
+        /// This computes projected position w/ proper constraints/etc.
+        /// Does not modify mesh.
+        /// </summary>
+        protected virtual Vector3d ComputeProjectedVertexPos(int vID, out bool bModified)
+        {
+            bModified = false;
+
+            if (vertex_is_constrained(vID))
+                return Mesh.GetVertex(vID);
+            if (VertexControlF != null && (VertexControlF(vID) & VertexControl.NoProject) != 0)
+                return Mesh.GetVertex(vID);
+
+            Vector3d curpos = mesh.GetVertex(vID);
+            Vector3d projected = ProjectionTarget.Project(curpos, vID);
+            bModified = true;
+            return projected;
+        }
+
+
+
+
+
+
+
+        /*
+         *  Implementation of face-aligned projection. Combined with rest of remesh
+         *  this is basically an RZN-flow-type algorithm. 
+         */
+
+
+
+        protected DVector<double> vBufferVWeights = new DVector<double>();
+
+        protected virtual void InitializeBuffersForFacePass()
+        {
+            base.InitializeVertexBufferForPass();
+            if (vBufferVWeights.size < vBufferV.size)
+                vBufferVWeights.resize(vBufferV.size);
+
+            int NV = mesh.MaxVertexID;
+            for (int i = 0; i < NV; ++i) {
+                vBufferV[i] = Vector3d.Zero;
+                vBufferVWeights[i] = 0;
+            }
+        }
+
+
+
+        // [TODO] projection pass
+        //   - only project vertices modified by smooth pass?
+        //   - and/or verts in set of modified edges? 
+        protected virtual void TrackedFaceProjectionPass()
+        {
+            IOrientedProjectionTarget normalTarget = ProjectionTarget as IOrientedProjectionTarget;
+            if (normalTarget == null)
+                throw new Exception("RemesherPro.TrackedFaceProjectionPass: projection target does not have normals!");
+
+            InitializeBuffersForFacePass();
+
+            SpinLock buffer_lock = new SpinLock();
+
+            // this function computes rotated position of triangle, such that it
+            // aligns with face normal on target surface. We accumulate weighted-average 
+            // of vertex positions, which we will then use further down where possible.
+            Action<int> process_triangle = (tid) => {
+                Vector3d normal; double area; Vector3d centroid;
+                mesh.GetTriInfo(tid, out normal, out area, out centroid);
+
+                Vector3d projNormal;
+                Vector3d projPos = normalTarget.Project(centroid, out projNormal);
+
+                Index3i tv = mesh.GetTriangle(tid);
+                Vector3d v0 = mesh.GetVertex(tv.a), v1 = mesh.GetVertex(tv.b), v2 = mesh.GetVertex(tv.c);
+
+                // ugh could probably do this more efficiently...
+                Frame3f triF = new Frame3f(centroid, normal);
+                v0 = triF.ToFrameP(ref v0); v1 = triF.ToFrameP(ref v1); v2 = triF.ToFrameP(ref v2);
+                triF.AlignAxis(2, (Vector3f)projNormal);
+                triF.Origin = (Vector3f)projPos;
+                v0 = triF.FromFrameP(ref v0); v1 = triF.FromFrameP(ref v1); v2 = triF.FromFrameP(ref v2);
+
+                double dot = normal.Dot(projNormal);
+                dot = MathUtil.Clamp(dot, 0, 1.0);
+                double w = area * (dot * dot * dot);
+
+                bool taken = false;
+                buffer_lock.Enter(ref taken);
+                vBufferV[tv.a] += w * v0; vBufferVWeights[tv.a] += w;
+                vBufferV[tv.b] += w * v1; vBufferVWeights[tv.b] += w;
+                vBufferV[tv.c] += w * v2; vBufferVWeights[tv.c] += w;
+                buffer_lock.Exit();
+            };
+            
+            // compute face-aligned vertex positions
+            gParallel.ForEach(mesh.TriangleIndices(), process_triangle);
+
+
+            // ok now we filter out all the positions we can't change, as well as vertices that
+            // did not actually move. We also queue any edges that moved far enough to fall
+            // under min/max edge length thresholds
+            gParallel.ForEach(mesh.VertexIndices(), (vID) => {
+                vModifiedV[vID] = false;
+                if (vBufferVWeights[vID] < MathUtil.ZeroTolerance)
+                    return;
+                if (vertex_is_constrained(vID))
+                    return;
+                if (VertexControlF != null && (VertexControlF(vID) & VertexControl.NoProject) != 0)
+                    return;
+
+                Vector3d curpos = mesh.GetVertex(vID);
+                Vector3d projPos = vBufferV[vID] / vBufferVWeights[vID];
+                if (curpos.EpsilonEqual(projPos, MathUtil.ZeroTolerancef))
+                    return;
+
+                vModifiedV[vID] = true;
+                vBufferV[vID] = projPos;
+
+                foreach (int eid in mesh.VtxEdgesItr(vID)) {
+                    Index2i ev = Mesh.GetEdgeV(eid);
+                    int othervid = (ev.a == vID) ? ev.b : ev.a;
+                    Vector3d otherv = mesh.GetVertex(othervid);
+                    double old_len = curpos.Distance(otherv);
+                    double new_len = projPos.Distance(otherv);
+                    if (new_len < MinEdgeLength || new_len > MaxEdgeLength)
+                        queue_edge_safe(eid);
+                }
+
+            });
+
+
+            // update vertices
+            ApplyVertexBuffer(true);
+        }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+        struct SettingState
+        {
+            public bool EnableFlips;
+            public bool EnableCollapses;
+            public bool EnableSplits;
+            public bool EnableSmoothing;
+
+            public double MinEdgeLength;
+            public double MaxEdgeLength;
+
+            public double SmoothSpeedT;
+            public SmoothTypes SmoothType;
+            public TargetProjectionMode ProjectionMode;
+        }
+        List<SettingState> stateStack = new List<SettingState>();
+
+        public void PushState()
+        {
+            SettingState s = new SettingState() {
+                EnableFlips = this.EnableFlips,
+                EnableCollapses = this.EnableCollapses,
+                EnableSplits = this.EnableSplits,
+                EnableSmoothing = this.EnableSmoothing,
+                MinEdgeLength = this.MinEdgeLength,
+                MaxEdgeLength = this.MaxEdgeLength,
+                SmoothSpeedT = this.SmoothSpeedT,
+                SmoothType = this.SmoothType,
+                ProjectionMode = this.ProjectionMode
+            };
+            stateStack.Add(s);
+        }
+
+        public void PopState()
+        {
+            SettingState s = stateStack.Last();
+            stateStack.RemoveAt(stateStack.Count - 1);
+
+            this.EnableFlips = s.EnableFlips;
+            this.EnableCollapses = s.EnableCollapses;
+            this.EnableSplits = s.EnableSplits;
+            this.EnableSmoothing = s.EnableSmoothing;
+            this.MinEdgeLength = s.MinEdgeLength;
+            this.MaxEdgeLength = s.MaxEdgeLength;
+            this.SmoothSpeedT = s.SmoothSpeedT;
+            this.SmoothType = s.SmoothType;
+            this.ProjectionMode = s.ProjectionMode;
+        }
+
+        
+
+
+
+    }
+}
diff --git a/mesh/SimpleMesh.cs b/mesh/SimpleMesh.cs
index eb45f241..404a6f4b 100644
--- a/mesh/SimpleMesh.cs
+++ b/mesh/SimpleMesh.cs
@@ -16,6 +16,8 @@ public class SimpleMesh : IDeformableMesh
         public DVector<int> Triangles;
         public DVector<int> FaceGroups;
 
+        int timestamp = 0;
+
         public SimpleMesh()
         {
             Initialize();
@@ -100,6 +102,17 @@ public MeshComponents Components {
 
 
 
+        /// <summary>
+        /// Timestamp is incremented any time any change is made to the mesh
+        /// </summary>
+        public int Timestamp {
+            get { return timestamp; }
+        }
+
+        void updateTimeStamp() {
+            timestamp++;
+        }
+
 
         /*
          * Construction
@@ -117,6 +130,7 @@ public int AppendVertex(double x, double y, double z)
                 UVs.Add(0); UVs.Add(0);
             }
             Vertices.Add(x); Vertices.Add(y); Vertices.Add(z);
+            updateTimeStamp();
             return i;
         }
         public int AppendVertex(NewVertexInfo info)
@@ -140,6 +154,7 @@ public int AppendVertex(NewVertexInfo info)
             }
 
             Vertices.Add(info.v[0]); Vertices.Add(info.v[1]); Vertices.Add(info.v[2]);
+            updateTimeStamp();
             return i;
         }
 
@@ -157,6 +172,7 @@ public void AppendVertices(VectorArray3d v, VectorArray3f n = null, VectorArray3
                 UVs.Add(uv.array);
             else if (HasVertexUVs)
                 UVs.Add(new float[] { 0, 0 }, v.Count);
+            updateTimeStamp();
         }
 
 
@@ -167,6 +183,7 @@ public int AppendTriangle(int i, int j, int k, int g = -1)
             if (HasTriangleGroups)
                 FaceGroups.Add((g == -1) ? 0 : g);
             Triangles.Add(i); Triangles.Add(j); Triangles.Add(k);
+            updateTimeStamp();
             return ti;
         }
 
@@ -180,6 +197,7 @@ public void AppendTriangles(int[] vTriangles, int[] vertexMap, int g = -1)
                 for (int ti = 0; ti < vTriangles.Length / 3; ++ti)
                     FaceGroups.Add((g == -1) ? 0 : g);
             }
+            updateTimeStamp();
         }
 
         public void AppendTriangles(IndexArray3i t, int[] groups = null)
@@ -191,6 +209,7 @@ public void AppendTriangles(IndexArray3i t, int[] groups = null)
                 else
                     FaceGroups.Add(0, t.Count);
             }
+            updateTimeStamp();
         }
 
 
@@ -198,7 +217,7 @@ public void AppendTriangles(IndexArray3i t, int[] groups = null)
          * Utility / Convenience
          */
 
-            // [RMS] this is convenience stuff...
+        // [RMS] this is convenience stuff...
         public void Translate(double tx, double ty, double tz)
         {
             int c = VertexCount;
@@ -207,6 +226,7 @@ public void Translate(double tx, double ty, double tz)
                 this.Vertices[3 * i + 1] += ty;
                 this.Vertices[3 * i + 2] += tz;
             }
+            updateTimeStamp();
         }
         public void Scale(double sx, double sy, double sz)
         {
@@ -216,10 +236,12 @@ public void Scale(double sx, double sy, double sz)
                 this.Vertices[3 * i + 1] *= sy;
                 this.Vertices[3 * i + 2] *= sz;
             }
+            updateTimeStamp();
         }
         public void Scale(double s)
         {
             Scale(s, s, s);
+            updateTimeStamp();
         }
 
 
@@ -382,23 +404,27 @@ public void SetVertex(int i, Vector3d v) {
             Vertices[3 * i] = v.x;
             Vertices[3 * i + 1] = v.y;
             Vertices[3 * i + 2] = v.z;
+            updateTimeStamp();
         }
 
         public void SetVertexNormal(int i, Vector3f n) {
             Normals[3 * i] = n.x;
             Normals[3 * i + 1] = n.y;
             Normals[3 * i + 2] = n.z;
+            updateTimeStamp();
         }
 
         public void SetVertexColor(int i, Vector3f c) {
             Colors[3 * i] = c.x;
             Colors[3 * i + 1] = c.y;
             Colors[3 * i + 2] = c.z;
+            updateTimeStamp();
         }
 
         public void SetVertexUV(int i, Vector2f uv) {
             UVs[2 * i] = uv.x;
             UVs[2 * i + 1] = uv.y;
+            updateTimeStamp();
         }
 
 
diff --git a/mesh_generators/GenCylGenerators.cs b/mesh_generators/GenCylGenerators.cs
index c365cccc..49c5996a 100644
--- a/mesh_generators/GenCylGenerators.cs
+++ b/mesh_generators/GenCylGenerators.cs
@@ -11,6 +11,14 @@ namespace g3
     /// However caps are triangulated using a fan around a center vertex (which you
     /// can set using CapCenter). If Polygon is non-convex, this will have foldovers.
     /// In that case, you have to triangulate and append it yourself.
+    /// 
+    /// If your profile curve does not contain the polygon bbox center, 
+    /// set OverrideCapCenter=true and set CapCenter to a suitable center point.
+    /// 
+    /// The output normals are currently set to those for a circular profile.
+    /// Call MeshNormals.QuickCompute() on the output DMesh to estimate proper
+    /// vertex normals
+    /// 
     /// </summary>
     public class TubeGenerator : MeshGenerator
     {
@@ -20,6 +28,7 @@ public class TubeGenerator : MeshGenerator
         public bool Capped = true;
 
         // center of endcap triangle fan, relative to Polygon
+        public bool OverrideCapCenter = false;
         public Vector2d CapCenter = Vector2d.Zero;
 
         public bool ClosedLoop = false;
@@ -33,7 +42,6 @@ public class TubeGenerator : MeshGenerator
         public int startCapCenterIndex = -1;
         public int endCapCenterIndex = -1;
 
-
         public TubeGenerator()
         {
         }
@@ -47,6 +55,22 @@ public TubeGenerator(Polygon2d tubePath, Frame3f pathPlane, Polygon2d tubeShape,
             ClosedLoop = true;
             Capped = false;
         }
+        public TubeGenerator(PolyLine2d tubePath, Frame3f pathPlane, Polygon2d tubeShape, int nPlaneNormal = 2)
+        {
+            Vertices = new List<Vector3d>();
+            foreach (Vector2d v in tubePath.Vertices)
+                Vertices.Add(pathPlane.FromPlaneUV((Vector2f)v, nPlaneNormal));
+            Polygon = new Polygon2d(tubeShape);
+            ClosedLoop = false;
+            Capped = true;
+        }
+        public TubeGenerator(DCurve3 tubePath, Polygon2d tubeShape)
+        {
+            Vertices = new List<Vector3d>(tubePath.Vertices);
+            Polygon = new Polygon2d(tubeShape);
+            ClosedLoop = tubePath.Closed;
+            Capped = ! ClosedLoop;
+        }
 
 
 
@@ -55,8 +79,9 @@ override public MeshGenerator Generate()
             if (Polygon == null)
                 Polygon = Polygon2d.MakeCircle(1.0f, 8);
 
+            int NV = Vertices.Count;
             int Slices = Polygon.VertexCount;
-            int nRings = Vertices.Count;
+            int nRings = (ClosedLoop && NoSharedVertices) ? NV + 1 : NV;
             int nRingSize = (NoSharedVertices) ? Slices + 1 : Slices;
             int nCapVertices = (NoSharedVertices) ? Slices + 1 : 1;
             if (Capped == false || ClosedLoop == true)
@@ -66,76 +91,85 @@ override public MeshGenerator Generate()
             uv = new VectorArray2f(vertices.Count);
             normals = new VectorArray3f(vertices.Count);
 
-            int quad_strips = ClosedLoop ? (nRings) : (nRings-1);
+            int quad_strips = (ClosedLoop) ? NV : NV-1;
             int nSpanTris = quad_strips * (2 * Slices);
             int nCapTris = (Capped && ClosedLoop == false) ? 2 * Slices : 0;
             triangles = new IndexArray3i(nSpanTris + nCapTris);
 
             Frame3f fCur = new Frame3f(Frame);
-            Vector3d dv = CurveUtils.GetTangent(Vertices, 0); ;
+            Vector3d dv = CurveUtils.GetTangent(Vertices, 0, ClosedLoop);
             fCur.Origin = (Vector3f)Vertices[0];
             fCur.AlignAxis(2, (Vector3f)dv);
             Frame3f fStart = new Frame3f(fCur);
 
+            double circumference = Polygon.ArcLength;
+            double pathLength = CurveUtils.ArcLength(Vertices, ClosedLoop);
+            double accum_path_u = 0;
+
             // generate tube
             for (int ri = 0; ri < nRings; ++ri) {
+                int vi = ri % NV;
 
                 // propagate frame
-                if (ri != 0) {
-                    Vector3d tan = CurveUtils.GetTangent(Vertices, ri);
-                    fCur.Origin = (Vector3f)Vertices[ri];
-                    if (ri == 11)
-                        dv = tan;
-                    fCur.AlignAxis(2, (Vector3f)tan);
-                }
-
-                float uv_along = (float)ri / (float)(nRings - 1);
+                Vector3d tangent = CurveUtils.GetTangent(Vertices, vi, ClosedLoop);
+                fCur.Origin = (Vector3f)Vertices[vi];
+                fCur.AlignAxis(2, (Vector3f)tangent);
 
                 // generate vertices
                 int nStartR = ri * nRingSize;
-                for (int j = 0; j < nRingSize; ++j) {
-                    float uv_around = (float)j / (float)(nRings);
 
+                double accum_ring_v = 0;
+                for (int j = 0; j < nRingSize; ++j) {
                     int k = nStartR + j;
                     Vector2d pv = Polygon.Vertices[j % Slices];
+                    Vector2d pvNext = Polygon.Vertices[(j + 1) % Slices];
                     Vector3d v = fCur.FromPlaneUV((Vector2f)pv, 2);
                     vertices[k] = v;
-                    uv[k] = new Vector2f(uv_along, uv_around);
+
+                    uv[k] = new Vector2f(accum_path_u, accum_ring_v);
+                    accum_ring_v += (pv.Distance(pvNext) / circumference);
+
                     Vector3f n = (Vector3f)(v - fCur.Origin).Normalized;
                     normals[k] = n;
                 }
+
+                int viNext = (ri + 1) % NV;
+                double d = Vertices[vi].Distance(Vertices[viNext]);
+                accum_path_u += d / pathLength;
             }
 
 
             // generate triangles
             int ti = 0;
-            int nStop = (ClosedLoop) ? nRings : (nRings - 1);
+            int nStop = (ClosedLoop && NoSharedVertices == false) ? nRings : (nRings - 1);
             for (int ri = 0; ri < nStop; ++ri) {
                 int r0 = ri * nRingSize;
                 int r1 = r0 + nRingSize;
-                if (ClosedLoop && ri == nStop - 1)
+                if (ClosedLoop && ri == nStop - 1 && NoSharedVertices == false)
                     r1 = 0;
                 for (int k = 0; k < nRingSize - 1; ++k) {
                     triangles.Set(ti++, r0 + k, r0 + k + 1, r1 + k + 1, Clockwise);
                     triangles.Set(ti++, r0 + k, r1 + k + 1, r1 + k, Clockwise);
                 }
-                if (NoSharedVertices == false) {      // close disc if we went all the way
-                    triangles.Set(ti++, r1 - 1, r0, r1, Clockwise);
-                    triangles.Set(ti++, r1 - 1, r1, r1 + nRingSize - 1, Clockwise);
+                if (NoSharedVertices == false) {      // last quad if we aren't sharing vertices
+                    int M = nRingSize-1;
+                    triangles.Set(ti++, r0 + M, r0, r1, Clockwise);
+                    triangles.Set(ti++, r0 + M, r1, r1 + M, Clockwise);
                 }
             }
 
             if (Capped && ClosedLoop == false) {
+                Vector2d c = (OverrideCapCenter) ? CapCenter : Polygon.Bounds.Center;
 
                 // add endcap verts
                 int nBottomC = nRings * nRingSize;
-                vertices[nBottomC] = fStart.FromPlaneUV((Vector2f)CapCenter,2);
+                vertices[nBottomC] = fStart.FromPlaneUV((Vector2f)c,2);
                 uv[nBottomC] = new Vector2f(0.5f, 0.5f);
                 normals[nBottomC] = -fStart.Z;
                 startCapCenterIndex = nBottomC;
 
                 int nTopC = nBottomC + 1;
-                vertices[nTopC] = fCur.FromPlaneUV((Vector2f)CapCenter, 2);
+                vertices[nTopC] = fCur.FromPlaneUV((Vector2f)c, 2);
                 uv[nTopC] = new Vector2f(0.5f, 0.5f);
                 normals[nTopC] = fCur.Z;
                 endCapCenterIndex = nTopC;
@@ -146,7 +180,8 @@ override public MeshGenerator Generate()
                     int nStartB = nTopC + 1;
                     for (int k = 0; k < Slices; ++k) {
                         vertices[nStartB + k] = vertices[nExistingB + k];
-                        uv[nStartB + k] = (Vector2f)Polygon.Vertices[k].Normalized;
+                        Vector2d vuv = ((Polygon[k] - c).Normalized + Vector2d.One) * 0.5;
+                        uv[nStartB + k] = (Vector2f)vuv;
                         normals[nStartB + k] = normals[nBottomC];
                     }
                     append_disc(Slices, nBottomC, nStartB, true, Clockwise, ref ti);
@@ -156,7 +191,7 @@ override public MeshGenerator Generate()
                     int nStartT = nStartB + Slices;
                     for (int k = 0; k < Slices; ++k) {
                         vertices[nStartT + k] = vertices[nExistingT + k];
-                        uv[nStartT + k] = (Vector2f)Polygon.Vertices[k].Normalized;
+                        uv[nStartT + k] = uv[nStartB + k];
                         normals[nStartT + k] = normals[nTopC];
                     }
                     append_disc(Slices, nTopC, nStartT, true, !Clockwise, ref ti);
@@ -170,4 +205,4 @@ override public MeshGenerator Generate()
             return this;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/mesh_generators/MarchingCubes.cs b/mesh_generators/MarchingCubes.cs
index de6bb5f1..fdf850ff 100644
--- a/mesh_generators/MarchingCubes.cs
+++ b/mesh_generators/MarchingCubes.cs
@@ -19,28 +19,52 @@ namespace g3
     /// </summary>
     public class MarchingCubes
     {
-        // this is the function we will evaluate
+        /// <summary>
+        /// this is the function we will evaluate
+        /// </summary>
         public ImplicitFunction3d Implicit;
 
-        // mesh surface will be at this isovalue. Normally 0 unless you want
-        // offset surface or field is not a distance-field.
+        /// <summary>
+        /// mesh surface will be at this isovalue. Normally 0 unless you want
+        /// offset surface or field is not a distance-field.
+        /// </summary>
         public double IsoValue = 0;
 
-        // bounding-box we will mesh inside of. We use the min-corner and
-        // the width/height/depth, but do not clamp vertices to stay within max-corner,
-        // we may spill one cell over
+        /// <summary> bounding-box we will mesh inside of. We use the min-corner and
+        /// the width/height/depth, but do not clamp vertices to stay within max-corner,
+        /// we may spill one cell over </summary>
         public AxisAlignedBox3d Bounds;
 
-        // Length of edges of cubes that are marching.
-        // currently, # of cells along axis = (int)(bounds_dimension / CellSize) + 1
+        /// <summary>
+        /// Length of edges of cubes that are marching.
+        /// currently, # of cells along axis = (int)(bounds_dimension / CellSize) + 1
+        /// </summary>
         public double CubeSize = 0.1;
 
-        // Use multi-threading? Generally a good idea unless problem is very small or
-        // you are multi-threading at a higher level (which may be more efficient as
-        // we currently use very fine-grained spinlocks to synchronize)
+        /// <summary>
+        /// Use multi-threading? Generally a good idea unless problem is very small or
+        /// you are multi-threading at a higher level (which may be more efficient as
+        /// we currently use very fine-grained spinlocks to synchronize)
+        /// </summary>
         public bool ParallelCompute = true;
 
 
+        public enum RootfindingModes { SingleLerp, LerpSteps, Bisection }
+
+        /// <summary>
+        /// Which rootfinding method will be used to converge on surface along edges
+        /// </summary>
+        public RootfindingModes RootMode = RootfindingModes.SingleLerp;
+
+        /// <summary>
+        /// number of iterations of rootfinding method (ignored for SingleLerp)
+        /// </summary>
+        public int RootModeSteps = 5;
+
+
+        /// <summary> if this function returns true, we should abort calculation </summary>
+        public Func<bool> CancelF = () => { return false; };
+
         /*
          * Outputs
          */
@@ -187,6 +211,8 @@ void generate_parallel()
                 GridCell cell = new GridCell();
                 Vector3d[] vertlist = new Vector3d[12];
                 for (int yi = 0; yi < CellDimensions.y; ++yi) {
+                    if (CancelF())
+                        return;
                     // compute full cell at x=0, then slide along x row, which saves half of value computes
                     Vector3i idx = new Vector3i(0, yi, zi);
                     initialize_cell(cell, ref idx);
@@ -215,6 +241,8 @@ void generate_basic()
 
             for (int zi = 0; zi < CellDimensions.z; ++zi) {
                 for (int yi = 0; yi < CellDimensions.y; ++yi) {
+                    if (CancelF())
+                        return;
                     // compute full cell at x=0, then slide along x row, which saves half of value computes
                     Vector3i idx = new Vector3i(0, yi, zi);
                     initialize_cell(cell, ref idx);
@@ -354,7 +382,7 @@ int append_triangle(int a, int b, int c)
 
 
         /// <summary>
-        /// estimate intersection along edge from f(p1)=valp1 to f(p2)=valp2, using linear interpolation
+        /// root-find the intersection along edge from f(p1)=valp1 to f(p2)=valp2
         /// </summary>
         void find_iso(ref Vector3d p1, ref Vector3d p2, double valp1, double valp2, ref Vector3d pIso)
         {
@@ -381,21 +409,54 @@ void find_iso(ref Vector3d p1, ref Vector3d p2, double valp1, double valp2, ref
 
             // [RMS] if we don't maintain min/max order here, then numerical error means
             //   that hashing on point x/y/z doesn't work
-            if (valp1 < valp2) {
-                double mu = (IsoValue - valp1) / (valp2 - valp1);
-                pIso.x = p1.x + mu * (p2.x - p1.x);
-                pIso.y = p1.y + mu * (p2.y - p1.y);
-                pIso.z = p1.z + mu * (p2.z - p1.z);
+            Vector3d a = p1, b = p2;
+            double fa = valp1, fb = valp2;
+            if (valp2 < valp1) {
+                a = p2; b = p1;
+                fb = valp1; fa = valp2;
+            }
+
+            // converge on root
+            if (RootMode == RootfindingModes.Bisection) {
+                for (int k = 0; k < RootModeSteps; ++k) {
+                    pIso.x = (a.x + b.x) * 0.5; pIso.y = (a.y + b.y) * 0.5; pIso.z = (a.z + b.z) * 0.5;
+                    double mid_f = Implicit.Value(ref pIso);
+                    if (mid_f < IsoValue) {
+                        a = pIso; fa = mid_f;
+                    } else {
+                        b = pIso; fb = mid_f;
+                    }
+                }
+                pIso = Vector3d.Lerp(a, b, 0.5);
+
             } else {
-                double mu = (IsoValue - valp2) / (valp1 - valp2);
-                pIso.x = p2.x + mu * (p1.x - p2.x);
-                pIso.y = p2.y + mu * (p1.y - p2.y);
-                pIso.z = p2.z + mu * (p1.z - p2.z);
+                double mu = 0;
+                if (RootMode == RootfindingModes.LerpSteps) {
+                    for (int k = 0; k < RootModeSteps; ++k) {
+                        mu = (IsoValue - fa) / (fb - fa);
+                        pIso.x = a.x + mu * (b.x - a.x);
+                        pIso.y = a.y + mu * (b.y - a.y);
+                        pIso.z = a.z + mu * (b.z - a.z);
+                        double mid_f = Implicit.Value(ref pIso);
+                        if (mid_f < IsoValue) {
+                            a = pIso; fa = mid_f;
+                        } else {
+                            b = pIso; fb = mid_f;
+                        }
+                    }
+                }
+
+                // final lerp
+                mu = (IsoValue - fa) / (fb - fa);
+                pIso.x = a.x + mu * (b.x - a.x);
+                pIso.y = a.y + mu * (b.y - a.y);
+                pIso.z = a.z + mu * (b.z - a.z);
             }
         }
 
 
 
+
         /*
          * Below here are standard marching-cubes tables. 
          */
diff --git a/mesh_generators/MarchingCubesPro.cs b/mesh_generators/MarchingCubesPro.cs
new file mode 100644
index 00000000..60f3da0a
--- /dev/null
+++ b/mesh_generators/MarchingCubesPro.cs
@@ -0,0 +1,1058 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using g3;
+
+namespace gs
+{
+    public class MarchingCubesPro
+    {
+        /// <summary>
+        /// this is the function we will evaluate
+        /// </summary>
+        public ImplicitFunction3d Implicit;
+
+        /// <summary>
+        /// mesh surface will be at this isovalue. Normally 0 unless you want
+        /// offset surface or field is not a distance-field.
+        /// </summary>
+        public double IsoValue = 0;
+
+        /// <summary> bounding-box we will mesh inside of. We use the min-corner and
+        /// the width/height/depth, but do not clamp vertices to stay within max-corner,
+        /// we may spill one cell over </summary>
+        public AxisAlignedBox3d Bounds;
+
+        /// <summary>
+        /// Length of edges of cubes that are marching.
+        /// currently, # of cells along axis = (int)(bounds_dimension / CellSize) + 1
+        /// </summary>
+        public double CubeSize = 0.1;
+
+        /// <summary>
+        /// Use multi-threading? Generally a good idea unless problem is very small or
+        /// you are multi-threading at a higher level (which may be more efficient as
+        /// we currently use very fine-grained spinlocks to synchronize)
+        /// </summary>
+        public bool ParallelCompute = true;
+
+        public enum RootfindingModes { SingleLerp, LerpSteps, Bisection }
+
+        /// <summary>
+        /// Which rootfinding method will be used to converge on surface along edges
+        /// </summary>
+        public RootfindingModes RootMode = RootfindingModes.SingleLerp;
+
+        /// <summary>
+        /// number of iterations of rootfinding method (ignored for SingleLerp)
+        /// </summary>
+        public int RootModeSteps = 5;
+
+
+        /// <summary> if this function returns true, we should abort calculation </summary>
+        public Func<bool> CancelF = () => { return false; };
+
+        /*
+         * Outputs
+         */
+
+        // cube indices range from [Origin,CellDimensions)   
+        public Vector3i CellDimensions;
+
+        // computed mesh
+        public DMesh3 Mesh;
+
+
+
+        public MarchingCubesPro()
+        {
+            // initialize w/ a basic sphere example
+            Implicit = new ImplicitSphere3d();
+            Bounds = new AxisAlignedBox3d(Vector3d.Zero, 8);
+            CubeSize = 0.25;
+        }
+
+
+
+        /// <summary>
+        /// Run MC algorithm and generate Output mesh
+        /// </summary>
+        public void Generate()
+        {
+            Mesh = new DMesh3();
+
+            int nx = (int)(Bounds.Width / CubeSize) + 1;
+            int ny = (int)(Bounds.Height / CubeSize) + 1;
+            int nz = (int)(Bounds.Depth / CubeSize) + 1;
+            CellDimensions = new Vector3i(nx, ny, nz);
+            GridBounds = new AxisAlignedBox3i(Vector3i.Zero, CellDimensions);
+
+            corner_values_grid = new DenseGrid3f(nx+1, ny+1, nz+1, float.MaxValue);
+            edge_vertices = new Dictionary<long, int>();
+            corner_values = new Dictionary<long, double>();
+
+            if (ParallelCompute) {
+                generate_parallel();
+            } else {
+                generate_basic();
+            }
+        }
+
+
+        public void GenerateContinuation(IEnumerable<Vector3d> seeds)
+        {
+            Mesh = new DMesh3();
+
+            int nx = (int)(Bounds.Width / CubeSize) + 1;
+            int ny = (int)(Bounds.Height / CubeSize) + 1;
+            int nz = (int)(Bounds.Depth / CubeSize) + 1;
+            CellDimensions = new Vector3i(nx, ny, nz);
+            GridBounds = new AxisAlignedBox3i(Vector3i.Zero, CellDimensions);
+
+            if (LastGridBounds != GridBounds) {
+                corner_values_grid = new DenseGrid3f(nx + 1, ny + 1, nz + 1, float.MaxValue);
+                edge_vertices = new Dictionary<long, int>();
+                corner_values = new Dictionary<long, double>();
+                if (ParallelCompute)
+                    done_cells = new DenseGrid3i(CellDimensions.x, CellDimensions.y, CellDimensions.z, 0);
+            } else {
+                edge_vertices.Clear();
+                corner_values.Clear();
+                corner_values_grid.assign(float.MaxValue);
+                if (ParallelCompute)
+                    done_cells.assign(0);
+            }
+
+            if (ParallelCompute) {
+                generate_continuation_parallel(seeds);
+            } else {
+                generate_continuation(seeds);
+            }
+
+            LastGridBounds = GridBounds;
+        }
+
+
+
+        AxisAlignedBox3i GridBounds;
+        AxisAlignedBox3i LastGridBounds;
+
+
+        // we pass Cells around, this makes code cleaner
+        class GridCell
+        {
+            public Vector3i[] i;    // indices of corners of cell
+            public double[] f;      // field values at corners
+
+            public GridCell()
+            {
+                // TODO we do not actually need to store i, we just need the min-corner!
+                i = new Vector3i[8];
+                f = new double[8];
+            }
+
+        }
+
+
+
+        void corner_pos(ref Vector3i ijk, ref Vector3d p)
+        {
+            p.x = Bounds.Min.x + CubeSize * ijk.x;
+            p.y = Bounds.Min.y + CubeSize * ijk.y;
+            p.z = Bounds.Min.z + CubeSize * ijk.z;
+        }
+        Vector3d corner_pos(ref Vector3i ijk)
+        {
+            return new Vector3d(Bounds.Min.x + CubeSize * ijk.x,
+                                 Bounds.Min.y + CubeSize * ijk.y,
+                                 Bounds.Min.z + CubeSize * ijk.z);
+        }
+        Vector3i cell_index(Vector3d pos)
+        {
+            return new Vector3i(
+                (int)((pos.x - Bounds.Min.x) / CubeSize),
+                (int)((pos.y - Bounds.Min.y) / CubeSize),
+                (int)((pos.z - Bounds.Min.z) / CubeSize));
+        }
+
+
+
+        //
+        // corner and edge hash functions, these pack the coordinate
+        // integers into 16-bits, so max of 65536 in any dimension.
+        //
+
+
+        long corner_hash(ref Vector3i idx) {
+            return ((long)idx.x&0xFFFF) | (((long)idx.y&0xFFFF) << 16) | (((long)idx.z&0xFFFF) << 32);
+        }
+        long corner_hash(int x, int y, int z)
+        {
+            return ((long)x & 0xFFFF) | (((long)y & 0xFFFF) << 16) | (((long)z & 0xFFFF) << 32);
+        }
+
+        const int EDGE_X = 1 << 60;
+        const int EDGE_Y = 1 << 61;
+        const int EDGE_Z = 1 << 62;
+
+        long edge_hash(ref Vector3i idx1, ref Vector3i idx2)
+        {
+            if ( idx1.x != idx2.x ) {
+                int xlo = Math.Min(idx1.x, idx2.x);
+                return corner_hash(xlo, idx1.y, idx1.z) | EDGE_X;
+            } else if ( idx1.y != idx2.y ) {
+                int ylo = Math.Min(idx1.y, idx2.y);
+                return corner_hash(idx1.x, ylo, idx1.z) | EDGE_Y;
+            } else {
+                int zlo = Math.Min(idx1.z, idx2.z);
+                return corner_hash(idx1.x, idx1.y, zlo) | EDGE_Z;
+            }
+        }
+
+
+
+        //
+        // Hash table for edge vertices
+        //
+
+        Dictionary<long, int> edge_vertices = new Dictionary<long, int>();
+        SpinLock edge_vertices_lock = new SpinLock();
+
+        int edge_vertex_id(ref Vector3i idx1, ref Vector3i idx2, double f1, double f2)
+        {
+            long hash = edge_hash(ref idx1, ref idx2);
+
+            int vid = DMesh3.InvalidID;
+            bool taken = false;
+            edge_vertices_lock.Enter(ref taken);
+            bool found = edge_vertices.TryGetValue(hash, out vid);
+            edge_vertices_lock.Exit();
+
+            if (found) 
+                return vid;
+
+            // ok this is a bit messy. We do not want to lock the entire hash table 
+            // while we do find_iso. However it is possible that during this time we
+            // are unlocked we have re-entered with the same edge. So when we
+            // re-acquire the lock we need to check again that we have not already
+            // computed this edge, otherwise we will end up with duplicate vertices!
+
+            Vector3d pa = Vector3d.Zero, pb = Vector3d.Zero;
+            corner_pos(ref idx1, ref pa);
+            corner_pos(ref idx2, ref pb);
+            Vector3d pos = Vector3d.Zero;
+            find_iso(ref pa, ref pb, f1, f2, ref pos);
+
+            taken = false;
+            edge_vertices_lock.Enter(ref taken);
+            if (edge_vertices.TryGetValue(hash, out vid) == false) {
+                vid = append_vertex(pos);
+                edge_vertices[hash] = vid;
+            }
+            edge_vertices_lock.Exit();
+
+            return vid;
+        }
+
+
+
+
+
+
+        //
+        // Store corner values in hash table. This doesn't make
+        // sense if we are evaluating entire grid, way too slow.
+        //
+
+        Dictionary<long, double> corner_values = new Dictionary<long, double>();
+        SpinLock corner_values_lock = new SpinLock();
+
+        double corner_value(ref Vector3i idx)
+        {
+            long hash = corner_hash(ref idx);
+            double value = 0;
+
+            if ( corner_values.TryGetValue(hash, out value) == false) {
+                Vector3d v = corner_pos(ref idx);
+                value = Implicit.Value(ref v);
+                corner_values[hash] = value;
+            } 
+            return value;
+        }
+        void initialize_cell_values(GridCell cell, bool shift)
+        {
+            bool taken = false;
+            corner_values_lock.Enter(ref taken);
+
+            if ( shift ) {
+                cell.f[1] = corner_value(ref cell.i[1]);
+                cell.f[2] = corner_value(ref cell.i[2]);
+                cell.f[5] = corner_value(ref cell.i[5]);
+                cell.f[6] = corner_value(ref cell.i[6]);
+            } else {
+                for (int i = 0; i < 8; ++i)
+                    cell.f[i] = corner_value(ref cell.i[i]);
+            }
+
+            corner_values_lock.Exit();
+        }
+
+
+
+        //
+        // store corner values in pre-allocated grid that has
+        // float.MaxValue as sentinel. 
+        // (note this is float grid, not double...)
+        //
+
+        DenseGrid3f corner_values_grid;
+
+        double corner_value_grid(ref Vector3i idx)
+        {
+            double val = corner_values_grid[idx];
+            if (val != float.MaxValue)
+                return val;
+
+            Vector3d v = corner_pos(ref idx);
+            val = Implicit.Value(ref v);
+            corner_values_grid[idx] = (float)val;
+            return val;
+        }
+        void initialize_cell_values_grid(GridCell cell, bool shift)
+        {
+            if (shift) {
+                cell.f[1] = corner_value_grid(ref cell.i[1]);
+                cell.f[2] = corner_value_grid(ref cell.i[2]);
+                cell.f[5] = corner_value_grid(ref cell.i[5]);
+                cell.f[6] = corner_value_grid(ref cell.i[6]);
+            } else {
+                for (int i = 0; i < 8; ++i)
+                    cell.f[i] = corner_value_grid(ref cell.i[i]);
+            }
+        }
+
+
+
+        //
+        // explicitly compute corner values as necessary
+        //
+        //
+
+        double corner_value_nohash(ref Vector3i idx) {
+            Vector3d v = corner_pos(ref idx);
+            return Implicit.Value(ref v);
+        }
+        void initialize_cell_values_nohash(GridCell cell, bool shift)
+        {
+            if (shift) {
+                cell.f[1] = corner_value_nohash(ref cell.i[1]);
+                cell.f[2] = corner_value_nohash(ref cell.i[2]);
+                cell.f[5] = corner_value_nohash(ref cell.i[5]);
+                cell.f[6] = corner_value_nohash(ref cell.i[6]);
+            } else {
+                for (int i = 0; i < 8; ++i)
+                    cell.f[i] = corner_value_nohash(ref cell.i[i]);
+            }
+        }
+
+
+
+
+
+        /// <summary>
+        /// compute 3D corner-positions and field values for cell at index
+        /// </summary>
+        void initialize_cell(GridCell cell, ref Vector3i idx)
+        {
+            cell.i[0] = new Vector3i(idx.x + 0, idx.y + 0, idx.z + 0);
+            cell.i[1] = new Vector3i(idx.x + 1, idx.y + 0, idx.z + 0);
+            cell.i[2] = new Vector3i(idx.x + 1, idx.y + 0, idx.z + 1);
+            cell.i[3] = new Vector3i(idx.x + 0, idx.y + 0, idx.z + 1);
+            cell.i[4] = new Vector3i(idx.x + 0, idx.y + 1, idx.z + 0);
+            cell.i[5] = new Vector3i(idx.x + 1, idx.y + 1, idx.z + 0);
+            cell.i[6] = new Vector3i(idx.x + 1, idx.y + 1, idx.z + 1);
+            cell.i[7] = new Vector3i(idx.x + 0, idx.y + 1, idx.z + 1);
+
+            //initialize_cell_values(cell, false);
+            initialize_cell_values_grid(cell, false);
+            //initialize_cell_values_nohash(cell, false);
+        }
+
+
+        // assume we just want to slide cell at xi-1 to cell at xi, while keeping
+        // yi and zi constant. Then only x-coords change, and we have already 
+        // computed half the values
+        void shift_cell_x(GridCell cell, int xi)
+        {
+            cell.f[0] = cell.f[1];
+            cell.f[3] = cell.f[2];
+            cell.f[4] = cell.f[5];
+            cell.f[7] = cell.f[6];
+
+            cell.i[0].x = xi; cell.i[1].x = xi+1; cell.i[2].x = xi+1; cell.i[3].x = xi;
+            cell.i[4].x = xi; cell.i[5].x = xi+1; cell.i[6].x = xi+1; cell.i[7].x = xi;
+
+            //initialize_cell_values(cell, true);
+            initialize_cell_values_grid(cell, true);
+            //initialize_cell_values_nohash(cell, true);
+        }
+
+
+        bool parallel_mesh_access = false;
+        SpinLock mesh_lock;
+
+        /// <summary>
+        /// processing z-slabs of cells in parallel
+        /// </summary>
+        void generate_parallel()
+        {
+            mesh_lock = new SpinLock();
+            parallel_mesh_access = true;
+
+            // [TODO] maybe shouldn't alway use Z axis here?
+            gParallel.ForEach(Interval1i.Range(CellDimensions.z), (zi) => {
+                GridCell cell = new GridCell();
+                int[] vertlist = new int[12];
+                for (int yi = 0; yi < CellDimensions.y; ++yi) {
+                    if (CancelF())
+                        return;
+                    // compute full cell at x=0, then slide along x row, which saves half of value computes
+                    Vector3i idx = new Vector3i(0, yi, zi);
+                    initialize_cell(cell, ref idx);
+                    polygonize_cell(cell, vertlist);
+                    for (int xi = 1; xi < CellDimensions.x; ++xi) {
+                        shift_cell_x(cell, xi);
+                        polygonize_cell(cell, vertlist);
+                    }
+                }
+            });
+
+
+            parallel_mesh_access = false;
+        }
+
+
+
+
+        /// <summary>
+        /// fully sequential version, no threading
+        /// </summary>
+        void generate_basic()
+        {
+            GridCell cell = new GridCell();
+            int[] vertlist = new int[12];
+
+            for (int zi = 0; zi < CellDimensions.z; ++zi) {
+                for (int yi = 0; yi < CellDimensions.y; ++yi) {
+                    if (CancelF())
+                        return;
+                    // compute full cell at x=0, then slide along x row, which saves half of value computes
+                    Vector3i idx = new Vector3i(0, yi, zi);
+                    initialize_cell(cell, ref idx);
+                    polygonize_cell(cell, vertlist);
+                    for (int xi = 1; xi < CellDimensions.x; ++xi) {
+                        shift_cell_x(cell, xi);
+                        polygonize_cell(cell, vertlist);
+                    }
+
+                }
+            }
+        }
+
+
+
+
+        /// <summary>
+        /// fully sequential version, no threading
+        /// </summary>
+        void generate_continuation(IEnumerable<Vector3d> seeds)
+        {
+            GridCell cell = new GridCell();
+            int[] vertlist = new int[12];
+
+            done_cells = new DenseGrid3i(CellDimensions.x, CellDimensions.y, CellDimensions.z, 0);
+
+            List<Vector3i> stack = new List<Vector3i>();
+
+            foreach (Vector3d seed in seeds) {
+                Vector3i seed_idx = cell_index(seed);
+                if (done_cells[seed_idx] == 1)
+                    continue;
+                stack.Add(seed_idx);
+                done_cells[seed_idx] = 1;
+
+                while ( stack.Count > 0 ) {
+                    Vector3i idx = stack[stack.Count-1]; 
+                    stack.RemoveAt(stack.Count-1);
+                    if (CancelF())
+                        return;
+
+                    initialize_cell(cell, ref idx);
+                    if ( polygonize_cell(cell, vertlist) ) {     // found crossing
+                        foreach ( Vector3i o in gIndices.GridOffsets6 ) {
+                            Vector3i nbr_idx = idx + o;
+                            if (GridBounds.Contains(nbr_idx) && done_cells[nbr_idx] == 0) {
+                                stack.Add(nbr_idx);
+                                done_cells[nbr_idx] = 1;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+
+
+
+        /// <summary>
+        /// parallel seed evaluation
+        /// </summary>
+        void generate_continuation_parallel(IEnumerable<Vector3d> seeds)
+        {
+            mesh_lock = new SpinLock();
+            parallel_mesh_access = true;
+
+            gParallel.ForEach(seeds, (seed) => {
+                Vector3i seed_idx = cell_index(seed);
+                if (set_cell_if_not_done(ref seed_idx) == false)
+                    return;
+
+                GridCell cell = new GridCell();
+                int[] vertlist = new int[12];
+
+                List<Vector3i> stack = new List<Vector3i>();
+                stack.Add(seed_idx);
+
+                while (stack.Count > 0) {
+                    Vector3i idx = stack[stack.Count - 1];
+                    stack.RemoveAt(stack.Count - 1);
+                    if (CancelF())
+                        return;
+
+                    initialize_cell(cell, ref idx);
+                    if (polygonize_cell(cell, vertlist)) {     // found crossing
+                        foreach (Vector3i o in gIndices.GridOffsets6) {
+                            Vector3i nbr_idx = idx + o;
+                            if (GridBounds.Contains(nbr_idx)) {
+                                if (set_cell_if_not_done(ref nbr_idx) == true) { 
+                                    stack.Add(nbr_idx);
+                                }
+                            }
+                        }
+                    }
+                }
+            });
+
+            parallel_mesh_access = false;
+        }
+
+
+
+        DenseGrid3i done_cells;
+        SpinLock done_cells_lock = new SpinLock();
+
+        bool set_cell_if_not_done(ref Vector3i idx)
+        {
+            bool was_set = false;
+            bool taken = false;
+            done_cells_lock.Enter(ref taken);
+            if (done_cells[idx] == 0) {
+                done_cells[idx] = 1;
+                was_set = true;
+            }
+            done_cells_lock.Exit();
+            return was_set;
+        }
+
+
+
+
+
+
+
+
+
+
+        /// <summary>
+        /// find edge crossings and generate triangles for this cell
+        /// </summary>
+        bool polygonize_cell(GridCell cell, int[] vertIndexList)
+        {
+            // construct bits of index into edge table, where bit for each
+            // corner is 1 if that value is < isovalue.
+            // This tell us which edges have sign-crossings, and the int value
+            // of the bitmap is an index into the edge and triangle tables
+            int cubeindex = 0, shift = 1;
+            for (int i = 0; i < 8; ++i) {
+                if (cell.f[i] < IsoValue)
+                    cubeindex |= shift;
+                shift <<= 1;
+            }
+
+            // no crossings!
+            if (edgeTable[cubeindex] == 0)
+                return false;
+
+            // check each bit of value in edge table. If it is 1, we
+            // have a crossing on that edge. Look up the indices of this
+            // edge and find the intersection point along it
+            shift = 1;
+            Vector3d pa = Vector3d.Zero, pb = Vector3d.Zero;
+            for (int i = 0; i <= 11; i++) {
+                if ((edgeTable[cubeindex] & shift) != 0) {
+                    int a = edge_indices[i, 0], b = edge_indices[i, 1];
+                    vertIndexList[i] = edge_vertex_id(ref cell.i[a], ref cell.i[b], cell.f[a], cell.f[b]);
+                }
+                shift <<= 1;
+            }
+
+            // now iterate through the set of triangles in triTable for this cube,
+            // and emit triangles using the vertices we found.
+            int tri_count = 0;
+            for (int i = 0; triTable[cubeindex, i] != -1; i += 3) {
+                int ta = triTable[cubeindex, i];
+                int tb = triTable[cubeindex, i + 1];
+                int tc = triTable[cubeindex, i + 2];
+                int a = vertIndexList[ta], b = vertIndexList[tb], c = vertIndexList[tc];
+
+                // if a corner is within tolerance of isovalue, then some triangles
+                // will be degenerate, and we can skip them w/o resulting in cracks (right?)
+                // !! this should never happen anymore...artifact of old hashtable impl
+                if (a == b || a == c || b == c)
+                    continue;
+
+                /*int tid = */
+                append_triangle(a, b, c);
+                tri_count++;
+            }
+
+            return (tri_count > 0);
+        }
+
+
+
+
+        /// <summary>
+        /// add vertex to mesh, with locking if we are computing in parallel
+        /// </summary>
+        int append_vertex(Vector3d v)
+        {
+            bool lock_taken = false;
+            if (parallel_mesh_access) {
+                mesh_lock.Enter(ref lock_taken);
+            }
+
+            int vid = Mesh.AppendVertex(v);
+
+            if (lock_taken)
+                mesh_lock.Exit();
+
+            return vid;
+        }
+
+
+
+        /// <summary>
+        /// add triangle to mesh, with locking if we are computing in parallel
+        /// </summary>
+        int append_triangle(int a, int b, int c)
+        {
+            bool lock_taken = false;
+            if (parallel_mesh_access) {
+                mesh_lock.Enter(ref lock_taken);
+            }
+
+            int tid = Mesh.AppendTriangle(a, b, c);
+
+            if (lock_taken)
+                mesh_lock.Exit();
+
+            return tid;
+        }
+
+
+
+        /// <summary>
+        /// root-find the intersection along edge from f(p1)=valp1 to f(p2)=valp2
+        /// </summary>
+        void find_iso(ref Vector3d p1, ref Vector3d p2, double valp1, double valp2, ref Vector3d pIso)
+        {
+            // Ok, this is a bit hacky but seems to work? If both isovalues
+            // are the same, we just return the midpoint. If one is nearly zero, we can
+            // but assume that's where the surface is. *However* if we return that point exactly,
+            // we can get nonmanifold vertices, because multiple fans may connect there. 
+            // Since DMesh3 disallows that, it results in holes. So we pull 
+            // slightly towards the other point along this edge. This means we will get
+            // repeated nearly-coincident vertices, but the mesh will be manifold.
+            const double dt = 0.999999;
+            if (Math.Abs(valp1 - valp2) < 0.00001) {
+                pIso = (p1 + p2) * 0.5;
+                return;
+            }
+            if (Math.Abs(IsoValue - valp1) < 0.00001) {
+                pIso = dt * p1 + (1.0 - dt) * p2;
+                return;
+            }
+            if (Math.Abs(IsoValue - valp2) < 0.00001) {
+                pIso = (dt) * p2 + (1.0 - dt) * p1;
+                return;
+            }
+
+            // [RMS] if we don't maintain min/max order here, then numerical error means
+            //   that hashing on point x/y/z doesn't work
+            Vector3d a = p1, b = p2;
+            double fa = valp1, fb = valp2;
+            if (valp2 < valp1) {
+                a = p2; b = p1;
+                fb = valp1; fa = valp2;
+            }
+
+            // converge on root
+            if (RootMode == RootfindingModes.Bisection) {
+                for (int k = 0; k < RootModeSteps; ++k) {
+                    pIso.x = (a.x + b.x) * 0.5; pIso.y = (a.y + b.y) * 0.5; pIso.z = (a.z + b.z) * 0.5;
+                    double mid_f = Implicit.Value(ref pIso);
+                    if (mid_f < IsoValue) {
+                        a = pIso; fa = mid_f;
+                    } else {
+                        b = pIso; fb = mid_f;
+                    }
+                }
+                pIso = Vector3d.Lerp(a, b, 0.5);
+
+            } else {
+                double mu = 0;
+                if (RootMode == RootfindingModes.LerpSteps) {
+                    for (int k = 0; k < RootModeSteps; ++k) {
+                        mu = (IsoValue - fa) / (fb - fa);
+                        pIso.x = a.x + mu * (b.x - a.x);
+                        pIso.y = a.y + mu * (b.y - a.y);
+                        pIso.z = a.z + mu * (b.z - a.z);
+                        double mid_f = Implicit.Value(ref pIso);
+                        if (mid_f < IsoValue) {
+                            a = pIso; fa = mid_f;
+                        } else {
+                            b = pIso; fb = mid_f;
+                        }
+                    }
+                }
+
+                // final lerp
+                mu = (IsoValue - fa) / (fb - fa);
+                pIso.x = a.x + mu * (b.x - a.x);
+                pIso.y = a.y + mu * (b.y - a.y);
+                pIso.z = a.z + mu * (b.z - a.z);
+            }
+        }
+
+
+
+
+        /*
+         * Below here are standard marching-cubes tables. 
+         */
+
+
+        static readonly int[,] edge_indices = new int[,] {
+            {0,1}, {1,2}, {2,3}, {3,0}, {4,5}, {5,6}, {6,7}, {7,4}, {0,4}, {1,5}, {2,6}, {3,7}
+        };
+
+        static readonly int[] edgeTable = new int[256] {
+            0x0  , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
+            0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
+            0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
+            0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
+            0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c,
+            0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
+            0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac,
+            0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
+            0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c,
+            0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
+            0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc,
+            0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
+            0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c,
+            0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
+            0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc ,
+            0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
+            0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
+            0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
+            0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
+            0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
+            0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
+            0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
+            0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
+            0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460,
+            0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
+            0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0,
+            0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
+            0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230,
+            0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
+            0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
+            0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
+            0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0   };
+
+
+        static readonly int[,] triTable = new int[256, 16]
+            {{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1},
+            {3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1},
+            {3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1},
+            {3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1},
+            {9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1},
+            {9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
+            {2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1},
+            {8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1},
+            {9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
+            {4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1},
+            {3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1},
+            {1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1},
+            {4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1},
+            {4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1},
+            {9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
+            {5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1},
+            {2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1},
+            {9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
+            {0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
+            {2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1},
+            {10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1},
+            {4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1},
+            {5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1},
+            {5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1},
+            {9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1},
+            {0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1},
+            {1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1},
+            {10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1},
+            {8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1},
+            {2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1},
+            {7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1},
+            {9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1},
+            {2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1},
+            {11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1},
+            {9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1},
+            {5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1},
+            {11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1},
+            {11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
+            {1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1},
+            {9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1},
+            {5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1},
+            {2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
+            {0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
+            {5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1},
+            {6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1},
+            {3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1},
+            {6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1},
+            {5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1},
+            {1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
+            {10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1},
+            {6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1},
+            {8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1},
+            {7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1},
+            {3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
+            {5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1},
+            {0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1},
+            {9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1},
+            {8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1},
+            {5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1},
+            {0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1},
+            {6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1},
+            {10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1},
+            {10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1},
+            {8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1},
+            {1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1},
+            {3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1},
+            {0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1},
+            {10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1},
+            {3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1},
+            {6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1},
+            {9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1},
+            {8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1},
+            {3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1},
+            {6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1},
+            {0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1},
+            {10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1},
+            {10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1},
+            {2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1},
+            {7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1},
+            {7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1},
+            {2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1},
+            {1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1},
+            {11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1},
+            {8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1},
+            {0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1},
+            {7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
+            {10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
+            {2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
+            {6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1},
+            {7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1},
+            {2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1},
+            {1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1},
+            {10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1},
+            {10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1},
+            {0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1},
+            {7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1},
+            {6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1},
+            {8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1},
+            {9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1},
+            {6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1},
+            {4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1},
+            {10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1},
+            {8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1},
+            {0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1},
+            {1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1},
+            {8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1},
+            {10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1},
+            {4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1},
+            {10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
+            {5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
+            {11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1},
+            {9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
+            {6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1},
+            {7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1},
+            {3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1},
+            {7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1},
+            {9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1},
+            {3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1},
+            {6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1},
+            {9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1},
+            {1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1},
+            {4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1},
+            {7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1},
+            {6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1},
+            {3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1},
+            {0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1},
+            {6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1},
+            {0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1},
+            {11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1},
+            {6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1},
+            {5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1},
+            {9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1},
+            {1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1},
+            {1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1},
+            {10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1},
+            {0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1},
+            {5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1},
+            {10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1},
+            {11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1},
+            {9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1},
+            {7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1},
+            {2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1},
+            {8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1},
+            {9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1},
+            {9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1},
+            {1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1},
+            {9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1},
+            {9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1},
+            {5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1},
+            {0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1},
+            {10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1},
+            {2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1},
+            {0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1},
+            {0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1},
+            {9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1},
+            {5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1},
+            {3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1},
+            {5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1},
+            {8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1},
+            {9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1},
+            {0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1},
+            {1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1},
+            {3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1},
+            {4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1},
+            {9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1},
+            {11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1},
+            {11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1},
+            {2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1},
+            {9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1},
+            {3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1},
+            {1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1},
+            {4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1},
+            {4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1},
+            {0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1},
+            {3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1},
+            {3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1},
+            {0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1},
+            {9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1},
+            {1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+            {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}};
+
+    }
+}
diff --git a/mesh_generators/MeshGenerators.cs b/mesh_generators/MeshGenerators.cs
index abac629a..eae88b8e 100644
--- a/mesh_generators/MeshGenerators.cs
+++ b/mesh_generators/MeshGenerators.cs
@@ -41,17 +41,19 @@ public virtual SimpleMesh MakeSimpleMesh()
         public virtual void MakeMesh(DMesh3 m)
         {
             int nV = vertices.Count;
-            if (WantNormals)
+            bool bWantNormals = WantNormals && normals != null && normals.Count == vertices.Count;
+            if (bWantNormals)
                 m.EnableVertexNormals(Vector3f.AxisY);
-            if (WantUVs)
+            bool bWantUVs = WantUVs && uv != null && uv.Count == vertices.Count;
+            if (bWantUVs)
                 m.EnableVertexUVs(Vector2f.Zero);
             for (int i = 0; i < nV; ++i) {
 				NewVertexInfo ni = new NewVertexInfo() { v = vertices[i] };
-				if ( WantNormals ) {
+				if (bWantNormals) {
 					ni.bHaveN = true; 
 					ni.n = normals[i];
 				}
-				if ( WantUVs ) {
+				if (bWantUVs) {
 					ni.bHaveUV = true;
 					ni.uv = uv[i];
 				}
@@ -293,6 +295,8 @@ public void MakeMesh(Mesh m, bool bRecalcNormals = false, bool bFlipLR = false)
                 m.uv = ToUnityVector2(uv);
             if (normals != null && WantNormals)
                 m.normals = ToUnityVector3(normals, bFlipLR);
+            if ( m.vertexCount > 64000 ||  triangles.Count > 64000 )
+                m.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
             m.triangles = triangles.array;
             if (bRecalcNormals)
                 m.RecalculateNormals();
diff --git a/mesh_generators/PointsMeshGenerators.cs b/mesh_generators/PointsMeshGenerators.cs
new file mode 100644
index 00000000..5ef34ea3
--- /dev/null
+++ b/mesh_generators/PointsMeshGenerators.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace g3
+{
+    /// <summary>
+    /// Create a mesh that contains a planar element for each point and normal
+    /// (currently only triangles)
+    /// </summary>
+    public class PointSplatsGenerator : MeshGenerator
+    {
+        public IEnumerable<int> PointIndices;
+        public int PointIndicesCount = -1;      // you can set this to avoid calling Count() on enumerable
+
+        public Func<int, Vector3d> PointF;      // required
+        public Func<int, Vector3d> NormalF;     // required
+        public double Radius = 1.0f;
+
+        public PointSplatsGenerator()
+        {
+            WantUVs = false;
+        }
+
+        public override MeshGenerator Generate()
+        {
+            int N = (PointIndicesCount == -1) ? PointIndices.Count() : PointIndicesCount;
+
+            vertices = new VectorArray3d(N * 3);
+            uv = null;
+            normals = new VectorArray3f(vertices.Count);
+            triangles = new IndexArray3i(N);
+
+            Matrix2f matRot = new Matrix2f(120 * MathUtil.Deg2Radf);
+            Vector2f uva = new Vector2f(0, Radius);
+            Vector2f uvb = matRot * uva;
+            Vector2f uvc = matRot * uvb;
+
+            int vi = 0;
+            int ti = 0;
+            foreach (int pid in PointIndices) {
+                Vector3d v = PointF(pid);
+                Vector3d n = NormalF(pid);
+                Frame3f f = new Frame3f(v, n);
+                triangles.Set(ti++, vi, vi + 1, vi + 2, Clockwise);
+                vertices[vi++] = f.FromPlaneUV(uva, 2);
+                vertices[vi++] = f.FromPlaneUV(uvb, 2);
+                vertices[vi++] = f.FromPlaneUV(uvc, 2);
+            }
+
+            return this;
+        }
+
+
+
+        /// <summary>
+        /// shortcut utility
+        /// </summary>
+        public static DMesh3 Generate(IList<int> indices,
+            Func<int, Vector3d> PointF, Func<int, Vector3d> NormalF,
+            double radius)
+        {
+            var gen = new PointSplatsGenerator() {
+                PointIndices = indices,
+                PointIndicesCount = indices.Count,
+                PointF = PointF, NormalF = NormalF, Radius = radius
+            };
+            return gen.Generate().MakeDMesh();
+        }
+
+    }
+}
diff --git a/mesh_generators/TriangulatedPolygonGenerator.cs b/mesh_generators/TriangulatedPolygonGenerator.cs
new file mode 100644
index 00000000..d09102a8
--- /dev/null
+++ b/mesh_generators/TriangulatedPolygonGenerator.cs
@@ -0,0 +1,100 @@
+using System;
+
+namespace g3
+{
+    /// <summary>
+    /// Triangulate a 2D polygon-with-holes by inserting it's edges into a meshed rectangle
+    /// and then removing the triangles outside the polygon.
+    /// </summary>
+    public class TriangulatedPolygonGenerator : MeshGenerator
+    {
+        public GeneralPolygon2d Polygon;
+        public Vector3f FixedNormal = Vector3f.AxisZ;
+
+        public TrivialRectGenerator.UVModes UVMode = TrivialRectGenerator.UVModes.FullUVSquare;
+
+		public int Subdivisions = 1;
+
+        override public MeshGenerator Generate()
+        {
+            MeshInsertPolygon insert;
+            DMesh3 base_mesh = ComputeResult(out insert);
+
+            DMesh3 compact = new DMesh3(base_mesh, true);
+
+            int NV = compact.VertexCount;
+            vertices = new VectorArray3d(NV);
+            uv = new VectorArray2f(NV);
+            normals = new VectorArray3f(NV);
+            for (int vi = 0; vi < NV; ++vi) {
+                vertices[vi] = compact.GetVertex(vi);
+                uv[vi] = compact.GetVertexUV(vi);
+                normals[vi] = FixedNormal;
+            }
+
+            int NT = compact.TriangleCount;
+            triangles = new IndexArray3i(NT);
+            for (int ti = 0; ti < NT; ++ti)
+                triangles[ti] = compact.GetTriangle(ti);
+
+            return this;
+        }
+
+
+
+
+        /// <summary>
+        /// Actually computes the insertion. In some cases we would like more info
+        /// coming back than we get by using Generate() api. Note that resulting
+        /// mesh is *not* compacted.
+        /// </summary>
+        public DMesh3 ComputeResult(out MeshInsertPolygon insertion)
+        {
+            AxisAlignedBox2d bounds = Polygon.Bounds;
+            double padding = 0.1 * bounds.DiagonalLength;
+            bounds.Expand(padding);
+
+			TrivialRectGenerator rectgen = (Subdivisions == 1) ?
+				new TrivialRectGenerator() : new GriddedRectGenerator() { EdgeVertices = Subdivisions };
+
+			rectgen.Width = (float)bounds.Width;
+			rectgen.Height = (float)bounds.Height;
+			rectgen.IndicesMap = new Index2i(1, 2);
+			rectgen.UVMode = UVMode;
+			rectgen.Clockwise = true;   // MeshPolygonInserter assumes mesh faces are CW? (except code says CCW...)
+			rectgen.Generate();
+			DMesh3 base_mesh = new DMesh3();
+			rectgen.MakeMesh(base_mesh);
+
+            GeneralPolygon2d shiftPolygon = new GeneralPolygon2d(Polygon);
+            Vector2d shift = bounds.Center;
+            shiftPolygon.Translate(-shift);
+
+            MeshInsertPolygon insert = new MeshInsertPolygon() {
+                Mesh = base_mesh, Polygon = shiftPolygon
+            };
+            bool bOK = insert.Insert();
+            if (!bOK)
+                throw new Exception("TriangulatedPolygonGenerator: failed to Insert()");
+
+            MeshFaceSelection selected = insert.InteriorTriangles;
+            MeshEditor editor = new MeshEditor(base_mesh);
+            editor.RemoveTriangles((tid) => { return selected.IsSelected(tid) == false; }, true);
+
+            Vector3d shift3 = new Vector3d(shift.x, shift.y, 0);
+            MeshTransforms.Translate(base_mesh, shift3);
+
+            insertion = insert;
+            return base_mesh;
+        }
+
+
+
+
+    }
+
+
+
+
+
+}
diff --git a/mesh_ops/AutoHoleFill.cs b/mesh_ops/AutoHoleFill.cs
new file mode 100644
index 00000000..7dfb8e5a
--- /dev/null
+++ b/mesh_ops/AutoHoleFill.cs
@@ -0,0 +1,364 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// Work in progress. Idea is that this class will analyze the hole and choose correct filling
+    /// strategy. Mainly just calling other fillers. 
+    /// 
+    /// Also contains prototype of filler that decomposes hole into spans based on normals and
+    /// then uses PlanarSpansFiller. See comments, is not really functional.
+    /// 
+    /// </summary>
+    public class AutoHoleFill
+    {
+        public DMesh3 Mesh;
+
+        public double TargetEdgeLength = 2.5;
+
+        public EdgeLoop FillLoop;
+
+        /*
+         *  Outputs
+         */
+
+        /// <summary> Final fill triangles. May include triangles outside initial fill loop, if ConstrainToHoleInterior=false </summary>
+        public int[] FillTriangles;
+
+
+        public AutoHoleFill(DMesh3 mesh, EdgeLoop fillLoop)
+        {
+            this.Mesh = mesh;
+            this.FillLoop = fillLoop;
+        }
+
+
+        enum UseFillType
+        {
+            PlanarFill,
+            MinimalFill,
+            PlanarSpansFill,
+            SmoothFill
+        }
+
+
+
+        public bool Apply()
+        {
+            UseFillType type = classify_hole();
+
+            bool bResult = false;
+
+            bool DISABLE_PLANAR_FILL = false;
+
+            if (type == UseFillType.PlanarFill && DISABLE_PLANAR_FILL == false)
+                bResult = fill_planar();
+            else if (type == UseFillType.MinimalFill)
+                bResult = fill_minimal();
+            else if (type == UseFillType.PlanarSpansFill)
+                bResult = fill_planar_spans();
+            else
+                bResult = fill_smooth();
+
+            if (bResult == false && type != UseFillType.SmoothFill)
+                bResult = fill_smooth();
+
+            return bResult;
+        }
+
+
+
+
+        UseFillType classify_hole()
+        {
+            return UseFillType.MinimalFill;
+#if false
+
+            int NV = FillLoop.VertexCount;
+            int NE = FillLoop.EdgeCount;
+
+            Vector3d size = FillLoop.ToCurve().GetBoundingBox().Diagonal;
+
+            NormalHistogram hist = new NormalHistogram(4096, true);
+
+            for (int k = 0; k < NE; ++k) {
+                int eid = FillLoop.Edges[k];
+                Index2i et = Mesh.GetEdgeT(eid);
+                Vector3d n = Mesh.GetTriNormal(et.a);
+                hist.Count(n, 1.0, true);
+            }
+
+            if (hist.UsedBins.Count == 1)
+                return UseFillType.PlanarFill;
+
+            //int nontrivial_bins = 0;
+            //foreach ( int bin in hist.UsedBins ) {
+            //    if (hist.Counts[bin] > 8)
+            //        nontrivial_bins++;
+            //}
+            //if (nontrivial_bins > 0)
+            //    return UseFillType.PlanarSpansFill;
+
+            return UseFillType.SmoothFill;
+#endif
+        }
+
+
+
+
+        bool fill_smooth()
+        {
+            SmoothedHoleFill fill = new SmoothedHoleFill(Mesh, FillLoop);
+            fill.TargetEdgeLength = TargetEdgeLength;
+            fill.SmoothAlpha = 1.0f;
+            fill.ConstrainToHoleInterior = true;
+            //fill.SmoothSolveIterations = 3;   // do this if we have a complicated hole - should be able to tell by normal histogram...
+            return fill.Apply();
+        }
+
+
+
+        bool fill_planar()
+        {
+            Vector3d n = Vector3d.Zero, c = Vector3d.Zero;
+            int NE = FillLoop.EdgeCount;
+            for (int k = 0; k < NE; ++k) {
+                int eid = FillLoop.Edges[k];
+                n += Mesh.GetTriNormal(Mesh.GetEdgeT(eid).a);
+                c += Mesh.GetEdgePoint(eid, 0.5);
+            }
+            n.Normalize(); c /= (double)NE;
+
+            PlanarHoleFiller filler = new PlanarHoleFiller(Mesh);
+            filler.FillTargetEdgeLen = TargetEdgeLength;
+            filler.AddFillLoop(FillLoop);
+            filler.SetPlane(c, n);
+
+            bool bOK = filler.Fill();
+            return bOK;
+        }
+
+
+
+        bool fill_minimal()
+        {
+            MinimalHoleFill minfill = new MinimalHoleFill(Mesh, FillLoop);
+            bool bOK = minfill.Apply();
+            return bOK;
+        }
+
+
+
+
+
+        /// <summary>
+        /// Here are reasons this isn't working:
+        ///    1) find_coplanar_span_sets does not actually limit to coplanar (see comments)
+        ///    2) 
+        /// 
+        /// </summary>
+        bool fill_planar_spans()
+        {
+            Dictionary<Vector3d, List<EdgeSpan>> span_sets = find_coplanar_span_sets(Mesh, FillLoop);
+
+            foreach ( var set in span_sets ) {
+                Vector3d normal = set.Key;
+                List<EdgeSpan> spans = set.Value;
+                Vector3d pos = spans[0].GetVertex(0);
+
+                if (spans.Count > 1) {
+                    List<List<EdgeSpan>> subset_set = sort_planar_spans(spans, normal);
+                    foreach ( var subset in subset_set) {
+                        if (subset.Count == 1 ) {
+                            PlanarSpansFiller filler = new PlanarSpansFiller(Mesh, subset);
+                            filler.FillTargetEdgeLen = TargetEdgeLength;
+                            filler.SetPlane(pos, normal);
+                            filler.Fill();
+                        }
+                    }
+
+                } else {
+                    PlanarSpansFiller filler = new PlanarSpansFiller(Mesh, spans);
+                    filler.FillTargetEdgeLen = TargetEdgeLength;
+                    filler.SetPlane(pos, normal);
+                    filler.Fill();
+                }
+            }
+
+            return true;
+        }
+
+
+        /// <summary>
+        /// This function is supposed to take a set of spans in a plane and sort them
+        /// into regions that can be filled with a polygon. Currently kind of clusters
+        /// based on intersecting bboxes. Does not work.
+        /// 
+        /// I think fundamentally it needs to look back at the input mesh, to see what 
+        /// is connected/not-connected. Or possibly use polygon winding number? Need
+        /// to somehow define what the holes are...
+        /// </summary>
+        List<List<EdgeSpan>> sort_planar_spans(List<EdgeSpan> allspans, Vector3d normal)
+        {
+            List<List<EdgeSpan>> result = new List<List<EdgeSpan>>();
+            Frame3f polyFrame = new Frame3f(Vector3d.Zero, normal);
+
+            int N = allspans.Count;
+
+            List<PolyLine2d> plines = new List<PolyLine2d>();
+            foreach (EdgeSpan span in allspans) {
+                plines.Add(to_polyline(span, polyFrame));
+            }
+
+            bool[] bad_poly = new bool[N];
+            for (int k = 0; k < N; ++k)
+                bad_poly[k] = false; // self_intersects(plines[k]);
+
+            bool[] used = new bool[N];
+            for (int k = 0; k < N; ++k) {
+                if (used[k])
+                    continue;
+                bool is_bad = bad_poly[k];
+                AxisAlignedBox2d bounds = plines[k].Bounds;
+                used[k] = true;
+
+                List<int> set = new List<int>() { k };
+
+                for ( int j = k+1; j < N; ++j ) {
+                    if (used[j])
+                        continue;
+                    AxisAlignedBox2d boundsj = plines[j].Bounds;
+                    if ( bounds.Intersects(boundsj) ) {
+                        used[j] = true;
+                        is_bad = is_bad || bad_poly[j];
+                        bounds.Contain(boundsj);
+                        set.Add(j);
+                    }
+                }
+
+                if ( is_bad == false ) {
+                    List<EdgeSpan> span_set = new List<EdgeSpan>();
+                    foreach (int idx in set)
+                        span_set.Add(allspans[idx]);
+                    result.Add(span_set);
+                }
+
+            }
+
+            return result;
+        }
+        PolyLine2d to_polyline(EdgeSpan span, Frame3f polyFrame)
+        {
+            int NV = span.VertexCount;
+            PolyLine2d poly = new PolyLine2d();
+            for (int k = 0; k < NV; ++k)
+                poly.AppendVertex(polyFrame.ToPlaneUV((Vector3f)span.GetVertex(k), 2));
+            return poly;
+        }
+        Polygon2d to_polygon(EdgeSpan span, Frame3f polyFrame)
+        {
+            int NV = span.VertexCount;
+            Polygon2d poly = new Polygon2d();
+            for (int k = 0; k < NV; ++k)
+                poly.AppendVertex(polyFrame.ToPlaneUV((Vector3f)span.GetVertex(k), 2));
+            return poly;
+        }
+        bool self_intersects(PolyLine2d poly)
+        {
+            Segment2d seg = new Segment2d(poly.Start, poly.End);
+            int NS = poly.VertexCount - 2;
+            for ( int i = 1; i < NS; ++i ) { 
+                if (poly.Segment(i).Intersects(ref seg))
+                    return true;
+            }
+            return false;
+        }
+
+
+
+
+        // NO DOES NOT WORK. DOES NOT FIND EDGE SPANS THAT ARE IN PLANE BUT HAVE DIFFERENT NORMAL!
+        // NEED TO COLLECT UP SPANS USING NORMAL HISTOGRAM NORMALS!
+        // ALSO NEED TO ACTUALLY CHECK FOR COPLANARITY, NOT JUST SAME NORMAL!!
+
+        Dictionary<Vector3d,List<EdgeSpan>> find_coplanar_span_sets(DMesh3 mesh, EdgeLoop loop)
+        {
+            double dot_thresh = 0.999;
+
+            Dictionary<Vector3d, List<EdgeSpan>> span_sets = new Dictionary<Vector3d, List<EdgeSpan>>();
+
+            int NV = loop.Vertices.Length;
+            int NE = loop.Edges.Length;
+
+            Vector3d[] edge_normals = new Vector3d[NE];
+            for (int k = 0; k < NE; ++k)
+                edge_normals[k] = mesh.GetTriNormal(mesh.GetEdgeT(loop.Edges[k]).a);
+            
+            // find coplanar verts
+            // [RMS] this is wrong, if normals vary smoothly enough we will mark non-coplanar spans as coplanar
+            bool[] vert_coplanar = new bool[NV];
+            int nc = 0;
+            for ( int k = 0; k < NV; ++k ) {
+                int prev = (k==0) ? NV-1 : k-1;
+                if (edge_normals[k].Dot(ref edge_normals[prev]) > dot_thresh) {
+                    vert_coplanar[k] = true;
+                    nc++;
+                }
+            }
+            if (nc < 2)
+                return null;
+
+            int iStart = 0;
+            while (vert_coplanar[iStart])
+                iStart++;
+
+            int iPrev = iStart;
+            int iCur = iStart+1;
+            while (iCur != iStart) {
+                if (vert_coplanar[iCur] == false) {
+                    iPrev = iCur;
+                    iCur = (iCur + 1) % NV;
+                    continue;
+                }
+
+                List<int> edges = new List<int>() { loop.Edges[iPrev] };
+                int span_start_idx = iCur;
+                while (vert_coplanar[iCur]) {
+                    edges.Add(loop.Edges[iCur]);
+                    iCur = (iCur + 1) % NV;
+                }
+
+                if ( edges.Count > 1 ) {
+                    Vector3d span_n = edge_normals[span_start_idx];
+                    EdgeSpan span = EdgeSpan.FromEdges(mesh, edges);
+                    span.CheckValidity();
+                    foreach ( var pair in span_sets ) {
+                        if ( pair.Key.Dot(ref span_n) > dot_thresh ) {
+                            span_n = pair.Key;
+                            break;
+                        }
+                    }
+                    List<EdgeSpan> found;
+                    if (span_sets.TryGetValue(span_n, out found) == false)
+                        span_sets[span_n] = new List<EdgeSpan>() { span };
+                    else
+                        found.Add(span);
+                }
+
+            }
+
+
+
+            return span_sets;
+
+        }
+
+
+    }
+}
diff --git a/mesh_ops/LaplacianMeshSmoother.cs b/mesh_ops/LaplacianMeshSmoother.cs
index d3dadbea..0ef409d6 100644
--- a/mesh_ops/LaplacianMeshSmoother.cs
+++ b/mesh_ops/LaplacianMeshSmoother.cs
@@ -350,28 +350,52 @@ public bool SolveAndUpdateMesh()
         /// Apply LaplacianMeshSmoother to subset of mesh triangles. 
         /// border of subset always has soft constraint with borderWeight, 
         /// but is then snapped back to original vtx pos after solve.
-        /// nConstrainLoops inner loops are also soft-constrained, with weight falloff via square roots.
+        /// nConstrainLoops inner loops are also soft-constrained, with weight falloff via square roots (defines continuity)
         /// interiorWeight is soft constraint added to all vertices
         /// </summary>
-        public static void RegionSmooth(DMesh3 mesh, IEnumerable<int> triangles, int nConstrainLoops, 
+        public static void RegionSmooth(DMesh3 mesh, IEnumerable<int> triangles, 
+            int nConstrainLoops, 
+            int nIncludeExteriorRings,
+            bool bPreserveExteriorRings,
             double borderWeight = 10.0, double interiorWeight = 0.0)
         {
+            HashSet<int> fixedVerts = new HashSet<int>();
+            if ( nIncludeExteriorRings > 0 ) {
+                MeshFaceSelection expandTris = new MeshFaceSelection(mesh);
+                expandTris.Select(triangles);
+                if (bPreserveExteriorRings) {
+                    MeshEdgeSelection bdryEdges = new MeshEdgeSelection(mesh);
+                    bdryEdges.SelectBoundaryTriEdges(expandTris);
+                    expandTris.ExpandToOneRingNeighbours(nIncludeExteriorRings);
+                    MeshVertexSelection startVerts = new MeshVertexSelection(mesh);
+                    startVerts.SelectTriangleVertices(triangles);
+                    startVerts.DeselectEdges(bdryEdges);
+                    MeshVertexSelection expandVerts = new MeshVertexSelection(mesh, expandTris);
+                    foreach (int vid in expandVerts) {
+                        if (startVerts.IsSelected(vid) == false)
+                            fixedVerts.Add(vid);
+                    }
+                } else {
+                    expandTris.ExpandToOneRingNeighbours(nIncludeExteriorRings);
+                }
+                triangles = expandTris;
+            }
+
             RegionOperator region = new RegionOperator(mesh, triangles);
             DSubmesh3 submesh = region.Region;
             DMesh3 smoothMesh = submesh.SubMesh;
             LaplacianMeshSmoother smoother = new LaplacianMeshSmoother(smoothMesh);
 
-            // soft constraint on all interior vertices, if requested
-            if (interiorWeight > 0) {
-                foreach (int vid in smoothMesh.VertexIndices())
-                    smoother.SetConstraint(vid, smoothMesh.GetVertex(vid), interiorWeight, false);
-            }
+            // map fixed verts to submesh
+            HashSet<int> subFixedVerts = new HashSet<int>();
+            foreach (int base_vid in fixedVerts)
+                subFixedVerts.Add(submesh.MapVertexToSubmesh(base_vid));
 
-            // now constrain borders
+            // constrain borders
             double w = borderWeight;
 
-            HashSet<int> constrained = (region.Region.BaseBorderV.Count > 0) ? new HashSet<int>() : null;
-            foreach (int base_vid in region.Region.BaseBorderV) {
+            HashSet<int> constrained = (submesh.BaseBorderV.Count > 0) ? new HashSet<int>() : null;
+            foreach (int base_vid in submesh.BaseBorderV) {
                 int sub_vid = submesh.BaseToSubV[base_vid];
                 smoother.SetConstraint(sub_vid, smoothMesh.GetVertex(sub_vid), w, true);
                 if (constrained != null)
@@ -386,7 +410,8 @@ public static void RegionSmooth(DMesh3 mesh, IEnumerable<int> triangles, int nCo
                     foreach (int sub_vid in constrained) {
                         foreach (int nbr_vid in smoothMesh.VtxVerticesItr(sub_vid)) {
                             if (constrained.Contains(nbr_vid) == false) {
-                                smoother.SetConstraint(nbr_vid, smoothMesh.GetVertex(nbr_vid), w, false);
+                                if ( smoother.IsConstrained(nbr_vid) == false )
+                                    smoother.SetConstraint(nbr_vid, smoothMesh.GetVertex(nbr_vid), w, subFixedVerts.Contains(nbr_vid));
                                 next_layer.Add(nbr_vid);
                             }
                         }
@@ -397,6 +422,18 @@ public static void RegionSmooth(DMesh3 mesh, IEnumerable<int> triangles, int nCo
                 }
             }
 
+            // soft constraint on all interior vertices, if requested
+            if (interiorWeight > 0) {
+                foreach (int vid in smoothMesh.VertexIndices()) {
+                    if ( smoother.IsConstrained(vid) == false )
+                        smoother.SetConstraint(vid, smoothMesh.GetVertex(vid), interiorWeight, subFixedVerts.Contains(vid));
+                }
+            } else if ( subFixedVerts.Count > 0 ) { 
+                foreach (int vid in subFixedVerts) {
+                    if (smoother.IsConstrained(vid) == false)
+                        smoother.SetConstraint(vid, smoothMesh.GetVertex(vid), 0, true);
+                }
+            }
 
             smoother.SolveAndUpdateMesh();
             region.BackPropropagateVertices(true);
diff --git a/mesh_ops/MergeCoincidentEdges.cs b/mesh_ops/MergeCoincidentEdges.cs
new file mode 100644
index 00000000..1d1237cd
--- /dev/null
+++ b/mesh_ops/MergeCoincidentEdges.cs
@@ -0,0 +1,159 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using g3;
+
+namespace gs
+{
+	/// <summary>
+	/// Merge coincident edges.
+	/// </summary>
+	public class MergeCoincidentEdges
+	{
+		public DMesh3 Mesh;
+
+		public double MergeDistance = MathUtil.ZeroTolerancef;
+
+        public bool OnlyUniquePairs = false;
+
+		public MergeCoincidentEdges(DMesh3 mesh)
+		{
+			Mesh = mesh;
+		}
+
+		double merge_r2;
+
+		public virtual bool Apply() {
+			merge_r2 = MergeDistance * MergeDistance;
+
+            // construct hash table for edge midpoints
+            MeshBoundaryEdgeMidpoints pointset = new MeshBoundaryEdgeMidpoints(this.Mesh);
+			PointSetHashtable hash = new PointSetHashtable(pointset);
+            int hashN = 64;
+            if (Mesh.TriangleCount > 100000)   hashN = 128;
+            if (Mesh.TriangleCount > 1000000)  hashN = 256;
+			hash.Build(hashN);
+
+			Vector3d a = Vector3d.Zero, b = Vector3d.Zero;
+			Vector3d c = Vector3d.Zero, d = Vector3d.Zero;
+
+			// find edge equivalence sets. First we find all other edges with same
+			// midpoint, and then we check if endpoints are the same in second loop
+			int[] buffer = new int[1024];
+			List<int>[] EquivSets = new List<int>[Mesh.MaxEdgeID];
+			HashSet<int> remaining = new HashSet<int>();
+			foreach ( int eid in Mesh.BoundaryEdgeIndices() ) {
+				Vector3d midpt = Mesh.GetEdgePoint(eid, 0.5);
+				int N;
+				while (hash.FindInBall(midpt, MergeDistance, buffer, out N) == false)
+					buffer = new int[buffer.Length];
+				if (N == 1 && buffer[0] != eid)
+					throw new Exception("MergeCoincidentEdges.Apply: how could this happen?!");
+				if (N <= 1)
+					continue;  // unique edge
+
+				Mesh.GetEdgeV(eid, ref a, ref b);
+
+				// if same endpoints, add to equivalence set
+				List<int> equiv = new List<int>(N - 1);
+				for (int i = 0; i < N; ++i) {
+					if (buffer[i] != eid) {
+						Mesh.GetEdgeV(buffer[i], ref c, ref d);
+						if ( is_same_edge(ref a, ref b, ref c, ref d))
+							equiv.Add(buffer[i]);
+					}
+				}
+				if (equiv.Count > 0) {
+					EquivSets[eid] = equiv;
+					remaining.Add(eid);
+				}
+			}
+
+			// [TODO] could replace remaining hashset w/ PQ, and use conservative count?
+
+			// add potential duplicate edges to priority queue, sorted by
+			// number of possible matches. 
+			// [TODO] Does this need to be a PQ? Not updating PQ below anyway...
+			DynamicPriorityQueue<DuplicateEdge> Q = new DynamicPriorityQueue<DuplicateEdge>();
+			foreach ( int i in remaining ) {
+                if (OnlyUniquePairs) {
+                    if (EquivSets[i].Count != 1)
+                        continue;
+                    foreach (int j in EquivSets[i]) {
+                        if (EquivSets[j].Count != 1 || EquivSets[j][0] != i)
+                            continue;
+                    }
+                }
+
+                Q.Enqueue(new DuplicateEdge() { eid = i }, EquivSets[i].Count);
+			}
+
+			while ( Q.Count > 0 ) {
+				DuplicateEdge e = Q.Dequeue();
+				if (Mesh.IsEdge(e.eid) == false || EquivSets[e.eid] == null || remaining.Contains(e.eid) == false )
+					continue;               // dealt with this edge already
+                if (Mesh.IsBoundaryEdge(e.eid) == false)
+                    continue;
+
+				List<int> equiv = EquivSets[e.eid];
+
+				// find viable match
+				// [TODO] how to make good decisions here? prefer planarity?
+				bool merged = false;
+				int failed = 0;
+				for (int i = 0; i < equiv.Count && merged == false; ++i ) {
+					int other_eid = equiv[i];
+					if ( Mesh.IsEdge(other_eid) == false || Mesh.IsBoundaryEdge(other_eid) == false )
+						continue;
+
+					DMesh3.MergeEdgesInfo info;
+					MeshResult result = Mesh.MergeEdges(e.eid, other_eid, out info);
+					if ( result != MeshResult.Ok ) {
+						equiv.RemoveAt(i);
+                        i--;
+
+						EquivSets[other_eid].Remove(e.eid);
+						//Q.UpdatePriority(...);  // how need ref to queue node to do this...??
+						//   maybe equiv set is queue node??
+
+						failed++;
+					} else {
+						// ok we merged, other edge is no longer free
+						merged = true;
+						EquivSets[other_eid] = null;
+						remaining.Remove(other_eid);
+					}
+				}
+
+				if ( merged ) {
+					EquivSets[e.eid] = null;
+					remaining.Remove(e.eid);					
+				} else {
+					// should we do something else here? doesn't make sense to put
+					// back into Q, as it should be at the top, right?
+					EquivSets[e.eid] = null;
+					remaining.Remove(e.eid);
+				}
+
+			}
+
+			return true;
+		}
+
+
+
+		bool is_same_edge(ref Vector3d a, ref Vector3d b, ref Vector3d c, ref Vector3d d) {
+			return (a.DistanceSquared(c) < merge_r2 && b.DistanceSquared(d) < merge_r2) ||
+				(a.DistanceSquared(d) < merge_r2 && b.DistanceSquared(c) < merge_r2);
+		}
+
+
+
+		class DuplicateEdge : DynamicPriorityQueueNode {
+			public int eid;
+		}
+
+
+    }
+}
diff --git a/mesh_ops/MeshAssembly.cs b/mesh_ops/MeshAssembly.cs
new file mode 100644
index 00000000..b290b3cd
--- /dev/null
+++ b/mesh_ops/MeshAssembly.cs
@@ -0,0 +1,131 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using g3;
+
+namespace gs
+{
+
+    /// <summary>
+    /// Given an input mesh, try to decompose it's connected components into
+    /// parts with some semantics - solids, open meshes, etc.
+    /// </summary>
+    public class MeshAssembly
+    {
+        public DMesh3 SourceMesh;
+
+        // if true, each shell is a separate solid
+        public bool HasNoVoids = false;
+
+        /*
+         * Outputs
+         */
+
+        public List<DMesh3> ClosedSolids;
+        public List<DMesh3> OpenMeshes;
+
+        
+        public MeshAssembly(DMesh3 sourceMesh)
+        {
+            SourceMesh = sourceMesh;
+
+            ClosedSolids = new List<DMesh3>();
+            OpenMeshes = new List<DMesh3>();
+        }
+
+
+        public void Decompose()
+        {
+            process();
+        }
+
+
+
+        void process()
+        {
+            DMesh3 useSourceMesh = SourceMesh;
+
+            // try to do simple mesh repairs
+            if ( useSourceMesh.CachedIsClosed == false ) {
+
+                useSourceMesh = new DMesh3(SourceMesh);
+
+                // [TODO] should remove duplicate triangles here?
+                RemoveDuplicateTriangles dupes = new RemoveDuplicateTriangles(useSourceMesh);
+                dupes.Apply();
+
+                // close cracks
+                MergeCoincidentEdges merge = new MergeCoincidentEdges(useSourceMesh);
+                //merge.OnlyUniquePairs = true;
+                merge.Apply();
+            }
+
+            //Util.WriteDebugMesh(useSourceMesh, "c:\\scratch\\__FIRST_MERGE.obj");
+
+
+            DMesh3[] components = MeshConnectedComponents.Separate(useSourceMesh);
+
+            List<DMesh3> solidComps = new List<DMesh3>();
+
+            foreach ( DMesh3 mesh in components ) {
+
+                // [TODO] check if this is a mesh w/ cracks, in which case we
+                // can do other processing?
+
+                bool closed = mesh.CachedIsClosed;
+                if ( closed == false ) {
+                    OpenMeshes.Add(mesh);
+                    continue;
+                }
+
+                solidComps.Add(mesh);
+            }
+
+
+            if (solidComps.Count == 0)
+                return;
+            if ( solidComps.Count == 1 ) {
+                ClosedSolids = new List<DMesh3>() { solidComps[0] };
+            }
+
+
+            if (HasNoVoids) {
+                // each solid is a separate solid
+                ClosedSolids = process_solids_novoid(solidComps);
+            } else {
+                ClosedSolids = process_solids(solidComps);
+            }
+
+        }
+
+
+
+        List<DMesh3> process_solids(List<DMesh3> solid_components)
+        {
+            // [TODO] maybe we can have special tags that extract out certain meshes?
+
+            DMesh3 combinedSolid = new DMesh3(SourceMesh.Components | MeshComponents.FaceGroups);
+            MeshEditor editor = new MeshEditor(combinedSolid);
+            foreach (DMesh3 solid in solid_components) {
+                editor.AppendMesh(solid, combinedSolid.AllocateTriangleGroup());
+            }
+
+            return new List<DMesh3>() { combinedSolid };
+        }
+
+
+
+        List<DMesh3> process_solids_novoid(List<DMesh3> solid_components)
+        {
+            return solid_components;
+        }
+
+
+
+
+    }
+}
diff --git a/mesh_ops/MeshAutoRepair.cs b/mesh_ops/MeshAutoRepair.cs
new file mode 100644
index 00000000..bc529466
--- /dev/null
+++ b/mesh_ops/MeshAutoRepair.cs
@@ -0,0 +1,388 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// Mesh Auto Repair top-level driver.
+    /// 
+    /// TODO:
+    ///   - remove degenerate *faces*  (which may still have all edges > length)
+    ///       - this is tricky, in many CAD meshes these faces can't just be collapsed. But can often remove via flipping...?
+    /// 
+    /// </summary>
+    public class MeshAutoRepair
+    {
+		public double RepairTolerance = MathUtil.ZeroTolerancef;
+
+        // assume edges shorter than this are degenerate and should be collapsed
+        public double MinEdgeLengthTol = 0.0001;
+
+        // number of times we will delete border triangles and try again
+        public int ErosionIterations = 5;
+
+
+        // [TODO] interior components?
+        public enum RemoveModes {
+			None = 0, Interior = 1, Occluded = 2
+		};
+		public RemoveModes RemoveMode = MeshAutoRepair.RemoveModes.None;
+
+
+
+
+
+        /// <summary>
+        /// Set this to be able to cancel running remesher
+        /// </summary>
+        public ProgressCancel Progress = null;
+
+        /// <summary>
+        /// if this returns true, abort computation. 
+        /// </summary>
+        protected virtual bool Cancelled()
+        {
+            return (Progress == null) ? false : Progress.Cancelled();
+        }
+
+
+
+
+
+        public DMesh3 Mesh;
+
+		public MeshAutoRepair(DMesh3 mesh3)
+		{
+			Mesh = mesh3;
+		}
+
+
+		public bool Apply()
+		{
+			bool do_checks = false;
+
+			if ( do_checks ) Mesh.CheckValidity();
+
+
+            /*
+             * Remove parts of the mesh we don't want before we bother with anything else
+             * TODO: maybe we need to repair orientation first? if we want to use MWN...
+             */
+			do_remove_inside();
+                if (Cancelled()) return false;
+
+            int repeat_count = 0;
+            repeat_all:
+
+            /*
+             * make sure orientation of connected components is consistent
+             * TODO: what about mobius strip problems?
+             */
+            repair_orientation(false);
+                if (Cancelled()) return false;
+
+            /*
+             *  Do safe close-cracks to handle easy cases
+             */
+
+            repair_cracks(true, RepairTolerance);
+                if (Mesh.IsClosed()) goto all_done;
+                if (Cancelled()) return false;
+
+            /*
+             * Collapse tiny edges and then try easy cases again, and
+             * then allow for handling of ambiguous cases
+             */
+
+            collapse_all_degenerate_edges(RepairTolerance*0.5, true);
+                if (Cancelled()) return false;
+            repair_cracks(true, 2*RepairTolerance);
+                if (Cancelled()) return false;
+            repair_cracks(false, 2*RepairTolerance);
+                if (Cancelled()) return false;
+                if (Mesh.IsClosed()) goto all_done;
+
+            /*
+             * Possibly we have joined regions with different orientation (is it?), fix that
+             * TODO: mobius strips again
+             */
+            repair_orientation(false);
+                if (Cancelled()) return false;
+
+            if (do_checks) Mesh.CheckValidity();
+
+            // get rid of any remaining single-triangles before we start filling holes
+            remove_loners();
+
+            /*
+             * Ok, fill simple holes. 
+             */
+            int nRemainingBowties = 0;
+			int nHoles; bool bSawSpans;
+			fill_trivial_holes(out nHoles, out bSawSpans);
+                if (Cancelled()) return false;
+                if (Mesh.IsClosed()) goto all_done;
+
+            /*
+             * Now fill harder holes. If we saw spans, that means boundary loops could
+             * not be resolved in some cases, do we disconnect bowties and try again.
+             */
+            fill_any_holes(out nHoles, out bSawSpans);
+                if (Cancelled()) return false;
+            if (bSawSpans) {
+				disconnect_bowties(out nRemainingBowties);
+				fill_any_holes(out nHoles, out bSawSpans);
+			}
+                if (Cancelled()) return false;
+                if (Mesh.IsClosed()) goto all_done;
+
+            /*
+             * We may have a closed mesh now but it might still have bowties (eg
+             * tetrahedra sharing vtx case). So disconnect those.
+             */
+            disconnect_bowties(out nRemainingBowties);
+                if (Cancelled()) return false;
+
+            /*
+             * If the mesh is not closed, we will do one more round to try again.
+             */
+            if (repeat_count == 0 && Mesh.IsClosed() == false) {
+				repeat_count++;
+				goto repeat_all;
+			}
+
+            /*
+             * Ok, we didn't get anywhere on our first repeat. If we are still not
+             * closed, we will try deleting boundary triangles and repeating.
+             * Repeat this N times.
+             */
+            if ( repeat_count <= ErosionIterations && Mesh.IsClosed() == false) {
+                repeat_count++;
+                MeshFaceSelection bdry_faces = new MeshFaceSelection(Mesh);
+                foreach (int eid in MeshIterators.BoundaryEdges(Mesh))
+                    bdry_faces.SelectEdgeTris(eid);
+                MeshEditor.RemoveTriangles(Mesh, bdry_faces, true);
+                goto repeat_all;
+            }
+
+            all_done:
+
+            /*
+             * Remove tiny edges
+             */
+            if (MinEdgeLengthTol > 0) {
+                collapse_all_degenerate_edges(MinEdgeLengthTol, false);
+            }
+                if (Cancelled()) return false;
+
+            /*
+             * finally do global orientation
+             */
+            repair_orientation(true);
+                if (Cancelled()) return false;
+
+            if (do_checks) Mesh.CheckValidity();
+
+            /*
+             * Might as well compact output mesh...
+             */
+			Mesh = new DMesh3(Mesh, true);
+            MeshNormals.QuickCompute(Mesh);
+
+			return true;
+		}
+
+
+
+
+		void fill_trivial_holes(out int nRemaining, out bool saw_spans)
+		{
+			MeshBoundaryLoops loops = new MeshBoundaryLoops(Mesh);
+			nRemaining = 0;
+			saw_spans = loops.SawOpenSpans;
+
+			foreach (var loop in loops) {
+                if (Cancelled()) break;
+                bool filled = false;
+				if (loop.VertexCount == 3) {
+					SimpleHoleFiller filler = new SimpleHoleFiller(Mesh, loop);
+					filled = filler.Fill();
+				} else if ( loop.VertexCount == 4 ) {
+					MinimalHoleFill filler = new MinimalHoleFill(Mesh, loop);
+					filled = filler.Apply();
+					if (filled == false) {
+						SimpleHoleFiller fallback = new SimpleHoleFiller(Mesh, loop);
+						filled = fallback.Fill();
+					}
+				}
+
+				if (filled == false)
+					++nRemaining;
+			}
+		}
+
+
+
+		void fill_any_holes(out int nRemaining, out bool saw_spans)
+		{
+			MeshBoundaryLoops loops = new MeshBoundaryLoops(Mesh);
+			nRemaining = 0;
+			saw_spans = loops.SawOpenSpans;
+
+			foreach (var loop in loops) {
+                if (Cancelled()) break;
+                MinimalHoleFill filler = new MinimalHoleFill(Mesh, loop);
+				bool filled = filler.Apply();
+				if (filled == false) {
+                    if (Cancelled()) break;
+                    SimpleHoleFiller fallback = new SimpleHoleFiller(Mesh, loop);
+					filled = fallback.Fill();
+				}
+			}
+		}
+
+
+
+
+		bool repair_cracks(bool bUniqueOnly, double mergeDist)
+		{
+			try {
+				MergeCoincidentEdges merge = new MergeCoincidentEdges(Mesh);
+				merge.OnlyUniquePairs = bUniqueOnly;
+				merge.MergeDistance = mergeDist;
+				return merge.Apply();
+			} catch (Exception /*e*/) {
+				// ??
+				return false;
+			}
+		}
+
+
+
+		bool remove_duplicate_faces(double vtxTolerance, out int nRemoved)
+		{
+			nRemoved = 0;
+			try {
+				RemoveDuplicateTriangles dupe = new RemoveDuplicateTriangles(Mesh);
+				dupe.VertexTolerance = vtxTolerance;
+				bool bOK = dupe.Apply();
+				nRemoved = dupe.Removed;
+				return bOK;
+
+			} catch (Exception/*e*/) {
+				return false;
+			}
+		}
+
+
+
+		bool collapse_degenerate_edges(
+			double minLength, bool bBoundaryOnly, 
+			out int collapseCount)
+		{
+			collapseCount = 0;
+            // don't iterate sequentially because there may be pathological cases
+            foreach (int eid in MathUtil.ModuloIteration(Mesh.MaxEdgeID)) {
+                if (Cancelled()) break;
+                if (Mesh.IsEdge(eid) == false)
+					continue;
+                bool is_boundary_edge = Mesh.IsBoundaryEdge(eid);
+                if (bBoundaryOnly && is_boundary_edge == false)
+					continue;
+				Index2i ev = Mesh.GetEdgeV(eid);
+				Vector3d a = Mesh.GetVertex(ev.a), b = Mesh.GetVertex(ev.b);
+				if (a.Distance(b) < minLength) {
+					int keep = Mesh.IsBoundaryVertex(ev.a) ? ev.a : ev.b;
+					int discard = (keep == ev.a) ? ev.b : ev.a;
+					DMesh3.EdgeCollapseInfo collapseInfo;
+					MeshResult result = Mesh.CollapseEdge(keep, discard, out collapseInfo);
+					if (result == MeshResult.Ok) {
+						++collapseCount;
+						if (Mesh.IsBoundaryVertex(keep) == false || is_boundary_edge)
+							Mesh.SetVertex(keep, (a + b) * 0.5);
+					}
+				}
+			}
+			return true;
+		}
+		bool collapse_all_degenerate_edges(double minLength, bool bBoundaryOnly)
+		{
+			bool repeat = true;
+			while (repeat) {
+                if (Cancelled()) break;
+                int collapse_count;
+				collapse_degenerate_edges(minLength, bBoundaryOnly, out collapse_count);
+				if (collapse_count == 0)
+					repeat = false;
+			}
+			return true;
+		}
+
+
+
+
+		bool disconnect_bowties(out int nRemaining)
+		{
+			MeshEditor editor = new MeshEditor(Mesh);
+			nRemaining = editor.DisconnectAllBowties();
+			return true;
+		}
+
+
+        void repair_orientation(bool bGlobal)
+        {
+            MeshRepairOrientation orient = new MeshRepairOrientation(Mesh);
+            orient.OrientComponents();
+            if (Cancelled()) return;
+            if (bGlobal)
+                orient.SolveGlobalOrientation();
+        }
+
+
+
+
+		bool remove_interior(out int nRemoved)
+		{
+			RemoveOccludedTriangles remove = new RemoveOccludedTriangles(Mesh);
+			remove.PerVertex = true;
+			remove.InsideMode = RemoveOccludedTriangles.CalculationMode.FastWindingNumber;
+			remove.Apply();
+			nRemoved = remove.RemovedT.Count();
+			return true;
+		}
+		bool remove_occluded(out int nRemoved)
+		{
+			RemoveOccludedTriangles remove = new RemoveOccludedTriangles(Mesh);
+			remove.PerVertex = true;
+			remove.InsideMode = RemoveOccludedTriangles.CalculationMode.SimpleOcclusionTest;
+			remove.Apply();
+			nRemoved = remove.RemovedT.Count();
+			return true;
+		}
+		bool do_remove_inside()
+		{
+			int nRemoved = 0;
+			if (RemoveMode == RemoveModes.Interior) {
+				return remove_interior(out nRemoved);
+			} else if (RemoveMode == RemoveModes.Occluded) {
+				return remove_occluded(out nRemoved);
+			}
+			return true;
+		}
+
+
+
+        bool remove_loners()
+        {
+            bool bOK = MeshEditor.RemoveIsolatedTriangles(Mesh);
+            return true;
+        }
+
+
+    }
+}
diff --git a/mesh_ops/MeshBoolean.cs b/mesh_ops/MeshBoolean.cs
new file mode 100644
index 00000000..6519bb19
--- /dev/null
+++ b/mesh_ops/MeshBoolean.cs
@@ -0,0 +1,152 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace g3
+{
+    public class MeshBoolean
+    {
+        public DMesh3 Target;
+        public DMesh3 Tool;
+
+        // points within this tolerance are merged
+        public double VertexSnapTol = 0.00001;
+
+        public DMesh3 Result;
+
+        MeshMeshCut cutTargetOp;
+        MeshMeshCut cutToolOp;
+
+        DMesh3 cutTargetMesh;
+        DMesh3 cutToolMesh;
+
+        public bool Compute()
+        {
+            // Alternate strategy:
+            //   - don't do RemoveContained
+            //   - match embedded vertices, split where possible
+            //   - find min-cut path through shared edges
+            //   - remove contiguous patches that are inside both/etc (use MWN)
+            //   ** no good for coplanar regions...
+
+
+            cutTargetOp = new MeshMeshCut() {
+                Target = new DMesh3(Target),
+                CutMesh = Tool,
+                VertexSnapTol = VertexSnapTol
+            };
+            cutTargetOp.Compute();
+            cutTargetOp.RemoveContained();
+            cutTargetMesh = cutTargetOp.Target;
+
+            cutToolOp = new MeshMeshCut() {
+                Target = new DMesh3(Tool),
+                CutMesh = Target,
+                VertexSnapTol = VertexSnapTol
+            };
+            cutToolOp.Compute();
+            cutToolOp.RemoveContained();
+            cutToolMesh = cutToolOp.Target;
+
+            resolve_vtx_pairs();
+
+            Result = cutToolMesh;
+            MeshEditor.Append(Result, cutTargetMesh);
+
+            return true;
+        }
+
+
+
+
+
+
+
+        void resolve_vtx_pairs()
+        {
+            //HashSet<int> targetVerts = new HashSet<int>(cutTargetOp.CutVertices);
+            //HashSet<int> toolVerts = new HashSet<int>(cutToolOp.CutVertices);
+
+            // tracking on-cut vertices is not working yet...
+            Util.gDevAssert(Target.IsClosed() && Tool.IsClosed());
+
+            HashSet<int> targetVerts = new HashSet<int>(MeshIterators.BoundaryVertices(cutTargetMesh));
+            HashSet<int> toolVerts = new HashSet<int>(MeshIterators.BoundaryVertices(cutToolMesh));
+
+            split_missing(cutTargetOp, cutToolOp, cutTargetMesh, cutToolMesh, targetVerts, toolVerts);
+            split_missing(cutToolOp, cutTargetOp, cutToolMesh, cutTargetMesh, toolVerts, targetVerts);
+        }
+
+
+        void split_missing(MeshMeshCut fromOp, MeshMeshCut toOp, 
+                           DMesh3 fromMesh, DMesh3 toMesh,
+                           HashSet<int> fromVerts, HashSet<int> toVerts)
+        {
+            List<int> missing = new List<int>();
+            foreach (int vid in fromVerts) {
+                Vector3d v = fromMesh.GetVertex(vid);
+                int near_vid = find_nearest_vertex(toMesh, v, toVerts);
+                if (near_vid == DMesh3.InvalidID )
+                    missing.Add(vid);
+            }
+
+            foreach (int vid in missing) {
+                Vector3d v = fromMesh.GetVertex(vid);
+                int near_eid = find_nearest_edge(toMesh, v, toVerts);
+                if ( near_eid == DMesh3.InvalidID) {
+                    System.Console.WriteLine("could not find edge to split?");
+                    continue;
+                }
+
+                DMesh3.EdgeSplitInfo splitInfo;
+                MeshResult result = toMesh.SplitEdge(near_eid, out splitInfo);
+                if ( result != MeshResult.Ok ) {
+                    System.Console.WriteLine("edge split failed");
+                    continue;
+                }
+
+                toMesh.SetVertex(splitInfo.vNew, v);
+                toVerts.Add(splitInfo.vNew);
+            }
+        }
+
+
+
+        int find_nearest_vertex(DMesh3 mesh, Vector3d v, HashSet<int> vertices)
+        {
+            int near_vid = DMesh3.InvalidID;
+            double nearSqr = VertexSnapTol * VertexSnapTol;
+            foreach ( int vid in vertices ) {
+                double dSqr = mesh.GetVertex(vid).DistanceSquared(ref v);
+                if ( dSqr < nearSqr ) {
+                    near_vid = vid;
+                    nearSqr = dSqr;
+                }
+            }
+            return near_vid;
+        }
+
+        int find_nearest_edge(DMesh3 mesh, Vector3d v, HashSet<int> vertices)
+        {
+            int near_eid = DMesh3.InvalidID;
+            double nearSqr = VertexSnapTol * VertexSnapTol;
+            foreach ( int eid in mesh.BoundaryEdgeIndices() ) {
+                Index2i ev = mesh.GetEdgeV(eid);
+                if (vertices.Contains(ev.a) == false || vertices.Contains(ev.b) == false)
+                    continue;
+                Segment3d seg = new Segment3d(mesh.GetVertex(ev.a), mesh.GetVertex(ev.b));
+                double dSqr = seg.DistanceSquared(v);
+                if (dSqr < nearSqr) {
+                    near_eid = eid;
+                    nearSqr = dSqr;
+                }
+            }
+            return near_eid;
+        }
+
+    }
+}
diff --git a/mesh_ops/MeshExtrudeFaces.cs b/mesh_ops/MeshExtrudeFaces.cs
index 482f2c77..13ec1a95 100644
--- a/mesh_ops/MeshExtrudeFaces.cs
+++ b/mesh_ops/MeshExtrudeFaces.cs
@@ -21,8 +21,6 @@ public class MeshExtrudeFaces
         public DMesh3 Mesh;
         public int[] Triangles;
 
-        // arguments
-
         public SetGroupBehavior Group = SetGroupBehavior.AutoGenerate;
 
         // set new position based on original loop vertex position, normal, and index
@@ -32,7 +30,9 @@ public class MeshExtrudeFaces
         public List<Index2i> EdgePairs;                 // pairs of edges (original, extruded) that were stitched together
         public MeshVertexSelection ExtrudeVertices;     // vertices of extruded region
         public int[] JoinTriangles;                     // triangles generated to connect original end extruded edges together
+                                                        // may contain invalid triangle IDs if JoinIncomplete=true
         public int JoinGroupID;                         // group ID of connection triangles
+        public bool JoinIncomplete = false;             // if true, errors were encountered during the join operation
 
 
         public MeshExtrudeFaces(DMesh3 mesh, int[] triangles, bool bForceCopyArray = false)
@@ -69,11 +69,17 @@ public virtual ValidationStatus Validate()
         }
 
 
+        /// <summary>
+        /// Apply the extrustion operation to input Mesh.
+        /// Will return false if operation is not completed.
+        /// However changes are not backed out, so if false is returned, input Mesh is in 
+        /// undefined state (generally means there are some holes)
+        /// </summary>
         public virtual bool Extrude()
         {
             MeshEditor editor = new MeshEditor(Mesh);
 
-            editor.SeparateTriangles(Triangles, true, out EdgePairs);
+            bool bOK = editor.SeparateTriangles(Triangles, true, out EdgePairs);
 
             MeshNormals normals = null;
             bool bHaveNormals = Mesh.HasVertexNormals;
@@ -97,9 +103,9 @@ public virtual bool Extrude()
                 Mesh.SetVertex(vid, NewVertices[k++]);
 
             JoinGroupID = Group.GetGroupID(Mesh);
-            JoinTriangles = editor.StitchUnorderedEdges(EdgePairs, JoinGroupID);
+            JoinTriangles = editor.StitchUnorderedEdges(EdgePairs, JoinGroupID, false, out JoinIncomplete);
 
-            return true;
+            return JoinTriangles != null && JoinIncomplete == false;
         }
 
 
diff --git a/mesh_ops/MeshInsertPolygon.cs b/mesh_ops/MeshInsertPolygon.cs
new file mode 100644
index 00000000..65830167
--- /dev/null
+++ b/mesh_ops/MeshInsertPolygon.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+
+namespace g3
+{
+    /// <summary>
+    /// Insert Polygon into Mesh. Assumption is that Mesh has 3D coordinates (u,v,0).
+    /// This is basically a helper/wrapper around MeshInsertUVPolyCurve.
+    /// Inserted edge set is avaliable as .InsertedPolygonEdges, and
+    /// triangles inside polygon as .InteriorTriangles
+    /// </summary>
+    public class MeshInsertPolygon
+    {
+        public DMesh3 Mesh;
+        public GeneralPolygon2d Polygon;
+
+
+        public bool SimplifyInsertion = true;
+
+        public MeshInsertUVPolyCurve OuterInsert;
+        public List<MeshInsertUVPolyCurve> HoleInserts;
+        public HashSet<int> InsertedPolygonEdges;
+        public MeshFaceSelection InteriorTriangles;
+
+        public bool Insert()
+        {
+            OuterInsert = new MeshInsertUVPolyCurve(Mesh, Polygon.Outer);
+            Util.gDevAssert(OuterInsert.Validate() == ValidationStatus.Ok);
+            bool outerApplyOK = OuterInsert.Apply();
+            if (outerApplyOK == false || OuterInsert.Loops.Count == 0)
+                return false;
+            if (SimplifyInsertion)
+                OuterInsert.Simplify();
+
+            HoleInserts = new List<MeshInsertUVPolyCurve>(Polygon.Holes.Count);
+            for (int hi = 0; hi < Polygon.Holes.Count; ++hi) {
+                MeshInsertUVPolyCurve insert = new MeshInsertUVPolyCurve(Mesh, Polygon.Holes[hi]);
+                Util.gDevAssert(insert.Validate() == ValidationStatus.Ok);
+                insert.Apply();
+                if (SimplifyInsertion)
+                    insert.Simplify();
+                HoleInserts.Add(insert);
+            }
+
+
+            // find a triangle connected to loop that is inside the polygon
+            //   [TODO] maybe we could be a bit more robust about this? at least
+            //   check if triangle is too degenerate...
+            int seed_tri = -1;
+            EdgeLoop outer_loop = OuterInsert.Loops[0];
+            for (int i = 0; i < outer_loop.EdgeCount; ++i) {
+                if ( ! Mesh.IsEdge(outer_loop.Edges[i]) )
+                    continue;
+
+                Index2i et = Mesh.GetEdgeT(outer_loop.Edges[i]);
+                Vector3d ca = Mesh.GetTriCentroid(et.a);
+                bool in_a = Polygon.Outer.Contains(ca.xy);
+                Vector3d cb = Mesh.GetTriCentroid(et.b);
+                bool in_b = Polygon.Outer.Contains(cb.xy);
+                if (in_a && in_b == false) {
+                    seed_tri = et.a;
+                    break;
+                } else if (in_b && in_a == false) {
+                    seed_tri = et.b;
+                    break;
+                }
+            }
+            if (seed_tri == -1)
+                throw new Exception("MeshPolygonsInserter: could not find seed triangle!");
+
+            // make list of all outer & hole edges
+            InsertedPolygonEdges = new HashSet<int>(outer_loop.Edges);
+            foreach (var insertion in HoleInserts) {
+                foreach (int eid in insertion.Loops[0].Edges)
+                    InsertedPolygonEdges.Add(eid);
+            }
+
+            // flood-fill inside loop from seed triangle
+            InteriorTriangles = new MeshFaceSelection(Mesh);
+            InteriorTriangles.FloodFill(seed_tri, null, (eid) => { return InsertedPolygonEdges.Contains(eid) == false; });
+
+            return true;
+        }
+
+    }
+}
diff --git a/mesh_ops/MeshInsertProjectedPolygon.cs b/mesh_ops/MeshInsertProjectedPolygon.cs
new file mode 100644
index 00000000..5b834ae7
--- /dev/null
+++ b/mesh_ops/MeshInsertProjectedPolygon.cs
@@ -0,0 +1,260 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// Inserts a polygon into a mesh using a planar projection. You provide a
+    /// projection frame and either the polygon in the frame's XY-coordinate system,
+    /// or a DCurve3 space curve that will be projected. 
+    /// 
+    /// Currently you must also provide a seed triangle, that intersects the curve.
+    /// We flood-fill from the vertices of that triangle to find the interior vertices,
+    /// and hence the set of faces that are modified.
+    /// 
+    /// The insertion operation splits the existing mesh edges, so the inserted polygon
+    /// will have more segments than the input polygon, in general. If you set
+    /// SimplifyInsertion = true, then we collapse these extra edges, so you (should)
+    /// get back an edge loop with the same number of vertices. However, on a non-planar
+    /// mesh this means the edges will no longer lie on the input surface.
+    /// 
+    /// If RemovePolygonInterior = true, the faces inside the polygon are deleted
+    /// 
+    /// returns:
+    ///   ModifiedRegion: this is the RegionOperator created to subset the mesh for editing.
+    ///       You can use this to access the modified mesh
+    ///       
+    ///   InsertedPolygonVerts: the output vertex ID for Polygon[i]. This *does not* 
+    ///   include the intermediate vertices, it's a 1-1 correspondence.
+    ///       
+    ///   InsertedLoop: inserted edge loop on output mesh
+    ///       
+    ///   InteriorTriangles: the triangles inside the polygon, null if RemovePolygonInterior=true
+    ///   
+    /// 
+    /// If you would like to change the behavior after the insertion is computed, you can 
+    /// subclass and override BackPropagate().
+    /// 
+    /// 
+    /// [TODO] currently we construct a planar BVTree (but 3D) to map the new vertices to
+    /// 3D via barycentric interpolation. However we could do this inline. MeshInsertUVPolyCurve 
+    /// needs to fully support working on separate coordinate set (it tries via Get/Set PointF, but
+    /// it is not 100% working), and it needs to let client know about poke and split events, w/
+    /// bary-coords, so that we can compute the new 3D positions. 
+    /// 
+    /// </summary>
+    public class MeshInsertProjectedPolygon
+    {
+        public DMesh3 Mesh;
+        public int SeedTriangle = -1;   // you must provide this so that we can efficiently
+                                        // find region of mesh to insert into
+        public Frame3f ProjectFrame;    // assumption is that Z is plane normal
+
+        // if true, we call Simply() on the inserted UV-curve, which means the
+        // resulting insertion should have as many vertices as Polygon, and
+        // the InsertedPolygonVerts list should be verts of a valid edge-loop
+        public bool SimplifyInsertion = true;
+
+        // if true, we delete triangles on polygon interior
+        public bool RemovePolygonInterior = true;
+
+
+        // internally a RegionOperator is constructed and the insertion is done
+        // on a submesh. This is that submesh, provided for your convenience
+        public RegionOperator ModifiedRegion;
+
+        // vertex IDs of inserted polygon vertices in output mesh. 
+        public int[] InsertedPolygonVerts;
+
+        // inserted edge loop
+        public EdgeLoop InsertedLoop;
+
+        // set of triangles inside polygon. null if RemovePolgonInterior = true
+        public int[] InteriorTriangles;
+
+        // inserted polygon, in case you did not save a reference
+        public Polygon2d Polygon;
+
+
+
+        /// <summary>
+        /// insert polygon in given frame
+        /// </summary>
+        public MeshInsertProjectedPolygon(DMesh3 mesh, Polygon2d poly, Frame3f frame, int seedTri)
+        {
+            Mesh = mesh;
+            Polygon = new Polygon2d(poly);
+            ProjectFrame = frame;
+            SeedTriangle = seedTri;
+        }
+
+        /// <summary>
+        /// create Polygon by projecting polygon3 into frame
+        /// </summary>
+        public MeshInsertProjectedPolygon(DMesh3 mesh, DCurve3 polygon3, Frame3f frame, int seedTri )
+        {
+            if (polygon3.Closed == false)
+                throw new Exception("MeshInsertPolyCurve(): only closed polygon3 supported for now");
+
+            Mesh = mesh;
+            ProjectFrame = frame;
+            SeedTriangle = seedTri;
+
+            Polygon = new Polygon2d();
+            foreach (Vector3d v3 in polygon3.Vertices) {
+                Vector2f uv = frame.ToPlaneUV((Vector3f)v3, 2);
+                Polygon.AppendVertex(uv);
+            }
+        }
+
+
+        public virtual ValidationStatus Validate()
+        {
+            if (Mesh.IsTriangle(SeedTriangle) == false)
+                return ValidationStatus.NotATriangle;
+
+            return ValidationStatus.Ok;
+        }
+
+
+        public bool Insert()
+        {
+            Func<int, bool> is_contained_v = (vid) => {
+                Vector3d v = Mesh.GetVertex(vid);
+                Vector2f vf2 = ProjectFrame.ToPlaneUV((Vector3f)v, 2);
+                return Polygon.Contains(vf2);
+            };
+
+            MeshVertexSelection vertexROI = new MeshVertexSelection(Mesh);
+            Index3i seedT = Mesh.GetTriangle(SeedTriangle);
+
+            // if a seed vert of seed triangle is containd in polygon, we will
+            // flood-fill out from there, this gives a better ROI. 
+            // If not, we will try flood-fill from the seed triangles.
+            List<int> seed_verts = new List<int>();
+            for ( int j = 0; j < 3; ++j ) {
+                if ( is_contained_v(seedT[j]) )
+                    seed_verts.Add(seedT[j]);
+            }
+            if (seed_verts.Count == 0) {
+                seed_verts.Add(seedT.a);
+                seed_verts.Add(seedT.b);
+                seed_verts.Add(seedT.c);
+            }
+
+            // flood-fill out from seed vertices until we have found all vertices
+            // contained in polygon
+            vertexROI.FloodFill(seed_verts.ToArray(), is_contained_v);
+
+            // convert vertex ROI to face ROI
+            MeshFaceSelection faceROI = new MeshFaceSelection(Mesh, vertexROI, 1);
+            faceROI.ExpandToOneRingNeighbours();
+            faceROI.FillEars(true);    // this might be a good idea...
+
+            // construct submesh
+            RegionOperator regionOp = new RegionOperator(Mesh, faceROI);
+            DSubmesh3 roiSubmesh = regionOp.Region;
+            DMesh3 roiMesh = roiSubmesh.SubMesh;
+
+            // save 3D positions of unmodified mesh
+            Vector3d[] initialPositions = new Vector3d[roiMesh.MaxVertexID];
+
+            // map roi mesh to plane
+            MeshTransforms.PerVertexTransform(roiMesh, roiMesh.VertexIndices(), (v, vid) => {
+                Vector2f uv = ProjectFrame.ToPlaneUV((Vector3f)v, 2);
+                initialPositions[vid] = v;
+                return new Vector3d(uv.x, uv.y, 0);
+            });
+
+            // save a copy of 2D mesh and construct bvtree. we will use
+            // this later to project back to 3d
+            // [TODO] can we use a better spatial DS here, that takes advantage of 2D?
+            DMesh3 projectMesh = new DMesh3(roiMesh);
+            DMeshAABBTree3 projecter = new DMeshAABBTree3(projectMesh, true);
+
+            MeshInsertUVPolyCurve insertUV = new MeshInsertUVPolyCurve(roiMesh, Polygon);
+            //insertUV.Validate()
+            bool bOK = insertUV.Apply();
+            if (!bOK)
+                throw new Exception("insertUV.Apply() failed");
+
+            if ( SimplifyInsertion )
+                insertUV.Simplify();
+
+            int[] insertedPolyVerts = insertUV.CurveVertices;
+
+            // grab inserted loop, assuming it worked
+            EdgeLoop insertedLoop = null;
+            if ( insertUV.Loops.Count == 1 ) {
+                insertedLoop = insertUV.Loops[0];
+            }
+
+            // find interior triangles
+            List<int> interiorT = new List<int>();
+            foreach (int tid in roiMesh.TriangleIndices()) {
+                Vector3d centroid = roiMesh.GetTriCentroid(tid);
+                if (Polygon.Contains(centroid.xy))
+                    interiorT.Add(tid);
+            }
+            if (RemovePolygonInterior) {
+                MeshEditor editor = new MeshEditor(roiMesh);
+                editor.RemoveTriangles(interiorT, true);
+                InteriorTriangles = null;
+            } else {
+                InteriorTriangles = interiorT.ToArray();
+            }
+
+
+            // map back to 3d
+            Vector3d a = Vector3d.Zero, b = Vector3d.Zero, c = Vector3d.Zero;
+            foreach ( int vid in roiMesh.VertexIndices() ) {
+                
+                // [TODO] somehow re-use exact positions from regionOp maps?
+
+                // construct new 3D pos w/ barycentric interpolation
+                Vector3d v = roiMesh.GetVertex(vid);
+                int tid = projecter.FindNearestTriangle(v);
+                Index3i tri = projectMesh.GetTriangle(tid);
+                projectMesh.GetTriVertices(tid, ref a, ref b, ref c);
+                Vector3d bary = MathUtil.BarycentricCoords(ref v, ref a, ref b, ref c);
+                Vector3d pos = bary.x * initialPositions[tri.a] + bary.y * initialPositions[tri.b] + bary.z * initialPositions[tri.c];
+
+                roiMesh.SetVertex(vid, pos);
+            }
+
+            bOK = BackPropagate(regionOp, insertedPolyVerts, insertedLoop);
+
+            return bOK;
+        }
+
+
+
+        protected virtual bool BackPropagate(RegionOperator regionOp, int[] insertedPolyVerts, EdgeLoop insertedLoop)
+        {
+            bool bOK = regionOp.BackPropropagate();
+            if (bOK) {
+                ModifiedRegion = regionOp;
+
+                IndexUtil.Apply(insertedPolyVerts, regionOp.ReinsertSubToBaseMapV);
+                InsertedPolygonVerts = insertedPolyVerts;
+
+                if (insertedLoop != null) {
+                    InsertedLoop = MeshIndexUtil.MapLoopViaVertexMap(regionOp.ReinsertSubToBaseMapV,
+                        regionOp.Region.SubMesh, regionOp.Region.BaseMesh, insertedLoop);
+                    if (RemovePolygonInterior)
+                        InsertedLoop.CorrectOrientation();
+                }
+            }
+            return bOK;
+        }
+
+
+ 
+
+    }
+}
diff --git a/mesh_ops/MeshInsertUVPolyCurve.cs b/mesh_ops/MeshInsertUVPolyCurve.cs
index 145c620e..68676e19 100644
--- a/mesh_ops/MeshInsertUVPolyCurve.cs
+++ b/mesh_ops/MeshInsertUVPolyCurve.cs
@@ -10,6 +10,14 @@ namespace g3
     /// Assumptions:
     ///   - mesh vertex x/y coordinates are 2D coordinates we want to use. Replace PointF if this is not the case.
     ///   - segments of Curve lie entirely within UV-triangles
+    ///   
+    /// Limitations:
+    ///   - currently not robust to near-parallel line segments that are within epsilon-band of the
+    ///     input loop. In this case, we will include all such segments in the 'cut' set, but we
+    ///     will probably not be able to find a connected path through them. 
+    ///   - not robust to degenerate geometry. Strongly recommend that you use Validate() and/or
+    ///     preprocess the input mesh to remove degenerate faces/edges
+    /// 
     /// </summary>
     public class MeshInsertUVPolyCurve
 	{
@@ -26,6 +34,11 @@ public class MeshInsertUVPolyCurve
         // the spans & loops take some compute time and can be disabled if you don't need it...
         public bool EnableCutSpansAndLoops = true;
 
+        // probably always makes sense to use this...maybe not for very small problems?
+        public bool UseTriSpatial = true;
+
+        // points/edges within this distance are considered the same
+        public double SpatialEpsilon = MathUtil.ZeroTolerance;
 
         // Results
 
@@ -96,39 +109,122 @@ public virtual ValidationStatus Validate(double fDegenerateTol = MathUtil.ZeroTo
 		}
 
 
+
+        // we use this simple 2D bins data structure to speed up containment queries
+
+        TriangleBinsGrid2d triSpatial;
+
+        void spatial_add_triangle(int tid) {
+            if (triSpatial == null)
+                return;
+            Index3i tv = Mesh.GetTriangle(tid);
+            Vector2d a = PointF(tv.a), b = PointF(tv.b), c = PointF(tv.c);
+            triSpatial.InsertTriangleUnsafe(tid, ref a, ref b, ref c);
+        }
+        void spatial_add_triangles(int t0, int t1) {
+            if (triSpatial == null)
+                return;
+            spatial_add_triangle(t0);
+            if (t1 != DMesh3.InvalidID)
+                spatial_add_triangle(t1);
+        }
+        void spatial_remove_triangle(int tid) {
+            if (triSpatial == null)
+                return;
+            Index3i tv = Mesh.GetTriangle(tid);
+            Vector2d a = PointF(tv.a), b = PointF(tv.b), c = PointF(tv.c);
+            triSpatial.RemoveTriangleUnsafe(tid, ref a, ref b, ref c);
+        }
+        void spatial_remove_triangles(int t0, int t1) {
+            if (triSpatial == null)
+                return;
+            spatial_remove_triangle(t0);
+            if (t1 != DMesh3.InvalidID)
+                spatial_remove_triangle(t1);
+        }
+
+
         // (sequentially) find each triangle that path point lies in, and insert a vertex for
         // that point into mesh.
-        void insert_corners()
+        void insert_corners(HashSet<int> MeshVertsOnCurve)
         {
             PrimalQuery2d query = new PrimalQuery2d(PointF);
 
-            // [TODO] can do everythnig up to PokeTriangle in parallel, 
-            // except if we are poking same tri w/ multiple points!
+            if (UseTriSpatial) {
+                int count = Mesh.TriangleCount + Curve.VertexCount;
+                int bins = 32;
+                if (count < 25) bins = 8;
+                else if (count < 100) bins = 16;
+                AxisAlignedBox3d bounds3 = Mesh.CachedBounds;
+                AxisAlignedBox2d bounds2 = new AxisAlignedBox2d(bounds3.Min.xy, bounds3.Max.xy);
+                triSpatial = new TriangleBinsGrid2d(bounds2, bins);
+                foreach (int tid in Mesh.TriangleIndices())
+                    spatial_add_triangle(tid);
+            }
+
+            Func<int, Vector2d, bool> inTriangleF = (tid, pos) => {
+                Index3i tv = Mesh.GetTriangle(tid);
+                int query_result = query.ToTriangleUnsigned(pos, tv.a, tv.b, tv.c);
+                return (query_result == -1 || query_result == 0);
+            };
 
             CurveVertices = new int[Curve.VertexCount];
             for ( int i = 0; i < Curve.VertexCount; ++i ) {
                 Vector2d vInsert = Curve[i];
                 bool inserted = false;
 
-                foreach (int tid in Mesh.TriangleIndices()) {
-                    Index3i tv = Mesh.GetTriangle(tid);
-                    // [RMS] using unsigned query here because we do not need to care about tri CW/CCW orientation
-                    //   (right? otherwise we have to explicitly invert mesh. Nothing else we do depends on tri orientation)
-                    //int query_result = query.ToTriangle(vInsert, tv.a, tv.b, tv.c);
-                    int query_result = query.ToTriangleUnsigned(vInsert, tv.a, tv.b, tv.c);
-                    if (query_result == -1 || query_result == 0) {
-                        Vector3d bary = MathUtil.BarycentricCoords(vInsert, PointF(tv.a), PointF(tv.b), PointF(tv.c));
-                        int vid = insert_corner_from_bary(i, tid, bary);
-                        if ( vid > 0 ) {    // this should be always happening..
-                            CurveVertices[i] = vid;
-                            inserted = true;
-
-                            //Util.WriteDebugMesh(Mesh, string.Format("C:\\git\\geometry3SharpDemos\\geometry3Test\\test_output\\after_insert_corner_{0}.obj", i));
+                // find the triangle that contains this curve point
+                int contain_tid = DMesh3.InvalidID;
+                if (triSpatial != null) {
+                    contain_tid = triSpatial.FindContainingTriangle(vInsert, inTriangleF);
+                } else {
+                    foreach (int tid in Mesh.TriangleIndices()) {
+                        Index3i tv = Mesh.GetTriangle(tid);
+                        // [RMS] using unsigned query here because we do not need to care about tri CW/CCW orientation
+                        //   (right? otherwise we have to explicitly invert mesh. Nothing else we do depends on tri orientation)
+                        //int query_result = query.ToTriangle(vInsert, tv.a, tv.b, tv.c);
+                        int query_result = query.ToTriangleUnsigned(vInsert, tv.a, tv.b, tv.c);
+                        if (query_result == -1 || query_result == 0) {
+                            contain_tid = tid;
                             break;
                         }
+                    }
+                }
+
+                // if we found one, insert the point via face-poke or edge-split,
+                // unless it is exactly at existing vertex, in which case we can re-use it
+                if ( contain_tid != DMesh3.InvalidID ) {
+                    Index3i tv = Mesh.GetTriangle(contain_tid);
+                    Vector3d bary = MathUtil.BarycentricCoords(vInsert, PointF(tv.a), PointF(tv.b), PointF(tv.c));
+                    // SpatialEpsilon is our zero-tolerance, so merge if we are closer than that
+                    bool is_existing_v;
+                    int vid = insert_corner_from_bary(i, contain_tid, bary, 0.01, 100*SpatialEpsilon, out is_existing_v);
+                    if (vid > 0) {
+                        CurveVertices[i] = vid;
+                        if (is_existing_v)
+                            MeshVertsOnCurve.Add(vid);
+                        inserted = true;
                     } 
                 }
 
+                // if we did not find containing triangle, 
+                // try matching with any existing vertices.
+                // This can happen if curve point is right on mesh border...
+                if (inserted == false) {
+                    foreach (int vid in Mesh.VertexIndices()) {
+                        Vector2d v = PointF(vid);
+                        if (vInsert.Distance(v) < SpatialEpsilon) {
+                            CurveVertices[i] = vid;
+                            MeshVertsOnCurve.Add(vid);
+                            inserted = true;
+                        }
+                    }
+                }
+
+                // TODO: also case where curve point is right on mesh border edge,
+                // and so it ends up being outside all triangles?
+
+
                 if (inserted == false) {
                     throw new Exception("MeshInsertUVPolyCurve.insert_corners: curve vertex " 
                         + i.ToString() + " is not inside or on any mesh triangle!");
@@ -140,45 +236,70 @@ void insert_corners()
 
         // insert point at bary_coords inside tid. If point is at vtx, just use that vtx.
         // If it is on an edge, do an edge split. Otherwise poke face.
-        int insert_corner_from_bary(int iCorner, int tid, Vector3d bary_coords, double tol = MathUtil.ZeroTolerance)
+        int insert_corner_from_bary(int iCorner, int tid, Vector3d bary_coords, 
+            double bary_tol, double spatial_tol, out bool is_existing_v)
         {
+            is_existing_v = false;
             Vector2d vInsert = Curve[iCorner];
             Index3i tv = Mesh.GetTriangle(tid);
 
             // handle cases where corner is on a vertex
-            if (bary_coords.x > 1 - tol)
-                return tv.a;
-            else if (bary_coords.y > 1 - tol)
-                return tv.b;
-            else if ( bary_coords.z > 1 - tol )
-                return tv.c;
+            int cornerv = -1;
+            if (bary_coords.x > 1 - bary_tol)
+                cornerv = tv.a;
+            else if (bary_coords.y > 1 - bary_tol)
+                cornerv = tv.b;
+            else if (bary_coords.z > 1 - bary_tol)
+                cornerv = tv.c;
+            if (cornerv != -1 && PointF(cornerv).Distance(vInsert) < spatial_tol) {
+                is_existing_v = true;
+                return cornerv;
+            }
 
             // handle cases where corner is on an edge
             int split_edge = -1;
-            if (bary_coords.x < tol)
+            if (bary_coords.x < bary_tol)
                 split_edge = 1;
-            else if (bary_coords.y < tol)
+            else if (bary_coords.y < bary_tol)
                 split_edge = 2;
-            else if (bary_coords.z < tol)
+            else if (bary_coords.z < bary_tol)
                 split_edge = 0;
             if (split_edge >= 0) {
                 int eid = Mesh.GetTriEdge(tid, split_edge);
 
-                DMesh3.EdgeSplitInfo split_info;
-                MeshResult splitResult = Mesh.SplitEdge(eid, out split_info);
-                if (splitResult != MeshResult.Ok)
-                    throw new Exception("MeshInsertUVPolyCurve.insert_corner_special: edge split failed in case sum==2");
-                SetPointF(split_info.vNew, vInsert);
-                return split_info.vNew;
+                Index2i ev = Mesh.GetEdgeV(eid);
+                Segment2d seg = new Segment2d(PointF(ev.a), PointF(ev.b));
+                if (seg.DistanceSquared(vInsert) < spatial_tol*spatial_tol) {
+                    Index2i et = Mesh.GetEdgeT(eid);
+                    spatial_remove_triangles(et.a, et.b);
+
+                    DMesh3.EdgeSplitInfo split_info;
+                    MeshResult splitResult = Mesh.SplitEdge(eid, out split_info);
+                    if (splitResult != MeshResult.Ok)
+                        throw new Exception("MeshInsertUVPolyCurve.insert_corner_from_bary: edge split failed in case sum==2 - " + splitResult.ToString());
+                    SetPointF(split_info.vNew, vInsert);
+
+                    spatial_add_triangles(et.a, et.b);
+                    spatial_add_triangles(split_info.eNewT2, split_info.eNewT3);
+
+                    return split_info.vNew;
+                }
             }
 
+            spatial_remove_triangle(tid);
+
             // otherwise corner is inside triangle
             DMesh3.PokeTriangleInfo pokeinfo;
             MeshResult result = Mesh.PokeTriangle(tid, bary_coords, out pokeinfo);
             if (result != MeshResult.Ok)
-                throw new Exception("MeshInsertUVPolyCurve.insert_corner_special: face poke failed!");
+                throw new Exception("MeshInsertUVPolyCurve.insert_corner_from_bary: face poke failed - " + result.ToString());
 
             SetPointF(pokeinfo.new_vid, vInsert);
+
+            spatial_add_triangle(tid);
+            spatial_add_triangle(pokeinfo.new_t1);
+            spatial_add_triangle(pokeinfo.new_t2);
+
             return pokeinfo.new_vid;
         }
 
@@ -189,7 +310,8 @@ int insert_corner_from_bary(int iCorner, int tid, Vector3d bary_coords, double t
 
         public virtual bool Apply()
 		{
-            insert_corners();
+            HashSet<int> OnCurveVerts = new HashSet<int>();     // original vertices that were epsilon-coincident w/ curve vertices
+            insert_corners(OnCurveVerts);
 
             // [RMS] not using this?
             //HashSet<int> corner_v = new HashSet<int>(CurveVertices);
@@ -199,6 +321,14 @@ public virtual bool Apply()
             HashSet<int> ZeroVertices = new HashSet<int>();
             OnCutEdges = new HashSet<int>();
 
+            HashSet<int> NewEdges = new HashSet<int>();
+            HashSet<int> NewCutVertices = new HashSet<int>();
+            sbyte[] signs = new sbyte[2 * Mesh.MaxVertexID + 2*Curve.VertexCount];
+
+            HashSet<int> segTriangles = new HashSet<int>();
+            HashSet<int> segVertices = new HashSet<int>();
+            HashSet<int> segEdges = new HashSet<int>();
+
             // loop over segments, insert each one in sequence
             int N = (IsLoop) ? Curve.VertexCount : Curve.VertexCount - 1;
             for ( int si = 0; si < N; ++si ) {
@@ -212,38 +342,55 @@ public virtual bool Apply()
                 // If these vertices are already connected by an edge, we can just continue.
                 int existing_edge = Mesh.FindEdge(i0_vid, i1_vid);
                 if ( existing_edge != DMesh3.InvalidID ) {
-                    OnCutEdges.Add(existing_edge);
+                    add_cut_edge(existing_edge);
                     continue;
                 }
 
+                if (triSpatial != null) {
+                    segTriangles.Clear(); segVertices.Clear(); segEdges.Clear();
+                    AxisAlignedBox2d segBounds = new AxisAlignedBox2d(seg.P0); segBounds.Contain(seg.P1);
+                    segBounds.Expand(MathUtil.ZeroTolerancef * 10);
+                    triSpatial.FindTrianglesInRange(segBounds, segTriangles);
+                    IndexUtil.TrianglesToVertices(Mesh, segTriangles, segVertices);
+                    IndexUtil.TrianglesToEdges(Mesh, segTriangles, segEdges);
+                }
+
+                int MaxVID = Mesh.MaxVertexID;
+                IEnumerable<int> vertices = Interval1i.Range(MaxVID);
+                if (triSpatial != null)
+                    vertices = segVertices;
+
                 // compute edge-crossing signs
                 // [TODO] could walk along mesh from a to b, rather than computing for entire mesh?
-                int MaxVID = Mesh.MaxVertexID;
-                int[] signs = new int[MaxVID];
-                gParallel.ForEach(Interval1i.Range(MaxVID), (vid) => {
+                if ( signs.Length < MaxVID )
+                    signs = new sbyte[2*MaxVID];
+                gParallel.ForEach(vertices, (vid) => {
                     if (Mesh.IsVertex(vid)) {
                         if (vid == i0_vid || vid == i1_vid) {
                             signs[vid] = 0;
                         } else {
                             Vector2d v2 = PointF(vid);
                             // tolerance defines band in which we will consider values to be zero
-                            signs[vid] = seg.WhichSide(v2, MathUtil.ZeroTolerance);
+                            signs[vid] = (sbyte)seg.WhichSide(v2, SpatialEpsilon);
                         }
                     } else
-                        signs[vid] = int.MaxValue;
+                        signs[vid] = sbyte.MaxValue;
                 });
 
                 // have to skip processing of new edges. If edge id
                 // is > max at start, is new. Otherwise if in NewEdges list, also new.
                 // (need both in case we re-use an old edge index)
                 int MaxEID = Mesh.MaxEdgeID;
-                HashSet<int> NewEdges = new HashSet<int>();
-                HashSet<int> NewCutVertices = new HashSet<int>();
+                NewEdges.Clear();
+                NewCutVertices.Clear();
                 NewCutVertices.Add(i0_vid);
                 NewCutVertices.Add(i1_vid);
 
                 // cut existing edges with segment
-                for (int eid = 0; eid < MaxEID; ++eid) {
+                IEnumerable<int> edges = Interval1i.Range(MaxEID);
+                if (triSpatial != null)
+                    edges = segEdges;
+                foreach ( int eid in edges ) { 
                     if (Mesh.IsEdge(eid) == false)
                         continue;
                     if (eid >= MaxEID || NewEdges.Contains(eid))
@@ -257,12 +404,15 @@ public virtual bool Apply()
                     int eva_sign = signs[ev.a];
                     int evb_sign = signs[ev.b];
 
+                    // [RMS] should we be using larger epsilon here? If we don't track OnCurveVerts explicitly, we 
+                    // need to at least use same epsilon we passed to insert_corner_from_bary...do we still also
+                    // need that to catch the edges we split in the poke?
                     bool eva_in_segment = false;
                     if ( eva_sign == 0 ) 
-                        eva_in_segment = Math.Abs(seg.Project(PointF(ev.a))) < (seg.Extent + MathUtil.ZeroTolerance);
+                        eva_in_segment = OnCurveVerts.Contains(ev.a) || Math.Abs(seg.Project(PointF(ev.a))) < (seg.Extent + SpatialEpsilon);
                     bool evb_in_segment = false;
                     if (evb_sign == 0)
-                        evb_in_segment = Math.Abs(seg.Project(PointF(ev.b))) < (seg.Extent + MathUtil.ZeroTolerance);
+                        evb_in_segment = OnCurveVerts.Contains(ev.b) || Math.Abs(seg.Project(PointF(ev.b))) < (seg.Extent + SpatialEpsilon);
 
                     // If one or both vertices are on-segment, we have special case.
                     // If just one vertex is on the segment, we can skip this edge.
@@ -270,9 +420,12 @@ public virtual bool Apply()
                     if (eva_in_segment || evb_in_segment) {
                         if (eva_in_segment && evb_in_segment) {
                             ZeroEdges.Add(eid);
-                            OnCutEdges.Add(eid); 
+                            add_cut_edge(eid);
+                            NewCutVertices.Add(ev.a); NewCutVertices.Add(ev.b);
                         } else {
-                            ZeroVertices.Add(eva_in_segment ? ev.a : ev.b);
+                            int zvid = eva_in_segment ? ev.a : ev.b;
+                            ZeroVertices.Add(zvid);
+                            NewCutVertices.Add(zvid);
                         }
                         continue;
                     }
@@ -291,27 +444,32 @@ public virtual bool Apply()
                         // [RMS] we should have already caught this above, so if it happens here it is probably spurious?
                         // we should have caught this case above, but numerics are different so it might occur again
                         ZeroEdges.Add(eid);
-                        OnCutEdges.Add(eid);
+                        NewCutVertices.Add(ev.a); NewCutVertices.Add(ev.b);
+                        add_cut_edge(eid);
                         continue;
                     } else if (intr.Type != IntersectionType.Point) {
                         continue; // no intersection
                     }
                     Vector2d x = intr.Point0;
+                    double t = Math.Sqrt(x.DistanceSquared(va) / va.DistanceSquared(vb));
 
                     // this case happens if we aren't "on-segment" but after we do the test the intersection pt 
                     // is within epsilon of one end of the edge. This is a spurious t-intersection and we
                     // can ignore it. Some other edge should exist that picks up this vertex as part of it.
                     // [TODO] what about if this edge is degenerate?
-                    bool x_in_segment = Math.Abs(edge_seg.Project(x)) < (edge_seg.Extent - MathUtil.ZeroTolerance);
+                    bool x_in_segment = Math.Abs(edge_seg.Project(x)) < (edge_seg.Extent - SpatialEpsilon);
                     if (! x_in_segment ) {
                         continue;
                     }
 
+                    Index2i et = Mesh.GetEdgeT(eid);
+                    spatial_remove_triangles(et.a, et.b);
+
                     // split edge at this segment
                     DMesh3.EdgeSplitInfo splitInfo;
-                    MeshResult result = Mesh.SplitEdge(eid, out splitInfo);
+                    MeshResult result = Mesh.SplitEdge(eid, out splitInfo, t);
                     if (result != MeshResult.Ok) {
-                        throw new Exception("MeshInsertUVSegment.Cut: failed in SplitEdge");
+                        throw new Exception("MeshInsertUVSegment.Apply: SplitEdge failed - " + result.ToString());
                         //return false;
                     }
 
@@ -322,29 +480,25 @@ public virtual bool Apply()
                     NewEdges.Add(splitInfo.eNewBN);
                     NewEdges.Add(splitInfo.eNewCN);
 
+                    spatial_add_triangles(et.a, et.b);
+                    spatial_add_triangles(splitInfo.eNewT2, splitInfo.eNewT3);
+
                     // some splits - but not all - result in new 'other' edges that are on
                     // the polypath. We want to keep track of these edges so we can extract loop later.
                     Index2i ecn = Mesh.GetEdgeV(splitInfo.eNewCN);
                     if (NewCutVertices.Contains(ecn.a) && NewCutVertices.Contains(ecn.b))
-                        OnCutEdges.Add(splitInfo.eNewCN);
+                        add_cut_edge(splitInfo.eNewCN);
 
                     // since we don't handle bdry edges this should never be false, but maybe we will handle bdry later...
                     if (splitInfo.eNewDN != DMesh3.InvalidID) {
                         NewEdges.Add(splitInfo.eNewDN);
                         Index2i edn = Mesh.GetEdgeV(splitInfo.eNewDN);
                         if (NewCutVertices.Contains(edn.a) && NewCutVertices.Contains(edn.b))
-                            OnCutEdges.Add(splitInfo.eNewDN);
+                            add_cut_edge(splitInfo.eNewDN);
                     }
                 }
             }
 
-
-            //MeshEditor editor = new MeshEditor(Mesh);
-            //foreach (int eid in OnCutEdges)
-            //    editor.AppendBox(new Frame3f(Mesh.GetEdgePoint(eid, 0.5)), 0.1f);
-            //Util.WriteDebugMesh(Mesh, string.Format("C:\\git\\geometry3SharpDemos\\geometry3Test\\test_output\\after_inserted.obj"));
-
-
             // extract the cut paths
             if (EnableCutSpansAndLoops)
                 find_cut_paths(OnCutEdges);
@@ -354,7 +508,10 @@ public virtual bool Apply()
 		} // Apply()
 
 
-
+        // useful to have all these calls centralized for debugging...
+        void add_cut_edge(int eid) {
+            OnCutEdges.Add(eid);
+        }
 
 
 
@@ -504,7 +661,7 @@ static List<int> walk_edge_span_forward(DMesh3 mesh, int start_edge, int start_p
             bool done = false;
             while (!done) {
 
-                // fink outgoing edge in set and connected to current pivot vtx
+                // find outgoing edge in set and connected to current pivot vtx
                 int next_edge = -1;
                 foreach (int nbr_edge in mesh.VtxEdgesItr(cur_pivot_v)) {
                     if (EdgeSet.Contains(nbr_edge)) {
diff --git a/mesh_ops/MeshIsoCurves.cs b/mesh_ops/MeshIsoCurves.cs
index e14d62e4..6169ba32 100644
--- a/mesh_ops/MeshIsoCurves.cs
+++ b/mesh_ops/MeshIsoCurves.cs
@@ -15,6 +15,26 @@ public class MeshIsoCurves
         /// </summary>
         public Func<int, double> VertexValueF = null;
 
+        /// <summary>
+        /// If true, then we internally precompute vertex values.
+        /// ***THIS COMPUTATION IS MULTI-THREADED***
+        /// </summary>
+        public bool PrecomputeVertexValues = false;
+
+
+        public enum RootfindingModes { SingleLerp, LerpSteps, Bisection }
+
+        /// <summary>
+        /// Which rootfinding method will be used to converge on surface along edges
+        /// </summary>
+        public RootfindingModes RootMode = RootfindingModes.SingleLerp;
+
+        /// <summary>
+        /// number of iterations of rootfinding method (ignored for SingleLerp)
+        /// </summary>
+        public int RootModeSteps = 5;
+
+
         public DGraph3 Graph = null;
 
         public enum TriangleCase
@@ -26,14 +46,23 @@ public enum TriangleCase
 
         public bool WantGraphEdgeInfo = false;
 
+        /// <summary>
+        /// Information about edge of the computed Graph. 
+        /// mesh_tri is triangle ID of crossed triangle
+        /// mesh_edges depends on case. EdgeEdge is [edgeid,edgeid], EdgeVertex is [edgeid,vertexid], and OnEdge is [edgeid,-1]
+        /// </summary>
         public struct GraphEdgeInfo
         {
             public TriangleCase caseType;
             public int mesh_tri;
             public Index2i mesh_edges;
+            public Index2i order;
         }
         public DVector<GraphEdgeInfo> GraphEdges = null;
 
+        // locations of edge crossings that we found during rootfinding. key is edge id.
+        Dictionary<int, Vector3d> EdgeLocations = new Dictionary<int, Vector3d>();
+
 
         public MeshIsoCurves(DMesh3 mesh, Func<Vector3d, double> valueF)
         {
@@ -43,7 +72,7 @@ public MeshIsoCurves(DMesh3 mesh, Func<Vector3d, double> valueF)
 
         public void Compute()
         {
-            compute_full(Mesh.TriangleIndices());
+            compute_full(Mesh.TriangleIndices(), true);
         }
         public void Compute(IEnumerable<int> Triangles)
         {
@@ -57,7 +86,7 @@ public void Compute(IEnumerable<int> Triangles)
 
         Dictionary<Vector3d, int> Vertices;
 
-        protected void compute_full(IEnumerable<int> Triangles)
+        protected void compute_full(IEnumerable<int> Triangles, bool bIsFullMeshHint = false)
         {
             Graph = new DGraph3();
             if (WantGraphEdgeInfo)
@@ -65,6 +94,24 @@ protected void compute_full(IEnumerable<int> Triangles)
 
             Vertices = new Dictionary<Vector3d, int>();
 
+
+            // multithreaded precomputation of per-vertex values
+            double[] vertex_values = null;
+            if (PrecomputeVertexValues) {
+                vertex_values = new double[Mesh.MaxVertexID];
+                IEnumerable<int> verts = Mesh.VertexIndices();
+                if (bIsFullMeshHint == false) {
+                    MeshVertexSelection vertices = new MeshVertexSelection(Mesh);
+                    vertices.SelectTriangleVertices(Triangles);
+                    verts = vertices;
+                }
+                gParallel.ForEach(verts, (vid) => {
+                    vertex_values[vid] = ValueF(Mesh.GetVertex(vid));
+                });
+                VertexValueF = (vid) => { return vertex_values[vid]; };
+            }
+
+
             foreach (int tid in Triangles) {
 
                 Vector3dTuple3 tv = new Vector3dTuple3();
@@ -93,11 +140,14 @@ protected void compute_full(IEnumerable<int> Triangles)
                     if (f[i1] == 0 || f[i2] == 0) {
                         // on-edge case
                         int z1 = f[i1] == 0 ? i1 : i2;
+                        if ( (z0+1)%3 != z1 ) {     
+                            int tmp = z0; z0 = z1; z1 = tmp;        // catch reverse-orientation cases
+                        }
                         int e0 = add_or_append_vertex(Mesh.GetVertex(triVerts[z0]));
                         int e1 = add_or_append_vertex(Mesh.GetVertex(triVerts[z1]));
                         int graph_eid = Graph.AppendEdge(e0, e1, (int)TriangleCase.OnEdge);
-                        if (WantGraphEdgeInfo)
-                            add_on_edge(graph_eid, tid, triEdges[z0]);
+						if (graph_eid >= 0 && WantGraphEdgeInfo)
+                            add_on_edge(graph_eid, tid, triEdges[z0], new Index2i(e0, e1));
 
                     } else {
                         // edge/vertex case
@@ -111,26 +161,45 @@ protected void compute_full(IEnumerable<int> Triangles)
                         }
                         Vector3d cross = find_crossing(tv[i], tv[j], f[i], f[j]);
                         int cross_vid = add_or_append_vertex(cross);
+                        add_edge_pos(triVerts[i], triVerts[j], cross);
 
-                        int graph_eid = Graph.AppendEdge(vert_vid, cross_vid, (int)TriangleCase.EdgeVertex);
-                        if (WantGraphEdgeInfo)
-                            add_edge_edge(graph_eid, tid, new Index2i(triEdges[(z0+1)%3], triVerts[z0]));
+                        if (vert_vid != cross_vid) {
+                            int graph_eid = Graph.AppendEdge(vert_vid, cross_vid, (int)TriangleCase.EdgeVertex);
+                            if (graph_eid >= 0 && WantGraphEdgeInfo)
+                                add_edge_vert(graph_eid, tid, triEdges[(z0 + 1) % 3], triVerts[z0], new Index2i(vert_vid, cross_vid));
+                        } // else degenerate edge
                     }
 
                 } else {
                     Index3i cross_verts = Index3i.Min;
-                    for (int ti = 0; ti < 3; ++ti) {
-                        int i = ti, j = (ti + 1) % 3;
+                    int less_than = 0;
+                    for (int tei = 0; tei < 3; ++tei) {
+                        int i = tei, j = (tei + 1) % 3;
+                        if (f[i] < 0)
+                            less_than++;
                         if (f[i] * f[j] > 0)
                             continue;
                         if ( triVerts[j] < triVerts[i] ) {
                             int tmp = i; i = j; j = tmp;
                         }
                         Vector3d cross = find_crossing(tv[i], tv[j], f[i], f[j]);
-                        cross_verts[ti] = add_or_append_vertex(cross);
+                        cross_verts[tei] = add_or_append_vertex(cross);
+                        add_edge_pos(triVerts[i], triVerts[j], cross);
                     }
                     int e0 = (cross_verts.a == int.MinValue) ? 1 : 0;
                     int e1 = (cross_verts.c == int.MinValue) ? 1 : 2;
+                    if (e0 == 0 && e1 == 2) {       // preserve orientation order
+                        e0 = 2; e1 = 0;
+                    }
+
+                    // preserving orientation does not mean we get a *consistent* orientation across faces.
+                    // To do that, we need to assign "sides". Either we have 1 less-than-0 or 1 greater-than-0 vtx.
+                    // Arbitrary decide that we want loops oriented like bdry loops would be if we discarded less-than side.
+                    // In that case, when we are "cutting off" one vertex, orientation would end up flipped
+                    if (less_than == 1) {
+                        int tmp = e0; e0 = e1; e1 = tmp;
+                    }
+
                     int ev0 = cross_verts[e0];
                     int ev1 = cross_verts[e1];
                     // [RMS] if function is garbage, we can end up w/ case where both crossings
@@ -139,8 +208,8 @@ protected void compute_full(IEnumerable<int> Triangles)
                     if (ev0 != ev1) {
                         Util.gDevAssert(ev0 != int.MinValue && ev1 != int.MinValue);
                         int graph_eid = Graph.AppendEdge(ev0, ev1, (int)TriangleCase.EdgeEdge);
-                        if (WantGraphEdgeInfo)
-                            add_edge_edge(graph_eid, tid, new Index2i(triEdges[e0], triEdges[e1]));
+						if (graph_eid >= 0 && WantGraphEdgeInfo)
+                            add_edge_edge(graph_eid, tid, new Index2i(triEdges[e0], triEdges[e1]), new Index2i(ev0,ev1));
                     }
                 }
             }
@@ -161,53 +230,149 @@ int add_or_append_vertex(Vector3d pos)
         }
 
 
-        void add_edge_edge(int graph_eid, int mesh_tri, Index2i mesh_edges)
+        void add_edge_edge(int graph_eid, int mesh_tri, Index2i mesh_edges, Index2i order)
         {
             GraphEdgeInfo einfo = new GraphEdgeInfo() {
                 caseType = TriangleCase.EdgeEdge,
                 mesh_edges = mesh_edges,
-                mesh_tri = mesh_tri
+                mesh_tri = mesh_tri,
+                order = order
             };
             GraphEdges.insertAt(einfo, graph_eid);
         }
 
-        void add_edge_vert(int graph_eid, int mesh_tri, int mesh_edge, int mesh_vert)
+        void add_edge_vert(int graph_eid, int mesh_tri, int mesh_edge, int mesh_vert, Index2i order)
         {
             GraphEdgeInfo einfo = new GraphEdgeInfo() {
                 caseType = TriangleCase.EdgeVertex,
                 mesh_edges = new Index2i(mesh_edge, mesh_vert),
-                mesh_tri = mesh_tri
+                mesh_tri = mesh_tri,
+                order = order
             };
             GraphEdges.insertAt(einfo, graph_eid);
         }
 
-        void add_on_edge(int graph_eid, int mesh_tri, int mesh_edge)
+        void add_on_edge(int graph_eid, int mesh_tri, int mesh_edge, Index2i order)
         {
             GraphEdgeInfo einfo = new GraphEdgeInfo() {
                 caseType = TriangleCase.OnEdge,
                 mesh_edges = new Index2i(mesh_edge, -1),
-                mesh_tri = mesh_tri
+                mesh_tri = mesh_tri,
+                order = order
             };
             GraphEdges.insertAt(einfo, graph_eid);
         }
 
 
+        // [TODO] should convert this to a utility function
         Vector3d find_crossing(Vector3d a, Vector3d b, double fA, double fB)
         {
-            double t = 0.5;
-            if (fA < fB) {
-                t = (0 - fA) / (fB - fA);
-                t = MathUtil.Clamp(t, 0, 1);
+            if (fB < fA) {
+                Vector3d tmp = a; a = b; b = tmp;
+                double f = fA; fA = fB; fB = f;
+            }
+
+            if (RootMode == RootfindingModes.Bisection) {
+                for ( int k = 0; k < RootModeSteps; ++k ) {
+                    Vector3d c = Vector3d.Lerp(a, b, 0.5);
+                    double f = ValueF(c);
+                    if ( f < 0 ) {
+                        fA = f;  a = c;
+                    } else {
+                        fB = f;  b = c;
+                    }
+                }
+                return Vector3d.Lerp(a, b, 0.5);
+
+            } else {
+                // really should check this every iteration...
+                if ( Math.Abs(fB-fA) < MathUtil.ZeroTolerance )
+                    return a;
+
+                double t = 0;
+                if (RootMode == RootfindingModes.LerpSteps) {
+                    for (int k = 0; k < RootModeSteps; ++k) {
+                        t = MathUtil.Clamp((0 - fA) / (fB - fA), 0, 1);
+                        Vector3d c = (1 - t)*a + (t)*b;
+                        double f = ValueF(c);
+                        if (f < 0) {
+                            fA = f; a = c;
+                        } else {
+                            fB = f; b = c;
+                        }
+                    }
+                }
+
+                t = MathUtil.Clamp((0 - fA) / (fB - fA), 0, 1);
                 return (1 - t) * a + (t) * b;
-            } else if ( fB < fA ) {
-                t = (0 - fB) / (fA - fB);
-                t = MathUtil.Clamp(t, 0, 1);
-                return (1 - t) * b + (t) * a;
-            } else
-                return a;
+            }
+        }
+
+
+
+        void add_edge_pos(int a, int b, Vector3d crossing_pos)
+        {
+            int eid = Mesh.FindEdge(a, b);
+            if (eid == DMesh3.InvalidID)
+                throw new Exception("MeshIsoCurves.add_edge_split: invalid edge?");
+            if (EdgeLocations.ContainsKey(eid))
+                return;
+            EdgeLocations[eid] = crossing_pos;
         }
 
 
+        /// <summary>
+        /// Split the mesh edges at the iso-crossings, unless edge is
+        /// shorter than min_len, or inserted point would be within min_len or vertex
+        /// [TODO] do we want to return any info here??
+        /// </summary>
+        public void SplitAtIsoCrossings(double min_len = 0)
+        {
+            foreach ( var pair in EdgeLocations ) {
+                int eid = pair.Key;
+                Vector3d pos = pair.Value;
+                if (!Mesh.IsEdge(eid))
+                    continue;
+
+                Index2i ev = Mesh.GetEdgeV(eid);
+                Vector3d a = Mesh.GetVertex(ev.a);
+                Vector3d b = Mesh.GetVertex(ev.b);
+                if (a.Distance(b) < min_len)
+                    continue;
+                Vector3d mid = (a + b) * 0.5;
+                if (a.Distance(mid) < min_len || b.Distance(mid) < min_len)
+                    continue;
+
+                DMesh3.EdgeSplitInfo splitInfo;
+                if (Mesh.SplitEdge(eid, out splitInfo) == MeshResult.Ok)
+                    Mesh.SetVertex(splitInfo.vNew, pos);
+            }
+        }
+
+
+
+
+        /// <summary>
+        /// DGraph3 edges are not oriented, which means they cannot inherit orientation from mesh.
+        /// This function returns true if, for a given graph_eid, the vertex pair returned by
+        /// Graph.GetEdgeV(graph_eid) should be reversed to be consistent with mesh orientation.
+        /// Mainly inteded to be passed to DGraph3Util.ExtractCurves
+        /// </summary>
+        public bool ShouldReverseGraphEdge(int graph_eid)
+        {
+            if (GraphEdges == null)
+                throw new Exception("MeshIsoCurves.OrientEdge: must track edge graph info to orient edge");
+
+            Index2i graph_ev = Graph.GetEdgeV(graph_eid);
+            GraphEdgeInfo einfo = GraphEdges[graph_eid];
+
+            if (graph_ev.b == einfo.order.a && graph_ev.a == einfo.order.b) {
+                return true;
+            }
+            Util.gDevAssert(graph_ev.a == einfo.order.a && graph_ev.b == einfo.order.b);
+            return false;
+        }
+
 
     }
 
diff --git a/mesh_ops/MeshMeshCut.cs b/mesh_ops/MeshMeshCut.cs
new file mode 100644
index 00000000..f00a0035
--- /dev/null
+++ b/mesh_ops/MeshMeshCut.cs
@@ -0,0 +1,664 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace g3
+{
+    /// <summary>
+    /// 
+    /// 
+    /// TODO:
+    ///    - track descendant triangles of each input face
+    ///    - for missing segments, can resolve in 2D in plane of face
+    /// 
+    /// 
+    /// </summary>
+    public class MeshMeshCut
+    {
+        public DMesh3 Target;
+        public DMesh3 CutMesh;
+
+        PointHashGrid3d<int> PointHash;
+
+        // points within this tolerance are merged
+        public double VertexSnapTol = 0.00001;
+
+        // List of vertices in output Target that are on the
+        // cut path, after calling RemoveContained. 
+        // TODO: still missing some vertices??
+        public List<int> CutVertices;
+
+
+        public void Compute()
+        {
+            double cellSize = Target.CachedBounds.MaxDim / 64;
+            PointHash = new PointHashGrid3d<int>(cellSize, -1);
+
+            // insert target vertices into hash
+            foreach ( int vid in Target.VertexIndices()) {
+                Vector3d v = Target.GetVertex(vid);
+                int existing = find_existing_vertex(v);
+                if (existing != -1)
+                    System.Console.WriteLine("VERTEX {0} IS DUPLICATE OF {1}!", vid, existing);
+                PointHash.InsertPointUnsafe(vid, v);
+            }
+
+            initialize();
+            find_segments();
+            insert_face_vertices();
+            insert_edge_vertices();
+            connect_edges();
+
+            // SegmentInsertVertices was constructed by planar polygon
+            // insertions in MeshInsertUVPolyCurve calls, but we also
+            // need to the segment vertices
+            foreach (SegmentVtx sv in SegVertices)
+                SegmentInsertVertices.Add(sv.vtx_id);
+        }
+
+
+        public void RemoveContained()
+        {
+            DMeshAABBTree3 spatial = new DMeshAABBTree3(CutMesh, true);
+            spatial.WindingNumber(Vector3d.Zero);
+            SafeListBuilder<int> removeT = new SafeListBuilder<int>();
+            gParallel.ForEach(Target.TriangleIndices(), (tid) => {
+                Vector3d v = Target.GetTriCentroid(tid);
+                if (spatial.WindingNumber(v) > 0.9)
+                    removeT.SafeAdd(tid);
+            });
+            MeshEditor.RemoveTriangles(Target, removeT.Result);
+
+            // [RMS] construct set of on-cut vertices? This is not
+            // necessarily all boundary vertices...
+            CutVertices = new List<int>();
+            foreach (int vid in SegmentInsertVertices) {
+                if (Target.IsVertex(vid))
+                    CutVertices.Add(vid);
+            }
+        }
+
+        public void AppendSegments(double r)
+        {
+            foreach ( var seg in Segments ) {
+                Segment3d s = new Segment3d(seg.v0.v, seg.v1.v);
+                if ( Target.FindEdge(seg.v0.vtx_id, seg.v1.vtx_id) == DMesh3.InvalidID )
+                    MeshEditor.AppendLine(Target, s, (float)r);
+            }
+        }
+
+        public void ColorFaces()
+        {
+            int counter = 1;
+            Dictionary<int, int> gidmap = new Dictionary<int, int>();
+            foreach (var key in SubFaces.Keys)
+                gidmap[key] = counter++;
+            Target.EnableTriangleGroups(0);
+            foreach ( int tid in Target.TriangleIndices() ) {
+                if (ParentFaces.ContainsKey(tid))
+                    Target.SetTriangleGroup(tid, gidmap[ParentFaces[tid]]);
+                else if (SubFaces.ContainsKey(tid))
+                    Target.SetTriangleGroup(tid, gidmap[tid]);
+            }
+        }
+
+
+        class SegmentVtx
+        {
+            public Vector3d v;
+            public int type = -1;
+            public int initial_type = -1;
+            public int vtx_id = DMesh3.InvalidID;
+            public int elem_id = DMesh3.InvalidID;
+        }
+        List<SegmentVtx> SegVertices;
+        Dictionary<int, SegmentVtx> VIDToSegVtxMap;
+
+
+        // segment vertices in each triangle that we still have to insert
+        Dictionary<int, List<SegmentVtx>> FaceVertices;
+
+        // segment vertices in each edge that we still have to insert
+        Dictionary<int, List<SegmentVtx>> EdgeVertices;
+
+
+        class IntersectSegment
+        {
+            public int base_tid;
+            public SegmentVtx v0;
+            public SegmentVtx v1;
+            public SegmentVtx this[int key] {
+                get { return (key == 0) ? v0 : v1; }
+                set { if (key == 0) v0 = value; else v1 = value; }
+            }
+        }
+        IntersectSegment[] Segments;
+
+        Vector3d[] BaseFaceCentroids;
+        Vector3d[] BaseFaceNormals;
+        Dictionary<int, HashSet<int>> SubFaces;
+        Dictionary<int, int> ParentFaces;
+
+        HashSet<int> SegmentInsertVertices;
+
+        void initialize()
+        {
+            BaseFaceCentroids = new Vector3d[Target.MaxTriangleID];
+            BaseFaceNormals = new Vector3d[Target.MaxTriangleID];
+            double area = 0;
+            foreach (int tid in Target.TriangleIndices())
+                Target.GetTriInfo(tid, out BaseFaceNormals[tid], out area, out BaseFaceCentroids[tid]);
+
+            // allocate internals
+            SegVertices = new List<SegmentVtx>();
+            EdgeVertices = new Dictionary<int, List<SegmentVtx>>();
+            FaceVertices = new Dictionary<int, List<SegmentVtx>>();
+            SubFaces = new Dictionary<int, HashSet<int>>();
+            ParentFaces = new Dictionary<int, int>();
+            SegmentInsertVertices = new HashSet<int>();
+            VIDToSegVtxMap = new Dictionary<int, SegmentVtx>();
+        }
+
+
+
+        /// <summary>
+        /// 1) Find intersection segments
+        /// 2) sort onto existing input mesh vtx/edge/face
+        /// </summary>
+        void find_segments()
+        {
+            Dictionary<Vector3d, SegmentVtx> SegVtxMap = new Dictionary<Vector3d, SegmentVtx>();
+
+            // find intersection segments
+            // TODO: intersection polygons
+            // TODO: do we need to care about intersection vertices?
+            DMeshAABBTree3 targetSpatial = new DMeshAABBTree3(Target, true);
+            DMeshAABBTree3 cutSpatial = new DMeshAABBTree3(CutMesh, true);
+            var intersections = targetSpatial.FindAllIntersections(cutSpatial);
+
+            // for each segment, for each vtx, determine if it is 
+            // at an existing vertex, on-edge, or in-face
+            Segments = new IntersectSegment[intersections.Segments.Count];
+            for ( int i = 0; i < Segments.Length; ++i ) {
+                var isect = intersections.Segments[i];
+                Vector3dTuple2 points = new Vector3dTuple2(isect.point0, isect.point1);
+                IntersectSegment iseg = new IntersectSegment() {
+                    base_tid = isect.t0
+                };
+                Segments[i] = iseg;
+                for (int j = 0; j < 2; ++j) {
+                    Vector3d v = points[j];
+
+                    // if this exact vtx coord has been seen, use same vtx
+                    SegmentVtx sv;
+                    if (SegVtxMap.TryGetValue(v, out sv)) {
+                        iseg[j] = sv;
+                        continue;
+                    }
+                    sv = new SegmentVtx() { v = v };
+                    SegVertices.Add(sv);
+                    SegVtxMap[v] = sv;
+                    iseg[j] = sv;
+
+                    // this vtx is tol-equal to input mesh vtx
+                    int existing_v = find_existing_vertex(isect.point0);
+                    if (existing_v >= 0) {
+                        sv.initial_type = sv.type = 0;
+                        sv.elem_id = existing_v;
+                        sv.vtx_id = existing_v;
+                        VIDToSegVtxMap[sv.vtx_id] = sv;
+                        continue;
+                    }
+
+                    Triangle3d tri = new Triangle3d();
+                    Target.GetTriVertices(isect.t0, ref tri.V0, ref tri.V1, ref tri.V2);
+                    Index3i tv = Target.GetTriangle(isect.t0);
+
+                    // this vtx is tol-on input mesh edge
+                    int on_edge_i = on_edge(ref tri, ref v);
+                    if ( on_edge_i >= 0 ) {
+                        sv.initial_type = sv.type = 1;
+                        sv.elem_id = Target.FindEdge(tv[on_edge_i], tv[(on_edge_i+1)%3]);
+                        Util.gDevAssert(sv.elem_id != DMesh3.InvalidID);
+                        add_edge_vtx(sv.elem_id, sv);
+                        continue;
+                    }
+
+                    // otherwise contained in input mesh face
+                    sv.initial_type = sv.type = 2;
+                    sv.elem_id = isect.t0;
+                    add_face_vtx(sv.elem_id, sv);
+                }
+
+            }
+
+        }
+
+
+
+
+        /// <summary>
+        /// For each on-face vtx, we poke the face, and re-sort 
+        /// the remaining vertices on that face onto new faces/edges
+        /// </summary>
+        void insert_face_vertices()
+        {
+            while ( FaceVertices.Count > 0 ) {
+                var pair = FaceVertices.First();
+                int tid = pair.Key;
+                List<SegmentVtx> triVerts = pair.Value;
+                SegmentVtx v = triVerts[triVerts.Count-1];
+                triVerts.RemoveAt(triVerts.Count-1);
+
+                DMesh3.PokeTriangleInfo pokeInfo;
+                MeshResult result = Target.PokeTriangle(tid, out pokeInfo);
+                if (result != MeshResult.Ok)
+                    throw new Exception("shit");
+                int new_v = pokeInfo.new_vid;
+
+                Target.SetVertex(new_v, v.v);
+                v.vtx_id = new_v;
+                VIDToSegVtxMap[v.vtx_id] = v;
+                PointHash.InsertPoint(v.vtx_id, v.v);
+
+                // remove this triangles vtx list because it is no longer valid
+                FaceVertices.Remove(tid);
+
+                // update remaining verts
+                Index3i pokeEdges = pokeInfo.new_edges;
+                Index3i pokeTris = new Index3i(tid, pokeInfo.new_t1, pokeInfo.new_t2);
+                foreach ( SegmentVtx sv in triVerts ) {
+                    update_from_poke(sv, pokeEdges, pokeTris);
+                    if (sv.type == 1)
+                        add_edge_vtx(sv.elem_id, sv);
+                    else if (sv.type == 2)
+                        add_face_vtx(sv.elem_id, sv);
+                }
+
+                // track poke subfaces
+                add_poke_subfaces(tid, ref pokeInfo);
+            }
+        }
+
+
+
+        /// <summary>
+        /// figure out which vtx/edge/face the input vtx is on
+        /// </summary>
+        void update_from_poke(SegmentVtx sv, Index3i pokeEdges, Index3i pokeTris)
+        {
+            // check if within tolerance of existing vtx, because we did not 
+            // sort that out before...
+            int existing_v = find_existing_vertex(sv.v);
+            if (existing_v >= 0) {
+                sv.type = 0;
+                sv.elem_id = existing_v;
+                sv.vtx_id = existing_v;
+                VIDToSegVtxMap[sv.vtx_id] = sv;
+                return;
+            }
+
+            for ( int j = 0; j < 3; ++j ) {
+                if ( is_on_edge(pokeEdges[j], sv.v) ) {
+                    sv.type = 1;
+                    sv.elem_id = pokeEdges[j];
+                    return;
+                }
+            }
+
+            // [TODO] should use PrimalQuery2d for this!
+            for ( int j = 0; j < 3; ++j ) {
+                if ( is_in_triangle(pokeTris[j], sv.v) ) {
+                    sv.type = 2;
+                    sv.elem_id = pokeTris[j];
+                    return;
+                }
+            }
+
+            System.Console.WriteLine("unsorted vertex!");
+            sv.elem_id = pokeTris.a;
+        }
+
+
+
+
+        /// <summary>
+        /// for each on-edge vtx, we split the edge and then
+        /// re-sort any of the vertices on that edge onto new edges
+        /// </summary>
+        void insert_edge_vertices()
+        {
+            while (EdgeVertices.Count > 0) {
+                var pair = EdgeVertices.First();
+                int eid = pair.Key;
+                List<SegmentVtx> edgeVerts = pair.Value;
+                SegmentVtx v = edgeVerts[edgeVerts.Count - 1];
+                edgeVerts.RemoveAt(edgeVerts.Count - 1);
+
+                Index2i splitTris = Target.GetEdgeT(eid);
+
+                DMesh3.EdgeSplitInfo splitInfo;
+                MeshResult result = Target.SplitEdge(eid, out splitInfo);
+                if (result != MeshResult.Ok)
+                    throw new Exception("insert_edge_vertices: split failed!");
+                int new_v = splitInfo.vNew;
+                Index2i splitEdges = new Index2i(eid, splitInfo.eNewBN);
+
+                Target.SetVertex(new_v, v.v);
+                v.vtx_id = new_v;
+                VIDToSegVtxMap[v.vtx_id] = v;
+                PointHash.InsertPoint(v.vtx_id, v.v);
+
+                // remove this triangles vtx list because it is no longer valid
+                EdgeVertices.Remove(eid);
+
+                // update remaining verts
+                foreach (SegmentVtx sv in edgeVerts) {
+                    update_from_split(sv, splitEdges);
+                    if (sv.type == 1)
+                        add_edge_vtx(sv.elem_id, sv);
+                }
+
+                // track subfaces
+                add_split_subfaces(splitTris, ref splitInfo);
+
+            }
+        }
+
+
+
+        /// <summary>
+        /// figure out which vtx/edge the input vtx is on
+        /// </summary>
+        void update_from_split(SegmentVtx sv, Index2i splitEdges)
+        {
+            // check if within tolerance of existing vtx, because we did not 
+            // sort that out before...
+            int existing_v = find_existing_vertex(sv.v);
+            if (existing_v >= 0) {
+                sv.type = 0;
+                sv.elem_id = existing_v;
+                sv.vtx_id = existing_v;
+                VIDToSegVtxMap[sv.vtx_id] = sv;
+                return;
+            }
+
+            for (int j = 0; j < 2; ++j) {
+                if (is_on_edge(splitEdges[j], sv.v)) {
+                    sv.type = 1;
+                    sv.elem_id = splitEdges[j];
+                    return;
+                }
+            }
+
+            throw new Exception("update_from_split: unsortable vertex?");
+        }
+
+
+
+
+
+
+
+        /// <summary>
+        /// Make sure that all intersection segments are represented by
+        /// a connected chain of edges.
+        /// </summary>
+        void connect_edges()
+        {
+            int NS = Segments.Length;
+            for ( int si = 0; si < NS; ++si ) {
+                IntersectSegment seg = Segments[si];
+                if (seg.v0 == seg.v1)
+                    continue;       // degenerate!
+                if (seg.v0.vtx_id == seg.v1.vtx_id)
+                    continue;       // also degenerate and how does this happen?
+
+                int a = seg.v0.vtx_id, b = seg.v1.vtx_id;
+
+                if (a == DMesh3.InvalidID || b == DMesh3.InvalidID)
+                    throw new Exception("segment vertex is not defined?");
+                int eid = Target.FindEdge(a, b);
+                if (eid != DMesh3.InvalidID)
+                    continue;       // already connected
+
+                // TODO: in many cases there is an edge we added during a
+                // poke or split that we could flip to get edge AB. 
+                // this is much faster and we should do it where possible!
+                // HOWEVER we need to know which edges we can and cannot flip
+                // is_inserted_free_edge() should do this but not implemented yet
+                // possibly also requires that we do all these flips before any
+                // calls to insert_segment() !
+
+                try {
+                    insert_segment(seg);
+                } catch (Exception) {
+                    // ignore?
+                }
+            }
+        }
+
+
+        void insert_segment(IntersectSegment seg)
+        {
+            List<int> subfaces = get_all_baseface_tris(seg.base_tid);
+
+            RegionOperator op = new RegionOperator(Target, subfaces);
+
+            Vector3d n = BaseFaceNormals[seg.base_tid];
+            Vector3d c = BaseFaceCentroids[seg.base_tid];
+            Vector3d e0, e1;
+            Vector3d.MakePerpVectors(ref n, out e0, out e1);
+
+            DMesh3 mesh = op.Region.SubMesh;
+            MeshTransforms.PerVertexTransform(mesh, (v) => {
+                v -= c;
+                return new Vector3d(v.Dot(e0), v.Dot(e1), 0);
+            });
+
+            Vector3d end0 = seg.v0.v, end1 = seg.v1.v;
+            end0 -= c; end1 -= c;
+            Vector2d p0 = new Vector2d(end0.Dot(e0), end0.Dot(e1));
+            Vector2d p1 = new Vector2d(end1.Dot(e0), end1.Dot(e1));
+            PolyLine2d path = new PolyLine2d();
+            path.AppendVertex(p0); path.AppendVertex(p1);
+
+            MeshInsertUVPolyCurve insert = new MeshInsertUVPolyCurve(mesh, path);
+            insert.Apply();
+
+            MeshVertexSelection cutVerts = new MeshVertexSelection(mesh);
+            cutVerts.SelectEdgeVertices(insert.OnCutEdges);
+
+            MeshTransforms.PerVertexTransform(mesh, (v) => {
+                return c + v.x * e0 + v.y * e1;
+            });
+
+            op.BackPropropagate();
+
+            // add new cut vertices to cut list
+            foreach (int vid in cutVerts)
+                SegmentInsertVertices.Add(op.ReinsertSubToBaseMapV[vid]);
+
+            add_regionop_subfaces(seg.base_tid, op);
+        }
+
+
+
+
+
+        void add_edge_vtx(int eid, SegmentVtx vtx)
+        {
+            List<SegmentVtx> l;
+            if (EdgeVertices.TryGetValue(eid, out l)) {
+                l.Add(vtx);
+            } else {
+                l = new List<SegmentVtx>() { vtx };
+                EdgeVertices[eid] = l;
+            }
+        }
+
+        void add_face_vtx(int tid, SegmentVtx vtx)
+        {
+            List<SegmentVtx> l;
+            if (FaceVertices.TryGetValue(tid, out l)) {
+                l.Add(vtx);
+            } else {
+                l = new List<SegmentVtx>() { vtx };
+                FaceVertices[tid] = l;
+            }
+        }
+
+
+
+        void add_poke_subfaces(int tid, ref DMesh3.PokeTriangleInfo pokeInfo)
+        {
+            int parent = get_parent(tid);
+            HashSet<int> subfaces = get_subfaces(parent);
+            if (tid != parent)
+                add_subface(subfaces, parent, tid);
+            add_subface(subfaces, parent, pokeInfo.new_t1);
+            add_subface(subfaces, parent, pokeInfo.new_t2);
+        }
+        void add_split_subfaces(Index2i origTris, ref DMesh3.EdgeSplitInfo splitInfo)
+        {
+            int parent_1 = get_parent(origTris.a);
+            HashSet<int> subfaces_1 = get_subfaces(parent_1);
+            if (origTris.a != parent_1)
+                add_subface(subfaces_1, parent_1, origTris.a);
+            add_subface(subfaces_1, parent_1, splitInfo.eNewT2);
+
+            if ( origTris.b != DMesh3.InvalidID ) {
+                int parent_2 = get_parent(origTris.b);
+                HashSet<int> subfaces_2 = get_subfaces(parent_2);
+                if (origTris.b != parent_2)
+                    add_subface(subfaces_2, parent_2, origTris.b);
+                add_subface(subfaces_2, parent_2, splitInfo.eNewT3);
+            }
+        }
+        void add_regionop_subfaces(int parent, RegionOperator op)
+        {
+            HashSet<int> subfaces = get_subfaces(parent);
+            foreach (int tid in op.CurrentBaseTriangles) {
+                if (tid != parent)
+                    add_subface(subfaces, parent, tid);
+            }
+        }
+
+
+        int get_parent(int tid)
+        {
+            int parent;
+            if (ParentFaces.TryGetValue(tid, out parent) == false)
+                parent = tid;
+            return parent;
+        }
+        HashSet<int> get_subfaces(int parent)
+        {
+            HashSet<int> subfaces;
+            if (SubFaces.TryGetValue(parent, out subfaces) == false) {
+                subfaces = new HashSet<int>();
+                SubFaces[parent] = subfaces;
+            }
+            return subfaces;
+        }
+        void add_subface(HashSet<int> subfaces, int parent, int tid)
+        {
+            subfaces.Add(tid);
+            ParentFaces[tid] = parent;
+        }
+        List<int> get_all_baseface_tris(int base_tid)
+        {
+            List<int> faces = new List<int>(get_subfaces(base_tid));
+            faces.Add(base_tid);
+            return faces;
+        }
+
+        bool is_inserted_free_edge(int eid)
+        {
+            Index2i et = Target.GetEdgeT(eid);
+            if (get_parent(et.a) != get_parent(et.b))
+                return false;
+            // TODO need to check if we need to save edge AB to connect vertices!
+            throw new Exception("not done yet!");
+            return true;
+        }
+
+
+
+
+        protected int on_edge(ref Triangle3d tri, ref Vector3d v)
+        {
+            Segment3d s01 = new Segment3d(tri.V0, tri.V1);
+            if (s01.DistanceSquared(v) < VertexSnapTol * VertexSnapTol)
+                return 0;
+            Segment3d s12 = new Segment3d(tri.V1, tri.V2);
+            if (s12.DistanceSquared(v) < VertexSnapTol * VertexSnapTol)
+                return 1;
+            Segment3d s20 = new Segment3d(tri.V2, tri.V0);
+            if (s20.DistanceSquared(v) < VertexSnapTol * VertexSnapTol)
+                return 2;
+            return -1;
+        }
+        protected int on_edge_eid(int tid, Vector3d v)
+        {
+            Index3i tv = Target.GetTriangle(tid);
+            Triangle3d tri = new Triangle3d();
+            Target.GetTriVertices(tid, ref tri.V0, ref tri.V1, ref tri.V2);
+            int eidx = on_edge(ref tri, ref v);
+            if (eidx < 0)
+                return DMesh3.InvalidID;
+            int eid = Target.FindEdge(tv[eidx], tv[(eidx+1)%3]);
+            Util.gDevAssert(eid != DMesh3.InvalidID);
+            return eid;            
+        }
+        protected bool is_on_edge(int eid, Vector3d v)
+        {
+            Index2i ev = Target.GetEdgeV(eid);
+            Segment3d seg = new Segment3d(Target.GetVertex(ev.a), Target.GetVertex(ev.b));
+            return seg.DistanceSquared(v) < VertexSnapTol * VertexSnapTol;
+        }
+
+        protected bool is_in_triangle(int tid, Vector3d v)
+        {
+            Triangle3d tri = new Triangle3d();
+            Target.GetTriVertices(tid, ref tri.V0, ref tri.V1, ref tri.V2);
+            Vector3d bary = tri.BarycentricCoords(v);
+            return (bary.x >= 0 && bary.y >= 0 && bary.z >= 0
+                  && bary.x < 1 && bary.y <= 1 && bary.z <= 1);
+                
+        }
+
+
+
+        /// <summary>
+        /// find existing vertex at point, if it exists
+        /// </summary>
+        protected int find_existing_vertex(Vector3d pt)
+        {
+            return find_nearest_vertex(pt, VertexSnapTol);
+        }
+        /// <summary>
+        /// find closest vertex, within searchRadius
+        /// </summary>
+        protected int find_nearest_vertex(Vector3d pt, double searchRadius, int ignore_vid = -1)
+        {
+            KeyValuePair<int, double> found = (ignore_vid == -1) ?
+                PointHash.FindNearestInRadius(pt, searchRadius,
+                            (b) => { return pt.DistanceSquared(Target.GetVertex(b)); })
+                            :
+                PointHash.FindNearestInRadius(pt, searchRadius,
+                            (b) => { return pt.DistanceSquared(Target.GetVertex(b)); },
+                            (vid) => { return vid == ignore_vid; });
+            if (found.Key == PointHash.InvalidValue)
+                return -1;
+            return found.Key;
+        }
+
+
+
+    }
+}
diff --git a/mesh_ops/MeshRepairOrientation.cs b/mesh_ops/MeshRepairOrientation.cs
new file mode 100644
index 00000000..6b4a7ad5
--- /dev/null
+++ b/mesh_ops/MeshRepairOrientation.cs
@@ -0,0 +1,177 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using g3;
+
+namespace gs
+{
+	public class MeshRepairOrientation
+	{
+		public DMesh3 Mesh;
+
+		DMeshAABBTree3 spatial;
+		protected DMeshAABBTree3 Spatial {
+			get {
+				if (spatial == null)
+					spatial = new DMeshAABBTree3(Mesh, true);
+				return spatial; 
+			}
+		}
+
+		public MeshRepairOrientation(DMesh3 mesh3, DMeshAABBTree3 spatial = null)
+		{
+			Mesh = mesh3;
+			this.spatial = spatial;
+		}
+
+
+		class Component
+		{
+			public List<int> triangles;
+			public double outFacing;
+			public double inFacing;
+		}
+		List<Component> Components = new List<Component>();
+
+
+
+
+		// TODO:
+		//  - (in merge coincident) don't merge tris with same/opposite normals (option)
+		//  - after orienting components, try to find adjacent open components and
+		//    transfer orientation between them
+		//  - orient via nesting
+
+
+		public void OrientComponents()
+		{
+			Components = new List<Component>();
+
+			HashSet<int> remaining = new HashSet<int>(Mesh.TriangleIndices());
+			List<int> stack = new List<int>();
+			while (remaining.Count > 0) {
+				Component c = new Component();
+				c.triangles = new List<int>();
+
+				stack.Clear();
+				int start = remaining.First();
+				remaining.Remove(start);
+				c.triangles.Add(start);
+				stack.Add(start);
+				while (stack.Count > 0) {
+					int cur = stack[stack.Count - 1];
+					stack.RemoveAt(stack.Count - 1);
+					Index3i tcur = Mesh.GetTriangle(cur);
+
+					Index3i nbrs = Mesh.GetTriNeighbourTris(cur);
+					for (int j = 0; j < 3; ++j) {
+						int nbr = nbrs[j];
+						if (remaining.Contains(nbr) == false)
+							continue;
+
+						int a = tcur[j];
+						int b = tcur[(j+1)%3];
+
+						Index3i tnbr = Mesh.GetTriangle(nbr);
+						if (IndexUtil.find_tri_ordered_edge(b, a, ref tnbr) == DMesh3.InvalidID) {
+							Mesh.ReverseTriOrientation(nbr);
+						}
+						stack.Add(nbr);
+						remaining.Remove(nbr);
+						c.triangles.Add(nbr);
+					}
+
+				}
+
+				Components.Add(c);
+			}
+		}
+
+
+
+
+
+		public void ComputeStatistics()
+		{
+			var s = this.Spatial;  // make sure this exists
+            // Cannot do in parallel because we set a filter on spatial DS. 
+            // Also we are doing rays in parallel anyway...
+            foreach ( var c in Components ) {
+                compute_statistics(c);
+            }
+		}
+		void compute_statistics(Component c)
+		{
+			int NC = c.triangles.Count;
+			c.inFacing = c.outFacing = 0;
+            double dist = 2 * Mesh.CachedBounds.DiagonalLength;
+
+            // only want to raycast triangles in this 
+            HashSet<int> tris = new HashSet<int>(c.triangles);
+            spatial.TriangleFilterF = tris.Contains;
+
+            // We want to try to figure out what is 'outside' relative to the world.
+            // Assumption is that faces we can hit from far away should be oriented outwards.
+            // So, for each triangle we construct far-away points in positive and negative normal
+            // direction, then raycast back towards the triangle. If we hit the triangle from
+            // one side and not the other, that is evidence we should keep/reverse that triangle.
+            // If it is not hit, or hit from both, that does not provide any evidence.
+            // We collect up this keep/reverse evidence and use the larger to decide on the global orientation.
+
+            SpinLock count_lock = new SpinLock();
+
+			gParallel.BlockStartEnd(0, NC - 1, (a, b) => {
+				for (int i = a; i <= b; ++i) {
+					int ti = c.triangles[i];
+					Vector3d normal, centroid; double area;
+					Mesh.GetTriInfo(ti, out normal, out area, out centroid);
+					if (area < MathUtil.ZeroTolerancef)
+						continue;
+
+                    // construct far away points
+                    Vector3d pos_pt = centroid + dist * normal;
+                    Vector3d neg_pt = centroid - dist * normal;
+
+                    // raycast towards triangle from far-away point
+                    int hit_pos = spatial.FindNearestHitTriangle(new Ray3d(pos_pt, -normal));
+                    int hit_neg = spatial.FindNearestHitTriangle(new Ray3d(neg_pt, normal));
+                    if (hit_pos != ti && hit_neg != ti)
+                        continue;       // no evidence
+                    if (hit_pos == ti && hit_neg == ti)
+                        continue;       // no evidence (?)
+
+                    bool taken = false;
+                    count_lock.Enter(ref taken);
+
+                    if (hit_neg == ti)
+                        c.inFacing += area;
+                    else if (hit_pos == ti)
+                        c.outFacing += area;
+
+                    count_lock.Exit();
+				}
+			});
+
+            spatial.TriangleFilterF = null;
+        }
+
+
+
+		public void SolveGlobalOrientation()
+		{
+			ComputeStatistics();
+			MeshEditor editor = new MeshEditor(Mesh);
+			foreach (Component c in Components) {
+				if (c.inFacing > c.outFacing) {
+					editor.ReverseTriangles(c.triangles);
+				}
+			}
+		}
+
+
+
+	}
+}
diff --git a/mesh_ops/MeshSpatialSort.cs b/mesh_ops/MeshSpatialSort.cs
new file mode 100644
index 00000000..b2f74a12
--- /dev/null
+++ b/mesh_ops/MeshSpatialSort.cs
@@ -0,0 +1,292 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// This class sorts a set of mesh components.
+    /// </summary>
+    public class MeshSpatialSort
+    {
+        // ComponentMesh is a wrapper around input meshes
+        public List<ComponentMesh> Components;
+
+        // a MeshSolid is an "Outer" mesh and a set of "Cavity" meshes 
+        // (the cavity list includes contained open meshes, though)
+        public List<MeshSolid> Solids;
+
+        public bool AllowOpenContainers = false;
+        public double FastWindingIso = 0.5f;
+
+
+        public MeshSpatialSort()
+        {
+            Components = new List<ComponentMesh>();
+        }
+
+
+        public void AddMesh(DMesh3 mesh, object identifier, DMeshAABBTree3 spatial = null)
+        {
+            ComponentMesh comp = new ComponentMesh(mesh, identifier, spatial);
+            if (spatial == null) {
+                if (comp.IsClosed || AllowOpenContainers)
+                    comp.Spatial = new DMeshAABBTree3(mesh, true);
+            }
+
+            Components.Add(comp);
+        }
+
+
+
+
+        public class ComponentMesh
+        {
+            public object Identifier;
+            public DMesh3 Mesh;
+            public bool IsClosed;
+            public DMeshAABBTree3 Spatial;
+            public AxisAlignedBox3d Bounds;
+
+            // meshes that contain this one
+            public List<ComponentMesh> InsideOf = new List<ComponentMesh>();
+
+            // meshes that are inside of this one
+            public List<ComponentMesh> InsideSet = new List<ComponentMesh>();
+
+            public ComponentMesh(DMesh3 mesh, object identifier, DMeshAABBTree3 spatial)
+            {
+                this.Mesh = mesh;
+                this.Identifier = identifier;
+                this.IsClosed = mesh.IsClosed();
+                this.Spatial = spatial;
+                Bounds = mesh.CachedBounds;
+            }
+
+            public bool Contains(ComponentMesh mesh2, double fIso = 0.5f)
+            {
+                if (this.Spatial == null)
+                    return false;
+                // make sure FWN is available
+                this.Spatial.FastWindingNumber(Vector3d.Zero);
+
+                // block-parallel iteration provides a reasonable speedup
+                int NV = mesh2.Mesh.VertexCount;
+                bool contained = true;
+                gParallel.BlockStartEnd(0, NV - 1, (a, b) => {
+                    if (contained == false)
+                        return;
+                    for (int vi = a; vi <= b && contained; vi++) {
+                        Vector3d v = mesh2.Mesh.GetVertex(vi);
+                        if ( Math.Abs(Spatial.FastWindingNumber(v)) < fIso) { 
+                            contained = false;
+                            break;
+                        }
+                    }
+                }, 100);
+
+                return contained;
+            }
+        }
+
+
+
+        public class MeshSolid
+        {
+            public ComponentMesh Outer;
+            public List<ComponentMesh> Cavities = new List<ComponentMesh>();
+        }
+
+
+
+
+
+
+
+        public void Sort()
+        {
+            int N = Components.Count;
+
+            ComponentMesh[] comps = Components.ToArray();
+
+            // sort by bbox containment to speed up testing (does it??)
+            Array.Sort(comps, (i,j) => {
+                return i.Bounds.Contains(j.Bounds) ? -1 : 1;
+            });
+
+            // containment sets
+            bool[] bIsContained = new bool[N];
+            Dictionary<int, List<int>> ContainSets = new Dictionary<int, List<int>>();
+            Dictionary<int, List<int>> ContainedParents = new Dictionary<int, List<int>>();
+            SpinLock dataLock = new SpinLock();
+
+            // [TODO] this is 90% of compute time...
+            //   - if I know X contains Y, and Y contains Z, then I don't have to check that X contains Z
+            //   - can we exploit this somehow?
+            //   - if j contains i, then it cannot be that i contains j. But we are
+            //     not checking for this!  (although maybe bbox check still early-outs it?)
+
+            // construct containment sets
+            gParallel.ForEach(Interval1i.Range(N), (i) => {
+                ComponentMesh compi = comps[i];
+
+                if (compi.IsClosed == false && AllowOpenContainers == false)
+                    return;
+
+                for (int j = 0; j < N; ++j) {
+                    if (i == j)
+                        continue;
+                    ComponentMesh compj = comps[j];
+
+                    // cannot be contained if bounds are not contained
+                    if (compi.Bounds.Contains(compj.Bounds) == false)
+                        continue;
+
+                    // any other early-outs??
+                    if (compi.Contains(compj)) {
+
+                        bool entered = false;
+                        dataLock.Enter(ref entered);
+
+                        compj.InsideOf.Add(compi);
+                        compi.InsideSet.Add(compj);
+
+                        if (ContainSets.ContainsKey(i) == false)
+                            ContainSets.Add(i, new List<int>());
+                        ContainSets[i].Add(j);
+                        bIsContained[j] = true;
+                        if (ContainedParents.ContainsKey(j) == false)
+                            ContainedParents.Add(j, new List<int>());
+                        ContainedParents[j].Add(i);
+
+                        dataLock.Exit();
+                    }
+
+                }
+            });
+
+
+            List<MeshSolid> solids = new List<MeshSolid>();
+            HashSet<ComponentMesh> used = new HashSet<ComponentMesh>();
+
+            Dictionary<ComponentMesh, int> CompToOuterIndex = new Dictionary<ComponentMesh, int>();
+
+            List<int> ParentsToProcess = new List<int>();
+
+
+            // The following is a lot of code but it is very similar, just not clear how
+            // to refactor out the common functionality
+            //   1) we find all the top-level uncontained polys and add them to the final polys list
+            //   2a) for any poly contained in those parent-polys, that is not also contained in anything else,
+            //       add as hole to that poly
+            //   2b) remove all those used parents & holes from consideration
+            //   2c) now find all the "new" top-level polys
+            //   3) repeat 2a-c until done all polys
+            //   4) any remaining polys must be interior solids w/ no holes
+            //          **or** weird leftovers like intersecting polys...
+
+            // add all top-level uncontained polys
+            for (int i = 0; i < N; ++i) {
+                ComponentMesh compi = comps[i];
+                if (bIsContained[i])
+                    continue;
+
+                MeshSolid g = new MeshSolid() { Outer = compi };
+
+                int idx = solids.Count;
+                CompToOuterIndex[compi] = idx;
+                used.Add(compi);
+
+                if (ContainSets.ContainsKey(i))
+                    ParentsToProcess.Add(i);
+
+                solids.Add(g);
+            }
+
+
+            // keep iterating until we processed all parents
+            while (ParentsToProcess.Count > 0) {
+                List<int> ContainersToRemove = new List<int>();
+
+                // now for all top-level components that contain children, add those children
+                // as long as they do not have multiple contain-parents
+                foreach (int i in ParentsToProcess) {
+                    ComponentMesh parentComp = comps[i];
+                    int outer_idx = CompToOuterIndex[parentComp];
+
+                    List<int> children = ContainSets[i];
+                    foreach (int childj in children) {
+                        ComponentMesh childComp = comps[childj];
+                        Util.gDevAssert(used.Contains(childComp) == false);
+
+                        // skip multiply-contained children
+                        List<int> parents = ContainedParents[childj];
+                        if (parents.Count > 1)
+                            continue;
+
+                        solids[outer_idx].Cavities.Add(childComp);
+
+                        used.Add(childComp);
+                        if (ContainSets.ContainsKey(childj))
+                            ContainersToRemove.Add(childj);
+                    }
+                    ContainersToRemove.Add(i);
+                }
+
+                // remove all containers that are no longer valid
+                foreach (int ci in ContainersToRemove) {
+                    ContainSets.Remove(ci);
+
+                    // have to remove from each ContainedParents list
+                    List<int> keys = new List<int>(ContainedParents.Keys);
+                    foreach (int j in keys) {
+                        if (ContainedParents[j].Contains(ci))
+                            ContainedParents[j].Remove(ci);
+                    }
+                }
+
+                ParentsToProcess.Clear();
+
+                // ok now find next-level uncontained parents...
+                for (int i = 0; i < N; ++i) {
+                    ComponentMesh compi = comps[i];
+                    if (used.Contains(compi))
+                        continue;
+                    if (ContainSets.ContainsKey(i) == false)
+                        continue;
+                    List<int> parents = ContainedParents[i];
+                    if (parents.Count > 0)
+                        continue;
+
+                    MeshSolid g = new MeshSolid() { Outer = compi };
+
+                    int idx = solids.Count;
+                    CompToOuterIndex[compi] = idx;
+                    used.Add(compi);
+
+                    if (ContainSets.ContainsKey(i))
+                        ParentsToProcess.Add(i);
+
+                    solids.Add(g);
+                }
+            }
+
+
+            // any remaining components must be top-level
+            for (int i = 0; i < N; ++i) {
+                ComponentMesh compi = comps[i];
+                if (used.Contains(compi))
+                    continue;
+                MeshSolid g = new MeshSolid() { Outer = compi };
+                solids.Add(g);
+            }
+
+            Solids = solids;
+        }
+
+
+    }
+}
diff --git a/mesh_ops/MeshStitchLoops.cs b/mesh_ops/MeshStitchLoops.cs
new file mode 100644
index 00000000..3cde53f8
--- /dev/null
+++ b/mesh_ops/MeshStitchLoops.cs
@@ -0,0 +1,190 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// Stitch together two edge loops without any constraint that they have the same vertex count
+    /// (otherwise can use MeshEditor.StitchLoop / StitchUnorderedEdges).
+    /// 
+    /// [TODO]
+    ///    - something smarter than stitch_span_simple(). For example, equalize length we have
+    ///      travelled along the span. Could also use normals to try to keep span "smooth"
+    ///    - currently Loop0 and Loop1 need to be reversed/not depending on whether we are
+    ///      stitching "through" mesh or not. If not set properly, then fill self-intersects.
+    ///      Could we (optionally) resolve this automatically, eg by checking total of the two alternatives?
+    /// </summary>
+    public class MeshStitchLoops
+    {
+        public DMesh3 Mesh;
+        public EdgeLoop Loop0;
+        public EdgeLoop Loop1;
+
+        // if you are not sure that loops have correct order relative to
+        // existing boundary edges, set this to false and we will figure out ourselves
+        public bool TrustLoopOrientations = true;
+
+        public SetGroupBehavior Group = SetGroupBehavior.AutoGenerate;
+
+
+        // span represents an interval of loop indices on either side that
+        // need to be stitched together
+        struct span
+        {
+            public Interval1i span0;
+            public Interval1i span1;
+        }
+        List<span> spans = new List<span>();
+
+
+        public MeshStitchLoops(DMesh3 mesh, EdgeLoop l0, EdgeLoop l1)
+        {
+            Mesh = mesh;
+            Loop0 = new EdgeLoop(l0);
+            Loop1 = new EdgeLoop(l1);
+
+            span s = new span() {
+                span0 = new Interval1i(0, 0),
+                span1 = new Interval1i(0, 0)
+            };
+            spans.Add(s);
+        }
+
+
+        /// <summary>
+        /// specify subset of vertices that have known correspondences. 
+        /// </summary>
+        public void AddKnownCorrespondences(int[] verts0, int[] verts1)
+        {
+            int N = verts0.Length;
+            if (N != verts1.Length)
+                throw new Exception("MeshStitchLoops.AddKnownCorrespondence: lengths not the same!");
+
+            // construct list of pair correspondences as loop indices
+            List<Index2i> pairs = new List<Index2i>();
+            for ( int k = 0; k < N; ++k ) {
+                int i0 = Loop0.FindVertexIndex(verts0[k]);
+                int i1 = Loop1.FindVertexIndex(verts1[k]);
+                pairs.Add(new Index2i(i0, i1));
+            }
+
+            // sort by increasing index in loop0 (arbitrary)
+            pairs.Sort((pair1, pair2) => { return pair1.a.CompareTo(pair2.a); });
+
+            // now construct spans
+            List<span> new_spans = new List<span>();
+            for ( int k = 0; k < pairs.Count; ++k ) {
+                Index2i p1 = pairs[k];
+                Index2i p2 = pairs[(k + 1) % pairs.Count];
+                span s = new span() {
+                    span0 = new Interval1i(p1.a, p2.a),
+                    span1 = new Interval1i(p1.b, p2.b)
+                };
+                new_spans.Add(s);
+            }
+            spans = new_spans;
+        }
+
+
+
+
+        public bool Stitch()
+        {
+            if (spans.Count == 1)
+                throw new Exception("MeshStitchLoops.Stitch: blind stitching not supported yet...");
+
+            int gid = Group.GetGroupID(Mesh);
+
+            bool all_ok = true;
+
+            int NS = spans.Count;
+            for ( int si = 0; si < NS; si++ ) {
+                span s = spans[si];
+
+                if (stitch_span_simple(s, gid) == false)
+                    all_ok = false;
+            }
+
+            return all_ok;
+        }
+
+
+
+        /// <summary>
+        /// this just does back-and-forth zippering, of as many quads as possible, and
+        /// then a triangle-fan to finish whichever side is longer
+        /// </summary>
+        bool stitch_span_simple(span s, int gid)
+        {
+            bool all_ok = true;
+
+            int N0 = Loop0.Vertices.Length;
+            int N1 = Loop1.Vertices.Length;
+
+            // stitch as many quads as we can
+            int cur0 = s.span0.a, end0 = s.span0.b;
+            int cur1 = s.span1.a, end1 = s.span1.b;
+            while (cur0 != end0 && cur1 != end1) {
+                int next0 = (cur0 + 1) % N0;
+                int next1 = (cur1 + 1) % N1;
+
+                int a = Loop0.Vertices[cur0], b = Loop0.Vertices[next0];
+                int c = Loop1.Vertices[cur1], d = Loop1.Vertices[next1];
+                if (add_triangle(b, a, c, gid) == false)
+                    all_ok = false;
+                if (add_triangle(c, d, b, gid) == false)
+                    all_ok = false;
+
+                cur0 = next0;
+                cur1 = next1;
+            }
+
+            // now finish remaining verts on one side
+            int last_c = Loop1.Vertices[cur1];
+            while (cur0 != end0) {
+                int next0 = (cur0 + 1) % N0;
+                int a = Loop0.Vertices[cur0], b = Loop0.Vertices[next0];
+                if (add_triangle(b, a, last_c, gid) == false)
+                    all_ok = false;
+                cur0 = next0;
+            }
+
+            // or the other (only one of these two loops will happen)
+            int last_b = Loop0.Vertices[cur0];
+            while (cur1 != end1) {
+                int next1 = (cur1 + 1) % N1;
+                int c = Loop1.Vertices[cur1], d = Loop1.Vertices[next1];
+                if (add_triangle(c, d, last_b, gid) == false)
+                    all_ok = false;
+                cur1 = next1;
+            }
+
+            return all_ok;
+        }
+
+
+
+
+        bool add_triangle(int a, int b, int c, int gid)
+        {
+            int new_tid = DMesh3.InvalidID;
+            if (TrustLoopOrientations == false) {
+                int eid = Mesh.FindEdge(a, b);
+                Index2i ab = Mesh.GetOrientedBoundaryEdgeV(eid);
+                new_tid = Mesh.AppendTriangle(ab.b, ab.a, c, gid);
+            } else {
+                new_tid = Mesh.AppendTriangle(a, b, c, gid);
+            }
+            return (new_tid >= 0);
+        }
+
+
+
+
+    }
+}
diff --git a/mesh_ops/MeshTopology.cs b/mesh_ops/MeshTopology.cs
new file mode 100644
index 00000000..fc5512c5
--- /dev/null
+++ b/mesh_ops/MeshTopology.cs
@@ -0,0 +1,229 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// Extract topological information about the mesh based on identifying
+    /// semantic edges/vertices/etc
+    /// 
+    /// WIP
+    /// 
+    /// </summary>
+    public class MeshTopology
+    {
+        public DMesh3 Mesh;
+
+        double crease_angle = 30.0f;
+        public double CreaseAngle {
+            get { return crease_angle; }
+            set { crease_angle = value; invalidate_topology(); }
+        }
+
+        public MeshTopology(DMesh3 mesh)
+        {
+            Mesh = mesh;
+        }
+
+
+        public HashSet<int> BoundaryEdges;
+        public HashSet<int> CreaseEdges;
+        public HashSet<int> AllEdges;
+
+        public HashSet<int> AllVertices;
+        public HashSet<int> JunctionVertices;
+
+        public EdgeLoop[] Loops;
+        public EdgeSpan[] Spans;
+
+        int topo_timestamp = -1;
+        public bool IgnoreTimestamp = false;
+
+
+        /// <summary>
+        /// Compute the topology elements
+        /// </summary>
+        public void Compute()
+        {
+            validate_topology();
+        }
+
+
+        /// <summary>
+        /// add topological edges/vertices as constraints for remeshing 
+        /// </summary>
+        public void AddRemeshConstraints(MeshConstraints constraints)
+        {
+            validate_topology();
+
+            int set_index = 10;
+
+            foreach (EdgeSpan span in Spans) {
+                DCurveProjectionTarget target = new DCurveProjectionTarget(span.ToCurve());
+                MeshConstraintUtil.ConstrainVtxSpanTo(constraints, Mesh, span.Vertices, target, set_index++);
+            }
+
+            foreach (EdgeLoop loop in Loops) {
+                DCurveProjectionTarget target = new DCurveProjectionTarget(loop.ToCurve());
+                MeshConstraintUtil.ConstrainVtxLoopTo(constraints, Mesh, loop.Vertices, target, set_index++);
+            }
+
+            VertexConstraint corners = VertexConstraint.Pinned;
+            corners.FixedSetID = -1;
+            foreach (int vid in JunctionVertices) {
+                if (constraints.HasVertexConstraint(vid)) {
+                    VertexConstraint v = constraints.GetVertexConstraint(vid);
+                    v.Target = null;
+                    v.Fixed = true;
+                    v.FixedSetID = -1;
+                    constraints.SetOrUpdateVertexConstraint(vid, v);
+                } else {
+                    constraints.SetOrUpdateVertexConstraint(vid, corners);
+                }
+            }
+        }
+
+
+
+        void invalidate_topology()
+        {
+            topo_timestamp = -1;
+        }
+
+
+        void validate_topology()
+        {
+            if (IgnoreTimestamp && AllEdges != null)
+                return;
+
+            if ( Mesh.ShapeTimestamp != topo_timestamp ) {
+                find_crease_edges(CreaseAngle);
+                extract_topology();
+                topo_timestamp = Mesh.ShapeTimestamp;
+            }
+        }
+
+
+
+        void find_crease_edges(double angle_tol)
+        {
+            CreaseEdges = new HashSet<int>();
+            BoundaryEdges = new HashSet<int>();
+
+            double dot_tol = Math.Cos(angle_tol * MathUtil.Deg2Rad);
+
+            foreach ( int eid in Mesh.EdgeIndices() ) {
+                Index2i et = Mesh.GetEdgeT(eid);
+                if ( et.b == DMesh3.InvalidID ) {
+                    BoundaryEdges.Add(eid);
+                    continue;
+                }
+
+                Vector3d n0 = Mesh.GetTriNormal(et.a);
+                Vector3d n1 = Mesh.GetTriNormal(et.b);
+                if ( Math.Abs(n0.Dot(n1)) < dot_tol ) {
+                    CreaseEdges.Add(eid);
+                }
+            }
+
+            AllEdges = new HashSet<int>(CreaseEdges); ;
+            foreach ( int eid in BoundaryEdges ) 
+                AllEdges.Add(eid);
+
+            AllVertices = new HashSet<int>();
+            IndexUtil.EdgesToVertices(Mesh, AllEdges, AllVertices);
+        }
+
+
+
+
+
+        void extract_topology()
+        {
+            DGraph3 graph = new DGraph3();
+
+            // add vertices to graph, and store mappings
+            int[] mapV = new int[Mesh.MaxVertexID];
+            int[] mapVFrom = new int[AllVertices.Count];
+            foreach (int vid in AllVertices) {
+                int new_vid = graph.AppendVertex(Mesh.GetVertex(vid));
+                mapV[vid] = new_vid;
+                mapVFrom[new_vid] = vid;
+            }
+
+            // add edges to graph. graph-to-mesh eid mapping is stored via graph edge-group-id
+            int[] mapE = new int[Mesh.MaxEdgeID];
+            foreach (int eid in AllEdges) {
+                Index2i ev = Mesh.GetEdgeV(eid);
+                int new_a = mapV[ev.a];
+                int new_b = mapV[ev.b];
+                int new_eid = graph.AppendEdge(new_a, new_b, eid);
+                mapE[eid] = new_eid;
+            }
+
+            // extract the graph topology
+            DGraph3Util.Curves curves = DGraph3Util.ExtractCurves(graph, true);
+
+            // reconstruct mesh spans / curves / junctions from graph topology
+
+            int NP = curves.PathEdges.Count;
+            Spans = new EdgeSpan[NP];
+            for (int pi = 0; pi < NP; ++pi) {
+                List<int> pathE = curves.PathEdges[pi];
+                for (int k = 0; k < pathE.Count; ++k) {
+                    pathE[k] = graph.GetEdgeGroup(pathE[k]);
+                }
+                Spans[pi] =  EdgeSpan.FromEdges(Mesh, pathE);
+            }
+
+            int NL = curves.LoopEdges.Count;
+            Loops = new EdgeLoop[NL];
+            for (int li = 0; li < NL; ++li) {
+                List<int> loopE = curves.LoopEdges[li];
+                for (int k = 0; k < loopE.Count; ++k) {
+                    loopE[k] = graph.GetEdgeGroup(loopE[k]);
+                }
+                Loops[li] = EdgeLoop.FromEdges(Mesh, loopE);
+            }
+
+            JunctionVertices = new HashSet<int>();
+            foreach (int gvid in curves.JunctionV)
+                JunctionVertices.Add(mapVFrom[gvid]);
+        }
+
+
+
+
+
+        public DMesh3 MakeElementsMesh(Polygon2d spanProfile, Polygon2d loopProfile)
+        {
+            DMesh3 result = new DMesh3();
+            validate_topology();
+
+            foreach (EdgeSpan span in Spans) {
+                DCurve3 curve = span.ToCurve(Mesh);
+                TubeGenerator tubegen = new TubeGenerator(curve, spanProfile);
+                MeshEditor.Append(result, tubegen.Generate().MakeDMesh());
+            }
+
+            foreach (EdgeLoop loop in Loops) {
+                DCurve3 curve = loop.ToCurve(Mesh);
+                TubeGenerator tubegen = new TubeGenerator(curve, loopProfile);
+                MeshEditor.Append(result, tubegen.Generate().MakeDMesh());
+            }
+
+            return result;
+        }
+
+
+
+
+
+    }
+}
diff --git a/mesh_ops/MeshTrimLoop.cs b/mesh_ops/MeshTrimLoop.cs
new file mode 100644
index 00000000..70cbddaf
--- /dev/null
+++ b/mesh_ops/MeshTrimLoop.cs
@@ -0,0 +1,167 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace g3
+{
+
+    /// <summary>
+    /// Delete triangles inside on/near-surface trimming curve, and then adapt the new
+    /// boundary loop to conform to the loop.
+    /// 
+    /// [DANGER] To use this class, we require a spatial data structure we can project onto. 
+    /// Currently we assume that this is a DMesh3AABBTree *because* if you don't provide a
+    /// seed triangle, we use FindNearestTriangle() to find this index on the input mesh.
+    /// So, it must be a tree for the exact same mesh (!). 
+    /// However we then delete a bunch of triangles and use this spatial DS only for reprojection.
+    /// Possibly these should be two separate things? Or force caller to provide seed triangle
+    /// for trim loop, instead of solving this problem for them?
+    /// (But basically there is no way around having a full mesh copy...)
+    /// 
+    /// 
+    /// TODO:
+    /// - output boundary EdgeLoop that has been aligned w/ trim curve
+    /// - handle cases where input mesh has open borders
+    /// </summary>
+    public class MeshTrimLoop
+	{
+		public DMesh3 Mesh;
+        public DMeshAABBTree3 Spatial;
+        public DCurve3 TrimLine;
+
+        public int RemeshBorderRings = 2;
+        public double SmoothingAlpha = 1.0;     // valid range is [0,1]
+        public double TargetEdgeLength = 0;     // if 0, will use average border edge length
+        public int RemeshRounds = 20;
+
+        int seed_tri = -1;
+        Vector3d seed_pt = Vector3d.MaxValue;
+
+        /// <summary>
+        /// Cut mesh with plane. Assumption is that plane normal is Z value.
+        /// </summary>
+        public MeshTrimLoop(DMesh3 mesh, DCurve3 trimline, int tSeedTID, DMeshAABBTree3 spatial = null)
+		{
+            if (spatial != null && spatial.Mesh == mesh)
+                throw new ArgumentException("MeshTrimLoop: input spatial DS must have its own copy of mesh");
+			Mesh = mesh;
+            TrimLine = new DCurve3(trimline);
+            if (spatial != null) {
+                Spatial = spatial;
+            }
+            seed_tri = tSeedTID;
+		}
+
+        public MeshTrimLoop(DMesh3 mesh, DCurve3 trimline, Vector3d vSeedPt, DMeshAABBTree3 spatial = null)
+        {
+            if (spatial != null && spatial.Mesh == mesh)
+                throw new ArgumentException("MeshTrimLoop: input spatial DS must have its own copy of mesh");
+            Mesh = mesh;
+            TrimLine = new DCurve3(trimline);
+            if (spatial != null) {
+                Spatial = spatial;
+            }
+            seed_pt = vSeedPt;
+        }
+
+        public virtual ValidationStatus Validate()
+		{
+			// [TODO]
+			return ValidationStatus.Ok;
+		}
+
+
+		public virtual bool Trim()
+		{
+            if ( Spatial == null ) {
+                Spatial = new DMeshAABBTree3(new DMesh3(Mesh, false, MeshComponents.None));
+                Spatial.Build();
+            }
+
+            if ( seed_tri == -1 ) {
+                seed_tri = Spatial.FindNearestTriangle(seed_pt);
+            }
+
+            MeshFacesFromLoop loop = new MeshFacesFromLoop(Mesh, TrimLine, Spatial, seed_tri);
+
+            MeshFaceSelection selection = loop.ToSelection();
+            selection.LocalOptimize(true, true);
+            MeshEditor editor = new MeshEditor(Mesh);
+            editor.RemoveTriangles(selection, true);
+
+            MeshConnectedComponents components = new MeshConnectedComponents(Mesh);
+            components.FindConnectedT();
+            if ( components.Count > 1 ) {
+                int keep = components.LargestByCount;
+                for ( int i = 0; i < components.Count; ++i ) {
+                    if ( i != keep )
+                        editor.RemoveTriangles(components[i].Indices, true);
+                }
+            }
+            editor.RemoveAllBowtieVertices(true);
+
+            MeshBoundaryLoops loops = new MeshBoundaryLoops(Mesh);
+            bool loopsOK = false;
+            try {
+                loopsOK = loops.Compute();
+            } catch (Exception) {
+                return false;
+            }
+            if (!loopsOK)
+                return false;
+
+
+            // [TODO] to support trimming mesh w/ existing holes, we need to figure out which
+            // loop we created in RemoveTriangles above!
+            if (loops.Count > 1)
+                return false;
+
+
+            int[] loopVerts = loops[0].Vertices;
+
+            MeshFaceSelection borderTris = new MeshFaceSelection(Mesh);
+            borderTris.SelectVertexOneRings(loopVerts);
+            borderTris.ExpandToOneRingNeighbours(RemeshBorderRings);
+
+            RegionRemesher remesh = new RegionRemesher(Mesh, borderTris.ToArray());
+            remesh.Region.MapVerticesToSubmesh(loopVerts);
+
+            double target_len = TargetEdgeLength;
+            if (target_len <= 0) {
+                double mine, maxe, avge;
+                MeshQueries.EdgeLengthStatsFromEdges(Mesh, loops[0].Edges, out mine, out maxe, out avge);
+                target_len = avge;
+            }
+
+            MeshProjectionTarget meshTarget = new MeshProjectionTarget(Spatial.Mesh, Spatial);
+            remesh.SetProjectionTarget(meshTarget);
+            remesh.SetTargetEdgeLength(target_len);
+            remesh.SmoothSpeedT = SmoothingAlpha;
+
+            DCurveProjectionTarget curveTarget = new DCurveProjectionTarget(TrimLine);
+            SequentialProjectionTarget multiTarget = new SequentialProjectionTarget(curveTarget, meshTarget);
+
+            int set_id = 3;
+            MeshConstraintUtil.ConstrainVtxLoopTo(remesh, loopVerts, multiTarget, set_id);
+
+            for (int i = 0; i < RemeshRounds; ++i) {
+                remesh.BasicRemeshPass();
+            }
+
+            remesh.BackPropropagate();
+
+            // [TODO] output loop somehow...use MeshConstraints.FindConstrainedEdgesBySetID(set_id)...
+
+            return true;
+
+        } // Trim()
+
+
+
+        
+
+
+	}
+}
diff --git a/mesh_ops/MinimalHoleFill.cs b/mesh_ops/MinimalHoleFill.cs
new file mode 100644
index 00000000..703901f1
--- /dev/null
+++ b/mesh_ops/MinimalHoleFill.cs
@@ -0,0 +1,489 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// Construct a "minimal" fill surface for the hole. This surface
+    /// is often quasi-developable, reconstructs sharp edges, etc. 
+    /// There are various options.
+    /// </summary>
+    public class MinimalHoleFill
+    {
+        public DMesh3 Mesh;
+        public EdgeLoop FillLoop;
+
+        public bool IgnoreBoundaryTriangles = false;
+        public bool OptimizeDevelopability = true;
+        public bool OptimizeTriangles = true;
+        public double DevelopabilityTolerance = 0.0001;
+
+        /*
+         *  Outputs
+         */
+
+        /// <summary> Final fill vertices (should be empty?)</summary>
+        public int[] FillVertices;
+
+        /// <summary> Final fill triangles </summary>
+        public int[] FillTriangles;
+
+
+        public MinimalHoleFill(DMesh3 mesh, EdgeLoop fillLoop)
+        {
+            this.Mesh = mesh;
+            this.FillLoop = fillLoop;
+        }
+
+
+        RegionOperator regionop;
+        DMesh3 fillmesh;
+        HashSet<int> boundaryv;
+        Dictionary<int, double> exterior_angle_sums;
+
+        double[] curvatures;
+
+        public bool Apply()
+        {
+            // do a simple fill
+            SimpleHoleFiller simplefill = new SimpleHoleFiller(Mesh, FillLoop);
+            int fill_gid = Mesh.AllocateTriangleGroup();
+            bool bOK = simplefill.Fill(fill_gid);
+			if (bOK == false)
+				return false;
+
+            if (FillLoop.Vertices.Length <= 3) {
+                FillTriangles = simplefill.NewTriangles;
+                FillVertices = new int[0];
+                return true;
+            }
+
+            // extract the simple fill mesh as a submesh, via RegionOperator, so we can backsub later
+            HashSet<int> intial_fill_tris = new HashSet<int>(simplefill.NewTriangles);
+            regionop = new RegionOperator(Mesh, simplefill.NewTriangles,
+                (submesh) => { submesh.ComputeTriMaps = true; });
+            fillmesh = regionop.Region.SubMesh;
+
+            // for each boundary vertex, compute the exterior angle sum
+            // we will use this to compute gaussian curvature later
+            boundaryv = new HashSet<int>(MeshIterators.BoundaryEdgeVertices(fillmesh));
+            exterior_angle_sums = new Dictionary<int, double>();
+            if (IgnoreBoundaryTriangles == false) {
+                foreach (int sub_vid in boundaryv) {
+                    double angle_sum = 0;
+                    int base_vid = regionop.Region.MapVertexToBaseMesh(sub_vid);
+                    foreach (int tid in regionop.BaseMesh.VtxTrianglesItr(base_vid)) {
+                        if (intial_fill_tris.Contains(tid) == false) {
+                            Index3i et = regionop.BaseMesh.GetTriangle(tid);
+                            int idx = IndexUtil.find_tri_index(base_vid, ref et);
+                            angle_sum += regionop.BaseMesh.GetTriInternalAngleR(tid, idx);
+                        }
+                    }
+                    exterior_angle_sums[sub_vid] = angle_sum;
+                }
+            }
+
+
+            // try to guess a reasonable edge length that will give us enough geometry to work with in simplify pass
+            double loop_mine, loop_maxe, loop_avge, fill_mine, fill_maxe, fill_avge;
+            MeshQueries.EdgeLengthStatsFromEdges(Mesh, FillLoop.Edges, out loop_mine, out loop_maxe, out loop_avge);
+            MeshQueries.EdgeLengthStats(fillmesh, out fill_mine, out fill_maxe, out fill_avge);
+            double remesh_target_len = loop_avge;
+            if (fill_maxe / remesh_target_len > 10)
+                remesh_target_len = fill_maxe / 10;
+            //double remesh_target_len = Math.Min(loop_avge, fill_avge / 4);
+
+            // remesh up to target edge length, ideally gives us some triangles to work with
+            RemesherPro remesh1 = new RemesherPro(fillmesh);
+            remesh1.SmoothSpeedT = 1.0;
+            MeshConstraintUtil.FixAllBoundaryEdges(remesh1);
+            //remesh1.SetTargetEdgeLength(remesh_target_len / 2);       // would this speed things up? on large regions?
+            //remesh1.FastestRemesh();
+            remesh1.SetTargetEdgeLength(remesh_target_len);
+            remesh1.FastestRemesh();
+
+            /*
+             * first round: collapse to minimal mesh, while flipping to try to 
+             * get to ballpark minimal mesh. We stop these passes as soon as
+             * we have done two rounds where we couldn't do another collapse
+             * 
+             * This is the most unstable part of the algorithm because there
+             * are strong ordering effects. maybe we could sort the edges somehow??
+             */
+
+            int zero_collapse_passes = 0;
+            int collapse_passes = 0;
+            while (collapse_passes++ < 20 && zero_collapse_passes < 2) {
+
+                // collapse pass
+                int NE = fillmesh.MaxEdgeID;
+                int collapses = 0;
+                for (int ei = 0; ei < NE; ++ei) {
+                    if (fillmesh.IsEdge(ei) == false || fillmesh.IsBoundaryEdge(ei))
+                        continue;
+                    Index2i ev = fillmesh.GetEdgeV(ei);
+                    bool a_bdry = boundaryv.Contains(ev.a), b_bdry = boundaryv.Contains(ev.b);
+                    if (a_bdry && b_bdry)
+                        continue;
+                    int keepv = (a_bdry) ? ev.a : ev.b;
+                    int otherv = (keepv == ev.a) ? ev.b : ev.a;
+                    Vector3d newv = fillmesh.GetVertex(keepv);
+                    if (MeshUtil.CheckIfCollapseCreatesFlip(fillmesh, ei, newv))
+                        continue;
+                    DMesh3.EdgeCollapseInfo info;
+                    MeshResult result = fillmesh.CollapseEdge(keepv, otherv, out info);
+                    if (result == MeshResult.Ok)
+                        collapses++;
+                }
+                if (collapses == 0) zero_collapse_passes++; else zero_collapse_passes = 0;
+
+                // flip pass. we flip in these cases:
+                //  1) if angle between current triangles is too small (slightly more than 90 degrees, currently)
+                //  2) if angle between flipped triangles is smaller than between current triangles
+                //  3) if flipped edge length is shorter *and* such a flip won't flip the normal
+                NE = fillmesh.MaxEdgeID;
+                Vector3d n1, n2, on1, on2;
+                for (int ei = 0; ei < NE; ++ei) {
+                    if (fillmesh.IsEdge(ei) == false || fillmesh.IsBoundaryEdge(ei))
+                        continue;
+                    bool do_flip = false;
+
+                    Index2i ev = fillmesh.GetEdgeV(ei);
+                    MeshUtil.GetEdgeFlipNormals(fillmesh, ei, out n1, out n2, out on1, out on2);
+                    double dot_cur = n1.Dot(n2);
+                    double dot_flip = on1.Dot(on2);
+                    if (n1.Dot(n2) < 0.1 || dot_flip > dot_cur+MathUtil.Epsilonf)
+                        do_flip = true;
+
+                    if (do_flip == false) {
+                        Index2i otherv = fillmesh.GetEdgeOpposingV(ei);
+                        double len_e = fillmesh.GetVertex(ev.a).Distance(fillmesh.GetVertex(ev.b));
+                        double len_flip = fillmesh.GetVertex(otherv.a).Distance(fillmesh.GetVertex(otherv.b));
+                        if (len_flip < len_e) {
+                            if (MeshUtil.CheckIfEdgeFlipCreatesFlip(fillmesh, ei) == false)
+                                do_flip = true;
+                        }
+                    }
+
+                    if (do_flip) {
+                        DMesh3.EdgeFlipInfo info;
+                        MeshResult result = fillmesh.FlipEdge(ei, out info);
+                    }
+                }
+            }
+
+            // Sometimes, for some reason, we have a remaining interior vertex (have only ever seen one?) 
+            // Try to force removal of such vertices, even if it makes ugly mesh
+            remove_remaining_interior_verts();
+
+
+            // enable/disable passes. 
+            bool DO_FLATTER_PASS = true;
+            bool DO_CURVATURE_PASS = OptimizeDevelopability && true;
+            bool DO_AREA_PASS = OptimizeDevelopability && OptimizeTriangles && true;
+
+
+            /*
+             * In this pass we repeat the flipping iterations from the previous pass.
+             * 
+             * Note that because of the always-flip-if-dot-is-small case (commented),
+             * this pass will frequently not converge, as some number of edges will
+             * be able to flip back and forth (because neither has large enough dot).
+             * This is not ideal, but also, if we remove this behavior, then we
+             * generally get worse fills. This case basically introduces a sort of 
+             * randomization factor that lets us escape local minima...
+             * 
+             */
+
+            HashSet<int> remaining_edges = new HashSet<int>(fillmesh.EdgeIndices());
+            HashSet<int> updated_edges = new HashSet<int>();
+
+            int flatter_passes = 0;
+            int zero_flips_passes = 0;
+            while ( flatter_passes++ < 40 && zero_flips_passes < 2 && remaining_edges.Count() > 0 && DO_FLATTER_PASS) {
+                zero_flips_passes++;
+                foreach (int ei in remaining_edges) {
+                    if (fillmesh.IsBoundaryEdge(ei))
+                        continue;
+
+                    bool do_flip = false;
+
+                    Index2i ev = fillmesh.GetEdgeV(ei);
+                    Vector3d n1, n2, on1, on2;
+                    MeshUtil.GetEdgeFlipNormals(fillmesh, ei, out n1, out n2, out on1, out on2);
+                    double dot_cur = n1.Dot(n2);
+                    double dot_flip = on1.Dot(on2);
+                    if (flatter_passes < 20 && dot_cur < 0.1)   // this check causes oscillatory behavior
+                        do_flip = true;
+                    if (dot_flip > dot_cur + MathUtil.Epsilonf)
+                        do_flip = true;
+
+                    if (do_flip) {
+                        DMesh3.EdgeFlipInfo info;
+                        MeshResult result = fillmesh.FlipEdge(ei, out info);
+                        if (result == MeshResult.Ok) {
+                            zero_flips_passes = 0;
+                            add_all_edges(ei, updated_edges);
+                        }
+                    }
+                }
+
+                var tmp = remaining_edges;
+                remaining_edges = updated_edges;
+                updated_edges = tmp; updated_edges.Clear();
+            }
+
+
+            int curvature_passes = 0;
+            if (DO_CURVATURE_PASS) {
+
+                curvatures = new double[fillmesh.MaxVertexID];
+                foreach (int vid in fillmesh.VertexIndices())
+                    update_curvature(vid);
+
+                remaining_edges = new HashSet<int>(fillmesh.EdgeIndices());
+                updated_edges = new HashSet<int>();
+
+                /*
+                 *  In this pass we try to minimize gaussian curvature at all the vertices.
+                 *  This will recover sharp edges, etc, and do lots of good stuff.
+                 *  However, this pass will not make much progress if we are not already
+                 *  relatively close to a minimal mesh, so it really relies on the previous
+                 *  passes getting us in the ballpark.
+                 */
+                while (curvature_passes++ < 40 && remaining_edges.Count() > 0 && DO_CURVATURE_PASS) {
+                    foreach (int ei in remaining_edges) {
+                        if (fillmesh.IsBoundaryEdge(ei))
+                            continue;
+
+                        Index2i ev = fillmesh.GetEdgeV(ei);
+                        Index2i ov = fillmesh.GetEdgeOpposingV(ei);
+
+                        int find_other = fillmesh.FindEdge(ov.a, ov.b);
+                        if (find_other != DMesh3.InvalidID)
+                            continue;
+
+                        double total_curv_cur = curvature_metric_cached(ev.a, ev.b, ov.a, ov.b);
+                        if (total_curv_cur < MathUtil.ZeroTolerancef)
+                            continue;
+
+                        DMesh3.EdgeFlipInfo info;
+                        MeshResult result = fillmesh.FlipEdge(ei, out info);
+                        if (result != MeshResult.Ok)
+                            continue;
+
+                        double total_curv_flip = curvature_metric_eval(ev.a, ev.b, ov.a, ov.b);
+
+                        bool keep_flip = total_curv_flip < total_curv_cur - MathUtil.ZeroTolerancef;
+                        if (keep_flip == false) {
+                            result = fillmesh.FlipEdge(ei, out info);
+                        } else {
+                            update_curvature(ev.a); update_curvature(ev.b);
+                            update_curvature(ov.a); update_curvature(ov.b);
+                            add_all_edges(ei, updated_edges);
+                        }
+                    }
+                    var tmp = remaining_edges;
+                    remaining_edges = updated_edges;
+                    updated_edges = tmp; updated_edges.Clear();
+                }
+            }
+            //System.Console.WriteLine("collapse {0}   flatter {1}   curvature {2}", collapse_passes, flatter_passes, curvature_passes);
+
+            /*
+             * In this final pass, we try to improve triangle quality. We flip if
+             * the flipped triangles have better total aspect ratio, and the 
+             * curvature doesn't change **too** much. The .DevelopabilityTolerance
+             * parameter determines what is "too much" curvature change.
+             */
+            if (DO_AREA_PASS) {
+                remaining_edges = new HashSet<int>(fillmesh.EdgeIndices());
+                updated_edges = new HashSet<int>();
+                int area_passes = 0;
+                while (remaining_edges.Count() > 0 && area_passes < 20) {
+                    area_passes++;
+                    foreach (int ei in remaining_edges) {
+                        if (fillmesh.IsBoundaryEdge(ei))
+                            continue;
+
+                        Index2i ev = fillmesh.GetEdgeV(ei);
+                        Index2i ov = fillmesh.GetEdgeOpposingV(ei);
+
+                        int find_other = fillmesh.FindEdge(ov.a, ov.b);
+                        if (find_other != DMesh3.InvalidID)
+                            continue;
+
+                        double total_curv_cur = curvature_metric_cached(ev.a, ev.b, ov.a, ov.b);
+
+                        double a = aspect_metric(ei);
+                        if (a > 1)
+                            continue;
+
+                        DMesh3.EdgeFlipInfo info;
+                        MeshResult result = fillmesh.FlipEdge(ei, out info);
+                        if (result != MeshResult.Ok)
+                            continue;
+
+                        double total_curv_flip = curvature_metric_eval(ev.a, ev.b, ov.a, ov.b);
+
+                        bool keep_flip = Math.Abs(total_curv_cur - total_curv_flip) < DevelopabilityTolerance;
+                        if (keep_flip == false) {
+                            result = fillmesh.FlipEdge(ei, out info);
+                        } else {
+                            update_curvature(ev.a); update_curvature(ev.b);
+                            update_curvature(ov.a); update_curvature(ov.b);
+                            add_all_edges(ei, updated_edges);
+                        }
+                    }
+                    var tmp = remaining_edges;
+                    remaining_edges = updated_edges;
+                    updated_edges = tmp; updated_edges.Clear();
+                }
+            }
+
+
+            regionop.BackPropropagate();
+            FillTriangles = regionop.CurrentBaseTriangles;
+            FillVertices = regionop.CurrentBaseInteriorVertices().ToArray();
+
+            return true;
+
+        }
+
+
+
+
+
+        void remove_remaining_interior_verts()
+        {
+            HashSet<int> interiorv = new HashSet<int>(MeshIterators.InteriorVertices(fillmesh));
+            int prev_count = 0;
+            while (interiorv.Count > 0 && interiorv.Count != prev_count) {
+                prev_count = interiorv.Count;
+                int[] curv = interiorv.ToArray();
+                foreach (int vid in curv) {
+                    foreach (int e in fillmesh.VtxEdgesItr(vid)) {
+                        Index2i ev = fillmesh.GetEdgeV(e);
+                        int otherv = (ev.a == vid) ? ev.b : ev.a;
+                        DMesh3.EdgeCollapseInfo info;
+                        MeshResult result = fillmesh.CollapseEdge(otherv, vid, out info);
+                        if (result == MeshResult.Ok)
+                            break;
+                    }
+                    if (fillmesh.IsVertex(vid) == false)
+                        interiorv.Remove(vid);
+                }
+            }
+            if (interiorv.Count > 0)
+                Util.gBreakToDebugger();
+        }
+
+
+
+
+
+        void add_all_edges(int ei, HashSet<int> edge_set)
+        {
+            Index2i et = fillmesh.GetEdgeT(ei);
+            Index3i te = fillmesh.GetTriEdges(et.a);
+            edge_set.Add(te.a); edge_set.Add(te.b); edge_set.Add(te.c);
+            te = fillmesh.GetTriEdges(et.b);
+            edge_set.Add(te.a); edge_set.Add(te.b); edge_set.Add(te.c);
+        }
+
+
+
+        double area_metric(int eid)
+        {
+            Index3i ta, tb, ota, otb;
+            MeshUtil.GetEdgeFlipTris(fillmesh, eid, out ta, out tb, out ota, out otb);
+            double area_a = get_tri_area(fillmesh, ref ta);
+            double area_b = get_tri_area(fillmesh, ref tb);
+            double area_c = get_tri_area(fillmesh, ref ota);
+            double area_d = get_tri_area(fillmesh, ref otb);
+            double avg_ab = (area_a + area_b) * 0.5;
+            double avg_cd = (area_c + area_d) * 0.5;
+            double metric_ab = Math.Abs(area_a - avg_ab) + Math.Abs(area_b - avg_ab);
+            double metric_cd = Math.Abs(area_c - avg_cd) + Math.Abs(area_d - avg_cd);
+            return metric_cd / metric_ab;
+        }
+
+
+        double aspect_metric(int eid)
+        {
+            Index3i ta, tb, ota, otb;
+            MeshUtil.GetEdgeFlipTris(fillmesh, eid, out ta, out tb, out ota, out otb);
+            double aspect_a = get_tri_aspect(fillmesh, ref ta);
+            double aspect_b = get_tri_aspect(fillmesh, ref tb);
+            double aspect_c = get_tri_aspect(fillmesh, ref ota);
+            double aspect_d = get_tri_aspect(fillmesh, ref otb);
+            double metric_ab = Math.Abs(aspect_a - 1.0) + Math.Abs(aspect_b - 1.0);
+            double metric_cd = Math.Abs(aspect_c - 1.0) + Math.Abs(aspect_d - 1.0);
+            return metric_cd / metric_ab;
+        }
+
+
+        void update_curvature(int vid)
+        {
+            double angle_sum = 0;
+            exterior_angle_sums.TryGetValue(vid, out angle_sum);
+            foreach (int tid in fillmesh.VtxTrianglesItr(vid)) {
+                Index3i et = fillmesh.GetTriangle(tid);
+                int idx = IndexUtil.find_tri_index(vid, ref et);
+                angle_sum += fillmesh.GetTriInternalAngleR(tid, idx);
+            }
+            curvatures[vid] = angle_sum - MathUtil.TwoPI;
+        }
+        double curvature_metric_cached(int a, int b, int c, int d)
+        {
+            double defect_a = curvatures[a];
+            double defect_b = curvatures[b];
+            double defect_c = curvatures[c];
+            double defect_d = curvatures[d];
+            return Math.Abs(defect_a) + Math.Abs(defect_b) + Math.Abs(defect_c) + Math.Abs(defect_d);
+        }
+
+
+        double curvature_metric_eval(int a, int b, int c, int d)
+        {
+            double defect_a = compute_gauss_curvature(a);
+            double defect_b = compute_gauss_curvature(b);
+            double defect_c = compute_gauss_curvature(c);
+            double defect_d = compute_gauss_curvature(d);
+            return Math.Abs(defect_a) + Math.Abs(defect_b) + Math.Abs(defect_c) + Math.Abs(defect_d);
+        }
+
+        double compute_gauss_curvature(int vid)
+        {
+            double angle_sum = 0;
+            exterior_angle_sums.TryGetValue(vid, out angle_sum);
+            foreach (int tid in fillmesh.VtxTrianglesItr(vid)) {
+                Index3i et = fillmesh.GetTriangle(tid);
+                int idx = IndexUtil.find_tri_index(vid, ref et);
+                angle_sum += fillmesh.GetTriInternalAngleR(tid, idx);
+            }
+            return angle_sum - MathUtil.TwoPI;
+        }
+
+
+
+        Vector3d get_tri_normal(DMesh3 mesh, Index3i tri)
+        {
+            return MathUtil.Normal(mesh.GetVertex(tri.a), mesh.GetVertex(tri.b), mesh.GetVertex(tri.c));
+        }
+        double get_tri_area(DMesh3 mesh, ref Index3i tri)
+        {
+            return MathUtil.Area(mesh.GetVertex(tri.a), mesh.GetVertex(tri.b), mesh.GetVertex(tri.c));
+        }
+        double get_tri_aspect(DMesh3 mesh, ref Index3i tri)
+        {
+            return MathUtil.AspectRatio(mesh.GetVertex(tri.a), mesh.GetVertex(tri.b), mesh.GetVertex(tri.c));
+        }
+
+    }
+}
diff --git a/mesh_ops/PlanarHoleFiller.cs b/mesh_ops/PlanarHoleFiller.cs
index 74d8db56..ad4daaf8 100644
--- a/mesh_ops/PlanarHoleFiller.cs
+++ b/mesh_ops/PlanarHoleFiller.cs
@@ -5,6 +5,24 @@
 
 namespace g3
 {
+    /// <summary>
+    /// Try to fill planar holes in a mesh. The fill is computed by mapping the hole boundary into 2D,
+    /// filling using 2D algorithms, and then mapping back to 3D. This allows us to properly handle cases like
+    /// nested holes (eg from slicing a torus in half). 
+    /// 
+    /// PlanarComplex is used to sort the input 2D polyons. 
+    /// 
+    /// MeshInsertUVPolyCurve is used to insert each 2D polygon into a generated planar mesh.
+    /// The resolution of the generated mesh is controlled by .FillTargetEdgeLen
+    /// 
+    /// In theory this approach can handle more geometric degeneracies than Delaunay triangluation.
+    /// However, the current code requires that MeshInsertUVPolyCurve produce output boundary loops that
+    /// have a 1-1 correspondence with the input polygons. This is not always possible.
+    /// 
+    /// Currently these failure cases are not handled properly. In that case the loops will
+    /// not be stitched.
+    /// 
+    /// </summary>
     public class PlanarHoleFiller
     {
         public DMesh3 Mesh;
@@ -18,6 +36,20 @@ public class PlanarHoleFiller
         /// </summary>
         public double FillTargetEdgeLen = double.MaxValue;
 
+        /// <summary>
+        /// in some cases fill can succeed but we can't merge w/o creating holes. In
+        /// such cases it might be better to not merge at all...
+        /// </summary>
+        public bool MergeFillBoundary = true;
+
+
+        /*
+         * Error feedback
+         */
+        public bool OutputHasCracks = false;
+        public int FailedInsertions = 0;
+        public int FailedMerges = 0;
+
         // these will be computed if you don't set them
         Vector3d PlaneX, PlaneY;
 
@@ -70,6 +102,11 @@ public void AddFillLoops(IEnumerable<EdgeLoop> loops)
         }
 
 
+        /// <summary>
+        /// Compute the fill mesh and append it.
+        /// This returns false if anything went wrong. 
+        /// The Error Feedback properties (.OutputHasCracks, etc) will provide more info.
+        /// </summary>
         public bool Fill()
         {
             compute_polygons();
@@ -148,7 +185,8 @@ public bool Fill()
                         if (insert.Apply()) {
                             insert.Simplify();
                             polyVertices[pi] = insert.CurveVertices;
-                            failed = false;
+                            failed = (insert.Loops.Count != 1) ||
+                                     (insert.Loops[0].VertexCount != polys[pi].VertexCount);
                         }
                     }
                     if (failed)
@@ -181,32 +219,30 @@ public bool Fill()
                 //Util.WriteDebugMesh(MeshEditor.Combine(FillMesh, Mesh), "c:\\scratch\\FILLED_MESH.obj");
 
                 // figure out map between new mesh and original edge loops
-                // [TODO] if # of verts is different, we can still find correspondence, it is just harder
                 // [TODO] should check that edges (ie sequential verts) are boundary edges on fill mesh
                 //    if not, can try to delete nbr tris to repair
                 IndexMap mergeMapV = new IndexMap(true);
-                for ( int pi = 0; pi < polys.Count; ++pi ) {
-                    if (polyVertices[pi] == null)
-                        continue;
-                    int[] fillLoopVerts = polyVertices[pi];
-                    int NV = fillLoopVerts.Length;
-
-                    PlanarComplex.Element sourceElem = (pi == 0) ? gsolid.Outer : gsolid.Holes[pi - 1];
-                    int loopi = ElemToLoopMap[sourceElem];
-                    EdgeLoop sourceLoop = Loops[loopi].edgeLoop;
-
-                    if (sourceLoop.VertexCount != NV) {
-                        failed_merges.Add(new Index2i(fi, pi));
-                        continue;
-                    }
-
-                    for ( int k = 0; k < NV; ++k ) {
-                        Vector3d fillV = FillMesh.GetVertex(fillLoopVerts[k]);
-                        Vector3d sourceV = Mesh.GetVertex(sourceLoop.Vertices[k]);
-                        if (fillV.Distance(sourceV) < MathUtil.ZeroTolerancef)
-                            mergeMapV[fillLoopVerts[k]] = sourceLoop.Vertices[k];
+                if (MergeFillBoundary) {
+                    for (int pi = 0; pi < polys.Count; ++pi) {
+                        if (polyVertices[pi] == null)
+                            continue;
+                        int[] fillLoopVerts = polyVertices[pi];
+                        int NV = fillLoopVerts.Length;
+
+                        PlanarComplex.Element sourceElem = (pi == 0) ? gsolid.Outer : gsolid.Holes[pi - 1];
+                        int loopi = ElemToLoopMap[sourceElem];
+                        EdgeLoop sourceLoop = Loops[loopi].edgeLoop;
+
+                        // construct vertex-merge map for this loop
+                        List<int> bad_indices = build_merge_map(FillMesh, fillLoopVerts, Mesh, sourceLoop.Vertices,
+                            MathUtil.ZeroTolerancef, mergeMapV);
+
+                        bool errors = (bad_indices != null && bad_indices.Count > 0);
+                        if (errors) {
+                            failed_inserts.Add(new Index2i(fi, pi));
+                            OutputHasCracks = true;
+                        }
                     }
-
                 }
 
                 // append this fill to input mesh
@@ -214,9 +250,12 @@ public bool Fill()
                 int[] mapV;
                 editor.AppendMesh(FillMesh, mergeMapV, out mapV, Mesh.AllocateTriangleGroup());
 
-                // [TODO] should verify that we actually merged the loops...
+                // [TODO] should verify that we actually merged all the loops. If there are bad_indices
+                // we could fill them
             }
 
+            FailedInsertions = failed_inserts.Count;
+            FailedMerges = failed_merges.Count;
             if (failed_inserts.Count > 0 || failed_merges.Count > 0)
                 return false;
 
@@ -225,6 +264,101 @@ public bool Fill()
 
 
 
+
+        /// <summary>
+        /// Construct vertex correspondences between fill mesh boundary loop
+        /// and input mesh boundary loop. In ideal case there is an easy 1-1 
+        /// correspondence. If that is not true, then do a brute-force search
+        /// to find the best correspondences we can.
+        /// 
+        /// Currently only returns unique correspondences. If any vertex
+        /// matches with multiple input vertices it is not merged. 
+        /// [TODO] we could do better in many cases...
+        /// 
+        /// Return value is list of indices into fillLoopV that were not merged
+        /// </summary>
+        List<int> build_merge_map(DMesh3 fillMesh, int[] fillLoopV,
+                             DMesh3 targetMesh, int[] targetLoopV,
+                             double tol, IndexMap mergeMapV)
+        {
+            if (fillLoopV.Length == targetLoopV.Length) {
+                if (build_merge_map_simple(fillMesh, fillLoopV, targetMesh, targetLoopV, tol, mergeMapV))
+                    return null;
+            }
+
+            int NF = fillLoopV.Length, NT = targetLoopV.Length;
+            bool[] doneF = new bool[NF], doneT = new bool[NT];
+            int[] countF = new int[NF], countT = new int[NT];
+            List<int> errorV = new List<int>();
+
+            SmallListSet matchF = new SmallListSet(); matchF.Resize(NF);
+
+            // find correspondences
+            double tol_sqr = tol*tol;
+            for (int i = 0; i < NF; ++i ) {
+                if ( fillMesh.IsVertex(fillLoopV[i]) == false ) {
+                    doneF[i] = true;
+                    errorV.Add(i);
+                    continue;
+                }
+                matchF.AllocateAt(i);
+                Vector3d v = fillMesh.GetVertex(fillLoopV[i]);
+                for ( int j = 0; j < NT; ++j ) {
+                    Vector3d v2 = targetMesh.GetVertex(targetLoopV[j]);
+                    if ( v.DistanceSquared(ref v2) < tol_sqr ) {
+                        matchF.Insert(i, j);
+                    }
+                }
+            }
+
+            for ( int i = 0; i < NF; ++i ) {
+                if (doneF[i]) continue;
+                if ( matchF.Count(i) == 1 ) {
+                    int j = matchF.First(i);
+                    mergeMapV[fillLoopV[i]] = targetLoopV[j];
+                    doneF[i] = true;
+                }
+            }
+
+            for ( int i = 0; i < NF; ++i ) {
+                if (doneF[i] == false)
+                    errorV.Add(i);
+            }
+
+            return errorV;
+        }
+
+
+
+
+        /// <summary>
+        /// verifies that there is a 1-1 correspondence between the fill and target loops.
+        /// If so, adds to mergeMapV and returns true;
+        /// </summary>
+        bool build_merge_map_simple(DMesh3 fillMesh, int[] fillLoopV, 
+                                    DMesh3 targetMesh, int[] targetLoopV, 
+                                    double tol, IndexMap mergeMapV )
+        {
+            if (fillLoopV.Length != targetLoopV.Length)
+                return false;
+            int NV = fillLoopV.Length;
+            for (int k = 0; k < NV; ++k) {
+                if (!fillMesh.IsVertex(fillLoopV[k]))
+                    return false;
+                Vector3d fillV = fillMesh.GetVertex(fillLoopV[k]);
+                Vector3d sourceV = Mesh.GetVertex(targetLoopV[k]);
+                if (fillV.Distance(sourceV) > tol)
+                    return false;
+            }
+            for (int k = 0; k < NV; ++k)
+                mergeMapV[fillLoopV[k]] = targetLoopV[k];
+            return true;
+        }
+
+
+
+
+
         void compute_polygons()
         {
             Bounds = AxisAlignedBox2d.Empty;
diff --git a/mesh_ops/PlanarSpansFiller.cs b/mesh_ops/PlanarSpansFiller.cs
new file mode 100644
index 00000000..245e7cf5
--- /dev/null
+++ b/mesh_ops/PlanarSpansFiller.cs
@@ -0,0 +1,210 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace g3
+{
+    /// <summary>
+    /// This class fills an ordered sequence of planar spans. The 2D polygon is formed
+    /// by chaining the spans.
+    /// 
+    /// Current issues:
+    ///    - connectors have a single segment, so when simplified, they become a single edge.
+    ///      should subsample them instead.
+    ///    - currently mapping from inserted edges back to span edges is not calculated, so
+    ///      we have no way to merge them (ie MergeFillBoundary not implemented)
+    ///    - fill triangles not returned?
+    ///   
+    /// 
+    /// </summary>
+    public class PlanarSpansFiller
+    {
+        public DMesh3 Mesh;
+
+        public Vector3d PlaneOrigin;
+        public Vector3d PlaneNormal;
+
+        /// <summary>
+        /// fill mesh will be tessellated to this length, set to
+        /// double.MaxValue to use zero-length tessellation
+        /// </summary>
+        public double FillTargetEdgeLen = double.MaxValue;
+
+        /// <summary>
+        /// in some cases fill can succeed but we can't merge w/o creating holes. In
+        /// such cases it might be better to not merge at all...
+        /// </summary>
+        public bool MergeFillBoundary = false;
+
+        // these will be computed if you don't set them
+        Vector3d PlaneX, PlaneY;
+
+
+        List<EdgeSpan> FillSpans;
+        Polygon2d SpansPoly;
+        AxisAlignedBox2d Bounds;
+
+        public PlanarSpansFiller(DMesh3 mesh, IList<EdgeSpan> spans)
+        {
+            Mesh = mesh;
+            FillSpans = new List<EdgeSpan>(spans);
+            Bounds = AxisAlignedBox2d.Empty;
+        }
+
+        public void SetPlane(Vector3d origin, Vector3d normal)
+        {
+            PlaneOrigin = origin;
+            PlaneNormal = normal;
+            Vector3d.ComputeOrthogonalComplement(1, PlaneNormal, ref PlaneX, ref PlaneY);
+        }
+        public void SetPlane(Vector3d origin, Vector3d normal, Vector3d planeX, Vector3d planeY)
+        {
+            PlaneOrigin = origin;
+            PlaneNormal = normal;
+            PlaneX = planeX;
+            PlaneY = planeY;
+        }
+
+
+        public bool Fill()
+        {
+            compute_polygon();
+
+            // translate/scale fill loops to unit box. This will improve
+            // accuracy in the calcs below...
+            Vector2d shiftOrigin = Bounds.Center;
+            double scale = 1.0 / Bounds.MaxDim;
+            SpansPoly.Translate(-shiftOrigin);
+            SpansPoly.Scale(scale * Vector2d.One, Vector2d.Zero);
+
+            Dictionary<PlanarComplex.Element, int> ElemToLoopMap = new Dictionary<PlanarComplex.Element, int>();
+
+            // generate planar mesh that we will insert polygons into
+            MeshGenerator meshgen;
+            float planeW = 1.5f;
+            int nDivisions = 0;
+            if ( FillTargetEdgeLen < double.MaxValue && FillTargetEdgeLen > 0) {
+                int n = (int)((planeW / (float)scale) / FillTargetEdgeLen) + 1;
+                nDivisions = (n <= 1) ? 0 : n;
+            }
+
+            if (nDivisions == 0) {
+                meshgen = new TrivialRectGenerator() {
+                    IndicesMap = new Index2i(1, 2), Width = planeW, Height = planeW,
+                };
+            } else {
+                meshgen = new GriddedRectGenerator() {
+                    IndicesMap = new Index2i(1, 2), Width = planeW, Height = planeW,
+                    EdgeVertices = nDivisions
+                };
+            }
+            DMesh3 FillMesh = meshgen.Generate().MakeDMesh();
+            FillMesh.ReverseOrientation();   // why?!?
+
+            int[] polyVertices = null;
+
+            // insert each poly
+            MeshInsertUVPolyCurve insert = new MeshInsertUVPolyCurve(FillMesh, SpansPoly);
+            ValidationStatus status = insert.Validate(MathUtil.ZeroTolerancef * scale);
+            bool failed = true;
+            if (status == ValidationStatus.Ok) {
+                if (insert.Apply()) {
+                    insert.Simplify();
+                    polyVertices = insert.CurveVertices;
+                    failed = false;
+                }
+            }
+            if (failed)
+                return false;
+            
+            // remove any triangles not contained in gpoly
+            // [TODO] degenerate triangle handling? may be 'on' edge of gpoly...
+            List<int> removeT = new List<int>();
+            foreach (int tid in FillMesh.TriangleIndices()) {
+                Vector3d v = FillMesh.GetTriCentroid(tid);
+                if ( SpansPoly.Contains(v.xy) == false)
+                    removeT.Add(tid);
+            }
+            foreach (int tid in removeT)
+                FillMesh.RemoveTriangle(tid, true, false);
+
+            //Util.WriteDebugMesh(FillMesh, "c:\\scratch\\CLIPPED_MESH.obj");
+
+            // transform fill mesh back to 3d
+            MeshTransforms.PerVertexTransform(FillMesh, (v) => {
+                Vector2d v2 = v.xy;
+                v2 /= scale;
+                v2 += shiftOrigin;
+                return to3D(v2);
+            });
+
+
+            //Util.WriteDebugMesh(FillMesh, "c:\\scratch\\PLANAR_MESH_WITH_LOOPS.obj");
+            //Util.WriteDebugMesh(MeshEditor.Combine(FillMesh, Mesh), "c:\\scratch\\FILLED_MESH.obj");
+
+            // figure out map between new mesh and original edge loops
+            // [TODO] if # of verts is different, we can still find correspondence, it is just harder
+            // [TODO] should check that edges (ie sequential verts) are boundary edges on fill mesh
+            //    if not, can try to delete nbr tris to repair
+            IndexMap mergeMapV = new IndexMap(true);
+            if (MergeFillBoundary && polyVertices != null) {
+                throw new NotImplementedException("PlanarSpansFiller: merge fill boundary not implemented!");
+
+                //int[] fillLoopVerts = polyVertices;
+                //int NV = fillLoopVerts.Length;
+
+                //PlanarComplex.Element sourceElem = (pi == 0) ? gsolid.Outer : gsolid.Holes[pi - 1];
+                //int loopi = ElemToLoopMap[sourceElem];
+                //EdgeLoop sourceLoop = Loops[loopi].edgeLoop;
+
+                //for (int k = 0; k < NV; ++k) {
+                //    Vector3d fillV = FillMesh.GetVertex(fillLoopVerts[k]);
+                //    Vector3d sourceV = Mesh.GetVertex(sourceLoop.Vertices[k]);
+                //    if (fillV.Distance(sourceV) < MathUtil.ZeroTolerancef)
+                //        mergeMapV[fillLoopVerts[k]] = sourceLoop.Vertices[k];
+                //}
+            }
+
+            // append this fill to input mesh
+            MeshEditor editor = new MeshEditor(Mesh);
+            int[] mapV;
+            editor.AppendMesh(FillMesh, mergeMapV, out mapV, Mesh.AllocateTriangleGroup());
+
+            // [TODO] should verify that we actually merged the loops...
+
+            return true;
+        }
+
+
+
+        void compute_polygon()
+        {
+            SpansPoly = new Polygon2d();
+            for ( int i = 0; i < FillSpans.Count; ++i ) {
+                foreach (int vid in FillSpans[i].Vertices) {
+                    Vector2d v = to2D(Mesh.GetVertex(vid));
+                    SpansPoly.AppendVertex(v);
+                }
+            }
+
+            Bounds = SpansPoly.Bounds;
+        }
+
+
+        Vector2d to2D(Vector3d v)
+        {
+            Vector3d dv = v - PlaneOrigin;
+            dv -= dv.Dot(PlaneNormal) * PlaneNormal;
+            return new Vector2d(PlaneX.Dot(dv), PlaneY.Dot(dv));
+        }
+
+        Vector3d to3D(Vector2d v)
+        {
+            return PlaneOrigin + PlaneX * v.x + PlaneY * v.y;
+        }
+
+    }
+}
diff --git a/mesh_ops/RegionOperator.cs b/mesh_ops/RegionOperator.cs
index efe3c29e..b90f3737 100644
--- a/mesh_ops/RegionOperator.cs
+++ b/mesh_ops/RegionOperator.cs
@@ -30,19 +30,25 @@ public class RegionOperator
 
         int[] cur_base_tris;
 
-        public RegionOperator(DMesh3 mesh, int[] regionTris)
+        public RegionOperator(DMesh3 mesh, int[] regionTris, Action<DSubmesh3> submeshConfigF = null)
         {
             BaseMesh = mesh;
-            Region = new DSubmesh3(mesh, regionTris);
+            Region = new DSubmesh3(mesh);
+            if (submeshConfigF != null)
+                submeshConfigF(Region);
+            Region.Compute(regionTris);
             Region.ComputeBoundaryInfo(regionTris);
 
             cur_base_tris = (int[])regionTris.Clone();
         }
 
-        public RegionOperator(DMesh3 mesh, IEnumerable<int> regionTris)
+        public RegionOperator(DMesh3 mesh, IEnumerable<int> regionTris, Action<DSubmesh3> submeshConfigF = null)
         {
             BaseMesh = mesh;
-            Region = new DSubmesh3(mesh, regionTris);
+            Region = new DSubmesh3(mesh);
+            if (submeshConfigF != null)
+                submeshConfigF(Region);
+            Region.Compute(regionTris);
             int count = regionTris.Count();
             Region.ComputeBoundaryInfo(regionTris, count);
 
@@ -59,6 +65,22 @@ public int[] CurrentBaseTriangles {
         }
 
 
+        /// <summary>
+        /// find base-mesh interior vertices of region (ie does not include region boundary vertices)
+        /// </summary>
+        public HashSet<int> CurrentBaseInteriorVertices()
+        {
+            HashSet<int> verts = new HashSet<int>();
+            IndexHashSet borderv = Region.BaseBorderV;
+            foreach ( int tid in cur_base_tris ) {
+                Index3i tv = BaseMesh.GetTriangle(tid);
+                if (borderv[tv.a] == false) verts.Add(tv.a);
+                if (borderv[tv.b] == false) verts.Add(tv.b);
+                if (borderv[tv.c] == false) verts.Add(tv.c);
+            }
+            return verts;
+        }
+
         // After remeshing we may create an internal edge between two boundary vertices [a,b].
         // Those vertices will be merged with vertices c and d in the base mesh. If the edge
         // [c,d] already exists in the base mesh, then after the merge we would have at least
diff --git a/mesh_ops/RemoveDuplicateTriangles.cs b/mesh_ops/RemoveDuplicateTriangles.cs
new file mode 100644
index 00000000..d06b3453
--- /dev/null
+++ b/mesh_ops/RemoveDuplicateTriangles.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using g3;
+
+namespace gs
+{
+	/// <summary>
+	/// Remove duplicate triangles.
+	/// </summary>
+	public class RemoveDuplicateTriangles
+	{
+		public DMesh3 Mesh;
+
+		public double VertexTolerance = MathUtil.ZeroTolerancef;
+        public bool CheckOrientation = true;
+
+        public int Removed = 0;
+
+		public RemoveDuplicateTriangles(DMesh3 mesh)
+		{
+			Mesh = mesh;
+		}
+
+
+		public virtual bool Apply() {
+            Removed = 0;
+
+            double merge_r2 = VertexTolerance * VertexTolerance;
+
+            // construct hash table for edge midpoints
+            TriCentroids pointset = new TriCentroids() { Mesh = this.Mesh };
+			PointSetHashtable hash = new PointSetHashtable(pointset);
+            int hashN = (Mesh.TriangleCount > 100000) ? 128 : 64;
+			hash.Build(hashN);
+
+            Vector3d a = Vector3d.Zero, b = Vector3d.Zero, c = Vector3d.Zero;
+            Vector3d x = Vector3d.Zero, y = Vector3d.Zero, z = Vector3d.Zero;
+
+            int MaxTriID = Mesh.MaxTriangleID;
+
+			// remove duplicate triangles
+			int[] buffer = new int[1024];
+            for ( int tid = 0; tid < MaxTriID; ++tid ) {
+                if (!Mesh.IsTriangle(tid))
+                    continue;
+
+                Vector3d centroid = Mesh.GetTriCentroid(tid);
+				int N;
+				while (hash.FindInBall(centroid, VertexTolerance, buffer, out N) == false)
+					buffer = new int[buffer.Length];
+				if (N == 1 && buffer[0] != tid)
+					throw new Exception("RemoveDuplicateTriangles.Apply: how could this happen?!");
+				if (N <= 1)
+					continue;  // unique edge
+
+				Mesh.GetTriVertices(tid, ref a, ref b, ref c);
+                Vector3d n = MathUtil.Normal(a, b, c);
+
+				for (int i = 0; i < N; ++i) {
+					if (buffer[i] != tid) {
+                        Mesh.GetTriVertices(buffer[i], ref x, ref y, ref z);
+                        if (is_same_triangle(ref a, ref b, ref c, ref x, ref y, ref z, merge_r2) == false)
+                            continue;
+
+                        if (CheckOrientation) {
+                            Vector3d n2 = MathUtil.Normal(x, y, z);
+                            if (n.Dot(n2) < 0.99)
+                                continue;
+                        }
+
+                        MeshResult result =  Mesh.RemoveTriangle(buffer[i], true, false);
+                        if (result == MeshResult.Ok)
+                            ++Removed;
+					}
+				}
+			}
+
+			return true;
+		}
+
+
+
+		bool is_same_triangle(ref Vector3d a, ref Vector3d b, ref Vector3d c,
+                              ref Vector3d x, ref Vector3d y, ref Vector3d z, double tolSqr) {
+            if ( a.DistanceSquared(x) < tolSqr) {
+                if (b.DistanceSquared(y) < tolSqr && c.DistanceSquared(z) < tolSqr)
+                    return true;
+                if (b.DistanceSquared(z) < tolSqr && c.DistanceSquared(y) < tolSqr)
+                    return true;
+            } else if (a.DistanceSquared(y) < tolSqr) {
+                if (b.DistanceSquared(x) < tolSqr && c.DistanceSquared(z) < tolSqr)
+                    return true;
+                if (b.DistanceSquared(z) < tolSqr && c.DistanceSquared(x) < tolSqr)
+                    return true;
+            } else if (a.DistanceSquared(z) < tolSqr) {
+                if (b.DistanceSquared(x) < tolSqr && c.DistanceSquared(y) < tolSqr)
+                    return true;
+                if (b.DistanceSquared(y) < tolSqr && c.DistanceSquared(x) < tolSqr)
+                    return true;
+            }
+            return false;
+		}
+
+
+
+		// present mesh tri centroids as a PointSet
+		class TriCentroids : IPointSet {
+			public DMesh3 Mesh;
+
+			public int VertexCount { get { return Mesh.TriangleCount; } }
+			public int MaxVertexID { get { return Mesh.MaxTriangleID; } }
+
+			public bool HasVertexNormals { get { return false; } }
+			public bool HasVertexColors { get { return false; } }
+
+			public Vector3d GetVertex(int i) { return Mesh.GetTriCentroid(i); }
+			public Vector3f GetVertexNormal(int i) { return Vector3f.AxisY; }
+			public Vector3f GetVertexColor(int i) { return Vector3f.One; }
+
+			public bool IsVertex(int tID) { return Mesh.IsTriangle(tID); }
+
+			// iterators allow us to work with gaps in index space
+			public System.Collections.Generic.IEnumerable<int> VertexIndices() {
+				return Mesh.TriangleIndices();
+			}
+
+            public int Timestamp { get { return Mesh.Timestamp; } }
+
+        }
+
+
+
+	}
+}
diff --git a/mesh_ops/RemoveOccludedTriangles.cs b/mesh_ops/RemoveOccludedTriangles.cs
new file mode 100644
index 00000000..06936376
--- /dev/null
+++ b/mesh_ops/RemoveOccludedTriangles.cs
@@ -0,0 +1,195 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Threading;
+using g3;
+
+namespace gs
+{
+	/// <summary>
+	/// Remove "occluded" triangles, ie triangles on the "inside" of the mesh. 
+    /// This is a fuzzy definition, current implementation is basically computing
+    /// something akin to ambient occlusion, and if face is fully occluded, then
+    /// we classify it as inside and remove it.
+	/// </summary>
+	public class RemoveOccludedTriangles
+	{
+		public DMesh3 Mesh;
+        public DMeshAABBTree3 Spatial;
+
+        // indices of removed triangles. List will be empty if nothing removed
+        public List<int> RemovedT = null;
+
+        // Mesh.RemoveTriange() can return false, if that happens, this will be true
+        public bool RemoveFailed = false;
+
+        // if true, then we discard tris if any vertex is occluded.
+        // Otherwise we discard based on tri centroids
+        public bool PerVertex = false;
+
+        // we nudge points out by this amount to try to counteract numerical issues
+        public double NormalOffset = MathUtil.ZeroTolerance;
+
+        // use this as winding isovalue for WindingNumber mode
+        public double WindingIsoValue = 0.5;
+
+        public enum CalculationMode
+        {
+            RayParity = 0,
+            AnalyticWindingNumber = 1,
+            FastWindingNumber = 2,
+            SimpleOcclusionTest = 3
+        }
+        public CalculationMode InsideMode = CalculationMode.RayParity;
+
+
+        /// <summary>
+        /// Set this to be able to cancel running remesher
+        /// </summary>
+        public ProgressCancel Progress = null;
+
+        /// <summary>
+        /// if this returns true, abort computation. 
+        /// </summary>
+        protected virtual bool Cancelled() {
+            return (Progress == null) ? false : Progress.Cancelled();
+        }
+
+
+
+        public RemoveOccludedTriangles(DMesh3 mesh)
+		{
+			Mesh = mesh;
+		}
+
+        public RemoveOccludedTriangles(DMesh3 mesh, DMeshAABBTree3 spatial)
+        {
+            Mesh = mesh;
+            Spatial = spatial;
+        }
+
+
+        public virtual bool Apply()
+        {
+            DMesh3 testAgainstMesh = Mesh;
+            if (InsideMode == CalculationMode.RayParity) {
+                MeshBoundaryLoops loops = new MeshBoundaryLoops(testAgainstMesh);
+                if (loops.Count > 0) {
+                    testAgainstMesh = new DMesh3(Mesh);
+                    foreach (var loop in loops) {
+                        if (Cancelled())
+                            return false;
+                        SimpleHoleFiller filler = new SimpleHoleFiller(testAgainstMesh, loop);
+                        filler.Fill();
+                    }
+                }
+            }
+
+            DMeshAABBTree3 spatial = (Spatial != null && testAgainstMesh == Mesh) ? 
+                Spatial : new DMeshAABBTree3(testAgainstMesh, true);
+            if (InsideMode == CalculationMode.AnalyticWindingNumber)
+                spatial.WindingNumber(Vector3d.Zero);
+            else if (InsideMode == CalculationMode.FastWindingNumber )
+                spatial.FastWindingNumber(Vector3d.Zero);
+
+            if (Cancelled())
+                return false;
+
+            // ray directions
+            List<Vector3d> ray_dirs = null; int NR = 0;
+            if (InsideMode == CalculationMode.SimpleOcclusionTest) {
+                ray_dirs = new List<Vector3d>();
+                ray_dirs.Add(Vector3d.AxisX); ray_dirs.Add(-Vector3d.AxisX);
+                ray_dirs.Add(Vector3d.AxisY); ray_dirs.Add(-Vector3d.AxisY);
+                ray_dirs.Add(Vector3d.AxisZ); ray_dirs.Add(-Vector3d.AxisZ);
+                NR = ray_dirs.Count;
+            }
+
+            Func<Vector3d, bool> isOccludedF = (pt) => {
+
+                if (InsideMode == CalculationMode.RayParity) {
+                    return spatial.IsInside(pt);
+                } else if (InsideMode == CalculationMode.AnalyticWindingNumber) {
+                    return spatial.WindingNumber(pt) > WindingIsoValue;
+                } else if (InsideMode == CalculationMode.FastWindingNumber) {
+                    return spatial.FastWindingNumber(pt) > WindingIsoValue;
+                } else {
+                    for (int k = 0; k < NR; ++k) {
+                        int hit_tid = spatial.FindNearestHitTriangle(new Ray3d(pt, ray_dirs[k]));
+                        if (hit_tid == DMesh3.InvalidID)
+                            return false;
+                    }
+                    return true;
+                }
+            };
+
+            bool cancel = false;
+
+            BitArray vertices = null;
+            if ( PerVertex ) {
+                vertices = new BitArray(Mesh.MaxVertexID);
+
+                MeshNormals normals = null;
+                if (Mesh.HasVertexNormals == false) {
+                    normals = new MeshNormals(Mesh);
+                    normals.Compute();
+                }
+
+                gParallel.ForEach(Mesh.VertexIndices(), (vid) => {
+                    if (cancel) return;
+                    if (vid % 10 == 0) cancel = Cancelled();
+
+                    Vector3d c = Mesh.GetVertex(vid);
+                    Vector3d n = (normals == null) ? Mesh.GetVertexNormal(vid) : normals[vid];
+                    c += n * NormalOffset;
+                    vertices[vid] = isOccludedF(c);
+                });
+            }
+            if (Cancelled())
+                return false;
+
+            RemovedT = new List<int>();
+            SpinLock removeLock = new SpinLock();
+
+            gParallel.ForEach(Mesh.TriangleIndices(), (tid) => {
+                if (cancel) return;
+                if (tid % 10 == 0) cancel = Cancelled();
+
+                bool inside = false;
+                if (PerVertex) {
+                    Index3i tri = Mesh.GetTriangle(tid);
+                    inside = vertices[tri.a] || vertices[tri.b] || vertices[tri.c];
+
+                } else {
+                    Vector3d c = Mesh.GetTriCentroid(tid);
+                    Vector3d n = Mesh.GetTriNormal(tid);
+                    c += n * NormalOffset;
+                    inside = isOccludedF(c);
+                }
+
+                if (inside) {
+                    bool taken = false;
+                    removeLock.Enter(ref taken);
+                    RemovedT.Add(tid);
+                    removeLock.Exit();
+                }
+            });
+
+            if (Cancelled())
+                return false;
+
+            if (RemovedT.Count > 0) {
+                MeshEditor editor = new MeshEditor(Mesh);
+                bool bOK = editor.RemoveTriangles(RemovedT, true);
+                RemoveFailed = (bOK == false);
+            } 
+
+            return true;
+		}
+
+
+        
+	}
+}
diff --git a/mesh_ops/SimpleHoleFiller.cs b/mesh_ops/SimpleHoleFiller.cs
index d6f6a2ce..1b0e072f 100644
--- a/mesh_ops/SimpleHoleFiller.cs
+++ b/mesh_ops/SimpleHoleFiller.cs
@@ -41,7 +41,7 @@ public virtual bool Fill(int group_id = -1)
             if ( Loop.Vertices.Length == 3 ) {
                 Index3i tri = new Index3i(Loop.Vertices[0], Loop.Vertices[2], Loop.Vertices[1]);
                 int new_tid = Mesh.AppendTriangle(tri, group_id);
-                if (new_tid == DMesh3.InvalidID)
+                if (new_tid < 0)
                     return false;
                 NewTriangles = new int[1] { new_tid };
                 NewVertex = DMesh3.InvalidID;
@@ -70,11 +70,11 @@ public virtual bool Fill(int group_id = -1)
 
             // if fill failed, back out vertex-add
             if ( NewTriangles == null ) {
-                Mesh.RemoveVertex(NewVertex);
+                Mesh.RemoveVertex(NewVertex, true, false);
                 NewVertex = DMesh3.InvalidID;
-            }
-
-            return true;
+				return false;
+            } else 
+            	return true;
 
         }
 
diff --git a/mesh_ops/SmoothedHoleFill.cs b/mesh_ops/SmoothedHoleFill.cs
new file mode 100644
index 00000000..65d50d1c
--- /dev/null
+++ b/mesh_ops/SmoothedHoleFill.cs
@@ -0,0 +1,263 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// This fills a hole in a mesh by doing a trivial fill, optionally offsetting along a fixed vector,
+    /// then doing a remesh, then a laplacian smooth, then a second remesh.
+    /// </summary>
+    public class SmoothedHoleFill
+    {
+        public DMesh3 Mesh;
+
+        // after initial coarse hole fill, (optionally) offset fill patch in this direction/distance
+        public Vector3d OffsetDirection = Vector3d.Zero;
+        public double OffsetDistance = 0.0;
+
+        // remeshing parameters
+        public double TargetEdgeLength = 2.5;
+        public double SmoothAlpha = 1.0;
+        public int InitialRemeshPasses = 20;
+        public bool RemeshBeforeSmooth = true;
+        public bool RemeshAfterSmooth = true;
+
+        // optionally allows extended customization of internal remesher
+        // bool argument is true before smooth, false after
+        public Action<Remesher, bool> ConfigureRemesherF = null;
+
+        // the laplacian smooth is what gives us the smooth fill
+        public bool EnableLaplacianSmooth = true;
+
+        // higher iterations == smoother result, but more expensive
+        // [TODO] currently only has effect if ConstrainToHoleInterior==true  (otherwise ROI expands each iteration...)
+        public int SmoothSolveIterations = 1;
+
+        /// <summary>If this is true, we don't modify any triangles outside hole (often results in lower-quality fill)</summary>
+        public bool ConstrainToHoleInterior = false;
+
+
+        /*
+         *  ways to specify the hole we will fill
+         *   (you really should use FillLoop unless you have a good reason not to)
+         */
+
+        // Option 1: fill this loop specifically
+        public EdgeLoop FillLoop = null;
+
+        // Option 2: identify right loop using these tris on the border of hole
+        public List<int> BorderHintTris = null;
+
+
+        /*
+         *  Outputs
+         */
+
+        /// <summary> Final fill triangles. May include triangles outside initial fill loop, if ConstrainToHoleInterior=false </summary>
+        public int[] FillTriangles;
+
+        /// <summary> Final fill vertices </summary>
+        public int[] FillVertices;
+
+
+        public SmoothedHoleFill(DMesh3 mesh, EdgeLoop fillLoop = null)
+        {
+            this.Mesh = mesh;
+            this.FillLoop = fillLoop;
+        }
+
+
+        public bool Apply()
+        {
+            EdgeLoop useLoop = null;
+
+            if (FillLoop == null) {
+                MeshBoundaryLoops loops = new MeshBoundaryLoops(Mesh, true);
+                if (loops.Count == 0)
+                    return false;
+
+                if (BorderHintTris != null)
+                    useLoop = select_loop_tris_hint(loops);
+                if (useLoop == null && loops.MaxVerticesLoopIndex >= 0)
+                    useLoop = loops[loops.MaxVerticesLoopIndex];
+            } else {
+                useLoop = FillLoop;
+            }
+            if (useLoop == null)
+                return false;
+
+            // step 1: do stupid hole fill
+            SimpleHoleFiller filler = new SimpleHoleFiller(Mesh, useLoop);
+            if (filler.Fill() == false)
+                return false;
+
+            if (useLoop.Vertices.Length <= 3 ) {
+                FillTriangles = filler.NewTriangles;
+                FillVertices = new int[0];
+                return true;
+            }
+
+            MeshFaceSelection tris = new MeshFaceSelection(Mesh);
+            tris.Select(filler.NewTriangles);
+
+            // extrude initial fill surface (this is used in socketgen for example)
+            if (OffsetDistance > 0) {
+                MeshExtrudeFaces extrude = new MeshExtrudeFaces(Mesh, tris);
+                extrude.ExtrudedPositionF = (v, n, vid) => {
+                    return v + OffsetDistance * OffsetDirection;
+                };
+                if (!extrude.Extrude())
+                    return false;
+                tris.Select(extrude.JoinTriangles);
+            }
+
+            // if we aren't trying to stay inside hole, expand out a bit,
+            // which allows us to clean up ugly edges
+            if (ConstrainToHoleInterior == false) {
+                tris.ExpandToOneRingNeighbours(2);
+                tris.LocalOptimize(true, true);
+            }
+
+            // remesh the initial coarse fill region
+            if (RemeshBeforeSmooth) {
+                RegionRemesher remesh = new RegionRemesher(Mesh, tris);
+                remesh.SetTargetEdgeLength(TargetEdgeLength);
+                remesh.EnableSmoothing = (SmoothAlpha > 0);
+                remesh.SmoothSpeedT = SmoothAlpha;
+                if (ConfigureRemesherF != null)
+                    ConfigureRemesherF(remesh, true);
+                for (int k = 0; k < InitialRemeshPasses; ++k)
+                    remesh.BasicRemeshPass();
+                remesh.BackPropropagate();
+
+                tris = new MeshFaceSelection(Mesh);
+                tris.Select(remesh.CurrentBaseTriangles);
+                if (ConstrainToHoleInterior == false)
+                    tris.LocalOptimize(true, true);
+            }
+
+            if (ConstrainToHoleInterior) {
+                for (int k = 0; k < SmoothSolveIterations; ++k ) {
+                    smooth_and_remesh_preserve(tris, k == SmoothSolveIterations-1);
+                    tris = new MeshFaceSelection(Mesh); tris.Select(FillTriangles);
+                }
+            } else {
+                smooth_and_remesh(tris);
+                tris = new MeshFaceSelection(Mesh); tris.Select(FillTriangles);
+            }
+
+            MeshVertexSelection fill_verts = new MeshVertexSelection(Mesh);
+            fill_verts.SelectInteriorVertices(tris);
+            FillVertices = fill_verts.ToArray();
+
+            return true;
+        }
+
+
+
+        void smooth_and_remesh_preserve(MeshFaceSelection tris, bool bFinal)
+        {
+            if (EnableLaplacianSmooth) {
+                LaplacianMeshSmoother.RegionSmooth(Mesh, tris, 2, 2, true);
+            }
+
+            if (RemeshAfterSmooth) {
+                MeshProjectionTarget target = (bFinal) ? MeshProjectionTarget.Auto(Mesh, tris, 5) : null;
+
+                RegionRemesher remesh2 = new RegionRemesher(Mesh, tris);
+                remesh2.SetTargetEdgeLength(TargetEdgeLength);
+                remesh2.SmoothSpeedT = 1.0;
+                remesh2.SetProjectionTarget(target);
+                if (ConfigureRemesherF != null)
+                    ConfigureRemesherF(remesh2, false);
+                for (int k = 0; k < 10; ++k)
+                    remesh2.BasicRemeshPass();
+                remesh2.BackPropropagate();
+
+                FillTriangles = remesh2.CurrentBaseTriangles;
+            } else {
+                FillTriangles = tris.ToArray();
+            }
+        }
+
+
+
+        void smooth_and_remesh(MeshFaceSelection tris)
+        {
+            if (EnableLaplacianSmooth) {
+                LaplacianMeshSmoother.RegionSmooth(Mesh, tris, 2, 2, false);
+            }
+
+            if (RemeshAfterSmooth) {
+                tris.ExpandToOneRingNeighbours(2);
+                tris.LocalOptimize(true, true);
+                MeshProjectionTarget target = MeshProjectionTarget.Auto(Mesh, tris, 5);
+
+                RegionRemesher remesh2 = new RegionRemesher(Mesh, tris);
+                remesh2.SetTargetEdgeLength(TargetEdgeLength);
+                remesh2.SmoothSpeedT = 1.0;
+                remesh2.SetProjectionTarget(target);
+                if (ConfigureRemesherF != null)
+                    ConfigureRemesherF(remesh2, false);
+                for (int k = 0; k < 10; ++k)
+                    remesh2.BasicRemeshPass();
+                remesh2.BackPropropagate();
+
+                FillTriangles = remesh2.CurrentBaseTriangles;
+            } else {
+                FillTriangles = tris.ToArray();
+            }
+        }
+
+
+
+
+
+
+
+
+
+        EdgeLoop select_loop_tris_hint(MeshBoundaryLoops loops)
+        {
+            HashSet<int> hint_edges = new HashSet<int>();
+            foreach ( int tid in BorderHintTris ) {
+                if (Mesh.IsTriangle(tid) == false)
+                    continue;
+                Index3i et = Mesh.GetTriEdges(tid);
+                for (int j = 0; j < 3; ++j) {
+                    if (Mesh.IsBoundaryEdge(et[j]))
+                        hint_edges.Add(et[j]);
+                }
+            }
+
+
+            int N = loops.Count;
+            int best_loop = -1;
+            int max_votes = 0;
+            for ( int li = 0; li < N; ++li ) {
+                int votes = 0;
+                EdgeLoop l = loops[li];
+                foreach (int eid in l.Edges) {
+                    if (hint_edges.Contains(eid))
+                        votes++;
+                }
+                if ( votes > max_votes ) {
+                    best_loop = li;
+                    max_votes = votes;
+                }
+            }
+
+            if (best_loop == -1)
+                return null;
+            return loops[best_loop];
+
+        }
+
+
+    }
+}
diff --git a/mesh_selection/MeshBoundaryLoops.cs b/mesh_selection/MeshBoundaryLoops.cs
index 654d57da..8b256483 100644
--- a/mesh_selection/MeshBoundaryLoops.cs
+++ b/mesh_selection/MeshBoundaryLoops.cs
@@ -25,7 +25,8 @@ public class MeshBoundaryLoops : IEnumerable<EdgeLoop>
 
         public List<EdgeSpan> Spans;       // spans are unclosed loops
         public bool SawOpenSpans = false;  // will be set to true if we find any open spans
-
+        public bool FellBackToSpansOnFailure = false;       // set to true if we had to add spans to recover from failure
+                                                            // currently this happens if we cannot extract simple loops from a loop with bowties
 
         // What should we do if we encounter open spans. Mainly a result of EdgeFilter, but can also
         // happen on meshes w/ crazy bowties
@@ -46,6 +47,10 @@ public enum FailureBehaviors
         // if enabled, only edges where this returns true are considered
         public Func<int, bool> EdgeFilterF = null;
 
+        // if we throw an exception, we will try to set FailureBowties, so that client
+        // can try repairing these vertices
+        public List<int> FailureBowties = null;
+
 
         public MeshBoundaryLoops(DMesh3 mesh, bool bAutoCompute = true)
         {
@@ -145,6 +150,10 @@ public bool Compute()
 			Loops = new List<EdgeLoop>();
             Spans = new List<EdgeSpan>();
 
+            // early-out if we don't actually have boundaries
+            if (Mesh.CachedIsClosed)
+                return true;
+
             int NE = Mesh.MaxEdgeID;
 
             // Temporary memory used to indicate when we have "used" an edge.
@@ -309,9 +318,14 @@ public bool Compute()
                 } else if (bowties.Count > 0) {
                     // if we saw a bowtie vertex, we might need to break up this loop,
                     // so call extract_subloops
-                    List<EdgeLoop> subloops = extract_subloops(loop_verts, loop_edges, bowties);
-                    for (int i = 0; i < subloops.Count; ++i)
-                        Loops.Add(subloops[i]);
+                    Subloops subloops = extract_subloops(loop_verts, loop_edges, bowties);
+                    foreach ( var loop in subloops.Loops )
+                        Loops.Add(loop);
+                    if ( subloops.Spans.Count > 0 ) {
+                        FellBackToSpansOnFailure = true;
+                        foreach (var span in subloops.Spans)
+                            Spans.Add(span);
+                    }
                 } else {
                     // clean simple loop, convert to EdgeLoop instance
                     EdgeLoop loop = new EdgeLoop(Mesh);
@@ -391,6 +405,11 @@ int find_left_turn_edge(int incoming_e, int bowtie_v, int[] bdry_edges, int bdry
 
 
 
+        struct Subloops
+        {
+            public List<EdgeLoop> Loops;
+            public List<EdgeSpan> Spans;
+        }
 
 
         // This is called when loopV contains one or more "bowtie" vertices.
@@ -402,9 +421,14 @@ int find_left_turn_edge(int incoming_e, int bowtie_v, int[] bdry_edges, int bdry
         //
         // Currently loopE is not used, and the returned EdgeLoop objects do not have their Edges
         // arrays initialized. Perhaps to improve in future.
-        List<EdgeLoop> extract_subloops(List<int> loopV, List<int> loopE, List<int> bowties )
+        //
+        // An unhandled case to think about is where we have a sequence [..A..B..A..B..] where
+        // A and B are bowties. In this case there are no A->A or B->B subloops. What should
+        // we do here??
+        Subloops extract_subloops(List<int> loopV, List<int> loopE, List<int> bowties )
         {
-            List<EdgeLoop> subs = new List<EdgeLoop>();
+            Subloops subs = new Subloops();
+            subs.Loops = new List<EdgeLoop>(); subs.Spans = new List<EdgeSpan>();
 
             // figure out which bowties we saw are actually duplicated in loopV
             List<int> dupes = new List<int>();
@@ -415,7 +439,7 @@ List<EdgeLoop> extract_subloops(List<int> loopV, List<int> loopE, List<int> bowt
 
             // we might not actually have any duplicates, if we got luck. Early out in that case
             if ( dupes.Count == 0 ) {
-                subs.Add(new EdgeLoop(Mesh) {
+                subs.Loops.Add(new EdgeLoop(Mesh) {
                     Vertices = loopV.ToArray(), Edges = loopE.ToArray(), BowtieVertices = bowties.ToArray()
                 });
                 return subs;
@@ -441,9 +465,29 @@ List<EdgeLoop> extract_subloops(List<int> loopV, List<int> loopE, List<int> bowt
                         }
                     }
                 }
+
+                // failed to find a simple loop. Not sure what to do in this situation. 
+                // If we don't want to throw, all we can do is convert the remaining 
+                // loop to a span and return. 
+                // (Or should we keep it as a loop and set flag??)
                 if (bv_shortest == -1) {
-                    throw new MeshBoundaryLoopsException("MeshBoundaryLoops.Compute: Cannot find a valid simple loop");
+                    if (FailureBehavior == FailureBehaviors.ThrowException) {
+                        FailureBowties = dupes;
+                        throw new MeshBoundaryLoopsException("MeshBoundaryLoops.Compute: Cannot find a valid simple loop");
+                    }
+                    EdgeSpan span = new EdgeSpan(Mesh);
+                    List<int> verts = new List<int>();
+                    for (int i = 0; i < loopV.Count; ++i) {
+                        if (loopV[i] != -1)
+                            verts.Add(loopV[i]);
+                    }
+                    span.Vertices = verts.ToArray();
+                    span.Edges = EdgeSpan.VerticesToEdges(Mesh, span.Vertices);
+                    span.BowtieVertices = bowties.ToArray();
+                    subs.Spans.Add(span);
+                    return subs;
                 }
+
                 if (bv != bv_shortest) {
                     bv = bv_shortest;
                     // running again just to get start_i and end_i...
@@ -456,7 +500,7 @@ List<EdgeLoop> extract_subloops(List<int> loopV, List<int> loopE, List<int> bowt
                 loop.Vertices = extract_span(loopV, start_i, end_i, true);
                 loop.Edges = EdgeLoop.VertexLoopToEdgeLoop(Mesh, loop.Vertices);
                 loop.BowtieVertices = bowties.ToArray();
-                subs.Add(loop);
+                subs.Loops.Add(loop);
 
                 // If there are no more duplicates of this bowtie, we can treat
                 // it like a regular vertex now
@@ -481,7 +525,7 @@ List<EdgeLoop> extract_subloops(List<int> loopV, List<int> loopE, List<int> bowt
                 }
                 loop.Edges = EdgeLoop.VertexLoopToEdgeLoop(Mesh, loop.Vertices);
                 loop.BowtieVertices = bowties.ToArray();
-                subs.Add(loop);
+                subs.Loops.Add(loop);
             }
 
             return subs;
diff --git a/mesh_selection/MeshConnectedComponents.cs b/mesh_selection/MeshConnectedComponents.cs
index 3c95befb..2c71559a 100644
--- a/mesh_selection/MeshConnectedComponents.cs
+++ b/mesh_selection/MeshConnectedComponents.cs
@@ -224,5 +224,32 @@ public static DMesh3 LargestT(DMesh3 meshIn)
             return submesh.SubMesh;
         }
 
+
+
+
+        /// <summary>
+        /// Utility function that finds set of triangles connected to tSeed. Does not use MeshConnectedComponents class.
+        /// </summary>
+        public static HashSet<int> FindConnectedT(DMesh3 mesh, int tSeed)
+        {
+            HashSet<int> found = new HashSet<int>();
+            found.Add(tSeed);
+            List<int> queue = new List<int>(64) { tSeed };
+            while ( queue.Count > 0 ) {
+                int tid = queue[queue.Count - 1];
+                queue.RemoveAt(queue.Count - 1);
+                Index3i nbr_t = mesh.GetTriNeighbourTris(tid);
+                for ( int j = 0; j < 3; ++j ) {
+                    int nbrid = nbr_t[j];
+                    if (nbrid == DMesh3.InvalidID || found.Contains(nbrid))
+                        continue;
+                    found.Add(nbrid);
+                    queue.Add(nbrid);
+                }
+            }
+            return found;
+        }
+
+
     }
 }
diff --git a/mesh_selection/MeshEdgeSelection.cs b/mesh_selection/MeshEdgeSelection.cs
index 10bdde83..958826e9 100644
--- a/mesh_selection/MeshEdgeSelection.cs
+++ b/mesh_selection/MeshEdgeSelection.cs
@@ -169,6 +169,20 @@ public void SelectTriangleEdges(IEnumerable<int> triangles)
         }
 
 
+        public void SelectBoundaryTriEdges(MeshFaceSelection triangles)
+        {
+            foreach ( int tid in triangles ) {
+                Index3i te = Mesh.GetTriEdges(tid);
+                for ( int j = 0; j < 3; ++j ) {
+                    Index2i et = Mesh.GetEdgeT(te[j]);
+                    int other_tid = (et.a == tid) ? et.b : et.a;
+                    if (triangles.IsSelected(other_tid) == false)
+                        add(te[j]);
+                }
+            }
+        }
+
+
         public void Deselect(int tid) {
             remove(tid);
         }
diff --git a/mesh_selection/MeshFaceSelection.cs b/mesh_selection/MeshFaceSelection.cs
index 9a697f92..e9540801 100644
--- a/mesh_selection/MeshFaceSelection.cs
+++ b/mesh_selection/MeshFaceSelection.cs
@@ -145,6 +145,11 @@ public void Select(Func<int,bool> selectF)
             Select(temp);
         }
 
+
+        public void SelectVertexOneRing(int vid) {
+            foreach (int tid in Mesh.VtxTrianglesItr(vid))
+                add(tid);
+        }
         public void SelectVertexOneRings(int[] vertices)
         {
             for ( int i = 0; i < vertices.Length; ++i ) {
@@ -162,6 +167,15 @@ public void SelectVertexOneRings(IEnumerable<int> vertices)
         }
 
 
+        public void SelectEdgeTris(int eid)
+        {
+            Index2i et = Mesh.GetEdgeT(eid);
+            add(et.a);
+            if (et.b != DMesh3.InvalidID)
+                add(et.b);
+        }
+
+
         public void Deselect(int tid) {
             remove(tid);
         }
@@ -474,28 +488,85 @@ public bool FillEars(bool bFillTinyHoles)
         }
 
         // returns true if selection was modified
-        public bool LocalOptimize(bool bClipFins, bool bFillEars, bool bFillTinyHoles = true, bool bClipLoners = true)
+        public bool LocalOptimize(bool bClipFins, bool bFillEars, bool bFillTinyHoles = true, bool bClipLoners = true, bool bRemoveBowties = false)
         {
             bool bModified = false;
             bool done = false;
+            int count = 0;
+            HashSet<int> temp_hash = new HashSet<int>();
             while ( ! done ) {
                 done = true;
+                if (count++ == 25)      // terminate in case we get stuck
+                    break;
                 if (bClipFins && ClipFins(bClipLoners))
                     done = false;
                 if (bFillEars && FillEars(bFillTinyHoles))
                     done = false;
+                if (bRemoveBowties && remove_bowties(temp_hash))
+                    done = false;
                 if (done == false)
                     bModified = true;
             }
+            if (bRemoveBowties)
+                remove_bowties(temp_hash);        // do a final pass of this because it is usually the most problematic...
             return bModified;
         }
-        public bool LocalOptimize() {
-            return LocalOptimize(true, true, true, true);
+        public bool LocalOptimize(bool bRemoveBowties = true) {
+            return LocalOptimize(true, true, true, true, bRemoveBowties);
         }
 
 
 
 
+        /// <summary>
+        /// Find any "bowtie" vertices - ie vertex v such taht there is multiple spans of triangles
+        /// selected in v's triangle one-ring - and deselect those one-rings.
+        /// Returns true if selection was modified.
+        /// </summary>
+        public bool RemoveBowties() {
+            return remove_bowties(null);
+        }
+        public bool remove_bowties(HashSet<int> tempHash)
+        {
+            bool bModified = false;
+            bool done = false;
+            HashSet<int> vertices = (tempHash == null) ? new HashSet<int>() : tempHash;
+            while (!done) {
+                done = true;
+                vertices.Clear();
+                foreach (int tid in Selected) {
+                    Index3i tv = Mesh.GetTriangle(tid);
+                    vertices.Add(tv.a); vertices.Add(tv.b); vertices.Add(tv.c);
+                }
+
+                foreach (int vid in vertices) {
+                    if (is_bowtie_vtx(vid)) {
+                        Deselect(Mesh.VtxTrianglesItr(vid));
+                        done = false;
+                    }
+                }
+                if (done == false)
+                    bModified = true;
+            }
+            return bModified;
+        }
+        private bool is_bowtie_vtx(int vid)
+        {
+            int border_edges = 0;
+            foreach ( int eid in Mesh.VtxEdgesItr(vid) ) {
+                Index2i et = Mesh.GetEdgeT(eid);
+                if (et.b != DMesh3.InvalidID) {
+                    bool in_a = IsSelected(et.a);
+                    bool in_b = IsSelected(et.b);
+                    if (in_a != in_b)
+                        border_edges++;
+                } else {
+                    if (IsSelected(et.a))
+                        border_edges++;
+                }
+            }
+            return border_edges > 2;
+        }
 
 
 
diff --git a/mesh_selection/MeshVertexSelection.cs b/mesh_selection/MeshVertexSelection.cs
index bf1629ea..cda76921 100644
--- a/mesh_selection/MeshVertexSelection.cs
+++ b/mesh_selection/MeshVertexSelection.cs
@@ -40,6 +40,14 @@ public MeshVertexSelection(DMesh3 mesh, MeshEdgeSelection convertE) : this(mesh)
         }
 
 
+        public HashSet<int> ExtractSelected()
+        {
+            var ret = Selected;
+            Selected = new HashSet<int>();
+            return ret;
+        }
+
+
         public IEnumerator<int> GetEnumerator() {
             return Selected.GetEnumerator();
         }
@@ -110,6 +118,84 @@ public void SelectTriangleVertices(MeshFaceSelection triangles)
         }
 
 
+        /// <summary>
+        /// for each vertex of input triangle set, select vertex if all
+        /// one-ring triangles are contained in triangle set (ie vertex is not on boundary of triangle set).
+        /// </summary>
+        public void SelectInteriorVertices(MeshFaceSelection triangles)
+        {
+            HashSet<int> borderv = new HashSet<int>();
+            foreach ( int tid in triangles ) {
+                Index3i tv = Mesh.GetTriangle(tid);
+                for ( int j = 0; j < 3; ++j ) {
+                    int vid = tv[j];
+                    if (Selected.Contains(vid) || borderv.Contains(vid))
+                        continue;
+                    bool full_ring = true;
+                    foreach (int ring_tid in Mesh.VtxTrianglesItr(vid)) {
+                        if (triangles.IsSelected(ring_tid) == false) {
+                            full_ring = false;
+                            break;
+                        }
+                    }
+                    if (full_ring)
+                        add(vid);
+                    else
+                        borderv.Add(vid);
+                }
+            }
+        }
+
+
+
+
+        /// <summary>
+        /// Select set of boundary vertices connected to vSeed.
+        /// </summary>
+        public void SelectConnectedBoundaryV(int vSeed)
+        {
+            if ( ! Mesh.IsBoundaryVertex(vSeed))
+                throw new Exception("MeshConnectedComponents.FindConnectedBoundaryV: vSeed is not a boundary vertex");
+
+            HashSet<int> found = (Selected.Count == 0) ? Selected : new HashSet<int>();
+            found.Add(vSeed);
+            List<int> queue = temp; queue.Clear();
+            queue.Add(vSeed);
+            while (queue.Count > 0) {
+                int vid = queue[queue.Count - 1];
+                queue.RemoveAt(queue.Count - 1);
+                foreach (int nbrid in Mesh.VtxVerticesItr(vid)) {
+                    if (Mesh.IsBoundaryVertex(nbrid) && found.Contains(nbrid) == false) {
+                        found.Add(nbrid);
+                        queue.Add(nbrid);
+                    }
+                }
+            }
+            if ( found != Selected ) {
+                foreach (int vid in found)
+                    add(vid);
+            }
+            temp.Clear();
+        }
+
+
+
+
+        public void SelectEdgeVertices(int[] edges)
+        {
+            for (int i = 0; i < edges.Length; ++i) {
+                Index2i ev = Mesh.GetEdgeV(edges[i]);
+                add(ev.a); add(ev.b);
+            }
+        }
+        public void SelectEdgeVertices(IEnumerable<int> edges) {
+            foreach (int eid in edges) {
+                Index2i ev = Mesh.GetEdgeV(eid);
+                add(ev.a); add(ev.b);
+            }
+        }
+
+
 
         public void Deselect(int vID) {
             remove(vID);
@@ -122,6 +208,16 @@ public void Deselect(IEnumerable<int> vertices) {
             foreach ( int vid in vertices )
                 remove(vid);
         }
+        public void DeselectEdge(int eid) {
+            Index2i ev = Mesh.GetEdgeV(eid);
+            remove(ev.a); remove(ev.b);
+        }
+        public void DeselectEdges(IEnumerable<int> edges) {
+            foreach ( int eid in edges ) {
+                Index2i ev = Mesh.GetEdgeV(eid);
+                remove(ev.a); remove(ev.b);
+            }
+        }
 
         public int[] ToArray()
         {
diff --git a/queries/MeshQueries.cs b/queries/MeshQueries.cs
index aff48aea..1bfc8dae 100644
--- a/queries/MeshQueries.cs
+++ b/queries/MeshQueries.cs
@@ -23,14 +23,14 @@ public static DistPoint3Triangle3 TriangleDistance(DMesh3 mesh, int ti, Vector3d
         }
 
         /// <summary>
-        /// Find point-normal frame at closest point to queryPoint on mesh.
+        /// Find point-normal(Z) frame at closest point to queryPoint on mesh.
         /// Returns interpolated vertex-normal frame if available, otherwise tri-normal frame.
         /// </summary>
-        public static Frame3f NearestPointFrame(DMesh3 mesh, ISpatial spatial, Vector3d queryPoint)
+        public static Frame3f NearestPointFrame(DMesh3 mesh, ISpatial spatial, Vector3d queryPoint, bool bForceFaceNormal = false)
         {
             int tid = spatial.FindNearestTriangle(queryPoint);
             Vector3d surfPt = TriangleDistance(mesh, tid, queryPoint).TriangleClosest;
-            if (mesh.HasVertexNormals)
+            if (mesh.HasVertexNormals && bForceFaceNormal == false)
                 return SurfaceFrame(mesh, tid, surfPt);
             else
                 return new Frame3f(surfPt, mesh.GetTriNormal(tid));
@@ -46,10 +46,38 @@ public static double NearestPointDistance(DMesh3 mesh, ISpatial spatial, Vector3
             int tid = spatial.FindNearestTriangle(queryPoint, maxDist);
             if (tid == DMesh3.InvalidID)
                 return double.MaxValue;
-            return Math.Sqrt(TriangleDistance(mesh, tid, queryPoint).DistanceSquared);
+            Triangle3d tri = new Triangle3d();
+            mesh.GetTriVertices(tid, ref tri.V0, ref tri.V1, ref tri.V2);
+            Vector3d closest, bary;
+            double dist_sqr = DistPoint3Triangle3.DistanceSqr(ref queryPoint, ref tri, out closest, out bary);
+            return Math.Sqrt(dist_sqr);
+        }
+
+
+
+        /// <summary>
+        /// find distance between two triangles, with optional
+        /// transform on second triangle
+        /// </summary>
+        public static DistTriangle3Triangle3 TriangleTriangleDistance(DMesh3 mesh1, int ti, DMesh3 mesh2, int tj, Func<Vector3d, Vector3d> TransformF = null)
+        {
+            if (mesh1.IsTriangle(ti) == false || mesh2.IsTriangle(tj) == false)
+                return null;
+            Triangle3d tri1 = new Triangle3d(), tri2 = new Triangle3d();
+            mesh1.GetTriVertices(ti, ref tri1.V0, ref tri1.V1, ref tri1.V2);
+            mesh2.GetTriVertices(tj, ref tri2.V0, ref tri2.V1, ref tri2.V2);
+            if (TransformF != null) {
+                tri2.V0 = TransformF(tri2.V0);
+                tri2.V1 = TransformF(tri2.V1);
+                tri2.V2 = TransformF(tri2.V2);
+            }
+            DistTriangle3Triangle3 dist = new DistTriangle3Triangle3(tri1, tri2);
+            dist.Compute();
+            return dist;
         }
 
 
+
         /// <summary>
         /// convenience function to construct a IntrRay3Triangle3 object for a mesh triangle
         /// </summary>
@@ -113,7 +141,7 @@ public static DistTriangle3Triangle3 TrianglesDistance(DMesh3 mesh1, int ti, DMe
         /// Find point-normal frame at ray-intersection point on mesh, or return false if no hit.
         /// Returns interpolated vertex-normal frame if available, otherwise tri-normal frame.
         /// </summary>
-        public static bool RayHitPointFrame(DMesh3 mesh, ISpatial spatial, Ray3d ray, out Frame3f hitPosFrame)
+        public static bool RayHitPointFrame(DMesh3 mesh, ISpatial spatial, Ray3d ray, out Frame3f hitPosFrame, bool bForceFaceNormal = false)
         {
             hitPosFrame = new Frame3f();
             int tid = spatial.FindNearestHitTriangle(ray);
@@ -123,8 +151,8 @@ public static bool RayHitPointFrame(DMesh3 mesh, ISpatial spatial, Ray3d ray, ou
             if (isect.Result != IntersectionResult.Intersects)
                 return false;
             Vector3d surfPt = ray.PointAt(isect.RayParameter);
-            if (mesh.HasVertexNormals)
-                hitPosFrame = SurfaceFrame(mesh, tid, surfPt);
+            if (mesh.HasVertexNormals && bForceFaceNormal == false)
+                hitPosFrame = SurfaceFrame(mesh, tid, surfPt);      // TODO isect has bary-coords already!!
             else
                 hitPosFrame = new Frame3f(surfPt, mesh.GetTriNormal(tid));
             return true;
@@ -135,7 +163,7 @@ public static bool RayHitPointFrame(DMesh3 mesh, ISpatial spatial, Ray3d ray, ou
         /// Get point-normal frame on surface of mesh. Assumption is that point lies in tID.
         /// returns interpolated vertex-normal frame if available, otherwise tri-normal frame.
         /// </summary>
-        public static Frame3f SurfaceFrame(DMesh3 mesh, int tID, Vector3d point)
+        public static Frame3f SurfaceFrame(DMesh3 mesh, int tID, Vector3d point, bool bForceFaceNormal = false)
         {
             if (!mesh.IsTriangle(tID))
                 throw new Exception("MeshQueries.SurfaceFrame: triangle " + tID + " does not exist!");
@@ -143,7 +171,7 @@ public static Frame3f SurfaceFrame(DMesh3 mesh, int tID, Vector3d point)
             mesh.GetTriVertices(tID, ref tri.V0, ref tri.V1, ref tri.V2);
             Vector3d bary = tri.BarycentricCoords(point);
             point = tri.PointAt(bary);
-            if (mesh.HasVertexNormals) {
+            if (mesh.HasVertexNormals && bForceFaceNormal == false) {
                 Vector3d normal = mesh.GetTriBaryNormal(tID, bary.x, bary.y, bary.z);
                 return new Frame3f(point, normal);
             } else
@@ -151,7 +179,17 @@ public static Frame3f SurfaceFrame(DMesh3 mesh, int tID, Vector3d point)
         }
 
 
-
+        /// <summary>
+        /// Get barycentric coords of point in triangle
+        /// </summary>
+        public static Vector3d BaryCoords(DMesh3 mesh, int tID, Vector3d point)
+        {
+            if (!mesh.IsTriangle(tID))
+                throw new Exception("MeshQueries.SurfaceFrame: triangle " + tID + " does not exist!");
+            Triangle3d tri = new Triangle3d();
+            mesh.GetTriVertices(tID, ref tri.V0, ref tri.V1, ref tri.V2);
+            return tri.BarycentricCoords(point);
+        }
 
 
         /// <summary>
@@ -166,10 +204,10 @@ public static double TriDistanceSqr(DMesh3 mesh, int ti, Vector3d point)
             Vector3d edge0 = V1 - V0;
             Vector3d edge1 = V2 - V0;
             double a00 = edge0.LengthSquared;
-            double a01 = edge0.Dot(edge1);
+            double a01 = edge0.Dot(ref edge1);
             double a11 = edge1.LengthSquared;
-            double b0 = diff.Dot(edge0);
-            double b1 = diff.Dot(edge1);
+            double b0 = diff.Dot(ref edge0);
+            double b1 = diff.Dot(ref edge1);
             double c = diff.LengthSquared;
             double det = Math.Abs(a00 * a11 - a01 * a01);
             double s = a01 * b1 - a11 * b0;
diff --git a/queries/RayIntersection.cs b/queries/RayIntersection.cs
index a3d27cd6..1c60a980 100644
--- a/queries/RayIntersection.cs
+++ b/queries/RayIntersection.cs
@@ -15,12 +15,12 @@ private RayIntersection()
         // basic ray-sphere intersection
         public static bool Sphere(Vector3f vOrigin, Vector3f vDirection, Vector3f vCenter, float fRadius, out float fRayT)
         {
-            bool bHit = SphereSigned(vOrigin, vDirection, vCenter, fRadius, out fRayT);
+            bool bHit = SphereSigned(ref vOrigin, ref vDirection, ref vCenter, fRadius, out fRayT);
             fRayT = Math.Abs(fRayT);
             return bHit;
         }
 
-        public static bool SphereSigned(Vector3f vOrigin, Vector3f vDirection, Vector3f vCenter, float fRadius, out float fRayT)
+        public static bool SphereSigned(ref Vector3f vOrigin, ref Vector3f vDirection, ref Vector3f vCenter, float fRadius, out float fRayT)
         {
             fRayT = 0.0f;
             Vector3f m = vOrigin - vCenter;
@@ -44,11 +44,11 @@ public static bool SphereSigned(Vector3f vOrigin, Vector3f vDirection, Vector3f
 
 
 
-        public static bool SphereSigned(Vector3d vOrigin, Vector3d vDirection, Vector3d vCenter, double fRadius, out double fRayT)
+        public static bool SphereSigned(ref Vector3d vOrigin, ref Vector3d vDirection, ref Vector3d vCenter, double fRadius, out double fRayT)
         {
             fRayT = 0.0;
             Vector3d m = vOrigin - vCenter;
-            double b = m.Dot(vDirection);
+            double b = m.Dot(ref vDirection);
             double c = m.Dot(m) - fRadius * fRadius;
 
             // Exit if r’s origin outside s (c > 0) and r pointing away from s (b > 0) 
diff --git a/spatial/BasicProjectionTargets.cs b/spatial/BasicProjectionTargets.cs
index fa731f31..6d274c5c 100644
--- a/spatial/BasicProjectionTargets.cs
+++ b/spatial/BasicProjectionTargets.cs
@@ -5,7 +5,11 @@
 
 namespace g3
 {
-    public class MeshProjectionTarget : IProjectionTarget
+    /// <summary>
+    /// MeshProjectionTarget provides an IProjectionTarget interface to a mesh + spatial data structure.
+    /// Use to project points to mesh surface.
+    /// </summary>
+    public class MeshProjectionTarget : IOrientedProjectionTarget
     {
         public DMesh3 Mesh { get; set; }
         public ISpatial Spatial { get; set; }
@@ -15,6 +19,8 @@ public MeshProjectionTarget(DMesh3 mesh, ISpatial spatial)
         {
             Mesh = mesh;
             Spatial = spatial;
+            if ( Spatial == null )
+                Spatial = new DMeshAABBTree3(mesh, true);
         }
 
         public MeshProjectionTarget(DMesh3 mesh)
@@ -23,11 +29,25 @@ public MeshProjectionTarget(DMesh3 mesh)
             Spatial = new DMeshAABBTree3(mesh, true);
         }
 
-        public Vector3d Project(Vector3d vPoint, int identifier = -1)
+        public virtual Vector3d Project(Vector3d vPoint, int identifier = -1)
+        {
+            int tNearestID = Spatial.FindNearestTriangle(vPoint);
+            Triangle3d triangle = new Triangle3d();
+            Mesh.GetTriVertices(tNearestID, ref triangle.V0, ref triangle.V1, ref triangle.V2);
+            Vector3d nearPt, bary;
+            DistPoint3Triangle3.DistanceSqr(ref vPoint, ref triangle, out nearPt, out bary);
+            return nearPt;
+        }
+
+        public virtual Vector3d Project(Vector3d vPoint, out Vector3d vProjectNormal, int identifier = -1)
         {
             int tNearestID = Spatial.FindNearestTriangle(vPoint);
-            DistPoint3Triangle3 q = MeshQueries.TriangleDistance(Mesh, tNearestID, vPoint);
-            return q.TriangleClosest;
+            Triangle3d triangle = new Triangle3d();
+            Mesh.GetTriVertices(tNearestID, ref triangle.V0, ref triangle.V1, ref triangle.V2);
+            Vector3d nearPt, bary;
+            DistPoint3Triangle3.DistanceSqr(ref vPoint, ref triangle, out nearPt, out bary);
+            vProjectNormal = triangle.Normal;
+            return nearPt;
         }
 
         /// <summary>
@@ -40,10 +60,73 @@ public static MeshProjectionTarget Auto(DMesh3 mesh, bool bForceCopy = true)
             else
                 return new MeshProjectionTarget(mesh);
         }
+
+
+        /// <summary>
+        /// Automatically construct fastest projection target for region of mesh
+        /// </summary>
+        public static MeshProjectionTarget Auto(DMesh3 mesh, IEnumerable<int> triangles, int nExpandRings = 5)
+        {
+            MeshFaceSelection targetRegion = new MeshFaceSelection(mesh);
+            targetRegion.Select(triangles);
+            targetRegion.ExpandToOneRingNeighbours(nExpandRings);
+            DSubmesh3 submesh = new DSubmesh3(mesh, targetRegion);
+            return new MeshProjectionTarget(submesh.SubMesh); 
+        }
+    }
+
+
+
+
+    /// <summary>
+    /// Extension of MeshProjectionTarget that allows the target to have a transformation
+    /// relative to it's internal space. Call SetTransform(), or initialize the transforms yourself
+    /// </summary>
+    public class TransformedMeshProjectionTarget : MeshProjectionTarget
+    {
+        public TransformSequence SourceToTargetXForm;
+        public TransformSequence TargetToSourceXForm;
+
+        public TransformedMeshProjectionTarget() { }
+        public TransformedMeshProjectionTarget(DMesh3 mesh, ISpatial spatial) : base(mesh, spatial)
+        {
+        }
+        public TransformedMeshProjectionTarget(DMesh3 mesh) : base(mesh)
+        {
+        }
+
+        public void SetTransform(TransformSequence sourceToTargetX)
+        {
+            SourceToTargetXForm = sourceToTargetX;
+            TargetToSourceXForm = SourceToTargetXForm.MakeInverse();
+        }
+
+        public override Vector3d Project(Vector3d vPoint, int identifier = -1)
+        {
+            Vector3d vTargetPt = SourceToTargetXForm.TransformP(vPoint);
+            Vector3d vTargetProj = base.Project(vTargetPt, identifier);
+            return TargetToSourceXForm.TransformP(vTargetProj);
+        }
+
+
+        public override Vector3d Project(Vector3d vPoint, out Vector3d vProjectNormal, int identifier = -1)
+        {
+            Vector3d vTargetPt = SourceToTargetXForm.TransformP(vPoint);
+            Vector3d vTargetProjNormal;
+            Vector3d vTargetProj = base.Project(vTargetPt, out vTargetProjNormal, identifier);
+            vProjectNormal = TargetToSourceXForm.TransformV(vTargetProjNormal).Normalized;
+            return TargetToSourceXForm.TransformP(vTargetProj);
+        }
     }
 
 
 
+
+
+
+
+
+
     public class PlaneProjectionTarget : IProjectionTarget
     {
         public Vector3d Origin;
diff --git a/spatial/DCurveBoxTree.cs b/spatial/DCurveBoxTree.cs
new file mode 100644
index 00000000..0c231788
--- /dev/null
+++ b/spatial/DCurveBoxTree.cs
@@ -0,0 +1,322 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace g3
+{
+
+    /// <summary>
+    /// tree of Oriented Boxes (OBB) for a DCurve3. 
+    /// Construction is sequential, ie pairs of segments are merged into boxes, then pairs of boxes, and so on
+    /// 
+    /// [TODO] is this the best strategy? is there maybe some kind of sorting/sweepline algo?
+    /// [TODO] would it make more sense to have more than just 2 segments at lowest level?
+    /// 
+    /// </summary>
+    public class DCurve3BoxTree
+    {
+        public DCurve3 Curve;
+
+        Box3d[] boxes;
+        int layers;
+        List<int> layer_counts;
+
+        public DCurve3BoxTree(DCurve3 curve)
+        {
+            Curve = curve;
+            build_sequential(curve);
+        }
+
+
+        public double DistanceSquared(Vector3d pt) {
+            int iSeg; double segT;
+            double distSqr = SquaredDistance(pt, out iSeg, out segT);
+            return distSqr;
+        }
+        public double Distance(Vector3d pt)
+        {
+            int iSeg; double segT;
+            double distSqr = SquaredDistance(pt, out iSeg, out segT);
+            return Math.Sqrt(distSqr);
+        }
+        public Vector3d NearestPoint(Vector3d pt)
+        {
+            int iSeg; double segT;
+            SquaredDistance(pt, out iSeg, out segT);
+            return Curve.PointAt(iSeg, segT);
+        }
+
+
+
+        public double SquaredDistance(Vector3d pt, out int iNearSeg, out double fNearSegT, double max_dist = double.MaxValue)
+        {
+            int iRoot = boxes.Length - 1;
+            int iLayer = layers - 1;
+
+            double min_dist = max_dist;
+            iNearSeg = -1;
+            fNearSegT = 0;
+
+            find_min_distance(ref pt, ref min_dist, ref iNearSeg, ref fNearSegT, 0, iRoot, iLayer);
+            if (iNearSeg == -1)
+                return double.MaxValue;
+            return min_dist;
+        }
+
+
+
+        void find_min_distance(ref Vector3d pt, ref double min_dist, ref int min_dist_seg, ref double min_dist_segt, int bi, int iLayerStart, int iLayer)
+        {
+            // hit polygon layer, check segments
+            if (iLayer == 0) {
+                int seg_i = 2 * bi;
+                Segment3d seg_a = Curve.GetSegment(seg_i);
+                double segt;
+                double segdist = seg_a.DistanceSquared(pt, out segt);
+                if (segdist <= min_dist) {
+                    min_dist = segdist;
+                    min_dist_seg = seg_i;
+                    min_dist_segt = segt;
+                }
+                if ( (seg_i+1) < Curve.SegmentCount ) {
+                    Segment3d seg_b = Curve.GetSegment(seg_i + 1);
+                    segdist = seg_b.DistanceSquared(pt, out segt);
+                    if (segdist <= min_dist) {
+                        min_dist = segdist;
+                        min_dist_seg = seg_i + 1;
+                        min_dist_segt = segt;
+                    }
+                }
+
+                return;
+            }
+
+            // test both boxes and recurse
+            int prev_layer = iLayer - 1;
+            int prev_count = layer_counts[prev_layer];
+            int prev_start = iLayerStart - prev_count;
+            int prev_a = prev_start + 2 * bi;
+            double dist = boxes[prev_a].DistanceSquared(pt);
+            if (dist <= min_dist) {
+                find_min_distance(ref pt, ref min_dist, ref min_dist_seg, ref min_dist_segt, 2 * bi, prev_start, prev_layer);
+            }
+            if ((2 * bi + 1) >= prev_count)
+                return;
+            int prev_b = prev_a + 1;
+            double dist2 = boxes[prev_b].DistanceSquared(pt);
+            if (dist2 <= min_dist) {
+                find_min_distance(ref pt, ref min_dist, ref min_dist_seg, ref min_dist_segt, 2 * bi + 1, prev_start, prev_layer);
+            }
+        }
+
+
+
+        /// <summary>
+        /// Find min-distance between ray and curve. Pass max_dist if you only care about a certain distance
+        /// TODO: not 100% sure this is working properly... ?
+        /// </summary>
+        public double SquaredDistance(Ray3d ray, out int iNearSeg, out double fNearSegT, out double fRayT, double max_dist = double.MaxValue)
+        {
+            int iRoot = boxes.Length - 1;
+            int iLayer = layers - 1;
+
+            double min_dist = max_dist;
+            iNearSeg = -1;
+            fNearSegT = 0;
+            fRayT = double.MaxValue;
+
+            find_min_distance(ref ray, ref min_dist, ref iNearSeg, ref fNearSegT, ref fRayT, 0, iRoot, iLayer);
+            if (iNearSeg == -1)
+                return double.MaxValue;
+            return min_dist;
+        }
+
+        void find_min_distance(ref Ray3d ray, ref double min_dist, ref int min_dist_seg, ref double min_dist_segt, ref double min_dist_rayt, int bi, int iLayerStart, int iLayer)
+        {
+            // hit polygon layer, check segments
+            if (iLayer == 0) {
+                int seg_i = 2 * bi;
+                Segment3d seg_a = Curve.GetSegment(seg_i);
+                double segt, rayt;
+                double segdist_sqr = DistRay3Segment3.SquaredDistance(ref ray, ref seg_a, out rayt, out segt);
+                double segdist = Math.Sqrt(segdist_sqr);
+                if (segdist <= min_dist) {
+                    min_dist = segdist;
+                    min_dist_seg = seg_i;
+                    min_dist_segt = segt;
+                    min_dist_rayt = rayt;
+                }
+                if ((seg_i + 1) < Curve.SegmentCount) {
+                    Segment3d seg_b = Curve.GetSegment(seg_i + 1);
+                    segdist_sqr = DistRay3Segment3.SquaredDistance(ref ray, ref seg_b, out rayt, out segt);
+                    segdist = Math.Sqrt(segdist_sqr);
+                    if (segdist <= min_dist) {
+                        min_dist = segdist;
+                        min_dist_seg = seg_i + 1;
+                        min_dist_segt = segt;
+                        min_dist_rayt = rayt;
+                    }
+                }
+
+                return;
+            }
+
+            // test both boxes and recurse
+            // TODO: verify that this intersection strategy makes sense?
+            int prev_layer = iLayer - 1;
+            int prev_count = layer_counts[prev_layer];
+            int prev_start = iLayerStart - prev_count;
+            int prev_a = prev_start + 2 * bi;
+            bool intersects = IntrRay3Box3.Intersects(ref ray, ref boxes[prev_a], min_dist);
+            if (intersects) {
+                find_min_distance(ref ray, ref min_dist, ref min_dist_seg, ref min_dist_segt, ref min_dist_rayt, 2 * bi, prev_start, prev_layer);
+            }
+            if ((2 * bi + 1) >= prev_count)
+                return;
+            int prev_b = prev_a + 1;
+            bool intersects2 = IntrRay3Box3.Intersects(ref ray, ref boxes[prev_b], min_dist);
+            if (intersects2) {
+                find_min_distance(ref ray, ref min_dist, ref min_dist_seg, ref min_dist_segt, ref min_dist_rayt, 2 * bi + 1, prev_start, prev_layer);
+            }
+        }
+
+
+
+
+
+
+
+        /// <summary>
+        /// Find min-distance between ray and curve. Pass max_dist if you only care about a certain distance
+        /// TODO: not 100% sure this is working properly... ?
+        /// </summary>
+        public bool FindClosestRayIntersction(Ray3d ray, double radius, out int hitSegment, out double fRayT)
+        {
+            int iRoot = boxes.Length - 1;
+            int iLayer = layers - 1;
+
+            hitSegment = -1;
+            fRayT = double.MaxValue;
+
+            find_closest_ray_intersction(ref ray, radius, ref hitSegment, ref fRayT, 0, iRoot, iLayer);
+            return (hitSegment != -1);
+        }
+
+        void find_closest_ray_intersction(ref Ray3d ray, double radius, ref int nearestSegment, ref double nearest_ray_t, int bi, int iLayerStart, int iLayer)
+        {
+            // hit polygon layer, check segments
+            if (iLayer == 0) {
+                int seg_i = 2 * bi;
+                Segment3d seg_a = Curve.GetSegment(seg_i);
+                double segt, rayt;
+                double segdist_sqr = DistRay3Segment3.SquaredDistance(ref ray, ref seg_a, out rayt, out segt);
+                if (segdist_sqr <= radius*radius && rayt < nearest_ray_t) {
+                    nearestSegment = seg_i;
+                    nearest_ray_t = rayt;
+                }
+                if ((seg_i + 1) < Curve.SegmentCount) {
+                    Segment3d seg_b = Curve.GetSegment(seg_i + 1);
+                    segdist_sqr = DistRay3Segment3.SquaredDistance(ref ray, ref seg_b, out rayt, out segt);
+                    if (segdist_sqr <= radius * radius && rayt < nearest_ray_t) {
+                        nearestSegment = seg_i+1;
+                        nearest_ray_t = rayt;
+                    }
+                }
+
+                return;
+            }
+
+            // test both boxes and recurse
+            // TODO: verify that this intersection strategy makes sense?
+            int prev_layer = iLayer - 1;
+            int prev_count = layer_counts[prev_layer];
+            int prev_start = iLayerStart - prev_count;
+            int prev_a = prev_start + 2 * bi;
+            bool intersects = IntrRay3Box3.Intersects(ref ray, ref boxes[prev_a], radius);
+            if (intersects) {
+                find_closest_ray_intersction(ref ray, radius, ref nearestSegment, ref nearest_ray_t, 2 * bi, prev_start, prev_layer);
+            }
+            if ((2 * bi + 1) >= prev_count)
+                return;
+            int prev_b = prev_a + 1;
+            bool intersects2 = IntrRay3Box3.Intersects(ref ray, ref boxes[prev_b], radius);
+            if (intersects2) {
+                find_closest_ray_intersction(ref ray, radius, ref nearestSegment, ref nearest_ray_t, 2 * bi + 1, prev_start, prev_layer);
+            }
+        }
+
+
+
+
+
+
+        // build tree of boxes as sequential array
+        void build_sequential(DCurve3 curve)
+        {
+            int NV = curve.VertexCount;
+            int N = (curve.Closed) ? NV : NV - 1;
+            int boxCount = 0;
+            layers = 0;
+            layer_counts = new List<int>();
+
+            // count how many boxes in each layer, building up from initial segments
+            int bi = 0;
+            while (N > 1) {
+                int layer_boxes = (N / 2) + (N % 2 == 0 ? 0 : 1);
+                boxCount += layer_boxes;
+                N = layer_boxes;
+
+                layer_counts.Add(layer_boxes);
+                bi += layer_boxes;
+                layers++;
+            }
+            // [RMS] this case happens if N = 1, previous loop is skipped and we have to 
+            // hardcode initialization to this redundant box
+            if ( layers == 0 ) {
+                layers = 1;
+                boxCount = 1;
+                layer_counts = new List<int>() { 1 };
+            }
+
+            boxes = new Box3d[boxCount];
+            bi = 0;
+
+            // make first layer
+            int NStop = (curve.Closed) ? NV : NV - 1;
+            for (int si = 0; si < NStop; si += 2) {
+                Vector3d v1 = curve[(si + 1) % NV];
+                Segment3d seg1 = new Segment3d(curve[si], v1);
+                Box3d box = new Box3d(seg1);
+                if (si < NV - 1) {
+                    Segment3d seg2 = new Segment3d(v1, curve[(si + 2) % NV]);
+                    Box3d box2 = new Box3d(seg2);
+                    box = Box3d.Merge(ref box, ref box2);
+                }
+                boxes[bi++] = box;
+            }
+
+            // repeatedly build layers until we hit a single box
+            N = bi;
+            if (N == 1)
+                return;
+            int prev_layer_start = 0;
+            bool done = false;
+            while (done == false) {
+                int layer_start = bi;
+
+                for (int k = 0; k < N; k += 2) {
+                    Box3d mbox = Box3d.Merge(ref boxes[prev_layer_start + k], ref boxes[prev_layer_start + k + 1]);
+                    boxes[bi++] = mbox;
+                }
+
+                N = (N / 2) + (N % 2 == 0 ? 0 : 1);
+                prev_layer_start = layer_start;
+                if (N == 1)
+                    done = true;
+            }
+        }
+
+
+    }
+}
diff --git a/spatial/DCurveProjection.cs b/spatial/DCurveProjection.cs
index 92501f68..87b36d7a 100644
--- a/spatial/DCurveProjection.cs
+++ b/spatial/DCurveProjection.cs
@@ -20,8 +20,9 @@ public Vector3d Project(Vector3d vPoint, int identifier = -1)
             Vector3d vNearest = Vector3d.Zero;
             double fNearestSqr = double.MaxValue;
 
-            int N = (Curve.Closed) ? Curve.VertexCount : Curve.VertexCount - 1;
-            for ( int i = 0; i < N; ++i ) {
+            int N = Curve.VertexCount;
+            int NStop = (Curve.Closed) ? N : N - 1;
+            for ( int i = 0; i < NStop; ++i ) {
                 Segment3d seg = new Segment3d(Curve[i], Curve[(i + 1) % N]);
                 Vector3d pt = seg.NearestPoint(vPoint);
                 double dsqr = pt.DistanceSquared(vPoint);
diff --git a/spatial/DMeshAABBTree.cs b/spatial/DMeshAABBTree.cs
index 90d1ac0c..b28dcdef 100644
--- a/spatial/DMeshAABBTree.cs
+++ b/spatial/DMeshAABBTree.cs
@@ -25,13 +25,14 @@ namespace g3
     ///   - FindNearestTriangles(otherAABBTree, maxdist)
     ///   - IsInside(point)
     ///   - WindingNumber(point)
+    ///   - FastWindingNumber(point)
     ///   - DoTraversal(generic_traversal_object)
     /// 
     /// </summary>
     public class DMeshAABBTree3 : ISpatial
     {
-        DMesh3 mesh;
-        int mesh_timestamp;
+        protected DMesh3 mesh;
+        protected int mesh_timestamp;
 
         public DMeshAABBTree3(DMesh3 m, bool autoBuild = false)
         {
@@ -44,7 +45,9 @@ public DMeshAABBTree3(DMesh3 m, bool autoBuild = false)
         public DMesh3 Mesh { get { return mesh; } }
 
 
-        // if non-null, return false to ignore certain triangles
+        /// <summary>
+        /// If non-null, only triangle IDs that pass this filter (ie filter is true) are considered
+        /// </summary>
         public Func<int, bool> TriangleFilterF = null;
 
 
@@ -103,6 +106,8 @@ public void Build(BuildStrategy eStrategy = BuildStrategy.TopDownMidpoint,
         }
 
 
+        public bool IsValid { get { return mesh_timestamp == mesh.ShapeTimestamp; } }
+
 
         /// <summary>
         /// Does this ISpatial implementation support nearest-point query? (yes)
@@ -124,6 +129,20 @@ public virtual int FindNearestTriangle(Vector3d p, double fMaxDist = double.MaxV
             find_nearest_tri(root_index, p, ref fNearestSqr, ref tNearID);
             return tNearID;
         }
+        /// <summary>
+        /// Find the triangle closest to p, and distance to it, within distance fMaxDist, or return InvalidID
+        /// Use MeshQueries.TriangleDistance() to get more information
+        /// </summary>
+        public virtual int FindNearestTriangle(Vector3d p, out double fNearestDistSqr, double fMaxDist = double.MaxValue)
+        {
+            if (mesh_timestamp != mesh.ShapeTimestamp)
+                throw new Exception("DMeshAABBTree3.FindNearestTriangle: mesh has been modified since tree construction");
+
+            fNearestDistSqr = (fMaxDist < double.MaxValue) ? fMaxDist * fMaxDist : double.MaxValue;
+            int tNearID = DMesh3.InvalidID;
+            find_nearest_tri(root_index, p, ref fNearestDistSqr, ref tNearID);
+            return tNearID;
+        }
         protected void find_nearest_tri(int iBox, Vector3d p, ref double fNearestSqr, ref int tID)
         {
             int idx = box_to_index[iBox];
@@ -174,6 +193,74 @@ protected void find_nearest_tri(int iBox, Vector3d p, ref double fNearestSqr, re
 
 
 
+        /// <summary>
+        /// Find the vertex closest to p, within distance fMaxDist, or return InvalidID
+        /// </summary>
+        public virtual int FindNearestVertex(Vector3d p, double fMaxDist = double.MaxValue)
+        {
+            if (mesh_timestamp != mesh.ShapeTimestamp)
+                throw new Exception("DMeshAABBTree3.FindNearestVertex: mesh has been modified since tree construction");
+
+            double fNearestSqr = (fMaxDist < double.MaxValue) ? fMaxDist * fMaxDist : double.MaxValue;
+            int vNearID = DMesh3.InvalidID;
+            find_nearest_vtx(root_index, p, ref fNearestSqr, ref vNearID);
+            return vNearID;
+        }
+        protected void find_nearest_vtx(int iBox, Vector3d p, ref double fNearestSqr, ref int vid)
+        {
+            int idx = box_to_index[iBox];
+            if (idx < triangles_end) {            // triange-list case, array is [N t1 t2 ... tN]
+                int num_tris = index_list[idx];
+                for (int i = 1; i <= num_tris; ++i) {
+                    int ti = index_list[idx + i];
+                    if (TriangleFilterF != null && TriangleFilterF(ti) == false)
+                        continue;
+                    Vector3i tv = mesh.GetTriangle(ti);
+                    for ( int j = 0; j < 3; ++j ) {
+                        double dsqr = mesh.GetVertex(tv[j]).DistanceSquared(ref p);
+                        if (  dsqr < fNearestSqr ) {
+                            fNearestSqr = dsqr;
+                            vid = tv[j];
+                        }
+                    }
+                }
+
+            } else {                                // internal node, either 1 or 2 child boxes
+                int iChild1 = index_list[idx];
+                if (iChild1 < 0) {                 // 1 child, descend if nearer than cur min-dist
+                    iChild1 = (-iChild1) - 1;
+                    double fChild1DistSqr = box_distance_sqr(iChild1, p);
+                    if (fChild1DistSqr <= fNearestSqr)
+                        find_nearest_vtx(iChild1, p, ref fNearestSqr, ref vid);
+
+                } else {                            // 2 children, descend closest first
+                    iChild1 = iChild1 - 1;
+                    int iChild2 = index_list[idx + 1] - 1;
+
+                    double fChild1DistSqr = box_distance_sqr(iChild1, p);
+                    double fChild2DistSqr = box_distance_sqr(iChild2, p);
+                    if (fChild1DistSqr < fChild2DistSqr) {
+                        if (fChild1DistSqr < fNearestSqr) {
+                            find_nearest_vtx(iChild1, p, ref fNearestSqr, ref vid);
+                            if (fChild2DistSqr < fNearestSqr)
+                                find_nearest_vtx(iChild2, p, ref fNearestSqr, ref vid);
+                        }
+                    } else {
+                        if (fChild2DistSqr < fNearestSqr) {
+                            find_nearest_vtx(iChild2, p, ref fNearestSqr, ref vid);
+                            if (fChild1DistSqr < fNearestSqr)
+                                find_nearest_vtx(iChild1, p, ref fNearestSqr, ref vid);
+                        }
+                    }
+
+                }
+            }
+        }
+
+
+
+
+
         /// <summary>
         /// Does this ISpatial implementation support ray-triangle intersection? (yes)
         /// </summary>
@@ -210,15 +297,21 @@ protected void find_hit_triangle(int iBox, ref Ray3d ray, ref double fNearestT,
                     if (TriangleFilterF != null && TriangleFilterF(ti) == false)
                         continue;
 
-                    // [TODO] optimize this
                     mesh.GetTriVertices(ti, ref tri.V0, ref tri.V1, ref tri.V2);
-                    IntrRay3Triangle3 ray_tri_hit = new IntrRay3Triangle3(ray, tri);
-                    if ( ray_tri_hit.Find() ) {
-                        if ( ray_tri_hit.RayParameter < fNearestT ) {
-                            fNearestT = ray_tri_hit.RayParameter;
+                    double rayt;
+                    if (IntrRay3Triangle3.Intersects(ref ray, ref tri.V0, ref tri.V1, ref tri.V2, out rayt)) {
+                        if (rayt < fNearestT) {
+                            fNearestT = rayt;
                             tID = ti;
                         }
                     }
+                    //IntrRay3Triangle3 ray_tri_hit = new IntrRay3Triangle3(ray, tri);
+                    //if ( ray_tri_hit.Find() ) {
+                    //    if ( ray_tri_hit.RayParameter < fNearestT ) {
+                    //        fNearestT = ray_tri_hit.RayParameter;
+                    //        tID = ti;
+                    //    }
+                    //}
                 }
 
             } else {                                // internal node, either 1 or 2 child boxes
@@ -297,16 +390,23 @@ protected int find_all_hit_triangles(int iBox, List<int> hitTriangles, ref Ray3d
                     if (TriangleFilterF != null && TriangleFilterF(ti) == false)
                         continue;
 
-                    // [TODO] optimize this
                     mesh.GetTriVertices(ti, ref tri.V0, ref tri.V1, ref tri.V2);
-                    IntrRay3Triangle3 ray_tri_hit = new IntrRay3Triangle3(ray, tri);
-                    if (ray_tri_hit.Find()) {
-                        if (ray_tri_hit.RayParameter < fMaxDist) {
+                    double rayt;
+                    if (IntrRay3Triangle3.Intersects(ref ray, ref tri.V0, ref tri.V1, ref tri.V2, out rayt)) {
+                        if (rayt < fMaxDist) {
                             if (hitTriangles != null)
                                 hitTriangles.Add(ti);
                             hit_count++;
                         }
                     }
+                    //IntrRay3Triangle3 ray_tri_hit = new IntrRay3Triangle3(ray, tri);
+                    //if (ray_tri_hit.Find()) {
+                    //    if (ray_tri_hit.RayParameter < fMaxDist) {
+                    //        if (hitTriangles != null)
+                    //            hitTriangles.Add(ti);
+                    //        hit_count++;
+                    //    }
+                    //}
                 }
 
             } else {                                // internal node, either 1 or 2 child boxes
@@ -395,9 +495,7 @@ protected int find_any_intersection(int iBox, ref Triangle3d triangle, ref AxisA
                     if (TriangleFilterF != null && TriangleFilterF(ti) == false)
                         continue;
                     mesh.GetTriVertices(ti, ref box_tri.V0, ref box_tri.V1, ref box_tri.V2);
-
-                    IntrTriangle3Triangle3 intr = new IntrTriangle3Triangle3(triangle, box_tri);
-                    if (intr.Test())
+                    if ( IntrTriangle3Triangle3.Intersects(ref triangle, ref box_tri))
                         return ti;
                 }
             } else {                                // internal node, either 1 or 2 child boxes
@@ -454,7 +552,7 @@ protected bool find_any_intersection(int iBox, DMeshAABBTree3 otherTree, Func<Ve
                 int num_tris = index_list[idx], onum_tris = otherTree.index_list[odx];
 
                 // can re-use because Test() doesn't cache anything
-                IntrTriangle3Triangle3 intr = new IntrTriangle3Triangle3(new Triangle3d(), new Triangle3d());
+                //IntrTriangle3Triangle3 intr = new IntrTriangle3Triangle3(new Triangle3d(), new Triangle3d());
 
                 // outer iteration is "other" tris that need to be transformed (more expensive)
                 for (int j = 1; j <= onum_tris; ++j) {
@@ -467,7 +565,6 @@ protected bool find_any_intersection(int iBox, DMeshAABBTree3 otherTree, Func<Ve
                         otri.V1 = TransformF(otri.V1);
                         otri.V2 = TransformF(otri.V2);
                     }
-                    intr.Triangle0 = otri;
 
                     // inner iteration over "our" triangles
                     for (int i = 1; i <= num_tris; ++i) {
@@ -475,8 +572,7 @@ protected bool find_any_intersection(int iBox, DMeshAABBTree3 otherTree, Func<Ve
                         if (TriangleFilterF != null && TriangleFilterF(ti) == false)
                             continue;
                         mesh.GetTriVertices(ti, ref tri.V0, ref tri.V1, ref tri.V2);
-                        intr.Triangle1 = tri;
-                        if (intr.Test())
+                        if (IntrTriangle3Triangle3.Intersects(ref otri, ref tri))
                             return true;
                     }
                 }
@@ -587,13 +683,15 @@ public virtual IntersectionsQueryResult FindAllIntersections(DMeshAABBTree3 othe
             result.Points = new List<PointIntersection>();
             result.Segments = new List<SegmentIntersection>();
 
-            find_intersections(root_index, otherTree, TransformF, otherTree.root_index, 0, result);
+            IntrTriangle3Triangle3 intr = new IntrTriangle3Triangle3(new Triangle3d(), new Triangle3d());
+            find_intersections(root_index, otherTree, TransformF, otherTree.root_index, 0, intr, result);
 
             return result;
         }
 
         protected void find_intersections(int iBox, DMeshAABBTree3 otherTree, Func<Vector3d, Vector3d> TransformF, 
-                                int oBox, int depth, IntersectionsQueryResult result)
+                                          int oBox, int depth,
+                                          IntrTriangle3Triangle3 intr, IntersectionsQueryResult result)
         {
             int idx = box_to_index[iBox];
             int odx = otherTree.box_to_index[oBox];
@@ -603,9 +701,6 @@ protected void find_intersections(int iBox, DMeshAABBTree3 otherTree, Func<Vecto
                 Triangle3d tri = new Triangle3d(), otri = new Triangle3d();
                 int num_tris = index_list[idx], onum_tris = otherTree.index_list[odx];
 
-                // can re-use
-                IntrTriangle3Triangle3 intr = new IntrTriangle3Triangle3(new Triangle3d(), new Triangle3d());
-
                 // outer iteration is "other" tris that need to be transformed (more expensive)
                 for (int j = 1; j <= onum_tris; ++j) {
                     int tj = otherTree.index_list[odx + j];
@@ -627,16 +722,20 @@ protected void find_intersections(int iBox, DMeshAABBTree3 otherTree, Func<Vecto
                         mesh.GetTriVertices(ti, ref tri.V0, ref tri.V1, ref tri.V2);
                         intr.Triangle1 = tri;
 
-                        if (intr.Find()) {
-                            if (intr.Quantity == 1) {
-                                result.Points.Add(new PointIntersection() 
-                                        { t0 = ti, t1 = tj, point = intr.Points[0] });
-                            } else if (intr.Quantity == 2) {
-                                result.Segments.Add( new SegmentIntersection() 
-                                        { t0 = ti, t1 = tj, point0 = intr.Points[0], point1 = intr.Points[1] });
-                            } else {
-                                throw new Exception("DMeshAABBTree.find_intersections: found quantity " + intr.Quantity );
-                            }
+                        // [RMS] Test() is much faster than Find() so it makes sense to call it first, as most
+                        // triangles will not intersect (right?)
+                        if (intr.Test()) {
+                            if ( intr.Find() ) { 
+                                if (intr.Quantity == 1) {
+                                    result.Points.Add(new PointIntersection() 
+                                            { t0 = ti, t1 = tj, point = intr.Points[0] });
+                                } else if (intr.Quantity == 2) {
+                                    result.Segments.Add( new SegmentIntersection() 
+                                            { t0 = ti, t1 = tj, point0 = intr.Points[0], point1 = intr.Points[1] });
+                                } else {
+                                    throw new Exception("DMeshAABBTree.find_intersections: found quantity " + intr.Quantity );
+                                }
+                                }
                         }
                     }
                 }
@@ -667,19 +766,19 @@ protected void find_intersections(int iBox, DMeshAABBTree3 otherTree, Func<Vecto
                     oChild1 = (-oChild1) - 1;
                     AxisAlignedBox3d oChild1Box = otherTree.get_boxd(oChild1, TransformF);
                     if (oChild1Box.Intersects(bounds) )
-                        find_intersections(iBox, otherTree, TransformF, oChild1, depth + 1, result);
+                        find_intersections(iBox, otherTree, TransformF, oChild1, depth + 1, intr, result);
 
                 } else {                            // 2 children
                     oChild1 = oChild1 - 1;
 
                     AxisAlignedBox3d oChild1Box = otherTree.get_boxd(oChild1, TransformF);
                     if ( oChild1Box.Intersects(bounds) ) 
-                        find_intersections(iBox, otherTree, TransformF, oChild1, depth + 1, result);
+                        find_intersections(iBox, otherTree, TransformF, oChild1, depth + 1, intr, result);
 
                     int oChild2 = otherTree.index_list[odx + 1] - 1;
                     AxisAlignedBox3d oChild2Box = otherTree.get_boxd(oChild2, TransformF);
                     if ( oChild2Box.Intersects(bounds) )
-                        find_intersections(iBox, otherTree, TransformF, oChild2, depth + 1, result);
+                        find_intersections(iBox, otherTree, TransformF, oChild2, depth + 1, intr, result);
                 }
 
             } else {
@@ -690,16 +789,16 @@ protected void find_intersections(int iBox, DMeshAABBTree3 otherTree, Func<Vecto
                 if ( iChild1 < 0 ) {                 // 1 child, descend if nearer than cur min-dist
                     iChild1 = (-iChild1) - 1;
                     if ( box_box_intersect(iChild1, ref oBounds) )
-                        find_intersections(iChild1, otherTree, TransformF, oBox, depth + 1, result);
+                        find_intersections(iChild1, otherTree, TransformF, oBox, depth + 1, intr, result);
 
                 } else {                            // 2 children
                     iChild1 = iChild1 - 1;          
                     if ( box_box_intersect(iChild1, ref oBounds) ) 
-                        find_intersections(iChild1, otherTree, TransformF, oBox, depth + 1, result);
+                        find_intersections(iChild1, otherTree, TransformF, oBox, depth + 1, intr, result);
 
                     int iChild2 = index_list[idx + 1] - 1;
                     if ( box_box_intersect(iChild2, ref oBounds) )
-                        find_intersections(iChild2, otherTree, TransformF, oBox, depth + 1, result);
+                        find_intersections(iChild2, otherTree, TransformF, oBox, depth + 1, intr, result);
                 }
 
             }
@@ -1186,6 +1285,219 @@ protected void collect_triangles(int iBox, HashSet<int> triangles)
 
 
 
+
+
+
+        /*
+          *  Fast Mesh Winding Number computation
+          */
+
+        /// <summary>
+        /// FWN beta parameter - is 2.0 in paper
+        /// </summary>
+        public double FWNBeta = 2.0;
+
+        /// <summary>
+        /// FWN approximation order. can be 1 or 2. 2 is more accurate, obviously.
+        /// </summary>
+        public int FWNApproxOrder = 2;
+
+
+        /// <summary>
+        /// Fast approximation of winding number using far-field approximations
+        /// </summary>
+        public virtual double FastWindingNumber(Vector3d p)
+        {
+            if (mesh_timestamp != mesh.ShapeTimestamp)
+                throw new Exception("DMeshAABBTree3.FastWindingNumber: mesh has been modified since tree construction");
+
+            if (FastWindingCache == null || fast_winding_cache_timestamp != mesh.ShapeTimestamp) {
+                build_fast_winding_cache();
+                fast_winding_cache_timestamp = mesh.ShapeTimestamp;
+            }
+
+            double sum = branch_fast_winding_num(root_index, p);
+            return sum;
+        }
+
+        // evaluate winding number contribution for all triangles below iBox
+        protected double branch_fast_winding_num(int iBox, Vector3d p)
+        {
+            Vector3d a = Vector3d.Zero, b = Vector3d.Zero, c = Vector3d.Zero;
+            double branch_sum = 0;
+
+            int idx = box_to_index[iBox];
+            if (idx < triangles_end) {            // triange-list case, array is [N t1 t2 ... tN]
+                int num_tris = index_list[idx];
+                for (int i = 1; i <= num_tris; ++i) {
+                    int ti = index_list[idx + i];
+                    mesh.GetTriVertices(ti, ref a, ref b, ref c);
+                    branch_sum += MathUtil.TriSolidAngle(a, b, c, ref p) / MathUtil.FourPI;
+                }
+
+            } else {                                // internal node, either 1 or 2 child boxes
+                int iChild1 = index_list[idx];
+                if (iChild1 < 0) {                 // 1 child, descend if nearer than cur min-dist
+                    iChild1 = (-iChild1) - 1;
+
+                    // if we have winding cache, we can more efficiently compute contribution of all triangles
+                    // below this box. Otherwise, recursively descend tree.
+                    bool contained = box_contains(iChild1, p);
+                    if (contained == false && can_use_fast_winding_cache(iChild1, ref p))
+                        branch_sum += evaluate_box_fast_winding_cache(iChild1, ref p);
+                    else
+                        branch_sum += branch_fast_winding_num(iChild1, p);
+
+                } else {                            // 2 children, descend closest first
+                    iChild1 = iChild1 - 1;
+                    int iChild2 = index_list[idx + 1] - 1;
+
+                    bool contained1 = box_contains(iChild1, p);
+                    if (contained1 == false && can_use_fast_winding_cache(iChild1, ref p))
+                        branch_sum += evaluate_box_fast_winding_cache(iChild1, ref p);
+                    else
+                        branch_sum += branch_fast_winding_num(iChild1, p);
+
+                    bool contained2 = box_contains(iChild2, p);
+                    if (contained2 == false && can_use_fast_winding_cache(iChild2, ref p))
+                        branch_sum += evaluate_box_fast_winding_cache(iChild2, ref p);
+                    else
+                        branch_sum += branch_fast_winding_num(iChild2, p);
+                }
+            }
+
+            return branch_sum;
+        }
+
+
+        struct FWNInfo
+        {
+            public Vector3d Center;
+            public double R;
+            public Vector3d Order1Vec;
+            public Matrix3d Order2Mat;
+        }
+
+        Dictionary<int, FWNInfo> FastWindingCache;
+        int fast_winding_cache_timestamp = -1;
+
+        protected void build_fast_winding_cache()
+        {
+            // set this to a larger number to ignore caches if number of triangles is too small.
+            // (seems to be no benefit to doing this...is holdover from tree-decomposition FWN code)
+            int WINDING_CACHE_THRESH = 1;
+
+            //MeshTriInfoCache triCache = null;
+            MeshTriInfoCache triCache = new MeshTriInfoCache(mesh);
+
+            FastWindingCache = new Dictionary<int, FWNInfo>();
+            HashSet<int> root_hash;
+            build_fast_winding_cache(root_index, 0, WINDING_CACHE_THRESH, out root_hash, triCache);
+        }
+        protected int build_fast_winding_cache(int iBox, int depth, int tri_count_thresh, out HashSet<int> tri_hash, MeshTriInfoCache triCache)
+        {
+            tri_hash = null;
+
+            int idx = box_to_index[iBox];
+            if (idx < triangles_end) {            // triange-list case, array is [N t1 t2 ... tN]
+                int num_tris = index_list[idx];
+                return num_tris;
+
+            } else {                                // internal node, either 1 or 2 child boxes
+                int iChild1 = index_list[idx];
+                if (iChild1 < 0) {                 // 1 child, descend if nearer than cur min-dist
+                    iChild1 = (-iChild1) - 1;
+                    int num_child_tris = build_fast_winding_cache(iChild1, depth + 1, tri_count_thresh, out tri_hash, triCache);
+
+                    // if count in child is large enough, we already built a cache at lower node
+                    return num_child_tris;
+
+                } else {                            // 2 children, descend closest first
+                    iChild1 = iChild1 - 1;
+                    int iChild2 = index_list[idx + 1] - 1;
+
+                    // let each child build its own cache if it wants. If so, it will return the
+                    // list of its child tris
+                    HashSet<int> child2_hash;
+                    int num_tris_1 = build_fast_winding_cache(iChild1, depth + 1, tri_count_thresh, out tri_hash, triCache);
+                    int num_tris_2 = build_fast_winding_cache(iChild2, depth + 1, tri_count_thresh, out child2_hash, triCache);
+                    bool build_cache = (num_tris_1 + num_tris_2 > tri_count_thresh);
+
+                    if (depth == 0)
+                        return num_tris_1 + num_tris_2;  // cannot build cache at level 0...
+
+                    // collect up the triangles we need. there are various cases depending on what children already did
+                    if (tri_hash != null || child2_hash != null || build_cache) {
+                        if (tri_hash == null && child2_hash != null) {
+                            collect_triangles(iChild1, child2_hash);
+                            tri_hash = child2_hash;
+                        } else {
+                            if (tri_hash == null) {
+                                tri_hash = new HashSet<int>();
+                                collect_triangles(iChild1, tri_hash);
+                            }
+                            if (child2_hash == null)
+                                collect_triangles(iChild2, tri_hash);
+                            else
+                                tri_hash.UnionWith(child2_hash);
+                        }
+                    }
+                    if (build_cache)
+                        make_box_fast_winding_cache(iBox, tri_hash, triCache);
+
+                    return (num_tris_1 + num_tris_2);
+                }
+            }
+        }
+
+
+        // check if we can use fwn 
+        protected bool can_use_fast_winding_cache(int iBox, ref Vector3d q)
+        {
+            FWNInfo cacheInfo;
+            if (FastWindingCache.TryGetValue(iBox, out cacheInfo) == false)
+                return false;
+
+            double dist_qp = cacheInfo.Center.Distance(ref q);
+            if (dist_qp > FWNBeta * cacheInfo.R)
+                return true;
+
+            return false;
+        }
+
+
+        // compute FWN cache for all triangles underneath this box
+        protected void make_box_fast_winding_cache(int iBox, IEnumerable<int> triangles, MeshTriInfoCache triCache)
+        {
+            Util.gDevAssert(FastWindingCache.ContainsKey(iBox) == false);
+
+            // construct cache
+            FWNInfo cacheInfo = new FWNInfo();
+            FastTriWinding.ComputeCoeffs(Mesh, triangles, ref cacheInfo.Center, ref cacheInfo.R, ref cacheInfo.Order1Vec, ref cacheInfo.Order2Mat, triCache);
+
+            FastWindingCache[iBox] = cacheInfo;
+        }
+
+        // evaluate the FWN cache for iBox
+        protected double evaluate_box_fast_winding_cache(int iBox, ref Vector3d q)
+        {
+            FWNInfo cacheInfo = FastWindingCache[iBox];
+
+            if (FWNApproxOrder == 2)
+                return FastTriWinding.EvaluateOrder2Approx(ref cacheInfo.Center, ref cacheInfo.Order1Vec, ref cacheInfo.Order2Mat, ref q);
+            else
+                return FastTriWinding.EvaluateOrder1Approx(ref cacheInfo.Center, ref cacheInfo.Order1Vec, ref q);
+        }
+
+
+
+
+
+
+
+
+
+
         /// <summary>
         /// Total sum of volumes of all boxes in the tree. Mainly useful to evaluate tree quality.
         /// </summary>
@@ -1219,6 +1531,13 @@ public double TotalExtentSum()
         }
 
 
+        /// <summary>
+        /// Root bounding box of tree (note: tree must be generated by calling a query function first!)
+        /// </summary>
+        public AxisAlignedBox3d Bounds {
+            get { return get_box(root_index); }
+        }
+
 
 
         //
@@ -1231,9 +1550,9 @@ public double TotalExtentSum()
         // storage for box nodes. 
         //   - box_to_index is a pointer into index_list
         //   - box_centers and box_extents are the centers/extents of the bounding boxes
-        DVector<int> box_to_index;
-        DVector<Vector3f> box_centers;
-        DVector<Vector3f> box_extents;
+        protected DVector<int> box_to_index;
+        protected DVector<Vector3f> box_centers;
+        protected DVector<Vector3f> box_extents;
 
         // list of indices for a given box. There is *no* marker/sentinel between
         // boxes, you have to get the starting index from box_to_index[]
@@ -1245,13 +1564,13 @@ public double TotalExtentSum()
         //       internal box, with index (-index_list[i])-1     (shift-by-one in case actual value is 0!)
         //   - if i > triangles_end and index_list[i] > 0, this is a two-child
         //       internal box, with indices index_list[i]-1 and index_list[i+1]-1
-        DVector<int> index_list;
+        protected DVector<int> index_list;
 
         // index_list[i] for i < triangles_end is a triangle-index list, otherwise box-index pair/single
-        int triangles_end = -1;
+        protected int triangles_end = -1;
 
         // box_to_index[root_index] is the root node of the tree
-        int root_index = -1;
+        protected int root_index = -1;
 
 
 
@@ -1920,11 +2239,10 @@ double box_ray_intersect_t(int iBox, Ray3d ray)
             Vector3f e = box_extents[iBox];
             AxisAlignedBox3d box = new AxisAlignedBox3d(ref c, e.x + box_eps, e.y + box_eps, e.z + box_eps);
 
-            IntrRay3AxisAlignedBox3 intr = new IntrRay3AxisAlignedBox3(ray, box);
-            if (intr.Find()) {
-                return intr.RayParam0;
+            double ray_t = double.MaxValue;
+            if (IntrRay3AxisAlignedBox3.FindRayIntersectT(ref ray, ref box, out ray_t)) {
+                return ray_t;
             } else {
-                Debug.Assert(intr.Result != IntersectionResult.InvalidQuery);
                 return double.MaxValue;
             }
         }
@@ -1960,7 +2278,7 @@ double box_distance_sqr(int iBox, Vector3d p)
         }
 
 
-        bool box_contains(int iBox, Vector3d p)
+        protected bool box_contains(int iBox, Vector3d p)
         {
             // [TODO] this could be way faster...
             Vector3d c = (Vector3d)box_centers[iBox];
diff --git a/spatial/DenseGrid2.cs b/spatial/DenseGrid2.cs
new file mode 100644
index 00000000..84eefdc1
--- /dev/null
+++ b/spatial/DenseGrid2.cs
@@ -0,0 +1,280 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+
+namespace g3
+{
+
+    /// <summary>
+    /// 2D dense grid of floating-point scalar values. 
+    /// </summary>
+    public class DenseGrid2f
+    {
+        public float[] Buffer;
+        public int ni, nj;
+
+        public DenseGrid2f()
+        {
+            ni = nj = 0;
+        }
+
+        public DenseGrid2f(int ni, int nj, float initialValue)
+        {
+            resize(ni, nj);
+            assign(initialValue);
+        }
+
+        public DenseGrid2f(DenseGrid2f copy)
+        {
+            Buffer = new float[copy.Buffer.Length];
+            Array.Copy(copy.Buffer, Buffer, Buffer.Length);
+            ni = copy.ni; nj = copy.nj;
+        }
+
+        public void swap(DenseGrid2f g2)
+        {
+            Util.gDevAssert(ni == g2.ni && nj == g2.nj);
+            var tmp = g2.Buffer;
+            g2.Buffer = this.Buffer;
+            this.Buffer = tmp;
+        }
+
+        public int size { get { return ni * nj; } }
+
+        public void resize(int ni, int nj)
+        {
+            Buffer = new float[ni * nj];
+            this.ni = ni; this.nj = nj;
+        }
+
+        public void assign(float value)
+        {
+            for (int i = 0; i < Buffer.Length; ++i)
+                Buffer[i] = value;
+        }
+
+        public void assign_border(float value, int rings)
+        {
+            for ( int j = 0; j < rings; ++j ) {
+                int jb = nj - 1 - j;
+                for ( int i = 0; i < ni; ++i ) {
+                    Buffer[i + ni * j] = value;
+                    Buffer[i + ni * jb] = value;
+                }
+            }
+            int stop = nj - 1 - rings;
+            for ( int j = rings; j < stop; ++j ) {
+                for ( int i = 0; i < rings; ++i ) {
+                    Buffer[i + ni * j] = value;
+                    Buffer[(ni - 1 - i) + ni * j] = value;
+                }
+            }
+        }
+
+
+        public void clear() {
+            Array.Clear(Buffer, 0, Buffer.Length);
+        }
+
+        public void copy(DenseGrid2f copy)
+        {
+            Array.Copy(copy.Buffer, this.Buffer, this.Buffer.Length);
+        }
+
+        public float this[int i] {
+            get { return Buffer[i]; }
+            set { Buffer[i] = value; }
+        }
+
+        public float this[int i, int j] {
+            get { return Buffer[i + ni * j]; }
+            set { Buffer[i + ni * j] = value; }
+        }
+
+        public float this[Vector2i ijk] {
+            get { return Buffer[ijk.x + ni * ijk.y]; }
+            set { Buffer[ijk.x + ni * ijk.y] = value; }
+        }
+
+        public void get_x_pair(int i0, int j, out double a, out double b)
+        {
+            int offset = ni * j;
+            a = Buffer[offset + i0];
+            b = Buffer[offset + i0 + 1];
+        }
+
+        public void apply(Func<float, float> f)
+        {
+            for (int j = 0; j < nj; j++ ) {
+                for ( int i = 0; i < ni; i++ ) {
+                    int idx = i + ni * j;
+                    Buffer[idx] = f(Buffer[idx]);
+                }
+            }
+        }
+
+        public void set_min(DenseGrid2f grid2)
+        {
+            for (int k = 0; k < Buffer.Length; ++k)
+                Buffer[k] = Math.Min(Buffer[k], grid2.Buffer[k]);
+        }
+        public void set_max(DenseGrid2f grid2)
+        {
+            for (int k = 0; k < Buffer.Length; ++k)
+                Buffer[k] = Math.Max(Buffer[k], grid2.Buffer[k]);
+        }
+
+        public AxisAlignedBox2i Bounds {
+            get { return new AxisAlignedBox2i(0, 0, ni, nj); }
+        }
+
+
+        public IEnumerable<Vector2i> Indices()
+        {
+            for (int y = 0; y < nj; ++y) {
+                for (int x = 0; x < ni; ++x)
+                    yield return new Vector2i(x, y);
+            }
+        }
+
+
+        public IEnumerable<Vector2i> InsetIndices(int border_width)
+        {
+            int stopy = nj - border_width, stopx = ni - border_width;
+            for (int y = border_width; y < stopy; ++y) {
+                for (int x = border_width; x < stopx; ++x)
+                    yield return new Vector2i(x, y);
+            }
+        }
+
+
+    }
+
+
+
+
+
+
+    /// <summary>
+    /// 2D dense grid of integers. 
+    /// </summary>
+    public class DenseGrid2i
+    {
+        public int[] Buffer;
+        public int ni, nj;
+
+        public DenseGrid2i()
+        {
+            ni = nj = 0;
+        }
+
+        public DenseGrid2i(int ni, int nj, int initialValue)
+        {
+            resize(ni, nj);
+            assign(initialValue);
+        }
+
+        public DenseGrid2i(DenseGrid2i copy)
+        {
+            resize(copy.ni, copy.nj);
+            Array.Copy(copy.Buffer, this.Buffer, this.Buffer.Length);
+        }
+
+        public int size { get { return ni * nj; } }
+
+        public void resize(int ni, int nj)
+        {
+            Buffer = new int[ni * nj];
+            this.ni = ni; this.nj = nj;
+        }
+
+        public void clear() {
+            Array.Clear(Buffer, 0, Buffer.Length);
+        }
+
+
+        public void copy(DenseGrid2i copy)
+        {
+            Array.Copy(copy.Buffer, this.Buffer, this.Buffer.Length);
+        }
+
+        public void assign(int value)
+        {
+            for (int i = 0; i < Buffer.Length; ++i)
+                Buffer[i] = value;
+        }
+
+        public int this[int i] {
+            get { return Buffer[i]; }
+            set { Buffer[i] = value; }
+        }
+
+        public int this[int i, int j] {
+            get { return Buffer[i + ni * j]; }
+            set { Buffer[i + ni * j] = value; }
+        }
+
+        public int this[Vector2i ijk] {
+            get { return Buffer[ijk.x + ni * ijk.y]; }
+            set { Buffer[ijk.x + ni * ijk.y] = value; }
+        }
+
+        public void increment(int i, int j)
+        {
+            Buffer[i + ni * j]++;
+        }
+        public void decrement(int i, int j)
+        {
+            Buffer[i + ni * j]--;
+        }
+
+        public void atomic_increment(int i, int j)
+        {
+            System.Threading.Interlocked.Increment(ref Buffer[i + ni * j]);
+        }
+
+        public void atomic_decrement(int i, int j)
+        {
+            System.Threading.Interlocked.Decrement(ref Buffer[i + ni * j]);
+        }
+
+        public void atomic_incdec(int i, int j, bool decrement = false) {
+            if ( decrement )
+                System.Threading.Interlocked.Decrement(ref Buffer[i + ni * j]);
+            else
+                System.Threading.Interlocked.Increment(ref Buffer[i + ni * j]);
+        }
+
+        public int sum() {
+            int sum = 0;
+            for (int i = 0; i < Buffer.Length; ++i)
+                sum += Buffer[i];
+            return sum;
+        }
+
+
+        public IEnumerable<Vector2i> Indices()
+        {
+            for (int y = 0; y < nj; ++y) {
+                for (int x = 0; x < ni; ++x)
+                    yield return new Vector2i(x, y);
+            }
+        }
+
+
+        public IEnumerable<Vector2i> InsetIndices(int border_width)
+        {
+            int stopy = nj - border_width, stopx = ni - border_width;
+            for (int y = border_width; y < stopy; ++y) {
+                for (int x = border_width; x < stopx; ++x)
+                    yield return new Vector2i(x, y);
+            }
+        }
+
+    }
+
+
+
+}
diff --git a/spatial/DenseGrid3.cs b/spatial/DenseGrid3.cs
index 6ce6041d..2b36404c 100644
--- a/spatial/DenseGrid3.cs
+++ b/spatial/DenseGrid3.cs
@@ -6,12 +6,42 @@
 
 namespace g3
 {
-
+    /// <summary>
+    /// 3D dense grid of floating-point scalar values. 
+    /// </summary>
     public class DenseGrid3f
     {
         public float[] Buffer;
         public int ni, nj, nk;
 
+        public DenseGrid3f()
+        {
+            ni = nj = nk = 0;
+        }
+
+        public DenseGrid3f(int ni, int nj, int nk, float initialValue)
+        {
+            resize(ni, nj, nk);
+            assign(initialValue);
+        }
+
+        public DenseGrid3f(DenseGrid3f copy)
+        {
+            Buffer = new float[copy.Buffer.Length];
+            Array.Copy(copy.Buffer, Buffer, Buffer.Length);
+            ni = copy.ni; nj = copy.nj; nk = copy.nk;
+        }
+
+        public void swap(DenseGrid3f g2)
+        {
+            Util.gDevAssert(ni == g2.ni && nj == g2.nj && nk == g2.nk);
+            var tmp = g2.Buffer;
+            g2.Buffer = this.Buffer;
+            this.Buffer = tmp;
+        }
+
+        public int size { get { return ni * nj * nk; } }
+
         public void resize(int ni, int nj, int nk)
         {
             Buffer = new float[ni * nj * nk];
@@ -24,6 +54,24 @@ public void assign(float value)
                 Buffer[i] = value;
         }
 
+        public void set_min(ref Vector3i ijk, float f)
+        {
+            int idx = ijk.x + ni * (ijk.y + nj * ijk.z);
+            if (f < Buffer[idx])
+                Buffer[idx] = f;
+        }
+        public void set_max(ref Vector3i ijk, float f)
+        {
+            int idx = ijk.x + ni * (ijk.y + nj * ijk.z);
+            if (f > Buffer[idx])
+                Buffer[idx] = f;
+        }
+
+        public float this[int i] {
+            get { return Buffer[i]; }
+            set { Buffer[i] = value; }
+        }
+
         public float this[int i, int j, int k] {
             get { return Buffer[i + ni * (j + nj * k)]; }
             set { Buffer[i + ni * (j + nj * k)] = value; }
@@ -34,6 +82,12 @@ public float this[Vector3i ijk] {
             set { Buffer[ijk.x + ni * (ijk.y + nj * ijk.z)] = value; }
         }
 
+        public void get_x_pair(int i0, int j, int k, out float a, out float b)
+        {
+            int offset = ni * (j + nj * k);
+            a = Buffer[offset + i0];
+            b = Buffer[offset + i0 + 1];
+        }
         public void get_x_pair(int i0, int j, int k, out double a, out double b)
         {
             int offset = ni * (j + nj * k);
@@ -53,10 +107,55 @@ public void apply(Func<float, float> f)
             }
         }
 
+
+        public DenseGrid2f get_slice(int slice_i, int dimension)
+        {
+            DenseGrid2f slice;
+            if (dimension == 0) {
+                slice = new DenseGrid2f(nj, nk, 0);
+                for (int k = 0; k < nk; ++k)
+                    for (int j = 0; j < nj; ++j)
+                        slice[j, k] = Buffer[slice_i + ni * (j + nj * k)];
+            } else if (dimension == 1) {
+                slice = new DenseGrid2f(ni, nk, 0);
+                for (int k = 0; k < nk; ++k)
+                    for (int i = 0; i < ni; ++i)
+                        slice[i, k] = Buffer[i + ni * (slice_i + nj * k)];
+            } else {
+                slice = new DenseGrid2f(ni, nj, 0);
+                for (int j = 0; j < nj; ++j)
+                    for (int i = 0; i < ni; ++i)
+                        slice[i, j] = Buffer[i + ni * (j + nj * slice_i)];
+            }
+            return slice;
+        }
+
+
+        public void set_slice(DenseGrid2f slice, int slice_i, int dimension)
+        {
+            if (dimension == 0) {
+                for (int k = 0; k < nk; ++k)
+                    for (int j = 0; j < nj; ++j)
+                        Buffer[slice_i + ni * (j + nj * k)] = slice[j, k];
+            } else if (dimension == 1) {
+                for (int k = 0; k < nk; ++k)
+                    for (int i = 0; i < ni; ++i)
+                        Buffer[i + ni * (slice_i + nj * k)] = slice[i, k];
+            } else {
+                for (int j = 0; j < nj; ++j)
+                    for (int i = 0; i < ni; ++i)
+                        Buffer[i + ni * (j + nj * slice_i)] = slice[i, j];
+            }
+        }
+
+
+
         public AxisAlignedBox3i Bounds {
             get { return new AxisAlignedBox3i(0, 0, 0, ni, nj, nk); }
         }
-
+        public AxisAlignedBox3i BoundsInclusive {
+            get { return new AxisAlignedBox3i(0, 0, 0, ni-1, nj-1, nk-1); }
+        }
 
         public IEnumerable<Vector3i> Indices()
         {
@@ -81,26 +180,53 @@ public IEnumerable<Vector3i> InsetIndices(int border_width)
         }
 
 
+        public Vector3i to_index(int idx) {
+            int x = idx % ni;
+            int y = (idx / ni) % nj;
+            int z = idx / (ni * nj);
+            return new Vector3i(x, y, z);
+        }
+        public int to_linear(int i, int j, int k)
+        {
+            return i + ni * (j + nj * k);
+        }
+        public int to_linear(ref Vector3i ijk)
+        {
+            return ijk.x + ni * (ijk.y + nj * ijk.z);
+        }
+        public int to_linear(Vector3i ijk)
+        {
+            return ijk.x + ni * (ijk.y + nj * ijk.z);
+        }
 
     }
 
 
-    
 
 
 
 
+    /// <summary>
+    /// 3D dense grid of integers. 
+    /// </summary>
     public class DenseGrid3i
     {
         public int[] Buffer;
         public int ni, nj, nk;
 
+        public DenseGrid3i()
+        {
+            ni = nj = nk = 0;
+        }
+
         public DenseGrid3i(int ni, int nj, int nk, int initialValue)
         {
             resize(ni, nj, nk);
             assign(initialValue);
         }
 
+        public int size { get { return ni * nj * nk; } }
+
         public void resize(int ni, int nj, int nk)
         {
             Buffer = new int[ni * nj * nk];
@@ -113,11 +239,21 @@ public void assign(int value)
                 Buffer[i] = value;
         }
 
+        public int this[int i] {
+            get { return Buffer[i]; }
+            set { Buffer[i] = value; }
+        }
+
         public int this[int i, int j, int k] {
             get { return Buffer[i + ni * (j + nj * k)]; }
             set { Buffer[i + ni * (j + nj * k)] = value; }
         }
 
+        public int this[Vector3i ijk] {
+            get { return Buffer[ijk.x + ni * (ijk.y + nj * ijk.z)]; }
+            set { Buffer[ijk.x + ni * (ijk.y + nj * ijk.z)] = value; }
+        }
+
         public void increment(int i, int j, int k)
         {
             Buffer[i + ni * (j + nj * k)]++;
@@ -143,6 +279,69 @@ public void atomic_incdec(int i, int j, int k, bool decrement = false) {
             else
                 System.Threading.Interlocked.Increment(ref Buffer[i + ni * (j + nj * k)]);
         }
+
+
+
+        public DenseGrid2i get_slice(int slice_i, int dimension)
+        {
+            DenseGrid2i slice;
+            if ( dimension == 0 ) {
+                slice = new DenseGrid2i(nj, nk, 0);
+                for (int k = 0; k < nk; ++k)
+                    for (int j = 0; j < nj; ++j)
+                        slice[j, k] = Buffer[slice_i + ni * (j + nj * k)];
+            } else if (dimension == 1) {
+                slice = new DenseGrid2i(ni, nk, 0);
+                for (int k = 0; k < nk; ++k)
+                    for (int i = 0; i < ni; ++i)
+                        slice[i, k] = Buffer[i + ni * (slice_i + nj * k)];
+            } else {
+                slice = new DenseGrid2i(ni, nj, 0);
+                for (int j = 0; j < nj; ++j)
+                    for (int i = 0; i < ni; ++i)
+                        slice[i, j] = Buffer[i + ni * (j + nj * slice_i)];
+            }
+            return slice;
+        }
+
+
+        /// <summary>
+        /// convert to binary bitmap
+        /// </summary>
+        public Bitmap3 get_bitmap(int thresh = 0)
+        {
+            Bitmap3 bmp = new Bitmap3(new Vector3i(ni, nj, nk));
+            for (int i = 0; i < Buffer.Length; ++i)
+                bmp[i] = (Buffer[i] > thresh) ? true : false;
+            return bmp;
+        }
+
+
+        public IEnumerable<Vector3i> Indices()
+        {
+            for (int z = 0; z < nk; ++z) {
+                for (int y = 0; y < nj; ++y) {
+                    for (int x = 0; x < ni; ++x)
+                        yield return new Vector3i(x, y, z);
+                }
+            }
+        }
+
+
+        public IEnumerable<Vector3i> InsetIndices(int border_width)
+        {
+            int stopy = nj - border_width, stopx = ni - border_width;
+            for (int z = border_width; z < nk - border_width; ++z) {
+                for (int y = border_width; y < stopy; ++y) {
+                    for (int x = border_width; x < stopx; ++x)
+                        yield return new Vector3i(x, y, z);
+                }
+            }
+        }
+
+
+
+
     }
 
 
diff --git a/spatial/EditMeshSpatial.cs b/spatial/EditMeshSpatial.cs
new file mode 100644
index 00000000..862c5b38
--- /dev/null
+++ b/spatial/EditMeshSpatial.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using g3;
+
+namespace gs
+{
+    /// <summary>
+    /// For use case where we are making local edits to a source mesh. We mask out
+    /// removed triangles from base mesh SpatialDS, and raycast new triangles separately.
+    /// </summary>
+    public class EditMeshSpatial : ISpatial
+    {
+        public DMesh3 SourceMesh;
+        public DMeshAABBTree3 SourceSpatial;
+        public DMesh3 EditMesh;
+
+        HashSet<int> RemovedT = new HashSet<int>();
+        HashSet<int> AddedT = new HashSet<int>();
+
+        public void RemoveTriangle(int tid)
+        {
+            if ( AddedT.Contains(tid) ) {
+                AddedT.Remove(tid);
+            } else {
+                RemovedT.Add(tid);
+            }
+        }
+        
+        public void AddTriangle(int tid)
+        {
+            AddedT.Add(tid);
+        }
+
+
+        public bool SupportsNearestTriangle { get { return false; } }
+        public int FindNearestTriangle(Vector3d p, double fMaxDist = double.MaxValue) {
+            return DMesh3.InvalidID;
+        }
+
+        public bool SupportsPointContainment { get { return false; } }
+        public bool IsInside(Vector3d p) { return false; }
+
+
+        public bool SupportsTriangleRayIntersection { get { return true; } }
+
+        public int FindNearestHitTriangle(Ray3d ray, double fMaxDist = double.MaxValue)
+        {
+            var save_filter = SourceSpatial.TriangleFilterF;
+            SourceSpatial.TriangleFilterF = source_filter;
+            int hit_source_tid = SourceSpatial.FindNearestHitTriangle(ray);
+            SourceSpatial.TriangleFilterF = save_filter;
+
+            int hit_edit_tid;
+            IntrRay3Triangle3 edit_hit = find_added_hit(ref ray, out hit_edit_tid);
+
+            if (hit_source_tid == DMesh3.InvalidID && hit_edit_tid == DMesh3.InvalidID)
+                return DMesh3.InvalidID;
+            else if (hit_source_tid == DMesh3.InvalidID)
+                return hit_edit_tid;
+            else if (hit_edit_tid == DMesh3.InvalidID)
+                return hit_source_tid;
+
+            IntrRay3Triangle3 source_hit = (hit_source_tid != -1) ?
+                MeshQueries.TriangleIntersection(SourceMesh, hit_source_tid, ray) : null;
+            return (edit_hit.RayParameter < source_hit.RayParameter) ?
+                hit_edit_tid : hit_source_tid;
+        }
+
+        bool source_filter(int tid)
+        {
+            return RemovedT.Contains(tid) == false;
+        }
+
+
+        IntrRay3Triangle3 find_added_hit(ref Ray3d ray, out int hit_tid)
+        {
+            hit_tid = DMesh3.InvalidID;
+            IntrRay3Triangle3 nearest = null;
+            double dNearT = double.MaxValue;
+
+            Triangle3d tri = new Triangle3d();
+            foreach ( int tid in AddedT) {
+                Index3i tv = EditMesh.GetTriangle(tid);
+                tri.V0 = EditMesh.GetVertex(tv.a);
+                tri.V1 = EditMesh.GetVertex(tv.b);
+                tri.V2 = EditMesh.GetVertex(tv.c);
+                IntrRay3Triangle3 intr = new IntrRay3Triangle3(ray, tri);
+                if ( intr.Find() && intr.RayParameter < dNearT ) {
+                    dNearT = intr.RayParameter;
+                    hit_tid = tid;
+                    nearest = intr;
+                }
+            }
+            return nearest;
+        }
+
+
+
+    }
+}
diff --git a/spatial/GridIndexing.cs b/spatial/GridIndexing.cs
index 70de2004..b6e53b64 100644
--- a/spatial/GridIndexing.cs
+++ b/spatial/GridIndexing.cs
@@ -167,26 +167,26 @@ public FrameGridIndexer3(Frame3f frame, Vector3f cellSize)
 
         public Vector3i ToGrid(Vector3d point) {
             Vector3f pointf = (Vector3f)point;
-            pointf = GridFrame.ToFrameP(pointf);
+            pointf = GridFrame.ToFrameP(ref pointf);
             return (Vector3i)(pointf / CellSize);
         }
 
         public Vector3d ToGridf(Vector3d point) {
-            Vector3f pointf = (Vector3f)point;
-            pointf = GridFrame.ToFrameP(pointf);
-            return (pointf / CellSize);
+            point = GridFrame.ToFrameP(ref point);
+            point.x /= CellSize.x; point.y /= CellSize.y; point.z /= CellSize.z;
+            return point;
         }
 
         public Vector3d FromGrid(Vector3i gridpoint)
         {
             Vector3f pointf = CellSize * (Vector3f)gridpoint;
-            return (Vector3d)GridFrame.FromFrameP(pointf);
+            return (Vector3d)GridFrame.FromFrameP(ref pointf);
         }
 
         public Vector3d FromGrid(Vector3d gridpointf)
         {
             gridpointf *= CellSize;
-            return (Vector3d)GridFrame.FromFrameP(gridpointf);
+            return (Vector3d)GridFrame.FromFrameP(ref gridpointf);
         }
     }
 
diff --git a/spatial/MeshScalarSamplingGrid.cs b/spatial/MeshScalarSamplingGrid.cs
new file mode 100644
index 00000000..927c4ce8
--- /dev/null
+++ b/spatial/MeshScalarSamplingGrid.cs
@@ -0,0 +1,344 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using g3;
+
+namespace gs
+{
+
+    /// <summary>
+    /// Sample a scalar function on a discrete grid. Can sample full grid, or
+    /// compute values around a specific iso-contour and then fill in rest of grid
+    /// with correctly-signed values via fast sweeping (this is the default)
+    /// 
+    /// TODO: 
+    ///   - I think we are over-exploring the grid most of the time. eg along an x-ray that
+    ///     intersects the surface, we only need at most 2 cells, but we are computing at least 3,
+    ///     and possibly 5. 
+    ///   - it may be better to use something like bloomenthal polygonizer continuation? where we 
+    ///     are keeping track of active edges instead of active cells?
+    /// 
+    /// </summary>
+    public class MeshScalarSamplingGrid
+    {
+        public DMesh3 Mesh;
+        public Func<Vector3d, double> ScalarF;
+
+        // size of cubes in the grid
+        public double CellSize;
+
+        // how many cells around border should we keep
+        public int BufferCells = 1;
+
+        // Should we compute values at all grid cells (expensive!!) or only in narrow band.
+        // In narrow-band mode, we guess rest of values by propagating along x-rows
+        public enum ComputeModes
+        {
+            FullGrid = 0,
+            NarrowBand = 1
+        }
+        public ComputeModes ComputeMode = ComputeModes.NarrowBand;
+
+        // in narrow-band mode, if mesh is not closed, we will explore space around this iso-value
+        public float IsoValue = 0.5f;
+
+        // in NarrowBand mode, we compute mesh SDF grid, if true then it can be accessed
+        // via SDFGrid property after Compute()
+        public bool WantMeshSDFGrid = true;
+
+        /// <summary> if this function returns true, we should abort calculation </summary>
+        public Func<bool> CancelF = () => { return false; };
+
+        public bool DebugPrint = false;
+
+        // computed results
+        Vector3f grid_origin;
+        DenseGrid3f scalar_grid;
+
+        // sdf grid we compute in narrow-band mode
+        MeshSignedDistanceGrid mesh_sdf;
+
+
+        public MeshScalarSamplingGrid(DMesh3 mesh, double cellSize, Func<Vector3d, double> scalarF)
+        {
+            Mesh = mesh;
+            ScalarF = scalarF;
+            CellSize = cellSize;
+        }
+
+
+        public void Compute()
+        {
+            // figure out origin & dimensions
+            AxisAlignedBox3d bounds = Mesh.CachedBounds;
+
+            float fBufferWidth = 2 * BufferCells * (float)CellSize;
+            grid_origin = (Vector3f)bounds.Min - fBufferWidth * Vector3f.One;
+            Vector3f max = (Vector3f)bounds.Max + fBufferWidth * Vector3f.One;
+            int ni = (int)((max.x - grid_origin.x) / (float)CellSize) + 1;
+            int nj = (int)((max.y - grid_origin.y) / (float)CellSize) + 1;
+            int nk = (int)((max.z - grid_origin.z) / (float)CellSize) + 1;
+
+            scalar_grid = new DenseGrid3f();
+            if ( ComputeMode == ComputeModes.FullGrid )
+                make_grid_dense(grid_origin, (float)CellSize, ni, nj, nk, scalar_grid);
+            else
+                make_grid(grid_origin, (float)CellSize, ni, nj, nk, scalar_grid);
+        }
+
+
+
+        public Vector3i Dimensions {
+            get { return new Vector3i(scalar_grid.ni, scalar_grid.nj, scalar_grid.nk); }
+        }
+
+        /// <summary>
+        /// scalar-values grid available after calling Compute()
+        /// </summary>
+        public DenseGrid3f Grid {
+            get { return scalar_grid; }
+        }
+
+        /// <summary>
+        /// Origin of the grid, in same coordinates as mesh
+        /// </summary>
+        public Vector3f GridOrigin {
+            get { return grid_origin; }
+        }
+
+
+        /// <summary>
+        /// If ComputeMode==NarrowBand, then we internally compute a signed-distance grid,
+        /// which will hang onto 
+        /// </summary>
+        public MeshSignedDistanceGrid SDFGrid {
+            get { return mesh_sdf; }
+        }
+
+
+        public float this[int i, int j, int k] {
+            get { return scalar_grid[i, j, k]; }
+        }
+
+        public Vector3f CellCenter(int i, int j, int k)
+        {
+            return new Vector3f((float)i * CellSize + grid_origin.x,
+                                (float)j * CellSize + grid_origin.y,
+                                (float)k * CellSize + grid_origin.z);
+        }
+
+
+
+
+        void make_grid(Vector3f origin, float dx,
+                             int ni, int nj, int nk,
+                             DenseGrid3f scalars)
+        {
+            scalars.resize(ni, nj, nk);
+            scalars.assign(float.MaxValue); // sentinel
+
+            if (DebugPrint) System.Console.WriteLine("start");
+
+            // Ok, because the whole idea is that the surface might have holes, we are going to 
+            // compute values along known triangles and then propagate the computed region outwards
+            // until any iso-sign-change is surrounded.
+            // To seed propagation, we compute unsigned SDF and then compute values for any voxels
+            // containing surface (ie w/ distance smaller than cellsize)
+
+            // compute unsigned SDF
+            MeshSignedDistanceGrid sdf = new MeshSignedDistanceGrid(Mesh, CellSize) { ComputeSigns = false };
+            sdf.CancelF = this.CancelF;
+            sdf.Compute();
+            if (CancelF())
+                return;
+
+            DenseGrid3f distances = sdf.Grid;
+            if (WantMeshSDFGrid)
+                mesh_sdf = sdf;
+            if (DebugPrint) System.Console.WriteLine("done initial sdf");
+
+            // compute values at surface voxels
+            double ox = (double)origin[0], oy = (double)origin[1], oz = (double)origin[2];
+            gParallel.ForEach(gIndices.Grid3IndicesYZ(nj, nk), (jk) => {
+                if (CancelF())
+                    return;
+                for (int i = 0; i < ni; ++i) {
+                    Vector3i ijk = new Vector3i(i, jk.y, jk.z);
+                    float dist = distances[ijk];
+                    // this could be tighter? but I don't think it matters...
+                    if (dist < CellSize) {
+                        Vector3d gx = new Vector3d((float)ijk.x * dx + origin[0], (float)ijk.y * dx + origin[1], (float)ijk.z * dx + origin[2]);
+                        scalars[ijk] = (float)ScalarF(gx);
+                    }
+                }
+            });
+            if (CancelF())
+                return;
+
+            if (DebugPrint) System.Console.WriteLine("done narrow-band");
+
+            // Now propagate outwards from computed voxels.
+            // Current procedure is to check 26-neighbours around each 'front' voxel,
+            // and if there are any sign changes, that neighbour is added to front.
+            // Front is initialized w/ all voxels we computed above
+
+            AxisAlignedBox3i bounds = scalars.Bounds;
+            bounds.Max -= Vector3i.One;
+
+            // since we will be computing new values as necessary, we cannot use
+            // grid to track whether a voxel is 'new' or not. 
+            // So, using 3D bitmap intead - is updated at end of each pass.
+            Bitmap3 bits = new Bitmap3(new Vector3i(ni, nj, nk));
+            List<Vector3i> cur_front = new List<Vector3i>();
+            foreach (Vector3i ijk in scalars.Indices()) {
+                if (scalars[ijk] != float.MaxValue) {
+                    cur_front.Add(ijk);
+                    bits[ijk] = true;
+                }
+            }
+            if (CancelF())
+                return;
+
+            // Unique set of 'new' voxels to compute in next iteration.
+            HashSet<Vector3i> queue = new HashSet<Vector3i>();
+            SpinLock queue_lock = new SpinLock();
+
+            while (true) {
+                if (CancelF())
+                    return;
+
+                // can process front voxels in parallel
+                bool abort = false;  int iter_count = 0;
+                gParallel.ForEach(cur_front, (ijk) => {
+                    Interlocked.Increment(ref iter_count);
+                    if (iter_count % 100 == 0)
+                        abort = CancelF();
+                    if (abort)
+                        return;
+
+                    float val = scalars[ijk];
+
+                    // check 26-neighbours to see if we have a crossing in any direction
+                    for (int k = 0; k < 26; ++k) {
+                        Vector3i nijk = ijk + gIndices.GridOffsets26[k];
+                        if (bounds.Contains(nijk) == false)
+                            continue;
+                        float val2 = scalars[nijk];
+                        if (val2 == float.MaxValue) {
+                            Vector3d gx = new Vector3d((float)nijk.x * dx + origin[0], (float)nijk.y * dx + origin[1], (float)nijk.z * dx + origin[2]);
+                            val2 = (float)ScalarF(gx);
+                            scalars[nijk] = val2;
+                        }
+                        if (bits[nijk] == false) {
+                            // this is a 'new' voxel this round.
+                            // If we have an iso-crossing, add it to the front next round
+                            bool crossing = (val < IsoValue && val2 > IsoValue) ||
+                                            (val > IsoValue && val2 < IsoValue);
+                            if (crossing) {
+                                bool taken = false;
+                                queue_lock.Enter(ref taken);
+                                queue.Add(nijk);
+                                queue_lock.Exit();
+                            }
+                        }
+                    }
+                });
+                if (DebugPrint) System.Console.WriteLine("front has {0} voxels", queue.Count);
+                if (queue.Count == 0)
+                    break;
+
+                // update known-voxels list and create front for next iteration
+                foreach (Vector3i idx in queue)
+                    bits[idx] = true;
+                cur_front.Clear();
+                cur_front.AddRange(queue);
+                queue.Clear();
+            }
+            if (DebugPrint) System.Console.WriteLine("done front-prop");
+
+            if (DebugPrint) {
+                int filled = 0;
+                foreach (Vector3i ijk in scalars.Indices()) {
+                    if (scalars[ijk] != float.MaxValue)
+                        filled++;
+                }
+                System.Console.WriteLine("filled: {0} / {1}  -  {2}%", filled, ni * nj * nk,
+                                    (double)filled / (double)(ni * nj * nk) * 100.0);
+            }
+
+            if (CancelF())
+                return;
+
+            // fill in the rest of the grid by propagating know values
+            fill_spans(ni, nj, nk, scalars);
+
+            if (DebugPrint) System.Console.WriteLine("done sweep");
+
+
+        }
+
+
+
+
+
+
+
+
+
+
+        void make_grid_dense(Vector3f origin, float dx,
+                             int ni, int nj, int nk,
+                             DenseGrid3f scalars)
+        {
+            scalars.resize(ni, nj, nk);
+
+            bool abort = false; int count = 0;
+            gParallel.ForEach(scalars.Indices(), (ijk) => {
+                Interlocked.Increment(ref count);
+                if (count % 100 == 0)
+                    abort = CancelF();
+                if (abort)
+                    return;
+
+                Vector3d gx = new Vector3d((float)ijk.x * dx + origin[0], (float)ijk.y * dx + origin[1], (float)ijk.z * dx + origin[2]);
+                scalars[ijk] = (float)ScalarF(gx);
+            });
+
+        }   // end make_level_set_3
+
+
+
+
+
+        void fill_spans(int ni, int nj, int nk, DenseGrid3f scalars)
+        {
+            gParallel.ForEach(gIndices.Grid3IndicesYZ(nj, nk), (idx) => {
+                int j = idx.y, k = idx.z;
+                float last = scalars[0, j, k];
+                if (last == float.MaxValue)
+                    last = 0;
+                for (int i = 0; i < ni; ++i) {
+                    if (scalars[i, j, k] == float.MaxValue) {
+                        scalars[i, j, k] = last;
+                    } else {
+                        last = scalars[i, j, k];
+                        if (last < IsoValue)   // propagate zeros on outside
+                            last = 0;
+                    }
+                }
+            });
+        }
+
+
+
+
+        
+        
+
+
+
+
+    }
+}
diff --git a/spatial/MeshSignedDistanceGrid.cs b/spatial/MeshSignedDistanceGrid.cs
index 448275fc..ec98da68 100644
--- a/spatial/MeshSignedDistanceGrid.cs
+++ b/spatial/MeshSignedDistanceGrid.cs
@@ -40,12 +40,17 @@ namespace g3
     public class MeshSignedDistanceGrid
     {
         public DMesh3 Mesh;
+        public DMeshAABBTree3 Spatial;
         public float CellSize;
 
         // Width of the band around triangles for which exact distances are computed
         // (In fact this is conservative, the band is often larger locally)
         public int ExactBandWidth = 1;
 
+        // Bounds of grid will be expanded this much in positive and negative directions.
+        // Useful for if you want field to extend outwards.
+        public Vector3d ExpandBounds = Vector3d.Zero;
+
         // Most of this parallelizes very well, makes a huge speed difference
         public bool UseParallel = true;
 
@@ -56,10 +61,16 @@ public class MeshSignedDistanceGrid
         public enum ComputeModes
         {
             FullGrid = 0,
-            NarrowBandOnly = 1
+            NarrowBandOnly = 1,
+            NarrowBand_SpatialFloodFill = 2
         }
         public ComputeModes ComputeMode = ComputeModes.NarrowBandOnly;
 
+        // how wide of narrow band should we compute. This value is 
+        // currently only used if there is a spatial data structure, as
+        // we can efficiently explore the space (in that case ExactBandWidth is not used)
+        public double NarrowBandMaxDistance = 0;
+
         // should we try to compute signs? if not, grid remains unsigned
         public bool ComputeSigns = true;
 
@@ -83,6 +94,9 @@ public enum InsideModes
         // grid of per-cell crossing or parity counts
         public bool WantIntersectionsGrid = false;
 
+        /// <summary> if this function returns true, we should abort calculation </summary>
+        public Func<bool> CancelF = () => { return false; };
+
 
         public bool DebugPrint = false;
 
@@ -93,10 +107,11 @@ public enum InsideModes
         DenseGrid3i closest_tri_grid;
         DenseGrid3i intersections_grid;
 
-        public MeshSignedDistanceGrid(DMesh3 mesh, double cellSize)
+        public MeshSignedDistanceGrid(DMesh3 mesh, double cellSize, DMeshAABBTree3 spatial = null)
         {
             Mesh = mesh;
             CellSize = (float)cellSize;
+            Spatial = spatial;
         }
 
 
@@ -106,17 +121,31 @@ public void Compute()
             AxisAlignedBox3d bounds = Mesh.CachedBounds;
 
             float fBufferWidth = 2 * ExactBandWidth * CellSize;
-            grid_origin = (Vector3f)bounds.Min - fBufferWidth * Vector3f.One;
-            Vector3f max = (Vector3f)bounds.Max + fBufferWidth * Vector3f.One;
+            if (ComputeMode == ComputeModes.NarrowBand_SpatialFloodFill)
+                fBufferWidth = (float)Math.Max(fBufferWidth, 2 * NarrowBandMaxDistance);
+            grid_origin = (Vector3f)bounds.Min - fBufferWidth * Vector3f.One - (Vector3f)ExpandBounds;
+            Vector3f max = (Vector3f)bounds.Max + fBufferWidth * Vector3f.One + (Vector3f)ExpandBounds;
             int ni = (int)((max.x - grid_origin.x) / CellSize) + 1;
             int nj = (int)((max.y - grid_origin.y) / CellSize) + 1;
             int nk = (int)((max.z - grid_origin.z) / CellSize) + 1;
 
             grid = new DenseGrid3f();
-            if ( UseParallel )
-                make_level_set3_parallel(grid_origin, CellSize, ni, nj, nk, grid, ExactBandWidth);
-            else
-                make_level_set3(grid_origin, CellSize, ni, nj, nk, grid, ExactBandWidth);
+            if (ComputeMode == ComputeModes.NarrowBand_SpatialFloodFill) {
+                if (Spatial == null || NarrowBandMaxDistance == 0 || UseParallel == false)
+                    throw new Exception("MeshSignedDistanceGrid.Compute: must set Spatial data structure and band max distance, and UseParallel=true");
+                make_level_set3_parallel_floodfill(grid_origin, CellSize, ni, nj, nk, grid, ExactBandWidth);
+
+            } else {
+                if (UseParallel) {
+                    if (Spatial != null) {
+                        make_level_set3_parallel_spatial(grid_origin, CellSize, ni, nj, nk, grid, ExactBandWidth);
+                    } else {
+                        make_level_set3_parallel(grid_origin, CellSize, ni, nj, nk, grid, ExactBandWidth);
+                    }
+                } else {
+                    make_level_set3(grid_origin, CellSize, ni, nj, nk, grid, ExactBandWidth);
+                }
+            }
         }
 
 
@@ -159,12 +188,31 @@ public DenseGrid3i IntersectionsGrid {
         public float this[int i, int j, int k] {
             get { return grid[i, j, k]; }
         }
+        public float this[Vector3i idx] {
+            get { return grid[idx.x, idx.y, idx.z]; }
+        }
+
+        public Vector3f CellCenter(int i, int j, int k) {
+            return cell_center(new Vector3i(i, j, k));
+        }
+        Vector3f cell_center(Vector3i ijk)
+        {
+            return new Vector3f((float)ijk.x * CellSize + grid_origin[0],
+                                (float)ijk.y * CellSize + grid_origin[1],
+                                (float)ijk.z * CellSize + grid_origin[2]);
+        }
 
-        public Vector3f CellCenter(int i, int j, int k)
+        float upper_bound(DenseGrid3f grid)
         {
-            return new Vector3f((float)i * CellSize + grid_origin.x, 
-                                (float)j * CellSize + grid_origin.y, 
-                                (float)k * CellSize + grid_origin.z);
+            return (float)((grid.ni + grid.nj + grid.nk) * CellSize);
+        }
+
+        float cell_tri_dist(Vector3i idx, int tid)
+        {
+            Vector3d xp = Vector3d.Zero, xq = Vector3d.Zero, xr = Vector3d.Zero;
+            Vector3d c = cell_center(idx);
+            Mesh.GetTriVertices(tid, ref xp, ref xq, ref xr);
+            return (float)point_triangle_distance(ref c, ref xp, ref xq, ref xr);
         }
 
 
@@ -175,7 +223,7 @@ void make_level_set3(Vector3f origin, float dx,
                              DenseGrid3f distances, int exact_band)
         {
             distances.resize(ni, nj, nk);
-            distances.assign((ni + nj + nk) * dx); // upper bound on distance
+            distances.assign(upper_bound(distances)); // upper bound on distance
 
             // closest triangle id for each grid cell
             DenseGrid3i closest_tri = new DenseGrid3i(ni, nj, nk, -1);
@@ -192,6 +240,8 @@ void make_level_set3(Vector3f origin, float dx,
             double ox = (double)origin[0], oy = (double)origin[1], oz = (double)origin[2];
             Vector3d xp = Vector3d.Zero, xq = Vector3d.Zero, xr = Vector3d.Zero;
             foreach (int tid in Mesh.TriangleIndices()) {
+                if (tid % 100 == 0 && CancelF())
+                    break;
                 Mesh.GetTriVertices(tid, ref xp, ref xq, ref xr);
 
                 // real ijk coordinates of xp/xq/xr
@@ -222,20 +272,27 @@ void make_level_set3(Vector3f origin, float dx,
                     }
                 }
             }
+            if (CancelF())
+                return;
 
             if (ComputeSigns == true) {
 
                 if (DebugPrint) System.Console.WriteLine("done narrow-band");
 
                 compute_intersections(origin, dx, ni, nj, nk, intersection_count);
+                if (CancelF())
+                    return;
 
                 if (DebugPrint) System.Console.WriteLine("done intersections");
 
                 if (ComputeMode == ComputeModes.FullGrid) {
                     // and now we fill in the rest of the distances with fast sweeping
-                    for (int pass = 0; pass < 2; ++pass)
+                    for (int pass = 0; pass < 2; ++pass) {
                         sweep_pass(origin, dx, distances, closest_tri);
-                    if (DebugPrint) System.Console.WriteLine("done sweeping");
+                        if (CancelF())
+                            return;
+                    }
+                        if (DebugPrint) System.Console.WriteLine("done sweeping");
                 } else {
                     // nothing!
                     if (DebugPrint) System.Console.WriteLine("skipped sweeping");
@@ -244,6 +301,8 @@ void make_level_set3(Vector3f origin, float dx,
 
                 // then figure out signs (inside/outside) from intersection counts
                 compute_signs(ni, nj, nk, distances, intersection_count);
+                if (CancelF())
+                    return;
 
                 if (DebugPrint) System.Console.WriteLine("done signs");
 
@@ -261,14 +320,12 @@ void make_level_set3(Vector3f origin, float dx,
 
 
 
-
-
         void make_level_set3_parallel(Vector3f origin, float dx,
                              int ni, int nj, int nk,
                              DenseGrid3f distances, int exact_band)
         {
             distances.resize(ni, nj, nk);
-            distances.assign((float)((ni + nj + nk) * dx)); // upper bound on distance
+            distances.assign(upper_bound(grid)); // upper bound on distance
 
             // closest triangle id for each grid cell
             DenseGrid3i closest_tri = new DenseGrid3i(ni, nj, nk, -1);
@@ -293,7 +350,13 @@ void make_level_set3_parallel(Vector3f origin, float dx,
             int wi = ni / 2, wj = nj / 2, wk = nk / 2;
             SpinLock[] grid_locks = new SpinLock[8];
 
+            bool abort = false;
             gParallel.ForEach(Mesh.TriangleIndices(), (tid) => {
+                if (tid % 100 == 0)
+                    abort = CancelF();
+                if (abort)
+                    return;
+
                 Vector3d xp = Vector3d.Zero, xq = Vector3d.Zero, xr = Vector3d.Zero;
                 Mesh.GetTriVertices(tid, ref xp, ref xq, ref xr);
 
@@ -334,6 +397,311 @@ void make_level_set3_parallel(Vector3f origin, float dx,
                     }
                 }
             });
+            if (DebugPrint) System.Console.WriteLine("done narrow-band");
+            if (CancelF())
+                return;
+
+
+            if (ComputeSigns == true) {
+
+                compute_intersections(origin, dx, ni, nj, nk, intersection_count);
+                if (CancelF())
+                    return;
+
+                if (DebugPrint) System.Console.WriteLine("done intersections");
+
+                if (ComputeMode == ComputeModes.FullGrid) {
+                    // and now we fill in the rest of the distances with fast sweeping
+                    for (int pass = 0; pass < 2; ++pass) {
+                        sweep_pass(origin, dx, distances, closest_tri);
+                        if (CancelF())
+                            return;
+                    }
+                    if (DebugPrint) System.Console.WriteLine("done sweeping");
+                } else {
+                    // nothing!
+                    if (DebugPrint) System.Console.WriteLine("skipped sweeping");
+                }
+
+                if (DebugPrint) System.Console.WriteLine("done sweeping");
+
+                // then figure out signs (inside/outside) from intersection counts
+                compute_signs(ni, nj, nk, distances, intersection_count);
+                if (CancelF())
+                    return;
+
+                if (WantIntersectionsGrid)
+                    intersections_grid = intersection_count;
+
+                if (DebugPrint) System.Console.WriteLine("done signs");
+            }
+
+            if (WantClosestTriGrid)
+                closest_tri_grid = closest_tri;
+
+        }   // end make_level_set_3
+
+
+
+
+
+        void make_level_set3_parallel_spatial(Vector3f origin, float dx,
+                             int ni, int nj, int nk,
+                             DenseGrid3f distances, int exact_band)
+        {
+            distances.resize(ni, nj, nk);
+            float upper_bound = this.upper_bound(distances);
+            distances.assign(upper_bound); // upper bound on distance
+
+            // closest triangle id for each grid cell
+            DenseGrid3i closest_tri = new DenseGrid3i(ni, nj, nk, -1);
+
+            // intersection_count(i,j,k) is # of tri intersections in (i-1,i]x{j}x{k}
+            DenseGrid3i intersection_count = new DenseGrid3i(ni, nj, nk, 0);
+
+            if (DebugPrint) System.Console.WriteLine("start");
+
+            double ox = (double)origin[0], oy = (double)origin[1], oz = (double)origin[2];
+            double invdx = 1.0 / dx;
+
+            // Compute narrow-band distances. For each triangle, we find its grid-coord-bbox,
+            // and compute exact distances within that box.
+
+            // To compute in parallel, we need to safely update grid cells. Current strategy is
+            // to use a spinlock to control access to grid. Partitioning the grid into a few regions,
+            // each w/ a separate spinlock, improves performance somewhat. Have also tried having a
+            // separate spinlock per-row, this resulted in a few-percent performance improvement.
+            // Also tried pre-sorting triangles into disjoint regions, this did not help much except
+            // on "perfect" cases like a sphere. 
+            bool abort = false;
+            gParallel.ForEach(Mesh.TriangleIndices(), (tid) => {
+                if (tid % 100 == 0)
+                    abort = CancelF();
+                if (abort)
+                    return;
+
+                Vector3d xp = Vector3d.Zero, xq = Vector3d.Zero, xr = Vector3d.Zero;
+                Mesh.GetTriVertices(tid, ref xp, ref xq, ref xr);
+
+                // real ijk coordinates of xp/xq/xr
+                double fip = (xp[0] - ox) * invdx, fjp = (xp[1] - oy) * invdx, fkp = (xp[2] - oz) * invdx;
+                double fiq = (xq[0] - ox) * invdx, fjq = (xq[1] - oy) * invdx, fkq = (xq[2] - oz) * invdx;
+                double fir = (xr[0] - ox) * invdx, fjr = (xr[1] - oy) * invdx, fkr = (xr[2] - oz) * invdx;
+
+                // clamped integer bounding box of triangle plus exact-band
+                int i0 = MathUtil.Clamp(((int)MathUtil.Min(fip, fiq, fir)) - exact_band, 0, ni - 1);
+                int i1 = MathUtil.Clamp(((int)MathUtil.Max(fip, fiq, fir)) + exact_band + 1, 0, ni - 1);
+                int j0 = MathUtil.Clamp(((int)MathUtil.Min(fjp, fjq, fjr)) - exact_band, 0, nj - 1);
+                int j1 = MathUtil.Clamp(((int)MathUtil.Max(fjp, fjq, fjr)) + exact_band + 1, 0, nj - 1);
+                int k0 = MathUtil.Clamp(((int)MathUtil.Min(fkp, fkq, fkr)) - exact_band, 0, nk - 1);
+                int k1 = MathUtil.Clamp(((int)MathUtil.Max(fkp, fkq, fkr)) + exact_band + 1, 0, nk - 1);
+
+                // compute distance for each tri inside this bounding box
+                // note: this can be very conservative if the triangle is large and on diagonal to grid axes
+                for (int k = k0; k <= k1; ++k) {
+                    for (int j = j0; j <= j1; ++j) {
+                        for (int i = i0; i <= i1; ++i) {
+                            distances[i, j, k] = 1;
+                        }
+                    }
+                }
+            });
+
+
+            if (DebugPrint) System.Console.WriteLine("done narrow-band tagging");
+
+            double max_dist = exact_band * (dx * MathUtil.SqrtTwo);
+            gParallel.ForEach(grid.Indices(), (idx) => {
+                if ( distances[idx] == 1 ) {
+                    int i = idx.x, j = idx.y, k = idx.z;
+                    Vector3d p = new Vector3d((float)i * dx + origin[0], (float)j * dx + origin[1], (float)k * dx + origin[2]);
+                    int near_tid = Spatial.FindNearestTriangle(p, max_dist);
+                    if ( near_tid == DMesh3.InvalidID ) {
+                        distances[idx] = upper_bound;
+                        return;
+                    }
+                    Triangle3d tri = new Triangle3d();
+                    Mesh.GetTriVertices(near_tid, ref tri.V0, ref tri.V1, ref tri.V2);
+                    Vector3d closest = new Vector3d(), bary = new Vector3d();
+                    double dsqr = DistPoint3Triangle3.DistanceSqr(ref p, ref tri, out closest, out bary);
+                    distances[idx] = (float)Math.Sqrt(dsqr);
+                    closest_tri[idx] = near_tid;
+                }
+            });
+
+
+            if (DebugPrint) System.Console.WriteLine("done distances");
+
+
+            if (CancelF())
+                return;
+
+            if (ComputeSigns == true) {
+
+                if (DebugPrint) System.Console.WriteLine("done narrow-band");
+
+                compute_intersections(origin, dx, ni, nj, nk, intersection_count);
+                if (CancelF())
+                    return;
+
+                if (DebugPrint) System.Console.WriteLine("done intersections");
+
+                if (ComputeMode == ComputeModes.FullGrid) {
+                    // and now we fill in the rest of the distances with fast sweeping
+                    for (int pass = 0; pass < 2; ++pass) {
+                        sweep_pass(origin, dx, distances, closest_tri);
+                        if (CancelF())
+                            return;
+                    }
+                    if (DebugPrint) System.Console.WriteLine("done sweeping");
+                } else {
+                    // nothing!
+                    if (DebugPrint) System.Console.WriteLine("skipped sweeping");
+                }
+
+                if (DebugPrint) System.Console.WriteLine("done sweeping");
+
+                // then figure out signs (inside/outside) from intersection counts
+                compute_signs(ni, nj, nk, distances, intersection_count);
+                if (CancelF())
+                    return;
+
+                if (WantIntersectionsGrid)
+                    intersections_grid = intersection_count;
+
+                if (DebugPrint) System.Console.WriteLine("done signs");
+            }
+
+            if (WantClosestTriGrid)
+                closest_tri_grid = closest_tri;
+
+        }   // end make_level_set_3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+        void make_level_set3_parallel_floodfill(Vector3f origin, float dx,
+                             int ni, int nj, int nk,
+                             DenseGrid3f distances, int exact_band)
+        {
+            distances.resize(ni, nj, nk);
+            float upper_bound = this.upper_bound(distances);
+            distances.assign(upper_bound); // upper bound on distance
+
+            // closest triangle id for each grid cell
+            DenseGrid3i closest_tri = new DenseGrid3i(ni, nj, nk, -1);
+
+            // intersection_count(i,j,k) is # of tri intersections in (i-1,i]x{j}x{k}
+            DenseGrid3i intersection_count = new DenseGrid3i(ni, nj, nk, 0);
+
+            if (DebugPrint) System.Console.WriteLine("start");
+
+            double ox = (double)origin[0], oy = (double)origin[1], oz = (double)origin[2];
+            double invdx = 1.0 / dx;
+
+            // compute values at vertices
+
+            SpinLock grid_lock = new SpinLock();
+            List<int> Q = new List<int>();
+            bool[] done = new bool[distances.size];
+
+            bool abort = false;
+            gParallel.ForEach(Mesh.VertexIndices(), (vid) => {
+                if (vid % 100 == 0) abort = CancelF();
+                if (abort) return;
+
+                Vector3d v = Mesh.GetVertex(vid);
+                // real ijk coordinates of v
+                double fi = (v.x-ox)*invdx, fj = (v.y-oy)*invdx, fk = (v.z-oz)*invdx;
+                Vector3i idx = new Vector3i(
+                    MathUtil.Clamp((int)fi, 0, ni - 1),
+                    MathUtil.Clamp((int)fj, 0, nj - 1),
+                    MathUtil.Clamp((int)fk, 0, nk - 1));
+
+                if (distances[idx] < upper_bound)
+                    return;
+
+                bool taken = false;
+                grid_lock.Enter(ref taken);
+
+                Vector3d p = cell_center(idx);
+                int near_tid = Spatial.FindNearestTriangle(p);
+                Triangle3d tri = new Triangle3d();
+                Mesh.GetTriVertices(near_tid, ref tri.V0, ref tri.V1, ref tri.V2);
+                Vector3d closest = new Vector3d(), bary = new Vector3d();
+                double dsqr = DistPoint3Triangle3.DistanceSqr(ref p, ref tri, out closest, out bary);
+                distances[idx] = (float)Math.Sqrt(dsqr);
+                closest_tri[idx] = near_tid;
+                int idx_linear = distances.to_linear(ref idx);
+                Q.Add(idx_linear);
+                done[idx_linear] = true;
+                grid_lock.Exit();
+            });
+            if (DebugPrint) System.Console.WriteLine("done vertices");
+            if (CancelF())
+                return;
+
+            // we could do this parallel w/ some kind of producer-consumer...
+            List<int> next_Q = new List<int>();
+            AxisAlignedBox3i bounds = distances.BoundsInclusive;
+            double max_dist = NarrowBandMaxDistance; 
+            double max_query_dist = max_dist + (2*dx*MathUtil.SqrtTwo);
+            int next_pass_count = Q.Count;
+            while (next_pass_count > 0) {
+
+                next_Q.Clear();
+                gParallel.ForEach(Q, (cur_linear_index) => {
+                    Vector3i cur_idx = distances.to_index(cur_linear_index);
+                    foreach (Vector3i idx_offset in gIndices.GridOffsets26) {
+                        Vector3i nbr_idx = cur_idx + idx_offset;
+                        if (bounds.Contains(nbr_idx) == false)
+                            continue;
+                        int nbr_linear_idx = distances.to_linear(ref nbr_idx);
+                        if (done[nbr_linear_idx])
+                            continue;
+
+                        Vector3d p = cell_center(nbr_idx);
+                        int near_tid = Spatial.FindNearestTriangle(p, max_query_dist);
+                        if (near_tid == -1) {
+                            done[nbr_linear_idx] = true;
+                            continue;
+                        }
+
+                        Triangle3d tri = new Triangle3d();
+                        Mesh.GetTriVertices(near_tid, ref tri.V0, ref tri.V1, ref tri.V2);
+                        Vector3d closest = new Vector3d(), bary = new Vector3d();
+                        double dsqr = DistPoint3Triangle3.DistanceSqr(ref p, ref tri, out closest, out bary);
+                        double dist = Math.Sqrt(dsqr);
+
+                        bool taken = false;
+                        grid_lock.Enter(ref taken);
+                        if (done[nbr_linear_idx] == false) {
+                            distances[nbr_linear_idx] = (float)dist;
+                            closest_tri[nbr_linear_idx] = near_tid;
+                            done[nbr_linear_idx] = true;
+                            if (dist < max_dist) 
+                                next_Q.Add(nbr_linear_idx);
+                        }
+                        grid_lock.Exit();
+                    }
+                });
+                // swap lists
+                var tmp = Q; Q = next_Q; next_Q = tmp;
+                next_pass_count = Q.Count;
+            }
+            if (DebugPrint) System.Console.WriteLine("done floodfill");
+            if (CancelF())
+                return;
 
 
             if (ComputeSigns == true) {
@@ -341,13 +709,18 @@ void make_level_set3_parallel(Vector3f origin, float dx,
                 if (DebugPrint) System.Console.WriteLine("done narrow-band");
 
                 compute_intersections(origin, dx, ni, nj, nk, intersection_count);
+                if (CancelF())
+                    return;
 
                 if (DebugPrint) System.Console.WriteLine("done intersections");
 
                 if (ComputeMode == ComputeModes.FullGrid) {
                     // and now we fill in the rest of the distances with fast sweeping
-                    for (int pass = 0; pass < 2; ++pass)
+                    for (int pass = 0; pass < 2; ++pass) {
                         sweep_pass(origin, dx, distances, closest_tri);
+                        if (CancelF())
+                            return;
+                    }
                     if (DebugPrint) System.Console.WriteLine("done sweeping");
                 } else {
                     // nothing!
@@ -358,6 +731,8 @@ void make_level_set3_parallel(Vector3f origin, float dx,
 
                 // then figure out signs (inside/outside) from intersection counts
                 compute_signs(ni, nj, nk, distances, intersection_count);
+                if (CancelF())
+                    return;
 
                 if (WantIntersectionsGrid)
                     intersections_grid = intersection_count;
@@ -372,18 +747,34 @@ void make_level_set3_parallel(Vector3f origin, float dx,
 
 
 
+      
+
+
+
+
+
+
+
+
 
         // sweep through grid in different directions, distances and closest tris
         void sweep_pass(Vector3f origin, float dx,
                         DenseGrid3f distances, DenseGrid3i closest_tri)
         {
             sweep(distances, closest_tri, origin, dx, +1, +1, +1);
+            if (CancelF()) return;
             sweep(distances, closest_tri, origin, dx, -1, -1, -1);
+            if (CancelF()) return;
             sweep(distances, closest_tri, origin, dx, +1, +1, -1);
+            if (CancelF()) return;
             sweep(distances, closest_tri, origin, dx, -1, -1, +1);
+            if (CancelF()) return;
             sweep(distances, closest_tri, origin, dx, +1, -1, +1);
+            if (CancelF()) return;
             sweep(distances, closest_tri, origin, dx, -1, +1, -1);
+            if (CancelF()) return;
             sweep(distances, closest_tri, origin, dx, +1, -1, -1);
+            if (CancelF()) return;
             sweep(distances, closest_tri, origin, dx, -1, +1, +1);
         }
 
@@ -400,6 +791,7 @@ void sweep(DenseGrid3f phi, DenseGrid3i closest_tri,
             int k0, k1;
             if (dk > 0) { k0 = 1; k1 = phi.nk; } else { k0 = phi.nk - 2; k1 = -1; }
             for (int k = k0; k != k1; k += dk) {
+                if (CancelF()) return;
                 for (int j = j0; j != j1; j += dj) {
                     for (int i = i0; i != i1; i += di) {
                         Vector3d gx = new Vector3d(i * dx + origin[0], j * dx + origin[1], k * dx + origin[2]);
@@ -440,12 +832,19 @@ void compute_intersections(Vector3f origin, float dx, int ni, int nj, int nk, De
             double ox = (double)origin[0], oy = (double)origin[1], oz = (double)origin[2];
             double invdx = 1.0 / dx;
 
+            bool cancelled = false;
+
             // this is what we will do for each triangle. There are no grid-reads, only grid-writes, 
             // since we use atomic_increment, it is always thread-safe
             Action<int> ProcessTriangleF = (tid) => {
+                if (tid % 100 == 0 && CancelF() == true)
+                    cancelled = true;
+                if (cancelled) return;
+
                 Vector3d xp = Vector3d.Zero, xq = Vector3d.Zero, xr = Vector3d.Zero;
                 Mesh.GetTriVertices(tid, ref xp, ref xq, ref xr);
 
+
                 bool neg_x = false;
                 if (InsideMode == InsideModes.ParityCount) {
                     Vector3d n = MathUtil.FastNormalDirection(ref xp, ref xq, ref xr);
@@ -508,6 +907,9 @@ void compute_signs(int ni, int nj, int nk, DenseGrid3f distances, DenseGrid3i in
                 // can process each x-row in parallel
                 AxisAlignedBox2i box = new AxisAlignedBox2i(0, 0, nj, nk);
                 gParallel.ForEach(box.IndicesExclusive(), (vi) => {
+                    if (CancelF())
+                        return;
+
                     int j = vi.x, k = vi.y;
                     int total_count = 0;
                     for (int i = 0; i < ni; ++i) {
@@ -521,6 +923,9 @@ void compute_signs(int ni, int nj, int nk, DenseGrid3f distances, DenseGrid3i in
             } else {
 
                 for (int k = 0; k < nk; ++k) {
+                    if (CancelF())
+                        return;
+
                     for (int j = 0; j < nj; ++j) {
                         int total_count = 0;
                         for (int i = 0; i < ni; ++i) {
@@ -543,7 +948,7 @@ void compute_signs(int ni, int nj, int nk, DenseGrid3f distances, DenseGrid3i in
 
 
         // find distance x0 is from segment x1-x2
-        static float point_segment_distance(ref Vector3f x0, ref Vector3f x1, ref Vector3f x2)
+        static public float point_segment_distance(ref Vector3f x0, ref Vector3f x1, ref Vector3f x2)
         {
             Vector3f dx = x2 - x1;
             float m2 = dx.LengthSquared;
@@ -560,7 +965,7 @@ static float point_segment_distance(ref Vector3f x0, ref Vector3f x1, ref Vector
 
 
         // find distance x0 is from segment x1-x2
-        static double point_segment_distance(ref Vector3d x0, ref Vector3d x1, ref Vector3d x2)
+        static public double point_segment_distance(ref Vector3d x0, ref Vector3d x1, ref Vector3d x2)
         {
             Vector3d dx = x2 - x1;
             double m2 = dx.LengthSquared;
@@ -578,7 +983,7 @@ static double point_segment_distance(ref Vector3d x0, ref Vector3d x1, ref Vecto
 
 
         // find distance x0 is from triangle x1-x2-x3
-        static float point_triangle_distance(ref Vector3f x0, ref Vector3f x1, ref Vector3f x2, ref Vector3f x3)
+        static public float point_triangle_distance(ref Vector3f x0, ref Vector3f x1, ref Vector3f x2, ref Vector3f x3)
         {
             // first find barycentric coordinates of closest point on infinite plane
             Vector3f x13 = (x1 - x3);
@@ -605,15 +1010,15 @@ static float point_triangle_distance(ref Vector3f x0, ref Vector3f x1, ref Vecto
 
 
         // find distance x0 is from triangle x1-x2-x3
-        static double point_triangle_distance(ref Vector3d x0, ref Vector3d x1, ref Vector3d x2, ref Vector3d x3)
+        static public double point_triangle_distance(ref Vector3d x0, ref Vector3d x1, ref Vector3d x2, ref Vector3d x3)
         {
             // first find barycentric coordinates of closest point on infinite plane
             Vector3d x13 = (x1 - x3);
             Vector3d x23 = (x2 - x3);
             Vector3d x03 = (x0 - x3);
-            double m13 = x13.LengthSquared, m23 = x23.LengthSquared, d = x13.Dot(x23);
+            double m13 = x13.LengthSquared, m23 = x23.LengthSquared, d = x13.Dot(ref x23);
             double invdet = 1.0 / Math.Max(m13 * m23 - d * d, 1e-30);
-            double a = x13.Dot(x03), b = x23.Dot(x03);
+            double a = x13.Dot(ref x03), b = x23.Dot(ref x03);
             // the barycentric coordinates themselves
             double w23 = invdet * (m23 * a - d * b);
             double w31 = invdet * (m13 * b - d * a);
@@ -635,7 +1040,7 @@ static double point_triangle_distance(ref Vector3d x0, ref Vector3d x1, ref Vect
 
         // calculate twice signed area of triangle (0,0)-(x1,y1)-(x2,y2)
         // return an SOS-determined sign (-1, +1, or 0 only if it's a truly degenerate triangle)
-        static int orientation(double x1, double y1, double x2, double y2, out double twice_signed_area)
+        static public int orientation(double x1, double y1, double x2, double y2, out double twice_signed_area)
         {
             twice_signed_area = y1 * x2 - x1 * y2;
             if (twice_signed_area > 0) return 1;
@@ -650,7 +1055,7 @@ static int orientation(double x1, double y1, double x2, double y2, out double tw
 
         // robust test of (x0,y0) in the triangle (x1,y1)-(x2,y2)-(x3,y3)
         // if true is returned, the barycentric coordinates are set in a,b,c.
-        static bool point_in_triangle_2d(double x0, double y0,
+        static public bool point_in_triangle_2d(double x0, double y0,
                                          double x1, double y1, double x2, double y2, double x3, double y3,
                                          out double a, out double b, out double c)
         {
diff --git a/spatial/MeshWindingNumberGrid.cs b/spatial/MeshWindingNumberGrid.cs
new file mode 100644
index 00000000..ebddcb1a
--- /dev/null
+++ b/spatial/MeshWindingNumberGrid.cs
@@ -0,0 +1,349 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using g3;
+
+namespace gs
+{
+
+    /// <summary>
+    /// Sample mesh winding number (MWN) on a discrete grid. Can sample full grid, or
+    /// compute MWN values along a specific iso-contour and then fill in rest of grid
+    /// with correctly-signed values via fast sweeping (this is the default)
+    /// 
+    /// TODO: 
+    ///   - I think we are over-exploring the grid most of the time. eg along an x-ray that
+    ///     intersects the surface, we only need at most 2 cells, but we are computing at least 3,
+    ///     and possibly 5. 
+    ///   - it may be better to use something like bloomenthal polygonizer continuation? where we 
+    ///     are keeping track of active edges instead of active cells?
+    /// 
+    /// </summary>
+    public class MeshWindingNumberGrid
+    {
+        public DMesh3 Mesh;
+        public DMeshAABBTree3 MeshSpatial;
+
+        // size of cubes in the grid
+        public double CellSize;
+
+        // how many cells around border should we keep
+        public int BufferCells = 1;
+
+        // Should we compute MWN at all grid cells (expensive!!) or only in narrow band.
+        // In narrow-band mode, we guess rest of MWN values by propagating along x-rows
+        public enum ComputeModes
+        {
+            FullGrid = 0,
+            NarrowBand = 1
+        }
+        public ComputeModes ComputeMode = ComputeModes.NarrowBand;
+
+        // in narrow-band mode, if mesh is not closed, we will explore space around
+        // this MWN iso-value
+        public float WindingIsoValue = 0.5f;
+
+        // in NarrowBand mode, we compute mesh SDF grid, if true then it can be accessed
+        // via SDFGrid property after Compute()
+        public bool WantMeshSDFGrid = true;
+
+        /// <summary> if this function returns true, we should abort calculation </summary>
+        public Func<bool> CancelF = () => { return false; };
+
+        public bool DebugPrint = false;
+
+        // computed results
+        Vector3f grid_origin;
+        DenseGrid3f winding_grid;
+
+        // sdf grid we compute in narrow-band mode
+        MeshSignedDistanceGrid mesh_sdf;
+
+
+        public MeshWindingNumberGrid(DMesh3 mesh, DMeshAABBTree3 spatial, double cellSize)
+        {
+            Mesh = mesh;
+            MeshSpatial = spatial;
+            CellSize = cellSize;
+        }
+
+
+        public void Compute()
+        {
+            // figure out origin & dimensions
+            AxisAlignedBox3d bounds = Mesh.CachedBounds;
+
+            float fBufferWidth = 2 * BufferCells * (float)CellSize;
+            grid_origin = (Vector3f)bounds.Min - fBufferWidth * Vector3f.One;
+            Vector3f max = (Vector3f)bounds.Max + fBufferWidth * Vector3f.One;
+            int ni = (int)((max.x - grid_origin.x) / (float)CellSize) + 1;
+            int nj = (int)((max.y - grid_origin.y) / (float)CellSize) + 1;
+            int nk = (int)((max.z - grid_origin.z) / (float)CellSize) + 1;
+
+            winding_grid = new DenseGrid3f();
+            if ( ComputeMode == ComputeModes.FullGrid )
+                make_grid_dense(grid_origin, (float)CellSize, ni, nj, nk, winding_grid);
+            else
+                make_grid(grid_origin, (float)CellSize, ni, nj, nk, winding_grid);
+        }
+
+
+
+        public Vector3i Dimensions {
+            get { return new Vector3i(winding_grid.ni, winding_grid.nj, winding_grid.nk); }
+        }
+
+        /// <summary>
+        /// winding-number grid available after calling Compute()
+        /// </summary>
+        public DenseGrid3f Grid {
+            get { return winding_grid; }
+        }
+
+        /// <summary>
+        /// Origin of the winding-number grid, in same coordinates as mesh
+        /// </summary>
+        public Vector3f GridOrigin {
+            get { return grid_origin; }
+        }
+
+
+        /// <summary>
+        /// If ComputeMode==NarrowBand, then we internally compute a signed-distance grid,
+        /// which will hang onto 
+        /// </summary>
+        public MeshSignedDistanceGrid SDFGrid {
+            get { return mesh_sdf; }
+        }
+
+
+        public float this[int i, int j, int k] {
+            get { return winding_grid[i, j, k]; }
+        }
+
+        public Vector3f CellCenter(int i, int j, int k)
+        {
+            return new Vector3f((float)i * CellSize + grid_origin.x,
+                                (float)j * CellSize + grid_origin.y,
+                                (float)k * CellSize + grid_origin.z);
+        }
+
+
+
+
+        void make_grid(Vector3f origin, float dx,
+                             int ni, int nj, int nk,
+                             DenseGrid3f winding)
+        {
+            winding.resize(ni, nj, nk);
+            winding.assign(float.MaxValue); // sentinel
+
+            // seed MWN cache
+            MeshSpatial.WindingNumber(Vector3d.Zero);
+
+            if (DebugPrint) System.Console.WriteLine("start");
+
+            // Ok, because the whole idea is that the surface might have holes, we are going to 
+            // compute MWN along known triangles and then propagate the computed region outwards
+            // until any MWN iso-sign-change is surrounded.
+            // To seed propagation, we compute unsigned SDF and then compute MWN for any voxels
+            // containing surface (ie w/ distance smaller than cellsize)
+
+            // compute unsigned SDF
+            MeshSignedDistanceGrid sdf = new MeshSignedDistanceGrid(Mesh, CellSize) { ComputeSigns = false };
+            sdf.CancelF = this.CancelF;
+            sdf.Compute();
+            if (CancelF())
+                return;
+
+            DenseGrid3f distances = sdf.Grid;
+            if (WantMeshSDFGrid)
+                mesh_sdf = sdf;
+            if (DebugPrint) System.Console.WriteLine("done initial sdf");
+
+            // compute MWN at surface voxels
+            double ox = (double)origin[0], oy = (double)origin[1], oz = (double)origin[2];
+            gParallel.ForEach(gIndices.Grid3IndicesYZ(nj, nk), (jk) => {
+                if (CancelF())
+                    return;
+                for (int i = 0; i < ni; ++i) {
+                    Vector3i ijk = new Vector3i(i, jk.y, jk.z);
+                    float dist = distances[ijk];
+                    // this could be tighter? but I don't think it matters...
+                    if (dist < CellSize) {
+                        Vector3d gx = new Vector3d((float)ijk.x * dx + origin[0], (float)ijk.y * dx + origin[1], (float)ijk.z * dx + origin[2]);
+                        winding[ijk] = (float)MeshSpatial.WindingNumber(gx);
+                    }
+                }
+            });
+            if (CancelF())
+                return;
+
+            if (DebugPrint) System.Console.WriteLine("done narrow-band");
+
+            // Now propagate outwards from computed voxels.
+            // Current procedure is to check 26-neighbours around each 'front' voxel,
+            // and if there are any MWN sign changes, that neighbour is added to front.
+            // Front is initialized w/ all voxels we computed above
+
+            AxisAlignedBox3i bounds = winding.Bounds;
+            bounds.Max -= Vector3i.One;
+
+            // since we will be computing new MWN values as necessary, we cannot use
+            // winding grid to track whether a voxel is 'new' or not. 
+            // So, using 3D bitmap intead - is updated at end of each pass.
+            Bitmap3 bits = new Bitmap3(new Vector3i(ni, nj, nk));
+            List<Vector3i> cur_front = new List<Vector3i>();
+            foreach (Vector3i ijk in winding.Indices()) {
+                if (winding[ijk] != float.MaxValue) {
+                    cur_front.Add(ijk);
+                    bits[ijk] = true;
+                }
+            }
+            if (CancelF())
+                return;
+
+            // Unique set of 'new' voxels to compute in next iteration.
+            HashSet<Vector3i> queue = new HashSet<Vector3i>();
+            SpinLock queue_lock = new SpinLock();
+
+            while (true) {
+                if (CancelF())
+                    return;
+
+                // can process front voxels in parallel
+                bool abort = false;  int iter_count = 0;
+                gParallel.ForEach(cur_front, (ijk) => {
+                    Interlocked.Increment(ref iter_count);
+                    if (iter_count % 100 == 0)
+                        abort = CancelF();
+                    if (abort)
+                        return;
+
+                    float val = winding[ijk];
+
+                    // check 26-neighbours to see if we have a crossing in any direction
+                    for (int k = 0; k < 26; ++k) {
+                        Vector3i nijk = ijk + gIndices.GridOffsets26[k];
+                        if (bounds.Contains(nijk) == false)
+                            continue;
+                        float val2 = winding[nijk];
+                        if (val2 == float.MaxValue) {
+                            Vector3d gx = new Vector3d((float)nijk.x * dx + origin[0], (float)nijk.y * dx + origin[1], (float)nijk.z * dx + origin[2]);
+                            val2 = (float)MeshSpatial.WindingNumber(gx);
+                            winding[nijk] = val2;
+                        }
+                        if (bits[nijk] == false) {
+                            // this is a 'new' voxel this round.
+                            // If we have a MWN-iso-crossing, add it to the front next round
+                            bool crossing = (val < WindingIsoValue && val2 > WindingIsoValue) ||
+                                            (val > WindingIsoValue && val2 < WindingIsoValue);
+                            if (crossing) {
+                                bool taken = false;
+                                queue_lock.Enter(ref taken);
+                                queue.Add(nijk);
+                                queue_lock.Exit();
+                            }
+                        }
+                    }
+                });
+                if (DebugPrint) System.Console.WriteLine("front has {0} voxels", queue.Count);
+                if (queue.Count == 0)
+                    break;
+
+                // update known-voxels list and create front for next iteration
+                foreach (Vector3i idx in queue)
+                    bits[idx] = true;
+                cur_front.Clear();
+                cur_front.AddRange(queue);
+                queue.Clear();
+            }
+            if (DebugPrint) System.Console.WriteLine("done front-prop");
+
+            if (DebugPrint) {
+                int filled = 0;
+                foreach (Vector3i ijk in winding.Indices()) {
+                    if (winding[ijk] != float.MaxValue)
+                        filled++;
+                }
+                System.Console.WriteLine("filled: {0} / {1}  -  {2}%", filled, ni * nj * nk,
+                                    (double)filled / (double)(ni * nj * nk) * 100.0);
+            }
+
+            if (CancelF())
+                return;
+
+            // fill in the rest of the grid by propagating know MWN values
+            fill_spans(ni, nj, nk, winding);
+
+            if (DebugPrint) System.Console.WriteLine("done sweep");
+
+
+        }
+
+
+
+
+
+
+
+
+
+
+        void make_grid_dense(Vector3f origin, float dx,
+                             int ni, int nj, int nk,
+                             DenseGrid3f winding)
+        {
+            winding.resize(ni, nj, nk);
+
+            MeshSpatial.WindingNumber(Vector3d.Zero);
+            bool abort = false; int count = 0;
+            gParallel.ForEach(winding.Indices(), (ijk) => {
+                Interlocked.Increment(ref count);
+                if (count % 100 == 0)
+                    abort = CancelF();
+                if (abort)
+                    return;
+
+                Vector3d gx = new Vector3d((float)ijk.x * dx + origin[0], (float)ijk.y * dx + origin[1], (float)ijk.z * dx + origin[2]);
+                winding[ijk] = (float)MeshSpatial.WindingNumber(gx);
+            });
+
+        }   // end make_level_set_3
+
+
+
+
+
+        void fill_spans(int ni, int nj, int nk, DenseGrid3f winding)
+        {
+            gParallel.ForEach(gIndices.Grid3IndicesYZ(nj, nk), (idx) => {
+                int j = idx.y, k = idx.z;
+                float last = winding[0, j, k];
+                if (last == float.MaxValue)
+                    last = 0;
+                for (int i = 0; i < ni; ++i) {
+                    if (winding[i, j, k] == float.MaxValue) {
+                        winding[i, j, k] = last;
+                    } else {
+                        last = winding[i, j, k];
+                        if (last < WindingIsoValue)   // propagate zeros on outside
+                            last = 0;
+                    }
+                }
+            });
+        }
+
+
+
+
+        
+        
+
+
+
+
+    }
+}
diff --git a/spatial/NormalHistogram.cs b/spatial/NormalHistogram.cs
index 3c0d3826..77ab6cf0 100644
--- a/spatial/NormalHistogram.cs
+++ b/spatial/NormalHistogram.cs
@@ -6,64 +6,75 @@
 namespace g3
 {
     /// <summary>
-    /// Construct "histogram" of normals of mesh. Basically each normal is scaled up
-    /// and then rounded to int. This is not a great strategy, but it works for 
-    /// finding planes/etc.
-    /// 
-    /// [TODO] variant that bins normals based on semi-regular mesh of sphere
+    /// Construct spherical histogram of normals of mesh. 
+    /// Binning is done using a Spherical Fibonacci point set.
     /// </summary>
     public class NormalHistogram
     {
-        public DMesh3 Mesh;
+        public int Bins = 1024;
+        public SphericalFibonacciPointSet Points;
+        public double[] Counts;
 
-        public int IntScale = 256;
-        public bool UseAreaWeighting = true;
-        public Dictionary<Vector3i, double> Histogram;
+        public HashSet<int> UsedBins;
 
+        public NormalHistogram(int bins, bool bTrackUsed = false)
+        {
+            Bins = bins;
+            Points = new SphericalFibonacciPointSet(bins);
+            Counts = new double[bins];
+            if (bTrackUsed)
+                UsedBins = new HashSet<int>();
+        }
 
-        public NormalHistogram(DMesh3 mesh)
+        /// <summary>
+        /// legacy API
+        /// </summary>
+        public NormalHistogram(DMesh3 mesh, bool bWeightByArea = true, int bins = 1024) : this(bins)
         {
-            Mesh = mesh;
-            Histogram = new Dictionary<Vector3i, double>();
-            build();
+            CountFaceNormals(mesh, bWeightByArea);
         }
 
 
         /// <summary>
-        /// return (rounded) normal associated w/ maximum weight/area
+        /// bin and count point, and optionally normalize
         /// </summary>
-        public Vector3d FindMaxNormal()
+        public void Count(Vector3d pt, double weight = 1.0, bool bIsNormalized = false) {
+            int bin = Points.NearestPoint(pt, bIsNormalized);
+            Counts[bin] += weight;
+            if (UsedBins != null)
+                UsedBins.Add(bin);
+        }
+
+        /// <summary>
+        /// Count all input mesh face normals
+        /// </summary>
+        public void CountFaceNormals(DMesh3 mesh, bool bWeightByArea = true)
         {
-            Vector3i maxN = Vector3i.AxisY; double maxArea = 0;
-            foreach (var pair in Histogram) {
-                if (pair.Value > maxArea) {
-                    maxArea = pair.Value;
-                    maxN = pair.Key;
+            foreach (int tid in mesh.TriangleIndices()) {
+                if (bWeightByArea) {
+                    Vector3d n, c; double area;
+                    mesh.GetTriInfo(tid, out n, out area, out c);
+                    Count(n, area, true);
+                } else {
+                    Count(mesh.GetTriNormal(tid), 1.0, true);
                 }
             }
-            Vector3d n = new Vector3d(maxN.x, maxN.y, maxN.z);
-            n.Normalize();
-            return n;
         }
 
 
-
-
-        void build()
+        /// <summary>
+        /// return (quantized) normal associated w/ maximum weight/area
+        /// </summary>
+        public Vector3d FindMaxNormal()
         {
-            foreach (int tid in Mesh.TriangleIndices()) {
-                double w = (UseAreaWeighting) ? Mesh.GetTriArea(tid) : 1.0;
-
-                Vector3d n = Mesh.GetTriNormal(tid);
-
-                Vector3i up = new Vector3i((int)(n.x * IntScale), (int)(n.y * IntScale), (int)(n.z * IntScale));
-
-                if (Histogram.ContainsKey(up))
-                    Histogram[up] += w;
-                else
-                    Histogram[up] = w;
+            int max_i = 0;
+            for ( int k = 1; k < Bins; ++k ) {
+                if (Counts[k] > Counts[max_i])
+                    max_i = k;
             }
+            return Points[max_i];
         }
 
+
     }
 }
diff --git a/spatial/PointAABBTree3.cs b/spatial/PointAABBTree3.cs
index 8b3edb3e..2b8af555 100644
--- a/spatial/PointAABBTree3.cs
+++ b/spatial/PointAABBTree3.cs
@@ -7,8 +7,7 @@
 namespace g3
 {
     /// <summary>
-    /// Hierarchical Axis-Aligned-Bounding-Box tree for a DMesh3 mesh.
-    /// This class supports a variety of spatial queries, listed below.
+    /// Hierarchical Axis-Aligned-Bounding-Box tree for an IPointSet
     /// 
     /// 
     /// TODO: no timestamp support right now...
@@ -17,7 +16,7 @@ namespace g3
     public class PointAABBTree3
     {
         IPointSet points;
-        //int points_timestamp;
+        int points_timestamp;
 
         public PointAABBTree3(IPointSet pointsIn, bool autoBuild = true)
         {
@@ -60,7 +59,7 @@ public void Build(BuildStrategy eStrategy = BuildStrategy.TopDownMidpoint)
             else if (eStrategy == BuildStrategy.Default)
                 build_top_down(false);
 
-            //points_timestamp = points.Timestamp;
+            points_timestamp = points.Timestamp;
         }
 
 
@@ -70,8 +69,8 @@ public void Build(BuildStrategy eStrategy = BuildStrategy.TopDownMidpoint)
         /// </summary>
         public virtual int FindNearestPoint(Vector3d p, double fMaxDist = double.MaxValue)
         {
-            //if (points_timestamp != points.Timestamp)
-            //    throw new Exception("PointAABBTree3.FindNearestPoint: mesh has been modified since tree construction");
+            if (points_timestamp != points.Timestamp)
+                throw new Exception("PointAABBTree3.FindNearestPoint: mesh has been modified since tree construction");
 
             double fNearestSqr = (fMaxDist < double.MaxValue) ? fMaxDist * fMaxDist : double.MaxValue;
             int tNearID = DMesh3.InvalidID;
@@ -152,8 +151,8 @@ public class TreeTraversal
         /// </summary>
         public virtual void DoTraversal(TreeTraversal traversal)
         {
-            //if (points_timestamp != points.Timestamp)
-            //    throw new Exception("PointAABBTree3.FindNearestPoint: mesh has been modified since tree construction");
+            if (points_timestamp != points.Timestamp)
+                throw new Exception("PointAABBTree3.FindNearestPoint: mesh has been modified since tree construction");
 
             tree_traversal(root_index, 0, traversal);
         }
@@ -194,6 +193,248 @@ protected virtual void tree_traversal(int iBox, int depth, TreeTraversal travers
 
 
 
+
+
+
+
+
+        /*
+         *  Fast Mesh Winding Number computation
+         */
+
+        /// <summary>
+        /// FWN beta parameter - is 2.0 in paper
+        /// </summary>
+        public double FWNBeta = 2.0;
+
+        /// <summary>
+        /// FWN approximation order. can be 1 or 2. 2 is more accurate, obviously.
+        /// </summary>
+        public int FWNApproxOrder = 2;
+
+        /// <summary>
+        /// Replace this with function that returns proper area estimate
+        /// </summary>
+        public Func<int, double> FWNAreaEstimateF = (vid) => { return 1.0; };
+
+
+        /// <summary>
+        /// Fast approximation of winding number using far-field approximations
+        /// </summary>
+        public virtual double FastWindingNumber(Vector3d p)
+        {
+            if (points_timestamp != points.Timestamp)
+                throw new Exception("PointAABBTree3.FindNearestPoint: mesh has been modified since tree construction");
+
+            if (FastWindingCache == null || fast_winding_cache_timestamp != points.Timestamp) {
+                build_fast_winding_cache();
+                fast_winding_cache_timestamp = points.Timestamp;
+            }
+
+            double sum = branch_fast_winding_num(root_index, p);
+            return sum;
+        }
+
+        // evaluate winding number contribution for all points below iBox
+        protected double branch_fast_winding_num(int iBox, Vector3d p)
+        {
+            double branch_sum = 0;
+
+            int idx = box_to_index[iBox];
+            if (idx < points_end) {            // point-list case, array is [N t1 t2 ... tN]
+                int num_pts = index_list[idx];
+                for (int i = 1; i <= num_pts; ++i) {
+                    int pi = index_list[idx + i];
+                    Vector3d v = Points.GetVertex(pi);
+                    Vector3d n = Points.GetVertexNormal(pi);
+                    double a = FastWindingAreaCache[pi];
+                    branch_sum += FastPointWinding.ExactEval(ref v, ref n, a, ref p);
+                }
+
+            } else {                                // internal node, either 1 or 2 child boxes
+                int iChild1 = index_list[idx];
+                if (iChild1 < 0) {                 // 1 child, descend if nearer than cur min-dist
+                    iChild1 = (-iChild1) - 1;
+
+                    // if we have winding cache, we can more efficiently compute contribution of all points
+                    // below this box. Otherwise, recursively descend tree.
+                    bool contained = box_contains(iChild1, p);
+                    if (contained == false && can_use_fast_winding_cache(iChild1, ref p))
+                        branch_sum += evaluate_box_fast_winding_cache(iChild1, ref p);
+                    else
+                        branch_sum += branch_fast_winding_num(iChild1, p);
+
+                } else {                            // 2 children, descend closest first
+                    iChild1 = iChild1 - 1;
+                    int iChild2 = index_list[idx + 1] - 1;
+
+                    bool contained1 = box_contains(iChild1, p);
+                    if (contained1 == false && can_use_fast_winding_cache(iChild1, ref p))
+                        branch_sum += evaluate_box_fast_winding_cache(iChild1, ref p);
+                    else
+                        branch_sum += branch_fast_winding_num(iChild1, p);
+
+                    bool contained2 = box_contains(iChild2, p);
+                    if (contained2 == false && can_use_fast_winding_cache(iChild2, ref p))
+                        branch_sum += evaluate_box_fast_winding_cache(iChild2, ref p);
+                    else
+                        branch_sum += branch_fast_winding_num(iChild2, p);
+                }
+            }
+
+            return branch_sum;
+        }
+
+
+        struct FWNInfo
+        {
+            public Vector3d Center;
+            public double R;
+            public Vector3d Order1Vec;
+            public Matrix3d Order2Mat;
+        }
+
+        Dictionary<int, FWNInfo> FastWindingCache;
+        double[] FastWindingAreaCache;
+        int fast_winding_cache_timestamp = -1;
+
+        protected void build_fast_winding_cache()
+        {
+            // set this to a larger number to ignore caches if number of points is too small.
+            // (seems to be no benefit to doing this...is holdover from tree-decomposition FWN code)
+            int WINDING_CACHE_THRESH = 1;
+
+            FastWindingAreaCache = new double[Points.MaxVertexID];
+            foreach (int vid in Points.VertexIndices())
+                FastWindingAreaCache[vid] = FWNAreaEstimateF(vid);
+
+            FastWindingCache = new Dictionary<int, FWNInfo>();
+            HashSet<int> root_hash;
+            build_fast_winding_cache(root_index, 0, WINDING_CACHE_THRESH, out root_hash);
+        }
+        protected int build_fast_winding_cache(int iBox, int depth, int pt_count_thresh, out HashSet<int> pts_hash)
+        {
+            pts_hash = null;
+
+            int idx = box_to_index[iBox];
+            if (idx < points_end) {            // point-list case, array is [N t1 t2 ... tN]
+                int num_pts = index_list[idx];
+                return num_pts;
+
+            } else {                                // internal node, either 1 or 2 child boxes
+                int iChild1 = index_list[idx];
+                if (iChild1 < 0) {                 // 1 child, descend if nearer than cur min-dist
+                    iChild1 = (-iChild1) - 1;
+                    int num_child_pts = build_fast_winding_cache(iChild1, depth + 1, pt_count_thresh, out pts_hash);
+
+                    // if count in child is large enough, we already built a cache at lower node
+                    return num_child_pts;
+
+                } else {                            // 2 children, descend closest first
+                    iChild1 = iChild1 - 1;
+                    int iChild2 = index_list[idx + 1] - 1;
+
+                    // let each child build its own cache if it wants. If so, it will return the
+                    // list of its child points
+                    HashSet<int> child2_hash;
+                    int num_pts_1 = build_fast_winding_cache(iChild1, depth + 1, pt_count_thresh, out pts_hash);
+                    int num_pts_2 = build_fast_winding_cache(iChild2, depth + 1, pt_count_thresh, out child2_hash);
+                    bool build_cache = (num_pts_1 + num_pts_2 > pt_count_thresh);
+
+                    if (depth == 0)
+                        return num_pts_1 + num_pts_2;  // cannot build cache at level 0...
+
+                    // collect up the points we need. there are various cases depending on what children already did
+                    if (pts_hash != null || child2_hash != null || build_cache) {
+                        if (pts_hash == null && child2_hash != null) {
+                            collect_points(iChild1, child2_hash);
+                            pts_hash = child2_hash;
+                        } else {
+                            if (pts_hash == null) {
+                                pts_hash = new HashSet<int>();
+                                collect_points(iChild1, pts_hash);
+                            }
+                            if (child2_hash == null)
+                                collect_points(iChild2, pts_hash);
+                            else
+                                pts_hash.UnionWith(child2_hash);
+                        }
+                    }
+                    if (build_cache)
+                        make_box_fast_winding_cache(iBox, pts_hash);
+
+                    return (num_pts_1 + num_pts_2);
+                }
+            }
+        }
+
+
+        // check if we can use fwn 
+        protected bool can_use_fast_winding_cache(int iBox, ref Vector3d q)
+        {
+            FWNInfo cacheInfo;
+            if (FastWindingCache.TryGetValue(iBox, out cacheInfo) == false)
+                return false;
+
+            double dist_qp = cacheInfo.Center.Distance(ref q);
+            if (dist_qp > FWNBeta * cacheInfo.R)
+                return true;
+
+            return false;
+        }
+
+
+        // compute FWN cache for all points underneath this box
+        protected void make_box_fast_winding_cache(int iBox, IEnumerable<int> pointIndices)
+        {
+            Util.gDevAssert(FastWindingCache.ContainsKey(iBox) == false);
+
+            // construct cache
+            FWNInfo cacheInfo = new FWNInfo();
+            FastPointWinding.ComputeCoeffs(points, pointIndices, FastWindingAreaCache,
+                ref cacheInfo.Center, ref cacheInfo.R, ref cacheInfo.Order1Vec, ref cacheInfo.Order2Mat);
+
+            FastWindingCache[iBox] = cacheInfo;
+        }
+
+        // evaluate the FWN cache for iBox
+        protected double evaluate_box_fast_winding_cache(int iBox, ref Vector3d q)
+        {
+            FWNInfo cacheInfo = FastWindingCache[iBox];
+
+            if (FWNApproxOrder == 2)
+                return FastPointWinding.EvaluateOrder2Approx(ref cacheInfo.Center, ref cacheInfo.Order1Vec, ref cacheInfo.Order2Mat, ref q);
+            else
+                return FastPointWinding.EvaluateOrder1Approx(ref cacheInfo.Center, ref cacheInfo.Order1Vec, ref q);
+        }
+
+
+        // collect all the triangles below iBox in a hash
+        protected void collect_points(int iBox, HashSet<int> points)
+        {
+            int idx = box_to_index[iBox];
+            if (idx < points_end) {            // triange-list case, array is [N t1 t2 ... tN]
+                int num_tris = index_list[idx];
+                for (int i = 1; i <= num_tris; ++i)
+                    points.Add(index_list[idx + i]);
+            } else {
+                int iChild1 = index_list[idx];
+                if (iChild1 < 0) {                 // 1 child, descend if nearer than cur min-dist
+                    collect_points((-iChild1) - 1, points);
+                } else {                           // 2 children, descend closest first
+                    collect_points(iChild1 - 1, points);
+                    collect_points(index_list[idx + 1] - 1, points);
+                }
+            }
+        }
+
+
+
+
+
+
+
+
         /// <summary>
         /// Total sum of volumes of all boxes in the tree. Mainly useful to evaluate tree quality.
         /// </summary>
@@ -227,6 +468,13 @@ public double TotalExtentSum()
         }
 
 
+        /// <summary>
+        /// Root bounding box of tree (note: tree must be generated by calling a query function first!)
+        /// </summary>
+        public AxisAlignedBox3d Bounds {
+            get { return get_box(root_index); }
+        }
+
 
 
         //
@@ -482,6 +730,8 @@ int split_point_set_midpoint(int[] pt_indices, Vector3d[] positions,
         }
 
 
+        const double box_eps = 50.0 * MathUtil.Epsilon;
+
 
         AxisAlignedBox3d get_box(int iBox)
         {
@@ -506,6 +756,16 @@ double box_distance_sqr(int iBox, ref Vector3d p)
         }
 
 
+        protected bool box_contains(int iBox, Vector3d p)
+        {
+            // [TODO] this could be way faster...
+            Vector3d c = (Vector3d)box_centers[iBox];
+            Vector3d e = box_extents[iBox];
+            AxisAlignedBox3d box = new AxisAlignedBox3d(ref c, e.x + box_eps, e.y + box_eps, e.z + box_eps);
+            return box.Contains(p);
+        }
+
+
 
         // 1) make sure we can reach every point through tree (also demo of how to traverse tree...)
         // 2) make sure that points are contained in parent boxes
diff --git a/spatial/PointSetHashtable.cs b/spatial/PointSetHashtable.cs
new file mode 100644
index 00000000..0805dc16
--- /dev/null
+++ b/spatial/PointSetHashtable.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Ryan Schmidt (rms@gradientspace.com) - All Rights Reserved
+// Distributed under the Boost Software License, Version 1.0. http://www.boost.org/LICENSE_1_0.txt
+using System;
+using System.Collections.Generic;
+using g3;
+
+namespace gs
+{
+	public class PointSetHashtable
+	{
+		IPointSet Points;
+		DSparseGrid3<PointList> Grid;
+		ShiftGridIndexer3 indexF;
+
+		Vector3d Origin;
+		double CellSize;
+
+		public PointSetHashtable(IPointSet points)
+		{
+			this.Points = points;
+		}
+
+
+		public void Build(int maxAxisSubdivs = 64) {
+			AxisAlignedBox3d bounds = BoundsUtil.Bounds(Points);
+			double cellsize = bounds.MaxDim / (double)maxAxisSubdivs;
+			Build(cellsize, bounds.Min);
+		}
+
+		public void Build(double cellSize, Vector3d origin) {
+			Origin = origin;
+			CellSize = cellSize;
+			indexF = new ShiftGridIndexer3(Origin, CellSize);
+
+			Grid = new DSparseGrid3<PointList>(new PointList());
+
+			foreach ( int vid in Points.VertexIndices() ) {
+				Vector3d v = Points.GetVertex(vid);
+				Vector3i idx = indexF.ToGrid(v);
+                PointList cell = Grid.Get(idx);
+				cell.Add(vid);
+			}
+		}
+
+
+
+		public bool FindInBall(Vector3d pt, double r, int[] buffer, out int buffer_count ) {
+			buffer_count = 0;
+
+            double halfCell = CellSize * 0.5;
+			Vector3i idx = indexF.ToGrid(pt);
+            Vector3d center = indexF.FromGrid(idx) + halfCell * Vector3d.One;
+
+			if (r > CellSize)
+				throw new ArgumentException("PointSetHashtable.FindInBall: large radius unsupported");
+
+			double r2 = r * r;
+
+			// check all in this cell
+			PointList center_cell = Grid.Get(idx, false);
+			if (center_cell != null) {
+				foreach (int vid in center_cell) {
+					if (pt.DistanceSquared(Points.GetVertex(vid)) < r2) {
+						if (buffer_count == buffer.Length)
+							return false;
+						buffer[buffer_count++] = vid;
+					}
+				}
+			}
+
+			// if we are close enough to cell border we need to check nbrs
+			// [TODO] could iterate over fewer cells here, if r is bounded by CellSize,
+            // then we should only ever need to look at 3, depending on which octant we are in.
+			if ( (pt-center).MaxAbs + r > halfCell ) {
+				for (int ci = 0; ci < 26; ++ci) {
+					Vector3i ioffset = gIndices.GridOffsets26[ci];
+
+                    // if we are within r from face, we need to look into it
+                    Vector3d ptToFaceCenter = new Vector3d(
+                        center.x + halfCell * ioffset.x - pt.x,
+                        center.y + halfCell * ioffset.y - pt.y,
+                        center.z + halfCell * ioffset.z - pt.z);
+                    if (ptToFaceCenter.MinAbs > r)
+                        continue;
+
+					PointList ncell = Grid.Get(idx + ioffset, false);
+					if ( ncell != null ) {
+						foreach (int vid in ncell) {
+							if (pt.DistanceSquared(Points.GetVertex(vid)) < r2) {
+								if (buffer_count == buffer.Length)
+									return false;
+								buffer[buffer_count++] = vid;
+							}
+						}						
+					}
+				}
+			}
+
+			return true;
+		}
+
+
+
+
+
+
+		public class PointList : List<int>, IGridElement3 {
+			public IGridElement3 CreateNewGridElement(bool bCopy) {
+				return new PointList();
+			}
+		}
+
+	}
+}
diff --git a/spatial/Polygon2dBoxTree.cs b/spatial/Polygon2dBoxTree.cs
index dc5027e6..b400e677 100644
--- a/spatial/Polygon2dBoxTree.cs
+++ b/spatial/Polygon2dBoxTree.cs
@@ -32,7 +32,8 @@ public double DistanceSquared(Vector2d pt, out int iHoleIndex, out int iNearSeg,
         {
             iHoleIndex = -1;
             double min_dist = OuterTree.SquaredDistance(pt, out iNearSeg, out fNearSegT);
-            for (int k = 0; k < HoleTrees.Length; ++k) {
+            int NH = (HoleTrees == null) ? 0 : HoleTrees.Length;
+            for (int k = 0; k < NH; ++k) {
                 int hole_near_seg; double hole_seg_t;
                 double hole_dist = HoleTrees[k].SquaredDistance(pt, out hole_near_seg, out hole_seg_t, min_dist);
                 if (hole_dist < min_dist) {
diff --git a/spatial/SegmentHashGrid.cs b/spatial/SegmentHashGrid.cs
index a54be453..2ef1f272 100644
--- a/spatial/SegmentHashGrid.cs
+++ b/spatial/SegmentHashGrid.cs
@@ -126,6 +126,7 @@ public void UpdateSegmentUnsafe(T value, Vector2d old_center, Vector2d new_cente
         /// Find nearest segment in grid, within radius, without locking / thread-safety
         /// You must provided distF which returns distance between query_pt and the segment argument
         /// You can ignore specific segments via ignoreF lambda - return true to ignore 
+        /// Return value is pair (nearest_index,min_dist) or (invalidValue,double.MaxValue)
         /// </summary>
         public KeyValuePair<T, double> FindNearestInRadius(Vector2d query_pt, double radius, Func<T, double> distF, Func<T, bool> ignoreF = null)
         {
@@ -165,7 +166,8 @@ public KeyValuePair<T, double> FindNearestInRadius(Vector2d query_pt, double rad
 
 
         /// <summary>
-        /// Variant of FindNearestInRadius that works with squared-distances
+        /// Variant of FindNearestInRadius that works with squared-distances.
+        /// Return value is pair (nearest_index,min_dist) or (invalidValue,double.MaxValue)
         /// </summary>
         public KeyValuePair<T, double> FindNearestInSquaredRadius(Vector2d query_pt, double radiusSqr, Func<T, double> distSqrF, Func<T, bool> ignoreF = null)
         {
diff --git a/spatial/SpatialInterfaces.cs b/spatial/SpatialInterfaces.cs
index 956b76b6..2c1225ec 100644
--- a/spatial/SpatialInterfaces.cs
+++ b/spatial/SpatialInterfaces.cs
@@ -34,6 +34,11 @@ public interface IProjectionTarget
         Vector3d Project(Vector3d vPoint, int identifier = -1);
     }
 
+    public interface IOrientedProjectionTarget : IProjectionTarget
+    {
+        Vector3d Project(Vector3d vPoint, out Vector3d vProjectNormal, int identifier = -1);
+    }
+
     public interface IIntersectionTarget
     {
         bool HasNormal { get; }
diff --git a/spatial/TriangleBinsGrid2d.cs b/spatial/TriangleBinsGrid2d.cs
new file mode 100644
index 00000000..1d3484fc
--- /dev/null
+++ b/spatial/TriangleBinsGrid2d.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace g3
+{
+
+
+    /// <summary>
+    /// This class is a spatial data structure for 2D triangles. It is intended
+    /// for point-containment and box-overlap queries. It does not store the
+    /// triangles, only indices, so you must pass in the triangle vertices to add/remove
+    /// functions, similar to PointHashGrid2d.
+    /// 
+    /// However, unlike the hash classes, this one is based on a grid of "bins" which 
+    /// has a fixed size, so you must provide a bounding box on construction. 
+    /// Each triangle is inserted into every bin that it overlaps. 
+    /// 
+    /// [TODO] currently each triangle is inserted into every bin that it's *bounding box*
+    /// overlaps. Need conservative rasterization to improve this. Can implement by
+    /// testing each bin bbox for intersection w/ triangle
+    /// </summary>
+    public class TriangleBinsGrid2d
+    {
+        ShiftGridIndexer2 indexer;
+        AxisAlignedBox2d bounds;
+
+        SmallListSet bins_list;
+        int bins_x, bins_y;
+        AxisAlignedBox2i grid_bounds;
+
+        SpinLock spinlock = new SpinLock();
+
+        /// <summary>
+        /// "invalid" value will be returned by queries if no valid result is found (eg bounded-distance query)
+        /// </summary>
+        public TriangleBinsGrid2d(AxisAlignedBox2d bounds, int numCells) 
+        {
+            this.bounds = bounds;
+            double cellsize = bounds.MaxDim / (double)numCells;
+            Vector2d origin = bounds.Min - cellsize * 0.5 * Vector2d.One;
+            indexer = new ShiftGridIndexer2(origin, cellsize);
+
+            bins_x = (int)(bounds.Width / cellsize) + 2;
+            bins_y = (int)(bounds.Height / cellsize) + 2;
+            grid_bounds = new AxisAlignedBox2i(0, 0, bins_x-1, bins_y-1);
+            bins_list = new SmallListSet();
+            bins_list.Resize(bins_x * bins_y);
+        }
+
+
+        public AxisAlignedBox2d Bounds {
+            get { return bounds; }
+        }
+
+        /// <summary>
+        /// Insert triangle. This function is thread-safe, uses a SpinLock internally
+        /// </summary>
+        public void InsertTriangle(int triangle_id, ref Vector2d a, ref Vector2d b, ref Vector2d c)
+        {
+            insert_triangle(triangle_id, ref a, ref b, ref c, true);
+        }
+
+        /// <summary>
+        /// Insert triangle without locking / thread-safety
+        /// </summary>
+        public void InsertTriangleUnsafe(int triangle_id, ref Vector2d a, ref Vector2d b, ref Vector2d c)
+        {
+            insert_triangle(triangle_id, ref a, ref b, ref c, false);
+        }
+
+
+        /// <summary>
+        /// Remove triangle. This function is thread-safe, uses a SpinLock internally
+        /// </summary>
+        public void RemoveTriangle(int triangle_id, ref Vector2d a, ref Vector2d b, ref Vector2d c)
+        {
+            remove_triangle(triangle_id, ref a, ref b, ref c, true);
+        }
+
+        /// <summary>
+        /// Remove triangle without locking / thread-safety
+        /// </summary>
+        public void RemoveTriangleUnsafe(int triangle_id, ref Vector2d a, ref Vector2d b, ref Vector2d c)
+        {
+            remove_triangle(triangle_id, ref a, ref b, ref c, false);
+        }
+
+
+        /// <summary>
+        /// Find triangle that contains point. Not thread-safe.
+        /// You provide containsF(), which does the containment check.
+        /// If you provide ignoreF(), then tri is skipped if ignoreF(tid) == true
+        /// </summary>
+        public int FindContainingTriangle(Vector2d query_pt, Func<int, Vector2d, bool> containsF, Func<int, bool> ignoreF = null)
+        {
+            Vector2i grid_idx = indexer.ToGrid(query_pt);
+            if (grid_bounds.Contains(grid_idx) == false)
+                return DMesh3.InvalidID; ;
+
+            int bin_i = grid_idx.y * bins_x + grid_idx.x;
+            if (ignoreF == null) {
+                foreach (int tid in bins_list.ValueItr(bin_i)) {
+                    if (containsF(tid, query_pt))
+                        return tid;
+                }
+            } else {
+                foreach (int tid in bins_list.ValueItr(bin_i)) {
+                    if (ignoreF(tid) == false && containsF(tid, query_pt))
+                        return tid;
+                }
+            }
+
+            return DMesh3.InvalidID;
+        }
+
+
+
+
+        /// <summary>
+        /// find all triangles that overlap range
+        /// </summary>
+        public void FindTrianglesInRange(AxisAlignedBox2d range, HashSet<int> triangles)
+        {
+            Vector2i grid_min = indexer.ToGrid(range.Min);
+            if (grid_bounds.Contains(grid_min) == false)
+                throw new Exception("TriangleBinsGrid2d.FindTrianglesInRange: range.Min is out of bounds");
+            Vector2i grid_max = indexer.ToGrid(range.Max);
+            if (grid_bounds.Contains(grid_max) == false)
+                throw new Exception("TriangleBinsGrid2d.FindTrianglesInRange: range.Max is out of bounds");
+
+            for (int yi = grid_min.y; yi <= grid_max.y; ++yi) {
+                for (int xi = grid_min.x; xi <= grid_max.x; ++xi) {
+                    int bin_i = yi * bins_x + xi;
+                    foreach (int tid in bins_list.ValueItr(bin_i))
+                        triangles.Add(tid);
+                }
+            }
+
+        }
+
+
+
+
+
+        void insert_triangle(int triangle_id, ref Vector2d a, ref Vector2d b, ref Vector2d c, bool threadsafe = true)
+        {
+            bool lockTaken = false;
+            while (threadsafe == true && lockTaken == false)
+                spinlock.Enter(ref lockTaken);
+
+            // [TODO] actually want to conservatively rasterize triangles here, not just
+            // store in every cell in bbox!
+
+            AxisAlignedBox2d bounds = BoundsUtil.Bounds(ref a, ref b, ref c);
+            Vector2i imin = indexer.ToGrid(bounds.Min);
+            Vector2i imax = indexer.ToGrid(bounds.Max);
+
+            for ( int yi = imin.y; yi <= imax.y; ++yi ) {
+                for (int xi = imin.x; xi <= imax.x; ++xi) {
+
+                    // check if triangle overlaps this grid cell...
+
+                    int bin_i = yi * bins_x + xi;
+                    bins_list.Insert(bin_i, triangle_id);
+                }
+            }
+
+            if (lockTaken)
+                spinlock.Exit();
+        }
+
+
+        void remove_triangle(int triangle_id, ref Vector2d a, ref Vector2d b, ref Vector2d c, bool threadsafe = true)
+        {
+            bool lockTaken = false;
+            while (threadsafe == true && lockTaken == false)
+                spinlock.Enter(ref lockTaken);
+
+            AxisAlignedBox2d bounds = BoundsUtil.Bounds(ref a, ref b, ref c);
+            Vector2i imin = indexer.ToGrid(bounds.Min);
+            Vector2i imax = indexer.ToGrid(bounds.Max);
+            for (int yi = imin.y; yi <= imax.y; ++yi) {
+                for (int xi = imin.x; xi <= imax.x; ++xi) {
+                    int bin_i = yi * bins_x + xi;
+                    bins_list.Remove(bin_i, triangle_id);
+                }
+            }
+
+            if (lockTaken)
+                spinlock.Exit();
+        }
+    }
+}