using System; using System.Collections.Generic; using System.Drawing; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using CodeImp.DoomBuilder.BuilderModes; using CodeImp.DoomBuilder.Geometry; using CodeImp.DoomBuilder.Map; namespace CodeImp.DoomBuilder.VisualModes { /// /// Stores the normalized angle (clockwise from the vertex) and if the original line's front points in a clockwise or counter clockwise direction /// internal struct LineAngleInfo { public double angle; public bool clockwise; public LineAngleInfo(Linedef ld, Vertex v, Sector s) { Line2D line; if (ld.Start == v) { line = ld.Line; clockwise = true; } else { line = new Line2D(ld.End.Position, ld.Start.Position); clockwise = false; } angle = line.GetAngle(); if (ld.Front?.Sector != s) clockwise = !clockwise; } } internal class VisualVertexSlope : BaseVisualSlope { #region ================== Variables private readonly Vertex vertex; private readonly Sector sector; private double angle; #endregion #region ================== Properties public Vertex Vertex { get { return vertex; } } public Sector Sector { get { return sector; } } #endregion #region ================== Constructor / Destructor public VisualVertexSlope(BaseVisualMode mode, SectorLevel level, Vertex vertex, Sector sector, bool up) : base(mode, level, up) { this.vertex = vertex; this.sector = sector; type = VisualSlopeType.Vertex; ComputeAngle(); Update(); GC.SuppressFinalize(this); } #endregion #region ================== Methods private void ComputeAngle() { List lines = new List(); List angles = new List(); // Get all lines that we have to take into account. Ignore linedefs where both sides // don't belong to the sector or don't belong to the sector foreach (Linedef ld in vertex.Linedefs) { if (ld.IsDisposed) continue; else { bool frontsame = false; bool backsame = false; if (ld.Front != null && ld.Front.Sector == sector) frontsame = true; if (ld.Back != null && ld.Back.Sector == sector) backsame = true; if (frontsame == backsame) continue; } lines.Add(new LineAngleInfo(ld, vertex, sector)); } // Special case handling if (lines.Count < 2) { if (vertex.Linedefs.Count == 1) { angle = vertex.Linedefs.First().Angle + Angle2D.PIHALF; return; } angle = 0.0; return; } // Sort lines by their normalized angle lines.Sort((a, b) => a.angle.CompareTo(b.angle)); // Get the other line we want to compute the angle between int other = 1; if (!lines[0].clockwise) other = lines.Count - 1; Vector2D v1 = Vector2D.FromAngle(lines[0].angle); Vector2D v2 = Vector2D.FromAngle(lines[other].angle); angle = lines[0].angle + (Math.Atan2(v2.y, v2.x) - Math.Atan2(v1.y, v1.x)) / 2.0; // If the first line is going clockwise we have to add 180° if (lines[0].clockwise) angle += Angle2D.PI; // Add 90° to get it in line with Doom's angles angle += Angle2D.PIHALF; // Also need to modify the angle for ceilings if (level.type == SectorLevelType.Ceiling) angle += Angle2D.PI; angle = Angle2D.Normalized(angle); } public override void Update() { plane = new Plane(level.plane.Normal, level.plane.Offset - 0.1f); if (!up) plane = plane.GetInverted(); UpdatePosition(); } public void UpdatePosition() { Vector2D av = Vector2D.FromAngle(angle); SetPosition(new Line2D(vertex.Position, vertex.Position + av), level.plane); } /// /// Finds a slope handle to pivot around. It takes the vertex that's furthest away from the given handle /// /// public override VisualSlope GetSmartPivotHandle() { List selectedsectors = mode.GetSelectedObjects(true, false, false, false, false); // Special handling for triangular sectors if (selectedsectors.Count == 0 && BuilderPlug.Me.UseOppositeSmartPivotHandle && sector.Sidedefs.Count == 3) { foreach (VisualSidedefSlope vss in mode.SidedefSlopeHandles[sector]) { if (vss.Level == level && !(vss.Sidedef.Line.Start == vertex || vss.Sidedef.Line.End == vertex)) return vss; } } VisualSlope handle = this; List potentialhandles = new List(); if (selectedsectors.Count == 0) { // No sectors selected, so find all handles that belong to the same level foreach (VisualVertexSlope checkhandle in mode.VertexSlopeHandles[sector]) { if (checkhandle != this && checkhandle.Level == level) potentialhandles.Add(checkhandle); } } else { // Sectors are selected, get all handles from those sectors that have the same level HashSet sectors = new HashSet(); foreach (BaseVisualGeometrySector bvgs in selectedsectors) sectors.Add(bvgs.Sector.Sector); foreach (Sector s in sectors) foreach (VisualVertexSlope checkhandle in mode.VertexSlopeHandles[s]) { if (checkhandle != this) foreach (BaseVisualGeometrySector bvgs in selectedsectors) if (bvgs.Level == checkhandle.Level) potentialhandles.Add(checkhandle); } } if (potentialhandles.Count == 0) return null; handle = potentialhandles.OrderByDescending(h => Vector2D.Distance(h.Vertex.Position, vertex.Position)).First(); if (handle == this) return null; return handle; } /// /// This is called when the thing must be tested for line intersection. This should reject /// as fast as possible to rule out all geometry that certainly does not touch the line. /// public override bool PickFastReject(Vector3D from, Vector3D to, Vector3D dir) { if (vertex.IsDisposed || sector.IsDisposed) return false; RectangleF bbox = sector.BBox; if ((up && plane.Distance(from) > 0.0f) || (!up && plane.Distance(from) < 0.0f)) { if (plane.GetIntersection(from, to, ref pickrayu)) { if (pickrayu > 0.0f) { pickintersect = from + (to - from) * pickrayu; return ((pickintersect.x >= bbox.Left) && (pickintersect.x <= bbox.Right) && (pickintersect.y >= bbox.Top) && (pickintersect.y <= bbox.Bottom)); } } } return false; } /// /// This is called when the thing must be tested for line intersection. This should perform /// accurate hit detection and set u_ray to the position on the ray where this hits the geometry. /// public override bool PickAccurate(Vector3D from, Vector3D to, Vector3D dir, ref double u_ray) { u_ray = pickrayu; Sidedef sd = MapSet.NearestSidedef(sector.Sidedefs, pickintersect); Vertex v = MapSet.NearestVertex(new Vertex[] { sd.Line.Start, sd.Line.End }, pickintersect); if (v == vertex) { double side = sd.Line.SideOfLine(pickintersect); if ((side <= 0.0f && sd.IsFront) || (side > 0.0f && !sd.IsFront)) return true; } return false; } /// /// Gets the pivor point for this slope handle /// /// The pivot point as Vector3D public override Vector3D GetPivotPoint() { return new Vector3D(vertex.Position, level.plane.GetZ(vertex.Position)); } #endregion #region ================== Events public override void OnChangeTargetHeight(int amount) { VisualSlope pivothandle = null; List selectedsectors = mode.GetSelectedObjects(true, false, false, false, false); List levels = new List(); if (selectedsectors.Count == 0) levels.Add(level); else { foreach (BaseVisualGeometrySector bvgs in selectedsectors) levels.Add(bvgs.Level); if (!levels.Contains(level)) levels.Add(level); } // Try to find a slope handle the user set to be the pivot handle // TODO: doing this every time is kind of stupid. Maybe store the pivot handle in the mode? foreach (KeyValuePair> kvp in mode.AllSlopeHandles) { foreach (VisualSlope handle in kvp.Value) { if (handle.Pivot) { pivothandle = handle; break; } } } // Can't pivot around itself if (pivothandle == this) { General.Interface.DisplayStatus(Windows.StatusType.Warning, "Slope handle to modify can't be the same as the pivot handle"); return; } // User didn't set a pivot handle, try to find the smart pivot handle if (pivothandle == null) pivothandle = GetSmartPivotHandle(); // Still no pivot handle, cancle if (pivothandle == null) return; Vector3D p1, p2, p3; if (pivothandle is VisualVertexSlope) { // Build a new plane. Since we only got 2 points (the pivot point of the pivot handle and the vertex slope vertex) we need // to create a third point. That's done by getting the perpendicular of the line between the aforementioned 2 points, then // add the perpendicular to the vertex position of the vertex slope vertex p3 = pivothandle.GetPivotPoint(); Vector2D perp = new Line2D(vertex.Position, p3).GetPerpendicular(); p1 = new Vector3D(vertex.Position, level.plane.GetZ(vertex.Position) + amount); p2 = new Vector3D(vertex.Position + perp, level.plane.GetZ(vertex.Position + perp) + amount); } else // VisualSidedefSlope { List pivotpoints = ((VisualSidedefSlope)pivothandle).GetPivotPoints(); p1 = new Vector3D(vertex.Position, level.plane.GetZ(vertex.Position) + amount); p2 = pivotpoints[0]; p3 = pivotpoints[1]; } Plane plane = new Plane(p1, p2, p3, true); // Completely vertical planes are not possible if (Math.Abs(plane.a) == 1.0 || Math.Abs(plane.b) == 1.0) { General.Interface.DisplayStatus(Windows.StatusType.Warning, "Resulting plane is completely vertical, which is impossible. Aborting"); return; } mode.CreateUndo("Change slope"); // Apply slope to surfaces foreach (SectorLevel l in levels) VisualSidedefSlope.ApplySlope(l, plane, mode); mode.SetActionResult("Changed slope."); } public override void OnSelectEnd() { // The base's OnSelectEnd might not change the selection status if the user tries to select // a pivot handle, so we have to check if the selection status changed bool oldselected = selected; base.OnSelectEnd(); if (selected != oldselected && BuilderPlug.Me.SelectAdjacentVisualVertexSlopeHandles) { HashSet sectors = new HashSet(); // Get the z position of this vertex slope handle. By comparing the t position of // other vertex slope handles we can find the ones that are really adjacent, since // there can be many vertex slope handles on one vertex on different heights especially // with 3D floors. The rounding is done since there can be the tiniest differences in // the planes, which lead to different z positions even when they are the "same". double z = Math.Round(level.plane.GetZ(vertex.Position), 5); // Get all sectors around the this slope handle's vertex foreach (Linedef ld in vertex.Linedefs) { if (ld.Front != null) sectors.Add(ld.Front.Sector); if (ld.Back != null) sectors.Add(ld.Back.Sector); } foreach (Sector s in sectors) { foreach (VisualVertexSlope vs in mode.VertexSlopeHandles[s]) { if (vs.vertex != vertex || vs == this) continue; // See above for the explanation of the rounding double z1 = Math.Round(vs.level.plane.GetZ(vertex.Position), 5); if (z == z1) { if (selected) { if (!vs.selected) { vs.selected = true; mode.AddSelectedObject(vs); mode.UsedSlopeHandles.Add(vs); } } else { if (vs.selected) { vs.selected = false; mode.RemoveSelectedObject(vs); mode.UsedSlopeHandles.Remove(vs); } } } } } } } #endregion } }