Files
OpenNest/OpenNest.Desktop/Rendering/ViewTransform.cs
T
ajandClaude Opus 5.5 049b45b394 feat(desktop): share plate-view geometry, view transform and overlays
Port the WinForms GraphicsHelper program walker to a toolkit-neutral
figure list (move/line/arc) with its layer routing: cuts without tabs,
leads, etches, and a material fill without break cuts whose tab-gapped
pieces are bridged. Suppressed moves are skipped and marks never bound
the fill. PartDisplayGeometry picks the fill as LayoutPart does
(common-line members, break cuts, open cuts) and keeps display-only
entities; PartLabels ports the label points.

ViewTransform carries DrawControl's world/screen mapping and zoom rules.
PlateOverlays computes what the view draws around parts (sheet, edge
spacing, irregular usable area, clamps, cut-off strokes, rapids, pierce
points, cut direction, offset outline, support) through the existing
Core code. PlateViewOptions reads and writes the View toggles under the
WinForms PlateViewDrawRapid/PlateViewDrawBounds names.

WinForms still draws with GraphicsHelper; nothing there changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:18:14 -05:00

146 lines
5.4 KiB
C#

using System;
using OpenNest.Geometry;
namespace OpenNest.Desktop.Rendering;
/// <summary>A point in a view's screen (control) pixels, y down.</summary>
public readonly record struct ScreenPoint(double X, double Y);
/// <summary>
/// A plate or CAD view's world-to-screen mapping: world Y up, screen Y down, a uniform
/// <see cref="Scale"/> in pixels per drawing unit and <see cref="OriginX"/>/<see cref="OriginY"/>, the
/// screen position of the world origin. The zoom rules are the WinForms <c>DrawControl</c>'s: the
/// scale stays within <see cref="MinScale"/>..<see cref="MaxScale"/>, a zoom about a point keeps that
/// point still, and fitting an area leaves <see cref="FitBorder"/> pixels of margin in all.
/// </summary>
public sealed class ViewTransform
{
/// <summary>The DrawControl's zoom step for one Zoom In.</summary>
public const double ZoomInFactor = 1.15;
/// <summary>Total pixels left around a fitted area (half on each side).</summary>
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; }
/// <summary>Raised after the scale, origin or viewport changes.</summary>
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;
/// <summary>The world area the viewport shows.</summary>
public Box VisibleWorld()
{
var bottomLeft = ScreenToWorld(0, ViewportHeight);
return new Box(bottomLeft.X, bottomLeft.Y, ViewportWidth / Scale, ViewportHeight / Scale);
}
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>Moves the view by a screen offset.</summary>
public void Pan(double dx, double dy)
{
if (dx == 0 && dy == 0)
return;
OriginX += dx;
OriginY += dy;
OnChanged();
}
/// <summary>Zooms by <paramref name="factor"/> about the world point under <paramref name="screen"/>.</summary>
public void ZoomAt(ScreenPoint screen, double factor) => ZoomToPoint(ScreenToWorld(screen), factor);
/// <summary>
/// Zooms by <paramref name="factor"/> keeping <paramref name="world"/> at the same screen point;
/// the factor is cut so the scale stays within its limits.
/// </summary>
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();
}
/// <summary>Zooms in or out by one step about the viewport centre.</summary>
public void ZoomStep(bool zoomIn) =>
ZoomAt(new ScreenPoint(ViewportWidth / 2, ViewportHeight / 2), zoomIn ? ZoomInFactor : 1 / ZoomInFactor);
/// <summary>
/// Fits <paramref name="area"/> in the viewport, centred, with <see cref="FitBorder"/> pixels of
/// margin in all; an empty area or viewport changes nothing. Returns whether it changed the view.
/// </summary>
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;
}
/// <summary>The current scale and origin, to restore later.</summary>
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);
}