UltimateZoneBuilder/Source/Plugins/BuilderModes/ClassicModes/DrawGeometryMode.cs
MaxED b37457dbc1 Changed, Draw Lines/Rectangle/Circle/Curve modes: line length labels are now repositioned to stay on screen when line's start or end is not visible.
Added, Map Analysis mode: added "Check map size" check. It will generate a warning when map's width or height is larger than 32767 m.u.
2015-08-24 21:49:15 +00:00

682 lines
21 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 System.Windows.Forms;
using CodeImp.DoomBuilder.Config;
using CodeImp.DoomBuilder.Windows;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.Rendering;
using CodeImp.DoomBuilder.Geometry;
using System.Drawing;
using CodeImp.DoomBuilder.Editing;
using CodeImp.DoomBuilder.Actions;
#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<DrawnVertex> points;
protected List<LineLengthLabel> 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;
#endregion
#region ================== Properties
#endregion
#region ================== Constructor / Disposer
// Constructor
public DrawGeometryMode()
{
// Initialize
points = new List<DrawnVertex>();
labels = new List<LineLengthLabel>();
// No selection in this mode
General.Map.Map.ClearAllSelected();
General.Map.Map.ClearAllMarks(false);
// 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;
PixelColor color;
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++)
SetLabelPosition(labels[i], points[i].pos, points[i + 1].pos);
// Update label for active line
SetLabelPosition(labels[labels.Count - 1], points[points.Count - 1].pos, curp.pos);
}
// Render drawing lines
if(renderer.StartOverlay(true))
{
// Go for all points to draw lines
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);
}
//mxd
protected void SetLabelPosition(LineLengthLabel label, Vector2D start, Vector2D end)
{
// Check if start/end point is on screen...
Vector2D lt = General.Map.Renderer2D.DisplayToMap(new Vector2D(0.0f, General.Map.Renderer2D.ViewportSize.Height));
Vector2D rb = General.Map.Renderer2D.DisplayToMap(new Vector2D(General.Map.Renderer2D.ViewportSize.Width, 0.0f));
RectangleF viewport = new RectangleF(lt.x, lt.y, rb.x - lt.x, rb.y - lt.y);
bool startvisible = viewport.Contains(start.x, start.y);
bool endvisible = viewport.Contains(end.x, end.y);
// Do this only when one point is visible, an the other isn't
if((!startvisible && endvisible) || (startvisible && !endvisible))
{
Line2D drawnline = new Line2D(start, end);
Line2D[] viewportsides = new[] {
new Line2D(lt, rb.x, lt.y), // top
new Line2D(lt.x, rb.y, rb.x, rb.y), // bottom
new Line2D(lt, lt.x, rb.y), // left
new Line2D(rb.x, lt.y, rb.x, rb.y), // right
};
float u;
foreach(Line2D side in viewportsides)
{
// Modify the start point so it stays on screen
if(!startvisible && side.GetIntersection(drawnline, out u))
{
start = drawnline.GetCoordinatesAt(u);
break;
}
// Modify the end point so it stays on screen
if(!endvisible && side.GetIntersection(drawnline, out u))
{
end = drawnline.GetCoordinatesAt(u);
break;
}
}
}
Vector2D perpendicular = (end - start).GetPerpendicular();
float angle = perpendicular.GetAngle();
float offset = label.TextLabel.TextSize.Width * Math.Abs((float)Math.Sin(angle)) + label.TextLabel.TextSize.Height * Math.Abs((float)Math.Cos(angle));
perpendicular = perpendicular.GetNormal().GetScaled(offset / 2.0f / renderer.Scale);
label.Start = start + perpendicular;
label.End = end + perpendicular;
}
// This returns the aligned and snapped draw position
public static DrawnVertex GetCurrentPosition(Vector2D mousemappos, bool snaptonearest, bool snaptogrid, bool snaptocardinal, IRenderer2D renderer, List<DrawnVertex> 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<Vector2D> 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<Line2D> blines = new List<Line2D>();
// 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(true));
Update();
// Check if point stitches with the first
if((points.Count > 1) && 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;
}
#endregion
#region ================== Events
public override void OnHelp()
{
General.ShowHelp("e_drawgeometry.html");
}
// Engaging
public override void OnEngage()
{
base.OnEngage();
EnableAutoPanning();
renderer.SetPresentation(Presentation.Standard);
// Set cursor
General.Interface.SetCursor(Cursors.Cross);
}
// Disengaging
public override void OnDisengage()
{
base.OnDisengage();
DisableAutoPanning();
}
// Cancelled
public override void OnCancel()
{
// 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
{
// 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();
return;
}
// 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<Sector> 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();
// Map is changed
General.Map.IsChanged = true;
}
// Done
Cursor.Current = Cursors.Default;
// 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.ThingsFilter.SortedHiddenThings, Presentation.THINGS_HIDDEN_ALPHA);
renderer.RenderThingSet(General.Map.ThingsFilter.SortedVisibleThings, 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();
}
#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
}
}