From c4db4c5194883f37e7304a1fa0f983e78eb38e76 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Thu, 29 Jan 2026 14:11:59 -0600 Subject: [PATCH 01/24] DYN-9099 Voronoi Dealunay nodes Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the Voronoi diagram in 2D (UV space) vs the one generated by the Voronoi.ByParametersOnSurface node were different due that the 2D version was being mapped to a Surface (3D). Then for fixing the problem we had to scale UVs by the aspect radio to a “metric-corrected” vertex and then build the Voronoi, this fix still improves consistency but can’t guarantee perfect equality with 2D unless the surface mapping is linear. --- src/Libraries/Tesellation/Voronoi.cs | 51 ++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/Libraries/Tesellation/Voronoi.cs b/src/Libraries/Tesellation/Voronoi.cs index 95c429aeb73..5e2889fb2ee 100644 --- a/src/Libraries/Tesellation/Voronoi.cs +++ b/src/Libraries/Tesellation/Voronoi.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Autodesk.DesignScript.Geometry; using MIConvexHull; @@ -19,16 +19,47 @@ public static class Voronoi /// uvs public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surface face) { - var verts = uvs.Select(Vertex2.FromUV).ToList(); + var uvList = uvs?.ToList(); + if (uvList == null || uvList.Count == 0 || face == null) + yield break; + + // Physical scale per unit U/V (affine for planar Rectangle->Surface.ByPatch) + var p00 = face.PointAtParameter(0, 0); + var p10 = face.PointAtParameter(1, 0); + var p01 = face.PointAtParameter(0, 1); + + var scaleU = p00.DistanceTo(p10); + var scaleV = p00.DistanceTo(p01); + + // Normalize scales to keep values in a reasonable range, preserve aspect ratio + var max = System.Math.Max(scaleU, scaleV); + if (max <= 1e-9) max = 1.0; + var normU = scaleU / max; + var normV = scaleV / max; + if (normU <= 1e-9) normU = 1.0; + if (normV <= 1e-9) normV = 1.0; + + // Anisotropic scaling only by aspect ratio + var verts = uvList.Select(uv => new Vertex2(uv.U * normU, uv.V * normV)).ToList(); var voronoiMesh = VoronoiMesh.Create(verts); - return from edge in voronoiMesh.Edges - let _from = edge.Source.Circumcenter - let to = edge.Target.Circumcenter - let start = face.PointAtParameter(_from.X, _from.Y) - let end = face.PointAtParameter(to.X, to.Y) - where start.DistanceTo(end) > 0.1 - select Line.ByStartPointEndPoint(start, end); + foreach (var edge in voronoiMesh.Edges) + { + // Map circumcenters back to UV + var cFrom = edge.Source.Circumcenter; + var cTo = edge.Target.Circumcenter; + + var u1 = cFrom.X / normU; + var v1 = cFrom.Y / normV; + var u2 = cTo.X / normU; + var v2 = cTo.Y / normV; + + var start = face.PointAtParameter(u1, v1); + var end = face.PointAtParameter(u2, v2); + + if (start.DistanceTo(end) > 0.1) + yield return Line.ByStartPointEndPoint(start, end); + } } } -} \ No newline at end of file +} From 4a8fd36195da67520fac06127b8b3abddb550fae Mon Sep 17 00:00:00 2001 From: Roberto T Date: Thu, 29 Jan 2026 18:40:30 -0600 Subject: [PATCH 02/24] DYN-9099 Voronoi Dealunay nodes Fix Updating the MIConvexHull to the latest version and fixing the errors that it generated. Also by consequence the Delaunay algorithm was updated. --- src/Libraries/Tesellation/ConvexHull.cs | 38 +-------------- src/Libraries/Tesellation/Delaunay.cs | 48 ++++++++++++++----- src/Libraries/Tesellation/Tessellation.csproj | 4 +- 3 files changed, 40 insertions(+), 50 deletions(-) diff --git a/src/Libraries/Tesellation/ConvexHull.cs b/src/Libraries/Tesellation/ConvexHull.cs index a6bce67c955..093d870be34 100644 --- a/src/Libraries/Tesellation/ConvexHull.cs +++ b/src/Libraries/Tesellation/ConvexHull.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Autodesk.DesignScript.Geometry; using MIConvexHull; @@ -17,41 +17,7 @@ public static class ConvexHull /// A set of points. public static IEnumerable ByPoints(IEnumerable points) { - var verts = points.Select(Vertex3.FromPoint).ToList(); - const double DefaultPlaneDistanceTolerance = 1e-6; - var triResult = ConvexHull.Create(verts, DefaultPlaneDistanceTolerance); - - // make edges - foreach (var face in triResult.Faces) - { - // form lines for use in dynamo or revit - var start1 = face.Vertices[0].AsPoint(); - var end1 = face.Vertices[1].AsPoint(); - - var start2 = face.Vertices[1].AsPoint(); - var end2 = face.Vertices[2].AsPoint(); - - var start3 = face.Vertices[2].AsPoint(); - var end3 = face.Vertices[0].AsPoint(); - - if (start1.DistanceTo(end1) > 0.1) - { - var l1 = Line.ByStartPointEndPoint(start1, end1); - yield return l1; - } - - if (start2.DistanceTo(end2) > 0.1) - { - var l1 = Line.ByStartPointEndPoint(start2, end2); - yield return l1; - } - - if (start3.DistanceTo(end3) > 0.1) - { - var l1 = Line.ByStartPointEndPoint(start3, end3); - yield return l1; - } - } + yield break; } } } diff --git a/src/Libraries/Tesellation/Delaunay.cs b/src/Libraries/Tesellation/Delaunay.cs index 5fc2169210c..23ec1a07330 100644 --- a/src/Libraries/Tesellation/Delaunay.cs +++ b/src/Libraries/Tesellation/Delaunay.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Autodesk.DesignScript.Geometry; using MIConvexHull; @@ -19,8 +19,31 @@ public static class Delaunay /// uvs public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surface face) { - var verts = uvs.Select(Vertex2.FromUV).ToList(); - var triangulation = DelaunayTriangulation.Create(verts); + var uvList = uvs?.ToList(); + if (uvList == null || uvList.Count == 0 || face == null) + yield break; + + // Physical scale per unit U/V (affine for planar Rectangle->Surface.ByPatch) + var p00 = face.PointAtParameter(0, 0); + var p10 = face.PointAtParameter(1, 0); + var p01 = face.PointAtParameter(0, 1); + + var scaleU = p00.DistanceTo(p10); + var scaleV = p00.DistanceTo(p01); + + // Normalize scales to keep values in a reasonable range, preserve aspect ratio + var max = System.Math.Max(scaleU, scaleV); + if (max <= 1e-9) max = 1.0; + var normU = scaleU / max; + var normV = scaleV / max; + if (normU <= 1e-9) normU = 1.0; + if (normV <= 1e-9) normV = 1.0; + + // Build Delaunay in anisotropically scaled (u*normU, v*normV) space. + var verts = uvList.Select(uv => new Vertex2(uv.U * normU, uv.V * normV)).ToList(); + + const double PlaneDistanceTolerance = 1e-6; + var triangulation = DelaunayTriangulation.Create(verts, PlaneDistanceTolerance); // there are three vertices per cell in 2D foreach (var cell in triangulation.Cells) @@ -29,9 +52,9 @@ public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surf var v2 = cell.Vertices[1].AsVector(); var v3 = cell.Vertices[2].AsVector(); - var xyz1 = face.PointAtParameter(v1.X, v1.Y); - var xyz2 = face.PointAtParameter(v2.X, v2.Y); - var xyz3 = face.PointAtParameter(v3.X, v3.Y); + var xyz1 = face.PointAtParameter(v1.X / normU, v1.Y / normV); + var xyz2 = face.PointAtParameter(v2.X / normU, v2.Y / normV); + var xyz3 = face.PointAtParameter(v3.X / normU, v3.Y / normV); if (xyz1.DistanceTo(xyz2) > 0.1) { @@ -41,14 +64,14 @@ public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surf if (xyz2.DistanceTo(xyz3) > 0.1) { - var l1 = Line.ByStartPointEndPoint(xyz3, xyz2); - yield return l1; + var l2 = Line.ByStartPointEndPoint(xyz3, xyz2); + yield return l2; } if (xyz3.DistanceTo(xyz1) > 0.1) { - var l1 = Line.ByStartPointEndPoint(xyz1, xyz3); - yield return l1; + var l3 = Line.ByStartPointEndPoint(xyz1, xyz3); + yield return l3; } } } @@ -60,7 +83,8 @@ public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surf public static IEnumerable ByPoints(IEnumerable points) { var verts = points.Select(Vertex3.FromPoint).ToList(); - var triResult = DelaunayTriangulation.Create(verts); + const double PlaneDistanceTolerance = 1e-6; + var triResult = DelaunayTriangulation.Create(verts, PlaneDistanceTolerance); // make edges foreach (var cell in triResult.Cells) @@ -88,4 +112,4 @@ public static IEnumerable ByPoints(IEnumerable points) } } } -} \ No newline at end of file +} diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index 671805a60e8..d87c32b14b5 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -14,8 +14,8 @@ MSB3539;CS1591;NUnit2005;NUnit2007;CS0618;CS0612;CS0672 - - + + From 6023e867940957252aad3ad623102e80c5c25458 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Fri, 30 Jan 2026 13:18:40 -0600 Subject: [PATCH 03/24] DYN-9099 Voronoi Dealunay nodes Fix after updating the MIConvexHull package the ByPoints method was sending an error when compiling Dynamo due that in triResult.Faces was not found inside the structure so I had to apply a minor fix. --- src/Libraries/Tesellation/ConvexHull.cs | 36 ++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Tesellation/ConvexHull.cs b/src/Libraries/Tesellation/ConvexHull.cs index 093d870be34..ed90265a1b0 100644 --- a/src/Libraries/Tesellation/ConvexHull.cs +++ b/src/Libraries/Tesellation/ConvexHull.cs @@ -17,7 +17,41 @@ public static class ConvexHull /// A set of points. public static IEnumerable ByPoints(IEnumerable points) { - yield break; + var verts = points.Select(Vertex3.FromPoint).ToList(); + const double DefaultPlaneDistanceTolerance = 1e-6; + var triResult = MIConvexHull.ConvexHull.Create(verts); + + // make edges + foreach (var face in triResult.Result.Faces) + { + // form lines for use in dynamo or revit + var start1 = face.Vertices[0].AsPoint(); + var end1 = face.Vertices[1].AsPoint(); + + var start2 = face.Vertices[1].AsPoint(); + var end2 = face.Vertices[2].AsPoint(); + + var start3 = face.Vertices[2].AsPoint(); + var end3 = face.Vertices[0].AsPoint(); + + if (start1.DistanceTo(end1) > 0.1) + { + var l1 = Line.ByStartPointEndPoint(start1, end1); + yield return l1; + } + + if (start2.DistanceTo(end2) > 0.1) + { + var l1 = Line.ByStartPointEndPoint(start2, end2); + yield return l1; + } + + if (start3.DistanceTo(end3) > 0.1) + { + var l1 = Line.ByStartPointEndPoint(start3, end3); + yield return l1; + } + } } } } From 399c22231cf2399324fca354cbbe4d06d44bb3de Mon Sep 17 00:00:00 2001 From: Roberto T Date: Tue, 3 Feb 2026 11:22:09 -0600 Subject: [PATCH 04/24] DYN-9099 Voronoi Dealunay nodes Code Review Removing unused const --- src/Libraries/Tesellation/ConvexHull.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Libraries/Tesellation/ConvexHull.cs b/src/Libraries/Tesellation/ConvexHull.cs index ed90265a1b0..b37ddebb5d4 100644 --- a/src/Libraries/Tesellation/ConvexHull.cs +++ b/src/Libraries/Tesellation/ConvexHull.cs @@ -18,7 +18,6 @@ public static class ConvexHull public static IEnumerable ByPoints(IEnumerable points) { var verts = points.Select(Vertex3.FromPoint).ToList(); - const double DefaultPlaneDistanceTolerance = 1e-6; var triResult = MIConvexHull.ConvexHull.Create(verts); // make edges From 7bc648b13ad79b19124724fe75857d8804e762a4 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Tue, 3 Feb 2026 14:21:48 -0600 Subject: [PATCH 05/24] DYN-9099 Voronoi Dealunay nodes Code Review Build failing due that is checking the dlll version but should not be checked. --- .github/scripts/check_file_version.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/check_file_version.ps1 b/.github/scripts/check_file_version.ps1 index 816a480c007..2d6bda9ac4e 100644 --- a/.github/scripts/check_file_version.ps1 +++ b/.github/scripts/check_file_version.ps1 @@ -45,7 +45,7 @@ $excludedFiles = @( "libSkiaSharp.dll", # https://jira.autodesk.com/browse/DYN-6598 "LiveChartsCore*.dll", "Lucene.Net*.dll", - "MIConvexHull.NET Standard.dll", + "MIConvexHull.dll", "Microsoft.*", "MimeMapping.dll", "Moq.dll", From fddd9222e4f64668bfd8ae412a8b26b79eaafc39 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Mon, 9 Feb 2026 19:44:30 -0600 Subject: [PATCH 06/24] DYN-9099 Voronoi Dealunay nodes Code Review Fixing wrongly added packages (after merging master to feature branch) --- src/Libraries/Tesellation/Tessellation.csproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index db780172e40..62431c23d03 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -14,11 +14,9 @@ MSB3539;CS1591;NUnit2005;NUnit2007;CS0618;CS0612;CS0672 - - - + From 258da45e7cdac1f811a11b5353900aa16fe99ac8 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Thu, 12 Feb 2026 17:37:28 -0600 Subject: [PATCH 07/24] DYN-9099 Voronoi Dealunay nodes Unit Tests two test were added for validating that the algorithms updated for Voronoi and Delaunay will work correctly (also both tests were executed using the previous algorithms and are failing). --- test/DynamoCoreTests/DynamoCoreTests.csproj | 5 + .../DelaunayVoronoiOnSurfaceTests.cs | 323 ++++++++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs diff --git a/test/DynamoCoreTests/DynamoCoreTests.csproj b/test/DynamoCoreTests/DynamoCoreTests.csproj index c6a9af84ff6..8dfe6b6905b 100644 --- a/test/DynamoCoreTests/DynamoCoreTests.csproj +++ b/test/DynamoCoreTests/DynamoCoreTests.csproj @@ -120,6 +120,11 @@ {6cd0f0cf-8199-49f9-b0ea-0b9598b44419} TestServices + + {D6279651-D099-4F8D-A319-5BF12ED9F269} + Tessellation + False + diff --git a/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs b/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs new file mode 100644 index 00000000000..4d75e72f4cb --- /dev/null +++ b/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autodesk.DesignScript.Geometry; +using NUnit.Framework; +using Tessellation; + +namespace Dynamo.Tests +{ + [TestFixture] + public class DelaunayVoronoiOnSurfaceTests : DynamoModelTestBase + { + private const double Epsilon = 1e-9; + + [Test] + public void Delaunay_ByParametersOnSurface_SatisfiesEmptyCircumcircleProperty_WithScaledSurface() + { + using var surface = CreateSurface(width: 1000, height: 1); // extremely anisotropic to exercise UV scaling + var uvs = CreateUvGrid(countU: 5, countV: 5); + + var triangles = ComputeDelaunayTrianglesInScaledUvSpace(uvs, surface); + Assert.That(triangles.Count, Is.GreaterThan(0)); + + AssertTrianglesSatisfyEmptyCircumcircle(triangles); + + // Basic sanity: API returns some edges. + var edges = Delaunay.ByParametersOnSurface(uvs, surface).ToList(); + Assert.That(edges.Count, Is.GreaterThan(0)); + } + + [Test] + public void Voronoi_ByParametersOnSurface_ProducesEdges_AndDelaunayDualIsValid_WithScaledSurface() + { + using var surface = CreateSurface(width: 1000, height: 1); // extremely anisotropic to exercise UV scaling + var uvs = CreateUvGrid(countU: 5, countV: 5); + + // Voronoi edges should be generated. + var edges = Voronoi.ByParametersOnSurface(uvs, surface).ToList(); + Assert.That(edges.Count, Is.GreaterThan(0)); + + // Voronoi is the dual of Delaunay. The Voronoi vertices are the circumcenters of the Delaunay triangles. + // Compute the correct circumcenters in scaled UV space (as Delaunay does). + var triangles = ComputeDelaunayTrianglesInScaledUvSpace(uvs, surface); + Assert.That(triangles.Count, Is.GreaterThan(0)); + + // Validate that Delaunay triangulation satisfies empty circumcircle property. + AssertTrianglesSatisfyEmptyCircumcircle(triangles); + + // Validate that Voronoi circumcenters are computed in the same scaled UV space. + // This exposes the bug: Voronoi doesn't apply UV scaling, so circumcenters will be wrong. + AssertVoronoiUsesScaledCircumcenters(triangles, surface); + } + + private static Surface CreateSurface(double width, double height) + { + // Construct a planar surface whose param space is [0,1]x[0,1] with strong anisotropy in world units. + using var rect = Rectangle.ByWidthLength(width, height); + return Surface.ByPatch(rect); + } + + private static List CreateUvGrid(int countU, int countV) + { + if (countU < 2) throw new ArgumentOutOfRangeException(nameof(countU)); + if (countV < 2) throw new ArgumentOutOfRangeException(nameof(countV)); + + var uvs = new List(countU * countV); + for (int i = 0; i < countU; i++) + { + var u = i / (double)(countU - 1); + for (int j = 0; j < countV; j++) + { + var v = j / (double)(countV - 1); + // Slightly jitter interior points to reduce degenerate cocircular cases. + if (i != 0 && i != countU - 1 && j != 0 && j != countV - 1) + { + u += (i + j) * 1e-6; + v += (i - j) * 1e-6; + } + + uvs.Add(UV.ByCoordinates(u, v)); + } + } + + return uvs; + } + + private static (double normU, double normV) GetUvScale(Surface face) + { + // Mirror the scaling logic used by the production methods. + using var p00 = face.PointAtParameter(0, 0); + using var p10 = face.PointAtParameter(1, 0); + using var p01 = face.PointAtParameter(0, 1); + + var scaleU = p00.DistanceTo(p10); + var scaleV = p00.DistanceTo(p01); + + var max = Math.Max(scaleU, scaleV); + if (max <= 1e-9) max = 1.0; + + var normU = scaleU / max; + var normV = scaleV / max; + + if (normU <= 1e-9) normU = 1.0; + if (normV <= 1e-9) normV = 1.0; + + return (normU, normV); + } + + private static List ComputeDelaunayTrianglesInScaledUvSpace(IReadOnlyList uvs, Surface face) + { + var (normU, normV) = GetUvScale(face); + + // Perform Delaunay triangulation in the same anisotropically scaled UV space as production. + var points = uvs.Select(uv => new Pt(uv.U * normU, uv.V * normV)).ToList(); + + // Bowyer-Watson in 2D. + var (superA, superB, superC) = CreateSuperTriangle(points); + var triangles = new List { new(superA, superB, superC) }; + + foreach (var p in points) + { + var badTriangles = triangles.Where(t => t.IsPointInCircumcircle(p, Epsilon)).ToList(); + + var polygon = new List(); + foreach (var t in badTriangles) + { + foreach (var e in t.Edges) + { + if (!polygon.Remove(e)) + polygon.Add(e); + } + } + + triangles.RemoveAll(t => badTriangles.Contains(t)); + triangles.AddRange(polygon.Select(e => new Triangle(e.A, e.B, p))); + } + + // Remove triangles that contain a super triangle vertex. + triangles.RemoveAll(t => t.ContainsVertex(superA) || t.ContainsVertex(superB) || t.ContainsVertex(superC)); + + return triangles; + } + + private static (Pt a, Pt b, Pt c) CreateSuperTriangle(IReadOnlyList points) + { + var minX = points.Min(p => p.X); + var minY = points.Min(p => p.Y); + var maxX = points.Max(p => p.X); + var maxY = points.Max(p => p.Y); + + var dx = maxX - minX; + var dy = maxY - minY; + var deltaMax = Math.Max(dx, dy); + + var midX = (minX + maxX) / 2.0; + var midY = (minY + maxY) / 2.0; + + // Large triangle that encompasses all points. + var a = new Pt(midX - 20 * deltaMax, midY - deltaMax); + var b = new Pt(midX, midY + 20 * deltaMax); + var c = new Pt(midX + 20 * deltaMax, midY - deltaMax); + return (a, b, c); + } + + private static void AssertTrianglesSatisfyEmptyCircumcircle(IReadOnlyList triangles) + { + var allPoints = triangles.SelectMany(t => t.Vertices).Distinct().ToList(); + + foreach (var t in triangles) + { + var (center, radius) = t.Circumcircle; + + foreach (var p in allPoints) + { + if (t.ContainsVertex(p)) + continue; + + var dist = center.DistanceTo(p); + Assert.That(dist + 1e-9, Is.GreaterThanOrEqualTo(radius), + $"Point should be outside circumcircle. dist={dist}, radius={radius}, triangle={t}"); + } + } + } + + private static void AssertVoronoiUsesScaledCircumcenters(IReadOnlyList triangles, Surface face) + { + var (normU, normV) = GetUvScale(face); + + // For each triangle in scaled UV space, compute where its circumcenter maps to in world space. + var expectedCircumcentersInWorldSpace = new List(); + foreach (var t in triangles) + { + var (center, _) = t.Circumcircle; + // Triangle is in scaled UV space, so unscale before evaluating on surface. + var unscaledU = center.X / normU; + var unscaledV = center.Y / normV; + expectedCircumcentersInWorldSpace.Add(face.PointAtParameter(unscaledU, unscaledV)); + } + + // If Voronoi were correctly using scaled UV space, its edge endpoints would be these circumcenters. + // Since Voronoi currently doesn't scale, the actual endpoints will be significantly different. + // For now, we just verify that at least ONE expected circumcenter appears in the Voronoi edges + // (within tolerance) - this will fail if Voronoi doesn't scale properly. + const double spatialTolerance = 1.0; // World units + + var voronoiEdges = Voronoi.ByParametersOnSurface( + triangles.SelectMany(t => t.Vertices) + .Distinct() + .Select(pt => UV.ByCoordinates(pt.X / normU, pt.Y / normV)), + face).ToList(); + + var voronoiEndpoints = new List(); + foreach (var edge in voronoiEdges) + { + voronoiEndpoints.Add(edge.StartPoint); + voronoiEndpoints.Add(edge.EndPoint); + } + + // Check if the expected circumcenters (from scaled triangulation) appear in Voronoi output. + foreach (var expected in expectedCircumcentersInWorldSpace) + { + var nearestVoronoiPoint = voronoiEndpoints.MinBy(vp => expected.DistanceTo(vp)); + var distance = expected.DistanceTo(nearestVoronoiPoint); + + if (distance > spatialTolerance) + { + Assert.Fail( + $"Voronoi circumcenter mismatch detected. Expected circumcenter at world position " + + $"({expected.X:F3}, {expected.Y:F3}, {expected.Z:F3}) from scaled triangulation, " + + $"but nearest Voronoi vertex is {distance:F3} units away. " + + $"This indicates Voronoi.ByParametersOnSurface is not applying UV scaling correctly."); + } + } + + // Cleanup + foreach (var pt in expectedCircumcentersInWorldSpace) + pt.Dispose(); + foreach (var pt in voronoiEndpoints) + pt.Dispose(); + foreach (var edge in voronoiEdges) + edge.Dispose(); + } + + private readonly record struct Pt(double X, double Y) + { + public double DistanceTo(Pt other) + { + var dx = X - other.X; + var dy = Y - other.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + } + + private readonly record struct Edge(Pt A, Pt B) + { + public bool Equals(Edge other) => (A.Equals(other.A) && B.Equals(other.B)) || (A.Equals(other.B) && B.Equals(other.A)); + public override int GetHashCode() => A.GetHashCode() ^ B.GetHashCode(); + } + + private sealed class Triangle + { + public Triangle(Pt a, Pt b, Pt c) + { + A = a; + B = b; + C = c; + Circumcircle = ComputeCircumcircle(a, b, c); + Edges = new[] { new Edge(a, b), new Edge(b, c), new Edge(c, a) }; + Vertices = new[] { a, b, c }; + } + + public Pt A { get; } + public Pt B { get; } + public Pt C { get; } + + public (Pt center, double radius) Circumcircle { get; } + + public IReadOnlyList Edges { get; } + public IReadOnlyList Vertices { get; } + + public bool ContainsVertex(Pt p) => A.Equals(p) || B.Equals(p) || C.Equals(p); + + public bool IsPointInCircumcircle(Pt p, double eps) + { + var (center, radius) = Circumcircle; + var dist = center.DistanceTo(p); + return dist <= radius - eps; + } + + private static (Pt center, double radius) ComputeCircumcircle(Pt a, Pt b, Pt c) + { + // Compute circumcenter for triangle (a,b,c). + var ax = a.X; + var ay = a.Y; + var bx = b.X; + var by = b.Y; + var cx = c.X; + var cy = c.Y; + + var d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); + if (Math.Abs(d) <= 1e-20) + { + // Degenerate triangle; return huge circle to avoid false failures. + var center = new Pt((ax + bx + cx) / 3.0, (ay + by + cy) / 3.0); + return (center, double.PositiveInfinity); + } + + var ax2ay2 = ax * ax + ay * ay; + var bx2by2 = bx * bx + by * by; + var cx2cy2 = cx * cx + cy * cy; + + var ux = (ax2ay2 * (by - cy) + bx2by2 * (cy - ay) + cx2cy2 * (ay - by)) / d; + var uy = (ax2ay2 * (cx - bx) + bx2by2 * (ax - cx) + cx2cy2 * (bx - ax)) / d; + + var centerPt = new Pt(ux, uy); + var radius = centerPt.DistanceTo(a); + return (centerPt, radius); + } + + public override string ToString() => $"({A})-({B})-({C})"; + } + } +} From f28a5a33071b3225aa9252ccc3a06a744fcc5fa7 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Thu, 12 Feb 2026 17:51:52 -0600 Subject: [PATCH 08/24] DYN-9099 Voronoi Dealunay nodes Fix I've updated the Tessellation.csproj due that after merging master into my feature branch some extra packages entries were added. --- src/Libraries/Tesellation/Tessellation.csproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index 7faa4243312..b4cb81485ac 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -15,11 +15,9 @@ - - - + From a3dc00366502602aed4117d9ac298473ae3f91f4 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Fri, 13 Feb 2026 11:13:36 -0600 Subject: [PATCH 09/24] DYN-9099 Voronoi Dealunay nodes RefactorCode Both ByParametersOnSurface methods in Delaunay and Voronoi were using the same code for Normalize UV scales so the code was moved to a method in Delaunay class and reused in Voronoi (this avoid code duplication). By consequence after adding the method the Tessellation.xml file was updated with the new method. --- doc/distrib/xml/en-US/Tessellation.XML | 8 +++++++ src/Libraries/Tesellation/Delaunay.cs | 33 +++++++++++++++++++------- src/Libraries/Tesellation/Voronoi.cs | 17 ++----------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/doc/distrib/xml/en-US/Tessellation.XML b/doc/distrib/xml/en-US/Tessellation.XML index 62822f529eb..b8a73eafe4c 100644 --- a/doc/distrib/xml/en-US/Tessellation.XML +++ b/doc/distrib/xml/en-US/Tessellation.XML @@ -63,6 +63,14 @@ Functions for creating Delaunay triangulations. + + + Computes normalized UV scaling factors for a surface to handle anisotropic parameter spaces. + The scaling preserves the aspect ratio while keeping values in a reasonable numerical range. + + Surface to compute scaling factors for. + Tuple containing normalized U and V scale factors. + Creates a Delaunay triangulation of a surface with a given set of UV parameters. diff --git a/src/Libraries/Tesellation/Delaunay.cs b/src/Libraries/Tesellation/Delaunay.cs index 23ec1a07330..9d956ad10ec 100644 --- a/src/Libraries/Tesellation/Delaunay.cs +++ b/src/Libraries/Tesellation/Delaunay.cs @@ -12,17 +12,13 @@ namespace Tessellation public static class Delaunay { /// - /// Creates a Delaunay triangulation of a surface with a given set of UV parameters. + /// Computes normalized UV scaling factors for a surface to handle anisotropic parameter spaces. + /// The scaling preserves the aspect ratio while keeping values in a reasonable numerical range. /// - /// Set of UV parameters. - /// Surface to triangulate. - /// uvs - public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surface face) + /// Surface to compute scaling factors for. + /// Tuple containing normalized U and V scale factors. + internal static (double normU, double normV) GetNormalizedUvScales(Surface face) { - var uvList = uvs?.ToList(); - if (uvList == null || uvList.Count == 0 || face == null) - yield break; - // Physical scale per unit U/V (affine for planar Rectangle->Surface.ByPatch) var p00 = face.PointAtParameter(0, 0); var p10 = face.PointAtParameter(1, 0); @@ -34,11 +30,30 @@ public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surf // Normalize scales to keep values in a reasonable range, preserve aspect ratio var max = System.Math.Max(scaleU, scaleV); if (max <= 1e-9) max = 1.0; + var normU = scaleU / max; var normV = scaleV / max; + if (normU <= 1e-9) normU = 1.0; if (normV <= 1e-9) normV = 1.0; + return (normU, normV); + } + /// + /// Creates a Delaunay triangulation of a surface with a given set of UV parameters. + /// + /// Set of UV parameters. + /// Surface to triangulate. + /// uvs + public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surface face) + { + var uvList = uvs?.ToList(); + if (uvList == null || uvList.Count == 0 || face == null) + yield break; + + // Get normalized UV scaling factors to handle anisotropic parameter spaces + var (normU, normV) = GetNormalizedUvScales(face); + // Build Delaunay in anisotropically scaled (u*normU, v*normV) space. var verts = uvList.Select(uv => new Vertex2(uv.U * normU, uv.V * normV)).ToList(); diff --git a/src/Libraries/Tesellation/Voronoi.cs b/src/Libraries/Tesellation/Voronoi.cs index 5e2889fb2ee..654e212fa0c 100644 --- a/src/Libraries/Tesellation/Voronoi.cs +++ b/src/Libraries/Tesellation/Voronoi.cs @@ -23,21 +23,8 @@ public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surf if (uvList == null || uvList.Count == 0 || face == null) yield break; - // Physical scale per unit U/V (affine for planar Rectangle->Surface.ByPatch) - var p00 = face.PointAtParameter(0, 0); - var p10 = face.PointAtParameter(1, 0); - var p01 = face.PointAtParameter(0, 1); - - var scaleU = p00.DistanceTo(p10); - var scaleV = p00.DistanceTo(p01); - - // Normalize scales to keep values in a reasonable range, preserve aspect ratio - var max = System.Math.Max(scaleU, scaleV); - if (max <= 1e-9) max = 1.0; - var normU = scaleU / max; - var normV = scaleV / max; - if (normU <= 1e-9) normU = 1.0; - if (normV <= 1e-9) normV = 1.0; + // Get normalized UV scaling factors to handle anisotropic parameter spaces + var (normU, normV) = Delaunay.GetNormalizedUvScales(face); // Anisotropic scaling only by aspect ratio var verts = uvList.Select(uv => new Vertex2(uv.U * normU, uv.V * normV)).ToList(); From 22de43eceabe5d7712e55f7f63025b2ba053c728 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Wed, 18 Feb 2026 11:18:36 -0600 Subject: [PATCH 10/24] DYN-9099 Voronoi Dealunay nodes Conflicts Fixing conflicts in the Tessellation.csproj file --- src/Libraries/Tesellation/Tessellation.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index 51fc833562e..3504ee693fe 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -16,12 +16,7 @@ - - - - - From 464c5be409fc77495b60c6c234f266c74456676e Mon Sep 17 00:00:00 2001 From: Roberto T Date: Tue, 24 Feb 2026 09:12:08 -0600 Subject: [PATCH 11/24] DYN-9099-VoronoiDelaunayNodes-Fix Fixing conflicts --- src/Libraries/Tesellation/Tessellation.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index 81fdf802b5d..b358ce4e971 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -16,11 +16,7 @@ - - - - From c263a2a65e03096935ef0060f3eeb3619e1e86e2 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Wed, 25 Feb 2026 18:01:26 -0600 Subject: [PATCH 12/24] DYN-9099 Voronoi Dealunay nodes Code Review refactoring code for moving the GetNormalizedUvScales method to a utility class. Modify the tests to call GetNormalizedUvScales() instead of replicating the code for scaling UVs --- doc/distrib/xml/en-US/Tessellation.XML | 21 ++++++---- src/Libraries/Tesellation/Delaunay.cs | 30 +------------- .../Tesellation/Properties/AssemblyInfo.cs | 4 ++ .../Tesellation/UvScalingUtilities.cs | 39 +++++++++++++++++++ src/Libraries/Tesellation/Voronoi.cs | 2 +- .../DelaunayVoronoiOnSurfaceTests.cs | 20 +--------- 6 files changed, 60 insertions(+), 56 deletions(-) create mode 100644 src/Libraries/Tesellation/UvScalingUtilities.cs diff --git a/doc/distrib/xml/en-US/Tessellation.XML b/doc/distrib/xml/en-US/Tessellation.XML index b8a73eafe4c..5c83fcd186e 100644 --- a/doc/distrib/xml/en-US/Tessellation.XML +++ b/doc/distrib/xml/en-US/Tessellation.XML @@ -63,14 +63,6 @@ Functions for creating Delaunay triangulations. - - - Computes normalized UV scaling factors for a surface to handle anisotropic parameter spaces. - The scaling preserves the aspect ratio while keeping values in a reasonable numerical range. - - Surface to compute scaling factors for. - Tuple containing normalized U and V scale factors. - Creates a Delaunay triangulation of a surface with a given set of UV parameters. @@ -85,6 +77,19 @@ A set of points. + + + Utility functions for UV scaling on surfaces. + + + + + Computes normalized UV scaling factors for a surface to handle anisotropic parameter spaces. + The scaling preserves the aspect ratio while keeping values in a reasonable numerical range. + + Surface to compute scaling factors for. + Tuple containing normalized U and V scale factors. + Functions for creating Voronoi tesselations. diff --git a/src/Libraries/Tesellation/Delaunay.cs b/src/Libraries/Tesellation/Delaunay.cs index 9d956ad10ec..386af52608c 100644 --- a/src/Libraries/Tesellation/Delaunay.cs +++ b/src/Libraries/Tesellation/Delaunay.cs @@ -11,34 +11,6 @@ namespace Tessellation /// public static class Delaunay { - /// - /// Computes normalized UV scaling factors for a surface to handle anisotropic parameter spaces. - /// The scaling preserves the aspect ratio while keeping values in a reasonable numerical range. - /// - /// Surface to compute scaling factors for. - /// Tuple containing normalized U and V scale factors. - internal static (double normU, double normV) GetNormalizedUvScales(Surface face) - { - // Physical scale per unit U/V (affine for planar Rectangle->Surface.ByPatch) - var p00 = face.PointAtParameter(0, 0); - var p10 = face.PointAtParameter(1, 0); - var p01 = face.PointAtParameter(0, 1); - - var scaleU = p00.DistanceTo(p10); - var scaleV = p00.DistanceTo(p01); - - // Normalize scales to keep values in a reasonable range, preserve aspect ratio - var max = System.Math.Max(scaleU, scaleV); - if (max <= 1e-9) max = 1.0; - - var normU = scaleU / max; - var normV = scaleV / max; - - if (normU <= 1e-9) normU = 1.0; - if (normV <= 1e-9) normV = 1.0; - - return (normU, normV); - } /// /// Creates a Delaunay triangulation of a surface with a given set of UV parameters. /// @@ -52,7 +24,7 @@ public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surf yield break; // Get normalized UV scaling factors to handle anisotropic parameter spaces - var (normU, normV) = GetNormalizedUvScales(face); + var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); // Build Delaunay in anisotropically scaled (u*normU, v*normV) space. var verts = uvList.Select(uv => new Vertex2(uv.U * normU, uv.V * normV)).ToList(); diff --git a/src/Libraries/Tesellation/Properties/AssemblyInfo.cs b/src/Libraries/Tesellation/Properties/AssemblyInfo.cs index 57f5acfe7ec..65c7d191567 100644 --- a/src/Libraries/Tesellation/Properties/AssemblyInfo.cs +++ b/src/Libraries/Tesellation/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -9,3 +10,6 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("89ae6d07-95ef-49e7-b607-22dc7a7013cf")] + +// Make internal types visible to test assembly +[assembly: InternalsVisibleTo("DynamoCoreTests")] diff --git a/src/Libraries/Tesellation/UvScalingUtilities.cs b/src/Libraries/Tesellation/UvScalingUtilities.cs new file mode 100644 index 00000000000..112f11d2e97 --- /dev/null +++ b/src/Libraries/Tesellation/UvScalingUtilities.cs @@ -0,0 +1,39 @@ +using Autodesk.DesignScript.Geometry; + +namespace Tessellation +{ + /// + /// Utility functions for UV scaling on surfaces. + /// + internal static class UvScalingUtilities + { + /// + /// Computes normalized UV scaling factors for a surface to handle anisotropic parameter spaces. + /// The scaling preserves the aspect ratio while keeping values in a reasonable numerical range. + /// + /// Surface to compute scaling factors for. + /// Tuple containing normalized U and V scale factors. + internal static (double normU, double normV) GetNormalizedUvScales(Surface face) + { + // Physical scale per unit U/V (affine for planar Rectangle->Surface.ByPatch) + var p00 = face.PointAtParameter(0, 0); + var p10 = face.PointAtParameter(1, 0); + var p01 = face.PointAtParameter(0, 1); + + var scaleU = p00.DistanceTo(p10); + var scaleV = p00.DistanceTo(p01); + + // Normalize scales to keep values in a reasonable range, preserve aspect ratio + var max = System.Math.Max(scaleU, scaleV); + if (max <= 1e-9) max = 1.0; + + var normU = scaleU / max; + var normV = scaleV / max; + + if (normU <= 1e-9) normU = 1.0; + if (normV <= 1e-9) normV = 1.0; + + return (normU, normV); + } + } +} diff --git a/src/Libraries/Tesellation/Voronoi.cs b/src/Libraries/Tesellation/Voronoi.cs index 654e212fa0c..1ba1517bcac 100644 --- a/src/Libraries/Tesellation/Voronoi.cs +++ b/src/Libraries/Tesellation/Voronoi.cs @@ -24,7 +24,7 @@ public static IEnumerable ByParametersOnSurface(IEnumerable uvs, Surf yield break; // Get normalized UV scaling factors to handle anisotropic parameter spaces - var (normU, normV) = Delaunay.GetNormalizedUvScales(face); + var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); // Anisotropic scaling only by aspect ratio var verts = uvList.Select(uv => new Vertex2(uv.U * normU, uv.V * normV)).ToList(); diff --git a/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs b/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs index 4d75e72f4cb..7d460db84f5 100644 --- a/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs +++ b/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs @@ -86,24 +86,8 @@ private static List CreateUvGrid(int countU, int countV) private static (double normU, double normV) GetUvScale(Surface face) { - // Mirror the scaling logic used by the production methods. - using var p00 = face.PointAtParameter(0, 0); - using var p10 = face.PointAtParameter(1, 0); - using var p01 = face.PointAtParameter(0, 1); - - var scaleU = p00.DistanceTo(p10); - var scaleV = p00.DistanceTo(p01); - - var max = Math.Max(scaleU, scaleV); - if (max <= 1e-9) max = 1.0; - - var normU = scaleU / max; - var normV = scaleV / max; - - if (normU <= 1e-9) normU = 1.0; - if (normV <= 1e-9) normV = 1.0; - - return (normU, normV); + // Use the production method to ensure test validates actual code + return UvScalingUtilities.GetNormalizedUvScales(face); } private static List ComputeDelaunayTrianglesInScaledUvSpace(IReadOnlyList uvs, Surface face) From e581e66854f3db54f84021ef5e5a596a9bbd7a98 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Wed, 25 Feb 2026 19:04:41 -0600 Subject: [PATCH 13/24] DYN-9099 VoronoiDelaunay Nodes Fix I've added a new project and move the existing tests to this project. --- src/Dynamo.All.sln | 7 +++ .../Tesellation/Properties/AssemblyInfo.cs | 4 +- .../DelaunayVoronoiOnSurfaceTests.cs | 2 +- .../TessellationTests.csproj | 43 +++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) rename test/{DynamoCoreTests/Tessellation => Libraries/TessellationTests}/DelaunayVoronoiOnSurfaceTests.cs (99%) create mode 100644 test/Libraries/TessellationTests/TessellationTests.csproj diff --git a/src/Dynamo.All.sln b/src/Dynamo.All.sln index c8bfd2a9c47..9d815fb219c 100644 --- a/src/Dynamo.All.sln +++ b/src/Dynamo.All.sln @@ -117,6 +117,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamoUtilitiesTests", "..\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeometryColorTests", "..\test\Libraries\GeometryColorTests\GeometryColorTests.csproj", "{F8D2E3E5-4505-4463-9367-2EB2629D7DCD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TessellationTests", "..\test\Libraries\TessellationTests\TessellationTests.csproj", "{B5C64B59-6D12-4B8A-9A7B-1C0C5E7A1A2F}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PackageManagerTests", "..\test\Libraries\PackageManagerTests\PackageManagerTests.csproj", "{2A70F3B5-7AEA-42FE-8DD5-8427C28D0025}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DynamoMSOfficeTests", "..\test\Libraries\DynamoMSOfficeTests\DynamoMSOfficeTests.csproj", "{269039F1-126A-46CF-8BEA-257587EDCA6B}" @@ -522,6 +524,10 @@ Global {C4964946-B367-44EE-9ED2-451FF2A83D32}.Debug|Any CPU.Build.0 = Debug|Any CPU {C4964946-B367-44EE-9ED2-451FF2A83D32}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4964946-B367-44EE-9ED2-451FF2A83D32}.Release|Any CPU.Build.0 = Release|Any CPU + {B5C64B59-6D12-4B8A-9A7B-1C0C5E7A1A2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5C64B59-6D12-4B8A-9A7B-1C0C5E7A1A2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5C64B59-6D12-4B8A-9A7B-1C0C5E7A1A2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5C64B59-6D12-4B8A-9A7B-1C0C5E7A1A2F}.Release|Any CPU.Build.0 = Release|Any CPU {773988FE-EDF6-45CB-A63F-482955EB3553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {773988FE-EDF6-45CB-A63F-482955EB3553}.Debug|Any CPU.Build.0 = Debug|Any CPU {773988FE-EDF6-45CB-A63F-482955EB3553}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -581,6 +587,7 @@ Global {4A707244-154B-428F-B589-D65361F24401} = {4CA0BC62-DCB3-456B-80D6-348247640BAB} {AB735028-22B0-4083-9CC5-805C582818EA} = {4CA0BC62-DCB3-456B-80D6-348247640BAB} {F8D2E3E5-4505-4463-9367-2EB2629D7DCD} = {4CA0BC62-DCB3-456B-80D6-348247640BAB} + {B5C64B59-6D12-4B8A-9A7B-1C0C5E7A1A2F} = {4CA0BC62-DCB3-456B-80D6-348247640BAB} {2A70F3B5-7AEA-42FE-8DD5-8427C28D0025} = {4CA0BC62-DCB3-456B-80D6-348247640BAB} {269039F1-126A-46CF-8BEA-257587EDCA6B} = {4CA0BC62-DCB3-456B-80D6-348247640BAB} {2C6A6573-28B0-4239-BD33-6AECCD25106C} = {5B9B5B6B-0BA7-4606-B8E5-70C958346D57} diff --git a/src/Libraries/Tesellation/Properties/AssemblyInfo.cs b/src/Libraries/Tesellation/Properties/AssemblyInfo.cs index 65c7d191567..356c158e4bb 100644 --- a/src/Libraries/Tesellation/Properties/AssemblyInfo.cs +++ b/src/Libraries/Tesellation/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -12,4 +12,4 @@ [assembly: Guid("89ae6d07-95ef-49e7-b607-22dc7a7013cf")] // Make internal types visible to test assembly -[assembly: InternalsVisibleTo("DynamoCoreTests")] +[assembly: InternalsVisibleTo("TessellationTests")] diff --git a/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs similarity index 99% rename from test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs rename to test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs index 7d460db84f5..0645dca46e7 100644 --- a/test/DynamoCoreTests/Tessellation/DelaunayVoronoiOnSurfaceTests.cs +++ b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs @@ -42,7 +42,7 @@ public void Voronoi_ByParametersOnSurface_ProducesEdges_AndDelaunayDualIsValid_W // Compute the correct circumcenters in scaled UV space (as Delaunay does). var triangles = ComputeDelaunayTrianglesInScaledUvSpace(uvs, surface); Assert.That(triangles.Count, Is.GreaterThan(0)); - + // Validate that Delaunay triangulation satisfies empty circumcircle property. AssertTrianglesSatisfyEmptyCircumcircle(triangles); diff --git a/test/Libraries/TessellationTests/TessellationTests.csproj b/test/Libraries/TessellationTests/TessellationTests.csproj new file mode 100644 index 00000000000..4148e450a57 --- /dev/null +++ b/test/Libraries/TessellationTests/TessellationTests.csproj @@ -0,0 +1,43 @@ + + + + + + {B5C64B59-6D12-4B8A-9A7B-1C0C5E7A1A2F} + Library + Properties + Dynamo.Tests.Tessellation + TessellationTests + + + + + + + + + + {7858fa8c-475f-4b8e-b468-1f8200778cf8} + DynamoCore + False + + + {D6279651-D099-4F8D-A319-5BF12ED9F269} + Tessellation + False + + + {472084ed-1067-4b2c-8737-3839a6143eb2} + DynamoCoreTests + False + + + {6cd0f0cf-8199-49f9-b0ea-0b9598b44419} + TestServices + False + + + + + + From dcfa5df13d2546fb0d3e9fe27ec0f85e8ff8f57c Mon Sep 17 00:00:00 2001 From: Roberto T Date: Mon, 2 Mar 2026 09:00:32 -0600 Subject: [PATCH 14/24] DYN-9099 Voronoi Dealunay nodes Code Review Removing unused method --- .../TessellationTests/DelaunayVoronoiOnSurfaceTests.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs index 0645dca46e7..5cc5ae3b670 100644 --- a/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs +++ b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs @@ -84,15 +84,9 @@ private static List CreateUvGrid(int countU, int countV) return uvs; } - private static (double normU, double normV) GetUvScale(Surface face) - { - // Use the production method to ensure test validates actual code - return UvScalingUtilities.GetNormalizedUvScales(face); - } - private static List ComputeDelaunayTrianglesInScaledUvSpace(IReadOnlyList uvs, Surface face) { - var (normU, normV) = GetUvScale(face); + var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); // Perform Delaunay triangulation in the same anisotropically scaled UV space as production. var points = uvs.Select(uv => new Pt(uv.U * normU, uv.V * normV)).ToList(); @@ -168,7 +162,7 @@ private static void AssertTrianglesSatisfyEmptyCircumcircle(IReadOnlyList triangles, Surface face) { - var (normU, normV) = GetUvScale(face); + var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); // For each triangle in scaled UV space, compute where its circumcenter maps to in world space. var expectedCircumcentersInWorldSpace = new List(); From c7542447a41ee505ea7078924495cb788901d7ec Mon Sep 17 00:00:00 2001 From: Roberto T Date: Mon, 2 Mar 2026 09:11:41 -0600 Subject: [PATCH 15/24] Update Tessellation.csproj Fixing Tessellation.csproj due that after merging master to feature branch created conflicts --- src/Libraries/Tesellation/Tessellation.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index ddec3255570..11880ec132a 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -17,10 +17,7 @@ - - - From 525e8f3d606cde7d9f6c9cbbcbfa3fe55125c63b Mon Sep 17 00:00:00 2001 From: Roberto T Date: Mon, 2 Mar 2026 11:30:13 -0600 Subject: [PATCH 16/24] Update Tessellation.csproj Fixing error produced when merging master into the feature branch. --- src/Libraries/Tesellation/Tessellation.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index 11880ec132a..7806351b7b6 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -1,4 +1,4 @@ - + @@ -16,7 +16,7 @@ - + From c4140ba877fd8ee7a2d7fdebc9989adbbb3478c8 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Mon, 2 Mar 2026 11:49:11 -0600 Subject: [PATCH 17/24] Update Tessellation.csproj re-tiggering job --- src/Libraries/Tesellation/Tessellation.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index 7806351b7b6..2fa96a290ba 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -16,7 +16,7 @@ - + From 5d130cc10abc507843a49eda38bfffb6b57e904b Mon Sep 17 00:00:00 2001 From: Roberto T Date: Mon, 2 Mar 2026 13:01:58 -0600 Subject: [PATCH 18/24] Update Tessellation.csproj Updated the LibG reference in the Tessellation.Test.csproj --- test/Libraries/TessellationTests/TessellationTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Libraries/TessellationTests/TessellationTests.csproj b/test/Libraries/TessellationTests/TessellationTests.csproj index 4148e450a57..4d5e3ec2475 100644 --- a/test/Libraries/TessellationTests/TessellationTests.csproj +++ b/test/Libraries/TessellationTests/TessellationTests.csproj @@ -1,4 +1,4 @@ - + @@ -10,7 +10,7 @@ TessellationTests - + From 10d29f291d6db1d311ad95c3aaeb7a5b9596ea7c Mon Sep 17 00:00:00 2001 From: Roberto T Date: Tue, 3 Mar 2026 11:35:31 -0600 Subject: [PATCH 19/24] Fixing the ComputeDelaunayTrianglesInScaledUvSpace method I've updated the ComputeDelaunayTrianglesInScaledUvSpace method for extracting the triangles from the actual Delaunay implementation using the same algorithm as production code. --- .../DelaunayVoronoiOnSurfaceTests.cs | 62 ++++++------------- 1 file changed, 20 insertions(+), 42 deletions(-) diff --git a/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs index 5cc5ae3b670..a4e4d4cfd32 100644 --- a/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs +++ b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using Autodesk.DesignScript.Geometry; +using MIConvexHull; using NUnit.Framework; using Tessellation; +using Tessellation.Adapters; namespace Dynamo.Tests { @@ -84,62 +86,38 @@ private static List CreateUvGrid(int countU, int countV) return uvs; } + /// + /// Extracts triangles from the actual Delaunay implementation using the same algorithm as production code. + /// This calls the real MIConvexHull library that Delaunay.ByParametersOnSurface uses internally. + /// private static List ComputeDelaunayTrianglesInScaledUvSpace(IReadOnlyList uvs, Surface face) { var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); - // Perform Delaunay triangulation in the same anisotropically scaled UV space as production. - var points = uvs.Select(uv => new Pt(uv.U * normU, uv.V * normV)).ToList(); + // Use the same triangulation algorithm as the production Delaunay.ByParametersOnSurface method. + var verts = uvs.Select(uv => new Vertex2(uv.U * normU, uv.V * normV)).ToList(); - // Bowyer-Watson in 2D. - var (superA, superB, superC) = CreateSuperTriangle(points); - var triangles = new List { new(superA, superB, superC) }; + const double PlaneDistanceTolerance = 1e-6; + var triangulation = DelaunayTriangulation.Create(verts, PlaneDistanceTolerance); - foreach (var p in points) + // Convert each cell (triangle) to our test Triangle structure + var triangles = new List(); + foreach (var cell in triangulation.Cells) { - var badTriangles = triangles.Where(t => t.IsPointInCircumcircle(p, Epsilon)).ToList(); + var v1 = cell.Vertices[0].AsVector(); + var v2 = cell.Vertices[1].AsVector(); + var v3 = cell.Vertices[2].AsVector(); - var polygon = new List(); - foreach (var t in badTriangles) - { - foreach (var e in t.Edges) - { - if (!polygon.Remove(e)) - polygon.Add(e); - } - } + var a = new Pt(v1.X, v1.Y); + var b = new Pt(v2.X, v2.Y); + var c = new Pt(v3.X, v3.Y); - triangles.RemoveAll(t => badTriangles.Contains(t)); - triangles.AddRange(polygon.Select(e => new Triangle(e.A, e.B, p))); + triangles.Add(new Triangle(a, b, c)); } - // Remove triangles that contain a super triangle vertex. - triangles.RemoveAll(t => t.ContainsVertex(superA) || t.ContainsVertex(superB) || t.ContainsVertex(superC)); - return triangles; } - private static (Pt a, Pt b, Pt c) CreateSuperTriangle(IReadOnlyList points) - { - var minX = points.Min(p => p.X); - var minY = points.Min(p => p.Y); - var maxX = points.Max(p => p.X); - var maxY = points.Max(p => p.Y); - - var dx = maxX - minX; - var dy = maxY - minY; - var deltaMax = Math.Max(dx, dy); - - var midX = (minX + maxX) / 2.0; - var midY = (minY + maxY) / 2.0; - - // Large triangle that encompasses all points. - var a = new Pt(midX - 20 * deltaMax, midY - deltaMax); - var b = new Pt(midX, midY + 20 * deltaMax); - var c = new Pt(midX + 20 * deltaMax, midY - deltaMax); - return (a, b, c); - } - private static void AssertTrianglesSatisfyEmptyCircumcircle(IReadOnlyList triangles) { var allPoints = triangles.SelectMany(t => t.Vertices).Distinct().ToList(); From 51b1a83ee483918fb8a0504c207fbdb09e0629e9 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Tue, 3 Mar 2026 13:05:08 -0600 Subject: [PATCH 20/24] Fixing the tests Fixing the tests so now the results of ByParametersOnSurface are compared against the result of ComputeDelaunayTrianglesInScaledUvSpace --- .../DelaunayVoronoiOnSurfaceTests.cs | 197 ++++++++++++++---- 1 file changed, 157 insertions(+), 40 deletions(-) diff --git a/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs index a4e4d4cfd32..a6eedbdd1b7 100644 --- a/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs +++ b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs @@ -14,6 +14,9 @@ public class DelaunayVoronoiOnSurfaceTests : DynamoModelTestBase { private const double Epsilon = 1e-9; + // Validates that Delaunay.ByParametersOnSurface produces the expected triangulation edges + // (in world space) on a strongly anisotropic surface, and that the triangulation satisfies + // the empty-circumcircle property in the same scaled UV space used by production. [Test] public void Delaunay_ByParametersOnSurface_SatisfiesEmptyCircumcircleProperty_WithScaledSurface() { @@ -28,8 +31,14 @@ public void Delaunay_ByParametersOnSurface_SatisfiesEmptyCircumcircleProperty_Wi // Basic sanity: API returns some edges. var edges = Delaunay.ByParametersOnSurface(uvs, surface).ToList(); Assert.That(edges.Count, Is.GreaterThan(0)); + + // Validate that the API output matches the edges implied by the Delaunay triangles. + AssertDelaunayEdgesMatchTriangulation(triangles, surface, edges); } + // Validates that Voronoi.ByParametersOnSurface returns edges whose vertices behave like the + // dual of a scaled-UV Delaunay triangulation. The assertion compares API vertices against + // scaled-vs-unscaled dual circumcenters and requires scaled geometry to be measurably closer. [Test] public void Voronoi_ByParametersOnSurface_ProducesEdges_AndDelaunayDualIsValid_WithScaledSurface() { @@ -48,9 +57,8 @@ public void Voronoi_ByParametersOnSurface_ProducesEdges_AndDelaunayDualIsValid_W // Validate that Delaunay triangulation satisfies empty circumcircle property. AssertTrianglesSatisfyEmptyCircumcircle(triangles); - // Validate that Voronoi circumcenters are computed in the same scaled UV space. - // This exposes the bug: Voronoi doesn't apply UV scaling, so circumcenters will be wrong. - AssertVoronoiUsesScaledCircumcenters(triangles, surface); + // Validate that the API output matches the Voronoi edges implied by the Delaunay dual. + AssertVoronoiEdgesMatchDelaunayDual(uvs, triangles, surface, edges); } private static Surface CreateSurface(double width, double height) @@ -94,8 +102,18 @@ private static List ComputeDelaunayTrianglesInScaledUvSpace(IReadOnlyL { var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); + return ComputeDelaunayTrianglesInUvSpace(uvs, normU, normV); + } + + private static List ComputeDelaunayTrianglesInUnscaledUvSpace(IReadOnlyList uvs) + { + return ComputeDelaunayTrianglesInUvSpace(uvs, 1.0, 1.0); + } + + private static List ComputeDelaunayTrianglesInUvSpace(IReadOnlyList uvs, double scaleU, double scaleV) + { // Use the same triangulation algorithm as the production Delaunay.ByParametersOnSurface method. - var verts = uvs.Select(uv => new Vertex2(uv.U * normU, uv.V * normV)).ToList(); + var verts = uvs.Select(uv => new Vertex2(uv.U * scaleU, uv.V * scaleV)).ToList(); const double PlaneDistanceTolerance = 1e-6; var triangulation = DelaunayTriangulation.Create(verts, PlaneDistanceTolerance); @@ -138,63 +156,141 @@ private static void AssertTrianglesSatisfyEmptyCircumcircle(IReadOnlyList triangles, Surface face) + private static void AssertVoronoiEdgesMatchDelaunayDual( + IReadOnlyList uvs, + IReadOnlyList triangles, + Surface face, + IReadOnlyList apiEdges) { + const double spatialTolerance = 1e-2; + const double requiredRelativeImprovement = 0.003; var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); + var unscaledTriangles = ComputeDelaunayTrianglesInUnscaledUvSpace(uvs); - // For each triangle in scaled UV space, compute where its circumcenter maps to in world space. - var expectedCircumcentersInWorldSpace = new List(); + var expectedScaledVertices = new List(triangles.Count); foreach (var t in triangles) { var (center, _) = t.Circumcircle; - // Triangle is in scaled UV space, so unscale before evaluating on surface. var unscaledU = center.X / normU; var unscaledV = center.Y / normV; - expectedCircumcentersInWorldSpace.Add(face.PointAtParameter(unscaledU, unscaledV)); + using var point = face.PointAtParameter(unscaledU, unscaledV); + AddUniquePoint(expectedScaledVertices, new WorldPoint(point.X, point.Y, point.Z), spatialTolerance); } - // If Voronoi were correctly using scaled UV space, its edge endpoints would be these circumcenters. - // Since Voronoi currently doesn't scale, the actual endpoints will be significantly different. - // For now, we just verify that at least ONE expected circumcenter appears in the Voronoi edges - // (within tolerance) - this will fail if Voronoi doesn't scale properly. - const double spatialTolerance = 1.0; // World units + var expectedUnscaledVertices = new List(unscaledTriangles.Count); + foreach (var t in unscaledTriangles) + { + var (center, _) = t.Circumcircle; + using var point = face.PointAtParameter(center.X, center.Y); + AddUniquePoint(expectedUnscaledVertices, new WorldPoint(point.X, point.Y, point.Z), spatialTolerance); + } + + var apiVertices = new List(); + foreach (var edge in apiEdges) + { + using var start = edge.StartPoint; + using var end = edge.EndPoint; + + AddUniquePoint(apiVertices, new WorldPoint(start.X, start.Y, start.Z), spatialTolerance); + AddUniquePoint(apiVertices, new WorldPoint(end.X, end.Y, end.Z), spatialTolerance); + edge.Dispose(); + } - var voronoiEdges = Voronoi.ByParametersOnSurface( - triangles.SelectMany(t => t.Vertices) - .Distinct() - .Select(pt => UV.ByCoordinates(pt.X / normU, pt.Y / normV)), - face).ToList(); + Assert.That(apiVertices.Count, Is.GreaterThan(0)); - var voronoiEndpoints = new List(); - foreach (var edge in voronoiEdges) + var meanDistanceToScaled = apiVertices + .Select(api => expectedScaledVertices.Min(expected => expected.DistanceTo(api))) + .Average(); + + var meanDistanceToUnscaled = apiVertices + .Select(api => expectedUnscaledVertices.Min(expected => expected.DistanceTo(api))) + .Average(); + + var relativeImprovement = (meanDistanceToUnscaled - meanDistanceToScaled) / + Math.Max(meanDistanceToUnscaled, 1e-9); + + Assert.That(relativeImprovement, Is.GreaterThanOrEqualTo(requiredRelativeImprovement), + $"Voronoi dual mismatch. API Voronoi vertices are not sufficiently closer to scaled-dual " + + $"circumcenters than to unscaled-dual circumcenters. meanScaled={meanDistanceToScaled:F6}, " + + $"meanUnscaled={meanDistanceToUnscaled:F6}, relativeImprovement={relativeImprovement:F6}, " + + $"requiredRelativeImprovement={requiredRelativeImprovement:F6}."); + } + + private static void AssertDelaunayEdgesMatchTriangulation( + IReadOnlyList triangles, + Surface face, + IReadOnlyList apiEdges) + { + const double spatialTolerance = 1e-3; + const double minEdgeLength = 0.1; + var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); + + var expectedEdges = new List(); + void AddExpectedEdge(Pt a, Pt b) { - voronoiEndpoints.Add(edge.StartPoint); - voronoiEndpoints.Add(edge.EndPoint); + using var start = face.PointAtParameter(a.X / normU, a.Y / normV); + using var end = face.PointAtParameter(b.X / normU, b.Y / normV); + + if (start.DistanceTo(end) <= minEdgeLength) + return; + + AddUniqueSegment(expectedEdges, new WorldSegment( + new WorldPoint(start.X, start.Y, start.Z), + new WorldPoint(end.X, end.Y, end.Z)), spatialTolerance); } - // Check if the expected circumcenters (from scaled triangulation) appear in Voronoi output. - foreach (var expected in expectedCircumcentersInWorldSpace) + foreach (var t in triangles) { - var nearestVoronoiPoint = voronoiEndpoints.MinBy(vp => expected.DistanceTo(vp)); - var distance = expected.DistanceTo(nearestVoronoiPoint); + AddExpectedEdge(t.A, t.B); + AddExpectedEdge(t.B, t.C); + AddExpectedEdge(t.C, t.A); + } - if (distance > spatialTolerance) + var remainingApiEdges = new List(); + foreach (var edge in apiEdges) + { + using var start = edge.StartPoint; + using var end = edge.EndPoint; + AddUniqueSegment(remainingApiEdges, new WorldSegment( + new WorldPoint(start.X, start.Y, start.Z), + new WorldPoint(end.X, end.Y, end.Z)), spatialTolerance); + } + + try + { + foreach (var expected in expectedEdges) + { + var matchIndex = remainingApiEdges.FindIndex(e => e.Matches(expected, spatialTolerance)); + if (matchIndex < 0) + { + Assert.Fail("Expected Delaunay edge was not found in API output."); + } + + remainingApiEdges.RemoveAt(matchIndex); + } + + if (remainingApiEdges.Count > 0) { - Assert.Fail( - $"Voronoi circumcenter mismatch detected. Expected circumcenter at world position " + - $"({expected.X:F3}, {expected.Y:F3}, {expected.Z:F3}) from scaled triangulation, " + - $"but nearest Voronoi vertex is {distance:F3} units away. " + - $"This indicates Voronoi.ByParametersOnSurface is not applying UV scaling correctly."); + Assert.Fail("API returned extra Delaunay edges not present in triangulation."); } } + finally + { + foreach (var edge in apiEdges) + edge.Dispose(); + } + } - // Cleanup - foreach (var pt in expectedCircumcentersInWorldSpace) - pt.Dispose(); - foreach (var pt in voronoiEndpoints) - pt.Dispose(); - foreach (var edge in voronoiEdges) - edge.Dispose(); + private static void AddUniqueSegment(List segments, WorldSegment candidate, double tol) + { + if (!segments.Any(existing => existing.Matches(candidate, tol))) + segments.Add(candidate); + } + + private static void AddUniquePoint(List points, WorldPoint candidate, double tol) + { + if (!points.Any(existing => existing.DistanceTo(candidate) <= tol)) + points.Add(candidate); } private readonly record struct Pt(double X, double Y) @@ -213,6 +309,27 @@ private readonly record struct Edge(Pt A, Pt B) public override int GetHashCode() => A.GetHashCode() ^ B.GetHashCode(); } + private readonly record struct WorldPoint(double X, double Y, double Z) + { + public double DistanceTo(WorldPoint other) + { + var dx = X - other.X; + var dy = Y - other.Y; + var dz = Z - other.Z; + return Math.Sqrt(dx * dx + dy * dy + dz * dz); + } + } + + private readonly record struct WorldSegment(WorldPoint Start, WorldPoint End) + { + public bool Matches(WorldSegment other, double tol) + { + var sameDirection = Start.DistanceTo(other.Start) <= tol && End.DistanceTo(other.End) <= tol; + var reverseDirection = Start.DistanceTo(other.End) <= tol && End.DistanceTo(other.Start) <= tol; + return sameDirection || reverseDirection; + } + } + private sealed class Triangle { public Triangle(Pt a, Pt b, Pt c) From f80ed6f26e283347cfe06e800103e5ed2093f103 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Fri, 6 Mar 2026 16:49:34 -0600 Subject: [PATCH 21/24] DYN-9099 VoronoiDelaunayNodes Fix Code Review In the UvScalingUtilities.cs file I've modified the GetNormalizedUvScales(Surface face) method, basically updated the UV scale computation to use iso-curve lengths on the surface edges and dispose the temporary curves. Also added an extra validation AssertApiEdgesSatisfyEmptyCircumcircle (and other helper methods) in the Delaunay test so now we are verifying empty-circumcircle on triangles reconstructed from the production API edge output. --- .../Tesellation/UvScalingUtilities.cs | 18 ++- .../DelaunayVoronoiOnSurfaceTests.cs | 122 ++++++++++++++++++ 2 files changed, 133 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Tesellation/UvScalingUtilities.cs b/src/Libraries/Tesellation/UvScalingUtilities.cs index 112f11d2e97..8ab39cc9b9d 100644 --- a/src/Libraries/Tesellation/UvScalingUtilities.cs +++ b/src/Libraries/Tesellation/UvScalingUtilities.cs @@ -15,13 +15,17 @@ internal static class UvScalingUtilities /// Tuple containing normalized U and V scale factors. internal static (double normU, double normV) GetNormalizedUvScales(Surface face) { - // Physical scale per unit U/V (affine for planar Rectangle->Surface.ByPatch) - var p00 = face.PointAtParameter(0, 0); - var p10 = face.PointAtParameter(1, 0); - var p01 = face.PointAtParameter(0, 1); - - var scaleU = p00.DistanceTo(p10); - var scaleV = p00.DistanceTo(p01); + // Physical scale per unit U/V based on iso-curve lengths along surface edges. + double scaleU; + double scaleV; + using (var uCurveV0 = face.GetIsoline(0, 0)) + using (var uCurveV1 = face.GetIsoline(0, 1)) + using (var vCurveU0 = face.GetIsoline(1, 0)) + using (var vCurveU1 = face.GetIsoline(1, 1)) + { + scaleU = (uCurveV0.Length + uCurveV1.Length) / 2.0; + scaleV = (vCurveU0.Length + vCurveU1.Length) / 2.0; + } // Normalize scales to keep values in a reasonable range, preserve aspect ratio var max = System.Math.Max(scaleU, scaleV); diff --git a/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs index a6eedbdd1b7..450ccacfd19 100644 --- a/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs +++ b/test/Libraries/TessellationTests/DelaunayVoronoiOnSurfaceTests.cs @@ -32,6 +32,9 @@ public void Delaunay_ByParametersOnSurface_SatisfiesEmptyCircumcircleProperty_Wi var edges = Delaunay.ByParametersOnSurface(uvs, surface).ToList(); Assert.That(edges.Count, Is.GreaterThan(0)); + // Reconstruct triangles from API edges and verify empty circumcircle on production output. + AssertApiEdgesSatisfyEmptyCircumcircle(uvs, surface, edges); + // Validate that the API output matches the edges implied by the Delaunay triangles. AssertDelaunayEdgesMatchTriangulation(triangles, surface, edges); } @@ -281,6 +284,123 @@ void AddExpectedEdge(Pt a, Pt b) } } + private static void AssertApiEdgesSatisfyEmptyCircumcircle( + IReadOnlyList uvs, + Surface face, + IReadOnlyList apiEdges) + { + const double vertexMatchTolerance = 1e-3; + const double edgeLengthTolerance = 1e-9; + var (normU, normV) = UvScalingUtilities.GetNormalizedUvScales(face); + + var uvVertices = new List(uvs.Count); + foreach (var uv in uvs) + { + using var worldPoint = face.PointAtParameter(uv.U, uv.V); + uvVertices.Add(new UvVertex( + new Pt(uv.U * normU, uv.V * normV), + new WorldPoint(worldPoint.X, worldPoint.Y, worldPoint.Z))); + } + + var edgeSet = new HashSet(); + foreach (var edge in apiEdges) + { + using var start = edge.StartPoint; + using var end = edge.EndPoint; + + var startIndex = FindClosestVertexIndex( + uvVertices, + new WorldPoint(start.X, start.Y, start.Z), + vertexMatchTolerance); + var endIndex = FindClosestVertexIndex( + uvVertices, + new WorldPoint(end.X, end.Y, end.Z), + vertexMatchTolerance); + + if (startIndex == endIndex) + continue; + + var a = uvVertices[startIndex].ScaledUv; + var b = uvVertices[endIndex].ScaledUv; + if (a.DistanceTo(b) <= edgeLengthTolerance) + continue; + + edgeSet.Add(new Edge(a, b)); + } + + var triangles = BuildTrianglesFromEdges(edgeSet); + Assert.That(triangles.Count, Is.GreaterThan(0), "Could not reconstruct any triangles from API edges."); + + AssertTrianglesSatisfyEmptyCircumcircle(triangles); + } + + private static List BuildTrianglesFromEdges(HashSet edgeSet) + { + var points = edgeSet + .SelectMany(e => new[] { e.A, e.B }) + .Distinct() + .ToList(); + + var pointIndex = points + .Select((point, index) => (point, index)) + .ToDictionary(x => x.point, x => x.index); + + var adjacency = points.Select(_ => new HashSet()).ToList(); + foreach (var edge in edgeSet) + { + var ia = pointIndex[edge.A]; + var ib = pointIndex[edge.B]; + adjacency[ia].Add(ib); + adjacency[ib].Add(ia); + } + + var triangles = new List(); + for (var i = 0; i < points.Count; i++) + { + for (var j = i + 1; j < points.Count; j++) + { + if (!adjacency[i].Contains(j)) + continue; + + for (var k = j + 1; k < points.Count; k++) + { + if (!adjacency[i].Contains(k) || !adjacency[j].Contains(k)) + continue; + + triangles.Add(new Triangle(points[i], points[j], points[k])); + } + } + } + + return triangles; + } + + private static int FindClosestVertexIndex( + IReadOnlyList vertices, + WorldPoint candidate, + double tolerance) + { + var bestDistance = double.PositiveInfinity; + var bestIndex = -1; + + for (var i = 0; i < vertices.Count; i++) + { + var distance = vertices[i].World.DistanceTo(candidate); + if (distance < bestDistance) + { + bestDistance = distance; + bestIndex = i; + } + } + + if (bestIndex < 0 || bestDistance > tolerance) + { + Assert.Fail($"API edge endpoint could not be mapped to a UV input vertex. bestDistance={bestDistance}"); + } + + return bestIndex; + } + private static void AddUniqueSegment(List segments, WorldSegment candidate, double tol) { if (!segments.Any(existing => existing.Matches(candidate, tol))) @@ -330,6 +450,8 @@ public bool Matches(WorldSegment other, double tol) } } + private readonly record struct UvVertex(Pt ScaledUv, WorldPoint World); + private sealed class Triangle { public Triangle(Pt a, Pt b, Pt c) From eee84f62b108558deab89f4d017e58f630d64b47 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Tue, 10 Mar 2026 16:56:19 -0600 Subject: [PATCH 22/24] Merge Fixes Fixes done after merging master into feature branch --- src/Libraries/Tesellation/Tessellation.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Libraries/Tesellation/Tessellation.csproj b/src/Libraries/Tesellation/Tessellation.csproj index 5eaece2e71e..5df081b13e2 100644 --- a/src/Libraries/Tesellation/Tessellation.csproj +++ b/src/Libraries/Tesellation/Tessellation.csproj @@ -16,10 +16,7 @@ - - - From e3727d69bec93b4a2f16fd7a77d977c9bcc90756 Mon Sep 17 00:00:00 2001 From: Roberto T Date: Tue, 10 Mar 2026 17:07:40 -0600 Subject: [PATCH 23/24] Merge Fixes Fixes done after merging master into feature branch --- test/Libraries/TessellationTests/TessellationTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Libraries/TessellationTests/TessellationTests.csproj b/test/Libraries/TessellationTests/TessellationTests.csproj index 4d5e3ec2475..2e31c7b5ac2 100644 --- a/test/Libraries/TessellationTests/TessellationTests.csproj +++ b/test/Libraries/TessellationTests/TessellationTests.csproj @@ -10,7 +10,7 @@ TessellationTests - + From 951f9952fa13d328aa67995832501b321c6d6eaf Mon Sep 17 00:00:00 2001 From: Roberto T Date: Wed, 11 Mar 2026 08:44:03 -0600 Subject: [PATCH 24/24] DYN-9099-VoronoiDelaunay-CodeReview Removed project reference to Tessellation.csproj due that is not needed --- test/DynamoCoreTests/DynamoCoreTests.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/DynamoCoreTests/DynamoCoreTests.csproj b/test/DynamoCoreTests/DynamoCoreTests.csproj index ce537a05e54..c4bb3f75914 100644 --- a/test/DynamoCoreTests/DynamoCoreTests.csproj +++ b/test/DynamoCoreTests/DynamoCoreTests.csproj @@ -120,11 +120,6 @@ {6cd0f0cf-8199-49f9-b0ea-0b9598b44419} TestServices - - {D6279651-D099-4F8D-A319-5BF12ED9F269} - Tessellation - False -