Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 95 additions & 3 deletions Scripts/ShapeEditor/Decomposition/PolyBoolCS/PolyboolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,18 @@ public static List<Polygon> ToPolygons(this PolyboolPolygon poly, PolyBool polyB

var roots = newNode(null);

// add all regions to the root
// add all regions to the root. PolyBool can return a weakly-simple
// region when two filled areas touch at a single point. Split those
// regions before building the hierarchy so convex decomposition only
// receives simple polygons.
for (var i = 0; i < poly.regions.Count; i++)
{
var region = poly.regions[i];
if (region.Count < 3) // regions must have at least 3 points (sanity check)
continue;
addChild(roots, region);

foreach (var simpleRegion in splitSingularities(region))
addChild(roots, simpleRegion);
}

// with our heirarchy, we can distinguish between exterior borders, and interior holes
Expand Down Expand Up @@ -80,6 +85,93 @@ public static List<Polygon> ToPolygons(this PolyboolPolygon poly, PolyBool polyB
return sePolys;
}

private static List<Region> splitSingularities(Region region)
{
var regions = new List<Region>() { region };

for (var i = 0; i < regions.Count;)
{
if (trySplitSingularity(regions[i], out var first, out var second))
{
regions[i] = first;
regions.Insert(i + 1, second);
continue;
}

i++;
}

return regions;
}

private static bool trySplitSingularity(Region region, out Region first, out Region second)
{
first = null;
second = null;

var count = region.Count;
for (var singularityIndex = 0; singularityIndex < count; singularityIndex++)
{
var singularity = region[singularityIndex];

for (var edgeStartIndex = 0; edgeStartIndex < count; edgeStartIndex++)
{
var edgeEndIndex = (edgeStartIndex + 1) % count;
if (edgeStartIndex == singularityIndex || edgeEndIndex == singularityIndex)
continue;

var edgeStart = region[edgeStartIndex];
var edgeEnd = region[edgeEndIndex];
if (!Epsilon.pointsCollinear(edgeStart, singularity, edgeEnd) ||
!Epsilon.pointBetween(singularity, edgeStart, edgeEnd))
{
continue;
}

var firstCount = 2 + CountPathVertices(singularityIndex, edgeStartIndex, count);
var secondCount = 2 + CountPathVertices(edgeEndIndex, singularityIndex, count);
if (firstCount < 3 || secondCount < 3)
continue;

first = new Region(firstCount);
first.Add(singularity);
AddPathVertices(first, region, singularityIndex, edgeStartIndex);

second = new Region(secondCount);
second.Add(singularity);
second.Add(edgeEnd);
AddPathVerticesBeforeEnd(second, region, edgeEndIndex, singularityIndex);
return true;
}
}

return false;
}

private static int CountPathVertices(int startIndex, int endIndex, int count)
{
var result = 0;
for (var index = (startIndex + 1) % count; index != endIndex; index = (index + 1) % count)
result++;
return result;
}

private static void AddPathVertices(Region destination, Region source, int startIndex, int endIndex)
{
var count = source.Count;
for (var index = (startIndex + 1) % count; index != endIndex; index = (index + 1) % count)
destination.Add(source[index]);

destination.Add(source[endIndex]);
}

private static void AddPathVerticesBeforeEnd(Region destination, Region source, int startIndex, int endIndex)
{
var count = source.Count;
for (var index = (startIndex + 1) % count; index != endIndex; index = (index + 1) % count)
destination.Add(source[index]);
}

// test if r1 is inside r2
private static bool regionInsideRegion(Region r1, Region r2)
{
Expand Down Expand Up @@ -187,4 +279,4 @@ private static Region getInterior(MyNode node, List<Region> polygons)
}
}

#endif
#endif
8 changes: 8 additions & 0 deletions Tests.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Tests/Editor.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions Tests/Editor/PolyboolExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#if UNITY_EDITOR

using AeternumGames.ShapeEditor.PolyBoolCS;
using NUnit.Framework;
using System.Collections.Generic;
using UnityEngine.TestTools;
using PolyboolPolygon = AeternumGames.ShapeEditor.PolyBoolCS.Polygon;
using Region = AeternumGames.ShapeEditor.PolyBoolCS.PointList;

namespace AeternumGames.ShapeEditor.Tests
{
public class PolyboolExtensionsTests
{
[Test]
public void ToPolygons_VertexOnNonAdjacentEdge_SplitsIntoSimpleRegions()
{
var polyBool = new PolyBool();
var singularPolygon = new PolyboolPolygon
{
regions = new List<Region>
{
new Region
{
new Point(-0.5, 0.5),
new Point(-0.5, -0.5),
new Point(0.0, -0.5),
new Point(-0.5, 0.0),
new Point(0.0, 0.5)
}
}
};

var polygons = singularPolygon.ToPolygons(polyBool);

Assert.That(polygons, Has.Count.EqualTo(2));

var totalArea = 0.0f;
foreach (var polygon in polygons)
{
Assert.That(polygon, Has.Count.EqualTo(3));
Assert.That(polygon.IsCounterClockWise2D(), Is.True);
totalArea += polygon.GetSignedArea2D();
}

Assert.That(totalArea, Is.EqualTo(0.25f).Within(0.000001f));
}

[Test]
public void GenerateConvexPolygons_PointTouchingShapes_DoesNotLogAndReturnsFourTriangles()
{
var project = new Project
{
shapes = new List<Shape>
{
CreateTriangle(-0.5f, -0.5f, 0.0f, -0.5f, -0.5f, 0.0f),
CreateTriangle(0.0f, -0.5f, 0.5f, -0.5f, 0.5f, 0.0f),
CreateTriangle(0.5f, 0.0f, 0.5f, 0.5f, 0.0f, 0.5f),
CreateTriangle(-0.5f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f)
}
};
project.Validate();

var polygons = project.GenerateConvexPolygons();

Assert.That(polygons, Has.Count.EqualTo(4));
foreach (var polygon in polygons)
{
Assert.That(polygon, Has.Count.EqualTo(3));
Assert.That(polygon.IsCounterClockWise2D(), Is.True);
}

LogAssert.NoUnexpectedReceived();
}

private static Shape CreateTriangle(
float x1, float y1,
float x2, float y2,
float x3, float y3)
{
var shape = new Shape();
shape.segments.Clear();
shape.AddSegment(new Segment(shape, x1, y1));
shape.AddSegment(new Segment(shape, x2, y2));
shape.AddSegment(new Segment(shape, x3, y3));
return shape;
}
}
}

#endif
2 changes: 2 additions & 0 deletions Tests/Editor/PolyboolExtensionsTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions Tests/Editor/com.aeternumgames.shapeeditor.tests.editor.asmdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "com.aeternumgames.shapeeditor.tests.editor",
"references": [
"com.aeternumgames.shapeeditor"
],
"optionalUnityReferences": [
"TestAssemblies"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.