Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/ResQ.Viz.Web/Models/VizFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,21 @@ public record VizFrame(
MeshVizState? Mesh);

/// <summary>Per-drone visual state in a VizFrame.</summary>
/// <remarks>
/// <paramref name="Vendor"/> tags the drone with an integrating agency's
/// equipment maker (e.g. <c>skydio</c>, <c>autel</c>, <c>anzu</c>) so the
/// client can render vendor-specific chassis tints in multi-agency scenarios.
/// Null for scenarios that don't need vendor differentiation.
/// </remarks>
public record DroneVizState(
string Id,
float[] Pos,
float[] Rot,
float[] Vel,
double Battery,
string Status,
bool Armed);
bool Armed,
string? Vendor = null);

/// <summary>A hazard zone (fire, flood, etc.).</summary>
public record HazardVizState(
Expand Down
16 changes: 10 additions & 6 deletions src/ResQ.Viz.Web/Services/ScenarioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,29 @@ namespace ResQ.Viz.Web.Services;
/// </summary>
public sealed class ScenarioService
{
private readonly IReadOnlyDictionary<string, IReadOnlyList<(string Id, Vector3 Pos)>> _scenarios;
/// <summary>Per-drone scenario entry: launch position and optional vendor tag.</summary>
public readonly record struct Entry(string Id, Vector3 Pos, string? Vendor);

private readonly IReadOnlyDictionary<string, IReadOnlyList<Entry>> _scenarios;

/// <summary>
/// Initialises the service and loads scenario presets from <paramref name="configuration"/>.
/// </summary>
/// <param name="configuration">Application configuration containing the <c>Scenarios</c> section.</param>
public ScenarioService(IConfiguration configuration)
{
var dict = new Dictionary<string, IReadOnlyList<(string Id, Vector3 Pos)>>(StringComparer.OrdinalIgnoreCase);
var dict = new Dictionary<string, IReadOnlyList<Entry>>(StringComparer.OrdinalIgnoreCase);
var section = configuration.GetSection("Scenarios");
foreach (var child in section.GetChildren())
{
var entries = new List<(string Id, Vector3 Pos)>();
var entries = new List<Entry>();
foreach (var entry in child.GetChildren())
{
var id = entry["id"] ?? string.Empty;
var pos = entry.GetSection("pos").Get<float[]>() ?? Array.Empty<float>();
var vendor = entry["vendor"];
if (!string.IsNullOrEmpty(id) && pos.Length == 3)
entries.Add((id, new Vector3(pos[0], pos[1], pos[2])));
entries.Add(new Entry(id, new Vector3(pos[0], pos[1], pos[2]), string.IsNullOrWhiteSpace(vendor) ? null : vendor));
}
if (entries.Count > 0)
dict[child.Key] = entries;
Expand All @@ -68,8 +72,8 @@ public bool TryRun(string name, SimulationService sim)
if (!_scenarios.TryGetValue(name, out var drones))
return false;

foreach (var (id, pos) in drones)
sim.AddDrone(id, pos);
foreach (var entry in drones)
sim.AddDrone(entry.Id, entry.Pos, entry.Vendor);

return true;
}
Expand Down
26 changes: 22 additions & 4 deletions src/ResQ.Viz.Web/Services/SimulationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public record DroneSnapshot(
float[] Velocity,
double Battery,
string Status,
bool Armed);
bool Armed,
string? Vendor = null);

/// <summary>
/// Background service that owns the <see cref="SimulationWorld"/> and ticks it at ~60 Hz.
Expand All @@ -62,6 +63,9 @@ public sealed class SimulationService : BackgroundService
private int _swarmTick;
private double _simTime;

// Per-drone metadata that isn't in the SDK's SimulatedDrone. Keyed by drone id.
private readonly Dictionary<string, string> _droneVendors = new(StringComparer.Ordinal);
Comment thread
WomB0ComB0 marked this conversation as resolved.

/// <summary>Broadcast a viz frame every N simulation ticks (60 Hz / 6 = 10 Hz).</summary>
private const int BroadcastEveryNTicks = 6;

Expand Down Expand Up @@ -89,12 +93,24 @@ public SimulationService(IHubContext<VizHub> hubContext, VizFrameBuilder frameBu
/// <summary>Adds a drone to the simulation world at the specified start position.</summary>
/// <param name="id">Unique drone identifier.</param>
/// <param name="position">World-space launch position.</param>
public void AddDrone(string id, Vector3 position)
public void AddDrone(string id, Vector3 position) => AddDrone(id, position, vendor: null);

/// <summary>
/// Adds a drone to the simulation world with an optional vendor tag.
/// </summary>
/// <param name="id">Unique drone identifier.</param>
/// <param name="position">World-space launch position.</param>
/// <param name="vendor">Optional integrating-agency vendor tag (e.g. <c>skydio</c>).</param>
public void AddDrone(string id, Vector3 position, string? vendor)
{
lock (_lock)
{
_world.AddDrone(id, position);
_logger.LogInformation("Drone {DroneId} added at ({X}, {Y}, {Z}).", id, position.X, position.Y, position.Z);
if (!string.IsNullOrEmpty(vendor))
{
_droneVendors[id] = vendor;
}
_logger.LogInformation("Drone {DroneId} added at ({X}, {Y}, {Z}) vendor={Vendor}.", id, position.X, position.Y, position.Z, vendor ?? "none");
}
}

Expand Down Expand Up @@ -165,6 +181,7 @@ public void Reset()
_simTime = 0;
_tickCount = 0;
_swarmTick = 0;
_droneVendors.Clear();
_logger.LogInformation("Simulation reset.");
}
}
Expand All @@ -187,7 +204,8 @@ public IReadOnlyList<DroneSnapshot> GetSnapshot()
Velocity: [state.Velocity.X, state.Velocity.Y, state.Velocity.Z],
Battery: state.BatteryPercent,
Status: d.FlightModel.HasLanded ? "landed" : "flying",
Armed: !d.FlightModel.HasLanded);
Armed: !d.FlightModel.HasLanded,
Vendor: _droneVendors.TryGetValue(d.Id, out var v) ? v : null);
}).ToList();
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/ResQ.Viz.Web/Services/VizFrameBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public VizFrameBuilder()
public VizFrame Build(IReadOnlyList<DroneSnapshot> drones, double simTime)
{
var droneStates = drones
.Select(d => new DroneVizState(d.Id, d.Position, d.Rotation, d.Velocity, d.Battery, d.Status, d.Armed))
.Select(d => new DroneVizState(d.Id, d.Position, d.Rotation, d.Velocity, d.Battery, d.Status, d.Armed, d.Vendor))
.ToList();

return new VizFrame(
Expand Down
14 changes: 14 additions & 0 deletions src/ResQ.Viz.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@
{ "id": "sar-lead", "pos": [ 0.0, 20.0, 0.0] },
{ "id": "sar-scout", "pos": [ 30.0, 25.0, 30.0] },
{ "id": "sar-relay", "pos": [-30.0, 18.0, -30.0] }
],
"multi-agency-sar": [
{ "id": "skydio-1", "pos": [-80.0, 20.0, -60.0], "vendor": "skydio" },
{ "id": "skydio-2", "pos": [-60.0, 22.0, -40.0], "vendor": "skydio" },
{ "id": "skydio-3", "pos": [-70.0, 18.0, -20.0], "vendor": "skydio" },
{ "id": "skydio-4", "pos": [-90.0, 24.0, -40.0], "vendor": "skydio" },
{ "id": "autel-1", "pos": [ 70.0, 20.0, -60.0], "vendor": "autel" },
{ "id": "autel-2", "pos": [ 90.0, 22.0, -40.0], "vendor": "autel" },
{ "id": "autel-3", "pos": [ 80.0, 18.0, -20.0], "vendor": "autel" },
{ "id": "autel-4", "pos": [ 60.0, 24.0, -40.0], "vendor": "autel" },
{ "id": "anzu-1", "pos": [-10.0, 28.0, 40.0], "vendor": "anzu" },
{ "id": "anzu-2", "pos": [ 10.0, 26.0, 60.0], "vendor": "anzu" },
{ "id": "anzu-3", "pos": [ 30.0, 24.0, 40.0], "vendor": "anzu" },
{ "id": "anzu-4", "pos": [-30.0, 30.0, 50.0], "vendor": "anzu" }
]
}
}
18 changes: 15 additions & 3 deletions src/ResQ.Viz.Web/client/drones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ function lerpAlpha(dt: number): number {
const BODY_COLOR = 0x161b22;
const ARM_COLOR = 0x21262d;

/**
* Chassis top-plate tint per integrating-agency vendor. Subtle — keeps the
* silhouette consistent while giving a visible agency signature in
* multi-agency scenarios. Unmapped/absent vendor falls back to BODY_COLOR.
*/
const VENDOR_COLORS: Record<string, number> = {
skydio: 0x2b3a55, // cool steel-blue
autel: 0x5a2a30, // deep oxblood
anzu: 0x2a4a36, // dark forest
};

/** Detection range in world metres — matches appsettings DetectionRangeMeters. */
const DETECTION_RANGE_M = 35;

Expand Down Expand Up @@ -222,7 +233,8 @@ export class DroneManager {

private _add(d: DroneState): void {
const color = STATUS_COLORS[d.status ?? ''] ?? DEFAULT_COLOR;
const { group, led, ring, rotors, label } = this._buildQuadrotor(color, d.id);
const bodyColor = d.vendor ? (VENDOR_COLORS[d.vendor] ?? BODY_COLOR) : BODY_COLOR;
const { group, led, ring, rotors, label } = this._buildQuadrotor(color, d.id, bodyColor);

const startPos = new THREE.Vector3(d.pos[0], d.pos[1], d.pos[2]);
group.position.copy(startPos);
Expand Down Expand Up @@ -258,13 +270,13 @@ export class DroneManager {
this._drones.set(d.id, entry);
}

private _buildQuadrotor(statusColor: number, droneId: string): QuadrotorMesh {
private _buildQuadrotor(statusColor: number, droneId: string, bodyColor: number = BODY_COLOR): QuadrotorMesh {
const group = new THREE.Group();

// ── Central body ──────────────────────────────────────────────────────
const topPlate = new THREE.Mesh(
new THREE.BoxGeometry(3.8, 0.35, 3.8),
new THREE.MeshStandardMaterial({ color: BODY_COLOR, metalness: 0.1, roughness: 0.75 }),
new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.1, roughness: 0.75 }),
);
topPlate.position.y = 0.3;
topPlate.castShadow = true;
Expand Down
6 changes: 6 additions & 0 deletions src/ResQ.Viz.Web/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export interface DroneState {
status?: string;
battery?: number;
armed?: boolean;
/**
* Optional vendor tag identifying the integrating agency's equipment maker
* (e.g. "skydio", "autel", "anzu"). Used for chassis-tint differentiation
* in multi-agency scenarios.
*/
vendor?: string;
}

export function isDroneReady(d: DroneState | undefined): d is DroneState & { pos: [number,number,number]; rot: [number,number,number,number]; vel: [number,number,number] } {
Expand Down
Loading