Exemplo n.º 1
0
        // Assumption here is that Submesh has been modified, but boundary loop has
        // been preserved, and that old submesh has already been removed from this mesh.
        // So, we just have to append new vertices and then rewrite triangles
        // If new_tris or new_verts is non-null, we will return this info.
        // new_tris should be set to TriangleCount (ie it is not necessarily a map)
        // For new_verts, if we used an existing bdry vtx instead, we set the value to -(existing_index+1),
        // otherwise the value is new_index (+1 is to handle 0)
        //
        // Returns true if submesh successfully inserted, false if any triangles failed
        // (which happens if triangle would result in non-manifold mesh)
        public bool ReinsertSubmesh(DSubmesh3 sub, ref int[] new_tris, out IndexMap SubToNewV,
                                    DuplicateTriBehavior eDuplicateBehavior = DuplicateTriBehavior.AssertAbort)
        {
            if (sub.BaseBorderV == null)
            {
                throw new Exception("MeshEditor.ReinsertSubmesh: Submesh does not have required boundary info. Call ComputeBoundaryInfo()!");
            }

            DMesh3 submesh = sub.SubMesh;
            bool   bAllOK  = true;

            IndexFlagSet done_v = new IndexFlagSet(submesh.MaxVertexID, submesh.TriangleCount / 2);

            SubToNewV = new IndexMap(submesh.MaxVertexID, submesh.VertexCount);

            int nti = 0;
            int NT  = submesh.MaxTriangleID;

            for (int ti = 0; ti < NT; ++ti)
            {
                if (submesh.IsTriangle(ti) == false)
                {
                    continue;
                }

                Index3i sub_t = submesh.GetTriangle(ti);
                int     gid   = submesh.GetTriangleGroup(ti);

                Index3i new_t = Index3i.Zero;
                for (int j = 0; j < 3; ++j)
                {
                    int sub_v = sub_t[j];
                    int new_v = -1;
                    if (done_v[sub_v] == false)
                    {
                        // first check if this is a boundary vtx on submesh and maps to a bdry vtx on base mesh
                        if (submesh.IsBoundaryVertex(sub_v))
                        {
                            int base_v = (sub_v < sub.SubToBaseV.size) ? sub.SubToBaseV[sub_v] : -1;
                            if (base_v >= 0 && Mesh.IsVertex(base_v) && sub.BaseBorderV[base_v] == true)
                            {
                                // [RMS] this should always be true, but assert in tests to find out
                                Debug.Assert(Mesh.IsBoundaryVertex(base_v));
                                if (Mesh.IsBoundaryVertex(base_v))
                                {
                                    new_v = base_v;
                                }
                            }
                        }

                        // if that didn't happen, append new vtx
                        if (new_v == -1)
                        {
                            new_v = Mesh.AppendVertex(submesh, sub_v);
                        }

                        SubToNewV[sub_v] = new_v;
                        done_v[sub_v]    = true;
                    }
                    else
                    {
                        new_v = SubToNewV[sub_v];
                    }

                    new_t[j] = new_v;
                }

                // try to handle duplicate-tri case
                if (eDuplicateBehavior == DuplicateTriBehavior.AssertContinue)
                {
                    Debug.Assert(Mesh.FindTriangle(new_t.a, new_t.b, new_t.c) == DMesh3.InvalidID);
                }
                else
                {
                    int existing_tid = Mesh.FindTriangle(new_t.a, new_t.b, new_t.c);
                    if (existing_tid != DMesh3.InvalidID)
                    {
                        if (eDuplicateBehavior == DuplicateTriBehavior.AssertAbort)
                        {
                            Debug.Assert(existing_tid == DMesh3.InvalidID);
                            return(false);
                        }
                        else if (eDuplicateBehavior == DuplicateTriBehavior.UseExisting)
                        {
                            if (new_tris != null)
                            {
                                new_tris[nti++] = existing_tid;
                            }
                            continue;
                        }
                        else if (eDuplicateBehavior == DuplicateTriBehavior.Replace)
                        {
                            Mesh.RemoveTriangle(existing_tid, false);
                        }
                    }
                }


                int new_tid = Mesh.AppendTriangle(new_t, gid);
                Debug.Assert(new_tid != DMesh3.InvalidID && new_tid != DMesh3.NonManifoldID);
                if (!Mesh.IsTriangle(new_tid))
                {
                    bAllOK = false;
                }

                if (new_tris != null)
                {
                    new_tris[nti++] = new_tid;
                }
            }

            return(bAllOK);
        }
Exemplo n.º 2
0
        /// <summary>
        /// Construct vertex correspondences between fill mesh boundary loop
        /// and input mesh boundary loop. In ideal case there is an easy 1-1
        /// correspondence. If that is not true, then do a brute-force search
        /// to find the best correspondences we can.
        ///
        /// Currently only returns unique correspondences. If any vertex
        /// matches with multiple input vertices it is not merged.
        /// [TODO] we could do better in many cases...
        ///
        /// Return value is list of indices into fillLoopV that were not merged
        /// </summary>
        List <int> build_merge_map(DMesh3 fillMesh, int[] fillLoopV,
                                   DMesh3 targetMesh, int[] targetLoopV,
                                   double tol, IndexMap mergeMapV)
        {
            if (fillLoopV.Length == targetLoopV.Length)
            {
                if (build_merge_map_simple(fillMesh, fillLoopV, targetMesh, targetLoopV, tol, mergeMapV))
                {
                    return(null);
                }
            }

            int NF = fillLoopV.Length, NT = targetLoopV.Length;

            bool[] doneF  = new bool[NF], doneT = new bool[NT];
            int[]  countF = new int[NF], countT = new int[NT];
            var    errorV = new List <int>();

            var 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);
        }
Exemplo n.º 3
0
        /// <summary>
        /// Compute the fill mesh and append it.
        /// This returns false if anything went wrong.
        /// The Error Feedback properties (.OutputHasCracks, etc) will provide more info.
        /// </summary>
        public bool Fill()
        {
            compute_polygons();

            // 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;

            foreach (var floop in Loops)
            {
                floop.poly.Translate(-shiftOrigin);
                floop.poly.Scale(scale * Vector2d.One, Vector2d.Zero);
            }

            var ElemToLoopMap = new Dictionary <PlanarComplex.Element, int>();

            // [TODO] if we have multiple components in input mesh, we could do this per-component.
            // This also helps avoid nested shells creating holes.
            // *However*, we shouldn't *have* to because FindSolidRegions will do the right thing if
            // the polygons have the same orientation

            // add all loops to planar complex
            var complex = new PlanarComplex();

            for (int i = 0; i < Loops.Count; ++i)
            {
                var elem = complex.Add(Loops[i].poly);
                ElemToLoopMap[elem] = i;
            }

            // sort into separate 2d solids
            PlanarComplex.SolidRegionInfo solids =
                complex.FindSolidRegions(PlanarComplex.FindSolidsOptions.SortPolygons);

            // fill each 2d solid
            var failed_inserts = new List <Index2i>();
            var failed_merges  = new List <Index2i>();

            for (int fi = 0; fi < solids.Polygons.Count; ++fi)
            {
                var gpoly = solids.Polygons[fi];
                PlanarComplex.GeneralSolid gsolid = solids.PolygonsSources[fi];

                // [TODO] could do scale/translate here, per-polygon would be more precise

                // 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?!?

                // convenient list
                var polys = new List <Polygon2d>()
                {
                    gpoly.Outer
                };
                polys.AddRange(gpoly.Holes);

                // for each poly, we track the set of vertices inserted into mesh
                int[][] polyVertices = new int[polys.Count][];

                // insert each poly
                for (int pi = 0; pi < polys.Count; ++pi)
                {
                    var insert = new MeshInsertUVPolyCurve(FillMesh, polys[pi]);
                    ValidationStatus status = insert.Validate(MathUtil.ZeroTolerancef * scale);
                    bool             failed = true;
                    if (status == ValidationStatus.Ok)
                    {
                        if (insert.Apply())
                        {
                            insert.Simplify();
                            polyVertices[pi] = insert.CurveVertices;
                            failed           = (insert.Loops.Count != 1) ||
                                               (insert.Loops[0].VertexCount != polys[pi].VertexCount);
                        }
                    }
                    if (failed)
                    {
                        failed_inserts.Add(new Index2i(fi, pi));
                    }
                }

                // remove any triangles not contained in gpoly
                // [TODO] degenerate triangle handling? may be 'on' edge of gpoly...
                var removeT = new List <int>();
                foreach (int tid in FillMesh.TriangleIndices())
                {
                    Vector3d v = FillMesh.GetTriCentroid(tid);
                    if (gpoly.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] should check that edges (ie sequential verts) are boundary edges on fill mesh
                //    if not, can try to delete nbr tris to repair
                var mergeMapV = new IndexMap(true);
                if (MergeFillBoundary)
                {
                    for (int pi = 0; pi < polys.Count; ++pi)
                    {
                        if (polyVertices[pi] == null)
                        {
                            continue;
                        }

                        int[] fillLoopVerts = polyVertices[pi];
                        int   NV            = fillLoopVerts.Length;

                        PlanarComplex.Element sourceElem = (pi == 0) ? gsolid.Outer : gsolid.Holes[pi - 1];
                        int      loopi      = ElemToLoopMap[sourceElem];
                        EdgeLoop sourceLoop = Loops[loopi].edgeLoop;

                        // construct vertex-merge map for this loop
                        List <int> bad_indices = build_merge_map(FillMesh, fillLoopVerts, Mesh, sourceLoop.Vertices,
                                                                 MathUtil.ZeroTolerancef, mergeMapV);

                        bool errors = (bad_indices != null && bad_indices.Count > 0);
                        if (errors)
                        {
                            failed_inserts.Add(new Index2i(fi, pi));
                            OutputHasCracks = true;
                        }
                    }
                }

                // append this fill to input mesh
                var   editor = new MeshEditor(Mesh);
                int[] mapV;
                editor.AppendMesh(FillMesh, mergeMapV, out mapV, Mesh.AllocateTriangleGroup());

                // [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);
            }

            return(true);
        }
Exemplo n.º 4
0
        public IOWriteResult Write(TextWriter writer, List <WriteMesh> vMeshes, WriteOptions options)
        {
            if (options.groupNamePrefix != null)
            {
                GroupNamePrefix = options.groupNamePrefix;
            }
            if (options.GroupNameF != null)
            {
                GroupNameF = options.GroupNameF;
            }

            int nAccumCountV  = 1;      // OBJ indices always start at 1
            int nAccumCountUV = 1;

            // collect materials
            string sMaterialLib   = "";
            int    nHaveMaterials = 0;

            if (options.bWriteMaterials && options.MaterialFilePath.Length > 0)
            {
                List <GenericMaterial> vMaterials = MeshIOUtil.FindUniqueMaterialList(vMeshes);
                IOWriteResult          ok         = write_materials(vMaterials, options);
                if (ok.code == IOCode.Ok)
                {
                    sMaterialLib   = Path.GetFileName(options.MaterialFilePath);
                    nHaveMaterials = vMeshes.Count;
                }
            }


            if (options.AsciiHeaderFunc != null)
            {
                writer.WriteLine(options.AsciiHeaderFunc());
            }

            if (sMaterialLib != "")
            {
                writer.WriteLine("mtllib {0}", sMaterialLib);
            }

            for (int mi = 0; mi < vMeshes.Count; ++mi)
            {
                IMesh mesh       = vMeshes[mi].Mesh;
                bool  bVtxColors = options.bPerVertexColors && mesh.HasVertexColors;
                bool  bNormals   = options.bPerVertexNormals && mesh.HasVertexNormals;

                // use separate UV set if we have it, otherwise write per-vertex UVs if we have those
                bool bVtxUVs = options.bPerVertexUVs && mesh.HasVertexUVs;
                if (vMeshes[mi].UVs != null)
                {
                    bVtxUVs = false;
                }

                int[] mapV = new int[mesh.MaxVertexID];

                // write vertices for this mesh
                foreach (int vi in mesh.VertexIndices())
                {
                    mapV[vi] = nAccumCountV++;
                    Vector3d v = mesh.GetVertex(vi);
                    if (bVtxColors)
                    {
                        Vector3d c = mesh.GetVertexColor(vi);
                        writer.WriteLine("v {0} {1} {2} {3:F8} {4:F8} {5:F8}", v[0], v[1], v[2], c[0], c[1], c[2]);
                    }
                    else
                    {
                        writer.WriteLine("v {0} {1} {2}", v[0], v[1], v[2]);
                    }

                    if (bNormals)
                    {
                        Vector3d n = mesh.GetVertexNormal(vi);
                        writer.WriteLine("vn {0:F10} {1:F10} {2:F10}", n[0], n[1], n[2]);
                    }

                    if (bVtxUVs)
                    {
                        Vector2f uv = mesh.GetVertexUV(vi);
                        writer.WriteLine("vt {0:F10} {1:F10}", uv.x, uv.y);
                    }
                }

                // write independent UVs for this mesh, if we have them
                IIndexMap   mapUV = (bVtxUVs) ? new IdentityIndexMap() : null;
                DenseUVMesh uvSet = null;
                if (vMeshes[mi].UVs != null)
                {
                    uvSet = vMeshes[mi].UVs;
                    int      nUV     = uvSet.UVs.Length;
                    IndexMap fullMap = new IndexMap(false, nUV);                       // [TODO] do we really need a map here? is just integer shift, no?
                    for (int ui = 0; ui < nUV; ++ui)
                    {
                        writer.WriteLine("vt {0:F8} {1:F8}", uvSet.UVs[ui].x, uvSet.UVs[ui].y);
                        fullMap[ui] = nAccumCountUV++;
                    }
                    mapUV = fullMap;
                }

                // check if we need to write usemtl lines for this mesh
                bool bWriteMaterials = nHaveMaterials > 0 &&
                                       vMeshes[mi].TriToMaterialMap != null &&
                                       vMeshes[mi].Materials != null;

                // various ways we can write triangles to minimize state changes...
                // [TODO] support writing materials when mesh has groups!!
                if (options.bWriteGroups && mesh.HasTriangleGroups)
                {
                    write_triangles_bygroup(writer, mesh, mapV, uvSet, mapUV, bNormals);
                }
                else
                {
                    write_triangles_flat(writer, vMeshes[mi], mapV, uvSet, mapUV, bNormals, bWriteMaterials);
                }
            }


            return(new IOWriteResult(IOCode.Ok, ""));
        }
Exemplo n.º 5
0
        public bool Fill()
        {
            compute_polygon();

            // translate/scale fill loops to unit box. This will improve
            // accuracy in the calcs below...
            Vector2d shiftOrigin = Bounds.Center;
            double   scale       = 1.0 / Bounds.MaxDim;

            SpansPoly.Translate(-shiftOrigin);
            SpansPoly.Scale(scale * Vector2d.One, Vector2d.Zero);

            Dictionary <PlanarComplex.Element, int> ElemToLoopMap = new Dictionary <PlanarComplex.Element, int>();

            // generate planar mesh that we will insert polygons into
            MeshGenerator meshgen;
            float         planeW     = 1.5f;
            int           nDivisions = 0;

            if (FillTargetEdgeLen < double.MaxValue && FillTargetEdgeLen > 0)
            {
                int n = (int)((planeW / (float)scale) / FillTargetEdgeLen) + 1;
                nDivisions = (n <= 1) ? 0 : n;
            }

            if (nDivisions == 0)
            {
                meshgen = new TrivialRectGenerator()
                {
                    IndicesMap = new Index2i(1, 2), Width = planeW, Height = planeW,
                };
            }
            else
            {
                meshgen = new GriddedRectGenerator()
                {
                    IndicesMap   = new Index2i(1, 2), Width = planeW, Height = planeW,
                    EdgeVertices = nDivisions
                };
            }
            DMesh3 FillMesh = meshgen.Generate().MakeDMesh();

            FillMesh.ReverseOrientation();   // why?!?

            int[] polyVertices = null;

            // insert each poly
            MeshInsertUVPolyCurve insert = new MeshInsertUVPolyCurve(FillMesh, SpansPoly);
            ValidationStatus      status = insert.Validate(MathUtil.ZeroTolerancef * scale);
            bool failed = true;

            if (status == ValidationStatus.Ok)
            {
                if (insert.Apply())
                {
                    insert.Simplify();
                    polyVertices = insert.CurveVertices;
                    failed       = false;
                }
            }
            if (failed)
            {
                return(false);
            }

            // remove any triangles not contained in gpoly
            // [TODO] degenerate triangle handling? may be 'on' edge of gpoly...
            List <int> removeT = new List <int>();

            foreach (int tid in FillMesh.TriangleIndices())
            {
                Vector3d v = FillMesh.GetTriCentroid(tid);
                if (SpansPoly.Contains(v.xy) == false)
                {
                    removeT.Add(tid);
                }
            }
            foreach (int tid in removeT)
            {
                FillMesh.RemoveTriangle(tid, true, false);
            }

            //Util.WriteDebugMesh(FillMesh, "c:\\scratch\\CLIPPED_MESH.obj");

            // transform fill mesh back to 3d
            MeshTransforms.PerVertexTransform(FillMesh, (v) => {
                Vector2d v2 = v.xy;
                v2         /= scale;
                v2         += shiftOrigin;
                return(to3D(v2));
            });


            //Util.WriteDebugMesh(FillMesh, "c:\\scratch\\PLANAR_MESH_WITH_LOOPS.obj");
            //Util.WriteDebugMesh(MeshEditor.Combine(FillMesh, Mesh), "c:\\scratch\\FILLED_MESH.obj");

            // figure out map between new mesh and original edge loops
            // [TODO] if # of verts is different, we can still find correspondence, it is just harder
            // [TODO] should check that edges (ie sequential verts) are boundary edges on fill mesh
            //    if not, can try to delete nbr tris to repair
            IndexMap mergeMapV = new IndexMap(true);

            if (MergeFillBoundary && polyVertices != null)
            {
                throw new NotImplementedException("PlanarSpansFiller: merge fill boundary not implemented!");

                //int[] fillLoopVerts = polyVertices;
                //int NV = fillLoopVerts.Length;

                //PlanarComplex.Element sourceElem = (pi == 0) ? gsolid.Outer : gsolid.Holes[pi - 1];
                //int loopi = ElemToLoopMap[sourceElem];
                //EdgeLoop sourceLoop = Loops[loopi].edgeLoop;

                //for (int k = 0; k < NV; ++k) {
                //    Vector3d fillV = FillMesh.GetVertex(fillLoopVerts[k]);
                //    Vector3d sourceV = Mesh.GetVertex(sourceLoop.Vertices[k]);
                //    if (fillV.Distance(sourceV) < MathUtil.ZeroTolerancef)
                //        mergeMapV[fillLoopVerts[k]] = sourceLoop.Vertices[k];
                //}
            }

            // append this fill to input mesh
            MeshEditor editor = new MeshEditor(Mesh);

            int[] mapV;
            editor.AppendMesh(FillMesh, mergeMapV, out mapV, Mesh.AllocateTriangleGroup());

            // [TODO] should verify that we actually merged the loops...

            return(true);
        }
Exemplo n.º 6
0
        public virtual bool Extrude()
        {
            MeshNormals normals      = null;
            bool        bHaveNormals = Mesh.HasVertexNormals;

            if (!bHaveNormals)
            {
                normals = new MeshNormals(Mesh);
                normals.Compute();
            }

            InitialLoops     = new MeshBoundaryLoops(Mesh);
            InitialTriangles = Mesh.TriangleIndices().ToArray();
            InitialVertices  = Mesh.VertexIndices().ToArray();

            // duplicate triangles of mesh
            InitialToOffsetMapV = new IndexMap(Mesh.MaxVertexID, Mesh.MaxVertexID);
            OffsetGroupID       = OffsetGroup.GetGroupID(Mesh);
            var editor = new MeshEditor(Mesh);

            OffsetTriangles = editor.DuplicateTriangles(InitialTriangles, ref InitialToOffsetMapV, OffsetGroupID);

            // set vertices to new positions
            foreach (int vid in InitialVertices)
            {
                int newvid = InitialToOffsetMapV[vid];
                if (!Mesh.IsVertex(newvid))
                {
                    continue;
                }

                Vector3d v    = Mesh.GetVertex(vid);
                Vector3f n    = (bHaveNormals) ? Mesh.GetVertexNormal(vid) : (Vector3f)normals.Normals[vid];
                Vector3d newv = ExtrudedPositionF(v, n, vid);

                Mesh.SetVertex(newvid, newv);
            }

            // we need to reverse one side
            if (IsPositiveOffset)
            {
                editor.ReverseTriangles(InitialTriangles);
            }
            else
            {
                editor.ReverseTriangles(OffsetTriangles);
            }

            // stitch each loop
            NewLoops        = new EdgeLoop[InitialLoops.Count];
            StitchTriangles = new int[InitialLoops.Count][];
            StitchGroupIDs  = new int[InitialLoops.Count];
            int li = 0;

            foreach (var loop in InitialLoops)
            {
                int[] loop2 = new int[loop.VertexCount];
                for (int k = 0; k < loop2.Length; ++k)
                {
                    loop2[k] = InitialToOffsetMapV[loop.Vertices[k]];
                }

                StitchGroupIDs[li] = StitchGroups.GetGroupID(Mesh);
                if (IsPositiveOffset)
                {
                    StitchTriangles[li] = editor.StitchLoop(loop2, loop.Vertices, StitchGroupIDs[li]);
                }
                else
                {
                    StitchTriangles[li] = editor.StitchLoop(loop.Vertices, loop2, StitchGroupIDs[li]);
                }
                NewLoops[li] = EdgeLoop.FromVertices(Mesh, loop2);
                li++;
            }

            return(true);
        }