diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58c6be6..2bbbcf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,11 +24,12 @@ concurrency: jobs: gates: - uses: resq-software/.github/.github/workflows/required.yml@f4b51a620aa1bf89c0bce4f434b36f92ff7d517d + uses: resq-software/.github/.github/workflows/required.yml@109c36b59b6c94e235e4590598fd5f719d7d321a with: lang: dotnet dotnet-solution: ResQ.Viz.sln codeql-languages: '["csharp"]' + submodules: recursive secrets: inherit required: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 2cef137..9dad734 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -24,4 +24,5 @@ jobs: uses: resq-software/.github/.github/workflows/security-scan.yml@main with: languages: '["csharp","actions"]' + submodules: recursive secrets: inherit diff --git a/lib/dotnet-sdk b/lib/dotnet-sdk index 5f80948..63bf1cb 160000 --- a/lib/dotnet-sdk +++ b/lib/dotnet-sdk @@ -1 +1 @@ -Subproject commit 5f809486ae68541ccebfbfaac3a90a2c3f365eb1 +Subproject commit 63bf1cb21b790098f74ba2c6f92fcd6662df3098 diff --git a/src/ResQ.Viz.Web/Controllers/SimController.cs b/src/ResQ.Viz.Web/Controllers/SimController.cs index 02ad755..ee2cdbe 100644 --- a/src/ResQ.Viz.Web/Controllers/SimController.cs +++ b/src/ResQ.Viz.Web/Controllers/SimController.cs @@ -112,12 +112,12 @@ public IActionResult SendCommand(string id, [FromBody] DroneCommandRequest reque FlightCommand command = request.Type.ToLowerInvariant() switch { "hover" => FlightCommand.Hover(), - "rtl" => FlightCommand.RTL(), - "land" => FlightCommand.Land(), + "rtl" => FlightCommand.RTL(), + "land" => FlightCommand.Land(), "goto" when request.Target is { Length: 3 } => FlightCommand.GoTo(new Vector3(request.Target[0], request.Target[1], request.Target[2])), "goto" => default, // handled below - _ => default, + _ => default, }; if (request.Type.ToLowerInvariant() == "goto" && request.Target is not { Length: 3 }) diff --git a/src/ResQ.Viz.Web/Program.cs b/src/ResQ.Viz.Web/Program.cs index fb50699..32744c9 100644 --- a/src/ResQ.Viz.Web/Program.cs +++ b/src/ResQ.Viz.Web/Program.cs @@ -4,7 +4,7 @@ using System.Threading.RateLimiting; using Microsoft.AspNetCore.RateLimiting; -using Vite.AspNetCore.Extensions; +using Vite.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddSignalR(); diff --git a/src/ResQ.Viz.Web/Services/ScenarioService.cs b/src/ResQ.Viz.Web/Services/ScenarioService.cs index f29eb52..254eccb 100644 --- a/src/ResQ.Viz.Web/Services/ScenarioService.cs +++ b/src/ResQ.Viz.Web/Services/ScenarioService.cs @@ -39,7 +39,7 @@ public ScenarioService(IConfiguration configuration) var entries = new List<(string Id, Vector3 Pos)>(); foreach (var entry in child.GetChildren()) { - var id = entry["id"] ?? string.Empty; + var id = entry["id"] ?? string.Empty; var pos = entry.GetSection("pos").Get() ?? Array.Empty(); if (!string.IsNullOrEmpty(id) && pos.Length == 3) entries.Add((id, new Vector3(pos[0], pos[1], pos[2]))); diff --git a/src/ResQ.Viz.Web/Services/SimulationService.cs b/src/ResQ.Viz.Web/Services/SimulationService.cs index 05b86d9..bbfc79c 100644 --- a/src/ResQ.Viz.Web/Services/SimulationService.cs +++ b/src/ResQ.Viz.Web/Services/SimulationService.cs @@ -76,13 +76,13 @@ public sealed class SimulationService : BackgroundService /// Logger instance. public SimulationService(IHubContext hubContext, VizFrameBuilder frameBuilder, ILogger logger) { - _hubContext = hubContext; + _hubContext = hubContext; _frameBuilder = frameBuilder; - _logger = logger; - _terrain = new TerrainNoiseService(); - _weather = new UpdatableWeatherSystem(new WeatherConfig()); - _world = new SimulationWorld(new SimulationConfig(), _terrain, _weather); - _swarm = new SwarmController(_terrain); + _logger = logger; + _terrain = new TerrainNoiseService(); + _weather = new UpdatableWeatherSystem(new WeatherConfig()); + _world = new SimulationWorld(new SimulationConfig(), _terrain, _weather); + _swarm = new SwarmController(_terrain); _logger.LogInformation("SimulationService initialised."); } @@ -124,9 +124,9 @@ public void SetWeather(string mode, double windSpeed, double direction) { var weatherMode = mode.ToLowerInvariant() switch { - "steady" => WeatherMode.Steady, + "steady" => WeatherMode.Steady, "turbulent" => WeatherMode.Turbulent, - _ => WeatherMode.Calm, + _ => WeatherMode.Calm, }; _weather.Update(new WeatherConfig(weatherMode, direction, windSpeed)); _logger.LogInformation("Weather updated: mode={Mode}, speed={Speed} m/s, direction={Dir}°.", weatherMode, windSpeed, direction); @@ -161,10 +161,10 @@ public void Reset() { lock (_lock) { - _world = new SimulationWorld(new SimulationConfig(), _terrain, _weather); - _simTime = 0; - _tickCount = 0; - _swarmTick = 0; + _world = new SimulationWorld(new SimulationConfig(), _terrain, _weather); + _simTime = 0; + _tickCount = 0; + _swarmTick = 0; _logger.LogInformation("Simulation reset."); } } @@ -181,13 +181,13 @@ public IReadOnlyList GetSnapshot() var q = state.Orientation; return new DroneSnapshot( - Id: d.Id, + Id: d.Id, Position: [state.Position.X, state.Position.Y, state.Position.Z], Rotation: [q.X, q.Y, q.Z, q.W], Velocity: [state.Velocity.X, state.Velocity.Y, state.Velocity.Z], - Battery: state.BatteryPercent, - Status: d.FlightModel.HasLanded ? "landed" : "flying", - Armed: !d.FlightModel.HasLanded); + Battery: state.BatteryPercent, + Status: d.FlightModel.HasLanded ? "landed" : "flying", + Armed: !d.FlightModel.HasLanded); }).ToList(); } } @@ -224,7 +224,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // Build and broadcast frame outside the lock to avoid holding it during async I/O. var snapshot = GetSnapshot(); - var frame = _frameBuilder.Build(snapshot, _simTime); + var frame = _frameBuilder.Build(snapshot, _simTime); try { await _hubContext.Clients.All.SendAsync("ReceiveFrame", frame, stoppingToken); diff --git a/src/ResQ.Viz.Web/Services/SwarmController.cs b/src/ResQ.Viz.Web/Services/SwarmController.cs index 2b9f669..8c37784 100644 --- a/src/ResQ.Viz.Web/Services/SwarmController.cs +++ b/src/ResQ.Viz.Web/Services/SwarmController.cs @@ -77,14 +77,14 @@ public DroneRole(Vector3[] route, int routeIndex, double assignedAt, bool retiri public void SetTerrainPreset(string preset, TerrainNoiseService terrain, IReadOnlyList drones) { _terrain = terrain; - _preset = preset.ToLowerInvariant(); - _minAgl = _preset switch + _preset = preset.ToLowerInvariant(); + _minAgl = _preset switch { "ridgeline" => 20f, - "coastal" => 15f, - "canyon" => 12f, - "dunes" => 8f, - _ => 25f, // alpine default + "coastal" => 15f, + "canyon" => 12f, + "dunes" => 8f, + _ => 25f, // alpine default }; // Rebuild routes for all current drones under the new terrain @@ -214,9 +214,9 @@ private Vector3[] BuildRoute(int droneIndex, int totalDrones, Vector3 spawnPos) { return _scenario switch { - "single" => BuildLawnmowerRoute(0, 0, 500f, 200f), - "sar" => BuildSarSectorRoute(droneIndex, totalDrones), - _ => BuildSectorPatrolRoute(droneIndex, totalDrones), + "single" => BuildLawnmowerRoute(0, 0, 500f, 200f), + "sar" => BuildSarSectorRoute(droneIndex, totalDrones), + _ => BuildSectorPatrolRoute(droneIndex, totalDrones), }; } @@ -256,11 +256,11 @@ private Vector3[] BuildSectorPatrolRoute(int idx, int total) // Terrain-specific route shape return _preset switch { - "coastal" => BuildIslandPatrolRoute(cx, cz, Math.Min(cellW, cellH) * 0.38f), - "canyon" => BuildCanyonCorridor(idx, total), - "dunes" => BuildDuneSweep(cx, cz, cellW * 0.4f, cellH * 0.4f), + "coastal" => BuildIslandPatrolRoute(cx, cz, Math.Min(cellW, cellH) * 0.38f), + "canyon" => BuildCanyonCorridor(idx, total), + "dunes" => BuildDuneSweep(cx, cz, cellW * 0.4f, cellH * 0.4f), "ridgeline" => BuildRidgelineRoute(cx, cz, cellW * 0.45f), - _ => BuildOctagonRoute(cx, cz, Math.Min(cellW, cellH) * 0.40f), + _ => BuildOctagonRoute(cx, cz, Math.Min(cellW, cellH) * 0.40f), }; } @@ -271,7 +271,7 @@ private Vector3[] BuildSarSectorRoute(int idx, int total) total = Math.Max(total, 1); float stripW = 2000f / total; float stripCx = -1000f + (idx + 0.5f) * stripW; - float halfW = stripW * 0.45f; + float halfW = stripW * 0.45f; return BuildLawnmowerRoute(stripCx, 0f, halfW, Math.Max(halfW * 0.4f, 60f)); } diff --git a/src/ResQ.Viz.Web/Services/TerrainNoiseService.cs b/src/ResQ.Viz.Web/Services/TerrainNoiseService.cs index bcdbadf..8849a77 100644 --- a/src/ResQ.Viz.Web/Services/TerrainNoiseService.cs +++ b/src/ResQ.Viz.Web/Services/TerrainNoiseService.cs @@ -34,10 +34,10 @@ public void SetPreset(string key) => public double GetElevation(double x, double z) => _preset switch { "ridgeline" => RidgelineHeight(x, z), - "coastal" => CoastalHeight(x, z), - "canyon" => CanyonHeight(x, z), - "dunes" => DuneHeight(x, z), - _ => AlpineHeight(x, z), + "coastal" => CoastalHeight(x, z), + "canyon" => CanyonHeight(x, z), + "dunes" => DuneHeight(x, z), + _ => AlpineHeight(x, z), }; /// @@ -61,10 +61,10 @@ private static double Noise(double x, double z) double fx = x - ix, fz = z - iz; double ux = fx * fx * fx * (fx * (fx * 6 - 15) + 10); double uz = fz * fz * fz * (fz * (fz * 6 - 15) + 10); - return H(ix, iz) * (1 - ux) * (1 - uz) - + H(ix+1, iz) * ux * (1 - uz) - + H(ix, iz+1) * (1 - ux) * uz - + H(ix+1, iz+1) * ux * uz; + return H(ix, iz) * (1 - ux) * (1 - uz) + + H(ix + 1, iz) * ux * (1 - uz) + + H(ix, iz + 1) * (1 - ux) * uz + + H(ix + 1, iz + 1) * ux * uz; } private static double Fbm(double x, double z, int octaves) @@ -84,12 +84,12 @@ private static double Ridged(double x, double z, int octaves, double value = 0, weight = 1; for (int i = 0; i < octaves; i++) { - double freq = Math.Pow(lacunarity, i); - double n = Noise(x * freq, z * freq); + double freq = Math.Pow(lacunarity, i); + double n = Noise(x * freq, z * freq); double signal = 1 - Math.Abs(n * 2 - 1); - double s2 = signal * signal * weight; - value += s2; - weight = Math.Min(signal * gain, 1.0); + double s2 = signal * signal * weight; + value += s2; + weight = Math.Min(signal * gain, 1.0); } return value / octaves; } @@ -110,9 +110,9 @@ private static double AlpineHeight(double x, double z) double wx = (Fbm(x * freq + 0.0, z * freq + 0.0, 3) * 2 - 1) * 260; double wz = (Fbm(x * freq + 5.2, z * freq + 1.3, 3) * 2 - 1) * 260; - double large = (Fbm((x + wx) * 0.00055, (z + wz) * 0.00055, 6) * 2 - 1) * 46; - double medium = (Fbm(x * 0.0028 + 4.1, z * 0.0028 + 8.6, 4) * 2 - 1) * 16; - double fine = (Fbm(x * 0.013 + 2.2, z * 0.013 + 5.9, 3) * 2 - 1) * 3; + double large = (Fbm((x + wx) * 0.00055, (z + wz) * 0.00055, 6) * 2 - 1) * 46; + double medium = (Fbm(x * 0.0028 + 4.1, z * 0.0028 + 8.6, 4) * 2 - 1) * 16; + double fine = (Fbm(x * 0.013 + 2.2, z * 0.013 + 5.9, 3) * 2 - 1) * 3; double peaks = 0; foreach (var (px, pz, ph, pr) in AlpinePeaks) @@ -129,7 +129,7 @@ private static double RidgelineHeight(double x, double z) { double ridge = Ridged(x * 0.00075 + 1.1, z * 0.00075 + 0.8, 8) * 195; double baseH = (Fbm(x * 0.0022 + 3.1, z * 0.0022 + 7.4, 4) * 2 - 1) * 22; - double fine = (Fbm(x * 0.011 + 2.2, z * 0.011 + 5.9, 3) * 2 - 1) * 4; + double fine = (Fbm(x * 0.011 + 2.2, z * 0.011 + 5.9, 3) * 2 - 1) * 4; return 8 + ridge + baseH + fine; } @@ -153,8 +153,8 @@ private static double CoastalHeight(double x, double z) if (t > 0) mask = Math.Max(mask, t); } double perturbN = (Fbm(x * 0.005 + 2.1, z * 0.005 + 0.7, 4) * 2 - 1) * 0.28; - double m = Math.Max(0, mask + perturbN); - double topo = (Fbm(x * 0.0040 + 1.3, z * 0.0040 + 5.2, 5) * 2 - 1) * 62; + double m = Math.Max(0, mask + perturbN); + double topo = (Fbm(x * 0.0040 + 1.3, z * 0.0040 + 5.2, 5) * 2 - 1) * 62; return topo * Math.Pow(m, 1.3) - 4; } @@ -164,12 +164,12 @@ private static double CanyonHeight(double x, double z) { double baseH = (Fbm(x * 0.00095 + 1.3, z * 0.00095 + 2.7, 5) * 2 - 1) * 28 + 55; const double T = 20; - double frac = (((baseH % T) + T) % T) / T; - double step = Math.Min(frac / 0.18, 1.0); - double sf = step * step * (3 - 2 * step); + double frac = (((baseH % T) + T) % T) / T; + double step = Math.Min(frac / 0.18, 1.0); + double sf = step * step * (3 - 2 * step); double terraced = baseH - frac * T + sf * T; - double canyonN = Fbm(x * 0.0048 + 7.1, z * 0.0038 + 3.4, 4); - double depth = canyonN < 0.32 ? Math.Pow(1 - canyonN / 0.32, 2) * 80 : 0; + double canyonN = Fbm(x * 0.0048 + 7.1, z * 0.0038 + 3.4, 4); + double depth = canyonN < 0.32 ? Math.Pow(1 - canyonN / 0.32, 2) * 80 : 0; return terraced - depth; } @@ -178,12 +178,12 @@ private static double CanyonHeight(double x, double z) private static double DuneHeight(double x, double z) { double d1n = Noise(x * 0.0028 + 0.0, z * 0.0145 + 0.0); - double d1 = Math.Pow(1 - Math.Abs(d1n * 2 - 1), 2.8) * 28; + double d1 = Math.Pow(1 - Math.Abs(d1n * 2 - 1), 2.8) * 28; double ang = Math.PI * 0.15; - double cx = x * Math.Cos(ang) + z * Math.Sin(ang); - double cz = -x * Math.Sin(ang) + z * Math.Cos(ang); + double cx = x * Math.Cos(ang) + z * Math.Sin(ang); + double cz = -x * Math.Sin(ang) + z * Math.Cos(ang); double d2n = Noise(cx * 0.0038 + 5.2, cz * 0.018 + 2.1); - double d2 = Math.Pow(1 - Math.Abs(d2n * 2 - 1), 2.2) * 14; + double d2 = Math.Pow(1 - Math.Abs(d2n * 2 - 1), 2.2) * 14; double baseH = (Fbm(x * 0.0010, z * 0.0010, 4) * 2 - 1) * 14; double field = Noise(x * 0.0018 + 1.7, z * 0.0018 + 3.3); return 4 + baseH + d1 * (0.5 + field * 0.5) + d2; diff --git a/src/ResQ.Viz.Web/Services/VizFrameBuilder.cs b/src/ResQ.Viz.Web/Services/VizFrameBuilder.cs index 5477cdb..0c43a41 100644 --- a/src/ResQ.Viz.Web/Services/VizFrameBuilder.cs +++ b/src/ResQ.Viz.Web/Services/VizFrameBuilder.cs @@ -27,16 +27,16 @@ public sealed class VizFrameBuilder private sealed record SurvivorTargetConfig { - public string Id { get; init; } = ""; + public string Id { get; init; } = ""; public float[] Pos { get; init; } = []; } private sealed record HazardZoneConfig { - public string Id { get; init; } = ""; - public string Type { get; init; } = ""; + public string Id { get; init; } = ""; + public string Type { get; init; } = ""; public float[] Center { get; init; } = []; - public float Radius { get; init; } + public float Radius { get; init; } } private sealed record SurvivorTarget(string Id, Vector3 Position); @@ -76,8 +76,8 @@ public VizFrameBuilder(IConfiguration configuration) /// public VizFrameBuilder() { - _survivors = []; - _hazards = []; + _survivors = []; + _hazards = []; _detectionRange = 35f; } @@ -94,11 +94,11 @@ public VizFrame Build(IReadOnlyList drones, double simTime) .ToList(); return new VizFrame( - Time: simTime, - Drones: droneStates, + Time: simTime, + Drones: droneStates, Detections: BuildDetections(drones), - Hazards: BuildHazards(), - Mesh: null); + Hazards: BuildHazards(), + Mesh: null); } // ── Private helpers ──────────────────────────────────────────────────────── @@ -116,10 +116,10 @@ private IReadOnlyList BuildDetections(IReadOnlyList BuildDetections(IReadOnlyList BuildHazards() => _hazards.Select(h => new HazardVizState( - Id: h.Id, - Type: h.Type, - Center: h.Center.Length == 3 ? [h.Center[0], h.Center[1], h.Center[2]] : [0f, 0f, 0f], - Radius: h.Radius, + Id: h.Id, + Type: h.Type, + Center: h.Center.Length == 3 ? [h.Center[0], h.Center[1], h.Center[2]] : [0f, 0f, 0f], + Radius: h.Radius, Severity: "medium")).ToList(); } diff --git a/tests/ResQ.Viz.Web.Tests/ScenarioServiceTests.cs b/tests/ResQ.Viz.Web.Tests/ScenarioServiceTests.cs index aa7c9bc..52955e3 100644 --- a/tests/ResQ.Viz.Web.Tests/ScenarioServiceTests.cs +++ b/tests/ResQ.Viz.Web.Tests/ScenarioServiceTests.cs @@ -33,69 +33,125 @@ private static ScenarioService CreateScenarioService() var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["Scenarios:single:0:id"] = "drone-1", - ["Scenarios:single:0:pos:0"] = "0", - ["Scenarios:single:0:pos:1"] = "15", - ["Scenarios:single:0:pos:2"] = "0", - - ["Scenarios:swarm-5:0:id"] = "drone-1", - ["Scenarios:swarm-5:0:pos:0"] = "-20", ["Scenarios:swarm-5:0:pos:1"] = "15", ["Scenarios:swarm-5:0:pos:2"] = "-20", - ["Scenarios:swarm-5:1:id"] = "drone-2", - ["Scenarios:swarm-5:1:pos:0"] = "20", ["Scenarios:swarm-5:1:pos:1"] = "18", ["Scenarios:swarm-5:1:pos:2"] = "-20", - ["Scenarios:swarm-5:2:id"] = "drone-3", - ["Scenarios:swarm-5:2:pos:0"] = "0", ["Scenarios:swarm-5:2:pos:1"] = "20", ["Scenarios:swarm-5:2:pos:2"] = "0", - ["Scenarios:swarm-5:3:id"] = "drone-4", - ["Scenarios:swarm-5:3:pos:0"] = "-20", ["Scenarios:swarm-5:3:pos:1"] = "18", ["Scenarios:swarm-5:3:pos:2"] = "20", - ["Scenarios:swarm-5:4:id"] = "drone-5", - ["Scenarios:swarm-5:4:pos:0"] = "20", ["Scenarios:swarm-5:4:pos:1"] = "15", ["Scenarios:swarm-5:4:pos:2"] = "20", - - ["Scenarios:swarm-20:0:id"] = "drone-1", - ["Scenarios:swarm-20:0:pos:0"] = "-60", ["Scenarios:swarm-20:0:pos:1"] = "15", ["Scenarios:swarm-20:0:pos:2"] = "-60", - ["Scenarios:swarm-20:1:id"] = "drone-2", - ["Scenarios:swarm-20:1:pos:0"] = "-20", ["Scenarios:swarm-20:1:pos:1"] = "18", ["Scenarios:swarm-20:1:pos:2"] = "-60", - ["Scenarios:swarm-20:2:id"] = "drone-3", - ["Scenarios:swarm-20:2:pos:0"] = "20", ["Scenarios:swarm-20:2:pos:1"] = "20", ["Scenarios:swarm-20:2:pos:2"] = "-60", - ["Scenarios:swarm-20:3:id"] = "drone-4", - ["Scenarios:swarm-20:3:pos:0"] = "60", ["Scenarios:swarm-20:3:pos:1"] = "15", ["Scenarios:swarm-20:3:pos:2"] = "-60", - ["Scenarios:swarm-20:4:id"] = "drone-5", - ["Scenarios:swarm-20:4:pos:0"] = "-60", ["Scenarios:swarm-20:4:pos:1"] = "22", ["Scenarios:swarm-20:4:pos:2"] = "-20", - ["Scenarios:swarm-20:5:id"] = "drone-6", - ["Scenarios:swarm-20:5:pos:0"] = "-20", ["Scenarios:swarm-20:5:pos:1"] = "18", ["Scenarios:swarm-20:5:pos:2"] = "-20", - ["Scenarios:swarm-20:6:id"] = "drone-7", - ["Scenarios:swarm-20:6:pos:0"] = "20", ["Scenarios:swarm-20:6:pos:1"] = "25", ["Scenarios:swarm-20:6:pos:2"] = "-20", - ["Scenarios:swarm-20:7:id"] = "drone-8", - ["Scenarios:swarm-20:7:pos:0"] = "60", ["Scenarios:swarm-20:7:pos:1"] = "20", ["Scenarios:swarm-20:7:pos:2"] = "-20", - ["Scenarios:swarm-20:8:id"] = "drone-9", - ["Scenarios:swarm-20:8:pos:0"] = "-60", ["Scenarios:swarm-20:8:pos:1"] = "15", ["Scenarios:swarm-20:8:pos:2"] = "20", - ["Scenarios:swarm-20:9:id"] = "drone-10", - ["Scenarios:swarm-20:9:pos:0"] = "-20", ["Scenarios:swarm-20:9:pos:1"] = "22", ["Scenarios:swarm-20:9:pos:2"] = "20", - ["Scenarios:swarm-20:10:id"] = "drone-11", - ["Scenarios:swarm-20:10:pos:0"] = "20", ["Scenarios:swarm-20:10:pos:1"] = "18", ["Scenarios:swarm-20:10:pos:2"] = "20", - ["Scenarios:swarm-20:11:id"] = "drone-12", - ["Scenarios:swarm-20:11:pos:0"] = "60", ["Scenarios:swarm-20:11:pos:1"] = "25", ["Scenarios:swarm-20:11:pos:2"] = "20", - ["Scenarios:swarm-20:12:id"] = "drone-13", - ["Scenarios:swarm-20:12:pos:0"] = "-60", ["Scenarios:swarm-20:12:pos:1"] = "20", ["Scenarios:swarm-20:12:pos:2"] = "60", - ["Scenarios:swarm-20:13:id"] = "drone-14", - ["Scenarios:swarm-20:13:pos:0"] = "-20", ["Scenarios:swarm-20:13:pos:1"] = "15", ["Scenarios:swarm-20:13:pos:2"] = "60", - ["Scenarios:swarm-20:14:id"] = "drone-15", - ["Scenarios:swarm-20:14:pos:0"] = "20", ["Scenarios:swarm-20:14:pos:1"] = "22", ["Scenarios:swarm-20:14:pos:2"] = "60", - ["Scenarios:swarm-20:15:id"] = "drone-16", - ["Scenarios:swarm-20:15:pos:0"] = "60", ["Scenarios:swarm-20:15:pos:1"] = "18", ["Scenarios:swarm-20:15:pos:2"] = "60", - ["Scenarios:swarm-20:16:id"] = "drone-17", - ["Scenarios:swarm-20:16:pos:0"] = "0", ["Scenarios:swarm-20:16:pos:1"] = "30", ["Scenarios:swarm-20:16:pos:2"] = "0", - ["Scenarios:swarm-20:17:id"] = "drone-18", - ["Scenarios:swarm-20:17:pos:0"] = "-40", ["Scenarios:swarm-20:17:pos:1"] = "28", ["Scenarios:swarm-20:17:pos:2"] = "0", - ["Scenarios:swarm-20:18:id"] = "drone-19", - ["Scenarios:swarm-20:18:pos:0"] = "40", ["Scenarios:swarm-20:18:pos:1"] = "28", ["Scenarios:swarm-20:18:pos:2"] = "0", - ["Scenarios:swarm-20:19:id"] = "drone-20", - ["Scenarios:swarm-20:19:pos:0"] = "0", ["Scenarios:swarm-20:19:pos:1"] = "25", ["Scenarios:swarm-20:19:pos:2"] = "40", - - ["Scenarios:sar:0:id"] = "sar-lead", - ["Scenarios:sar:0:pos:0"] = "0", ["Scenarios:sar:0:pos:1"] = "20", ["Scenarios:sar:0:pos:2"] = "0", - ["Scenarios:sar:1:id"] = "sar-scout", - ["Scenarios:sar:1:pos:0"] = "30", ["Scenarios:sar:1:pos:1"] = "25", ["Scenarios:sar:1:pos:2"] = "30", - ["Scenarios:sar:2:id"] = "sar-relay", - ["Scenarios:sar:2:pos:0"] = "-30", ["Scenarios:sar:2:pos:1"] = "18", ["Scenarios:sar:2:pos:2"] = "-30", + ["Scenarios:single:0:id"] = "drone-1", + ["Scenarios:single:0:pos:0"] = "0", + ["Scenarios:single:0:pos:1"] = "15", + ["Scenarios:single:0:pos:2"] = "0", + + ["Scenarios:swarm-5:0:id"] = "drone-1", + ["Scenarios:swarm-5:0:pos:0"] = "-20", + ["Scenarios:swarm-5:0:pos:1"] = "15", + ["Scenarios:swarm-5:0:pos:2"] = "-20", + ["Scenarios:swarm-5:1:id"] = "drone-2", + ["Scenarios:swarm-5:1:pos:0"] = "20", + ["Scenarios:swarm-5:1:pos:1"] = "18", + ["Scenarios:swarm-5:1:pos:2"] = "-20", + ["Scenarios:swarm-5:2:id"] = "drone-3", + ["Scenarios:swarm-5:2:pos:0"] = "0", + ["Scenarios:swarm-5:2:pos:1"] = "20", + ["Scenarios:swarm-5:2:pos:2"] = "0", + ["Scenarios:swarm-5:3:id"] = "drone-4", + ["Scenarios:swarm-5:3:pos:0"] = "-20", + ["Scenarios:swarm-5:3:pos:1"] = "18", + ["Scenarios:swarm-5:3:pos:2"] = "20", + ["Scenarios:swarm-5:4:id"] = "drone-5", + ["Scenarios:swarm-5:4:pos:0"] = "20", + ["Scenarios:swarm-5:4:pos:1"] = "15", + ["Scenarios:swarm-5:4:pos:2"] = "20", + + ["Scenarios:swarm-20:0:id"] = "drone-1", + ["Scenarios:swarm-20:0:pos:0"] = "-60", + ["Scenarios:swarm-20:0:pos:1"] = "15", + ["Scenarios:swarm-20:0:pos:2"] = "-60", + ["Scenarios:swarm-20:1:id"] = "drone-2", + ["Scenarios:swarm-20:1:pos:0"] = "-20", + ["Scenarios:swarm-20:1:pos:1"] = "18", + ["Scenarios:swarm-20:1:pos:2"] = "-60", + ["Scenarios:swarm-20:2:id"] = "drone-3", + ["Scenarios:swarm-20:2:pos:0"] = "20", + ["Scenarios:swarm-20:2:pos:1"] = "20", + ["Scenarios:swarm-20:2:pos:2"] = "-60", + ["Scenarios:swarm-20:3:id"] = "drone-4", + ["Scenarios:swarm-20:3:pos:0"] = "60", + ["Scenarios:swarm-20:3:pos:1"] = "15", + ["Scenarios:swarm-20:3:pos:2"] = "-60", + ["Scenarios:swarm-20:4:id"] = "drone-5", + ["Scenarios:swarm-20:4:pos:0"] = "-60", + ["Scenarios:swarm-20:4:pos:1"] = "22", + ["Scenarios:swarm-20:4:pos:2"] = "-20", + ["Scenarios:swarm-20:5:id"] = "drone-6", + ["Scenarios:swarm-20:5:pos:0"] = "-20", + ["Scenarios:swarm-20:5:pos:1"] = "18", + ["Scenarios:swarm-20:5:pos:2"] = "-20", + ["Scenarios:swarm-20:6:id"] = "drone-7", + ["Scenarios:swarm-20:6:pos:0"] = "20", + ["Scenarios:swarm-20:6:pos:1"] = "25", + ["Scenarios:swarm-20:6:pos:2"] = "-20", + ["Scenarios:swarm-20:7:id"] = "drone-8", + ["Scenarios:swarm-20:7:pos:0"] = "60", + ["Scenarios:swarm-20:7:pos:1"] = "20", + ["Scenarios:swarm-20:7:pos:2"] = "-20", + ["Scenarios:swarm-20:8:id"] = "drone-9", + ["Scenarios:swarm-20:8:pos:0"] = "-60", + ["Scenarios:swarm-20:8:pos:1"] = "15", + ["Scenarios:swarm-20:8:pos:2"] = "20", + ["Scenarios:swarm-20:9:id"] = "drone-10", + ["Scenarios:swarm-20:9:pos:0"] = "-20", + ["Scenarios:swarm-20:9:pos:1"] = "22", + ["Scenarios:swarm-20:9:pos:2"] = "20", + ["Scenarios:swarm-20:10:id"] = "drone-11", + ["Scenarios:swarm-20:10:pos:0"] = "20", + ["Scenarios:swarm-20:10:pos:1"] = "18", + ["Scenarios:swarm-20:10:pos:2"] = "20", + ["Scenarios:swarm-20:11:id"] = "drone-12", + ["Scenarios:swarm-20:11:pos:0"] = "60", + ["Scenarios:swarm-20:11:pos:1"] = "25", + ["Scenarios:swarm-20:11:pos:2"] = "20", + ["Scenarios:swarm-20:12:id"] = "drone-13", + ["Scenarios:swarm-20:12:pos:0"] = "-60", + ["Scenarios:swarm-20:12:pos:1"] = "20", + ["Scenarios:swarm-20:12:pos:2"] = "60", + ["Scenarios:swarm-20:13:id"] = "drone-14", + ["Scenarios:swarm-20:13:pos:0"] = "-20", + ["Scenarios:swarm-20:13:pos:1"] = "15", + ["Scenarios:swarm-20:13:pos:2"] = "60", + ["Scenarios:swarm-20:14:id"] = "drone-15", + ["Scenarios:swarm-20:14:pos:0"] = "20", + ["Scenarios:swarm-20:14:pos:1"] = "22", + ["Scenarios:swarm-20:14:pos:2"] = "60", + ["Scenarios:swarm-20:15:id"] = "drone-16", + ["Scenarios:swarm-20:15:pos:0"] = "60", + ["Scenarios:swarm-20:15:pos:1"] = "18", + ["Scenarios:swarm-20:15:pos:2"] = "60", + ["Scenarios:swarm-20:16:id"] = "drone-17", + ["Scenarios:swarm-20:16:pos:0"] = "0", + ["Scenarios:swarm-20:16:pos:1"] = "30", + ["Scenarios:swarm-20:16:pos:2"] = "0", + ["Scenarios:swarm-20:17:id"] = "drone-18", + ["Scenarios:swarm-20:17:pos:0"] = "-40", + ["Scenarios:swarm-20:17:pos:1"] = "28", + ["Scenarios:swarm-20:17:pos:2"] = "0", + ["Scenarios:swarm-20:18:id"] = "drone-19", + ["Scenarios:swarm-20:18:pos:0"] = "40", + ["Scenarios:swarm-20:18:pos:1"] = "28", + ["Scenarios:swarm-20:18:pos:2"] = "0", + ["Scenarios:swarm-20:19:id"] = "drone-20", + ["Scenarios:swarm-20:19:pos:0"] = "0", + ["Scenarios:swarm-20:19:pos:1"] = "25", + ["Scenarios:swarm-20:19:pos:2"] = "40", + + ["Scenarios:sar:0:id"] = "sar-lead", + ["Scenarios:sar:0:pos:0"] = "0", + ["Scenarios:sar:0:pos:1"] = "20", + ["Scenarios:sar:0:pos:2"] = "0", + ["Scenarios:sar:1:id"] = "sar-scout", + ["Scenarios:sar:1:pos:0"] = "30", + ["Scenarios:sar:1:pos:1"] = "25", + ["Scenarios:sar:1:pos:2"] = "30", + ["Scenarios:sar:2:id"] = "sar-relay", + ["Scenarios:sar:2:pos:0"] = "-30", + ["Scenarios:sar:2:pos:1"] = "18", + ["Scenarios:sar:2:pos:2"] = "-30", }) .Build(); return new ScenarioService(config); @@ -103,8 +159,8 @@ private static ScenarioService CreateScenarioService() private static SimulationService CreateSimulationService() { - var mockClients = new Mock(); - var mockProxy = new Mock(); + var mockClients = new Mock(); + var mockProxy = new Mock(); mockClients.Setup(c => c.All).Returns(mockProxy.Object); mockProxy.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); diff --git a/tests/ResQ.Viz.Web.Tests/SimControllerTests.cs b/tests/ResQ.Viz.Web.Tests/SimControllerTests.cs index 1ee2378..fae9b6d 100644 --- a/tests/ResQ.Viz.Web.Tests/SimControllerTests.cs +++ b/tests/ResQ.Viz.Web.Tests/SimControllerTests.cs @@ -35,7 +35,7 @@ public class SimControllerTests private static SimulationService CreateSimService() { var mockClients = new Mock(); - var mockProxy = new Mock(); + var mockProxy = new Mock(); mockClients.Setup(c => c.All).Returns(mockProxy.Object); mockProxy.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); @@ -49,29 +49,49 @@ private static ScenarioService CreateScenarioService() var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["Scenarios:single:0:id"] = "drone-1", - ["Scenarios:single:0:pos:0"] = "0", ["Scenarios:single:0:pos:1"] = "15", ["Scenarios:single:0:pos:2"] = "0", - - ["Scenarios:swarm-5:0:id"] = "drone-1", - ["Scenarios:swarm-5:0:pos:0"] = "-20", ["Scenarios:swarm-5:0:pos:1"] = "15", ["Scenarios:swarm-5:0:pos:2"] = "-20", - ["Scenarios:swarm-5:1:id"] = "drone-2", - ["Scenarios:swarm-5:1:pos:0"] = "20", ["Scenarios:swarm-5:1:pos:1"] = "18", ["Scenarios:swarm-5:1:pos:2"] = "-20", - ["Scenarios:swarm-5:2:id"] = "drone-3", - ["Scenarios:swarm-5:2:pos:0"] = "0", ["Scenarios:swarm-5:2:pos:1"] = "20", ["Scenarios:swarm-5:2:pos:2"] = "0", - ["Scenarios:swarm-5:3:id"] = "drone-4", - ["Scenarios:swarm-5:3:pos:0"] = "-20", ["Scenarios:swarm-5:3:pos:1"] = "18", ["Scenarios:swarm-5:3:pos:2"] = "20", - ["Scenarios:swarm-5:4:id"] = "drone-5", - ["Scenarios:swarm-5:4:pos:0"] = "20", ["Scenarios:swarm-5:4:pos:1"] = "15", ["Scenarios:swarm-5:4:pos:2"] = "20", - - ["Scenarios:swarm-20:0:id"] = "drone-1", - ["Scenarios:swarm-20:0:pos:0"] = "0", ["Scenarios:swarm-20:0:pos:1"] = "15", ["Scenarios:swarm-20:0:pos:2"] = "0", - - ["Scenarios:sar:0:id"] = "sar-lead", - ["Scenarios:sar:0:pos:0"] = "0", ["Scenarios:sar:0:pos:1"] = "20", ["Scenarios:sar:0:pos:2"] = "0", - ["Scenarios:sar:1:id"] = "sar-scout", - ["Scenarios:sar:1:pos:0"] = "30", ["Scenarios:sar:1:pos:1"] = "25", ["Scenarios:sar:1:pos:2"] = "30", - ["Scenarios:sar:2:id"] = "sar-relay", - ["Scenarios:sar:2:pos:0"] = "-30", ["Scenarios:sar:2:pos:1"] = "18", ["Scenarios:sar:2:pos:2"] = "-30", + ["Scenarios:single:0:id"] = "drone-1", + ["Scenarios:single:0:pos:0"] = "0", + ["Scenarios:single:0:pos:1"] = "15", + ["Scenarios:single:0:pos:2"] = "0", + + ["Scenarios:swarm-5:0:id"] = "drone-1", + ["Scenarios:swarm-5:0:pos:0"] = "-20", + ["Scenarios:swarm-5:0:pos:1"] = "15", + ["Scenarios:swarm-5:0:pos:2"] = "-20", + ["Scenarios:swarm-5:1:id"] = "drone-2", + ["Scenarios:swarm-5:1:pos:0"] = "20", + ["Scenarios:swarm-5:1:pos:1"] = "18", + ["Scenarios:swarm-5:1:pos:2"] = "-20", + ["Scenarios:swarm-5:2:id"] = "drone-3", + ["Scenarios:swarm-5:2:pos:0"] = "0", + ["Scenarios:swarm-5:2:pos:1"] = "20", + ["Scenarios:swarm-5:2:pos:2"] = "0", + ["Scenarios:swarm-5:3:id"] = "drone-4", + ["Scenarios:swarm-5:3:pos:0"] = "-20", + ["Scenarios:swarm-5:3:pos:1"] = "18", + ["Scenarios:swarm-5:3:pos:2"] = "20", + ["Scenarios:swarm-5:4:id"] = "drone-5", + ["Scenarios:swarm-5:4:pos:0"] = "20", + ["Scenarios:swarm-5:4:pos:1"] = "15", + ["Scenarios:swarm-5:4:pos:2"] = "20", + + ["Scenarios:swarm-20:0:id"] = "drone-1", + ["Scenarios:swarm-20:0:pos:0"] = "0", + ["Scenarios:swarm-20:0:pos:1"] = "15", + ["Scenarios:swarm-20:0:pos:2"] = "0", + + ["Scenarios:sar:0:id"] = "sar-lead", + ["Scenarios:sar:0:pos:0"] = "0", + ["Scenarios:sar:0:pos:1"] = "20", + ["Scenarios:sar:0:pos:2"] = "0", + ["Scenarios:sar:1:id"] = "sar-scout", + ["Scenarios:sar:1:pos:0"] = "30", + ["Scenarios:sar:1:pos:1"] = "25", + ["Scenarios:sar:1:pos:2"] = "30", + ["Scenarios:sar:2:id"] = "sar-relay", + ["Scenarios:sar:2:pos:0"] = "-30", + ["Scenarios:sar:2:pos:1"] = "18", + ["Scenarios:sar:2:pos:2"] = "-30", }) .Build(); return new ScenarioService(config); @@ -79,9 +99,9 @@ private static ScenarioService CreateScenarioService() private static (SimController ctrl, SimulationService sim) CreateController() { - var sim = CreateSimService(); + var sim = CreateSimService(); var scenarios = CreateScenarioService(); - var ctrl = new SimController(sim, scenarios, Mock.Of>()); + var ctrl = new SimController(sim, scenarios, Mock.Of>()); return (ctrl, sim); } diff --git a/tests/ResQ.Viz.Web.Tests/SimulationServiceTests.cs b/tests/ResQ.Viz.Web.Tests/SimulationServiceTests.cs index 7bc00e6..e61c0b1 100644 --- a/tests/ResQ.Viz.Web.Tests/SimulationServiceTests.cs +++ b/tests/ResQ.Viz.Web.Tests/SimulationServiceTests.cs @@ -166,7 +166,7 @@ public void Multiple_Drones_Snapshot_Has_Correct_Ids() { var svc = CreateService(); svc.AddDrone("alpha", new Vector3(0f, 50f, 0f)); - svc.AddDrone("beta", new Vector3(10f, 50f, 0f)); + svc.AddDrone("beta", new Vector3(10f, 50f, 0f)); svc.AddDrone("gamma", new Vector3(20f, 50f, 0f)); var ids = svc.GetSnapshot().Select(d => d.Id).ToList(); @@ -189,7 +189,7 @@ public void GetSnapshot_Rotation_Is_Unit_Quaternion() svc.AddDrone("d1", new Vector3(0f, 50f, 0f)); svc.StepOnce(); var rot = svc.GetSnapshot()[0].Rotation; - var mag = Math.Sqrt(rot[0]*rot[0] + rot[1]*rot[1] + rot[2]*rot[2] + rot[3]*rot[3]); + var mag = Math.Sqrt(rot[0] * rot[0] + rot[1] * rot[1] + rot[2] * rot[2] + rot[3] * rot[3]); mag.Should().BeApproximately(1.0, 0.001, "quaternion must be unit-length"); } } diff --git a/tests/ResQ.Viz.Web.Tests/SwarmControllerTests.cs b/tests/ResQ.Viz.Web.Tests/SwarmControllerTests.cs index 51dac60..4aa1c9b 100644 --- a/tests/ResQ.Viz.Web.Tests/SwarmControllerTests.cs +++ b/tests/ResQ.Viz.Web.Tests/SwarmControllerTests.cs @@ -44,7 +44,7 @@ public void Tick_WithZeroDrones_DoesNotThrow() public void SetScenario_AssignsRoutes_ForAllDrones() { var terrain = FlatTerrain(); - var ctrl = new SwarmController(terrain); + var ctrl = new SwarmController(terrain); var world = MakeWorld(terrain); world.AddDrone("d1", new Vector3(0, 30, 0)); @@ -60,8 +60,8 @@ public void SetScenario_AssignsRoutes_ForAllDrones() public void Tick_AppliesGoToCommand_OnFirstTick() { var terrain = FlatTerrain(); - var ctrl = new SwarmController(terrain); - var world = MakeWorld(terrain); + var ctrl = new SwarmController(terrain); + var world = MakeWorld(terrain); world.AddDrone("d1", new Vector3(0, 30, 0)); ctrl.SetScenario("swarm-5", world.Drones); @@ -76,8 +76,8 @@ public void Tick_AppliesGoToCommand_OnFirstTick() public void SetTerrainPreset_UpdatesMinAgl_AndDoesNotThrow() { var terrain = FlatTerrain(); - var ctrl = new SwarmController(terrain); - var world = MakeWorld(terrain); + var ctrl = new SwarmController(terrain); + var world = MakeWorld(terrain); world.AddDrone("d1", new Vector3(0, 30, 0)); ctrl.SetScenario("swarm-5", world.Drones); @@ -95,8 +95,8 @@ public void SetTerrainPreset_UpdatesMinAgl_AndDoesNotThrow() public void SetScenario_AllScenarios_BuildRoutesWithoutThrowing(string scenario) { var terrain = FlatTerrain(); - var ctrl = new SwarmController(terrain); - var world = MakeWorld(terrain); + var ctrl = new SwarmController(terrain); + var world = MakeWorld(terrain); for (int i = 0; i < 4; i++) world.AddDrone($"d{i}", new Vector3(i * 30, 30, 0));