using System.Text.Json.Serialization; namespace Flyshot.Core.Domain; /// /// Represents an uploaded flyshot program built from teach points and shot metadata. /// public sealed class FlyshotProgram { /// /// Initializes a validated flyshot program. /// public FlyshotProgram( string name, IEnumerable waypoints, IEnumerable shotFlags, IEnumerable offsetValues, IEnumerable addressGroups) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("Program name is required.", nameof(name)); } ArgumentNullException.ThrowIfNull(waypoints); ArgumentNullException.ThrowIfNull(shotFlags); ArgumentNullException.ThrowIfNull(offsetValues); ArgumentNullException.ThrowIfNull(addressGroups); // Freeze the uploaded data so later orchestration cannot silently drift from the request payload. var copiedWaypoints = waypoints.ToArray(); var copiedShotFlags = shotFlags.ToArray(); var copiedOffsetValues = offsetValues.ToArray(); var copiedAddressGroups = addressGroups.ToArray(); if (copiedWaypoints.Length == 0) { throw new ArgumentException("At least one waypoint is required.", nameof(waypoints)); } if (copiedShotFlags.Length != copiedWaypoints.Length) { throw new ArgumentException("Shot flag count must match waypoint count.", nameof(shotFlags)); } if (copiedOffsetValues.Length != copiedWaypoints.Length) { throw new ArgumentException("Offset value count must match waypoint count.", nameof(offsetValues)); } if (copiedAddressGroups.Length != copiedWaypoints.Length) { throw new ArgumentException("Address group count must match waypoint count.", nameof(addressGroups)); } var jointDimension = copiedWaypoints[0].Positions.Count; if (copiedWaypoints.Any(waypoint => waypoint.Positions.Count != jointDimension)) { throw new ArgumentException("All waypoints must share the same joint dimension.", nameof(waypoints)); } Name = name; Waypoints = copiedWaypoints; ShotFlags = copiedShotFlags; OffsetValues = copiedOffsetValues; AddressGroups = copiedAddressGroups; JointDimension = jointDimension; ShotWaypointCount = copiedShotFlags.Count(flag => flag); } /// /// Gets the program name used by cache and gateway lookups. /// [JsonPropertyName("name")] public string Name { get; } /// /// Gets the immutable teach waypoint list. /// [JsonPropertyName("waypoints")] public IReadOnlyList Waypoints { get; } /// /// Gets the per-waypoint shot trigger flags. /// [JsonPropertyName("shotFlags")] public IReadOnlyList ShotFlags { get; } /// /// Gets the per-waypoint trigger offset values in servo cycles. /// [JsonPropertyName("offsetValues")] public IReadOnlyList OffsetValues { get; } /// /// Gets the per-waypoint IO address groups. /// [JsonPropertyName("addressGroups")] public IReadOnlyList AddressGroups { get; } /// /// Gets the joint dimension shared by all waypoints. /// [JsonPropertyName("jointDimension")] public int JointDimension { get; } /// /// Gets the number of waypoints that request a shot trigger. /// [JsonPropertyName("shotWaypointCount")] public int ShotWaypointCount { get; } } /// /// Represents a single teach waypoint in joint space. /// public sealed class JointWaypoint { /// /// Initializes a validated joint waypoint. /// public JointWaypoint(IEnumerable positions) { ArgumentNullException.ThrowIfNull(positions); // Copy the input once so the waypoint remains immutable after upload. var copiedPositions = positions.ToArray(); if (copiedPositions.Length == 0) { throw new ArgumentException("Joint waypoint must contain at least one joint value.", nameof(positions)); } Positions = copiedPositions; } /// /// Gets the immutable joint value vector. /// [JsonPropertyName("positions")] public IReadOnlyList Positions { get; } } /// /// Represents the list of IO addresses that should fire at one logical trigger point. /// public sealed class IoAddressGroup { /// /// Initializes a validated IO address group. /// public IoAddressGroup(IEnumerable addresses) { ArgumentNullException.ThrowIfNull(addresses); // Preserve address order because some upper-layer tooling expects exported order stability. var copiedAddresses = addresses.ToArray(); if (copiedAddresses.Length > 8) { throw new ArgumentException("A single trigger point can contain at most eight IO addresses.", nameof(addresses)); } if (copiedAddresses.Any(address => address < 0)) { throw new ArgumentException("IO addresses must be zero or positive.", nameof(addresses)); } Addresses = copiedAddresses; } /// /// Gets the immutable ordered IO address list. /// [JsonPropertyName("addresses")] public IReadOnlyList Addresses { get; } }