#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; using System.Collections.Generic; using System.Globalization; using System.Text; using CodeImp.DoomBuilder.Geometry; using CodeImp.DoomBuilder.Rendering; using SlimDX.Direct3D9; using System.Drawing; using CodeImp.DoomBuilder.Map; using CodeImp.DoomBuilder.IO; #endregion namespace CodeImp.DoomBuilder.Geometry { /// /// Tools to work with geometry. /// public static class Tools { #region ================== Structures private struct SidedefSettings { public string newtexhigh; public string newtexmid; public string newtexlow; } #endregion #region ================== Constants #endregion #region ================== Polygons and Triangles // Point inside the polygon? // See: http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/ public static bool PointInPolygon(ICollection polygon, Vector2D point) { Vector2D v1 = General.GetByIndex(polygon, polygon.Count - 1); uint c = 0; // Go for all vertices foreach(Vector2D v2 in polygon) { // Determine min/max values float miny = Math.Min(v1.y, v2.y); float maxy = Math.Max(v1.y, v2.y); float maxx = Math.Max(v1.x, v2.x); // Check for intersection if((point.y > miny) && (point.y <= maxy)) { if(point.x <= maxx) { if(v1.y != v2.y) { float xint = (point.y - v1.y) * (v2.x - v1.x) / (v2.y - v1.y) + v1.x; if((v1.x == v2.x) || (point.x <= xint)) c++; } } } // Move to next v1 = v2; } // Inside this polygon? return (c & 0x00000001UL) != 0; } #endregion #region ================== Pathfinding /// /// This finds a potential sector at the given coordinates, /// or returns null when a sector is not possible there. /// public static List FindPotentialSectorAt(Vector2D pos) { // Find the nearest line and determine side, then use the other method to create the sector Linedef l = General.Map.Map.NearestLinedef(pos); return FindPotentialSectorAt(l, (l.SideOfLine(pos) <= 0)); } /// /// This finds a potential sector starting at the given line and side, /// or returns null when sector is not possible. /// public static List FindPotentialSectorAt(Linedef line, bool front) { List alllines = new List(); // Find the outer lines EarClipPolygon p = FindOuterLines(line, front, alllines); if(p != null) { // Find the inner lines FindInnerLines(p, alllines); return alllines; } else return null; } // This finds the inner lines of the sector and adds them to the sector polygon private static void FindInnerLines(EarClipPolygon p, List alllines) { Vertex foundv; bool vvalid, findmore; Linedef foundline; float foundangle = 0f; bool foundlinefront; do { findmore = false; // Go for all vertices to find the right-most vertex inside the polygon foundv = null; foreach(Vertex v in General.Map.Map.Vertices) { // More to the right? if((foundv == null) || (v.Position.x >= foundv.Position.x)) { // Vertex is inside the polygon? if(p.Intersect(v.Position)) { // Vertex has lines attached? if(v.Linedefs.Count > 0) { // Go for all lines to see if the vertex is not of the polygon itsself vvalid = true; foreach(LinedefSide ls in alllines) { if((ls.Line.Start == v) || (ls.Line.End == v)) { vvalid = false; break; } } // Valid vertex? if(vvalid) foundv = v; } } } } // Found a vertex inside the polygon? if(foundv != null) { // Find the attached linedef with the smallest angle to the right float targetangle = Angle2D.PIHALF; foundline = null; foreach(Linedef l in foundv.Linedefs) { // We need an angle unrelated to line direction, so correct for that float lineangle = l.Angle; if(l.End == foundv) lineangle += Angle2D.PI; // Better result? float deltaangle = Angle2D.Difference(targetangle, lineangle); if((foundline == null) || (deltaangle < foundangle)) { foundline = l; foundangle = deltaangle; } } // We already know that each linedef will go from this vertex // to the left, because this is the right-most vertex in this area. // If the line would go to the right, that means the other vertex of // that line must lie outside this area and the mapper made an error. // Should I check for this error and fail to create a sector in // that case or ignore it and create a malformed sector (possibly // breaking another sector also)? // Find the side at which to start pathfinding Vector2D testpos = new Vector2D(100.0f, 0.0f); foundlinefront = (foundline.SideOfLine(foundv.Position + testpos) < 0.0f); // Find inner path List innerlines = FindClosestPath(foundline, foundlinefront, true); if(innerlines != null) { // Make polygon LinedefTracePath tracepath = new LinedefTracePath(innerlines); EarClipPolygon innerpoly = tracepath.MakePolygon(true); // Check if the front of the line is outside the polygon if(!innerpoly.Intersect(foundline.GetSidePoint(foundlinefront))) { // Valid hole found! alllines.AddRange(innerlines); p.InsertChild(innerpoly); findmore = true; } } } } // Continue until no more holes found while(findmore); } // This finds the outer lines of the sector as a polygon // Returns null when no valid outer polygon can be found private static EarClipPolygon FindOuterLines(Linedef line, bool front, List alllines) { Linedef scanline = line; bool scanfront = front; do { // Find closest path List pathlines = FindClosestPath(scanline, scanfront, true); if(pathlines != null) { // Make polygon LinedefTracePath tracepath = new LinedefTracePath(pathlines); EarClipPolygon poly = tracepath.MakePolygon(true); // Check if the front of the line is inside the polygon if(poly.Intersect(line.GetSidePoint(front))) { // Outer lines found! alllines.AddRange(pathlines); return poly; } else { // Inner lines found. This is not what we need, we want the outer lines. // Find the right-most vertex to start a scan from there towards the outer lines. Vertex foundv = null; foreach(LinedefSide ls in pathlines) { if((foundv == null) || (ls.Line.Start.Position.x > foundv.Position.x)) foundv = ls.Line.Start; if((foundv == null) || (ls.Line.End.Position.x > foundv.Position.x)) foundv = ls.Line.End; } // If foundv is null then something is horribly wrong with the // path we received from FindClosestPath! if(foundv == null) throw new Exception("FAIL!"); // From the right-most vertex trace outward to the right to // find the next closest linedef, this is based on the idea that // all sectors are closed. Vector2D lineoffset = new Vector2D(100.0f, 0.0f); Line2D testline = new Line2D(foundv.Position, foundv.Position + lineoffset); scanline = null; float foundu = float.MaxValue; foreach(Linedef ld in General.Map.Map.Linedefs) { // Line to the right of start point? if((ld.Start.Position.x > foundv.Position.x) || (ld.End.Position.x > foundv.Position.x)) { // Line intersecting the y axis? if( !((ld.Start.Position.y > foundv.Position.y) && (ld.End.Position.y > foundv.Position.y)) && !((ld.Start.Position.y < foundv.Position.y) && (ld.End.Position.y < foundv.Position.y))) { // Check if this linedef intersects our test line at a closer range float thisu; ld.Line.GetIntersection(testline, out thisu); if((thisu > 0.00001f) && (thisu < foundu) && !float.IsNaN(thisu)) { scanline = ld; foundu = thisu; } } } } // Did we meet another line? if(scanline != null) { // Determine on which side we should start the next pathfind scanfront = (scanline.SideOfLine(foundv.Position) < 0.0f); } else { // Appearently we reached the end of the map, no sector possible here return null; } } } else { // Can't find a path return null; } } while(true); } /// /// This finds the closest path from one vertex to another. /// When turnatends is true, the algorithm will continue at the other side of the /// line when a dead end has been reached. Returns null when no path could be found. /// //public static List FindClosestPath(Vertex start, float startangle, Vertex end, bool turnatends) //{ //} /// /// This finds the closest path from the beginning of a line to the end of the line. /// When turnatends is true, the algorithm will continue at the other side of the /// line when a dead end has been reached. Returns null when no path could be found. /// public static List FindClosestPath(Linedef startline, bool startfront, bool turnatends) { return FindClosestPath(startline, startfront, startline, startfront, turnatends); } /// /// This finds the closest path from the beginning of a line to the end of the line. /// When turnatends is true, the algorithm will continue at the other side of the /// line when a dead end has been reached. Returns null when no path could be found. /// public static List FindClosestPath(Linedef startline, bool startfront, Linedef endline, bool endfront, bool turnatends) { List path = new List(); Dictionary tracecount = new Dictionary(); Linedef nextline = startline; bool nextfront = startfront; do { // Add line to path path.Add(new LinedefSide(nextline, nextfront)); if(!tracecount.ContainsKey(nextline)) tracecount.Add(nextline, 1); else tracecount[nextline]++; // Determine next vertex to use Vertex v = nextfront ? nextline.End : nextline.Start; // Get list of linedefs and sort by angle List lines = new List(v.Linedefs); LinedefAngleSorter sorter = new LinedefAngleSorter(nextline, nextfront, v); lines.Sort(sorter); // Source line is the only one? if(lines.Count == 1) { // Are we allowed to trace along this line again? if(turnatends && (!tracecount.ContainsKey(nextline) || (tracecount[nextline] < 3))) { // Turn around and go back along the other side of the line nextfront = !nextfront; } else { // No more lines, trace ends here path = null; } } else { // Trace along the next line Linedef prevline = nextline; if(lines[0] == nextline) nextline = lines[1]; else nextline = lines[0]; // Are we allowed to trace this line again? if(!tracecount.ContainsKey(nextline) || (tracecount[nextline] < 3)) { // Check if front side changes if((prevline.Start == nextline.Start) || (prevline.End == nextline.End)) nextfront = !nextfront; } else { // No more lines, trace ends here path = null; } } } // Continue as long as we have not reached the start yet // or we have no next line to trace while((path != null) && ((nextline != endline) || (nextfront != endfront))); // If start and front are not the same, add the end to the list also if((path != null) && ((startline != endline) || (startfront != endfront))) path.Add(new LinedefSide(endline, endfront)); // Return path (null when trace failed) return path; } #endregion #region ================== Sector Making // This makes the sector from the given lines and sides public static Sector MakeSector(List alllines) { Sector newsector = General.Map.Map.CreateSector(); Sector sourcesector = null; SidedefSettings sourceside = new SidedefSettings(); bool removeuselessmiddle; // Check if any of the sides already has a sidedef // Then we use information from that sidedef to make the others foreach(LinedefSide ls in alllines) { if(ls.Front) { if(ls.Line.Front != null) { // Copy sidedef information if not already found if(sourcesector == null) sourcesector = ls.Line.Front.Sector; TakeSidedefSettings(ref sourceside, ls.Line.Front); break; } } else { if(ls.Line.Back != null) { // Copy sidedef information if not already found if(sourcesector == null) sourcesector = ls.Line.Back.Sector; TakeSidedefSettings(ref sourceside, ls.Line.Back); break; } } } // Now do the same for the other sides // Note how information is only copied when not already found // so this won't override information from the sides searched above foreach(LinedefSide ls in alllines) { if(ls.Front) { if(ls.Line.Back != null) { // Copy sidedef information if not already found if(sourcesector == null) sourcesector = ls.Line.Back.Sector; TakeSidedefSettings(ref sourceside, ls.Line.Back); break; } } else { if(ls.Line.Front != null) { // Copy sidedef information if not already found if(sourcesector == null) sourcesector = ls.Line.Front.Sector; TakeSidedefSettings(ref sourceside, ls.Line.Front); break; } } } // Use defaults where no settings could be found TakeSidedefDefaults(ref sourceside); // Found a source sector? if(sourcesector != null) { // Copy properties from source to new sector sourcesector.CopyPropertiesTo(newsector); } else { // No source sector, apply default sector properties ApplyDefaultsToSector(newsector); } // Go for all sides to make sidedefs foreach(LinedefSide ls in alllines) { // We may only remove a useless middle texture when // the line was previously singlesided removeuselessmiddle = (ls.Line.Back == null) || (ls.Line.Front == null); if(ls.Front) { // Create sidedef is needed and ensure it points to the new sector if(ls.Line.Front == null) General.Map.Map.CreateSidedef(ls.Line, true, newsector); if(ls.Line.Front.Sector != newsector) ls.Line.Front.ChangeSector(newsector); ApplyDefaultsToSidedef(ls.Line.Front, sourceside); } else { // Create sidedef is needed and ensure it points to the new sector if(ls.Line.Back == null) General.Map.Map.CreateSidedef(ls.Line, false, newsector); if(ls.Line.Back.Sector != newsector) ls.Line.Back.ChangeSector(newsector); ApplyDefaultsToSidedef(ls.Line.Back, sourceside); } // Update line if(ls.Line.Front != null) ls.Line.Front.RemoveUnneededTextures(removeuselessmiddle); if(ls.Line.Back != null) ls.Line.Back.RemoveUnneededTextures(removeuselessmiddle); ls.Line.ApplySidedFlags(); } // Return the new sector return newsector; } // This joins a sector with the given lines and sides public static Sector JoinSector(List alllines, Sidedef original) { SidedefSettings sourceside = new SidedefSettings(); // Take settings fro mthe original side TakeSidedefSettings(ref sourceside, original); // Use defaults where no settings could be found TakeSidedefDefaults(ref sourceside); // Go for all sides to make sidedefs foreach(LinedefSide ls in alllines) { if(ls.Front) { // Create sidedef if needed if(ls.Line.Front == null) { General.Map.Map.CreateSidedef(ls.Line, true, original.Sector); ApplyDefaultsToSidedef(ls.Line.Front, sourceside); } // Added 23-9-08, can we do this or will it break things? else { // Link to the new sector ls.Line.Front.ChangeSector(original.Sector); } } else { // Create sidedef if needed if(ls.Line.Back == null) { General.Map.Map.CreateSidedef(ls.Line, false, original.Sector); ApplyDefaultsToSidedef(ls.Line.Back, sourceside); } // Added 23-9-08, can we do this or will it break things? else { // Link to the new sector ls.Line.Back.ChangeSector(original.Sector); } } // Update line ls.Line.ApplySidedFlags(); } // Return the new sector return original.Sector; } // This takes default settings if not taken yet private static void TakeSidedefDefaults(ref SidedefSettings settings) { // Use defaults where no settings could be found if(settings.newtexhigh == null) settings.newtexhigh = General.Settings.DefaultTexture; if(settings.newtexmid == null) settings.newtexmid = General.Settings.DefaultTexture; if(settings.newtexlow == null) settings.newtexlow = General.Settings.DefaultTexture; } // This takes sidedef settings if not taken yet private static void TakeSidedefSettings(ref SidedefSettings settings, Sidedef side) { if((side.LongHighTexture != MapSet.EmptyLongName) && (settings.newtexhigh == null)) settings.newtexhigh = side.HighTexture; if((side.LongMiddleTexture != MapSet.EmptyLongName) && (settings.newtexmid == null)) settings.newtexmid = side.MiddleTexture; if((side.LongLowTexture != MapSet.EmptyLongName) && (settings.newtexlow == null)) settings.newtexlow = side.LowTexture; } // This applies defaults to a sidedef private static void ApplyDefaultsToSidedef(Sidedef sd, SidedefSettings defaults) { if(sd.HighRequired() && sd.HighTexture.StartsWith("-")) sd.SetTextureHigh(defaults.newtexhigh); if(sd.MiddleRequired() && sd.MiddleTexture.StartsWith("-")) sd.SetTextureMid(defaults.newtexmid); if(sd.LowRequired() && sd.LowTexture.StartsWith("-")) sd.SetTextureLow(defaults.newtexlow); } // This applies defaults to a sector private static void ApplyDefaultsToSector(Sector s) { s.SetFloorTexture(General.Settings.DefaultFloorTexture); s.SetCeilTexture(General.Settings.DefaultCeilingTexture); s.FloorHeight = General.Settings.DefaultFloorHeight; s.CeilHeight = General.Settings.DefaultCeilingHeight; s.Brightness = General.Settings.DefaultBrightness; } #endregion #region ================== Sector Labels // This finds the ideal label positions for a sector public static List FindLabelPositions(Sector s) { List positions = new List(2); int islandoffset = 0; // Do we have a triangulation? Triangulation triangles = s.Triangles; if(triangles != null) { // Go for all islands for(int i = 0; i < triangles.IslandVertices.Count; i++) { Dictionary sides = new Dictionary(triangles.IslandVertices[i] >> 1); List candidatepositions = new List(triangles.IslandVertices[i] >> 1); float founddistance = float.MinValue; Vector2D foundposition = new Vector2D(); float minx = float.MaxValue; float miny = float.MaxValue; float maxx = float.MinValue; float maxy = float.MinValue; // Make candidate lines that are not along sidedefs // We do this before testing the candidate against the sidedefs so that // we can collect the relevant sidedefs first in the same run for(int t = 0; t < triangles.IslandVertices[i]; t += 3) { int triangleoffset = islandoffset + t; Vector2D v1 = triangles.Vertices[triangleoffset + 2]; Sidedef sd = triangles.Sidedefs[triangleoffset + 2]; for(int v = 0; v < 3; v++) { Vector2D v2 = triangles.Vertices[triangleoffset + v]; // Not along a sidedef? Then this line is across the sector // and guaranteed to be inside the sector! if(sd == null) { // Make the line candidatepositions.Add(v1 + (v2 - v1) * 0.5f); } else { // This sidedefs is part of this island and must be checked // so add it to the dictionary sides[sd] = sd.Line; } // Make bbox of this island minx = Math.Min(minx, v1.x); miny = Math.Min(miny, v1.y); maxx = Math.Max(maxx, v1.x); maxy = Math.Max(maxy, v1.y); // Next sd = triangles.Sidedefs[triangleoffset + v]; v1 = v2; } } // Any candidate lines found at all? if(candidatepositions.Count > 0) { // Start with the first line foreach(Vector2D candidatepos in candidatepositions) { // Check distance against other lines float smallestdist = int.MaxValue; foreach(KeyValuePair sd in sides) { // Check the distance float distance = sd.Value.DistanceToSq(candidatepos, true); smallestdist = Math.Min(smallestdist, distance); } // Keep this candidate if it is better than previous if(smallestdist > founddistance) { foundposition = candidatepos; founddistance = smallestdist; } } // No cceptable line found, just use the first! positions.Add(new LabelPositionInfo(foundposition, (float)Math.Sqrt(founddistance))); } else { // No candidate lines found. // Check to see if the island is a triangle if(triangles.IslandVertices[i] == 3) { // Use the center of the triangle // TODO: Use the 'incenter' instead, see http://mathworld.wolfram.com/Incenter.html Vector2D v = (triangles.Vertices[islandoffset] + triangles.Vertices[islandoffset + 1] + triangles.Vertices[islandoffset + 2]) / 3.0f; float d = Line2D.GetDistanceToLineSq(triangles.Vertices[islandoffset], triangles.Vertices[islandoffset + 1], v, false); d = Math.Min(d, Line2D.GetDistanceToLineSq(triangles.Vertices[islandoffset + 1], triangles.Vertices[islandoffset + 2], v, false)); d = Math.Min(d, Line2D.GetDistanceToLineSq(triangles.Vertices[islandoffset + 2], triangles.Vertices[islandoffset], v, false)); positions.Add(new LabelPositionInfo(v, (float)Math.Sqrt(d))); } else { // Use the center of this island. float d = Math.Min((maxx - minx) * 0.5f, (maxy - miny) * 0.5f); positions.Add(new LabelPositionInfo(new Vector2D(minx + (maxx - minx) * 0.5f, miny + (maxy - miny) * 0.5f), d)); } } // Done with this island islandoffset += triangles.IslandVertices[i]; } } else { // No triangulation was made. FAIL! General.Fail("No triangulation exists for sector " + s, "Triangulation is required to create label positions for a sector."); } // Done return positions; } #endregion } }