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