CnC_Remastered_Collection/CnCTDRAMapEditor/Controls/MapPanel.cs

536 lines
16 KiB
C#

//
// Copyright 2020 Electronic Arts Inc.
//
// The Command & Conquer Map Editor and corresponding source code is free
// software: you can redistribute it and/or modify it under the terms of
// the GNU General Public License as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
// The Command & Conquer Map Editor and corresponding source code is distributed
// in the hope that it will be useful, but with permitted additional restrictions
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
// distributed with this program. You should have received a copy of the
// GNU General Public License along with permitted additional restrictions
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
using MobiusEditor.Event;
using MobiusEditor.Interface;
using MobiusEditor.Model;
using MobiusEditor.Utility;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace MobiusEditor.Controls
{
public partial class MapPanel : Panel
{
private bool updatingCamera;
private Rectangle cameraBounds;
private Point lastScrollPosition;
private (Point map, SizeF client)? referencePositions;
private Matrix mapToViewTransform = new Matrix();
private Matrix viewToPageTransform = new Matrix();
private Matrix compositeTransform = new Matrix();
private Matrix invCompositeTransform = new Matrix();
private readonly HashSet<Point> invalidateCells = new HashSet<Point>();
private bool fullInvalidation;
private Image mapImage;
public Image MapImage
{
get => mapImage;
set
{
if (mapImage != value)
{
mapImage = value;
UpdateCamera();
}
}
}
private int minZoom = 1;
public int MinZoom
{
get => minZoom;
set
{
if (minZoom != value)
{
minZoom = value;
Zoom = zoom;
}
}
}
private int maxZoom = 8;
public int MaxZoom
{
get => maxZoom;
set
{
if (maxZoom != value)
{
maxZoom = value;
Zoom = zoom;
}
}
}
private int zoomStep = 1;
public int ZoomStep
{
get => zoomStep;
set
{
if (zoomStep != value)
{
zoomStep = value;
Zoom = (Zoom / zoomStep) * zoomStep;
}
}
}
private int zoom = 1;
public int Zoom
{
get => zoom;
set
{
var newZoom = Math.Max(MinZoom, Math.Min(MaxZoom, value));
if (zoom != newZoom)
{
zoom = newZoom;
var clientPosition = PointToClient(MousePosition);
referencePositions = (ClientToMap(clientPosition), new SizeF(clientPosition.X / (float)ClientSize.Width, clientPosition.Y / (float)ClientSize.Height));
UpdateCamera();
}
}
}
private int quality = Properties.Settings.Default.Quality;
public int Quality
{
get => quality;
set
{
if (quality != value)
{
quality = value;
Invalidate();
}
}
}
[Category("Behavior")]
[DefaultValue(false)]
public bool FocusOnMouseEnter { get; set; }
public event EventHandler<RenderEventArgs> PreRender;
public event EventHandler<RenderEventArgs> PostRender;
public MapPanel()
{
InitializeComponent();
DoubleBuffered = true;
}
public Point MapToClient(Point point)
{
var points = new Point[] { point };
compositeTransform.TransformPoints(points);
return points[0];
}
public Size MapToClient(Size size)
{
var points = new Point[] { (Point)size };
compositeTransform.VectorTransformPoints(points);
return (Size)points[0];
}
public Rectangle MapToClient(Rectangle rectangle)
{
var points = new Point[] { rectangle.Location, new Point(rectangle.Right, rectangle.Bottom) };
compositeTransform.TransformPoints(points);
return new Rectangle(points[0], new Size(points[1].X - points[0].X, points[1].Y - points[0].Y));
}
public Point ClientToMap(Point point)
{
var points = new Point[] { point };
invCompositeTransform.TransformPoints(points);
return points[0];
}
public Size ClientToMap(Size size)
{
var points = new Point[] { (Point)size };
invCompositeTransform.VectorTransformPoints(points);
return (Size)points[0];
}
public Rectangle ClientToMap(Rectangle rectangle)
{
var points = new Point[] { rectangle.Location, new Point(rectangle.Right, rectangle.Bottom) };
invCompositeTransform.TransformPoints(points);
return new Rectangle(points[0], new Size(points[1].X - points[0].X, points[1].Y - points[0].Y));
}
public void Invalidate(Map invalidateMap)
{
if (!fullInvalidation)
{
invalidateCells.Clear();
fullInvalidation = true;
Invalidate();
}
}
public void Invalidate(Map invalidateMap, Rectangle cellBounds)
{
if (fullInvalidation)
{
return;
}
var count = invalidateCells.Count;
invalidateCells.UnionWith(cellBounds.Points());
if (invalidateCells.Count > count)
{
var overlapCells = invalidateMap.Overlappers.Overlaps(invalidateCells).ToHashSet();
invalidateCells.UnionWith(overlapCells);
Invalidate();
}
}
public void Invalidate(Map invalidateMap, IEnumerable<Rectangle> cellBounds)
{
if (fullInvalidation)
{
return;
}
var count = invalidateCells.Count;
invalidateCells.UnionWith(cellBounds.SelectMany(c => c.Points()));
if (invalidateCells.Count > count)
{
var overlapCells = invalidateMap.Overlappers.Overlaps(invalidateCells).ToHashSet();
invalidateCells.UnionWith(overlapCells);
Invalidate();
}
}
public void Invalidate(Map invalidateMap, Point location)
{
if (fullInvalidation)
{
return;
}
Invalidate(invalidateMap, new Rectangle(location, new Size(1, 1)));
}
public void Invalidate(Map invalidateMap, IEnumerable<Point> locations)
{
if (fullInvalidation)
{
return;
}
Invalidate(invalidateMap, locations.Select(l => new Rectangle(l, new Size(1, 1))));
}
public void Invalidate(Map invalidateMap, int cell)
{
if (fullInvalidation)
{
return;
}
if (invalidateMap.Metrics.GetLocation(cell, out Point location))
{
Invalidate(invalidateMap, location);
}
}
public void Invalidate(Map invalidateMap, IEnumerable<int> cells)
{
if (fullInvalidation)
{
return;
}
Invalidate(invalidateMap, cells
.Where(c => invalidateMap.Metrics.GetLocation(c, out Point location))
.Select(c =>
{
invalidateMap.Metrics.GetLocation(c, out Point location);
return location;
})
);
}
public void Invalidate(Map invalidateMap, ICellOverlapper overlapper)
{
if (fullInvalidation)
{
return;
}
var rectangle = invalidateMap.Overlappers[overlapper];
if (rectangle.HasValue)
{
Invalidate(invalidateMap, rectangle.Value);
}
}
protected override void OnMouseEnter(EventArgs e)
{
base.OnMouseEnter(e);
if (FocusOnMouseEnter)
{
Focus();
}
}
protected override void OnMouseWheel(MouseEventArgs e)
{
Zoom += ZoomStep * Math.Sign(e.Delta);
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
UpdateCamera();
}
protected override void OnScroll(ScrollEventArgs se)
{
base.OnScroll(se);
InvalidateScroll();
}
protected override void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground(e);
e.Graphics.Clear(BackColor);
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
InvalidateScroll();
PreRender?.Invoke(this, new RenderEventArgs(pe.Graphics, fullInvalidation ? null : invalidateCells));
if (mapImage != null)
{
pe.Graphics.Transform = compositeTransform;
var oldCompositingMode = pe.Graphics.CompositingMode;
var oldCompositingQuality = pe.Graphics.CompositingQuality;
var oldInterpolationMode = pe.Graphics.InterpolationMode;
if (Quality > 1)
{
pe.Graphics.CompositingMode = CompositingMode.SourceCopy;
pe.Graphics.CompositingQuality = CompositingQuality.HighSpeed;
}
pe.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
pe.Graphics.DrawImage(mapImage, 0, 0);
pe.Graphics.CompositingMode = oldCompositingMode;
pe.Graphics.CompositingQuality = oldCompositingQuality;
pe.Graphics.InterpolationMode = oldInterpolationMode;
}
PostRender?.Invoke(this, new RenderEventArgs(pe.Graphics, fullInvalidation ? null : invalidateCells));
#if DEVELOPER
if (Globals.Developer.ShowOverlapCells)
{
var invalidPen = new Pen(Color.DarkRed);
foreach (var cell in invalidateCells)
{
pe.Graphics.DrawRectangle(invalidPen, new Rectangle(cell.X * Globals.TileWidth, cell.Y * Globals.TileHeight, Globals.TileWidth, Globals.TileHeight));
}
}
#endif
invalidateCells.Clear();
fullInvalidation = false;
}
private void UpdateCamera()
{
if (mapImage == null)
{
return;
}
if (ClientSize.IsEmpty)
{
return;
}
updatingCamera = true;
var mapAspect = (double)mapImage.Width / mapImage.Height;
var panelAspect = (double)ClientSize.Width / ClientSize.Height;
var cameraLocation = cameraBounds.Location;
var size = Size.Empty;
if (panelAspect > mapAspect)
{
size.Height = mapImage.Height / zoom;
size.Width = (int)(size.Height * panelAspect);
}
else
{
size.Width = mapImage.Width / zoom;
size.Height = (int)(size.Width / panelAspect);
}
var location = Point.Empty;
var scrollSize = Size.Empty;
if (size.Width < mapImage.Width)
{
location.X = Math.Max(0, Math.Min(mapImage.Width - size.Width, cameraBounds.Left));
scrollSize.Width = mapImage.Width * ClientSize.Width / size.Width;
}
else
{
location.X = (mapImage.Width - size.Width) / 2;
}
if (size.Height < mapImage.Height)
{
location.Y = Math.Max(0, Math.Min(mapImage.Height - size.Height, cameraBounds.Top));
scrollSize.Height = mapImage.Height * ClientSize.Height / size.Height;
}
else
{
location.Y = (mapImage.Height - size.Height) / 2;
}
cameraBounds = new Rectangle(location, size);
RecalculateTransforms();
if (referencePositions.HasValue)
{
var mapPoint = referencePositions.Value.map;
var clientSize = referencePositions.Value.client;
cameraLocation = cameraBounds.Location;
if (scrollSize.Width != 0)
{
cameraLocation.X = Math.Max(0, Math.Min(mapImage.Width - cameraBounds.Width, (int)(mapPoint.X - (cameraBounds.Width * clientSize.Width))));
}
if (scrollSize.Height != 0)
{
cameraLocation.Y = Math.Max(0, Math.Min(mapImage.Height - cameraBounds.Height, (int)(mapPoint.Y - (cameraBounds.Height * clientSize.Height))));
}
if (!scrollSize.IsEmpty)
{
cameraBounds.Location = cameraLocation;
RecalculateTransforms();
}
referencePositions = null;
}
SuspendDrawing();
AutoScrollMinSize = scrollSize;
AutoScrollPosition = (Point)MapToClient((Size)cameraBounds.Location);
lastScrollPosition = AutoScrollPosition;
ResumeDrawing();
updatingCamera = false;
Invalidate();
}
private void RecalculateTransforms()
{
mapToViewTransform.Reset();
mapToViewTransform.Translate(cameraBounds.Left, cameraBounds.Top);
mapToViewTransform.Scale(cameraBounds.Width, cameraBounds.Height);
mapToViewTransform.Invert();
viewToPageTransform.Reset();
viewToPageTransform.Scale(ClientSize.Width, ClientSize.Height);
compositeTransform.Reset();
compositeTransform.Multiply(viewToPageTransform);
compositeTransform.Multiply(mapToViewTransform);
invCompositeTransform.Reset();
invCompositeTransform.Multiply(compositeTransform);
invCompositeTransform.Invert();
}
private void InvalidateScroll()
{
if (updatingCamera)
{
return;
}
if ((lastScrollPosition.X != AutoScrollPosition.X) || (lastScrollPosition.Y != AutoScrollPosition.Y))
{
var delta = ClientToMap((Size)(lastScrollPosition - (Size)AutoScrollPosition));
lastScrollPosition = AutoScrollPosition;
var cameraLocation = cameraBounds.Location;
if (AutoScrollMinSize.Width != 0)
{
cameraLocation.X = Math.Max(0, Math.Min(mapImage.Width - cameraBounds.Width, cameraBounds.Left + delta.Width));
}
if (AutoScrollMinSize.Height != 0)
{
cameraLocation.Y = Math.Max(0, Math.Min(mapImage.Height - cameraBounds.Height, cameraBounds.Top + delta.Height));
}
if (!AutoScrollMinSize.IsEmpty)
{
cameraBounds.Location = cameraLocation;
RecalculateTransforms();
}
Invalidate();
}
}
[DllImport("user32.dll")]
private static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);
private const int WM_SETREDRAW = 11;
private void SuspendDrawing()
{
SendMessage(Handle, WM_SETREDRAW, false, 0);
}
private void ResumeDrawing()
{
SendMessage(Handle, WM_SETREDRAW, true, 0);
}
}
}