using PaintDotNet.Measurement.Enum; using PaintDotNet.Measurement.HistoryMementos; using PaintDotNet.Measurement.ObjInfo; using System; using System.Collections; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace PaintDotNet.Measurement.Tools { /// /// Allows the user to draw a shape that can be defined using two points on the canvas. /// The user clicks and drags between two points to define the area that bounds the shape. /// public abstract class ShapeTool : Tool { private const char defaultShortcut = 'o'; private bool moveOriginMode; private PointF lastXY; private bool mouseDown; private MouseButtons mouseButton; private BitmapLayer bitmapLayer; private RenderArgs renderArgs; private PdnRegion interiorSaveRegion; private PdnRegion outlineSaveRegion; private List points; private PdnRegion lastDrawnRegion = null; private Cursor cursorMouseUp; private Cursor cursorMouseDown; private bool shapeWasCommited = true; private CompoundHistoryMemento chaAlreadyOnStack = null; private bool useDashStyle = false; // if set to false, then the DashStyle will always be forced to DashStyle.Flat private bool forceShapeType = false; private ShapeDrawType forcedShapeDrawType = ShapeDrawType.Both; protected override bool SupportsInk { get { return true; } } // This is for shapes that should only be draw in one ShapeDrawType // The line shape, for instance, should only ever be drawn in ShapeDrawType.Outline protected bool ForceShapeDrawType { get { return this.forceShapeType; } set { this.forceShapeType = value; } } protected ShapeDrawType ForcedShapeDrawType { get { return this.forcedShapeDrawType; } set { this.forcedShapeDrawType = value; } } protected bool UseDashStyle { get { return this.useDashStyle; } set { this.useDashStyle = value; } } /// /// Different shapes may not require all the points given to them, and as such /// if the user is drawing for a long time there may be lots of memory that's /// allocated that doesn't need to be. So before CreateShapePath is called, /// this method is called first. /// For example, the LineTool would return a new array containing only the /// first and last points. /// It is ok to return the same array that was passed in, even if it is modified. /// /// A list containing PointF instances. /// protected virtual List TrimShapePath(List trimThesePoints) { return trimThesePoints; } /// /// Override this function to return an "optimized" region that encompasses /// the shape's outline. For example, a circle would return a list of rectangles /// that traces the outline. This is necessary because normally simplification /// will produce a region that, for a circle's outline, encompasses its /// interior as well. If you return null, then the default simplification /// algorithm will be used. /// /// /// /// protected virtual RectangleF[] GetOptimizedShapeOutlineRegion(PointF[] optimizeThesePoints, PdnGraphicsPath path) { return null; } // Implement this! protected abstract PdnGraphicsPath CreateShapePath(PointF[] shapePoints); protected override void OnActivate() { base.OnActivate(); outlineSaveRegion = null; interiorSaveRegion = null; // creates a bitmap layer from the active layer bitmapLayer = (BitmapLayer)ActiveLayer; // create Graphics object renderArgs = new RenderArgs(bitmapLayer.Surface); lastDrawnRegion = new PdnRegion(); lastDrawnRegion.MakeEmpty(); } protected override void OnDeactivate() { base.OnDeactivate(); if (mouseDown) { PointF lastPoint = (PointF)points[points.Count - 1]; OnStylusUp(new StylusEventArgs(mouseButton, 0, lastPoint.X, lastPoint.Y, 0)); } if (!this.shapeWasCommited) { CommitShape(); } bitmapLayer = null; if (renderArgs != null) { renderArgs.Dispose(); renderArgs = null; } if (outlineSaveRegion != null) { outlineSaveRegion.Dispose(); outlineSaveRegion = null; } if (interiorSaveRegion != null) { interiorSaveRegion.Dispose(); interiorSaveRegion = null; } points = null; } protected virtual void OnShapeBegin() { } /// /// Called when the shape is finished being traced by the default input handlers. /// /// Do not call the base implementation of this method if you are overriding it. /// true to commit the shape immediately protected virtual bool OnShapeEnd() { return true; } protected override void OnStylusDown(StylusEventArgs e) { base.OnStylusDown(e); if (!this.shapeWasCommited) { CommitShape(); } this.ClearSavedMemory(); this.ClearSavedRegion(); cursorMouseUp = Cursor; Cursor = cursorMouseDown; if (mouseDown && e.Button == mouseButton) { return; } if (mouseDown) { moveOriginMode = true; lastXY = new PointF(e.Fx, e.Fy); OnStylusMove(e); } else if (((e.Button & MouseButtons.Left) == MouseButtons.Left) || ((e.Button & MouseButtons.Right) == MouseButtons.Right)) { // begin new shape this.shapeWasCommited = false; OnShapeBegin(); mouseDown = true; mouseButton = e.Button; using (PdnRegion clipRegion = Selection.CreateRegion()) { renderArgs.Graphics.SetClip(clipRegion.GetRegionReadOnly(), CombineMode.Replace); } // reset the points we're drawing! points = new List(); OnStylusMove(e); } } protected override void OnStylusMove(StylusEventArgs e) { base.OnStylusMove(e); if (moveOriginMode) { SizeF delta = new SizeF(e.Fx - lastXY.X, e.Fy - lastXY.Y); for (int i = 0; i < points.Count; ++i) { PointF ptF = (PointF)points[i]; ptF.X += delta.Width; ptF.Y += delta.Height; points[i] = ptF; } lastXY = new PointF(e.Fx, e.Fy); } else if (mouseDown && ((e.Button & mouseButton) != MouseButtons.None)) { PointF mouseXY = new PointF(e.Fx, e.Fy); points.Add(mouseXY); } } public virtual PixelOffsetMode GetPixelOffsetMode() { return PixelOffsetMode.Half; } protected List GetTrimmedShapePath() { List pointsCopy = new List(this.points); pointsCopy = TrimShapePath(pointsCopy); return pointsCopy; } protected void SetShapePath(List newPoints) { this.points = newPoints; } protected void RenderShape() { // create the Pen we will use to draw with Pen outlinePen = null; Brush interiorBrush = null; PenInfo pi = AppEnvironment.PenInfo(); BrushInfo bi = AppEnvironment.BrushInfo(); ColorBgra primary = AppEnvironment.PrimaryColor(); ColorBgra secondary = AppEnvironment.SecondaryColor(); if (!ForceShapeDrawType && AppEnvironment.ShapeDrawType() == ShapeDrawType.Interior) { Utility.Swap(ref primary, ref secondary); } // Initialize pens and brushes to the correct colors if ((mouseButton & MouseButtons.Left) == MouseButtons.Left) { outlinePen = pi.CreatePen(AppEnvironment.BrushInfo(), primary.ToColor(), secondary.ToColor()); interiorBrush = bi.CreateBrush(secondary.ToColor(), primary.ToColor()); } else if ((mouseButton & MouseButtons.Right) == MouseButtons.Right) { outlinePen = pi.CreatePen(AppEnvironment.BrushInfo(), secondary.ToColor(), primary.ToColor()); interiorBrush = bi.CreateBrush(primary.ToColor(), secondary.ToColor()); } if (!this.useDashStyle) { outlinePen.DashStyle = DashStyle.Solid; } outlinePen.LineJoin = LineJoin.MiterClipped; outlinePen.MiterLimit = 2; // redraw the old saveSurface if (interiorSaveRegion != null) { RestoreRegion(interiorSaveRegion); interiorSaveRegion.Dispose(); interiorSaveRegion = null; } if (outlineSaveRegion != null) { RestoreRegion(outlineSaveRegion); outlineSaveRegion.Dispose(); outlineSaveRegion = null; } // anti-aliasing? Don't mind if I do if (AppEnvironment.AntiAliasing()) { renderArgs.Graphics.SmoothingMode = SmoothingMode.AntiAlias; } else { renderArgs.Graphics.SmoothingMode = SmoothingMode.None; } // also set the pixel offset mode renderArgs.Graphics.PixelOffsetMode = GetPixelOffsetMode(); // figure out how we're going to draw ShapeDrawType drawType; if (ForceShapeDrawType) { drawType = ForcedShapeDrawType; } else { drawType = AppEnvironment.ShapeDrawType(); } // get the region we want to save points = this.TrimShapePath(points); PointF[] pointsArray = points.ToArray(); PdnGraphicsPath shapePath = CreateShapePath(pointsArray); if (shapePath != null) { // create non-optimized interior region PdnRegion interiorRegion = new PdnRegion(shapePath); // create non-optimized outline region PdnRegion outlineRegion; using (PdnGraphicsPath outlinePath = (PdnGraphicsPath)shapePath.Clone()) { try { outlinePath.Widen(outlinePen); outlineRegion = new PdnRegion(outlinePath); } // Sometimes GDI+ gets cranky if we have a very small shape (e.g. all points // are coincident). catch (OutOfMemoryException) { outlineRegion = new PdnRegion(shapePath); } } // create optimized outlineRegion for purposes of rendering, if it is possible to do so // shapes will often provide an "optimized" region that circumvents the fact that // we'd otherwise get a region that encompasses the outline *and* the interior, thus // slowing rendering significantly in many cases. RectangleF[] optimizedOutlineRegion = GetOptimizedShapeOutlineRegion(pointsArray, shapePath); PdnRegion invalidOutlineRegion; if (optimizedOutlineRegion != null) { Utility.InflateRectanglesInPlace(optimizedOutlineRegion, (int)(outlinePen.Width + 2)); invalidOutlineRegion = Utility.RectanglesToRegion(optimizedOutlineRegion); } else { invalidOutlineRegion = Utility.SimplifyAndInflateRegion(outlineRegion, Utility.DefaultSimplificationFactor, (int)(outlinePen.Width + 2)); } // create optimized interior region PdnRegion invalidInteriorRegion = Utility.SimplifyAndInflateRegion(interiorRegion, Utility.DefaultSimplificationFactor, 3); PdnRegion invalidRegion = new PdnRegion(); invalidRegion.MakeEmpty(); // set up alpha blending renderArgs.Graphics.CompositingMode = AppEnvironment.GetCompositingMode(); SaveRegion(invalidOutlineRegion, invalidOutlineRegion.GetBoundsInt()); this.outlineSaveRegion = invalidOutlineRegion; if ((drawType & ShapeDrawType.Outline) != 0) { shapePath.Draw(renderArgs.Graphics, outlinePen); } invalidRegion.Union(invalidOutlineRegion); // draw shape if ((drawType & ShapeDrawType.Interior) != 0) { SaveRegion(invalidInteriorRegion, invalidInteriorRegion.GetBoundsInt()); this.interiorSaveRegion = invalidInteriorRegion; renderArgs.Graphics.FillPath(interiorBrush, shapePath); invalidRegion.Union(invalidInteriorRegion); } else { invalidInteriorRegion.Dispose(); invalidInteriorRegion = null; } bitmapLayer.Invalidate(invalidRegion); invalidRegion.Dispose(); invalidRegion = null; outlineRegion.Dispose(); outlineRegion = null; interiorRegion.Dispose(); interiorRegion = null; } Update(); if (shapePath != null) { shapePath.Dispose(); shapePath = null; } outlinePen.Dispose(); interiorBrush.Dispose(); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); // if mouse button not down then leave function if (mouseDown && ((e.Button & mouseButton) != MouseButtons.None)) { RenderShape(); } } protected override void OnKeyDown(KeyEventArgs e) { if (mouseDown) { RenderShape(); } base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { if (mouseDown) { RenderShape(); } base.OnKeyUp(e); } protected virtual void OnShapeCommitting() { } protected void CommitShape() { OnShapeCommitting(); mouseDown = false; ArrayList has = new ArrayList(); PdnRegion activeRegion = Selection.CreateRegion(); if (outlineSaveRegion != null) { using (PdnRegion clipTest = activeRegion.Clone()) { clipTest.Intersect(outlineSaveRegion); if (!clipTest.IsEmpty()) { BitmapHistoryMemento bha = new BitmapHistoryMemento(Name, Image, this.DocumentWorkspace, ActiveLayerIndex, outlineSaveRegion, this.ScratchSurface); has.Add(bha); outlineSaveRegion.Dispose(); outlineSaveRegion = null; } } } if (interiorSaveRegion != null) { using (PdnRegion clipTest = activeRegion.Clone()) { clipTest.Intersect(interiorSaveRegion); if (!clipTest.IsEmpty()) { BitmapHistoryMemento bha = new BitmapHistoryMemento(Name, Image, this.DocumentWorkspace, ActiveLayerIndex, interiorSaveRegion, this.ScratchSurface); has.Add(bha); interiorSaveRegion.Dispose(); interiorSaveRegion = null; } } } if (has.Count > 0) { CompoundHistoryMemento cha = new CompoundHistoryMemento(Name, Image, (HistoryMemento[])has.ToArray(typeof(HistoryMemento))); if (this.chaAlreadyOnStack == null) { //HistoryStack.PushNewMemento(cha); } else { this.chaAlreadyOnStack.PushNewAction(cha); this.chaAlreadyOnStack = null; } } activeRegion.Dispose(); points = null; Update(); this.shapeWasCommited = true; } protected override void OnStylusUp(StylusEventArgs e) { base.OnStylusUp(e); Cursor = cursorMouseUp; if (moveOriginMode) { moveOriginMode = false; } else if (mouseDown) { bool doCommit = OnShapeEnd(); if (doCommit) { CommitShape(); } else { // place a 'sentinel' history action on the stack that will be filled in later CompoundHistoryMemento cha = new CompoundHistoryMemento(Name, Image, new List()); //HistoryStack.PushNewMemento(cha); this.chaAlreadyOnStack = cha; } } } public ShapeTool(IDocumentWorkspace documentWorkspace, ImageResource toolBarImage, string name, string helpText) : this(documentWorkspace, toolBarImage, name, helpText, defaultShortcut, ToolBarConfigItems.None, ToolBarConfigItems.None) { } public ShapeTool(IDocumentWorkspace documentWorkspace, ImageResource toolBarImage, string name, string helpText, ToolBarConfigItems toolBarConfigItemsInclude, ToolBarConfigItems toolBarConfigItemsExclude) : this(documentWorkspace, toolBarImage, name, helpText, defaultShortcut, toolBarConfigItemsInclude, toolBarConfigItemsExclude) { } public ShapeTool(IDocumentWorkspace documentWorkspace, ImageResource toolBarImage, string name, string helpText, char hotKey, ToolBarConfigItems toolBarConfigItemsInclude, ToolBarConfigItems toolBarConfigItemsExclude) : base(documentWorkspace, toolBarImage, name, helpText, hotKey, false, (toolBarConfigItemsInclude | (ToolBarConfigItems.Brush | ToolBarConfigItems.Pen | ToolBarConfigItems.ShapeType | ToolBarConfigItems.Antialiasing | ToolBarConfigItems.AlphaBlending)) & ~(toolBarConfigItemsExclude)) { this.mouseDown = false; this.points = null; this.cursorMouseUp = new Cursor(PdnResources.GetResourceStream("Cursors.ShapeToolCursor.cur")); this.cursorMouseDown = new Cursor(PdnResources.GetResourceStream("Cursors.ShapeToolCursorMouseDown.cur")); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { if (cursorMouseUp != null) { cursorMouseUp.Dispose(); cursorMouseUp = null; } if (cursorMouseDown != null) { cursorMouseDown.Dispose(); cursorMouseDown = null; } } } } }