LineTool.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. using PaintDotNet.Measurement.Enum;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Drawing;
  5. using System.Drawing.Drawing2D;
  6. using System.Windows.Forms;
  7. namespace PaintDotNet.Measurement.Tools
  8. {
  9. public class LineTool : ShapeTool
  10. {
  11. private const int controlPointCount = 4;
  12. private const float flattenConstant = 0.1f;
  13. private Cursor lineToolCursor;
  14. private Cursor lineToolMouseDownCursor;
  15. private string statusTextFormat = PdnResources.GetString("LineTool.StatusText.Format");
  16. private ImageResource lineToolIcon;
  17. private MoveNubRenderer[] moveNubs;
  18. private bool inCurveMode = false;
  19. private int draggingNubIndex = -1;
  20. private CurveType curveType;
  21. private enum CurveType
  22. {
  23. NotDecided,
  24. Bezier,
  25. Spline
  26. }
  27. private PointF[] LineToSpline(PointF a, PointF b, int points)
  28. {
  29. PointF[] spline = new PointF[points];
  30. for (int i = 0; i < spline.Length; ++i)
  31. {
  32. float frac = (float)i / (float)(spline.Length - 1);
  33. PointF mid = Utility.Lerp(a, b, frac);
  34. spline[i] = mid;
  35. }
  36. return spline;
  37. }
  38. protected override List<PointF> TrimShapePath(List<PointF> points)
  39. {
  40. if (this.inCurveMode)
  41. {
  42. return points;
  43. }
  44. else
  45. {
  46. List<PointF> array = new List<PointF>();
  47. if (points.Count > 0)
  48. {
  49. array.Add(points[0]);
  50. if (points.Count > 1)
  51. {
  52. array.Add(points[points.Count - 1]);
  53. }
  54. }
  55. return array;
  56. }
  57. }
  58. private void ConstrainPoints(ref PointF a, ref PointF b)
  59. {
  60. PointF dir = new PointF(b.X - a.X, b.Y - a.Y);
  61. double theta = Math.Atan2(dir.Y, dir.X);
  62. double len = Math.Sqrt(dir.X * dir.X + dir.Y * dir.Y);
  63. theta = Math.Round(12 * theta / Math.PI) * Math.PI / 12;
  64. b = new PointF((float)(a.X + len * Math.Cos(theta)), (float)(a.Y + len * Math.Sin(theta)));
  65. }
  66. protected override PdnGraphicsPath CreateShapePath(PointF[] points)
  67. {
  68. if (points.Length >= 4)
  69. {
  70. PdnGraphicsPath path = new PdnGraphicsPath();
  71. switch (this.curveType)
  72. {
  73. default:
  74. case CurveType.Spline:
  75. path.AddCurve(points);
  76. break;
  77. case CurveType.Bezier:
  78. path.AddBezier(points[0], points[1], points[2], points[3]);
  79. break;
  80. }
  81. path.Flatten(Utility.IdentityMatrix, flattenConstant);
  82. return path;
  83. }
  84. else //if (points.Length <= 2)
  85. {
  86. PointF a = points[0];
  87. PointF b = points[points.Length - 1];
  88. if (0 != (ModifierKeys & Keys.Shift) && a != b)
  89. {
  90. ConstrainPoints(ref a, ref b);
  91. }
  92. double angle = -180.0 * Math.Atan2(b.Y - a.Y, b.X - a.X) / Math.PI;
  93. MeasurementUnit units = AppWorkspace.GetUnits();
  94. double offsetXPhysical = Document.PixelToPhysicalX(b.X - a.X, units);
  95. double offsetYPhysical = Document.PixelToPhysicalY(b.Y - a.Y, units);
  96. double offsetLengthPhysical = Math.Sqrt(offsetXPhysical * offsetXPhysical + offsetYPhysical * offsetYPhysical);
  97. string numberFormat;
  98. string unitsAbbreviation;
  99. if (units != MeasurementUnit.Pixel)
  100. {
  101. string unitsAbbreviationName = "MeasurementUnit." + units.ToString() + ".Abbreviation";
  102. unitsAbbreviation = PdnResources.GetString(unitsAbbreviationName);
  103. numberFormat = "F2";
  104. }
  105. else
  106. {
  107. unitsAbbreviation = string.Empty;
  108. numberFormat = "F0";
  109. }
  110. string unitsString = PdnResources.GetString("MeasurementUnit." + units.ToString() + ".Plural");
  111. string statusText = string.Format(
  112. this.statusTextFormat,
  113. offsetXPhysical.ToString(numberFormat),
  114. unitsAbbreviation,
  115. offsetYPhysical.ToString(numberFormat),
  116. unitsAbbreviation,
  117. offsetLengthPhysical.ToString("F2"),
  118. unitsString,
  119. angle.ToString("F2"));
  120. SetStatus(this.lineToolIcon, statusText);
  121. if (a == b)
  122. {
  123. return null;
  124. }
  125. else
  126. {
  127. PdnGraphicsPath path = new PdnGraphicsPath();
  128. PointF[] spline = LineToSpline(a, b, controlPointCount);
  129. path.AddCurve(spline);
  130. path.Flatten(Utility.IdentityMatrix, flattenConstant);
  131. return path;
  132. }
  133. }
  134. }
  135. public override PixelOffsetMode GetPixelOffsetMode()
  136. {
  137. return PixelOffsetMode.None;
  138. }
  139. protected override void OnPulse()
  140. {
  141. if (this.moveNubs != null)
  142. {
  143. for (int i = 0; i < this.moveNubs.Length; ++i)
  144. {
  145. if (!this.moveNubs[i].Visible)
  146. {
  147. continue;
  148. }
  149. // Oscillate between 25% and 100% alpha over a period of 2 seconds
  150. // Alpha value of 100% is sustained for a large duration of this period
  151. const int period = 10000 * 2000; // 10000 ticks per ms, 2000ms per second
  152. long tick = (DateTime.Now.Ticks % period) + (i * (period / this.moveNubs.Length)); ;
  153. double sin = Math.Sin(((double)tick / (double)period) * (2.0 * Math.PI));
  154. // sin is [-1, +1]
  155. sin = Math.Min(0.5, sin);
  156. // sin is [-1, +0.5]
  157. sin += 1.0;
  158. // sin is [0, 1.5]
  159. sin /= 2.0;
  160. // sin is [0, 0.75]
  161. sin += 0.25;
  162. // sin is [0.25, 1]
  163. int newAlpha = (int)(sin * 255.0);
  164. int clampedAlpha = Utility.Clamp(newAlpha, 0, 255);
  165. this.moveNubs[i].Alpha = clampedAlpha;
  166. }
  167. }
  168. base.OnPulse();
  169. }
  170. private const int toggleStartCapOrdinal = 0;
  171. private const int toggleDashOrdinal = 1;
  172. private const int toggleEndCapOrdinal = 2;
  173. protected override bool OnWildShortcutKey(int ordinal)
  174. {
  175. switch (ordinal)
  176. {
  177. case toggleStartCapOrdinal:
  178. AppWorkspace.CyclePenStartCap();//Widgets.ToolConfigStrip.
  179. return true;
  180. case toggleDashOrdinal:
  181. AppWorkspace.CyclePenDashStyle();//Widgets.ToolConfigStrip.
  182. return true;
  183. case toggleEndCapOrdinal:
  184. AppWorkspace.CyclePenEndCap();//Widgets.ToolConfigStrip.
  185. return true;
  186. }
  187. return base.OnWildShortcutKey(ordinal);
  188. }
  189. private bool controlKeyDown = false;
  190. private DateTime controlKeyDownTime = DateTime.MinValue;
  191. private readonly TimeSpan controlKeyDownThreshold = new TimeSpan(0, 0, 0, 0, 400);
  192. protected override void OnKeyDown(KeyEventArgs e)
  193. {
  194. switch (e.KeyCode)
  195. {
  196. case Keys.ControlKey:
  197. if (!this.controlKeyDown)
  198. {
  199. this.controlKeyDown = true;
  200. this.controlKeyDownTime = DateTime.Now;
  201. }
  202. break;
  203. }
  204. base.OnKeyDown(e);
  205. }
  206. protected override void OnKeyUp(KeyEventArgs e)
  207. {
  208. switch (e.KeyCode)
  209. {
  210. case Keys.ControlKey:
  211. TimeSpan heldDuration = (DateTime.Now - this.controlKeyDownTime);
  212. // If the user taps Ctrl, then we should toggle the visiblity of the moveNubs
  213. if (heldDuration < this.controlKeyDownThreshold)
  214. {
  215. for (int i = 0; i < this.moveNubs.Length; ++i)
  216. {
  217. this.moveNubs[i].Visible = this.inCurveMode && !this.moveNubs[i].Visible;
  218. }
  219. }
  220. this.controlKeyDown = false;
  221. break;
  222. }
  223. base.OnKeyUp(e); base.OnKeyUp(e);
  224. }
  225. protected override void OnKeyPress(KeyPressEventArgs e)
  226. {
  227. if (this.inCurveMode)
  228. {
  229. switch (e.KeyChar)
  230. {
  231. case '\r': // Enter
  232. e.Handled = true;
  233. CommitShape();
  234. break;
  235. case (char)27: // Escape
  236. // Only recognize if the user is not pressing Ctrl.
  237. // Reason for this is that Ctrl+[ ends up being sent
  238. // to us as (char)27 as well, but the user probably
  239. // wants to use that for the decrease brush size
  240. // shortcut, not cancel :)
  241. if ((ModifierKeys & Keys.Control) == 0)
  242. {
  243. e.Handled = true;
  244. //HistoryStack.StepBackward();
  245. }
  246. break;
  247. }
  248. }
  249. base.OnKeyPress(e);
  250. }
  251. protected override void OnShapeCommitting()
  252. {
  253. for (int i = 0; i < this.moveNubs.Length; ++i)
  254. {
  255. this.moveNubs[i].Visible = false;
  256. }
  257. this.inCurveMode = false;
  258. this.curveType = CurveType.NotDecided;
  259. this.Cursor = this.lineToolCursor;
  260. this.draggingNubIndex = -1;
  261. DocumentWorkspace.UpdateStatusBarToToolHelpText();
  262. }
  263. protected override bool OnShapeEnd()
  264. {
  265. // init move nubs
  266. List<PointF> points = GetTrimmedShapePath();
  267. if (points.Count < 2)
  268. {
  269. return true;
  270. }
  271. else
  272. {
  273. PointF a = (PointF)points[0];
  274. PointF b = (PointF)points[points.Count - 1];
  275. if (0 != (ModifierKeys & Keys.Shift) && a != b)
  276. {
  277. ConstrainPoints(ref a, ref b);
  278. }
  279. PointF[] spline = LineToSpline(a, b, controlPointCount);
  280. List<PointF> newPoints = new List<PointF>();
  281. this.inCurveMode = true;
  282. for (int i = 0; i < this.moveNubs.Length; ++i)
  283. {
  284. this.moveNubs[i].Location = spline[i];
  285. this.moveNubs[i].Visible = true;
  286. newPoints.Add(spline[i]);
  287. }
  288. string helpText2 = PdnResources.GetString("LineTool.PreCurveHelpText");
  289. this.SetStatus(null, helpText2);
  290. SetShapePath(newPoints);
  291. return false;
  292. }
  293. }
  294. protected override void OnStylusDown(StylusEventArgs e)
  295. {
  296. bool callBase = false;
  297. if (!this.inCurveMode)
  298. {
  299. callBase = true;
  300. }
  301. else
  302. {
  303. PointF mousePtF = new PointF(e.Fx, e.Fy);
  304. Point mousePt = Point.Truncate(mousePtF);
  305. float minDistance = float.MaxValue;
  306. for (int i = 0; i < this.moveNubs.Length; ++i)
  307. {
  308. if (this.moveNubs[i].IsPointTouching(mousePt, true))
  309. {
  310. float distance = Utility.Distance(mousePtF, this.moveNubs[i].Location);
  311. if (distance < minDistance)
  312. {
  313. minDistance = distance;
  314. this.draggingNubIndex = i;
  315. }
  316. }
  317. }
  318. if (this.draggingNubIndex == -1)
  319. {
  320. callBase = true;
  321. }
  322. else
  323. {
  324. this.Cursor = this.handCursorMouseDown;
  325. if (this.curveType == CurveType.NotDecided)
  326. {
  327. if (e.Button == MouseButtons.Right)
  328. {
  329. this.curveType = CurveType.Bezier;
  330. }
  331. else
  332. {
  333. this.curveType = CurveType.Spline;
  334. }
  335. }
  336. for (int i = 0; i < this.moveNubs.Length; ++i)
  337. {
  338. this.moveNubs[i].Visible = false;
  339. }
  340. string helpText2 = PdnResources.GetString("LineTool.CurvingHelpText");
  341. SetStatus(null, helpText2);
  342. OnStylusMove(e);
  343. }
  344. }
  345. if (callBase)
  346. {
  347. base.OnStylusDown(e);
  348. Cursor = this.lineToolMouseDownCursor;
  349. }
  350. }
  351. protected override void OnMouseDown(MouseEventArgs e)
  352. {
  353. if (!this.inCurveMode)
  354. {
  355. base.OnMouseDown(e);
  356. }
  357. }
  358. protected override void OnStylusUp(StylusEventArgs e)
  359. {
  360. if (!this.inCurveMode)
  361. {
  362. base.OnStylusUp(e);
  363. }
  364. else
  365. {
  366. if (this.draggingNubIndex != -1)
  367. {
  368. OnStylusMove(e);
  369. this.draggingNubIndex = -1;
  370. this.Cursor = this.lineToolCursor;
  371. for (int i = 0; i < this.moveNubs.Length; ++i)
  372. {
  373. this.moveNubs[i].Visible = true;
  374. }
  375. }
  376. }
  377. }
  378. protected override void OnMouseUp(MouseEventArgs e)
  379. {
  380. if (!this.inCurveMode)
  381. {
  382. base.OnMouseUp(e);
  383. }
  384. }
  385. protected override void OnStylusMove(StylusEventArgs e)
  386. {
  387. if (!this.inCurveMode)
  388. {
  389. base.OnStylusMove(e);
  390. }
  391. else if (this.draggingNubIndex != -1)
  392. {
  393. PointF mousePt = new PointF(e.Fx, e.Fy);
  394. this.moveNubs[this.draggingNubIndex].Location = mousePt;
  395. List<PointF> points = GetTrimmedShapePath();
  396. points[this.draggingNubIndex] = mousePt;
  397. SetShapePath(points);
  398. }
  399. }
  400. protected override void OnMouseMove(MouseEventArgs e)
  401. {
  402. if (this.draggingNubIndex != -1)
  403. {
  404. RenderShape();
  405. Update();
  406. }
  407. else
  408. {
  409. Point mousePt = new Point(e.X, e.Y);
  410. bool hot = false;
  411. for (int i = 0; i < this.moveNubs.Length; ++i)
  412. {
  413. if (this.moveNubs[i].Visible && this.moveNubs[i].IsPointTouching(Point.Truncate(mousePt), true))
  414. {
  415. this.Cursor = this.handCursor;
  416. hot = true;
  417. break;
  418. }
  419. }
  420. if (!hot)
  421. {
  422. if (IsMouseDown)
  423. {
  424. Cursor = this.lineToolMouseDownCursor;
  425. }
  426. else
  427. {
  428. Cursor = this.lineToolCursor;
  429. }
  430. }
  431. }
  432. base.OnMouseMove(e);
  433. }
  434. protected override void OnActivate()
  435. {
  436. this.lineToolCursor = new Cursor(PdnResources.GetResourceStream("Cursors.LineToolCursor.cur"));
  437. this.lineToolMouseDownCursor = new Cursor(PdnResources.GetResourceStream("Cursors.GenericToolCursorMouseDown.cur"));
  438. this.Cursor = this.lineToolCursor;
  439. this.lineToolIcon = this.Image;
  440. this.moveNubs = new MoveNubRenderer[controlPointCount];
  441. for (int i = 0; i < this.moveNubs.Length; ++i)
  442. {
  443. this.moveNubs[i] = new MoveNubRenderer(this.RendererList);
  444. this.moveNubs[i].Visible = false;
  445. this.RendererList.Add(this.moveNubs[i], false);
  446. }
  447. AppEnvironment.PrimaryColorChanged += new EventHandler(RenderShapeBecauseOfEvent);
  448. AppEnvironment.SecondaryColorChanged += new EventHandler(RenderShapeBecauseOfEvent);
  449. AppEnvironment.AntiAliasingChanged += new EventHandler(RenderShapeBecauseOfEvent);
  450. AppEnvironment.AlphaBlendingChanged += new EventHandler(RenderShapeBecauseOfEvent);
  451. AppEnvironment.BrushInfoChanged += new EventHandler(RenderShapeBecauseOfEvent);
  452. AppEnvironment.PenInfoChanged += new EventHandler(RenderShapeBecauseOfEvent);
  453. AppWorkspace.UnitsChanged += new EventHandler(RenderShapeBecauseOfEvent);
  454. base.OnActivate();
  455. }
  456. private void RenderShapeBecauseOfEvent(object sender, EventArgs e)
  457. {
  458. if (this.inCurveMode)
  459. {
  460. RenderShape();
  461. }
  462. }
  463. protected override void OnDeactivate()
  464. {
  465. base.OnDeactivate();
  466. AppEnvironment.PrimaryColorChanged -= new EventHandler(RenderShapeBecauseOfEvent);
  467. AppEnvironment.SecondaryColorChanged -= new EventHandler(RenderShapeBecauseOfEvent);
  468. AppEnvironment.AntiAliasingChanged -= new EventHandler(RenderShapeBecauseOfEvent);
  469. AppEnvironment.AlphaBlendingChanged -= new EventHandler(RenderShapeBecauseOfEvent);
  470. AppEnvironment.BrushInfoChanged -= new EventHandler(RenderShapeBecauseOfEvent);
  471. AppEnvironment.PenInfoChanged -= new EventHandler(RenderShapeBecauseOfEvent);
  472. AppWorkspace.UnitsChanged -= new EventHandler(RenderShapeBecauseOfEvent);
  473. for (int i = 0; i < this.moveNubs.Length; ++i)
  474. {
  475. this.RendererList.Remove(this.moveNubs[i]);
  476. this.moveNubs[i].Dispose();
  477. this.moveNubs[i] = null;
  478. }
  479. this.moveNubs = null;
  480. if (this.lineToolCursor != null)
  481. {
  482. this.lineToolCursor.Dispose();
  483. this.lineToolCursor = null;
  484. }
  485. if (this.lineToolMouseDownCursor != null)
  486. {
  487. this.lineToolMouseDownCursor.Dispose();
  488. this.lineToolMouseDownCursor = null;
  489. }
  490. }
  491. public LineTool(IDocumentWorkspace documentWorkspace)
  492. : base(documentWorkspace,
  493. PdnResources.GetImageResource("Icons.LineToolIcon.png"),
  494. PdnResources.GetString("LineTool.Name"),
  495. PdnResources.GetString("LineTool.HelpText"),
  496. ToolBarConfigItems.None | ToolBarConfigItems.PenCaps,
  497. ToolBarConfigItems.ShapeType)
  498. {
  499. this.ForceShapeDrawType = true;
  500. this.ForcedShapeDrawType = ShapeDrawType.Outline;
  501. this.UseDashStyle = true;
  502. }
  503. }
  504. }