using System; using OpenNest.Geometry; namespace OpenNest.Desktop.Rendering; /// A point in a view's screen (control) pixels, y down. public readonly record struct ScreenPoint(double X, double Y); /// /// A plate or CAD view's world-to-screen mapping: world Y up, screen Y down, a uniform /// in pixels per drawing unit and /, the /// screen position of the world origin. The zoom rules are the WinForms DrawControl's: the /// scale stays within .., a zoom about a point keeps that /// point still, and fitting an area leaves pixels of margin in all. /// public sealed class ViewTransform { /// The DrawControl's zoom step for one Zoom In. public const double ZoomInFactor = 1.15; /// Total pixels left around a fitted area (half on each side). public const double FitBorder = 50; public double Scale { get; private set; } = 1; public double OriginX { get; private set; } = 100; public double OriginY { get; private set; } = 100; public double MinScale { get; set; } = 0.3; public double MaxScale { get; set; } = 10000; public double ViewportWidth { get; private set; } public double ViewportHeight { get; private set; } /// Raised after the scale, origin or viewport changes. public event EventHandler Changed; public ScreenPoint WorldToScreen(Vector world) => WorldToScreen(world.X, world.Y); public ScreenPoint WorldToScreen(double x, double y) => new(OriginX + x * Scale, OriginY - y * Scale); public Vector ScreenToWorld(ScreenPoint screen) => ScreenToWorld(screen.X, screen.Y); public Vector ScreenToWorld(double x, double y) => new((x - OriginX) / Scale, (OriginY - y) / Scale); public double LengthToScreen(double worldLength) => worldLength * Scale; public double LengthToWorld(double screenLength) => screenLength / Scale; /// The world area the viewport shows. public Box VisibleWorld() { var bottomLeft = ScreenToWorld(0, ViewportHeight); return new Box(bottomLeft.X, bottomLeft.Y, ViewportWidth / Scale, ViewportHeight / Scale); } /// /// Sets the viewport size. After the first size, a resize keeps the view centred by moving /// the origin half the change, as the WinForms view does. /// public void Resize(double width, double height) { if (width == ViewportWidth && height == ViewportHeight) return; OriginX += (width - ViewportWidth) * 0.5; OriginY += (height - ViewportHeight) * 0.5; ViewportWidth = width; ViewportHeight = height; OnChanged(); } /// Moves the view by a screen offset. public void Pan(double dx, double dy) { if (dx == 0 && dy == 0) return; OriginX += dx; OriginY += dy; OnChanged(); } /// Zooms by about the world point under . public void ZoomAt(ScreenPoint screen, double factor) => ZoomToPoint(ScreenToWorld(screen), factor); /// /// Zooms by keeping at the same screen point; /// the factor is cut so the scale stays within its limits. /// public void ZoomToPoint(Vector world, double factor) { var newScale = Scale * factor; if (newScale < MinScale) factor = MinScale / Scale; else if (newScale > MaxScale) factor = MaxScale / Scale; OriginX -= (world.X * factor - world.X) * Scale; OriginY += (world.Y * factor - world.Y) * Scale; Scale *= factor; OnChanged(); } /// Zooms in or out by one step about the viewport centre. public void ZoomStep(bool zoomIn) => ZoomAt(new ScreenPoint(ViewportWidth / 2, ViewportHeight / 2), zoomIn ? ZoomInFactor : 1 / ZoomInFactor); /// /// Fits in the viewport, centred, with pixels of /// margin in all; an empty area or viewport changes nothing. Returns whether it changed the view. /// public bool ZoomToArea(Box area) { if (area == null || area.Length <= 0 || area.Width <= 0 || ViewportWidth <= 0 || ViewportHeight <= 0) return false; var a = (ViewportHeight - FitBorder) / area.Width; var b = (ViewportWidth - FitBorder) / area.Length; Scale = System.Math.Clamp(a < b ? a : b, MinScale, MaxScale); var px = area.X * Scale; var py = area.Y * Scale; var pw = area.Length * Scale; var ph = area.Width * Scale; OriginX = (ViewportWidth - pw) * 0.5 - px; OriginY = (ViewportHeight + ph) * 0.5 + py; OnChanged(); return true; } /// The current scale and origin, to restore later. public (double Scale, double OriginX, double OriginY) Capture() => (Scale, OriginX, OriginY); public void Restore((double Scale, double OriginX, double OriginY) state) { Scale = System.Math.Clamp(state.Scale, MinScale, MaxScale); OriginX = state.OriginX; OriginY = state.OriginY; OnChanged(); } private void OnChanged() => Changed?.Invoke(this, EventArgs.Empty); }