< Summary

Information
Class: Trailblazer.Heightmaps.HeightmapSurface
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Heightmaps/HeightmapSurface.cs
Line coverage
95%
Covered lines: 80
Uncovered lines: 4
Coverable lines: 84
Total lines: 221
Line coverage: 95.2%
Branch coverage
80%
Covered branches: 29
Total branches: 36
Branch coverage: 80.5%
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(...)80%101092.85%
TrySampleGround(...)100%22100%
TryResolveLocalPosition(...)87.5%88100%
GetHeight(...)100%11100%
ValidateName(...)50%2266.66%
ValidateSamples(...)66.66%6680%
ValidateInterval(...)100%22100%
ValidateCompression(...)50%2266.66%
CopySamples(...)100%44100%

File(s)

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

#LineLine coverage
 1using FixedMathSharp;
 2using SwiftCollections.Dimensions;
 3using System;
 4
 5namespace Trailblazer.Heightmaps;
 6
 7/// <summary>
 8/// Immutable compressed X/Z lattice that resolves environment ground/contact Y.
 9/// </summary>
 10public sealed class HeightmapSurface
 11{
 12    private readonly SwiftShortArray2D _samples;
 13
 4014    private HeightmapSurface(
 4015        string name,
 4016        SwiftShortArray2D samples,
 4017        Vector3d minBounds,
 4018        Fixed64 interval,
 4019        HeightmapCompression compression)
 20    {
 4021        Name = name;
 4022        _samples = samples;
 4023        MinBounds = minBounds;
 4024        Interval = interval;
 4025        Compression = compression;
 4026        Width = samples.Width;
 4027        Depth = samples.Height;
 4028        MaxBounds = new Vector3d(
 4029            minBounds.x + (Width - 1) * interval,
 4030            minBounds.y,
 4031            minBounds.z + (Depth - 1) * interval);
 4032    }
 33
 34    /// <summary>
 35    /// Stable authored surface name.
 36    /// </summary>
 37    public string Name { get; }
 38
 39    /// <summary>
 40    /// Minimum world-space X/Z sample coordinate. The Y value is descriptive metadata only.
 41    /// </summary>
 42    public Vector3d MinBounds { get; }
 43
 44    /// <summary>
 45    /// Maximum inclusive world-space X/Z sample coordinate. The Y value matches <see cref="MinBounds"/>.
 46    /// </summary>
 47    public Vector3d MaxBounds { get; }
 48
 49    /// <summary>
 50    /// Distance between adjacent height samples along X and Z.
 51    /// </summary>
 52    public Fixed64 Interval { get; }
 53
 54    /// <summary>
 55    /// Number of samples along X.
 56    /// </summary>
 57    public int Width { get; }
 58
 59    /// <summary>
 60    /// Number of samples along Z.
 61    /// </summary>
 62    public int Depth { get; }
 63
 64    /// <summary>
 65    /// Compression metadata used by all stored samples.
 66    /// </summary>
 67    public HeightmapCompression Compression { get; }
 68
 69    /// <summary>
 70    /// Creates a heightmap from already compressed baked samples. This is the preferred runtime
 71    /// construction path because it avoids setup-time quantization work.
 72    /// </summary>
 73    /// <param name="name">Stable authored surface name.</param>
 74    /// <param name="samples">Compressed X/Z samples indexed by X, then Z.</param>
 75    /// <param name="minBounds">Minimum world-space X/Z sample coordinate.</param>
 76    /// <param name="interval">Positive distance between adjacent samples.</param>
 77    /// <param name="compression">Compression metadata used to decompress samples.</param>
 78    public static HeightmapSurface FromCompressed(
 79        string name,
 80        SwiftShortArray2D samples,
 81        Vector3d minBounds,
 82        Fixed64 interval,
 83        HeightmapCompression compression)
 84    {
 785        ValidateName(name);
 786        ValidateSamples(samples);
 687        ValidateInterval(interval);
 488        ValidateCompression(compression);
 89
 490        SwiftShortArray2D copy = CopySamples(samples);
 491        return new HeightmapSurface(name, copy, minBounds, interval, compression);
 92    }
 93
 94    /// <summary>
 95    /// Creates a heightmap from fixed-point heights by quantizing them once into compressed storage.
 96    /// Use this for tests, generated data, or host tooling that already owns fixed-point heights;
 97    /// runtime sampling still uses the same compressed representation as <see cref="FromCompressed"/>.
 98    /// </summary>
 99    /// <param name="name">Stable authored surface name.</param>
 100    /// <param name="heights">Ground/contact Y samples indexed by X, then Z.</param>
 101    /// <param name="minBounds">Minimum world-space X/Z sample coordinate.</param>
 102    /// <param name="interval">Positive distance between adjacent samples.</param>
 103    /// <param name="compression">Compression metadata used to quantize samples.</param>
 104    public static HeightmapSurface FromHeights(
 105        string name,
 106        Fixed64[,] heights,
 107        Vector3d minBounds,
 108        Fixed64 interval,
 109        HeightmapCompression compression)
 110    {
 38111        ValidateName(name);
 38112        if (heights == null)
 1113            throw new ArgumentNullException(nameof(heights));
 37114        ValidateInterval(interval);
 36115        ValidateCompression(compression);
 116
 36117        int width = heights.GetLength(0);
 36118        int depth = heights.GetLength(1);
 36119        if (width <= 0 || depth <= 0)
 0120            throw new ArgumentException("Height samples must have positive width and depth.", nameof(heights));
 121
 36122        var compressed = new SwiftShortArray2D(width, depth);
 146123        for (int x = 0; x < width; x++)
 152124            for (int z = 0; z < depth; z++)
 39125                compressed[x, z] = compression.CompressClamped(heights[x, z]);
 126
 36127        return new HeightmapSurface(name, compressed, minBounds, interval, compression);
 128    }
 129
 130    /// <summary>
 131    /// Attempts to sample environment ground/contact Y at the supplied world X/Z coordinate.
 132    /// </summary>
 133    public bool TrySampleGround(Vector3d worldPosition, out Fixed64 groundY)
 134    {
 29135        if (!TryResolveLocalPosition(worldPosition, out int x0, out int z0, out int x1, out int z1, out Fixed64 fraction
 136        {
 2137            groundY = Fixed64.Zero;
 2138            return false;
 139        }
 140
 27141        Fixed64 h00 = GetHeight(x0, z0);
 27142        Fixed64 h10 = GetHeight(x1, z0);
 27143        Fixed64 h01 = GetHeight(x0, z1);
 27144        Fixed64 h11 = GetHeight(x1, z1);
 145
 27146        Fixed64 xLerp0 = FixedMath.LinearInterpolate(h00, h10, fractionX);
 27147        Fixed64 xLerp1 = FixedMath.LinearInterpolate(h01, h11, fractionX);
 27148        groundY = FixedMath.LinearInterpolate(xLerp0, xLerp1, fractionZ);
 27149        return true;
 150    }
 151
 152    private bool TryResolveLocalPosition(
 153        Vector3d worldPosition,
 154        out int x0,
 155        out int z0,
 156        out int x1,
 157        out int z1,
 158        out Fixed64 fractionX,
 159        out Fixed64 fractionZ)
 160    {
 29161        Fixed64 localX = (worldPosition.x - MinBounds.x) / Interval;
 29162        Fixed64 localZ = (worldPosition.z - MinBounds.z) / Interval;
 29163        Fixed64 maxX = (Fixed64)(Width - 1);
 29164        Fixed64 maxZ = (Fixed64)(Depth - 1);
 29165        if (localX < Fixed64.Zero || localZ < Fixed64.Zero || localX > maxX || localZ > maxZ)
 166        {
 2167            x0 = z0 = x1 = z1 = -1;
 2168            fractionX = fractionZ = Fixed64.Zero;
 2169            return false;
 170        }
 171
 27172        x0 = localX.FloorToInt();
 27173        z0 = localZ.FloorToInt();
 27174        x1 = Math.Min(x0 + 1, Width - 1);
 27175        z1 = Math.Min(z0 + 1, Depth - 1);
 27176        fractionX = localX - (Fixed64)x0;
 27177        fractionZ = localZ - (Fixed64)z0;
 27178        return true;
 179    }
 180
 181    private Fixed64 GetHeight(int x, int z)
 182    {
 108183        return Compression.Decompress(_samples[x, z]);
 184    }
 185
 186    private static void ValidateName(string name)
 187    {
 45188        if (string.IsNullOrWhiteSpace(name))
 0189            throw new ArgumentException("Heightmap surface name cannot be null or whitespace.", nameof(name));
 45190    }
 191
 192    private static void ValidateSamples(SwiftShortArray2D samples)
 193    {
 7194        if (samples == null)
 1195            throw new ArgumentNullException(nameof(samples));
 6196        if (samples.Width <= 0 || samples.Height <= 0)
 0197            throw new ArgumentException("Height samples must have positive width and depth.", nameof(samples));
 6198    }
 199
 200    private static void ValidateInterval(Fixed64 interval)
 201    {
 43202        if (interval <= Fixed64.Zero)
 3203            throw new ArgumentOutOfRangeException(nameof(interval), "Heightmap interval must be positive.");
 40204    }
 205
 206    private static void ValidateCompression(HeightmapCompression compression)
 207    {
 40208        if (!compression.IsValid)
 0209            throw new ArgumentException("Heightmap compression requires a positive height step.", nameof(compression));
 40210    }
 211
 212    private static SwiftShortArray2D CopySamples(SwiftShortArray2D source)
 213    {
 4214        var copy = new SwiftShortArray2D(source.Width, source.Height);
 24215        for (int x = 0; x < source.Width; x++)
 48216            for (int z = 0; z < source.Height; z++)
 16217                copy[x, z] = source[x, z];
 218
 4219        return copy;
 220    }
 221}