MapPanel.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. //
  2. // Copyright 2020 Electronic Arts Inc.
  3. //
  4. // The Command & Conquer Map Editor and corresponding source code is free
  5. // software: you can redistribute it and/or modify it under the terms of
  6. // the GNU General Public License as published by the Free Software Foundation,
  7. // either version 3 of the License, or (at your option) any later version.
  8. // The Command & Conquer Map Editor and corresponding source code is distributed
  9. // in the hope that it will be useful, but with permitted additional restrictions
  10. // under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
  11. // distributed with this program. You should have received a copy of the
  12. // GNU General Public License along with permitted additional restrictions
  13. // with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
  14. using MobiusEditor.Event;
  15. using MobiusEditor.Interface;
  16. using MobiusEditor.Model;
  17. using MobiusEditor.Utility;
  18. using System;
  19. using System.Collections.Generic;
  20. using System.ComponentModel;
  21. using System.Drawing;
  22. using System.Drawing.Drawing2D;
  23. using System.Linq;
  24. using System.Runtime.InteropServices;
  25. using System.Windows.Forms;
  26. namespace MobiusEditor.Controls
  27. {
  28. public partial class MapPanel : Panel
  29. {
  30. private bool updatingCamera;
  31. private Rectangle cameraBounds;
  32. private Point lastScrollPosition;
  33. private (Point map, SizeF client)? referencePositions;
  34. private Matrix mapToViewTransform = new Matrix();
  35. private Matrix viewToPageTransform = new Matrix();
  36. private Matrix compositeTransform = new Matrix();
  37. private Matrix invCompositeTransform = new Matrix();
  38. private readonly HashSet<Point> invalidateCells = new HashSet<Point>();
  39. private bool fullInvalidation;
  40. private Image mapImage;
  41. public Image MapImage
  42. {
  43. get => mapImage;
  44. set
  45. {
  46. if (mapImage != value)
  47. {
  48. mapImage = value;
  49. UpdateCamera();
  50. }
  51. }
  52. }
  53. private int minZoom = 1;
  54. public int MinZoom
  55. {
  56. get => minZoom;
  57. set
  58. {
  59. if (minZoom != value)
  60. {
  61. minZoom = value;
  62. Zoom = zoom;
  63. }
  64. }
  65. }
  66. private int maxZoom = 8;
  67. public int MaxZoom
  68. {
  69. get => maxZoom;
  70. set
  71. {
  72. if (maxZoom != value)
  73. {
  74. maxZoom = value;
  75. Zoom = zoom;
  76. }
  77. }
  78. }
  79. private int zoomStep = 1;
  80. public int ZoomStep
  81. {
  82. get => zoomStep;
  83. set
  84. {
  85. if (zoomStep != value)
  86. {
  87. zoomStep = value;
  88. Zoom = (Zoom / zoomStep) * zoomStep;
  89. }
  90. }
  91. }
  92. private int zoom = 1;
  93. public int Zoom
  94. {
  95. get => zoom;
  96. set
  97. {
  98. var newZoom = Math.Max(MinZoom, Math.Min(MaxZoom, value));
  99. if (zoom != newZoom)
  100. {
  101. zoom = newZoom;
  102. var clientPosition = PointToClient(MousePosition);
  103. referencePositions = (ClientToMap(clientPosition), new SizeF(clientPosition.X / (float)ClientSize.Width, clientPosition.Y / (float)ClientSize.Height));
  104. UpdateCamera();
  105. }
  106. }
  107. }
  108. private int quality = Properties.Settings.Default.Quality;
  109. public int Quality
  110. {
  111. get => quality;
  112. set
  113. {
  114. if (quality != value)
  115. {
  116. quality = value;
  117. Invalidate();
  118. }
  119. }
  120. }
  121. [Category("Behavior")]
  122. [DefaultValue(false)]
  123. public bool FocusOnMouseEnter { get; set; }
  124. public event EventHandler<RenderEventArgs> PreRender;
  125. public event EventHandler<RenderEventArgs> PostRender;
  126. public MapPanel()
  127. {
  128. InitializeComponent();
  129. DoubleBuffered = true;
  130. }
  131. public Point MapToClient(Point point)
  132. {
  133. var points = new Point[] { point };
  134. compositeTransform.TransformPoints(points);
  135. return points[0];
  136. }
  137. public Size MapToClient(Size size)
  138. {
  139. var points = new Point[] { (Point)size };
  140. compositeTransform.VectorTransformPoints(points);
  141. return (Size)points[0];
  142. }
  143. public Rectangle MapToClient(Rectangle rectangle)
  144. {
  145. var points = new Point[] { rectangle.Location, new Point(rectangle.Right, rectangle.Bottom) };
  146. compositeTransform.TransformPoints(points);
  147. return new Rectangle(points[0], new Size(points[1].X - points[0].X, points[1].Y - points[0].Y));
  148. }
  149. public Point ClientToMap(Point point)
  150. {
  151. var points = new Point[] { point };
  152. invCompositeTransform.TransformPoints(points);
  153. return points[0];
  154. }
  155. public Size ClientToMap(Size size)
  156. {
  157. var points = new Point[] { (Point)size };
  158. invCompositeTransform.VectorTransformPoints(points);
  159. return (Size)points[0];
  160. }
  161. public Rectangle ClientToMap(Rectangle rectangle)
  162. {
  163. var points = new Point[] { rectangle.Location, new Point(rectangle.Right, rectangle.Bottom) };
  164. invCompositeTransform.TransformPoints(points);
  165. return new Rectangle(points[0], new Size(points[1].X - points[0].X, points[1].Y - points[0].Y));
  166. }
  167. public void Invalidate(Map invalidateMap)
  168. {
  169. if (!fullInvalidation)
  170. {
  171. invalidateCells.Clear();
  172. fullInvalidation = true;
  173. Invalidate();
  174. }
  175. }
  176. public void Invalidate(Map invalidateMap, Rectangle cellBounds)
  177. {
  178. if (fullInvalidation)
  179. {
  180. return;
  181. }
  182. var count = invalidateCells.Count;
  183. invalidateCells.UnionWith(cellBounds.Points());
  184. if (invalidateCells.Count > count)
  185. {
  186. var overlapCells = invalidateMap.Overlappers.Overlaps(invalidateCells).ToHashSet();
  187. invalidateCells.UnionWith(overlapCells);
  188. Invalidate();
  189. }
  190. }
  191. public void Invalidate(Map invalidateMap, IEnumerable<Rectangle> cellBounds)
  192. {
  193. if (fullInvalidation)
  194. {
  195. return;
  196. }
  197. var count = invalidateCells.Count;
  198. invalidateCells.UnionWith(cellBounds.SelectMany(c => c.Points()));
  199. if (invalidateCells.Count > count)
  200. {
  201. var overlapCells = invalidateMap.Overlappers.Overlaps(invalidateCells).ToHashSet();
  202. invalidateCells.UnionWith(overlapCells);
  203. Invalidate();
  204. }
  205. }
  206. public void Invalidate(Map invalidateMap, Point location)
  207. {
  208. if (fullInvalidation)
  209. {
  210. return;
  211. }
  212. Invalidate(invalidateMap, new Rectangle(location, new Size(1, 1)));
  213. }
  214. public void Invalidate(Map invalidateMap, IEnumerable<Point> locations)
  215. {
  216. if (fullInvalidation)
  217. {
  218. return;
  219. }
  220. Invalidate(invalidateMap, locations.Select(l => new Rectangle(l, new Size(1, 1))));
  221. }
  222. public void Invalidate(Map invalidateMap, int cell)
  223. {
  224. if (fullInvalidation)
  225. {
  226. return;
  227. }
  228. if (invalidateMap.Metrics.GetLocation(cell, out Point location))
  229. {
  230. Invalidate(invalidateMap, location);
  231. }
  232. }
  233. public void Invalidate(Map invalidateMap, IEnumerable<int> cells)
  234. {
  235. if (fullInvalidation)
  236. {
  237. return;
  238. }
  239. Invalidate(invalidateMap, cells
  240. .Where(c => invalidateMap.Metrics.GetLocation(c, out Point location))
  241. .Select(c =>
  242. {
  243. invalidateMap.Metrics.GetLocation(c, out Point location);
  244. return location;
  245. })
  246. );
  247. }
  248. public void Invalidate(Map invalidateMap, ICellOverlapper overlapper)
  249. {
  250. if (fullInvalidation)
  251. {
  252. return;
  253. }
  254. var rectangle = invalidateMap.Overlappers[overlapper];
  255. if (rectangle.HasValue)
  256. {
  257. Invalidate(invalidateMap, rectangle.Value);
  258. }
  259. }
  260. protected override void OnMouseEnter(EventArgs e)
  261. {
  262. base.OnMouseEnter(e);
  263. if (FocusOnMouseEnter)
  264. {
  265. Focus();
  266. }
  267. }
  268. protected override void OnMouseWheel(MouseEventArgs e)
  269. {
  270. Zoom += ZoomStep * Math.Sign(e.Delta);
  271. }
  272. protected override void OnClientSizeChanged(EventArgs e)
  273. {
  274. base.OnClientSizeChanged(e);
  275. UpdateCamera();
  276. }
  277. protected override void OnScroll(ScrollEventArgs se)
  278. {
  279. base.OnScroll(se);
  280. InvalidateScroll();
  281. }
  282. protected override void OnPaintBackground(PaintEventArgs e)
  283. {
  284. base.OnPaintBackground(e);
  285. e.Graphics.Clear(BackColor);
  286. }
  287. protected override void OnPaint(PaintEventArgs pe)
  288. {
  289. base.OnPaint(pe);
  290. InvalidateScroll();
  291. PreRender?.Invoke(this, new RenderEventArgs(pe.Graphics, fullInvalidation ? null : invalidateCells));
  292. if (mapImage != null)
  293. {
  294. pe.Graphics.Transform = compositeTransform;
  295. var oldCompositingMode = pe.Graphics.CompositingMode;
  296. var oldCompositingQuality = pe.Graphics.CompositingQuality;
  297. var oldInterpolationMode = pe.Graphics.InterpolationMode;
  298. if (Quality > 1)
  299. {
  300. pe.Graphics.CompositingMode = CompositingMode.SourceCopy;
  301. pe.Graphics.CompositingQuality = CompositingQuality.HighSpeed;
  302. }
  303. pe.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
  304. pe.Graphics.DrawImage(mapImage, 0, 0);
  305. pe.Graphics.CompositingMode = oldCompositingMode;
  306. pe.Graphics.CompositingQuality = oldCompositingQuality;
  307. pe.Graphics.InterpolationMode = oldInterpolationMode;
  308. }
  309. PostRender?.Invoke(this, new RenderEventArgs(pe.Graphics, fullInvalidation ? null : invalidateCells));
  310. #if DEVELOPER
  311. if (Globals.Developer.ShowOverlapCells)
  312. {
  313. var invalidPen = new Pen(Color.DarkRed);
  314. foreach (var cell in invalidateCells)
  315. {
  316. pe.Graphics.DrawRectangle(invalidPen, new Rectangle(cell.X * Globals.TileWidth, cell.Y * Globals.TileHeight, Globals.TileWidth, Globals.TileHeight));
  317. }
  318. }
  319. #endif
  320. invalidateCells.Clear();
  321. fullInvalidation = false;
  322. }
  323. private void UpdateCamera()
  324. {
  325. if (mapImage == null)
  326. {
  327. return;
  328. }
  329. if (ClientSize.IsEmpty)
  330. {
  331. return;
  332. }
  333. updatingCamera = true;
  334. var mapAspect = (double)mapImage.Width / mapImage.Height;
  335. var panelAspect = (double)ClientSize.Width / ClientSize.Height;
  336. var cameraLocation = cameraBounds.Location;
  337. var size = Size.Empty;
  338. if (panelAspect > mapAspect)
  339. {
  340. size.Height = mapImage.Height / zoom;
  341. size.Width = (int)(size.Height * panelAspect);
  342. }
  343. else
  344. {
  345. size.Width = mapImage.Width / zoom;
  346. size.Height = (int)(size.Width / panelAspect);
  347. }
  348. var location = Point.Empty;
  349. var scrollSize = Size.Empty;
  350. if (size.Width < mapImage.Width)
  351. {
  352. location.X = Math.Max(0, Math.Min(mapImage.Width - size.Width, cameraBounds.Left));
  353. scrollSize.Width = mapImage.Width * ClientSize.Width / size.Width;
  354. }
  355. else
  356. {
  357. location.X = (mapImage.Width - size.Width) / 2;
  358. }
  359. if (size.Height < mapImage.Height)
  360. {
  361. location.Y = Math.Max(0, Math.Min(mapImage.Height - size.Height, cameraBounds.Top));
  362. scrollSize.Height = mapImage.Height * ClientSize.Height / size.Height;
  363. }
  364. else
  365. {
  366. location.Y = (mapImage.Height - size.Height) / 2;
  367. }
  368. cameraBounds = new Rectangle(location, size);
  369. RecalculateTransforms();
  370. if (referencePositions.HasValue)
  371. {
  372. var mapPoint = referencePositions.Value.map;
  373. var clientSize = referencePositions.Value.client;
  374. cameraLocation = cameraBounds.Location;
  375. if (scrollSize.Width != 0)
  376. {
  377. cameraLocation.X = Math.Max(0, Math.Min(mapImage.Width - cameraBounds.Width, (int)(mapPoint.X - (cameraBounds.Width * clientSize.Width))));
  378. }
  379. if (scrollSize.Height != 0)
  380. {
  381. cameraLocation.Y = Math.Max(0, Math.Min(mapImage.Height - cameraBounds.Height, (int)(mapPoint.Y - (cameraBounds.Height * clientSize.Height))));
  382. }
  383. if (!scrollSize.IsEmpty)
  384. {
  385. cameraBounds.Location = cameraLocation;
  386. RecalculateTransforms();
  387. }
  388. referencePositions = null;
  389. }
  390. SuspendDrawing();
  391. AutoScrollMinSize = scrollSize;
  392. AutoScrollPosition = (Point)MapToClient((Size)cameraBounds.Location);
  393. lastScrollPosition = AutoScrollPosition;
  394. ResumeDrawing();
  395. updatingCamera = false;
  396. Invalidate();
  397. }
  398. private void RecalculateTransforms()
  399. {
  400. mapToViewTransform.Reset();
  401. mapToViewTransform.Translate(cameraBounds.Left, cameraBounds.Top);
  402. mapToViewTransform.Scale(cameraBounds.Width, cameraBounds.Height);
  403. mapToViewTransform.Invert();
  404. viewToPageTransform.Reset();
  405. viewToPageTransform.Scale(ClientSize.Width, ClientSize.Height);
  406. compositeTransform.Reset();
  407. compositeTransform.Multiply(viewToPageTransform);
  408. compositeTransform.Multiply(mapToViewTransform);
  409. invCompositeTransform.Reset();
  410. invCompositeTransform.Multiply(compositeTransform);
  411. invCompositeTransform.Invert();
  412. }
  413. private void InvalidateScroll()
  414. {
  415. if (updatingCamera)
  416. {
  417. return;
  418. }
  419. if ((lastScrollPosition.X != AutoScrollPosition.X) || (lastScrollPosition.Y != AutoScrollPosition.Y))
  420. {
  421. var delta = ClientToMap((Size)(lastScrollPosition - (Size)AutoScrollPosition));
  422. lastScrollPosition = AutoScrollPosition;
  423. var cameraLocation = cameraBounds.Location;
  424. if (AutoScrollMinSize.Width != 0)
  425. {
  426. cameraLocation.X = Math.Max(0, Math.Min(mapImage.Width - cameraBounds.Width, cameraBounds.Left + delta.Width));
  427. }
  428. if (AutoScrollMinSize.Height != 0)
  429. {
  430. cameraLocation.Y = Math.Max(0, Math.Min(mapImage.Height - cameraBounds.Height, cameraBounds.Top + delta.Height));
  431. }
  432. if (!AutoScrollMinSize.IsEmpty)
  433. {
  434. cameraBounds.Location = cameraLocation;
  435. RecalculateTransforms();
  436. }
  437. Invalidate();
  438. }
  439. }
  440. [DllImport("user32.dll")]
  441. private static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);
  442. private const int WM_SETREDRAW = 11;
  443. private void SuspendDrawing()
  444. {
  445. SendMessage(Handle, WM_SETREDRAW, false, 0);
  446. }
  447. private void ResumeDrawing()
  448. {
  449. SendMessage(Handle, WM_SETREDRAW, true, 0);
  450. }
  451. }
  452. }