#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 System.Drawing; using System.Windows.Forms; using CodeImp.DoomBuilder.Actions; using CodeImp.DoomBuilder.Editing; using CodeImp.DoomBuilder.Geometry; using CodeImp.DoomBuilder.Map; using CodeImp.DoomBuilder.Rendering; using CodeImp.DoomBuilder.Windows; #endregion namespace CodeImp.DoomBuilder.BuilderModes { [EditMode(DisplayName = "Draw Lines Mode", SwitchAction = "drawlinesmode", ButtonImage = "DrawGeometryMode.png", //mxd ButtonOrder = int.MinValue + 1, //mxd ButtonGroup = "000_drawing", //mxd AllowCopyPaste = false, Volatile = true, UseByDefault = true, Optional = false)] public class DrawGeometryMode : BaseClassicMode { #region ================== Constants protected const float LINE_THICKNESS = 0.8f; #endregion #region ================== Variables // Drawing points protected List points; protected List labels; // Keep track of view changes (never used. mxd) //protected float lastoffsetx; //protected float lastoffsety; //protected float lastscale; // Options protected bool snaptogrid; // SHIFT to toggle protected bool snaptonearest; // CTRL to enable protected bool snaptocardinaldirection; //mxd. ALT-SHIFT to enable protected static bool usefourcardinaldirections; protected bool continuousdrawing; //mxd. Restart after finishing drawing? protected bool autoclosedrawing; //mxd. Finish drawing when new points and existing geometry form a closed shape protected bool drawingautoclosed; //mxd //mxd. Labels display style protected bool labelshowangle = true; protected bool labeluseoffset = true; //mxd. Interface private DrawLineOptionsPanel panel; #endregion #region ================== Properties #endregion #region ================== Constructor / Disposer // Constructor public DrawGeometryMode() { // Initialize points = new List(); labels = new List(); // No selection in this mode General.Map.Map.ClearAllSelected(); General.Map.Map.ClearAllMarks(false); //mxd SetupInterface(); // We have no destructor GC.SuppressFinalize(this); } // Disposer public override void Dispose() { // Not already disposed? if(!isdisposed) { // Clean up if(labels != null) foreach(LineLengthLabel l in labels) l.Dispose(); // Done base.Dispose(); } } #endregion #region ================== Methods // This checks if the view offset/zoom changed and updates the check (never used. mxd) /*protected bool CheckViewChanged() { // View changed? bool viewchanged = (renderer.OffsetX != lastoffsetx || renderer.OffsetY != lastoffsety || renderer.Scale != lastscale); // Keep view information lastoffsetx = renderer.OffsetX; lastoffsety = renderer.OffsetY; lastscale = renderer.Scale; // Return result return viewchanged; }*/ // This updates the dragging protected virtual void Update() { PixelColor stitchcolor = General.Colors.Highlight; PixelColor losecolor = General.Colors.Selection; snaptocardinaldirection = General.Interface.ShiftState && General.Interface.AltState; //mxd snaptogrid = (snaptocardinaldirection || General.Interface.ShiftState ^ General.Interface.SnapToGrid); snaptonearest = General.Interface.CtrlState ^ General.Interface.AutoMerge; DrawnVertex curp = GetCurrentPosition(); float vsize = (renderer.VertexSize + 1.0f) / renderer.Scale; // Update label positions (mxd) if(labels.Count > 0) { // Update labels for already drawn lines for(int i = 0; i < labels.Count - 1; i++) labels[i].Move(points[i].pos, points[i + 1].pos); // Update label for active line labels[labels.Count - 1].Move(points[points.Count - 1].pos, curp.pos); } // Render drawing lines if(renderer.StartOverlay(true)) { // Go for all points to draw lines PixelColor color; if(points.Count > 0) { // Render lines DrawnVertex lastp = points[0]; for(int i = 1; i < points.Count; i++) { // Determine line color if(lastp.stitchline && points[i].stitchline) color = stitchcolor; else color = losecolor; // Render line renderer.RenderLine(lastp.pos, points[i].pos, LINE_THICKNESS, color, true); RenderLinedefDirectionIndicator(lastp.pos, points[i].pos, color); //mxd lastp = points[i]; } // Determine line color color = (lastp.stitchline && snaptonearest ? stitchcolor : losecolor); // Render line to cursor renderer.RenderLine(lastp.pos, curp.pos, LINE_THICKNESS, color, true); RenderLinedefDirectionIndicator(lastp.pos, curp.pos, color); //mxd // Render vertices for(int i = 0; i < points.Count; i++) { // Determine vertex color color = points[i].stitch ? stitchcolor : losecolor; // Render vertex renderer.RenderRectangleFilled(new RectangleF(points[i].pos.x - vsize, points[i].pos.y - vsize, vsize * 2.0f, vsize * 2.0f), color, true); } } // Determine point color color = snaptonearest ? stitchcolor : losecolor; // Render vertex at cursor renderer.RenderRectangleFilled(new RectangleF(curp.pos.x - vsize, curp.pos.y - vsize, vsize * 2.0f, vsize * 2.0f), color, true); // Go for all labels foreach(LineLengthLabel l in labels) renderer.RenderText(l.TextLabel); // Done renderer.Finish(); } // Done renderer.Present(); } //mxd private void RenderLinedefDirectionIndicator(Vector2D start, Vector2D end, PixelColor color) { Vector2D delta = end - start; Vector2D middlePoint = new Vector2D(start.x + delta.x / 2, start.y + delta.y / 2); Vector2D scaledPerpendicular = delta.GetPerpendicular().GetNormal().GetScaled(18f / renderer.Scale); renderer.RenderLine(middlePoint, new Vector2D(middlePoint.x - scaledPerpendicular.x, middlePoint.y - scaledPerpendicular.y), LINE_THICKNESS, color, true); } // This returns the aligned and snapped draw position public static DrawnVertex GetCurrentPosition(Vector2D mousemappos, bool snaptonearest, bool snaptogrid, bool snaptocardinal, IRenderer2D renderer, List points) { DrawnVertex p = new DrawnVertex(); p.stitch = true; //mxd. Setting these to false seems to be a good way to create invalid geometry... p.stitchline = true; //mxd //mxd. If snap to cardinal directions is enabled and we have points, modify mouse position Vector2D vm; if(snaptocardinal && points.Count > 0) { Vector2D offset = mousemappos - points[points.Count - 1].pos; float angle; if(usefourcardinaldirections) angle = Angle2D.DegToRad((General.ClampAngle((int)Angle2D.RadToDeg(offset.GetAngle()))) / 90 * 90 + 45); else angle = Angle2D.DegToRad((General.ClampAngle((int)Angle2D.RadToDeg(offset.GetAngle()) + 22)) / 45 * 45); offset = new Vector2D(0, -offset.GetLength()).GetRotated(angle); vm = points[points.Count - 1].pos + offset; } else { vm = mousemappos; } float vrange = BuilderPlug.Me.StitchRange / renderer.Scale; // Snap to nearest? if(snaptonearest) { // Go for all drawn points foreach(DrawnVertex v in points) { if(Vector2D.DistanceSq(mousemappos, v.pos) < (vrange * vrange)) { p.pos = v.pos; //p.stitch = true; //p.stitchline = true; return p; } } // Try the nearest vertex Vertex nv = General.Map.Map.NearestVertexSquareRange(mousemappos, vrange); if(nv != null) { p.pos = nv.Position; //p.stitch = true; //p.stitchline = true; return p; } // Try the nearest linedef Linedef nl = General.Map.Map.NearestLinedefRange(mousemappos, BuilderPlug.Me.StitchRange / renderer.Scale); if(nl != null) { // Snap to grid? if(snaptogrid) { // Get grid intersection coordinates List coords = nl.GetGridIntersections(); // Find nearest grid intersection bool found = false; float found_distance = float.MaxValue; Vector2D found_coord = new Vector2D(); foreach(Vector2D v in coords) { Vector2D delta = mousemappos - v; if(delta.GetLengthSq() < found_distance) { found_distance = delta.GetLengthSq(); found_coord = v; found = true; } } if(found) { // Align to the closest grid intersection p.pos = found_coord; //p.stitch = true; //p.stitchline = true; return p; } } else { // Aligned to line p.pos = nl.NearestOnLine(mousemappos); //p.stitch = true; //p.stitchline = true; return p; } } } else { // Always snap to the first drawn vertex so that the user can finish a complete sector without stitching if(points.Count > 0) { if(Vector2D.DistanceSq(mousemappos, points[0].pos) < (vrange * vrange)) { p.pos = points[0].pos; //p.stitch = true; //p.stitchline = false; return p; } } } // if the mouse cursor is outside the map bondaries check if the line between the last set point and the // mouse cursor intersect any of the boundary lines. If it does, set the position to this intersection if(points.Count > 0 && (mousemappos.x < General.Map.Config.LeftBoundary || mousemappos.x > General.Map.Config.RightBoundary || mousemappos.y > General.Map.Config.TopBoundary || mousemappos.y < General.Map.Config.BottomBoundary)) { Line2D dline = new Line2D(mousemappos, points[points.Count - 1].pos); bool foundintersection = false; float u = 0.0f; List blines = new List(); // lines for left, top, right and bottom boundaries blines.Add(new Line2D(General.Map.Config.LeftBoundary, General.Map.Config.BottomBoundary, General.Map.Config.LeftBoundary, General.Map.Config.TopBoundary)); blines.Add(new Line2D(General.Map.Config.LeftBoundary, General.Map.Config.TopBoundary, General.Map.Config.RightBoundary, General.Map.Config.TopBoundary)); blines.Add(new Line2D(General.Map.Config.RightBoundary, General.Map.Config.TopBoundary, General.Map.Config.RightBoundary, General.Map.Config.BottomBoundary)); blines.Add(new Line2D(General.Map.Config.RightBoundary, General.Map.Config.BottomBoundary, General.Map.Config.LeftBoundary, General.Map.Config.BottomBoundary)); // check for intersections with boundaries for(int i = 0; i < blines.Count; i++) { if(!foundintersection) { // only check for intersection if the last set point is not on the // line we are checking against if(blines[i].GetSideOfLine(points[points.Count - 1].pos) != 0.0f) { foundintersection = blines[i].GetIntersection(dline, out u); } } } // if there was no intersection set the position to the last set point if(!foundintersection) vm = points[points.Count - 1].pos; else vm = dline.GetCoordinatesAt(u); } // Snap to grid? if(snaptogrid) { // Aligned to grid p.pos = General.Map.Grid.SnappedToGrid(vm); // special handling if(p.pos.x > General.Map.Config.RightBoundary) p.pos.x = General.Map.Config.RightBoundary; if(p.pos.y < General.Map.Config.BottomBoundary) p.pos.y = General.Map.Config.BottomBoundary; //p.stitch = snaptonearest; //p.stitchline = snaptonearest; return p; } else { // Normal position vm.x = (float)Math.Round(vm.x); //mxd vm.y = (float)Math.Round(vm.y); //mxd p.pos = vm; //p.stitch = snaptonearest; //p.stitchline = snaptonearest; return p; } } // This gets the aligned and snapped draw position protected DrawnVertex GetCurrentPosition() { return GetCurrentPosition(mousemappos, snaptonearest, snaptogrid, snaptocardinaldirection, renderer, points); } // This draws a point at a specific location public bool DrawPointAt(DrawnVertex p) { return DrawPointAt(p.pos, p.stitch, p.stitchline); } // This draws a point at a specific location public virtual bool DrawPointAt(Vector2D pos, bool stitch, bool stitchline) { if(pos.x < General.Map.Config.LeftBoundary || pos.x > General.Map.Config.RightBoundary || pos.y > General.Map.Config.TopBoundary || pos.y < General.Map.Config.BottomBoundary) return false; DrawnVertex newpoint = new DrawnVertex(); newpoint.pos = pos; newpoint.stitch = stitch; newpoint.stitchline = stitchline; points.Add(newpoint); labels.Add(new LineLengthLabel(labelshowangle, labeluseoffset)); Update(); if(points.Count > 1) { // Check if point stitches with the first if(points[points.Count - 1].stitch) { Vector2D p1 = points[0].pos; Vector2D p2 = points[points.Count - 1].pos; Vector2D delta = p1 - p2; if((Math.Abs(delta.x) <= 0.001f) && (Math.Abs(delta.y) <= 0.001f)) { //mxd. Seems... logical? if(points.Count == 2) { OnCancel(); return true; } // Finish drawing FinishDraw(); return true; } } //mxd. Points and existing geometry form a closed shape? if(continuousdrawing && autoclosedrawing) { // Determive center point float minx = float.MaxValue; float maxx = float.MinValue; float miny = float.MaxValue; float maxy = float.MinValue; foreach(DrawnVertex v in points) { if(v.pos.x < minx) minx = v.pos.x; if(v.pos.x > maxx) maxx = v.pos.x; if(v.pos.y < miny) miny = v.pos.y; if(v.pos.y > maxy) maxy = v.pos.y; } Vector2D shapecenter = new Vector2D(minx + (maxx - minx) / 2, miny + (maxy - miny) / 2); // Determine center point between start and end points minx = Math.Min(points[0].pos.x, points[points.Count - 1].pos.x); maxx = Math.Max(points[0].pos.x, points[points.Count - 1].pos.x); miny = Math.Min(points[0].pos.y, points[points.Count - 1].pos.y); maxy = Math.Max(points[0].pos.y, points[points.Count - 1].pos.y); Vector2D startendcenter = new Vector2D(minx + (maxx - minx) / 2, miny + (maxy - miny) / 2); // Offset the center perpendicular to the start -> end line direction... if(shapecenter == startendcenter) { shapecenter -= new Line2D(points[0].pos, points[points.Count - 1].pos).GetPerpendicular().GetNormal(); } // Do the check if(CanFinishDrawing(points[0].pos, points[points.Count - 1].pos, shapecenter)) { drawingautoclosed = true; FinishDraw(); } } } return true; } //mxd private static bool CanFinishDrawing(Vector2D start, Vector2D end, Vector2D center) { Linedef startline = FindPotentialLine(start, center); if(startline == null) return false; Linedef endline = FindPotentialLine(end, center); if(endline == null) return false; // Can finish drawing if a path between startline and endline exists return Tools.FindClosestPath(startline, startline.SideOfLine(center) < 0.0f, endline, endline.SideOfLine(center) < 0.0f, true) != null; } //mxd private static Linedef FindPotentialLine(Vector2D target, Vector2D center) { // Target position on top of existing vertex? Vertex v = General.Map.Map.NearestVertex(target); if(v == null) return null; Linedef result = null; if(v.Position == target) { float mindistance = float.MaxValue; foreach(Linedef l in v.Linedefs) { if(result == null) { result = l; mindistance = Vector2D.DistanceSq(l.GetCenterPoint(), center); } else { float curdistance = Vector2D.DistanceSq(l.GetCenterPoint(), center); if(curdistance < mindistance) { mindistance = curdistance; result = l; } } } } else { // Result position will split a line? result = General.Map.Map.NearestLinedef(target); if(result.DistanceTo(target, true) > BuilderPlug.Me.StitchRange) return null; } return result; } #endregion #region ================== mxd. Settings panel protected virtual void SetupInterface() { //Add options docker panel = new DrawLineOptionsPanel(); panel.OnContinuousDrawingChanged += OnContinuousDrawingChanged; panel.OnAutoCloseDrawingChanged += OnAutoCloseDrawingChanged; // Needs to be set after adding the events... panel.ContinuousDrawing = General.Settings.ReadPluginSetting("drawlinesmode.continuousdrawing", false); panel.AutoCloseDrawing = General.Settings.ReadPluginSetting("drawlinesmode.autoclosedrawing", false); } protected virtual void AddInterface() { panel.Register(); } protected virtual void RemoveInterface() { General.Settings.WritePluginSetting("drawlinesmode.continuousdrawing", panel.ContinuousDrawing); General.Settings.WritePluginSetting("drawlinesmode.autoclosedrawing", panel.AutoCloseDrawing); panel.Unregister(); } #endregion #region ================== Events public override void OnHelp() { General.ShowHelp("e_drawgeometry.html"); } // Engaging public override void OnEngage() { base.OnEngage(); EnableAutoPanning(); AddInterface(); //mxd renderer.SetPresentation(Presentation.Standard); // Set cursor General.Interface.SetCursor(Cursors.Cross); } // Disengaging public override void OnDisengage() { RemoveInterface(); //mxd base.OnDisengage(); DisableAutoPanning(); } // Cancelled public override void OnCancel() { //mxd. Cannot leave this way when continuous drawing is enabled if(continuousdrawing) { drawingautoclosed = false; return; } // Cancel base class base.OnCancel(); // Return to original mode General.Editing.ChangeMode(General.Editing.PreviousStableMode.Name); } // Accepted public override void OnAccept() { Cursor.Current = Cursors.AppStarting; General.Settings.FindDefaultDrawSettings(); // When points have been drawn if(points.Count > 0) { // Make undo for the draw General.Map.UndoRedo.CreateUndo("Line draw"); // Make an analysis and show info string[] adjectives = new[] { "beautiful", "lovely", "romantic", "stylish", "cheerful", "comical", "awesome", "accurate", "adorable", "adventurous", "attractive", "cute", "elegant", "glamorous", "gorgeous", "handsome", "magnificent", "unusual", "outstanding", "mysterious", "amusing", "charming", "fantastic", "jolly" }; string word = adjectives[points.Count % adjectives.Length]; word = (points.Count > adjectives.Length) ? "very " + word : word; string a = ((word[0] == 'a') || (word[0] == 'e') || (word[0] == 'o') || (word[0] == 'u')) ? "an " : "a "; General.Interface.DisplayStatus(StatusType.Action, "Created " + a + word + " drawing."); // Make the drawing if(Tools.DrawLines(points, true, BuilderPlug.Me.AutoAlignTextureOffsetsOnCreate)) //mxd { // Snap to map format accuracy General.Map.Map.SnapAllToAccuracy(); // Clear selection General.Map.Map.ClearAllSelected(); // Update cached values General.Map.Map.Update(); // Edit new sectors? List newsectors = General.Map.Map.GetMarkedSectors(true); if(BuilderPlug.Me.EditNewSector && (newsectors.Count > 0)) General.Interface.ShowEditSectors(newsectors); // Update the used textures General.Map.Data.UpdateUsedTextures(); //mxd General.Map.Renderer2D.UpdateExtraFloorFlag(); // Map is changed General.Map.IsChanged = true; } else { // Drawing failed // NOTE: I have to call this twice, because the first time only cancels this volatile mode General.Map.UndoRedo.WithdrawUndo(); General.Map.UndoRedo.WithdrawUndo(); } } // Done Cursor.Current = Cursors.Default; if(continuousdrawing) { //mxd. Reset settings points.Clear(); labels.Clear(); drawingautoclosed = false; //mxd. Redraw display General.Interface.RedrawDisplay(); } else { // Return to original mode General.Editing.ChangeMode(General.Editing.PreviousStableMode.Name); } } // This redraws the display public override void OnRedrawDisplay() { renderer.RedrawSurface(); // Render lines if(renderer.StartPlotter(true)) { renderer.PlotLinedefSet(General.Map.Map.Linedefs); renderer.PlotVerticesSet(General.Map.Map.Vertices); renderer.Finish(); } // Render things if(renderer.StartThings(true)) { renderer.RenderThingSet(General.Map.Map.Things, Presentation.THINGS_ALPHA); renderer.Finish(); } // Normal update Update(); } // Mouse moving public override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if(panning) return; //mxd. Skip all this jazz while panning Update(); } // When a key is released public override void OnKeyUp(KeyEventArgs e) { base.OnKeyUp(e); if((snaptogrid != (General.Interface.ShiftState ^ General.Interface.SnapToGrid)) || (snaptonearest != (General.Interface.CtrlState ^ General.Interface.AutoMerge)) || (snaptocardinaldirection != (General.Interface.AltState && General.Interface.ShiftState))) Update(); } // When a key is pressed public override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if((snaptogrid != (General.Interface.ShiftState ^ General.Interface.SnapToGrid)) || (snaptonearest != (General.Interface.CtrlState ^ General.Interface.AutoMerge)) || (snaptocardinaldirection != (General.Interface.AltState && General.Interface.ShiftState))) Update(); } //mxd protected void OnContinuousDrawingChanged(object value, EventArgs e) { continuousdrawing = (bool)value; } //mxd protected void OnAutoCloseDrawingChanged(object value, EventArgs e) { autoclosedrawing = (bool)value; } #endregion #region ================== Actions // Drawing a point [BeginAction("drawpoint")] public void DrawPoint() { // Mouse inside window? if(General.Interface.MouseInDisplay) { DrawnVertex newpoint = GetCurrentPosition(); if(!DrawPointAt(newpoint)) General.Interface.DisplayStatus(StatusType.Warning, "Failed to draw point: outside of map boundaries."); } } // Remove a point [BeginAction("removepoint")] public virtual void RemovePoint() { if(points.Count > 0) points.RemoveAt(points.Count - 1); if(labels.Count > 0) { labels[labels.Count - 1].Dispose(); labels.RemoveAt(labels.Count - 1); } Update(); } // Finish drawing [BeginAction("finishdraw")] public void FinishDraw() { // Accept the changes General.Editing.AcceptMode(); } #endregion } }