diff --git a/src/MeshExplorer/FormTopology.cs b/src/MeshExplorer/FormTopology.cs index af7ea42..3ec3d89 100644 --- a/src/MeshExplorer/FormTopology.cs +++ b/src/MeshExplorer/FormTopology.cs @@ -52,20 +52,20 @@ namespace MeshExplorer topoControlView.SetTriangle(current.Triangle); } - private ITriangle FindTriangleAt(float x, float y) + private ITriangle FindTriangleAt(float xr, float yr) { // Get mesh coordinates - var p = new System.Drawing.PointF(x, y); - renderControl.Zoom.ScreenToWorld(ref p); + var p = new System.Drawing.PointF(xr, yr); + renderControl.Zoom.ScreenToWorld(p, out double x, out double y); - topoControlView.SetPosition(p); + topoControlView.SetPosition(x, y); if (tree == null) { tree = new TriangleQuadTree(mesh, 5, 2); } - return tree.Query(p.X, p.Y); + return tree.Query(x, y); } private void InvokePrimitive(string name) diff --git a/src/MeshExplorer/IO/FileProcessor.cs b/src/MeshExplorer/IO/FileProcessor.cs index eb22796..d676664 100644 --- a/src/MeshExplorer/IO/FileProcessor.cs +++ b/src/MeshExplorer/IO/FileProcessor.cs @@ -6,13 +6,12 @@ namespace MeshExplorer.IO { + using MeshExplorer.IO.Formats; using System; using System.Collections.Generic; using System.IO; - using MeshExplorer.IO.Formats; - using TriangleNet.IO; - using TriangleNet.Geometry; using TriangleNet; + using TriangleNet.Geometry; /// /// Provides static methods to read and write mesh files. diff --git a/src/MeshExplorer/Topology/TopologyControlView.cs b/src/MeshExplorer/Topology/TopologyControlView.cs index 665af76..351684e 100644 --- a/src/MeshExplorer/Topology/TopologyControlView.cs +++ b/src/MeshExplorer/Topology/TopologyControlView.cs @@ -15,11 +15,11 @@ namespace MeshExplorer.Topology InitializeComponent(); } - public void SetPosition(PointF p) + public void SetPosition(double x, double y) { var nfi = NumberFormatInfo.InvariantInfo; - lbPosition.Text = String.Format(nfi, "X: {0:0.0}, Y: {1:0.0}", p.X, p.Y); + lbPosition.Text = string.Format(nfi, "X: {0:0.0}, Y: {1:0.0}", x, y); } public void SetTriangle(ITriangle tri) diff --git a/src/MeshExplorer/Topology/TopologyRenderControl.cs b/src/MeshExplorer/Topology/TopologyRenderControl.cs index 5818118..d8090c9 100644 --- a/src/MeshExplorer/Topology/TopologyRenderControl.cs +++ b/src/MeshExplorer/Topology/TopologyRenderControl.cs @@ -45,15 +45,13 @@ namespace MeshExplorer.Topology renderer = new TopologyRenderer(mesh); zoom = new Projection(ClientRectangle); - //zoom.ClipMargin = 10.0f; - zoom.Initialize(mesh.Bounds); InitializeBuffer(); initialized = true; - this.Render(); + Render(); } public void Update(Otri otri) diff --git a/src/MeshExplorer/Topology/TopologyRenderer.cs b/src/MeshExplorer/Topology/TopologyRenderer.cs index e2c6f4c..cf815b9 100644 --- a/src/MeshExplorer/Topology/TopologyRenderer.cs +++ b/src/MeshExplorer/Topology/TopologyRenderer.cs @@ -6,7 +6,7 @@ namespace MeshExplorer.Topology using TriangleNet; using TriangleNet.Geometry; using TriangleNet.Rendering; - + public class TopologyRenderer { Projection zoom; @@ -39,9 +39,16 @@ namespace MeshExplorer.Topology int k = 0; + var b = mesh.Bounds; + + double s = 1d / Math.Max(b.Width, b.Height); + double dx = b.X; + double dy = b.Y; + foreach (var v in mesh.Vertices) { - points[k++] = new PointF((float)v.X, (float)v.Y); + // Normalized device coordinates. + points[k++] = new PointF((float)((v.X - dx) * s), (float)((v.Y - dy) * s)); } font = new Font("Arial", 7.5f); @@ -146,7 +153,7 @@ namespace MeshExplorer.Topology var brush = i == id ? Brushes.DarkRed : Point; pt = points[i]; - zoom.WorldToScreen(ref pt); + zoom.NdcToScreen(ref pt); g.FillEllipse(brush, pt.X - 10f, pt.Y - 10f, 20, 20); pt.X -= i > 9 ? 7 : 4; @@ -168,9 +175,9 @@ namespace MeshExplorer.Topology p1 = points[tri.GetVertexID(1)]; p2 = points[tri.GetVertexID(2)]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); - zoom.WorldToScreen(ref p2); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); + zoom.NdcToScreen(ref p2); g.DrawLine(Line, p0, p1); g.DrawLine(Line, p1, p2); @@ -197,9 +204,9 @@ namespace MeshExplorer.Topology p1 = points[tri.GetVertexID(1)]; p2 = points[tri.GetVertexID(2)]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); - zoom.WorldToScreen(ref p2); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); + zoom.NdcToScreen(ref p2); center = GetIncenter(p0, p1, p2); center.X -= 5; @@ -221,8 +228,8 @@ namespace MeshExplorer.Topology p0 = points[edge.P0]; p1 = points[edge.P1]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); g.DrawLine(Line, p0, p1); } @@ -239,8 +246,8 @@ namespace MeshExplorer.Topology p0 = points[seg.P0]; p1 = points[seg.P1]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); g.DrawLine(Segment, p0, p1); } @@ -255,8 +262,8 @@ namespace MeshExplorer.Topology p0 = points[currentOrg.ID]; p1 = points[currentDest.ID]; - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); g.DrawLine(SelectedEdge, p0, p1); } @@ -272,9 +279,9 @@ namespace MeshExplorer.Topology p[1] = points[currentTri.GetVertexID(1)]; p[2] = points[currentTri.GetVertexID(2)]; - zoom.WorldToScreen(ref p[0]); - zoom.WorldToScreen(ref p[1]); - zoom.WorldToScreen(ref p[2]); + zoom.NdcToScreen(ref p[0]); + zoom.NdcToScreen(ref p[1]); + zoom.NdcToScreen(ref p[2]); g.FillPolygon(SelectedTriangle, p); } diff --git a/src/Triangle.Rendering/Buffer/IndexBuffer.cs b/src/Triangle.Rendering/Buffer/IndexBuffer.cs index 108d8a7..a9a2152 100644 --- a/src/Triangle.Rendering/Buffer/IndexBuffer.cs +++ b/src/Triangle.Rendering/Buffer/IndexBuffer.cs @@ -1,11 +1,11 @@  +using System.Collections.Generic; +using System.Linq; +using TriangleNet.Geometry; +using TriangleNet.Topology; + namespace TriangleNet.Rendering.Buffer { - using System.Collections.Generic; - using System.Linq; - using TriangleNet.Geometry; - using TriangleNet.Topology; - public class IndexBuffer : BufferBase { #region Static methods diff --git a/src/Triangle.Rendering/Buffer/VertexBuffer.cs b/src/Triangle.Rendering/Buffer/VertexBuffer.cs index 3dcb1f7..b8b8815 100644 --- a/src/Triangle.Rendering/Buffer/VertexBuffer.cs +++ b/src/Triangle.Rendering/Buffer/VertexBuffer.cs @@ -1,9 +1,10 @@  +using System; +using System.Collections.Generic; +using TriangleNet.Geometry; + namespace TriangleNet.Rendering.Buffer { - using System.Collections.Generic; - using TriangleNet.Geometry; - public class VertexBuffer : BufferBase { #region Static methods @@ -12,25 +13,40 @@ namespace TriangleNet.Rendering.Buffer /// Create a vertex buffer from given point collection. /// /// The points to render. - /// The points bounding box. - /// + /// Returns the vertex buffer. + public static IBuffer Create(ICollection points) + { + return Create(points, new Rectangle(0d, 0d, 1d, 1d)); + } + + /// + /// Create a normalized vertex buffer from given point collection. + /// + /// The points to render. + /// The bounding box used for normalization. + /// Returns a buffer of normalized coordinates. public static IBuffer Create(ICollection points, Rectangle bounds) { var buffer = new VertexBuffer(2 * points.Count); var data = buffer.Data; - float x, y; + double dx = bounds.X; + double dy = bounds.Y; + + double scale = 1.0 / Math.Max(bounds.Width, bounds.Height); int i = 0; + double x, y; + foreach (var p in points) { - x = (float)p.X; - y = (float)p.Y; + x = (p.X - dx) * scale; + y = (p.Y - dy) * scale; - data[2 * i] = x; - data[2 * i + 1] = y; + data[2 * i] = (float)x; + data[2 * i + 1] = (float)y; i++; } @@ -39,23 +55,43 @@ namespace TriangleNet.Rendering.Buffer } /// - /// Create a vertex buffer from given vertex collection. + /// Create a vertex buffer from given point collection. + /// + /// The points to render. + /// Returns the vertex buffer. + public static IBuffer Create(ICollection points) + { + return Create(points, new Rectangle(0d, 0d, 1d, 1d)); + } + + /// + /// Create a normalized vertex buffer from given vertex collection. /// /// The vertices to render. - /// The vertices bounding box. - /// + /// The bounding box used for normalization. + /// Returns a buffer of normalized coordinates. public static IBuffer Create(ICollection points, Rectangle bounds) { var buffer = new VertexBuffer(2 * points.Count); var data = buffer.Data; + double dx = bounds.X; + double dy = bounds.Y; + + double scale = 1.0 / Math.Max(bounds.Width, bounds.Height); + int i = 0; + double x, y; + foreach (var p in points) { - data[2 * i] = (float)p.X; - data[2 * i + 1] = (float)p.Y; + x = (p.X - dx) * scale; + y = (p.Y - dy) * scale; + + data[2 * i] = (float)x; + data[2 * i + 1] = (float)y; i++; } diff --git a/src/Triangle.Rendering/GDI/FunctionRenderer.cs b/src/Triangle.Rendering/GDI/FunctionRenderer.cs index 24f0c08..2af4cac 100644 --- a/src/Triangle.Rendering/GDI/FunctionRenderer.cs +++ b/src/Triangle.Rendering/GDI/FunctionRenderer.cs @@ -48,7 +48,7 @@ namespace TriangleNet.Rendering.GDI Color color; PointF p = new PointF((float)data[0], (float)data[1]); - zoom.WorldToScreen(ref p); + zoom.NdcToScreen(ref p); // Get correction distance float dx = (p.X - (int)p.X) * 2.0f; @@ -60,7 +60,7 @@ namespace TriangleNet.Rendering.GDI p.X = (float)data[size * i]; p.Y = (float)data[size * i + 1]; - zoom.WorldToScreen(ref p); + zoom.NdcToScreen(ref p); color = colors[i]; diff --git a/src/Triangle.Rendering/GDI/MeshRenderer.cs b/src/Triangle.Rendering/GDI/MeshRenderer.cs index df19ca5..f630cee 100644 --- a/src/Triangle.Rendering/GDI/MeshRenderer.cs +++ b/src/Triangle.Rendering/GDI/MeshRenderer.cs @@ -58,7 +58,7 @@ namespace TriangleNet.Rendering.GDI if (zoom.Viewport.Contains(p)) { - zoom.WorldToScreen(ref p); + zoom.NdcToScreen(ref p); g.FillEllipse(brush, p.X - 1.5f, p.Y - 1.5f, 3, 3); } } @@ -109,9 +109,9 @@ namespace TriangleNet.Rendering.GDI if (zoom.Viewport.Intersects(tri[0], tri[1], tri[2])) { - zoom.WorldToScreen(ref tri[0]); - zoom.WorldToScreen(ref tri[1]); - zoom.WorldToScreen(ref tri[2]); + zoom.NdcToScreen(ref tri[0]); + zoom.NdcToScreen(ref tri[1]); + zoom.NdcToScreen(ref tri[2]); if (filled) { @@ -162,8 +162,8 @@ namespace TriangleNet.Rendering.GDI if (zoom.Viewport.Intersects(p0, p1)) { - zoom.WorldToScreen(ref p0); - zoom.WorldToScreen(ref p1); + zoom.NdcToScreen(ref p0); + zoom.NdcToScreen(ref p1); g.DrawLine(pen, p0, p1); } diff --git a/src/Triangle.Rendering/Projection.cs b/src/Triangle.Rendering/Projection.cs index 5ed8f5f..090e199 100644 --- a/src/Triangle.Rendering/Projection.cs +++ b/src/Triangle.Rendering/Projection.cs @@ -1,6 +1,6 @@ // ----------------------------------------------------------------------- // -// TODO: Update copyright text. +// Triangle.NET code by Christian Woltering, http://triangle.codeplex.com/ // // ----------------------------------------------------------------------- @@ -9,74 +9,95 @@ namespace TriangleNet.Rendering using System; using System.Drawing; + using TRectangle = Geometry.Rectangle; + /// /// Manages a world to screen transformation (2D orthographic projection). /// + /// + /// + /// The projection implementation is actually not world-to-screen, but NDC-to-screen + /// (Normalized-Device-Coordinates). NDC here is - in contrast for example to OpenGL, the + /// transformation of world coordinates to a unit rectangle with origin (0,0) and a max + /// side length 1 (the width/height ratio is preserved). It's a simple translate-scale + /// transform, which is automatically applied in VertexBuffer.Create(points, bounds). + /// + /// + /// Since the upper-left corner of the display is usually the screen coordinate origin + /// (0,0), the project will automatically invert the y-axis. + /// + /// public class Projection { - // The screen. + // The original mesh bounds (needed for screen-to-world projection). + TRectangle world_; + + // Precomputed scaling factor for normalized coordinates. + double scale_; + + // The screen dimensions. Rectangle screen; - // The complete mesh. - RectangleF world; - - RectangleF viewport; + // The original mesh and the viewport in normalized coordinates. + RectangleF world, viewport; /// - /// Gets or sets the current viewport (visible mesh). + /// Gets or sets the current viewport (normalized coordinates). /// public RectangleF Viewport => viewport; - /// - /// Gets the current scale. - /// - public float Scale => screen.Width / viewport.Width; - /// /// Gets the zoom level. /// public int Level { get; private set; } - // The y-direction of windows screen coordinates is upside down, - // so invertY should be set to true. - bool invertY; + private const int MAX_ZOOM = 100; - const int maxZoomLevel = 100; - - public Projection(Rectangle screen, bool invertY = true) + /// + /// Initializes a new instance of the class. + /// + /// The current screen (viewport) dimensions. + public Projection(Rectangle screen) { this.screen = screen; - this.invertY = invertY; - world = viewport = screen; + world = viewport = new RectangleF(screen.X, screen.Y, screen.Width, screen.Height); Level = 1; } /// - /// Inititialize the projection. + /// Initialize the projection. /// - /// The world that should be transformed to screen coordinates. - public void Initialize(Geometry.Rectangle world) + /// The world that should be transformed to screen coordinates. + public void Initialize(TRectangle world) { Level = 1; - float ww = (float)world.Width; - float wh = (float)world.Height; + // Bounding box of original (non-normalized) coordinates. + world_ = world; + + // Scaling factor for normalized coordinates. + scale_ = Math.Max(world.Width, world.Height); + + // Dimensions in normalized coordinates. + float ww = (float)(world.Width / scale_); + float wh = (float)(world.Height / scale_); + + // Add a margin so there's some space around the screen borders. + float margin = (ww < wh) ? wh * 0.05f : ww * 0.05f; int sw = screen.Width; int sh = screen.Height; - // Add a margin so there's some space around the border - float margin = (ww < wh) ? wh * 0.05f : ww * 0.05f; - float wRatio = ww / wh; float sRatio = sw / (float)sh; - float scale = (sRatio < wRatio) ? (ww + margin) / sw : (wh + margin) / sh; + float scale = (sRatio < wRatio) ? (ww + margin) / sw : (wh + margin) / sh; - float centerX = (float)world.X + ww / 2; - float centerY = (float)world.Y + wh / 2; + // Center in normalized coordinates (left = bottom = 0) + float centerX = ww / 2; + float centerY = wh / 2; // Get the initial viewport (complete mesh centered on the screen) this.world = viewport = new RectangleF( @@ -87,9 +108,9 @@ namespace TriangleNet.Rendering } /// - /// Handle resize of the screen. + /// Handle resize of the screen (viewport). /// - /// The new screen dimensions. + /// The new screen (viewport) dimensions. public void Resize(Rectangle newScreen) { // The viewport has to be updated, but we want to keep @@ -149,25 +170,23 @@ namespace TriangleNet.Rendering /// /// Zoom in or out of the viewport. /// - /// Zoom amount - /// Relative x point position - /// Relative y point position + /// Zoom amount. + /// Relative x point position (in [0..1] range). + /// Relative y point position (in [0..1] range). public bool Zoom(int amount, float focusX, float focusY) { float width, height; - if (invertY) - { - focusY = 1 - focusY; - } + // Invert y coordinate. + focusY = 1 - focusY; if (amount > 0) // Zoom in { Level++; - if (Level > maxZoomLevel) + if (Level > MAX_ZOOM) { - Level = maxZoomLevel; + Level = MAX_ZOOM; return false; } @@ -221,22 +240,40 @@ namespace TriangleNet.Rendering return true; } + /// + /// Reset the zoom to initial state. + /// public void Reset() { viewport = world; Level = 1; } - public void WorldToScreen(ref PointF pt) + /// + /// Project a normalized device coordinate to screen coordinates. + /// + /// Input normalized device coordinate, output screen coordinate. + public void NdcToScreen(ref PointF pt) { pt.X = (pt.X - viewport.X) / viewport.Width * screen.Width; pt.Y = (1 - (pt.Y - viewport.Y) / viewport.Height) * screen.Height; } + /// + /// Project a screen coordinate to world coordinates. + /// + /// Normalized position on screen (both coordinates in [0..1] range). + /// The world x-coordinate. + /// The world y-coordinate. public void ScreenToWorld(PointF pt, out double x, out double y) { - x = viewport.X + viewport.Width * pt.X; - y = viewport.Y + viewport.Height * (1 - pt.Y); + // Position in normalized coordinates. + var nx = viewport.X + viewport.Width * pt.X; + var ny = viewport.Y + viewport.Height * (1 - pt.Y); + + // Translate and scale to world coordinates. + x = world_.X + nx * scale_; + y = world_.Y + ny * scale_; } } }