< Summary

Information
Class: Trailblazer.Heightmaps.HeightmapSurface
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Heightmaps/HeightmapSurface.cs
Line coverage
100%
Covered lines: 93
Uncovered lines: 0
Coverable lines: 93
Total lines: 237
Line coverage: 100%
Branch coverage
100%
Covered branches: 22
Total branches: 22
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
FromCompressed(...)100%11100%
FromHeights(...)100%66100%
TrySampleGround(...)100%22100%
TryResolveLocalPosition(...)100%88100%
GetHeight(...)100%11100%
ValidateName(...)100%11100%
ValidateSamples(...)100%22100%
ValidateInterval(...)100%11100%
ValidateCompression(...)100%11100%
CopySamples(...)100%44100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Heightmaps/HeightmapSurface.cs

#LineLine coverage
 1//=======================================================================
 2// HeightmapSurface.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using System;
 9using FixedMathSharp;
 10using SwiftCollections.Dimensions;
 11
 12namespace Trailblazer.Heightmaps;
 13
 14/// <summary>
 15/// Immutable compressed X/Z lattice that resolves environment ground/contact Y.
 16/// </summary>
 17public sealed class HeightmapSurface
 18{
 19    private readonly SwiftShortArray2D _samples;
 20
 4921    private HeightmapSurface(
 4922        string name,
 4923        SwiftShortArray2D samples,
 4924        Vector3d minBounds,
 4925        Fixed64 interval,
 4926        HeightmapCompression compression)
 27    {
 4928        Name = name;
 4929        _samples = samples;
 4930        MinBounds = minBounds;
 4931        Interval = interval;
 4932        Compression = compression;
 4933        Width = samples.Width;
 4934        Depth = samples.Height;
 4935        MaxBounds = new Vector3d(
 4936            minBounds.X + (Width - 1) * interval,
 4937            minBounds.Y,
 4938            minBounds.Z + (Depth - 1) * interval);
 4939    }
 40
 41    /// <summary>
 42    /// Stable authored surface name.
 43    /// </summary>
 44    public string Name { get; }
 45
 46    /// <summary>
 47    /// Minimum world-space X/Z sample coordinate. The Y value is descriptive metadata only.
 48    /// </summary>
 49    public Vector3d MinBounds { get; }
 50
 51    /// <summary>
 52    /// Maximum inclusive world-space X/Z sample coordinate. The Y value matches <see cref="MinBounds"/>.
 53    /// </summary>
 54    public Vector3d MaxBounds { get; }
 55
 56    /// <summary>
 57    /// Distance between adjacent height samples along X and Z.
 58    /// </summary>
 59    public Fixed64 Interval { get; }
 60
 61    /// <summary>
 62    /// Number of samples along X.
 63    /// </summary>
 64    public int Width { get; }
 65
 66    /// <summary>
 67    /// Number of samples along Z.
 68    /// </summary>
 69    public int Depth { get; }
 70
 71    /// <summary>
 72    /// Compression metadata used by all stored samples.
 73    /// </summary>
 74    public HeightmapCompression Compression { get; }
 75
 76    /// <summary>
 77    /// Creates a heightmap from already compressed baked samples. This is the preferred runtime
 78    /// construction path because it avoids setup-time quantization work.
 79    /// </summary>
 80    /// <param name="name">Stable authored surface name.</param>
 81    /// <param name="samples">Compressed X/Z samples indexed by X, then Z.</param>
 82    /// <param name="minBounds">Minimum world-space X/Z sample coordinate.</param>
 83    /// <param name="interval">Positive distance between adjacent samples.</param>
 84    /// <param name="compression">Compression metadata used to decompress samples.</param>
 85    public static HeightmapSurface FromCompressed(
 86        string name,
 87        SwiftShortArray2D samples,
 88        Vector3d minBounds,
 89        Fixed64 interval,
 90        HeightmapCompression compression)
 91    {
 1192        ValidateName(name);
 1193        ValidateSamples(samples);
 994        ValidateInterval(interval);
 795        ValidateCompression(compression);
 96
 797        SwiftShortArray2D copy = CopySamples(samples);
 798        return new HeightmapSurface(name, copy, minBounds, interval, compression);
 99    }
 100
 101    /// <summary>
 102    /// Creates a heightmap from fixed-point heights by quantizing them once into compressed storage.
 103    /// Use this for tests, generated data, or host tooling that already owns fixed-point heights;
 104    /// runtime sampling still uses the same compressed representation as <see cref="FromCompressed"/>.
 105    /// </summary>
 106    /// <param name="name">Stable authored surface name.</param>
 107    /// <param name="heights">Ground/contact Y samples indexed by X, then Z.</param>
 108    /// <param name="minBounds">Minimum world-space X/Z sample coordinate.</param>
 109    /// <param name="interval">Positive distance between adjacent samples.</param>
 110    /// <param name="compression">Compression metadata used to quantize samples.</param>
 111    public static HeightmapSurface FromHeights(
 112        string name,
 113        Fixed64[,] heights,
 114        Vector3d minBounds,
 115        Fixed64 interval,
 116        HeightmapCompression compression)
 117    {
 47118        ValidateName(name);
 46119        SwiftThrowHelper.ThrowIfNull(heights, nameof(heights));
 45120        ValidateInterval(interval);
 44121        ValidateCompression(compression);
 122
 43123        int width = heights.GetLength(0);
 43124        int depth = heights.GetLength(1);
 43125        SwiftThrowHelper.ThrowIfArgument(
 43126            width <= 0 || depth <= 0,
 43127            paramName: nameof(heights),
 43128            message: "Height samples must have positive width and depth.");
 129
 42130        var compressed = new SwiftShortArray2D(width, depth);
 170131        for (int x = 0; x < width; x++)
 176132            for (int z = 0; z < depth; z++)
 45133                compressed[x, z] = compression.CompressClamped(heights[x, z]);
 134
 42135        return new HeightmapSurface(name, compressed, minBounds, interval, compression);
 136    }
 137
 138    /// <summary>
 139    /// Attempts to sample environment ground/contact Y at the supplied world X/Z coordinate.
 140    /// </summary>
 141    public bool TrySampleGround(Vector3d worldPosition, out Fixed64 groundY)
 142    {
 38143        if (!TryResolveLocalPosition(worldPosition, out int x0, out int z0, out int x1, out int z1, out Fixed64 fraction
 144        {
 4145            groundY = Fixed64.Zero;
 4146            return false;
 147        }
 148
 34149        Fixed64 h00 = GetHeight(x0, z0);
 34150        Fixed64 h10 = GetHeight(x1, z0);
 34151        Fixed64 h01 = GetHeight(x0, z1);
 34152        Fixed64 h11 = GetHeight(x1, z1);
 153
 34154        Fixed64 xLerp0 = FixedMath.Lerp(h00, h10, fractionX);
 34155        Fixed64 xLerp1 = FixedMath.Lerp(h01, h11, fractionX);
 34156        groundY = FixedMath.Lerp(xLerp0, xLerp1, fractionZ);
 34157        return true;
 158    }
 159
 160    private bool TryResolveLocalPosition(
 161        Vector3d worldPosition,
 162        out int x0,
 163        out int z0,
 164        out int x1,
 165        out int z1,
 166        out Fixed64 fractionX,
 167        out Fixed64 fractionZ)
 168    {
 38169        Fixed64 localX = (worldPosition.X - MinBounds.X) / Interval;
 38170        Fixed64 localZ = (worldPosition.Z - MinBounds.Z) / Interval;
 38171        Fixed64 maxX = (Fixed64)(Width - 1);
 38172        Fixed64 maxZ = (Fixed64)(Depth - 1);
 38173        if (localX < Fixed64.Zero || localZ < Fixed64.Zero || localX > maxX || localZ > maxZ)
 174        {
 4175            x0 = z0 = x1 = z1 = -1;
 4176            fractionX = fractionZ = Fixed64.Zero;
 4177            return false;
 178        }
 179
 34180        x0 = localX.FloorToInt();
 34181        z0 = localZ.FloorToInt();
 34182        x1 = Math.Min(x0 + 1, Width - 1);
 34183        z1 = Math.Min(z0 + 1, Depth - 1);
 34184        fractionX = localX - (Fixed64)x0;
 34185        fractionZ = localZ - (Fixed64)z0;
 34186        return true;
 187    }
 188
 189    private Fixed64 GetHeight(int x, int z)
 190    {
 136191        return Compression.Decompress(_samples[x, z]);
 192    }
 193
 194    private static void ValidateName(string name)
 195    {
 58196        SwiftThrowHelper.ThrowIfArgument(
 58197            string.IsNullOrWhiteSpace(name),
 58198            paramName: nameof(name),
 58199            message: "Heightmap surface name cannot be null or whitespace.");
 57200    }
 201
 202    private static void ValidateSamples(SwiftShortArray2D samples)
 203    {
 11204        SwiftThrowHelper.ThrowIfNull(samples, nameof(samples));
 10205        SwiftThrowHelper.ThrowIfArgument(
 10206            samples.Width <= 0 || samples.Height <= 0,
 10207            paramName: nameof(samples),
 10208            message: "Height samples must have positive width and depth.");
 9209    }
 210
 211    private static void ValidateInterval(Fixed64 interval)
 212    {
 54213        SwiftThrowHelper.ThrowIfArgumentOutOfRange(
 54214            interval <= Fixed64.Zero,
 54215            actualValue: null,
 54216            paramName: nameof(interval),
 54217            message: "Heightmap interval must be positive.");
 51218    }
 219
 220    private static void ValidateCompression(HeightmapCompression compression)
 221    {
 51222        SwiftThrowHelper.ThrowIfArgument(
 51223            !compression.IsValid,
 51224            paramName: nameof(compression),
 51225            message: "Heightmap compression requires a positive height step.");
 50226    }
 227
 228    private static SwiftShortArray2D CopySamples(SwiftShortArray2D source)
 229    {
 7230        var copy = new SwiftShortArray2D(source.Width, source.Height);
 42231        for (int x = 0; x < source.Width; x++)
 84232            for (int z = 0; z < source.Height; z++)
 28233                copy[x, z] = source[x, z];
 234
 7235        return copy;
 236    }
 237}