mirror of
https://github.com/electronicarts/CnC_Remastered_Collection.git
synced 2025-08-14 10:04:30 +02:00
C&C Remastered Map Editor
Initial commit of C&C Remastered Map Editor code
This commit is contained in:
535
CnCTDRAMapEditor/Controls/MapPanel.cs
Normal file
535
CnCTDRAMapEditor/Controls/MapPanel.cs
Normal file
@@ -0,0 +1,535 @@
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user