mirror of
https://git.do.srb2.org/STJr/UltimateZoneBuilder.git
synced 2024-11-24 04:41:10 +00:00
0369c969d1
Draw Curve Mode: added settings panel. Sectors mode: added "Make Door" button to the toolbar. Swapped Side panel and Info panel z-order. Interface: split toolbar into 3 separate toolbars. All toolbar buttons are now viewable at 1024x768. Interface: grouped stuff in "Modes" menu a bit better. Interface: added "Draw [stuff]" buttons to modes toolbar. Interface: reorganized main menu. Hope it makes more sense now. API: added General.Interface.AddModesButton() and General.Interface.AddModesMenu(), which can be used to add buttons to specific group in "Modes" toolbar and menu items to specific group in "Modes" menu, so actions, which behave like an editing mode, but are not part of one can be added there.
969 lines
31 KiB
C#
969 lines
31 KiB
C#
|
|
#region ================== Copyright (c) 2007 Pascal vd Heiden
|
|
|
|
/*
|
|
* Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
|
|
* This program is released under GNU General Public License
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
*/
|
|
|
|
#endregion
|
|
|
|
#region ================== Namespaces
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using CodeImp.DoomBuilder.Map;
|
|
using System.Collections.ObjectModel;
|
|
using CodeImp.DoomBuilder.IO;
|
|
|
|
#endregion
|
|
|
|
namespace CodeImp.DoomBuilder.Geometry
|
|
{
|
|
/// <summary>
|
|
/// Responsible for creating sector polygons.
|
|
/// Performs triangulation of sectors by using ear clipping.
|
|
/// </summary>
|
|
public sealed class Triangulation
|
|
{
|
|
#region ================== Delegates
|
|
|
|
#if DEBUG
|
|
|
|
// For debugging purpose only!
|
|
// These are not called in a release build
|
|
public delegate void ShowPolygon(LinkedList<EarClipVertex> p);
|
|
public delegate void ShowEarClip(EarClipVertex[] found, LinkedList<EarClipVertex> remaining);
|
|
public delegate void ShowRemaining(LinkedList<EarClipVertex> remaining);
|
|
|
|
// For debugging purpose only!
|
|
// These are not called in a release build
|
|
public ShowPolygon OnShowPolygon;
|
|
public ShowEarClip OnShowEarClip;
|
|
public ShowRemaining OnShowRemaining;
|
|
|
|
#endif
|
|
|
|
#endregion
|
|
|
|
#region ================== Constants
|
|
|
|
#endregion
|
|
|
|
#region ================== Variables
|
|
|
|
// Number of vertices per island
|
|
private ReadOnlyCollection<int> islandvertices;
|
|
|
|
// Vertices that result from the triangulation, 3 per triangle.
|
|
private ReadOnlyCollection<Vector2D> vertices;
|
|
|
|
// These sidedefs match with the vertices. If a vertex is not the start
|
|
// along a sidedef, this list contains a null entry for that vertex.
|
|
private ReadOnlyCollection<Sidedef> sidedefs;
|
|
|
|
// Temporary array for the sidedefs deserialization
|
|
private int[] sidedefindices;
|
|
|
|
#endregion
|
|
|
|
#region ================== Properties
|
|
|
|
public ReadOnlyCollection<int> IslandVertices { get { return islandvertices; } }
|
|
public ReadOnlyCollection<Vector2D> Vertices { get { return vertices; } }
|
|
public ReadOnlyCollection<Sidedef> Sidedefs { get { return sidedefs; } }
|
|
|
|
#endregion
|
|
|
|
#region ================== Constructor / Disposer
|
|
|
|
// Constructor
|
|
public static Triangulation Create(Sector sector)
|
|
{
|
|
Triangulation t = new Triangulation();
|
|
t.Triangulate(sector);
|
|
return t;
|
|
}
|
|
|
|
// Constructor
|
|
public Triangulation()
|
|
{
|
|
islandvertices = Array.AsReadOnly<int>(new int[0]);
|
|
vertices = Array.AsReadOnly<Vector2D>(new Vector2D[0]);
|
|
sidedefs = Array.AsReadOnly<Sidedef>(new Sidedef[0]);
|
|
}
|
|
|
|
// This performs the triangulation
|
|
public void Triangulate(Sector s)
|
|
{
|
|
// Initialize
|
|
List<EarClipPolygon> polys;
|
|
List<int> islandslist = new List<int>();
|
|
List<Vector2D> verticeslist = new List<Vector2D>();
|
|
List<Sidedef> sidedefslist = new List<Sidedef>();
|
|
|
|
// We have no destructor
|
|
GC.SuppressFinalize(this);
|
|
|
|
/*
|
|
* This process is divided into several steps:
|
|
*
|
|
* 1) Tracing the sector lines to find clockwise outer polygons
|
|
* and counter-clockwise inner polygons. These are arranged in a
|
|
* polygon tree for the next step.
|
|
*
|
|
* 2) Cutting the inner polygons to make a flat list of only
|
|
* outer polygons.
|
|
*
|
|
* 3) Ear-clipping the polygons to create triangles.
|
|
*
|
|
*/
|
|
|
|
// TRACING
|
|
polys = DoTrace(s);
|
|
|
|
// CUTTING
|
|
DoCutting(polys);
|
|
|
|
// EAR-CLIPPING
|
|
foreach(EarClipPolygon p in polys)
|
|
islandslist.Add(DoEarClip(p, verticeslist, sidedefslist));
|
|
|
|
// Make arrays
|
|
islandvertices = Array.AsReadOnly<int>(islandslist.ToArray());
|
|
vertices = Array.AsReadOnly<Vector2D>(verticeslist.ToArray());
|
|
sidedefs = Array.AsReadOnly<Sidedef>(sidedefslist.ToArray());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== Serialization
|
|
|
|
// Serialize / deserialize
|
|
internal void ReadWrite(IReadWriteStream s)
|
|
{
|
|
if(s.IsWriting)
|
|
{
|
|
s.wInt(islandvertices.Count);
|
|
for(int i = 0; i < islandvertices.Count; i++) s.wInt(islandvertices[i]);
|
|
|
|
s.wInt(vertices.Count);
|
|
for(int i = 0; i < vertices.Count; i++) s.wVector2D(vertices[i]);
|
|
|
|
s.wInt(sidedefs.Count);
|
|
for(int i = 0; i < sidedefs.Count; i++)
|
|
{
|
|
if(sidedefs[i] != null)
|
|
s.wInt(sidedefs[i].SerializedIndex);
|
|
else
|
|
s.wInt(-1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int c;
|
|
|
|
s.rInt(out c);
|
|
int[] islandverticeslist = new int[c];
|
|
for(int i = 0; i < c; i++) s.rInt(out islandverticeslist[i]);
|
|
islandvertices = Array.AsReadOnly<int>(islandverticeslist);
|
|
|
|
s.rInt(out c);
|
|
Vector2D[] verticeslist = new Vector2D[c];
|
|
for(int i = 0; i < c; i++) s.rVector2D(out verticeslist[i]);
|
|
vertices = Array.AsReadOnly<Vector2D>(verticeslist);
|
|
|
|
s.rInt(out c);
|
|
sidedefindices = new int[c];
|
|
for(int i = 0; i < c; i++) s.rInt(out sidedefindices[i]);
|
|
}
|
|
}
|
|
|
|
// After deserialization we need to find the actual sidedefs back
|
|
internal void PostDeserialize(MapSet map)
|
|
{
|
|
// Find our sidedefs
|
|
List<Sidedef> sides = new List<Sidedef>(sidedefindices.Length);
|
|
for(int i = 0; i < sidedefindices.Length; i++)
|
|
{
|
|
if(sidedefindices[i] >= 0)
|
|
sides.Add(map.SidedefIndices[sidedefindices[i]]);
|
|
else
|
|
sides.Add(null);
|
|
}
|
|
|
|
// We don't need this array any longer
|
|
sidedefindices = null;
|
|
|
|
// Keep readonly array
|
|
sidedefs = Array.AsReadOnly<Sidedef>(sides.ToArray());
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region ================== Tracing
|
|
|
|
// This traces sector lines to create a polygon tree
|
|
private List<EarClipPolygon> DoTrace(Sector s)
|
|
{
|
|
Dictionary<Sidedef, bool> todosides = new Dictionary<Sidedef, bool>(s.Sidedefs.Count);
|
|
Dictionary<Vertex, Vertex> ignores = new Dictionary<Vertex,Vertex>();
|
|
List<EarClipPolygon> root = new List<EarClipPolygon>();
|
|
SidedefsTracePath path;
|
|
EarClipPolygon newpoly;
|
|
Vertex start;
|
|
|
|
// Fill the dictionary
|
|
// The bool value is used to indicate lines which has been visited in the trace
|
|
foreach(Sidedef sd in s.Sidedefs) todosides.Add(sd, false);
|
|
|
|
// First remove all sides that refer to the same sector on both sides of the line
|
|
RemoveDoubleSidedefReferences(todosides, s.Sidedefs);
|
|
|
|
// Continue until all sidedefs have been processed
|
|
while(todosides.Count > 0)
|
|
{
|
|
// Reset all visited indicators
|
|
foreach(Sidedef sd in s.Sidedefs) if(todosides.ContainsKey(sd)) todosides[sd] = false;
|
|
|
|
// Find the right-most vertex to start a trace with.
|
|
// This guarantees that we start out with an outer polygon and we just
|
|
// have to check if it is inside a previously found polygon.
|
|
start = FindRightMostVertex(todosides, ignores);
|
|
|
|
// No more possible start vertex found?
|
|
// Then leave with what we have up till now.
|
|
if(start == null) break;
|
|
|
|
// Trace to find a polygon
|
|
path = DoTracePath(new SidedefsTracePath(), start, null, s, todosides);
|
|
|
|
// If tracing is not possible (sector not closed?)
|
|
// then add the start to the ignore list and try again later
|
|
if(path == null)
|
|
{
|
|
// Ignore vertex as start
|
|
ignores.Add(start, start);
|
|
}
|
|
else
|
|
{
|
|
// Remove the sides found in the path
|
|
foreach(Sidedef sd in path) todosides.Remove(sd);
|
|
|
|
// Create the polygon
|
|
newpoly = path.MakePolygon();
|
|
|
|
// Determine where this polygon goes in our tree
|
|
foreach(EarClipPolygon p in root)
|
|
{
|
|
// Insert if it belongs as a child
|
|
if(p.InsertChild(newpoly))
|
|
{
|
|
// Done
|
|
newpoly = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Still not inserted in our tree?
|
|
if(newpoly != null)
|
|
{
|
|
// Then add it at root level as outer polygon
|
|
newpoly.Inner = false;
|
|
root.Add(newpoly);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return result
|
|
return root;
|
|
}
|
|
|
|
// This recursively traces a path
|
|
// Returns the resulting TracePath when the search is complete
|
|
// or returns null when no path found.
|
|
private SidedefsTracePath DoTracePath(SidedefsTracePath history, Vertex fromhere, Vertex findme, Sector sector, Dictionary<Sidedef, bool> sides)
|
|
{
|
|
SidedefsTracePath nextpath;
|
|
SidedefsTracePath result;
|
|
Vertex nextvertex;
|
|
List<Sidedef> allsides;
|
|
|
|
// Found the vertex we are tracing to?
|
|
if(fromhere == findme) return history;
|
|
|
|
// On the first run, findme is null (otherwise the trace would end
|
|
// immeditely when it starts) so set findme here on the first run.
|
|
if(findme == null) findme = fromhere;
|
|
|
|
// Make a list of sides referring to the same sector
|
|
allsides = new List<Sidedef>(fromhere.Linedefs.Count * 2);
|
|
foreach(Linedef l in fromhere.Linedefs)
|
|
{
|
|
// Should we go along the front or back side?
|
|
// This is very important for clockwise polygon orientation!
|
|
if(l.Start == fromhere)
|
|
{
|
|
// Front side of line connected to sector?
|
|
if((l.Front != null) && (l.Front.Sector == sector))
|
|
{
|
|
// Visit here when not visited yet
|
|
if(sides.ContainsKey(l.Front) && !sides[l.Front]) allsides.Add(l.Front);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Back side of line connected to sector?
|
|
if((l.Back != null) && (l.Back.Sector == sector))
|
|
{
|
|
// Visit here when not visited yet
|
|
if(sides.ContainsKey(l.Back) && !sides[l.Back]) allsides.Add(l.Back);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Previous line available?
|
|
if(history.Count > 0)
|
|
{
|
|
// This is done to ensure the tracing works along vertices that are shared by
|
|
// more than 2 lines/sides of the same sector. We must continue tracing along
|
|
// the first next smallest delta angle! This sorts the smallest delta angle to
|
|
// the top of the list.
|
|
SidedefAngleSorter sorter = new SidedefAngleSorter(history[history.Count - 1], fromhere);
|
|
allsides.Sort(sorter);
|
|
}
|
|
|
|
// Go for all lines connected to this vertex
|
|
foreach(Sidedef s in allsides)
|
|
{
|
|
// Mark sidedef as visited and move to next vertex
|
|
sides[s] = true;
|
|
nextpath = new SidedefsTracePath(history, s);
|
|
if(s.Line.Start == fromhere) nextvertex = s.Line.End; else nextvertex = s.Line.Start;
|
|
|
|
result = DoTracePath(nextpath, nextvertex, findme, sector, sides);
|
|
if(result != null) return result;
|
|
}
|
|
|
|
// Nothing found
|
|
return null;
|
|
}
|
|
|
|
// This removes all sidedefs which has a sidedefs on the other side
|
|
// of the same line that refers to the same sector. These are removed
|
|
// because they are useless and make the triangulation inefficient.
|
|
private void RemoveDoubleSidedefReferences(Dictionary<Sidedef, bool> todosides, ICollection<Sidedef> sides)
|
|
{
|
|
// Go for all sides
|
|
foreach(Sidedef sd in sides)
|
|
{
|
|
// Double sided?
|
|
if(sd.Other != null)
|
|
{
|
|
// Referring to the same sector on both sides?
|
|
if(sd.Sector == sd.Other.Sector)
|
|
{
|
|
// Remove this one
|
|
todosides.Remove(sd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This finds the right-most vertex to start tracing with
|
|
private Vertex FindRightMostVertex(Dictionary<Sidedef, bool> sides, Dictionary<Vertex, Vertex> ignores)
|
|
{
|
|
Vertex found = null;
|
|
|
|
// Go for all sides to find the right-most side
|
|
foreach(KeyValuePair<Sidedef, bool> sd in sides)
|
|
{
|
|
// First found?
|
|
if((found == null) && !ignores.ContainsKey(sd.Key.Line.Start)) found = sd.Key.Line.Start;
|
|
if((found == null) && !ignores.ContainsKey(sd.Key.Line.End)) found = sd.Key.Line.End;
|
|
|
|
// Compare?
|
|
if(found != null)
|
|
{
|
|
// Check if more to the right than the previous found
|
|
if((sd.Key.Line.Start.Position.x > found.Position.x) && !ignores.ContainsKey(sd.Key.Line.Start)) found = sd.Key.Line.Start;
|
|
if((sd.Key.Line.End.Position.x > found.Position.x) && !ignores.ContainsKey(sd.Key.Line.End)) found = sd.Key.Line.End;
|
|
}
|
|
}
|
|
|
|
// Return result
|
|
return found;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== Cutting
|
|
|
|
// This cuts into outer polygons to solve inner polygons and make the polygon tree flat
|
|
private void DoCutting(List<EarClipPolygon> polys)
|
|
{
|
|
Queue<EarClipPolygon> todo = new Queue<EarClipPolygon>(polys);
|
|
|
|
// Begin processing outer polygons
|
|
while(todo.Count > 0)
|
|
{
|
|
// Get outer polygon to process
|
|
EarClipPolygon p = todo.Dequeue();
|
|
|
|
// Any inner polygons to work with?
|
|
if(p.Children.Count > 0)
|
|
{
|
|
// Go for all the children
|
|
foreach(EarClipPolygon c in p.Children)
|
|
{
|
|
// The children of the children are outer polygons again,
|
|
// so move them to the root and add for processing
|
|
polys.AddRange(c.Children);
|
|
foreach(EarClipPolygon sc in c.Children) todo.Enqueue(sc);
|
|
|
|
// Remove from inner polygon
|
|
c.Children.Clear();
|
|
}
|
|
|
|
// Now do some cutting on this polygon to merge the inner polygons
|
|
MergeInnerPolys(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
// This takes an outer polygon and a set of inner polygons to start cutting on
|
|
private void MergeInnerPolys(EarClipPolygon p)
|
|
{
|
|
LinkedList<EarClipPolygon> todo = new LinkedList<EarClipPolygon>(p.Children);
|
|
LinkedListNode<EarClipVertex> start;
|
|
LinkedListNode<EarClipPolygon> ip;
|
|
LinkedListNode<EarClipPolygon> found;
|
|
LinkedListNode<EarClipVertex> foundstart;
|
|
|
|
// Continue until no more inner polygons to process
|
|
while(todo.Count > 0)
|
|
{
|
|
// Find the inner polygon with the highest x vertex
|
|
found = null;
|
|
foundstart = null;
|
|
ip = todo.First;
|
|
while(ip != null)
|
|
{
|
|
start = FindRightMostVertex(ip.Value);
|
|
if((foundstart == null) || (start.Value.Position.x > foundstart.Value.Position.x))
|
|
{
|
|
// Found a better start
|
|
found = ip;
|
|
foundstart = start;
|
|
}
|
|
|
|
// Next!
|
|
ip = ip.Next;
|
|
}
|
|
|
|
// Remove from todo list
|
|
todo.Remove(found);
|
|
|
|
// Get cut start and end
|
|
SplitOuterWithInner(foundstart, p);
|
|
}
|
|
|
|
// Remove the children, they should be merged in the polygon by now
|
|
p.Children.Clear();
|
|
}
|
|
|
|
// This finds the right-most vertex in an inner polygon to use for cut startpoint.
|
|
private LinkedListNode<EarClipVertex> FindRightMostVertex(EarClipPolygon p)
|
|
{
|
|
LinkedListNode<EarClipVertex> found = p.First;
|
|
LinkedListNode<EarClipVertex> v = found.Next;
|
|
|
|
// Go for all vertices to find the on with the biggest x value
|
|
while(v != null)
|
|
{
|
|
if(v.Value.Position.x > found.Value.Position.x) found = v;
|
|
v = v.Next;
|
|
}
|
|
|
|
// Return result
|
|
return found;
|
|
}
|
|
|
|
// This finds the cut coordinates and splits the other poly with inner vertices
|
|
private void SplitOuterWithInner(LinkedListNode<EarClipVertex> start, EarClipPolygon p)
|
|
{
|
|
LinkedListNode<EarClipVertex> v1, v2;
|
|
LinkedListNode<EarClipVertex> insertbefore = null;
|
|
float u, ul, bonus, foundu = float.MaxValue;
|
|
Vector2D foundpos = new Vector2D();
|
|
EarClipVertex split;
|
|
|
|
// Create a line from start that goes beyond the right most vertex of p
|
|
LinkedListNode<EarClipVertex> pr = FindRightMostVertex(p);
|
|
float startx = start.Value.Position.x;
|
|
float endx = pr.Value.Position.x + 10.0f;
|
|
Line2D starttoright = new Line2D(start.Value.Position, new Vector2D(endx, start.Value.Position.y));
|
|
|
|
// Calculate a small bonus (0.1 mappixel)
|
|
bonus = starttoright.GetNearestOnLine(new Vector2D(start.Value.Position.x + 0.1f, start.Value.Position.y));
|
|
|
|
// Go for all lines in the outer polygon
|
|
v1 = p.Last;
|
|
v2 = p.First;
|
|
while(v2 != null)
|
|
{
|
|
// Check if the line goes between startx and endx
|
|
if(((v1.Value.Position.x > startx) ||
|
|
(v2.Value.Position.x > startx)) &&
|
|
((v1.Value.Position.x < endx) ||
|
|
(v2.Value.Position.x < endx)))
|
|
{
|
|
// Find intersection
|
|
Line2D pl = new Line2D(v1.Value.Position, v2.Value.Position);
|
|
pl.GetIntersection(starttoright, out u, out ul);
|
|
if(float.IsNaN(u))
|
|
{
|
|
// We have found a line that is perfectly horizontal
|
|
// (parallel to the cut scan line) Check if the line
|
|
// is overlapping the cut scan line.
|
|
if(v1.Value.Position.y == start.Value.Position.y)
|
|
{
|
|
// This is an exceptional situation which causes a bit of a problem, because
|
|
// this could be a previously made cut, which overlaps another line from the
|
|
// same cut and we have to determine which of the two we will join with. If we
|
|
// pick the wrong one, the polygon is no longer valid and triangulation will fail.
|
|
|
|
// Calculate distance of each vertex in units
|
|
u = starttoright.GetNearestOnLine(v1.Value.Position);
|
|
ul = starttoright.GetNearestOnLine(v2.Value.Position);
|
|
|
|
// Rule out vertices before the scan line
|
|
if(u < 0.0f) u = float.MaxValue;
|
|
if(ul < 0.0f) ul = float.MaxValue;
|
|
|
|
float insert_u = Math.Min(u, ul);
|
|
Vector2D inserpos = starttoright.GetCoordinatesAt(insert_u);
|
|
|
|
// Check in which direction the line goes.
|
|
if(v1.Value.Position.x > v2.Value.Position.x)
|
|
{
|
|
// The line goes from right to left (towards our start point)
|
|
// so we must always insert our cut after this line.
|
|
|
|
// If the next line goes up, we consider this a better candidate than
|
|
// a horizontal line that goes from left to right (the other cut line)
|
|
// so we give it a small bonus.
|
|
LinkedListNode<EarClipVertex> v3 = v2.Next ?? v2.List.First;
|
|
if(v3.Value.Position.y < v2.Value.Position.y)
|
|
insert_u -= bonus;
|
|
|
|
// Remember this when it is a closer match
|
|
if(insert_u <= foundu)
|
|
{
|
|
insertbefore = v2.Next ?? v2.List.First;
|
|
foundu = insert_u;
|
|
foundpos = inserpos;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The line goes from left to right (away from our start point)
|
|
// so we must always insert our cut before this line.
|
|
|
|
// If the previous line goes down, we consider this a better candidate than
|
|
// a horizontal line that goes from right to left (the other cut line)
|
|
// so we give it a small bonus.
|
|
LinkedListNode<EarClipVertex> v3 = v1.Previous ?? v1.List.Last;
|
|
if(v3.Value.Position.y > v1.Value.Position.y)
|
|
insert_u -= bonus;
|
|
|
|
// Remember this when it is a closer match
|
|
if(insert_u <= foundu)
|
|
{
|
|
insertbefore = v2;
|
|
foundu = insert_u;
|
|
foundpos = inserpos;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Found a closer match?
|
|
else if((ul >= 0.0f) && (ul <= 1.0f) && (u > 0.0f) && (u <= foundu))
|
|
{
|
|
// Found a closer intersection
|
|
insertbefore = v2;
|
|
foundu = u;
|
|
foundpos = starttoright.GetCoordinatesAt(u);
|
|
}
|
|
}
|
|
|
|
// Next
|
|
v1 = v2;
|
|
v2 = v2.Next;
|
|
}
|
|
|
|
// Found anything?
|
|
if(insertbefore != null)
|
|
{
|
|
Sidedef sd = (insertbefore.Previous == null) ? insertbefore.List.Last.Value.Sidedef : insertbefore.Previous.Value.Sidedef;
|
|
|
|
// Find the position where we have to split the outer polygon
|
|
split = new EarClipVertex(foundpos, null);
|
|
|
|
// Insert manual split vertices
|
|
p.AddBefore(insertbefore, new EarClipVertex(split, sd));
|
|
|
|
// Start inserting from the start (do I make sense this time?)
|
|
v1 = start;
|
|
do
|
|
{
|
|
// Insert inner polygon vertex
|
|
p.AddBefore(insertbefore, new EarClipVertex(v1.Value));
|
|
if(v1.Next != null) v1 = v1.Next; else v1 = v1.List.First;
|
|
}
|
|
while(v1 != start);
|
|
|
|
// Insert manual split vertices
|
|
p.AddBefore(insertbefore, new EarClipVertex(start.Value, sd));
|
|
if(split.Position != insertbefore.Value.Position)
|
|
p.AddBefore(insertbefore, new EarClipVertex(split, sd));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== Ear Clipping
|
|
|
|
// This clips a polygon and returns the triangles
|
|
// The polygon may not have any holes or islands
|
|
// See: http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
|
|
private int DoEarClip(EarClipPolygon poly, List<Vector2D> verticeslist, List<Sidedef> sidedefslist)
|
|
{
|
|
LinkedList<EarClipVertex> verts = new LinkedList<EarClipVertex>();
|
|
List<EarClipVertex> convexes = new List<EarClipVertex>(poly.Count);
|
|
LinkedList<EarClipVertex> reflexes = new LinkedList<EarClipVertex>();
|
|
LinkedList<EarClipVertex> eartips = new LinkedList<EarClipVertex>();
|
|
LinkedListNode<EarClipVertex> n1, n2;
|
|
EarClipVertex v, v1, v2;
|
|
EarClipVertex[] t, t1, t2;
|
|
int countvertices = 0;
|
|
|
|
// Go for all vertices to fill list
|
|
foreach(EarClipVertex vec in poly)
|
|
vec.SetVertsLink(verts.AddLast(vec));
|
|
|
|
// Remove any zero-length lines, these will give problems
|
|
n1 = verts.First;
|
|
do
|
|
{
|
|
// Continue until adjacent zero-length lines are removed
|
|
n2 = n1.Next ?? verts.First;
|
|
Vector2D d = n1.Value.Position - n2.Value.Position;
|
|
while((Math.Abs(d.x) < 0.00001f) && (Math.Abs(d.y) < 0.00001f))
|
|
{
|
|
n2.Value.Remove();
|
|
n2 = n1.Next ?? verts.First;
|
|
if(n2 != null) d = n1.Value.Position - n2.Value.Position; else break;
|
|
}
|
|
|
|
// Next!
|
|
n1 = n2;
|
|
}
|
|
while(n1 != verts.First);
|
|
|
|
// Optimization: Vertices which have lines with the
|
|
// same angle are useless. Remove them!
|
|
n1 = verts.First;
|
|
while(n1 != null)
|
|
{
|
|
// Get the next vertex
|
|
n2 = n1.Next;
|
|
|
|
// Get triangle for v
|
|
t = GetTriangle(n1.Value);
|
|
|
|
// Check if both lines have the same angle
|
|
Line2D a = new Line2D(t[0].Position, t[1].Position);
|
|
Line2D b = new Line2D(t[1].Position, t[2].Position);
|
|
if(Math.Abs(Angle2D.Difference(a.GetAngle(), b.GetAngle())) < 0.00001f)
|
|
{
|
|
// Same angles, remove vertex
|
|
n1.Value.Remove();
|
|
}
|
|
|
|
// Next!
|
|
n1 = n2;
|
|
}
|
|
|
|
// Go for all vertices to determine reflex or convex
|
|
foreach(EarClipVertex vv in verts)
|
|
{
|
|
// Add to reflex or convex list
|
|
if(IsReflex(GetTriangle(vv))) vv.AddReflex(reflexes); else convexes.Add(vv);
|
|
}
|
|
|
|
// Go for all convex vertices to see if they are ear tips
|
|
foreach(EarClipVertex cv in convexes)
|
|
{
|
|
// Add when this is a valid ear
|
|
t = GetTriangle(cv);
|
|
if(CheckValidEar(t, reflexes)) cv.AddEarTip(eartips);
|
|
}
|
|
|
|
#if DEBUG
|
|
if(OnShowPolygon != null) OnShowPolygon(verts);
|
|
#endif
|
|
|
|
// Process ears until done
|
|
while((eartips.Count > 0) && (verts.Count > 2))
|
|
{
|
|
// Get next ear
|
|
v = eartips.First.Value;
|
|
t = GetTriangle(v);
|
|
|
|
// Only save this triangle when it has an area
|
|
if(TriangleHasArea(t))
|
|
{
|
|
// Add ear as triangle
|
|
AddTriangleToList(t, verticeslist, sidedefslist, (verts.Count == 3));
|
|
countvertices += 3;
|
|
}
|
|
|
|
// Remove this ear from all lists
|
|
v.Remove();
|
|
v1 = t[0];
|
|
v2 = t[2];
|
|
|
|
#if DEBUG
|
|
if(TriangleHasArea(t))
|
|
{
|
|
if(OnShowEarClip != null) OnShowEarClip(t, verts);
|
|
}
|
|
#endif
|
|
|
|
// Test first neighbour
|
|
t1 = GetTriangle(v1);
|
|
bool t1a = true; //TriangleHasArea(t1);
|
|
if(t1a && IsReflex(t1))
|
|
{
|
|
// List as reflex if not listed yet
|
|
if(!v1.IsReflex) v1.AddReflex(reflexes);
|
|
v1.RemoveEarTip();
|
|
}
|
|
else
|
|
{
|
|
// Remove from reflexes
|
|
v1.RemoveReflex();
|
|
}
|
|
|
|
// Test second neighbour
|
|
t2 = GetTriangle(v2);
|
|
bool t2a = true; //TriangleHasArea(t2);
|
|
if(t2a && IsReflex(t2))
|
|
{
|
|
// List as reflex if not listed yet
|
|
if(!v2.IsReflex) v2.AddReflex(reflexes);
|
|
v2.RemoveEarTip();
|
|
}
|
|
else
|
|
{
|
|
// Remove from reflexes
|
|
v2.RemoveReflex();
|
|
}
|
|
|
|
// Check if any neightbour have become a valid or invalid ear
|
|
if(!v1.IsReflex && (!t1a || CheckValidEar(t1, reflexes))) v1.AddEarTip(eartips); else v1.RemoveEarTip();
|
|
if(!v2.IsReflex && (!t2a || CheckValidEar(t2, reflexes))) v2.AddEarTip(eartips); else v2.RemoveEarTip();
|
|
}
|
|
|
|
#if DEBUG
|
|
if(OnShowRemaining != null) OnShowRemaining(verts);
|
|
#endif
|
|
|
|
// Dispose remaining vertices
|
|
foreach(EarClipVertex ecv in verts) ecv.Dispose();
|
|
|
|
// Return the number of vertices in the result
|
|
return countvertices;
|
|
}
|
|
|
|
// This checks if a given ear is a valid (no intersections from reflex vertices)
|
|
private bool CheckValidEar(EarClipVertex[] t, LinkedList<EarClipVertex> reflexes)
|
|
{
|
|
// Go for all reflex vertices
|
|
foreach(EarClipVertex rv in reflexes)
|
|
{
|
|
// Not one of the triangle corners?
|
|
if((rv.Position != t[0].Position) && (rv.Position != t[1].Position) && (rv.Position != t[2].Position))
|
|
{
|
|
// Return false on intersection
|
|
if(PointInsideTriangle(t, rv.MainListNode)) return false;
|
|
}
|
|
}
|
|
|
|
// Valid ear!
|
|
return true;
|
|
}
|
|
|
|
// This returns the 3-vertex array triangle for an ear
|
|
private EarClipVertex[] GetTriangle(EarClipVertex v)
|
|
{
|
|
EarClipVertex[] t = new EarClipVertex[3];
|
|
t[0] = (v.MainListNode.Previous == null) ? v.MainListNode.List.Last.Value : v.MainListNode.Previous.Value;
|
|
t[1] = v;
|
|
t[2] = (v.MainListNode.Next == null) ? v.MainListNode.List.First.Value : v.MainListNode.Next.Value;
|
|
return t;
|
|
}
|
|
|
|
// This checks if a vertex is reflex (corner > 180 deg) or convex (corner < 180 deg)
|
|
private bool IsReflex(EarClipVertex[] t)
|
|
{
|
|
// Return true when corner is > 180 deg
|
|
return (Line2D.GetSideOfLine(t[0].Position, t[2].Position, t[1].Position) < 0.0f);
|
|
}
|
|
|
|
// This checks if a point is inside a triangle
|
|
// When the point is on an edge of the triangle, it depends on the lines
|
|
// adjacent to the point if it is considered inside or not
|
|
// NOTE: vertices in t must be in clockwise order!
|
|
private bool PointInsideTriangle(EarClipVertex[] t, LinkedListNode<EarClipVertex> p)
|
|
{
|
|
// If the triangle has no area, there can never be a point inside
|
|
if(TriangleHasArea(t))
|
|
{
|
|
float lineside01 = Line2D.GetSideOfLine(t[0].Position, t[1].Position, p.Value.Position);
|
|
float lineside12 = Line2D.GetSideOfLine(t[1].Position, t[2].Position, p.Value.Position);
|
|
float lineside20 = Line2D.GetSideOfLine(t[2].Position, t[0].Position, p.Value.Position);
|
|
float u_on_line = 0.5f;
|
|
|
|
// If point p is on the line of an edge, find out where on the edge segment p is.
|
|
if(lineside01 == 0.0f)
|
|
u_on_line = Line2D.GetNearestOnLine(t[0].Position, t[1].Position, p.Value.Position);
|
|
else if(lineside12 == 0.0f)
|
|
u_on_line = Line2D.GetNearestOnLine(t[1].Position, t[2].Position, p.Value.Position);
|
|
else if(lineside20 == 0.0f)
|
|
u_on_line = Line2D.GetNearestOnLine(t[2].Position, t[0].Position, p.Value.Position);
|
|
|
|
// If any of the lineside results are 0 then that means the point p lies on that edge and we
|
|
// need to test if the lines adjacent to the point p are in the triangle or not.
|
|
// If the lines are intersecting the triangle, we also consider the point inside.
|
|
if((lineside01 == 0.0f) || (lineside12 == 0.0f) || (lineside20 == 0.0f))
|
|
{
|
|
// When the point p is outside the edge segment, then it is not inside the triangle
|
|
if((u_on_line < 0.0f) || (u_on_line > 1.0f))
|
|
return false;
|
|
|
|
// Point p is on an edge segment. We'll have to decide by it's lines if we call it inside or outside the triangle.
|
|
LinkedListNode<EarClipVertex> p1 = p.Previous ?? p.List.Last;
|
|
LinkedListNode<EarClipVertex> p2 = p.Next ?? p.List.First;
|
|
if(LineInsideTriangle(t, p.Value.Position, p1.Value.Position)) return true;
|
|
if(LineInsideTriangle(t, p.Value.Position, p2.Value.Position)) return true;
|
|
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return (lineside01 < 0.0f) && (lineside12 < 0.0f) && (lineside20 < 0.0f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// This checks if a line is inside a triangle (touching the triangle is allowed)
|
|
// NOTE: We already know p1 is on an edge segment of the triangle
|
|
private bool LineInsideTriangle(EarClipVertex[] t, Vector2D p1, Vector2D p2)
|
|
{
|
|
float s01 = Line2D.GetSideOfLine(t[0].Position, t[1].Position, p2);
|
|
float s12 = Line2D.GetSideOfLine(t[1].Position, t[2].Position, p2);
|
|
float s20 = Line2D.GetSideOfLine(t[2].Position, t[0].Position, p2);
|
|
float p2_on_edge = 2.0f; // somewhere outside the 0 .. 1 range
|
|
float p1_on_same_edge = 2.0f;
|
|
|
|
// Test if p2 is inside the triangle
|
|
if((s01 < 0.0f) && (s12 < 0.0f) && (s20 < 0.0f))
|
|
{
|
|
// Line is inside triangle, because p2 is
|
|
return true;
|
|
}
|
|
// Test if p2 is on an edge of the triangle and if it is we would
|
|
// like to know where on the edge segment p2 is
|
|
else if(s01 == 0.0f)
|
|
{
|
|
p2_on_edge = Line2D.GetNearestOnLine(t[0].Position, t[1].Position, p2);
|
|
p1_on_same_edge = Line2D.GetSideOfLine(t[0].Position, t[1].Position, p1);
|
|
}
|
|
else if(s12 == 0.0f)
|
|
{
|
|
p2_on_edge = Line2D.GetNearestOnLine(t[1].Position, t[2].Position, p2);
|
|
p1_on_same_edge = Line2D.GetSideOfLine(t[1].Position, t[2].Position, p1);
|
|
}
|
|
else if(s20 == 0.0f)
|
|
{
|
|
p2_on_edge = Line2D.GetNearestOnLine(t[2].Position, t[0].Position, p2);
|
|
p1_on_same_edge = Line2D.GetSideOfLine(t[2].Position, t[0].Position, p1);
|
|
}
|
|
|
|
// Is p2 actually on the edge segment?
|
|
if((p2_on_edge >= 0.0f) && (p2_on_edge <= 1.0f))
|
|
{
|
|
// If p1 is on the same edge (or the unlimited line of that edge)
|
|
// then the line is not inside this triangle.
|
|
if(p1_on_same_edge == 0.0f)
|
|
return false;
|
|
}
|
|
|
|
// Do a complete line-triangle intersection test
|
|
// We already know p1 is not inside the triangle (possibly on an edge)
|
|
Line2D p = new Line2D(p1, p2);
|
|
Line2D t01 = new Line2D(t[0].Position, t[1].Position);
|
|
Line2D t12 = new Line2D(t[1].Position, t[2].Position);
|
|
Line2D t20 = new Line2D(t[2].Position, t[0].Position);
|
|
float pu, pt;
|
|
|
|
// Test intersections
|
|
t01.GetIntersection(p, out pu, out pt);
|
|
if(!float.IsNaN(pu) && (pu >= 0.0f) && (pu <= 1.0f) && (pt >= 0.0f) && (pt <= 1.0f)) return true;
|
|
t12.GetIntersection(p, out pu, out pt);
|
|
if(!float.IsNaN(pu) && (pu >= 0.0f) && (pu <= 1.0f) && (pt >= 0.0f) && (pt <= 1.0f)) return true;
|
|
t20.GetIntersection(p, out pu, out pt);
|
|
if(!float.IsNaN(pu) && (pu >= 0.0f) && (pu <= 1.0f) && (pt >= 0.0f) && (pt <= 1.0f)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// This checks if the triangle has an area greater than 0
|
|
private bool TriangleHasArea(EarClipVertex[] t)
|
|
{
|
|
return ((t[0].Position.x * (t[1].Position.y - t[2].Position.y) +
|
|
t[1].Position.x * (t[2].Position.y - t[0].Position.y) +
|
|
t[2].Position.x * (t[0].Position.y - t[1].Position.y)) != 0.0f);
|
|
}
|
|
|
|
// This adds an array of vertices
|
|
private void AddTriangleToList(EarClipVertex[] triangle, List<Vector2D> verticeslist, List<Sidedef> sidedefslist, bool last)
|
|
{
|
|
// Create triangle
|
|
verticeslist.Add(triangle[0].Position);
|
|
sidedefslist.Add(triangle[0].Sidedef);
|
|
verticeslist.Add(triangle[1].Position);
|
|
sidedefslist.Add(triangle[1].Sidedef);
|
|
verticeslist.Add(triangle[2].Position);
|
|
if(!last) sidedefslist.Add(null); else sidedefslist.Add(triangle[2].Sidedef);
|
|
|
|
// Modify the first earclipvertex of this triangle, it no longer lies along a sidedef
|
|
triangle[0].Sidedef = null;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|