using PaintDotNet.Measurement.Enum; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace PaintDotNet.Measurement.Tools { public class LineTool : ShapeTool { private const int controlPointCount = 4; private const float flattenConstant = 0.1f; private Cursor lineToolCursor; private Cursor lineToolMouseDownCursor; private string statusTextFormat = PdnResources.GetString("LineTool.StatusText.Format"); private ImageResource lineToolIcon; private MoveNubRenderer[] moveNubs; private bool inCurveMode = false; private int draggingNubIndex = -1; private CurveType curveType; private enum CurveType { NotDecided, Bezier, Spline } private PointF[] LineToSpline(PointF a, PointF b, int points) { PointF[] spline = new PointF[points]; for (int i = 0; i < spline.Length; ++i) { float frac = (float)i / (float)(spline.Length - 1); PointF mid = Utility.Lerp(a, b, frac); spline[i] = mid; } return spline; } protected override List TrimShapePath(List points) { if (this.inCurveMode) { return points; } else { List array = new List(); if (points.Count > 0) { array.Add(points[0]); if (points.Count > 1) { array.Add(points[points.Count - 1]); } } return array; } } private void ConstrainPoints(ref PointF a, ref PointF b) { PointF dir = new PointF(b.X - a.X, b.Y - a.Y); double theta = Math.Atan2(dir.Y, dir.X); double len = Math.Sqrt(dir.X * dir.X + dir.Y * dir.Y); theta = Math.Round(12 * theta / Math.PI) * Math.PI / 12; b = new PointF((float)(a.X + len * Math.Cos(theta)), (float)(a.Y + len * Math.Sin(theta))); } protected override PdnGraphicsPath CreateShapePath(PointF[] points) { if (points.Length >= 4) { PdnGraphicsPath path = new PdnGraphicsPath(); switch (this.curveType) { default: case CurveType.Spline: path.AddCurve(points); break; case CurveType.Bezier: path.AddBezier(points[0], points[1], points[2], points[3]); break; } path.Flatten(Utility.IdentityMatrix, flattenConstant); return path; } else //if (points.Length <= 2) { PointF a = points[0]; PointF b = points[points.Length - 1]; if (0 != (ModifierKeys & Keys.Shift) && a != b) { ConstrainPoints(ref a, ref b); } double angle = -180.0 * Math.Atan2(b.Y - a.Y, b.X - a.X) / Math.PI; MeasurementUnit units = AppWorkspace.GetUnits(); double offsetXPhysical = Document.PixelToPhysicalX(b.X - a.X, units); double offsetYPhysical = Document.PixelToPhysicalY(b.Y - a.Y, units); double offsetLengthPhysical = Math.Sqrt(offsetXPhysical * offsetXPhysical + offsetYPhysical * offsetYPhysical); string numberFormat; string unitsAbbreviation; if (units != MeasurementUnit.Pixel) { string unitsAbbreviationName = "MeasurementUnit." + units.ToString() + ".Abbreviation"; unitsAbbreviation = PdnResources.GetString(unitsAbbreviationName); numberFormat = "F2"; } else { unitsAbbreviation = string.Empty; numberFormat = "F0"; } string unitsString = PdnResources.GetString("MeasurementUnit." + units.ToString() + ".Plural"); string statusText = string.Format( this.statusTextFormat, offsetXPhysical.ToString(numberFormat), unitsAbbreviation, offsetYPhysical.ToString(numberFormat), unitsAbbreviation, offsetLengthPhysical.ToString("F2"), unitsString, angle.ToString("F2")); SetStatus(this.lineToolIcon, statusText); if (a == b) { return null; } else { PdnGraphicsPath path = new PdnGraphicsPath(); PointF[] spline = LineToSpline(a, b, controlPointCount); path.AddCurve(spline); path.Flatten(Utility.IdentityMatrix, flattenConstant); return path; } } } public override PixelOffsetMode GetPixelOffsetMode() { return PixelOffsetMode.None; } protected override void OnPulse() { if (this.moveNubs != null) { for (int i = 0; i < this.moveNubs.Length; ++i) { if (!this.moveNubs[i].Visible) { continue; } // Oscillate between 25% and 100% alpha over a period of 2 seconds // Alpha value of 100% is sustained for a large duration of this period const int period = 10000 * 2000; // 10000 ticks per ms, 2000ms per second long tick = (DateTime.Now.Ticks % period) + (i * (period / this.moveNubs.Length)); ; double sin = Math.Sin(((double)tick / (double)period) * (2.0 * Math.PI)); // sin is [-1, +1] sin = Math.Min(0.5, sin); // sin is [-1, +0.5] sin += 1.0; // sin is [0, 1.5] sin /= 2.0; // sin is [0, 0.75] sin += 0.25; // sin is [0.25, 1] int newAlpha = (int)(sin * 255.0); int clampedAlpha = Utility.Clamp(newAlpha, 0, 255); this.moveNubs[i].Alpha = clampedAlpha; } } base.OnPulse(); } private const int toggleStartCapOrdinal = 0; private const int toggleDashOrdinal = 1; private const int toggleEndCapOrdinal = 2; protected override bool OnWildShortcutKey(int ordinal) { switch (ordinal) { case toggleStartCapOrdinal: AppWorkspace.CyclePenStartCap();//Widgets.ToolConfigStrip. return true; case toggleDashOrdinal: AppWorkspace.CyclePenDashStyle();//Widgets.ToolConfigStrip. return true; case toggleEndCapOrdinal: AppWorkspace.CyclePenEndCap();//Widgets.ToolConfigStrip. return true; } return base.OnWildShortcutKey(ordinal); } private bool controlKeyDown = false; private DateTime controlKeyDownTime = DateTime.MinValue; private readonly TimeSpan controlKeyDownThreshold = new TimeSpan(0, 0, 0, 0, 400); protected override void OnKeyDown(KeyEventArgs e) { switch (e.KeyCode) { case Keys.ControlKey: if (!this.controlKeyDown) { this.controlKeyDown = true; this.controlKeyDownTime = DateTime.Now; } break; } base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { switch (e.KeyCode) { case Keys.ControlKey: TimeSpan heldDuration = (DateTime.Now - this.controlKeyDownTime); // If the user taps Ctrl, then we should toggle the visiblity of the moveNubs if (heldDuration < this.controlKeyDownThreshold) { for (int i = 0; i < this.moveNubs.Length; ++i) { this.moveNubs[i].Visible = this.inCurveMode && !this.moveNubs[i].Visible; } } this.controlKeyDown = false; break; } base.OnKeyUp(e); base.OnKeyUp(e); } protected override void OnKeyPress(KeyPressEventArgs e) { if (this.inCurveMode) { switch (e.KeyChar) { case '\r': // Enter e.Handled = true; CommitShape(); break; case (char)27: // Escape // Only recognize if the user is not pressing Ctrl. // Reason for this is that Ctrl+[ ends up being sent // to us as (char)27 as well, but the user probably // wants to use that for the decrease brush size // shortcut, not cancel :) if ((ModifierKeys & Keys.Control) == 0) { e.Handled = true; //HistoryStack.StepBackward(); } break; } } base.OnKeyPress(e); } protected override void OnShapeCommitting() { for (int i = 0; i < this.moveNubs.Length; ++i) { this.moveNubs[i].Visible = false; } this.inCurveMode = false; this.curveType = CurveType.NotDecided; this.Cursor = this.lineToolCursor; this.draggingNubIndex = -1; DocumentWorkspace.UpdateStatusBarToToolHelpText(); } protected override bool OnShapeEnd() { // init move nubs List points = GetTrimmedShapePath(); if (points.Count < 2) { return true; } else { PointF a = (PointF)points[0]; PointF b = (PointF)points[points.Count - 1]; if (0 != (ModifierKeys & Keys.Shift) && a != b) { ConstrainPoints(ref a, ref b); } PointF[] spline = LineToSpline(a, b, controlPointCount); List newPoints = new List(); this.inCurveMode = true; for (int i = 0; i < this.moveNubs.Length; ++i) { this.moveNubs[i].Location = spline[i]; this.moveNubs[i].Visible = true; newPoints.Add(spline[i]); } string helpText2 = PdnResources.GetString("LineTool.PreCurveHelpText"); this.SetStatus(null, helpText2); SetShapePath(newPoints); return false; } } protected override void OnStylusDown(StylusEventArgs e) { bool callBase = false; if (!this.inCurveMode) { callBase = true; } else { PointF mousePtF = new PointF(e.Fx, e.Fy); Point mousePt = Point.Truncate(mousePtF); float minDistance = float.MaxValue; for (int i = 0; i < this.moveNubs.Length; ++i) { if (this.moveNubs[i].IsPointTouching(mousePt, true)) { float distance = Utility.Distance(mousePtF, this.moveNubs[i].Location); if (distance < minDistance) { minDistance = distance; this.draggingNubIndex = i; } } } if (this.draggingNubIndex == -1) { callBase = true; } else { this.Cursor = this.handCursorMouseDown; if (this.curveType == CurveType.NotDecided) { if (e.Button == MouseButtons.Right) { this.curveType = CurveType.Bezier; } else { this.curveType = CurveType.Spline; } } for (int i = 0; i < this.moveNubs.Length; ++i) { this.moveNubs[i].Visible = false; } string helpText2 = PdnResources.GetString("LineTool.CurvingHelpText"); SetStatus(null, helpText2); OnStylusMove(e); } } if (callBase) { base.OnStylusDown(e); Cursor = this.lineToolMouseDownCursor; } } protected override void OnMouseDown(MouseEventArgs e) { if (!this.inCurveMode) { base.OnMouseDown(e); } } protected override void OnStylusUp(StylusEventArgs e) { if (!this.inCurveMode) { base.OnStylusUp(e); } else { if (this.draggingNubIndex != -1) { OnStylusMove(e); this.draggingNubIndex = -1; this.Cursor = this.lineToolCursor; for (int i = 0; i < this.moveNubs.Length; ++i) { this.moveNubs[i].Visible = true; } } } } protected override void OnMouseUp(MouseEventArgs e) { if (!this.inCurveMode) { base.OnMouseUp(e); } } protected override void OnStylusMove(StylusEventArgs e) { if (!this.inCurveMode) { base.OnStylusMove(e); } else if (this.draggingNubIndex != -1) { PointF mousePt = new PointF(e.Fx, e.Fy); this.moveNubs[this.draggingNubIndex].Location = mousePt; List points = GetTrimmedShapePath(); points[this.draggingNubIndex] = mousePt; SetShapePath(points); } } protected override void OnMouseMove(MouseEventArgs e) { if (this.draggingNubIndex != -1) { RenderShape(); Update(); } else { Point mousePt = new Point(e.X, e.Y); bool hot = false; for (int i = 0; i < this.moveNubs.Length; ++i) { if (this.moveNubs[i].Visible && this.moveNubs[i].IsPointTouching(Point.Truncate(mousePt), true)) { this.Cursor = this.handCursor; hot = true; break; } } if (!hot) { if (IsMouseDown) { Cursor = this.lineToolMouseDownCursor; } else { Cursor = this.lineToolCursor; } } } base.OnMouseMove(e); } protected override void OnActivate() { this.lineToolCursor = new Cursor(PdnResources.GetResourceStream("Cursors.LineToolCursor.cur")); this.lineToolMouseDownCursor = new Cursor(PdnResources.GetResourceStream("Cursors.GenericToolCursorMouseDown.cur")); this.Cursor = this.lineToolCursor; this.lineToolIcon = this.Image; this.moveNubs = new MoveNubRenderer[controlPointCount]; for (int i = 0; i < this.moveNubs.Length; ++i) { this.moveNubs[i] = new MoveNubRenderer(this.RendererList); this.moveNubs[i].Visible = false; this.RendererList.Add(this.moveNubs[i], false); } AppEnvironment.PrimaryColorChanged += new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.SecondaryColorChanged += new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.AntiAliasingChanged += new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.AlphaBlendingChanged += new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.BrushInfoChanged += new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.PenInfoChanged += new EventHandler(RenderShapeBecauseOfEvent); AppWorkspace.UnitsChanged += new EventHandler(RenderShapeBecauseOfEvent); base.OnActivate(); } private void RenderShapeBecauseOfEvent(object sender, EventArgs e) { if (this.inCurveMode) { RenderShape(); } } protected override void OnDeactivate() { base.OnDeactivate(); AppEnvironment.PrimaryColorChanged -= new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.SecondaryColorChanged -= new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.AntiAliasingChanged -= new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.AlphaBlendingChanged -= new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.BrushInfoChanged -= new EventHandler(RenderShapeBecauseOfEvent); AppEnvironment.PenInfoChanged -= new EventHandler(RenderShapeBecauseOfEvent); AppWorkspace.UnitsChanged -= new EventHandler(RenderShapeBecauseOfEvent); for (int i = 0; i < this.moveNubs.Length; ++i) { this.RendererList.Remove(this.moveNubs[i]); this.moveNubs[i].Dispose(); this.moveNubs[i] = null; } this.moveNubs = null; if (this.lineToolCursor != null) { this.lineToolCursor.Dispose(); this.lineToolCursor = null; } if (this.lineToolMouseDownCursor != null) { this.lineToolMouseDownCursor.Dispose(); this.lineToolMouseDownCursor = null; } } public LineTool(IDocumentWorkspace documentWorkspace) : base(documentWorkspace, PdnResources.GetImageResource("Icons.LineToolIcon.png"), PdnResources.GetString("LineTool.Name"), PdnResources.GetString("LineTool.HelpText"), ToolBarConfigItems.None | ToolBarConfigItems.PenCaps, ToolBarConfigItems.ShapeType) { this.ForceShapeDrawType = true; this.ForcedShapeDrawType = ShapeDrawType.Outline; this.UseDashStyle = true; } } }