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 +{ + /// + /// 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 + /// + public class SphericalFibonacciPointSet + { + public int N = 64; + + public SphericalFibonacciPointSet(int n = 64) { + N = n; + } + + + public int Count { get { return N; } } + + + /// + /// Compute i'th spherical point + /// + 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); } + } + + + /// + /// Find index of nearest point-set point for input arbitrary point + /// + 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); + } + + /// + /// 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). + /// + 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) - // + /// + /// Convenience functions for working with arrays. + /// - Math functions on arrays of floats/doubles + /// - "automatic" conversion from IEnumerable (via introspection) + /// - byte[] conversions + /// - zlib compress/decompress byte[] buffers + /// 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(IEnumerable values) { + /// + /// convert byte array to int array + /// + 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; + } + + + /// + /// convert byte array to short array + /// + 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; + } + + + /// + /// convert byte array to double array + /// + 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; + } + + + /// + /// convert byte array to float array + /// + 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; + } + + + /// + /// convert byte array to VectorArray3d + /// + 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; + } + + + + /// + /// convert byte array to VectorArray2f + /// + 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; + } + + /// + /// convert byte array to VectorArray3f + /// + 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; + } + + + + + /// + /// convert byte array to VectorArray3i + /// + 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; + } + + + /// + /// convert byte array to IndexArray4i + /// + 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; + } + + + /// + /// convert int array to bytes + /// + static public byte[] ToBytes(int[] array) + { + byte[] result = new byte[array.Length * sizeof(int)]; + Buffer.BlockCopy(array, 0, result, 0, result.Length); + return result; + } + + /// + /// convert short array to bytes + /// + static public byte[] ToBytes(short[] array) + { + byte[] result = new byte[array.Length * sizeof(short)]; + Buffer.BlockCopy(array, 0, result, 0, result.Length); + return result; + } + + /// + /// convert float array to bytes + /// + static public byte[] ToBytes(float[] array) + { + byte[] result = new byte[array.Length * sizeof(float)]; + Buffer.BlockCopy(array, 0, result, 0, result.Length); + return result; + } + + /// + /// convert double array to bytes + /// + static public byte[] ToBytes(double[] array) + { + byte[] result = new byte[array.Length * sizeof(double)]; + Buffer.BlockCopy(array, 0, result, 0, result.Length); + return result; + } + + + + + /// + /// Compress a byte buffer using Deflate/ZLib compression. + /// + 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; + } + + + /// + /// Decompress a byte buffer that has been compressed using Deflate/ZLib compression + /// + 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 + /// + /// utility class for porting C++ code that uses this kind of idiom: + /// T * ptr = &array[i]; + /// ptr[k] = value + /// public struct ArrayAlias { 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 + public class DVector : IEnumerable { List Blocks; int iCurBlock; @@ -53,7 +52,6 @@ public DVector(IEnumerable init) iCurBlockUsed = 0; Blocks = new List(); 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 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 v, int * pBuffer) + public IEnumerator 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 /// /// public class DijkstraGraphDistance { + public const float InvalidValue = float.MaxValue; /// /// 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); } + } + + /// + /// shortcut to construct graph for mesh vertices + /// + 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); } + + /// + /// shortcut to construct graph for mesh triangles + /// + public static DijkstraGraphDistance MeshTriangles(DMesh3 mesh, bool bSparse = false) + { + Func 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); + } + + + + /// + /// reset internal data structures/etc + /// 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) } + + + /// + /// 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) ? + /// + public void ComputeToNode(int node_id, float fMaxDistance = InvalidValue) + { + if (TrackOrder == true) + order = new List(); + + 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); + } + } + + + + + + + + /// + /// Compute distances until node_id is frozen, or (optional) max distance is reached + /// Terminates early, so Queue may not be empty + /// + public int ComputeToNode(Func terminatingNodeF, float fMaxDistance = InvalidValue) + { + if (TrackOrder == true) + order = new List(); + + if (SparseNodes != null) + return ComputeToNode_Sparse(terminatingNodeF, fMaxDistance); + else + return ComputeToNode_Dense(terminatingNodeF, fMaxDistance); + } + protected int ComputeToNode_Sparse(Func 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 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; + } + + + + + + + /// /// Get the maximum distance encountered during the Compute() /// @@ -251,23 +408,27 @@ public float MaxDistance { /// - /// 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. /// 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; } } + /// + /// Get (internal) list of frozen nodes in increasing distance-order. + /// Requries that TrackOrder=true before Compute call. + /// public List GetOrder() { if (TrackOrder == false) @@ -277,6 +438,43 @@ public List GetOrder() + /// + /// Walk from node fromv back to the (graph-)nearest seed point. + /// + public bool GetPathToSeed(int fromv, List 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); } } } - /// - /// shortcut to setup functions for mesh vertices - /// - 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) } + /// + /// Query the priority at node id, assuming it exists in queue + /// + 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 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 +{ + /// + /// interface that provides a cancel function + /// + public interface ICancelSource + { + bool Cancelled(); + } + + + /// + /// Just wraps a func as an ICancelSource + /// + public class CancelFunction : ICancelSource + { + public Func CancelF; + public CancelFunction(Func cancelF) { + CancelF = cancelF; + } + public bool Cancelled() { return CancelF(); } + } + + + /// + /// 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 + /// + 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 cancelF) + { + Source = new CancelFunction(cancelF); + } + + /// + /// Check if client would like to cancel + /// + 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 - // + /// + /// 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. + /// + /// public class RefCountVector : System.Collections.IEnumerable { public static readonly short invalid = -1; - DVector ref_counts; DVector 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) { } + + /// + /// 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. + /// + 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; + } + } + + + /// + /// 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. + /// + 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> opF) + { + bool lockTaken = false; + while (lockTaken == false) + spinlock.Enter(ref lockTaken); + + opF(List); + + spinlock.Exit(); + } + + public List 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) } + /// + /// integer type/value pair, packed into 32 bits - 8 for type, 24 for value + /// + 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; } } + } + + /// 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() } + + + /// + /// IList wrapper for an Interval1i, ie sequential list of integers + /// + public struct IntSequence : IList + { + Interval1i range; + + public IntSequence(Interval1i ival) { + range = ival; + } + public IntSequence(int iStart, int iEnd) { + range = new Interval1i(iStart, iEnd); + } + + /// construct interval [0, N-1] + static public IntSequence Range(int N) { return new IntSequence(0, N - 1); } + + /// construct interval [0, N-1] + static public IntSequence RangeInclusive(int N) { return new IntSequence(0, N); } + + /// construct interval [start, start+N-1] + static public IntSequence Range(int start, int N) { return new IntSequence(start, start + N - 1); } + + + /// construct interval [a, b] + 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 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(IEnumerable source, Action body) } public static void ForEach( IEnumerable source, Action body ) { -#if G3_USING_UNITY +#if G3_USING_UNITY && (NET_2_0 || NET_2_0_SUBSET) for_each(source, body); #else Parallel.ForEach(source, body); @@ -42,8 +42,9 @@ public static void Evaluate(params Action[] funcs) /// - /// 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 /// public static void BlockStartEnd(int iStart, int iEnd, Action 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 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 +{ + /// + /// 2D Bezier curve of arbitrary degree + /// Ported from WildMagic5 Wm5BezierCurve2 + /// + 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; + } + + /// + /// Radius of n-sided regular polygon that contains circle of radius r + /// + 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 vertices, int i) + public static Vector3d GetTangent(List 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 vertices) { + public static double ArcLength(List 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 vertices) { @@ -38,6 +51,7 @@ public static double ArcLength(IEnumerable 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 vertices, int iStart, int iEnd, + /// + /// smooth set of vertices using extra buffer + /// + public static void IterativeSmooth(IList vertices, double alpha, int nIterations, bool bClosed) + { + IterativeSmooth(vertices, 0, vertices.Count, alpha, nIterations, bClosed); + } + /// + /// smooth set of vertices using extra buffer + /// + public static void IterativeSmooth(IList 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 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 + /// + /// return list of objects for which keepF(obj) returns true + /// + public static List Filter(List objects, Func keepF) + { + List result = new List(objects.Count); + foreach (var obj in objects) { + if (keepF(obj)) + result.Add(obj); + } + return result; + } + + + /// + /// Split the input list into two new lists, based on predicate (set1 == true) + /// + public static void Split(List objects, out List set1, out List set2, Func splitF) + { + set1 = new List(); + set2 = new List(); + 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; + } + + + + + /// + /// Remove polygons and polygon-holes smaller than minArea + /// + public static List FilterDegenerate(List polygons, double minArea) + { + List result = new List(polygons.Count); + List filteredHoles = new List(); + 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 { + /// + /// DCurve3 is a 3D polyline, either open or closed (via .Closed) + /// Despite the D prefix, it is *not* dynamic + /// 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(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 Vertices { @@ -122,57 +147,107 @@ public IEnumerable 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 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); + } + } + + + /// + /// Compute opening angle at vertex i in degrees + /// + 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); + } + + /// + /// Find nearest vertex to point p + /// public int NearestVertex(Vector3d p) { double nearSqr = double.MaxValue; @@ -189,6 +264,9 @@ public int NearestVertex(Vector3d p) } + /// + /// find squared distance from p to nearest segment on polyline + /// 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); } + + + /// + /// 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? + /// + 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 Loops; public List 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) + /// + /// 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! + /// + public static void ChainOpenPaths(Curves c, double epsilon = MathUtil.Epsilon) + { + List to_process = new List(c.Paths); + c.Paths = new List(); + + // first we separate out 'dangling' curves that have no match on at least one side + List dangling = new List(); + List remaining = new List(); + + 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 find_connected_start(PolyLine2d pTest, List potential, double eps = MathUtil.Epsilon) + { + List result = new List(); + 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 find_connected_end(PolyLine2d pTest, List potential, double eps = MathUtil.Epsilon) + { + List result = new List(); + 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; + } + + + + /// /// 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 Loops; public List Paths; + + public HashSet BoundaryV; + public HashSet JunctionV; + + public List> LoopEdges; + public List> PathEdges; } /// /// Decompose graph into simple polylines and polygons. /// - public static Curves ExtractCurves(DGraph3 graph) + public static Curves ExtractCurves(DGraph3 graph, + bool bWantLoopIndices = false, + Func CurveOrientationF = null ) { Curves c = new Curves(); c.Loops = new List(); c.Paths = new List(); + if (bWantLoopIndices) { + c.LoopEdges = new List>(); + c.PathEdges = new List>(); + } HashSet used = new HashSet(); @@ -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 pathE = (bWantLoopIndices) ? new List() : 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 pathE = (bWantLoopIndices) ? new List() : 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 polyE = (bWantLoopIndices) ? new List() : 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 WalkToNextNonRegularVtx(DGraph3 graph, int fromVtx, int + + + /// + /// Erode inwards from open boundary vertices of graph (ie vtx with single edge). + /// Resulting graph is not compact (!) + /// + public static void ErodeOpenSpurs(DGraph3 graph) + { + HashSet used = new HashSet(); // do we need this? + + // find boundary and junction vertices + HashSet boundaries = new HashSet(); + HashSet junctions = new HashSet(); + 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 pathE = new List(); + 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; } + /// + /// Checks that all points on a segment are within the area defined by the GeneralPolygon2d; + /// holes are included in the calculation. + /// + 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 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 +{ + /// + /// 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) + /// + /// + 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 SoftConstraints = new Dictionary(); + 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 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 Solvers = new List(); + 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 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 copy) Timestamp = 0; } + public PolyLine2d(IEnumerable copy) + { + vertices = new List(copy); + Timestamp = 0; + } + public PolyLine2d(Vector2d[] v) { vertices = new List(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) } + + /// + /// simplify outer and holes of a polygon solid with same thresholds + /// + 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 collapse_by_deviation_tol(List 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 collapse_by_deviation_tol(List 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 collapse_by_deviation_tol(List input, bool[] keep_segme } + + List handle_tiny_case(List result, List 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 copy) Timestamp = 0; } + public Polygon2d(IEnumerable copy) + { + vertices = new List(copy); + Timestamp = 0; + } + public Polygon2d(Vector2d[] v) { vertices = new List(v); @@ -89,6 +95,11 @@ public void AppendVertex(Vector2d v) vertices.Add(v); Timestamp++; } + public void AppendVertices(IEnumerable v) + { + vertices.AddRange(v); + Timestamp++; + } public void RemoveVertex(int idx) { @@ -96,6 +107,20 @@ public void RemoveVertex(int idx) Timestamp++; } + + public void SetVertices(List 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; } + /// + /// Checks that all points on a segment are within the area defined by the Polygon2d. + /// + 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; } + /// + /// Checks if any point on a segment is within the area defined by the Polygon2d. + /// + 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 FindIntersections(Polygon2d o) { + + public List FindIntersections(Polygon2d o) { List v = new List(); if ( ! this.GetBounds().Intersects( o.GetBounds() ) ) return v; @@ -684,6 +749,17 @@ public void Chamfer(double chamfer_dist, double minConvexAngleDeg = 30, double m + /// + /// Return minimal bounding box of vertices, computed to epsilon tolerance + /// + 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 + /// + /// Distance between ray and segment + /// ported from WildMagic5 + /// + 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() } + + + + + /// + /// compute w/o allocating temporaries/etc + /// + 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 @@ prompt 4 true + AnyCPU pdbonly @@ -31,6 +32,7 @@ prompt 4 true + AnyCPU @@ -53,6 +55,7 @@ + @@ -77,7 +80,9 @@ + + @@ -88,6 +93,7 @@ + @@ -100,9 +106,13 @@ + + + + @@ -119,6 +129,7 @@ + @@ -144,7 +155,11 @@ + + + + @@ -163,6 +178,7 @@ + @@ -178,9 +194,11 @@ + + @@ -285,8 +303,13 @@ + + + + + @@ -294,6 +317,8 @@ + + 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 +{ + + /// + /// [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. + /// + 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 +{ + + /// + /// [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. + /// + 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; + + /// if this function returns true, we should abort calculation + public Func 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); } + } + + /// + /// SDF grid available after calling Compute() + /// + public DenseGrid3f Grid { + get { return grid; } + } + + /// + /// Origin of the SDF grid, in same coordinates as mesh + /// + 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 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 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; + } + + } + + + + + + + + + + + + + /// + /// 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. + /// + 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 + { + /// + /// t is value in range [0,1], returns value in range [0,1] + /// + double FalloffT(double t); + + /// + /// In most cases, users of IFalloffFunction will make a local copy + /// + IFalloffFunction Duplicate(); + } + + + + /// + /// returns 1 in range [0,ConstantRange], and then falls off to 0 in range [ConstantRange,1] + /// + 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 + }; + } + } + + + + /// + /// returns 1 in range [0,ConstantRange], and then falls off to 0 in range [ConstantRange,1] + /// + 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. /// - 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 { - + /// + /// Minimalist implicit function interface + /// public interface ImplicitFunction3d { double Value(ref Vector3d pt); } + /// + /// Bounded implicit function has a bounding box within which + /// the "interesting" part of the function is contained + /// (eg the surface) + /// + public interface BoundedImplicitFunction3d : ImplicitFunction3d + { + AxisAlignedBox3d Bounds(); + } + + + /// + /// Implicit sphere, where zero isocontour is at Radius + /// + 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); + } + } + + + /// + /// Implicit half-space. "Inside" is opposite of Normal direction. + /// + 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); + } + } + + + + /// + /// Implicit axis-aligned box + /// + public class ImplicitAxisAlignedBox3d : BoundedImplicitFunction3d + { + public AxisAlignedBox3d AABox; + + public double Value(ref Vector3d pt) + { + return AABox.SignedDistance(pt); + } + + public AxisAlignedBox3d Bounds() + { + return AABox; + } + } + + + + /// + /// Implicit oriented box + /// + 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; + } + } + + + + /// + /// Implicit tube around line segment + /// + 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; + } + } + + + + + /// + /// Offset the zero-isocontour of an implicit function. + /// Assumes that negative is inside, if not, reverse offset. + /// + 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; + } + } + + + + /// + /// 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 + /// + 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; + } + } + + + + + /// + /// Boolean Union of two implicit functions, A OR B. + /// Assumption is that both have surface at zero isocontour and + /// negative is inside. + /// + 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; + } + } + + + + /// + /// Boolean Intersection of two implicit functions, A AND B + /// Assumption is that both have surface at zero isocontour and + /// negative is inside. + /// + 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; + } + } + + + + /// + /// 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. + /// + 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(); + } + } + + + + + /// + /// Boolean Union of N implicit functions, A OR B. + /// Assumption is that both have surface at zero isocontour and + /// negative is inside. + /// + public class ImplicitNaryUnion3d : BoundedImplicitFunction3d + { + public List 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; + } + } + + + + + /// + /// Boolean Intersection of N implicit functions, A AND B. + /// Assumption is that both have surface at zero isocontour and + /// negative is inside. + /// + public class ImplicitNaryIntersection3d : BoundedImplicitFunction3d + { + public List 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; + } + } + + + + + + /// + /// Boolean Difference of N implicit functions, A - Union(B1..BN) + /// Assumption is that both have surface at zero isocontour and + /// negative is inside. + /// + public class ImplicitNaryDifference3d : BoundedImplicitFunction3d + { + public BoundedImplicitFunction3d A; + public List 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(); + } + } + + + + + /// + /// 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. + /// + 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; + } + } + + + + /// + /// 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. + /// + 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; + } + } + + + + + /// + /// 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. + /// + 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; + } + } + + + + + /// + /// Blend of two implicit surfaces. Assumes surface is at zero iscontour. + /// Uses Pasko blend from http://www.hyperfun.org/F-rep.pdf + /// + public class ImplicitBlend3d : BoundedImplicitFunction3d + { + public BoundedImplicitFunction3d A; + public BoundedImplicitFunction3d B; + + + /// Weight on implicit A + public double WeightA { + get { return weightA; } + set { weightA = MathUtil.Clamp(value, 0.00001, 100000); } + } + double weightA = 0.01; + + /// Weight on implicit B + public double WeightB { + get { return weightB; } + set { weightB = MathUtil.Clamp(value, 0.00001, 100000); } + } + double weightB = 0.01; + + /// Blending power + 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 + */ + + + + /// + /// 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) + /// + 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 + + + + + /// + /// sum-blend + /// + 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; } } + + /// + /// Ricci blend + /// + 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; + } + } + + + + + /// + /// Boolean Union of N implicit functions, A OR B. + /// Assumption is that both have surface at zero isocontour and + /// negative is inside. + /// + public class SkeletalRicciNaryBlend3d : BoundedImplicitFunction3d + { + public List 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 +{ + /// + /// Sample implicit fields into a dense grid + /// + 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 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 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. /// 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); + } + + + /// + /// test if ray intersects box. + /// expandExtents allows you to scale box for hit-testing purposes. + /// + 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; + } + + + /// + /// 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) + /// + 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); + /// + /// test if ray intersects box. + /// expandExtents allows you to scale box for hit-testing purposes. + /// + 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; } + + + + /// + /// minimal intersection test, computes ray-t + /// + 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 /// public class OBJWriter : IMeshWriter { + // stream-opener. Override to write to something other than a file. + public Func OpenStreamF = (sFilename) => { + return File.Open(sFilename, FileMode.Create); + }; + public Action CloseStreamF = (stream) => { + stream.Dispose(); + }; + public string GroupNamePrefix = "mmGroup"; // default, compatible w/ meshmixer public Func GroupNameF = null; // use this to replace standard group names w/ your own @@ -57,7 +65,7 @@ public IOWriteResult Write(TextWriter writer, List 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 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 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 { + /// + /// 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 colors = Builder.Metadata[0][STLReader.PerTriAttribMetadataName] as DVector; + /// (for DMesh3Builder, which is the only builder that supports Metadata) + /// 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...) } + + /// + /// Which algorithm is used to try to reconstruct mesh topology from STL triangle soup + /// public Strategy RebuildStrategy = Strategy.AutoBestResult; + /// + /// Vertices within this distance are considered "the same" by welding strategies. + /// public double WeldTolerance = MathUtil.ZeroTolerancef; - // connect to this to get warning messages + /// + /// 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 + /// + public bool WantPerTriAttribs = false; + + /// + /// name argument passed to IMeshBuilder.AppendMetaData + /// + public static string PerTriAttribMetadataName = "tri_attrib"; + + + /// connect to this event to get warning messages public event ParsingMessagesHandler warningEvent; @@ -33,12 +73,21 @@ public enum Strategy + /// ReadOptions.CustomFlags flag for configuring .RebuildStrategy public const string StrategyFlag = "-stl-weld-strategy"; + + /// ReadOptions.CustomFlags flag for configuring .WantPerTriAttribs + 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 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 tri_attribs = new DVector(); + 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 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 polygons1, string color1, float width1, + List 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 { + /// + /// 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 + /// + /// public class StandardMeshWriter : IDisposable { + /// /// 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; + + /// + /// 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) + /// + public Func OpenStreamF = (sFilename) => { + return File.Open(sFilename, FileMode.Create); + }; + + /// + /// called on Streams returned by OpenStreamF when we are done with them. + /// + public Action CloseStreamF = (stream) => { + stream.Close(); + stream.Dispose(); + }; + + + + public void Dispose() { } @@ -45,7 +78,6 @@ public IOWriteResult Write(string sFilename, List vMeshes, WriteOptio { Func, 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 vMeshes, WriteOptio IOWriteResult Write_OBJ(string sFilename, List 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 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 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 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); } + } + + + + /// + /// 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) + /// + public class SimpleStore + { + // only ever append to this list! + public List Meshes = new List(); + public List Points = new List(); + public List Strings = new List(); + public List> IntLists = new List>(); + + 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) + IntLists.Add(o as List); + else if (o is IEnumerable) + IntLists.Add(new List(o as IEnumerable)); + 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 l = new List(); 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); } + /// + /// init box [0,size] x [0,size] x [0,size] + /// public AxisAlignedBox3d(double fCubeSize) { Min = new Vector3d(0, 0, 0); Max = new Vector3d(fCubeSize, fCubeSize, fCubeSize); } + /// + /// Init box [0,width] x [0,height] x [0,depth] + /// 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); } + /// + /// Returns point on face/edge/corner. For each coord value neg==min, 0==center, pos==max + /// + 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) } + /// + /// Returns point on face/edge/corner. For each coord value neg==min, 0==center, pos==max + /// + 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) } + /// + /// Clamp v to grid bounds [min, max] + /// + 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)); + } + + /// + /// clamp v to grid bounds [min,max) + /// + 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 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 TransformF) @@ -43,7 +57,39 @@ public static AxisAlignedBox3d Bounds(ref AxisAlignedBox3d boxIn, Func(IEnumerable values, Func PositionF) + public static AxisAlignedBox3d Bounds(IEnumerable positions) + { + AxisAlignedBox3d box = AxisAlignedBox3d.Empty; + foreach (Vector3d v in positions) + box.Contain(v); + return box; + } + public static AxisAlignedBox3f Bounds(IEnumerable positions) + { + AxisAlignedBox3f box = AxisAlignedBox3f.Empty; + foreach (Vector3f v in positions) + box.Contain(v); + return box; + } + + + public static AxisAlignedBox2d Bounds(IEnumerable positions) + { + AxisAlignedBox2d box = AxisAlignedBox2d.Empty; + foreach (Vector2d v in positions) + box.Contain(v); + return box; + } + public static AxisAlignedBox2f Bounds(IEnumerable positions) + { + AxisAlignedBox2f box = AxisAlignedBox2f.Empty; + foreach (Vector2f v in positions) + box.Contain(v); + return box; + } + + + public static AxisAlignedBox3d Bounds(IEnumerable values, Func PositionF) { AxisAlignedBox3d box = AxisAlignedBox3d.Empty; foreach ( T t in values ) @@ -59,6 +105,18 @@ public static AxisAlignedBox3f Bounds(IEnumerable values, Func + /// compute axis-aligned bounds of set of points after transforming + /// + public static AxisAlignedBox3d Bounds(IEnumerable values, TransformSequence xform) + { + AxisAlignedBox3d box = AxisAlignedBox3d.Empty; + foreach (Vector3d v in values) + box.Contain(xform.TransformP(v)); + return box; + } + + /// /// compute axis-aligned bounds of set of points after transforming into frame f /// 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; } + + + + /// + /// Returns distance to box, or 0 if point is inside box. + /// Ported from WildMagic5 Wm5DistPoint3Box3.cpp + /// + 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; + } + + + + /// + /// Returns distance to box, or 0 if point is inside box. + /// Ported from WildMagic5 Wm5DistPoint3Box3.cpp + /// + 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 +{ + + + /// + /// Formulas for triangle winding number approximation + /// + public static class FastTriWinding + { + /// + /// 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 + /// + public static void ComputeCoeffs(DMesh3 mesh, IEnumerable 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)); + } + } + + + /// + /// Evaluate first-order FWN approximation at point q, relative to center c + /// + 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)); + } + + + /// + /// Evaluate second-order FWN approximation at point q, relative to center c + /// + 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; + } + } + + + + + /// + /// Formulas for point-set winding number approximation + /// + public static class FastPointWinding + { + /// + /// 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 + /// + public static void ComputeCoeffs( + IPointSet pointSet, IEnumerable 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)); + } + } + + + /// + /// Evaluate first-order FWN approximation at point q, relative to center c + /// + 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)); + } + + + + /// + /// Evaluate second-order FWN approximation at point q, relative to center c + /// + 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); } + /// + /// 3D projection of point p onto frame-axis plane orthogonal to normal axis + /// 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); } + /// + /// map from 2D coordinates in frame-axes plane perpendicular to normal axis, to 3D + /// [TODO] check that mapping preserves orientation? + /// 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) + + /// + /// Project p onto plane axes + /// [TODO] check that mapping preserves orientation? + /// + 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); + } + /// distance from p to frame-axes-plane perpendicular to normal axis public float DistanceToPlane(Vector3f p, int nNormal) { return Math.Abs((p - origin).Dot(GetAxis(nNormal))); } + /// signed distance from p to frame-axes-plane perpendicular to normal axis public float DistanceToPlaneSigned(Vector3f p, int nNormal) { return (p - origin).Dot(GetAxis(nNormal)); @@ -226,116 +252,162 @@ public float DistanceToPlaneSigned(Vector3f p, int nNormal) /// Map point *into* local coordinates of Frame - 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); } /// Map point *into* local coordinates of Frame - 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); + } + /// Map point *into* local coordinates of Frame + public Vector3d ToFrameP(Vector3d v) { + v.x -= origin.x; v.y -= origin.y; v.z -= origin.z; + return rotation.InverseMultiply(ref v); + } + /// Map point *into* local coordinates of Frame + 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); } /// Map point *from* local frame coordinates into "world" coordinates - public Vector3f FromFrameP(Vector3f v) - { + public Vector3f FromFrameP(Vector3f v) { return this.rotation * v + this.origin; } /// Map point *from* local frame coordinates into "world" coordinates - public Vector3d FromFrameP(Vector3d v) - { + public Vector3f FromFrameP(ref Vector3f v) { + return this.rotation * v + this.origin; + } + /// Map point *from* local frame coordinates into "world" coordinates + public Vector3d FromFrameP(Vector3d v) { + return this.rotation * v + this.origin; + } + /// Map point *from* local frame coordinates into "world" coordinates + public Vector3d FromFrameP(ref Vector3d v) { return this.rotation * v + this.origin; } /// Map vector *into* local coordinates of Frame - public Vector3f ToFrameV(Vector3f v) - { - return Quaternionf.Inverse(this.rotation) * v; + public Vector3f ToFrameV(Vector3f v) { + return rotation.InverseMultiply(ref v); } /// Map vector *into* local coordinates of Frame - public Vector3d ToFrameV(Vector3d v) - { - return Quaternionf.Inverse(this.rotation) * v; + public Vector3f ToFrameV(ref Vector3f v) { + return rotation.InverseMultiply(ref v); + } + /// Map vector *into* local coordinates of Frame + public Vector3d ToFrameV(Vector3d v) { + return rotation.InverseMultiply(ref v); + } + /// Map vector *into* local coordinates of Frame + public Vector3d ToFrameV(ref Vector3d v) { + return rotation.InverseMultiply(ref v); } /// Map vector *from* local frame coordinates into "world" coordinates - public Vector3f FromFrameV(Vector3f v) - { + public Vector3f FromFrameV(Vector3f v) { return this.rotation * v; } /// Map vector *from* local frame coordinates into "world" coordinates - public Vector3d FromFrameV(Vector3d v) - { + public Vector3f FromFrameV(ref Vector3f v) { + return this.rotation * v; + } + /// Map vector *from* local frame coordinates into "world" coordinates + public Vector3d FromFrameV(ref Vector3d v) { + return this.rotation * v; + } + /// Map vector *from* local frame coordinates into "world" coordinates + public Vector3d FromFrameV(Vector3d v) { return this.rotation * v; } /// Map quaternion *into* local coordinates of Frame - public Quaternionf ToFrame(Quaternionf q) - { + public Quaternionf ToFrame(Quaternionf q) { + return Quaternionf.Inverse(this.rotation) * q; + } + /// Map quaternion *into* local coordinates of Frame + public Quaternionf ToFrame(ref Quaternionf q) { return Quaternionf.Inverse(this.rotation) * q; } /// Map quaternion *from* local frame coordinates into "world" coordinates - public Quaternionf FromFrame(Quaternionf q) - { + public Quaternionf FromFrame(Quaternionf q) { + return this.rotation * q; + } + /// Map quaternion *from* local frame coordinates into "world" coordinates + public Quaternionf FromFrame(ref Quaternionf q) { return this.rotation * q; } /// Map ray *into* local coordinates of Frame - 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)); + } + /// Map ray *into* local coordinates of Frame + public Ray3f ToFrame(ref Ray3f r) { + return new Ray3f(ToFrameP(ref r.Origin), ToFrameV(ref r.Direction)); } /// Map ray *from* local frame coordinates into "world" coordinates - 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)); + } + /// Map ray *from* local frame coordinates into "world" coordinates + public Ray3f FromFrame(ref Ray3f r) { + return new Ray3f(FromFrameP(ref r.Origin), FromFrameV(ref r.Direction)); } + /// Map frame *into* local coordinates of Frame - 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)); + } + /// Map frame *into* local coordinates of Frame + public Frame3f ToFrame(ref Frame3f f) { + return new Frame3f(ToFrameP(ref f.origin), ToFrame(ref f.rotation)); } /// Map frame *from* local frame coordinates into "world" coordinates - 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)); + } + /// Map frame *from* local frame coordinates into "world" coordinates + 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); + /// Map box *into* local coordinates of Frame + 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); + /// Map box *from* local frame coordinates into "world" coordinates + 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); + /// Map box *into* local coordinates of Frame + 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); + /// Map box *from* local frame coordinates into "world" coordinates + 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) + /// + /// Interpolate between two frames - Lerp for origin, Slerp for rotation + /// + 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; } + /// + /// find sequence [a,b] in tri_verts (mod3) and return index of a, or InvalidID if not found + /// + 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 tri_array, int t return DMesh3.InvalidID; } + + /// + /// assuming a is in tri-verts, returns other two vertices, in correct order (or Index2i.Max if not found) + /// + 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); + } + + /// /// Filter out invalid entries in indices[] list. Will return indices itself if @@ -294,6 +350,50 @@ public static void Apply(int[] indices, IList map) indices[i] = map[indices[i]]; } + + + public static void TrianglesToVertices(DMesh3 mesh, IEnumerable triangles, HashSet 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 triangles, HashSet 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 triangles, HashSet 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 triangles, HashSet 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 edges, HashSet 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 edges, HashSet 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 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)); } + /// + /// clamp value f to interval [a,b] + /// + public double Clamp(double f) { + return (f < a) ? a : (f > b) ? b : f; + } + + /// + /// interpolate between a and b using value t in range [0,1] + /// + public double Interpolate(double t) { + return (1 - t) * a + (t) * b; + } + + /// + /// Convert value into (clamped) t value in range [0,1] + /// + 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 } + /// + /// aspect ratio of triangle + /// + 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) { } + /// + /// Iterate from 0 to (nMax-1) using prime-modulo, so we see every index once, but not in-order + /// + public static IEnumerable 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); } + /// + /// Construct outer-product of u*transpose(v) of u and v + /// result is that Mij = u_i * v_j + /// + 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, IEquatable { // 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) { } + + /// Inverse() * v + 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; + } + + + /// Inverse() * v + 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 Operations; @@ -54,6 +59,17 @@ public TransformSequence() Operations = new List(); } + public TransformSequence(TransformSequence copy) + { + Operations = new List(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) + }); + } /// @@ -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) + /// + /// Apply transforms to vector. Includes scaling. + /// + 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; + } + + + + + /// + /// Apply transforms to point + /// + public Vector3f TransformP(Vector3f p) { + return (Vector3f)TransformP((Vector3d)p); + } + + + /// + /// construct inverse transformation sequence + /// + 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 + /// + /// 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 + /// 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 + /// + /// 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 + /// 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. + /// + /// 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. + /// 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; } + + + /// + /// Returns two vectors perpendicular to n, as efficiently as possible. + /// Duff et all method, from https://graphics.pixar.com/library/OrthonormalB/paper.pdf + /// + 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, IEquatable + { + 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++; } + + /// + /// Timestamp is incremented any time any change is made to the mesh + /// public int Timestamp { get { return timestamp; } } + + /// + /// ShapeTimestamp is incremented any time any vertex position is changed or the mesh topology is modified + /// 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) + /// + /// compute internal angle at vertex i of triangle (where i is 0,1,2); + /// TODO can be more efficient here, probably... + /// + 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 + /// + /// Append new vertex at position, returns new vid + /// public int AppendVertex(Vector3d v) { return AppendVertex(new NewVertexInfo() { v = v, bHaveC = false, bHaveUV = false, bHaveN = false }); } - public int AppendVertex(NewVertexInfo info) + + /// + /// Append new vertex at position and other fields, returns new vid + /// + 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 + /// + /// copy vertex fromVID from existing source mesh, returns new vid + /// public int AppendVertex(DMesh3 from, int fromVID) { int bi = 3 * fromVID; @@ -1002,6 +1050,67 @@ public int AppendVertex(DMesh3 from, int fromVID) } + + /// + /// 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 + /// + 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) + /// + /// 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 + /// + 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 EdgeIndices() { } + /// + /// Enumerate ids of boundary edges + /// public IEnumerable BoundaryEdgeIndices() { foreach ( int eid in edges_refcount ) { if (edges[4 * eid + 3] == InvalidID) @@ -1164,12 +1346,19 @@ public IEnumerable BoundaryEdgeIndices() { } + /// + /// Enumerate vertices + /// public IEnumerable Vertices() { foreach (int vid in vertices_refcount) { int i = 3 * vid; yield return new Vector3d(vertices[i], vertices[i + 1], vertices[i + 2]); } } + + /// + /// Enumerate triangles + /// public IEnumerable Triangles() { foreach (int tid in triangles_refcount) { int i = 3 * tid; @@ -1177,7 +1366,9 @@ public IEnumerable Triangles() { } } - // return value is [v0,v1,t0,t1], where t1 will be InvalidID if this is a boundary edge + /// + /// Enumerage edges. return value is [v0,v1,t0,t1], where t1 will be InvalidID if this is a boundary edge + /// public IEnumerable Edges() { foreach (int eid in edges_refcount) { int i = 4 * eid; @@ -1188,21 +1379,30 @@ public IEnumerable Edges() { // queries - // linear search through edges of vA + /// + /// Find edgeid for edge [a,b] + /// 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); + /// + /// Find edgeid for edge [a,b] from triangle that contains the edge. + /// This is faster than FindEdge() because it is constant-time + /// + 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...) + /// + /// 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 + /// 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) } + /// + /// Find triangle made up of any permutation of vertices [a,b,c] + /// 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) + /// + /// Enumerate "other" vertices of edges connected to vertex (ie vertex one-ring) + /// public IEnumerable VtxVerticesItr(int vID) { if ( vertices_refcount.isValid(vID) ) { foreach ( int eid in vertex_edges.ValueItr(vID) ) @@ -1246,6 +1452,9 @@ public IEnumerable VtxVerticesItr(int vID) { } + /// + /// Enumerate edge ids connected to vertex (ie edge one-ring) + /// public IEnumerable 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) } /// + /// 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 /// @@ -1299,7 +1509,10 @@ public int VtxAllBoundaryEdges(int vID, int[] e) } - + /// + /// Get triangle one-ring at vertex. + /// bUseOrientation is more efficient but returns incorrect result if vertex is a bowtie + /// public MeshResult GetVtxTriangles(int vID, List vTriangles, bool bUseOrientation) { if (!IsVertex(vID)) @@ -1363,6 +1576,9 @@ public int GetVtxTriangleCount(int vID, bool bBruteForce = false) } + /// + /// iterate over triangle IDs of vertex one-ring + /// public IEnumerable VtxTrianglesItr(int vID) { if ( IsVertex(vID) ) { foreach (int eid in vertex_edges.ValueItr(vID)) { @@ -1379,9 +1595,10 @@ public IEnumerable 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) + /// + /// 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) { 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 } + /// + /// Fastest possible one-ring centroid. This is used inside many other algorithms + /// so it helps to have it be maximally efficient + /// + 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) { } + /// + /// Returns true if any edge of triangle is a boundary edge + /// 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 + /// + /// Returns true if the two triangles connected to edge have different group IDs + /// 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 + /// + /// returns true if vertex has more than one tri group in its tri nbrhood + /// 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 + /// + /// returns true if more than two group boundary edges meet at vertex (ie 3+ groups meet at this vertex) + /// public bool IsGroupJunctionVertex(int vID) { if (IsVertex(vID) == false) @@ -1615,7 +1867,7 @@ public bool IsGroupJunctionVertex(int vID) /// - /// 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 /// public bool GetVertexGroups(int vID, out Index4i groups) { @@ -1648,7 +1900,7 @@ public bool GetVertexGroups(int vID, out Index4i groups) /// - /// returns up to 4 group IDs at input vid. Returns false if > 4 encountered + /// returns all group IDs at vertex /// public bool GetAllVertexGroups(int vID, ref List groups) { @@ -1684,17 +1936,64 @@ public List 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 + /// + /// Computes bounding box of all vertices. + /// 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 + /// + /// cached bounding box, lazily re-computed on access if mesh has changed + /// 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 { + /// returns true if vertices, edges, and triangles are all "dense" (Count == MaxID) public bool IsCompact { get { return vertices_refcount.is_dense && edges_refcount.is_dense && triangles_refcount.is_dense; } } + + /// Returns true if vertex count == max vertex id public bool IsCompactV { get { return vertices_refcount.is_dense; } } + + /// returns true if triangle count == max triangle id public bool IsCompactT { get { return triangles_refcount.is_dense; } } + /// returns measure of compactness in range [0,1], where 1 is fully compacted + public double CompactMetric { + get { return ((double)VertexCount / (double)MaxVertexID + (double)TriangleCount / (double)MaxTriangleID) * 0.5; } + } + /// @@ -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 + /// + /// 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. + /// 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 +{ + /// + /// Mesh change for vertex deformations. Currently minimal support for initializing buffers. + /// AppendNewVertex() can be used to accumulate modified vertices and their initial positions. + /// + public class ModifyVerticesMeshChange + { + public DVector ModifiedV; + public DVector OldPositions, NewPositions; + public DVector OldNormals, NewNormals; + public DVector OldColors, NewColors; + public DVector OldUVs, NewUVs; + + public Action OnApplyF; + public Action 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(); + NewPositions = new DVector(); + OldPositions = new DVector(); + if (mesh.HasVertexNormals && (components & MeshComponents.VertexNormals) != 0) { + NewNormals = new DVector(); + OldNormals = new DVector(); + } + if (mesh.HasVertexColors && (components & MeshComponents.VertexColors) != 0) { + NewColors = new DVector(); + OldColors = new DVector(); + } + if (mesh.HasVertexUVs && (components & MeshComponents.VertexUVs) != 0) { + NewUVs = new DVector(); + OldUVs = new DVector(); + } + } + + } + + + + + + + + + /// + /// Mesh change for full-mesh vertex deformations - more efficient than ModifyVerticesMeshChange. + /// Note that this does not enforce that vertex count does not change! + /// + public class SetVerticesMeshChange + { + public DVector OldPositions, NewPositions; + public DVector OldNormals, NewNormals; + public DVector OldColors, NewColors; + public DVector OldUVs, NewUVs; + + public Action OnApplyF; + public Action 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); + } + } + + + + + + + + + + + + + /// + /// 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 + /// + public class RemoveTrianglesMeshChange + { + protected DVector RemovedV; + protected DVector Positions; + protected DVector Normals; + protected DVector Colors; + protected DVector UVs; + + protected DVector RemovedT; + protected DVector Triangles; + + public Action,IEnumerable> OnApplyF; + public Action, IEnumerable> OnRevertF; + + public RemoveTrianglesMeshChange() + { + } + + + public void InitializeFromApply(DMesh3 mesh, IEnumerable 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 remove_t) + { + initialize_buffers(mesh); + bool has_groups = mesh.HasTriangleGroups; + + HashSet triangles = new HashSet(remove_t); + HashSet vertices = new HashSet(); + IndexUtil.TrianglesToVertices(mesh, remove_t, vertices); + List save_v = new List(); + 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(); + Positions = new DVector(); + if (mesh.HasVertexNormals) + Normals = new DVector(); + if (mesh.HasVertexColors) + Colors = new DVector(); + if (mesh.HasVertexUVs) + UVs = new DVector(); + + RemovedT = new DVector(); + Triangles = new DVector(); + } + + } + + + + + + + + + /// + /// 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 + /// + public class AddTrianglesMeshChange + { + protected DVector AddedV; + protected DVector Positions; + protected DVector Normals; + protected DVector Colors; + protected DVector UVs; + + protected DVector AddedT; + protected DVector Triangles; + + public Action, IEnumerable> OnApplyF; + public Action, IEnumerable> OnRevertF; + + + public AddTrianglesMeshChange() + { + } + + + public void InitializeFromExisting(DMesh3 mesh, IEnumerable added_v, IEnumerable 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(); + Positions = new DVector(); + if (mesh.HasVertexNormals) + Normals = new DVector(); + if (mesh.HasVertexColors) + Colors = new DVector(); + if (mesh.HasVertexUVs) + UVs = new DVector(); + + AddedT = new DVector(); + Triangles = new DVector(); + } + + } + + + + +} 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 /// - 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 /// - 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) + /// + /// Split edge eab. + /// split_t defines position along edge, and is assumed to be based on order of vertices returned by GetEdgeV() + /// + 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 vertex_edges_list(int vid) { return new List( vertex_edges.ValueItr(vid) ); } - + List vertex_vertices_list(int vid) + { + List vnbrs = new List(); + 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 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 +{ + + /// + /// 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. + /// + public class DSubmesh3Set : IEnumerable + { + public DMesh3 Mesh; + + public IEnumerable TriangleSetKeys; + public Func> TriangleSetF; + + // outputs + + /// List of computed submeshes + public List Submeshes; + + /// Mapping from keys to submeshes + public Dictionary KeyToMesh; + + + /// + /// Construct submesh set from given keys and key-to-indices Func + /// + public DSubmesh3Set(DMesh3 mesh, IEnumerable keys, Func> indexSetsF) + { + Mesh = mesh; + TriangleSetKeys = keys; + TriangleSetF = indexSetsF; + + ComputeSubMeshes(); + } + + + /// + /// Construct submesh set for an already-computed MeshConnectedComponents instance + /// + public DSubmesh3Set(DMesh3 mesh, MeshConnectedComponents components) + { + Mesh = mesh; + + TriangleSetF = (idx) => { + return components.Components[(int)idx].Indices; + }; + List keys = new List(); + for (int k = 0; k < components.Count; ++k) + keys.Add(k); + TriangleSetKeys = keys; + + ComputeSubMeshes(); + } + + + public IEnumerator GetEnumerator() { + return Submeshes.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() { + return Submeshes.GetEnumerator(); + } + + + + virtual protected void ComputeSubMeshes() + { + Submeshes = new List(); + KeyToMesh = new Dictionary(); + + 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 edges) } + /// + /// construct EdgeLoop from a list of vertices of mesh + /// + public static EdgeLoop FromVertices(DMesh3 mesh, IList 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); + } + + + /// /// 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; + } + /// /// if this is a border edge-loop, we can check that it is oriented correctly, and @@ -173,15 +203,18 @@ public bool IsInternalLoop() /// - /// 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 /// - 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 edges) } + /// + /// construct EdgeSpan from a list of vertices of mesh + /// + public static EdgeSpan FromVertices(DMesh3 mesh, IList 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 FindTrianglesByGroup(IMesh mesh, int findGroupID) /// split input mesh into submeshes based on group ID /// **does not** separate disconnected components w/ same group ID /// - public static DMesh3[] SeparateMeshByGroups(DMesh3 mesh) + public static DMesh3[] SeparateMeshByGroups(DMesh3 mesh, out int[] groupIDs) { Dictionary> meshes = new Dictionary>(); foreach ( int tid in mesh.TriangleIndices() ) { @@ -167,18 +167,23 @@ public static DMesh3[] SeparateMeshByGroups(DMesh3 mesh) tris = new List(); 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 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 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 Centroids; + public DVector Normals; + public DVector Areas; + + public MeshTriInfoCache(DMesh3 mesh) + { + int NT = mesh.TriangleCount; + Centroids = new DVector(); Centroids.resize(NT); + Normals = new DVector(); Normals.resize(NT); + Areas = new DVector(); 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 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 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 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 frames, IList 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) + + + /// + /// 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. + /// + 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); + } + + } + + + + + + /// /// 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! /// - public virtual int[] StitchUnorderedEdges(List EdgePairs, int group_id = -1) + public virtual int[] StitchUnorderedEdges(List 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 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 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 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 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 EdgePairs, int group_id /// vertex ordering must reslut in appropriate orientation (which is...??) /// [TODO] check and fail on bad orientation /// - public virtual int[] StitchSpan(int[] vspan1, int[] vspan2, int group_id = -1) + public virtual int[] StitchSpan(IList vspan1, IList 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 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 selectorF, bool bRemoveIsolatedVerts) return bAllOK; } + public static bool RemoveTriangles(DMesh3 Mesh, IList triangles, bool bRemoveIsolatedVerts = true) { + MeshEditor editor = new MeshEditor(Mesh); + return editor.RemoveTriangles(triangles, bRemoveIsolatedVerts); + } + public static bool RemoveTriangles(DMesh3 Mesh, IEnumerable triangles, bool bRemoveIsolatedVerts = true) { + MeshEditor editor = new MeshEditor(Mesh); + return editor.RemoveTriangles(triangles, bRemoveIsolatedVerts); + } + /// + /// Remove 'loner' triangles that have no connected neighbours. + /// + 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); + } + + /// + /// 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! + /// + public static int RemoveFinTriangles(DMesh3 mesh, Func removeF = null, bool bRepeatToConvergence = true) + { + MeshEditor editor = new MeshEditor(mesh); + + int nRemoved = 0; + List to_remove = new List(); + 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 triangles, bool bFlipVtxNormals = + /// + /// separate triangle one-ring at vertex into connected components, and + /// then duplicate vertex once for each component + /// + public void DisconnectBowtie(int vid) + { + List> sets = new List>(); + foreach ( int tid in Mesh.VtxTrianglesItr(vid)) { + Index3i nbrs = Mesh.GetTriNeighbourTris(tid); + bool found = false; + foreach ( List 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 set = new List() { 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 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 l1, List l2) { + if (l1.Count == l2.Count) return 0; + return (l1.Count > l2.Count) ? -1 : 1; + } + + + + /// + /// Disconnect all bowtie vertices in mesh. Iterates because sometimes + /// disconnecting a bowtie creates new bowties (how??). + /// Returns number of remaining bowties after iterations. + /// + public int DisconnectAllBowties(int nMaxIters = 10) + { + List bowties = new List(MeshIterators.BowtieVertices(Mesh)); + int iter = 0; + while (bowties.Count > 0 && iter++ < nMaxIters) { + foreach (int vid in bowties) + DisconnectBowtie(vid); + bowties = new List(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 vertices, double radius, Colorf color) + { + TubeGenerator tubegen = new TubeGenerator() { + Vertices = new List(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) + + /// + /// Remove any unused vertices in mesh, ie vertices with no edges. + /// Returns number of removed vertices. + /// + 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(); + } + + + + + + + /// + /// Remove any connected components with volume < min_volume area lt; min_area + /// + 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 BoundaryEdges(DMesh3 mesh) } + public static IEnumerable 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 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( + /// + /// 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... + /// + public static Vector2d VolumeArea( DMesh3 mesh, IEnumerable triangles, + Func 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); + } + + + + /// + /// Compute area of one-ring of mesh vertex by summing triangle areas. + /// If bDisjoint = true, we multiple each triangle area by 1/3 + /// + 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 vertices) { @@ -200,7 +248,7 @@ public static AxisAlignedBox3d Bounds(DMesh3 mesh, Func 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 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 triangleIndices) } + /// + /// calculate extents of mesh along axes of frame, with optional transform + /// + public static AxisAlignedBox3d BoundsInFrame(DMesh3 mesh, Frame3f frame, Func 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; + } - + /// + /// Calculate extents of mesh along an axis, with optional transform + /// public static Interval1d ExtentsOnAxis(DMesh3 mesh, Vector3d axis, Func 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; + } + + + /// + /// Calculate extents of mesh along an axis, with optional transform + /// + public static Interval1d ExtentsOnAxis(IMesh mesh, Vector3d axis, Func 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 + /// Calculate the two most extreme vertices along an axis, with optional transform + /// + public static Interval1i ExtremeVertices(DMesh3 mesh, Vector3d axis, Func 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 VertexIndices() { return Mesh.EdgeIndices(); } + + /// + /// Timestamp is incremented any time any change is made to the mesh + /// + public int Timestamp { + get { return Mesh.Timestamp; } + } } @@ -75,6 +82,13 @@ public IEnumerable VertexIndices() { return Mesh.BoundaryEdgeIndices(); } + + /// + /// Timestamp is incremented any time any change is made to the mesh + /// + 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; + /// + /// If normals dot product is less than this, we consider it a normal flip. default = 0 + /// + 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) } + /// + /// Set this to be able to cancel running remesher + /// + public ProgressCancel Progress = null; + + /// + /// if this returns true, abort computation. + /// + 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); + } + } + /// /// 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); } - + ///Map mesh *into* local coordinates of Frame 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); } } } } + + /// Map mesh *from* local frame coordinates into "world" coordinates 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 + /// Apply TransformF to vertices and normals of mesh + /// + public static void PerVertexTransform(IDeformableMesh mesh, Func 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); + } + } + } + + + /// + /// Apply Transform to vertices and normals of mesh + /// + 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))); + } + } + } + + + /// /// Apply TransformF to subset of vertices of mesh /// @@ -295,5 +351,22 @@ public static void PerVertexTransform(IDeformableMesh mesh, IEnumerable ver } } + + /// + /// Apply TransformF to subset of mesh vertices defined by MapV[vertices] + /// + public static void PerVertexTransform(IDeformableMesh targetMesh, IDeformableMesh sourceMesh, int[] mapV, Func 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) { - + /// + /// computes opening angle between the two triangles connected to edge + /// 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) } + /// + /// computes sum of opening-angles in triangles around vid, minus 2pi. + /// This is zero on flat areas. + /// + 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; + } + + /// @@ -106,6 +131,91 @@ public static bool CheckIfCollapseCreatesFlip(DMesh3 mesh, int edgeID, Vector3d + /// + /// 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) + /// + 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); + } + + + + /// + /// For given edge, return it's triangles and the triangles that would + /// be created if it was flipped (used in edge-flip optimizers) + /// + 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); + } + + + /// + /// 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) + /// + 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 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 vTris = new List(), vTris2 = new List(); + List vTris = new List(); 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 DebugEdges = new List(); @@ -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 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 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(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 +{ + /// + /// 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 + /// + /// + public class RemesherPro : Remesher + { + + public bool UseFaceAlignedProjection = false; + public int FaceProjectionPassesPerIteration = 1; + + + + public RemesherPro(DMesh3 m) : base(m) + { + } + + + HashSet modified_edges; + SpinLock modified_edges_lock = new SpinLock(); + + + protected IEnumerable 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 SplitF = null; + + + protected override void OnEdgeSplit(int edgeID, int va, int vb, DMesh3.EdgeSplitInfo splitInfo) + { + if (SplitF != null) + SplitF(edgeID, va, vb, splitInfo.vNew); + } + + + + /// + /// Converge on remeshed result as quickly as possible + /// + 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(); + } + + + + + + + /// + /// 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). + /// + 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); + } + + + + + + + + + + + + /// + /// 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) + /// + public void ResetQueue() + { + if (modified_edges != null) { + modified_edges.Clear(); + modified_edges = null; + } + } + + List edges_buffer = new List(); + + + /// + /// This pass only does edge splits. Returns number of split edges. + /// Tracks previously-split + /// + 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 edgesItr = EdgesIterator(); + if (modified_edges == null) { + modified_edges = new HashSet(); + } 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 edgesItr = EdgesIterator(); + if (modified_edges == null) { + modified_edges = new HashSet(); + } 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 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 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(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 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(smooth_vertices(), project); + } else { + foreach (int vID in smooth_vertices()) + project(vID); + } + + ApplyVertexBuffer(bParallel); + //System.Console.WriteLine("Projection Pass: queue: {0}", modified_edges.Count); + } + + + + + /// + /// This computes projected position w/ proper constraints/etc. + /// Does not modify mesh. + /// + 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 vBufferVWeights = new DVector(); + + 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 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 stateStack = new List(); + + 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 Triangles; public DVector FaceGroups; + int timestamp = 0; + public SimpleMesh() { Initialize(); @@ -100,6 +102,17 @@ public MeshComponents Components { + /// + /// Timestamp is incremented any time any change is made to the mesh + /// + 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 + /// /// 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(); + 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(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 /// public class MarchingCubes { - // this is the function we will evaluate + /// + /// this is the function we will evaluate + /// public ImplicitFunction3d Implicit; - // mesh surface will be at this isovalue. Normally 0 unless you want - // offset surface or field is not a distance-field. + /// + /// mesh surface will be at this isovalue. Normally 0 unless you want + /// offset surface or field is not a distance-field. + /// 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 + /// 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 public AxisAlignedBox3d Bounds; - // Length of edges of cubes that are marching. - // currently, # of cells along axis = (int)(bounds_dimension / CellSize) + 1 + /// + /// Length of edges of cubes that are marching. + /// currently, # of cells along axis = (int)(bounds_dimension / CellSize) + 1 + /// 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) + /// + /// 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) + /// public bool ParallelCompute = true; + public enum RootfindingModes { SingleLerp, LerpSteps, Bisection } + + /// + /// Which rootfinding method will be used to converge on surface along edges + /// + public RootfindingModes RootMode = RootfindingModes.SingleLerp; + + /// + /// number of iterations of rootfinding method (ignored for SingleLerp) + /// + public int RootModeSteps = 5; + + + /// if this function returns true, we should abort calculation + public Func 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) /// - /// 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 /// 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 + { + /// + /// this is the function we will evaluate + /// + public ImplicitFunction3d Implicit; + + /// + /// mesh surface will be at this isovalue. Normally 0 unless you want + /// offset surface or field is not a distance-field. + /// + 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 + public AxisAlignedBox3d Bounds; + + /// + /// Length of edges of cubes that are marching. + /// currently, # of cells along axis = (int)(bounds_dimension / CellSize) + 1 + /// + 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) + /// + public bool ParallelCompute = true; + + public enum RootfindingModes { SingleLerp, LerpSteps, Bisection } + + /// + /// Which rootfinding method will be used to converge on surface along edges + /// + public RootfindingModes RootMode = RootfindingModes.SingleLerp; + + /// + /// number of iterations of rootfinding method (ignored for SingleLerp) + /// + public int RootModeSteps = 5; + + + /// if this function returns true, we should abort calculation + public Func 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; + } + + + + /// + /// Run MC algorithm and generate Output mesh + /// + 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(); + corner_values = new Dictionary(); + + if (ParallelCompute) { + generate_parallel(); + } else { + generate_basic(); + } + } + + + public void GenerateContinuation(IEnumerable 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(); + corner_values = new Dictionary(); + 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 edge_vertices = new Dictionary(); + 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 corner_values = new Dictionary(); + 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]); + } + } + + + + + + /// + /// compute 3D corner-positions and field values for cell at index + /// + 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; + + /// + /// processing z-slabs of cells in parallel + /// + 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; + } + + + + + /// + /// fully sequential version, no threading + /// + 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); + } + + } + } + } + + + + + /// + /// fully sequential version, no threading + /// + void generate_continuation(IEnumerable seeds) + { + GridCell cell = new GridCell(); + int[] vertlist = new int[12]; + + done_cells = new DenseGrid3i(CellDimensions.x, CellDimensions.y, CellDimensions.z, 0); + + List stack = new List(); + + 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; + } + } + } + } + } + } + + + + + /// + /// parallel seed evaluation + /// + void generate_continuation_parallel(IEnumerable 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 stack = new List(); + 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; + } + + + + + + + + + + + /// + /// find edge crossings and generate triangles for this cell + /// + 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); + } + + + + + /// + /// add vertex to mesh, with locking if we are computing in parallel + /// + 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; + } + + + + /// + /// add triangle to mesh, with locking if we are computing in parallel + /// + 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; + } + + + + /// + /// root-find the intersection along edge from f(p1)=valp1 to f(p2)=valp2 + /// + 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 +{ + /// + /// Create a mesh that contains a planar element for each point and normal + /// (currently only triangles) + /// + public class PointSplatsGenerator : MeshGenerator + { + public IEnumerable PointIndices; + public int PointIndicesCount = -1; // you can set this to avoid calling Count() on enumerable + + public Func PointF; // required + public Func 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; + } + + + + /// + /// shortcut utility + /// + public static DMesh3 Generate(IList indices, + Func PointF, Func 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 +{ + /// + /// Triangulate a 2D polygon-with-holes by inserting it's edges into a meshed rectangle + /// and then removing the triangles outside the polygon. + /// + 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; + } + + + + + /// + /// 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. + /// + 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 +{ + /// + /// 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. + /// + /// + public class AutoHoleFill + { + public DMesh3 Mesh; + + public double TargetEdgeLength = 2.5; + + public EdgeLoop FillLoop; + + /* + * Outputs + */ + + /// Final fill triangles. May include triangles outside initial fill loop, if ConstrainToHoleInterior=false + 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; + } + + + + + + /// + /// Here are reasons this isn't working: + /// 1) find_coplanar_span_sets does not actually limit to coplanar (see comments) + /// 2) + /// + /// + bool fill_planar_spans() + { + Dictionary> span_sets = find_coplanar_span_sets(Mesh, FillLoop); + + foreach ( var set in span_sets ) { + Vector3d normal = set.Key; + List spans = set.Value; + Vector3d pos = spans[0].GetVertex(0); + + if (spans.Count > 1) { + List> 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; + } + + + /// + /// 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... + /// + List> sort_planar_spans(List allspans, Vector3d normal) + { + List> result = new List>(); + Frame3f polyFrame = new Frame3f(Vector3d.Zero, normal); + + int N = allspans.Count; + + List plines = new List(); + 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 set = new List() { 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 span_set = new List(); + 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> find_coplanar_span_sets(DMesh3 mesh, EdgeLoop loop) + { + double dot_thresh = 0.999; + + Dictionary> span_sets = new Dictionary>(); + + 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 edges = new List() { 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 found; + if (span_sets.TryGetValue(span_n, out found) == false) + span_sets[span_n] = new List() { 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 /// - public static void RegionSmooth(DMesh3 mesh, IEnumerable triangles, int nConstrainLoops, + public static void RegionSmooth(DMesh3 mesh, IEnumerable triangles, + int nConstrainLoops, + int nIncludeExteriorRings, + bool bPreserveExteriorRings, double borderWeight = 10.0, double interiorWeight = 0.0) { + HashSet fixedVerts = new HashSet(); + 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 subFixedVerts = new HashSet(); + foreach (int base_vid in fixedVerts) + subFixedVerts.Add(submesh.MapVertexToSubmesh(base_vid)); - // now constrain borders + // constrain borders double w = borderWeight; - HashSet constrained = (region.Region.BaseBorderV.Count > 0) ? new HashSet() : null; - foreach (int base_vid in region.Region.BaseBorderV) { + HashSet constrained = (submesh.BaseBorderV.Count > 0) ? new HashSet() : 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 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 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 +{ + /// + /// Merge coincident edges. + /// + 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[] EquivSets = new List[Mesh.MaxEdgeID]; + HashSet remaining = new HashSet(); + 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 equiv = new List(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 Q = new DynamicPriorityQueue(); + 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 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 +{ + + /// + /// Given an input mesh, try to decompose it's connected components into + /// parts with some semantics - solids, open meshes, etc. + /// + public class MeshAssembly + { + public DMesh3 SourceMesh; + + // if true, each shell is a separate solid + public bool HasNoVoids = false; + + /* + * Outputs + */ + + public List ClosedSolids; + public List OpenMeshes; + + + public MeshAssembly(DMesh3 sourceMesh) + { + SourceMesh = sourceMesh; + + ClosedSolids = new List(); + OpenMeshes = new List(); + } + + + 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 solidComps = new List(); + + 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() { solidComps[0] }; + } + + + if (HasNoVoids) { + // each solid is a separate solid + ClosedSolids = process_solids_novoid(solidComps); + } else { + ClosedSolids = process_solids(solidComps); + } + + } + + + + List process_solids(List 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() { combinedSolid }; + } + + + + List process_solids_novoid(List 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 +{ + /// + /// 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...? + /// + /// + 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; + + + + + + /// + /// Set this to be able to cancel running remesher + /// + public ProgressCancel Progress = null; + + /// + /// if this returns true, abort computation. + /// + 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 targetVerts = new HashSet(cutTargetOp.CutVertices); + //HashSet toolVerts = new HashSet(cutToolOp.CutVertices); + + // tracking on-cut vertices is not working yet... + Util.gDevAssert(Target.IsClosed() && Tool.IsClosed()); + + HashSet targetVerts = new HashSet(MeshIterators.BoundaryVertices(cutTargetMesh)); + HashSet toolVerts = new HashSet(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 fromVerts, HashSet toVerts) + { + List missing = new List(); + 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 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 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 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() } + /// + /// 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) + /// 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 +{ + /// + /// 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 + /// + public class MeshInsertPolygon + { + public DMesh3 Mesh; + public GeneralPolygon2d Polygon; + + + public bool SimplifyInsertion = true; + + public MeshInsertUVPolyCurve OuterInsert; + public List HoleInserts; + public HashSet 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(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(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 +{ + /// + /// 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. + /// + /// + 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; + + + + /// + /// insert polygon in given frame + /// + public MeshInsertProjectedPolygon(DMesh3 mesh, Polygon2d poly, Frame3f frame, int seedTri) + { + Mesh = mesh; + Polygon = new Polygon2d(poly); + ProjectFrame = frame; + SeedTriangle = seedTri; + } + + /// + /// create Polygon by projecting polygon3 into frame + /// + 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 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 seed_verts = new List(); + 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 interiorT = new List(); + 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 + /// /// 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 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 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 OnCurveVerts = new HashSet(); // original vertices that were epsilon-coincident w/ curve vertices + insert_corners(OnCurveVerts); // [RMS] not using this? //HashSet corner_v = new HashSet(CurveVertices); @@ -199,6 +321,14 @@ public virtual bool Apply() HashSet ZeroVertices = new HashSet(); OnCutEdges = new HashSet(); + HashSet NewEdges = new HashSet(); + HashSet NewCutVertices = new HashSet(); + sbyte[] signs = new sbyte[2 * Mesh.MaxVertexID + 2*Curve.VertexCount]; + + HashSet segTriangles = new HashSet(); + HashSet segVertices = new HashSet(); + HashSet segEdges = new HashSet(); + // 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 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 NewEdges = new HashSet(); - HashSet NewCutVertices = new HashSet(); + 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 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 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 /// public Func VertexValueF = null; + /// + /// If true, then we internally precompute vertex values. + /// ***THIS COMPUTATION IS MULTI-THREADED*** + /// + public bool PrecomputeVertexValues = false; + + + public enum RootfindingModes { SingleLerp, LerpSteps, Bisection } + + /// + /// Which rootfinding method will be used to converge on surface along edges + /// + public RootfindingModes RootMode = RootfindingModes.SingleLerp; + + /// + /// number of iterations of rootfinding method (ignored for SingleLerp) + /// + public int RootModeSteps = 5; + + public DGraph3 Graph = null; public enum TriangleCase @@ -26,14 +46,23 @@ public enum TriangleCase public bool WantGraphEdgeInfo = false; + /// + /// 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] + /// public struct GraphEdgeInfo { public TriangleCase caseType; public int mesh_tri; public Index2i mesh_edges; + public Index2i order; } public DVector GraphEdges = null; + // locations of edge crossings that we found during rootfinding. key is edge id. + Dictionary EdgeLocations = new Dictionary(); + public MeshIsoCurves(DMesh3 mesh, Func valueF) { @@ -43,7 +72,7 @@ public MeshIsoCurves(DMesh3 mesh, Func valueF) public void Compute() { - compute_full(Mesh.TriangleIndices()); + compute_full(Mesh.TriangleIndices(), true); } public void Compute(IEnumerable Triangles) { @@ -57,7 +86,7 @@ public void Compute(IEnumerable Triangles) Dictionary Vertices; - protected void compute_full(IEnumerable Triangles) + protected void compute_full(IEnumerable Triangles, bool bIsFullMeshHint = false) { Graph = new DGraph3(); if (WantGraphEdgeInfo) @@ -65,6 +94,24 @@ protected void compute_full(IEnumerable Triangles) Vertices = new Dictionary(); + + // multithreaded precomputation of per-vertex values + double[] vertex_values = null; + if (PrecomputeVertexValues) { + vertex_values = new double[Mesh.MaxVertexID]; + IEnumerable 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 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 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 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; } + /// + /// 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?? + /// + 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); + } + } + + + + + /// + /// 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 + /// + 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 +{ + /// + /// + /// + /// TODO: + /// - track descendant triangles of each input face + /// - for missing segments, can resolve in 2D in plane of face + /// + /// + /// + public class MeshMeshCut + { + public DMesh3 Target; + public DMesh3 CutMesh; + + PointHashGrid3d 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 CutVertices; + + + public void Compute() + { + double cellSize = Target.CachedBounds.MaxDim / 64; + PointHash = new PointHashGrid3d(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 removeT = new SafeListBuilder(); + 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(); + 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 gidmap = new Dictionary(); + 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 SegVertices; + Dictionary VIDToSegVtxMap; + + + // segment vertices in each triangle that we still have to insert + Dictionary> FaceVertices; + + // segment vertices in each edge that we still have to insert + Dictionary> 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> SubFaces; + Dictionary ParentFaces; + + HashSet 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(); + EdgeVertices = new Dictionary>(); + FaceVertices = new Dictionary>(); + SubFaces = new Dictionary>(); + ParentFaces = new Dictionary(); + SegmentInsertVertices = new HashSet(); + VIDToSegVtxMap = new Dictionary(); + } + + + + /// + /// 1) Find intersection segments + /// 2) sort onto existing input mesh vtx/edge/face + /// + void find_segments() + { + Dictionary SegVtxMap = new Dictionary(); + + // 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); + } + + } + + } + + + + + /// + /// For each on-face vtx, we poke the face, and re-sort + /// the remaining vertices on that face onto new faces/edges + /// + void insert_face_vertices() + { + while ( FaceVertices.Count > 0 ) { + var pair = FaceVertices.First(); + int tid = pair.Key; + List 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); + } + } + + + + /// + /// figure out which vtx/edge/face the input vtx is on + /// + 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; + } + + + + + /// + /// for each on-edge vtx, we split the edge and then + /// re-sort any of the vertices on that edge onto new edges + /// + void insert_edge_vertices() + { + while (EdgeVertices.Count > 0) { + var pair = EdgeVertices.First(); + int eid = pair.Key; + List 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); + + } + } + + + + /// + /// figure out which vtx/edge the input vtx is on + /// + 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?"); + } + + + + + + + + /// + /// Make sure that all intersection segments are represented by + /// a connected chain of edges. + /// + 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 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 l; + if (EdgeVertices.TryGetValue(eid, out l)) { + l.Add(vtx); + } else { + l = new List() { vtx }; + EdgeVertices[eid] = l; + } + } + + void add_face_vtx(int tid, SegmentVtx vtx) + { + List l; + if (FaceVertices.TryGetValue(tid, out l)) { + l.Add(vtx); + } else { + l = new List() { vtx }; + FaceVertices[tid] = l; + } + } + + + + void add_poke_subfaces(int tid, ref DMesh3.PokeTriangleInfo pokeInfo) + { + int parent = get_parent(tid); + HashSet 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 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 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 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 get_subfaces(int parent) + { + HashSet subfaces; + if (SubFaces.TryGetValue(parent, out subfaces) == false) { + subfaces = new HashSet(); + SubFaces[parent] = subfaces; + } + return subfaces; + } + void add_subface(HashSet subfaces, int parent, int tid) + { + subfaces.Add(tid); + ParentFaces[tid] = parent; + } + List get_all_baseface_tris(int base_tid) + { + List faces = new List(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); + + } + + + + /// + /// find existing vertex at point, if it exists + /// + protected int find_existing_vertex(Vector3d pt) + { + return find_nearest_vertex(pt, VertexSnapTol); + } + /// + /// find closest vertex, within searchRadius + /// + protected int find_nearest_vertex(Vector3d pt, double searchRadius, int ignore_vid = -1) + { + KeyValuePair 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 triangles; + public double outFacing; + public double inFacing; + } + List Components = new List(); + + + + + // 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(); + + HashSet remaining = new HashSet(Mesh.TriangleIndices()); + List stack = new List(); + while (remaining.Count > 0) { + Component c = new Component(); + c.triangles = new List(); + + 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 tris = new HashSet(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 +{ + /// + /// This class sorts a set of mesh components. + /// + public class MeshSpatialSort + { + // ComponentMesh is a wrapper around input meshes + public List Components; + + // a MeshSolid is an "Outer" mesh and a set of "Cavity" meshes + // (the cavity list includes contained open meshes, though) + public List Solids; + + public bool AllowOpenContainers = false; + public double FastWindingIso = 0.5f; + + + public MeshSpatialSort() + { + Components = new List(); + } + + + 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 InsideOf = new List(); + + // meshes that are inside of this one + public List InsideSet = new List(); + + 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 Cavities = new List(); + } + + + + + + + + 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> ContainSets = new Dictionary>(); + Dictionary> ContainedParents = new Dictionary>(); + 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()); + ContainSets[i].Add(j); + bIsContained[j] = true; + if (ContainedParents.ContainsKey(j) == false) + ContainedParents.Add(j, new List()); + ContainedParents[j].Add(i); + + dataLock.Exit(); + } + + } + }); + + + List solids = new List(); + HashSet used = new HashSet(); + + Dictionary CompToOuterIndex = new Dictionary(); + + List ParentsToProcess = new List(); + + + // 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 ContainersToRemove = new List(); + + // 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 children = ContainSets[i]; + foreach (int childj in children) { + ComponentMesh childComp = comps[childj]; + Util.gDevAssert(used.Contains(childComp) == false); + + // skip multiply-contained children + List 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 keys = new List(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 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 +{ + /// + /// 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? + /// + 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 spans = new List(); + + + 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); + } + + + /// + /// specify subset of vertices that have known correspondences. + /// + 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 pairs = new List(); + 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 new_spans = new List(); + 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; + } + + + + /// + /// this just does back-and-forth zippering, of as many quads as possible, and + /// then a triangle-fan to finish whichever side is longer + /// + 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 +{ + /// + /// Extract topological information about the mesh based on identifying + /// semantic edges/vertices/etc + /// + /// WIP + /// + /// + 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 BoundaryEdges; + public HashSet CreaseEdges; + public HashSet AllEdges; + + public HashSet AllVertices; + public HashSet JunctionVertices; + + public EdgeLoop[] Loops; + public EdgeSpan[] Spans; + + int topo_timestamp = -1; + public bool IgnoreTimestamp = false; + + + /// + /// Compute the topology elements + /// + public void Compute() + { + validate_topology(); + } + + + /// + /// add topological edges/vertices as constraints for remeshing + /// + 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(); + BoundaryEdges = new HashSet(); + + 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(CreaseEdges); ; + foreach ( int eid in BoundaryEdges ) + AllEdges.Add(eid); + + AllVertices = new HashSet(); + 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 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 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(); + 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 +{ + + /// + /// 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 + /// + 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; + + /// + /// Cut mesh with plane. Assumption is that plane normal is Z value. + /// + 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 +{ + /// + /// Construct a "minimal" fill surface for the hole. This surface + /// is often quasi-developable, reconstructs sharp edges, etc. + /// There are various options. + /// + 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 + */ + + /// Final fill vertices (should be empty?) + public int[] FillVertices; + + /// Final fill triangles + public int[] FillTriangles; + + + public MinimalHoleFill(DMesh3 mesh, EdgeLoop fillLoop) + { + this.Mesh = mesh; + this.FillLoop = fillLoop; + } + + + RegionOperator regionop; + DMesh3 fillmesh; + HashSet boundaryv; + Dictionary 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 intial_fill_tris = new HashSet(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(MeshIterators.BoundaryEdgeVertices(fillmesh)); + exterior_angle_sums = new Dictionary(); + 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 remaining_edges = new HashSet(fillmesh.EdgeIndices()); + HashSet updated_edges = new HashSet(); + + 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(fillmesh.EdgeIndices()); + updated_edges = new HashSet(); + + /* + * 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(fillmesh.EdgeIndices()); + updated_edges = new HashSet(); + 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 interiorv = new HashSet(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 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 { + /// + /// 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. + /// + /// public class PlanarHoleFiller { public DMesh3 Mesh; @@ -18,6 +36,20 @@ public class PlanarHoleFiller /// public double FillTargetEdgeLen = double.MaxValue; + /// + /// 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... + /// + 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 loops) } + /// + /// Compute the fill mesh and append it. + /// This returns false if anything went wrong. + /// The Error Feedback properties (.OutputHasCracks, etc) will provide more info. + /// 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 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() + + /// + /// 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 + /// + List 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 errorV = new List(); + + 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; + } + + + + + /// + /// verifies that there is a 1-1 correspondence between the fill and target loops. + /// If so, adds to mergeMapV and returns true; + /// + 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 +{ + /// + /// 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? + /// + /// + /// + public class PlanarSpansFiller + { + public DMesh3 Mesh; + + public Vector3d PlaneOrigin; + public Vector3d PlaneNormal; + + /// + /// fill mesh will be tessellated to this length, set to + /// double.MaxValue to use zero-length tessellation + /// + public double FillTargetEdgeLen = double.MaxValue; + + /// + /// 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... + /// + public bool MergeFillBoundary = false; + + // these will be computed if you don't set them + Vector3d PlaneX, PlaneY; + + + List FillSpans; + Polygon2d SpansPoly; + AxisAlignedBox2d Bounds; + + public PlanarSpansFiller(DMesh3 mesh, IList spans) + { + Mesh = mesh; + FillSpans = new List(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 ElemToLoopMap = new Dictionary(); + + // 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 removeT = new List(); + 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 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 regionTris) + public RegionOperator(DMesh3 mesh, IEnumerable regionTris, Action 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 { } + /// + /// find base-mesh interior vertices of region (ie does not include region boundary vertices) + /// + public HashSet CurrentBaseInteriorVertices() + { + HashSet verts = new HashSet(); + 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 +{ + /// + /// Remove duplicate triangles. + /// + 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 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 +{ + /// + /// 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. + /// + public class RemoveOccludedTriangles + { + public DMesh3 Mesh; + public DMeshAABBTree3 Spatial; + + // indices of removed triangles. List will be empty if nothing removed + public List 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; + + + /// + /// Set this to be able to cancel running remesher + /// + public ProgressCancel Progress = null; + + /// + /// if this returns true, abort computation. + /// + 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 ray_dirs = null; int NR = 0; + if (InsideMode == CalculationMode.SimpleOcclusionTest) { + ray_dirs = new List(); + 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 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(); + 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 +{ + /// + /// 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. + /// + 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 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; + + /// If this is true, we don't modify any triangles outside hole (often results in lower-quality fill) + 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 BorderHintTris = null; + + + /* + * Outputs + */ + + /// Final fill triangles. May include triangles outside initial fill loop, if ConstrainToHoleInterior=false + public int[] FillTriangles; + + /// Final fill vertices + 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 hint_edges = new HashSet(); + 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 public List 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 EdgeFilterF = null; + // if we throw an exception, we will try to set FailureBowties, so that client + // can try repairing these vertices + public List FailureBowties = null; + public MeshBoundaryLoops(DMesh3 mesh, bool bAutoCompute = true) { @@ -145,6 +150,10 @@ public bool Compute() Loops = new List(); Spans = new List(); + // 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 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 Loops; + public List 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 extract_subloops(List loopV, List loopE, List 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 loopV, List loopE, List bowties ) { - List subs = new List(); + Subloops subs = new Subloops(); + subs.Loops = new List(); subs.Spans = new List(); // figure out which bowties we saw are actually duplicated in loopV List dupes = new List(); @@ -415,7 +439,7 @@ List extract_subloops(List loopV, List loopE, List 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 extract_subloops(List loopV, List loopE, List 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 verts = new List(); + 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 extract_subloops(List loopV, List loopE, List 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 extract_subloops(List loopV, List loopE, List 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; } + + + + /// + /// Utility function that finds set of triangles connected to tSeed. Does not use MeshConnectedComponents class. + /// + public static HashSet FindConnectedT(DMesh3 mesh, int tSeed) + { + HashSet found = new HashSet(); + found.Add(tSeed); + List queue = new List(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 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 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 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 temp_hash = new HashSet(); 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); } + /// + /// 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. + /// + public bool RemoveBowties() { + return remove_bowties(null); + } + public bool remove_bowties(HashSet tempHash) + { + bool bModified = false; + bool done = false; + HashSet vertices = (tempHash == null) ? new HashSet() : 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 ExtractSelected() + { + var ret = Selected; + Selected = new HashSet(); + return ret; + } + + public IEnumerator GetEnumerator() { return Selected.GetEnumerator(); } @@ -110,6 +118,84 @@ public void SelectTriangleVertices(MeshFaceSelection triangles) } + /// + /// 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). + /// + public void SelectInteriorVertices(MeshFaceSelection triangles) + { + HashSet borderv = new HashSet(); + 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); + } + } + } + + + + + /// + /// Select set of boundary vertices connected to vSeed. + /// + public void SelectConnectedBoundaryV(int vSeed) + { + if ( ! Mesh.IsBoundaryVertex(vSeed)) + throw new Exception("MeshConnectedComponents.FindConnectedBoundaryV: vSeed is not a boundary vertex"); + + HashSet found = (Selected.Count == 0) ? Selected : new HashSet(); + found.Add(vSeed); + List 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 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 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 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 } /// - /// 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. /// - 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); + } + + + + /// + /// find distance between two triangles, with optional + /// transform on second triangle + /// + public static DistTriangle3Triangle3 TriangleTriangleDistance(DMesh3 mesh1, int ti, DMesh3 mesh2, int tj, Func 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; } + /// /// convenience function to construct a IntrRay3Triangle3 object for a mesh triangle /// @@ -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. /// - 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. /// - 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) } - + /// + /// Get barycentric coords of point in triangle + /// + 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); + } /// @@ -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 + /// + /// MeshProjectionTarget provides an IProjectionTarget interface to a mesh + spatial data structure. + /// Use to project points to mesh surface. + /// + 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; } /// @@ -40,10 +60,73 @@ public static MeshProjectionTarget Auto(DMesh3 mesh, bool bForceCopy = true) else return new MeshProjectionTarget(mesh); } + + + /// + /// Automatically construct fastest projection target for region of mesh + /// + public static MeshProjectionTarget Auto(DMesh3 mesh, IEnumerable 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); + } + } + + + + + /// + /// Extension of MeshProjectionTarget that allows the target to have a transformation + /// relative to it's internal space. Call SetTransform(), or initialize the transforms yourself + /// + 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 +{ + + /// + /// 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? + /// + /// + public class DCurve3BoxTree + { + public DCurve3 Curve; + + Box3d[] boxes; + int layers; + List 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); + } + } + + + + /// + /// 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... ? + /// + 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); + } + } + + + + + + + + /// + /// 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... ? + /// + 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(); + + // 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() { 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) /// /// 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 + /// + /// If non-null, only triangle IDs that pass this filter (ie filter is true) are considered + /// public Func TriangleFilterF = null; @@ -103,6 +106,8 @@ public void Build(BuildStrategy eStrategy = BuildStrategy.TopDownMidpoint, } + public bool IsValid { get { return mesh_timestamp == mesh.ShapeTimestamp; } } + /// /// 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; } + /// + /// Find the triangle closest to p, and distance to it, within distance fMaxDist, or return InvalidID + /// Use MeshQueries.TriangleDistance() to get more information + /// + 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 + /// + /// Find the vertex closest to p, within distance fMaxDist, or return InvalidID + /// + 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); + } + } + + } + } + } + + + + + /// /// Does this ISpatial implementation support ray-triangle intersection? (yes) /// @@ -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 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(); result.Segments = new List(); - 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 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 triangles) + + + + /* + * Fast Mesh Winding Number computation + */ + + /// + /// FWN beta parameter - is 2.0 in paper + /// + public double FWNBeta = 2.0; + + /// + /// FWN approximation order. can be 1 or 2. 2 is more accurate, obviously. + /// + public int FWNApproxOrder = 2; + + + /// + /// Fast approximation of winding number using far-field approximations + /// + 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 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(); + HashSet 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 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 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(); + 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 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); + } + + + + + + + + + + /// /// Total sum of volumes of all boxes in the tree. Mainly useful to evaluate tree quality. /// @@ -1219,6 +1531,13 @@ public double TotalExtentSum() } + /// + /// Root bounding box of tree (note: tree must be generated by calling a query function first!) + /// + 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 box_to_index; - DVector box_centers; - DVector box_extents; + protected DVector box_to_index; + protected DVector box_centers; + protected DVector 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 index_list; + protected DVector 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 +{ + + /// + /// 2D dense grid of floating-point scalar values. + /// + 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 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 Indices() + { + for (int y = 0; y < nj; ++y) { + for (int x = 0; x < ni; ++x) + yield return new Vector2i(x, y); + } + } + + + public IEnumerable 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); + } + } + + + } + + + + + + + /// + /// 2D dense grid of integers. + /// + 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 Indices() + { + for (int y = 0; y < nj; ++y) { + for (int x = 0; x < ni; ++x) + yield return new Vector2i(x, y); + } + } + + + public IEnumerable 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 { - + /// + /// 3D dense grid of floating-point scalar values. + /// 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 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 Indices() { @@ -81,26 +180,53 @@ public IEnumerable 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); + } } - + /// + /// 3D dense grid of integers. + /// 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; + } + + + /// + /// convert to binary bitmap + /// + 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 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 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 +{ + /// + /// 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. + /// + public class EditMeshSpatial : ISpatial + { + public DMesh3 SourceMesh; + public DMeshAABBTree3 SourceSpatial; + public DMesh3 EditMesh; + + HashSet RemovedT = new HashSet(); + HashSet AddedT = new HashSet(); + + 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 +{ + + /// + /// 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? + /// + /// + public class MeshScalarSamplingGrid + { + public DMesh3 Mesh; + public Func 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; + + /// if this function returns true, we should abort calculation + public Func 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 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); } + } + + /// + /// scalar-values grid available after calling Compute() + /// + public DenseGrid3f Grid { + get { return scalar_grid; } + } + + /// + /// Origin of the grid, in same coordinates as mesh + /// + public Vector3f GridOrigin { + get { return grid_origin; } + } + + + /// + /// If ComputeMode==NarrowBand, then we internally compute a signed-distance grid, + /// which will hang onto + /// + 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 cur_front = new List(); + 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 queue = new HashSet(); + 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; + /// if this function returns true, we should abort calculation + public Func 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 Q = new List(); + 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 next_Q = new List(); + 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 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 +{ + + /// + /// 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? + /// + /// + 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; + + /// if this function returns true, we should abort calculation + public Func 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); } + } + + /// + /// winding-number grid available after calling Compute() + /// + public DenseGrid3f Grid { + get { return winding_grid; } + } + + /// + /// Origin of the winding-number grid, in same coordinates as mesh + /// + public Vector3f GridOrigin { + get { return grid_origin; } + } + + + /// + /// If ComputeMode==NarrowBand, then we internally compute a signed-distance grid, + /// which will hang onto + /// + 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 cur_front = new List(); + 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 queue = new HashSet(); + 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 { /// - /// 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. /// 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 Histogram; + public HashSet UsedBins; + public NormalHistogram(int bins, bool bTrackUsed = false) + { + Bins = bins; + Points = new SphericalFibonacciPointSet(bins); + Counts = new double[bins]; + if (bTrackUsed) + UsedBins = new HashSet(); + } - public NormalHistogram(DMesh3 mesh) + /// + /// legacy API + /// + public NormalHistogram(DMesh3 mesh, bool bWeightByArea = true, int bins = 1024) : this(bins) { - Mesh = mesh; - Histogram = new Dictionary(); - build(); + CountFaceNormals(mesh, bWeightByArea); } /// - /// return (rounded) normal associated w/ maximum weight/area + /// bin and count point, and optionally normalize /// - 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); + } + + /// + /// Count all input mesh face normals + /// + 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() + /// + /// return (quantized) normal associated w/ maximum weight/area + /// + 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 { /// - /// 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) /// 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 /// 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 + */ + + /// + /// FWN beta parameter - is 2.0 in paper + /// + public double FWNBeta = 2.0; + + /// + /// FWN approximation order. can be 1 or 2. 2 is more accurate, obviously. + /// + public int FWNApproxOrder = 2; + + /// + /// Replace this with function that returns proper area estimate + /// + public Func FWNAreaEstimateF = (vid) => { return 1.0; }; + + + /// + /// Fast approximation of winding number using far-field approximations + /// + 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 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(); + HashSet 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 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 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(); + 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 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 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); + } + } + } + + + + + + + + /// /// Total sum of volumes of all boxes in the tree. Mainly useful to evaluate tree quality. /// @@ -227,6 +468,13 @@ public double TotalExtentSum() } + /// + /// Root bounding box of tree (note: tree must be generated by calling a query function first!) + /// + 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 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(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, 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) /// public KeyValuePair FindNearestInRadius(Vector2d query_pt, double radius, Func distF, Func ignoreF = null) { @@ -165,7 +166,8 @@ public KeyValuePair FindNearestInRadius(Vector2d query_pt, double rad /// - /// 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) /// public KeyValuePair FindNearestInSquaredRadius(Vector2d query_pt, double radiusSqr, Func distSqrF, Func 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 +{ + + + /// + /// 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 + /// + public class TriangleBinsGrid2d + { + ShiftGridIndexer2 indexer; + AxisAlignedBox2d bounds; + + SmallListSet bins_list; + int bins_x, bins_y; + AxisAlignedBox2i grid_bounds; + + SpinLock spinlock = new SpinLock(); + + /// + /// "invalid" value will be returned by queries if no valid result is found (eg bounded-distance query) + /// + 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; } + } + + /// + /// Insert triangle. This function is thread-safe, uses a SpinLock internally + /// + 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); + } + + /// + /// Insert triangle without locking / thread-safety + /// + 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); + } + + + /// + /// Remove triangle. This function is thread-safe, uses a SpinLock internally + /// + 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); + } + + /// + /// Remove triangle without locking / thread-safety + /// + 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); + } + + + /// + /// 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 + /// + public int FindContainingTriangle(Vector2d query_pt, Func containsF, Func 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; + } + + + + + /// + /// find all triangles that overlap range + /// + public void FindTrianglesInRange(AxisAlignedBox2d range, HashSet 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(); + } + } +}