Coordinate System
Absolute coordinate system, pan/zoom transforms, and grid rendering
Related Classes: RenderingHelper, DrawingManager — Key File: DrawingManager.Rendering.cs
Table of Contents
Absolute Coordinates
Beep.Skia uses an absolute coordinate system where every component stores its position in a virtual, unbounded world space. Component positions are independent of the current viewport, scroll position, or zoom level. This design means:
- A component at
(500, 300)is always at(500, 300)regardless of how the user pans or zooms. - The coordinate origin
(0, 0)is at the top-left of the infinite canvas, with positive X extending rightward and positive Y extending downward. - All component bounds, port positions, and connection-line endpoints are expressed in world-space coordinates.
- Pan and zoom transforms are applied once globally by
RenderingHelperbefore any drawing occurs.
World-Space vs. Screen-Space
World-space is the conceptual coordinate system of the diagram. A node at (200, 100) in world-space is at that position relative to the canvas origin.
Screen-space is the pixel coordinate system of the display device. Mouse events arrive in screen-space and must be converted to world-space before hit testing.
The transform between these two spaces is: worldPoint = (screenPoint - panOffset) / zoomFactor
Pan & Zoom
The viewport is controlled by two parameters on DrawingManager: ZoomFactor and PanOffset. These are composed into an SKMatrix that is applied to the canvas before any component rendering:
Applying the Viewport Transform
private void ApplyViewportTransform(SKCanvas canvas)
{
canvas.Save();
var matrix = SKMatrix.CreateScale(ZoomFactor, ZoomFactor);
matrix = matrix.PostConcat(
SKMatrix.CreateTranslation(PanOffset.X, PanOffset.Y));
canvas.SetMatrix(canvas.TotalMatrix.PostConcat(matrix));
RenderComponents(canvas);
RenderConnections(canvas);
RenderGrid(canvas);
canvas.Restore();
}
Screen-to-World and World-to-Screen Conversion
Mouse events arrive in screen-space pixel coordinates. Before hit testing, they must be converted to world-space. Conversely, when displaying tooltips or UI overlays at a component position, world-space coordinates must be converted back to screen-space:
Coordinate Conversion Methods
public SKPoint ScreenToWorld(SKPoint screenPoint)
{
float worldX = (screenPoint.X - PanOffset.X) / ZoomFactor;
float worldY = (screenPoint.Y - PanOffset.Y) / ZoomFactor;
return new SKPoint(worldX, worldY);
}
public SKPoint WorldToScreen(SKPoint worldPoint)
{
float screenX = worldPoint.X * ZoomFactor + PanOffset.X;
float screenY = worldPoint.Y * ZoomFactor + PanOffset.Y;
return new SKPoint(screenX, screenY);
}
These conversions are used throughout the interaction system:
Usage in Mouse Handlers
public void OnMouseDown(MouseEventArgs e)
{
var screenPoint = new SKPoint(e.X, e.Y);
var worldPoint = drawingManager.ScreenToWorld(screenPoint);
var hitComponent = drawingManager.FindComponentAt(worldPoint);
if (hitComponent != null)
{
SelectComponent(hitComponent);
StartDrag(hitComponent, worldPoint);
}
}
Zoom Constraints
| Parameter | Value | Description |
|---|---|---|
MinZoom | 0.1f | Minimum zoom level (10%). Prevents excessive zoom-out where components become invisible. |
MaxZoom | 5.0f | Maximum zoom level (500%). Prevents excessive zoom-in where a single component fills the viewport. |
ZoomStep | 0.1f | Increment/decrement per mouse wheel notch. With Ctrl held. |
ZoomOrigin | SKPoint | The point in screen-space that remains fixed during zoom. Typically the mouse cursor position. |
Grid System
The grid provides a visual reference for component placement and an optional snapping mechanism for precise alignment. Grid rendering is handled by RenderingHelper and toggled by properties on DrawingManager:
| Property | Type | Default | Description |
|---|---|---|---|
GridSpacing | float | 20 | Spacing between grid lines in world-space units. The grid adapts to zoom level — at 200% zoom, each cell appears as 40 screen pixels. |
ShowGrid | bool | true | Toggles grid visibility. When false, the background renders as a solid color with no grid lines. |
SnapToGrid | bool | true | When enabled, component positions snap to the nearest grid intersection during drag and resize operations. |
Configuring the Grid
drawingManager.GridSpacing = 16;
drawingManager.ShowGrid = true;
drawingManager.SnapToGrid = true;
// Grid line color comes from the active theme
var theme = SkiaTheme.LightTheme;
theme.GridColor = new SKColor(0xE8, 0xE8, 0xE8);
Grid Snapping
When SnapToGrid is enabled, component positions are quantized to the nearest grid intersection. This is applied during move and resize operations:
Snap-to-Grid Logic
public float SnapToGridValue(float value)
{
if (!SnapToGrid) return value;
return (float)Math.Round(value / GridSpacing) * GridSpacing;
}
var snappedX = SnapToGridValue(component.X);
var snappedY = SnapToGridValue(component.Y);
component.X = snappedX; component.Y = snappedY;
Coordinate Transforms
All coordinate transforms use SKMatrix from SkiaSharp for high-performance 2D affine transformations. The rendering pipeline applies transforms efficiently using canvas state management:
Canvas State Management with Save/Restore
public void Render(SKCanvas canvas)
{
canvas.Save();
canvas.Clear(SKColors.White);
ApplyViewportTransform(canvas);
if (ShowGrid)
DrawGrid(canvas);
foreach (var component in Components.OrderBy(c => c.ZOrder))
{
canvas.Save();
canvas.Translate(component.X, component.Y);
component.DrawContent(canvas);
canvas.Restore();
}
foreach (var connection in Connections)
{
connection.Draw(canvas);
}
canvas.Restore();
}
Key principles of the transform system:
- Viewport transform applied once: Pan and zoom are composed as a single
SKMatrixand applied at the start of the render cycle. Individual components do not need to handle viewport transforms. - Component-local transforms: Each component uses
Save()/Restore()pairs to apply local translation to its position. This allows components to draw using local coordinates (0,0 at the component's top-left corner). - Efficient state restoration: The
Save()/Restore()pattern ensures that each component's transforms do not leak into subsequent draw calls. SkiaSharp's internal state stack handles this efficiently. - No per-component matrix storage: Unlike some frameworks that store a transform matrix per object, Beep.Skia applies transforms transiently during rendering, reducing memory overhead.
DPI Scaling
Beep.Skia provides automatic support for high-DPI displays through SkiaSharp's built-in scaling capabilities:
| Feature | Description |
|---|---|
| Automatic Detection | The the rendering surface queries the display's DPI scale factor (e.g., 1.0 for 96 DPI, 2.0 for 192 DPI) and adjusts the backing surface accordingly. |
| Logical vs. Physical Pixels | All component coordinates and sizes are expressed in logical pixels (device-independent). The DPI scale is applied at the canvas level, so component code is DPI-agnostic. |
| Sharp Rendering | The backing SKSurface is created at the physical pixel resolution, ensuring crisp text and vector graphics on high-DPI displays without blurring. |
High-DPI Canvas Configuration
var dpiScale = 1.5f; // 144 DPI; the host control reads this automatically
using var surface = SKSurface.Create(new SKImageInfo(
(int)(logicalWidth * dpiScale),
(int)(logicalHeight * dpiScale)));
var canvas = surface.Canvas;
canvas.Scale(dpiScale, dpiScale); // component coordinates stay logical
Tip
All component code should use logical coordinates only. Never multiply positions by the DPI scale factor in component DrawContent methods — the canvas handles this transparently.