Related Classes: RenderingHelper, DrawingManager — Key File: DrawingManager.Rendering.cs

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 RenderingHelper before 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

ParameterValueDescription
MinZoom0.1fMinimum zoom level (10%). Prevents excessive zoom-out where components become invisible.
MaxZoom5.0fMaximum zoom level (500%). Prevents excessive zoom-in where a single component fills the viewport.
ZoomStep0.1fIncrement/decrement per mouse wheel notch. With Ctrl held.
ZoomOriginSKPointThe 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:

PropertyTypeDefaultDescription
GridSpacingfloat20Spacing between grid lines in world-space units. The grid adapts to zoom level — at 200% zoom, each cell appears as 40 screen pixels.
ShowGridbooltrueToggles grid visibility. When false, the background renders as a solid color with no grid lines.
SnapToGridbooltrueWhen 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 SKMatrix and 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:

FeatureDescription
Automatic DetectionThe 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 PixelsAll 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 RenderingThe 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.