using System.Text.Json.Serialization; namespace Flyshot.Core.Domain; /// /// Represents the stable planning result returned to orchestration, SDKs, and monitoring layers. /// public sealed class TrajectoryResult { /// /// Initializes a validated trajectory result. /// public TrajectoryResult( string programName, PlanningMethod method, bool isValid, TimeSpan duration, IEnumerable shotEvents, IEnumerable triggerTimeline, IEnumerable artifacts, string? failureReason, bool usedCache, int originalWaypointCount, int plannedWaypointCount, IEnumerable>? denseJointTrajectory = null) { if (string.IsNullOrWhiteSpace(programName)) { throw new ArgumentException("Program name is required.", nameof(programName)); } if (duration < TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be zero or positive."); } if (originalWaypointCount < 0) { throw new ArgumentOutOfRangeException(nameof(originalWaypointCount), "Original waypoint count must be zero or positive."); } if (plannedWaypointCount < originalWaypointCount) { throw new ArgumentOutOfRangeException(nameof(plannedWaypointCount), "Planned waypoint count must be greater than or equal to the original waypoint count."); } ArgumentNullException.ThrowIfNull(shotEvents); ArgumentNullException.ThrowIfNull(triggerTimeline); ArgumentNullException.ThrowIfNull(artifacts); // Materialize once so the result remains stable after the planner hands it off. var copiedShotEvents = shotEvents.ToArray(); var copiedTriggerTimeline = triggerTimeline.ToArray(); var copiedArtifacts = artifacts.ToArray(); var copiedDenseJointTrajectory = denseJointTrajectory?.Select(static row => row.ToArray()).ToArray(); ProgramName = programName; Method = method; IsValid = isValid; Duration = duration; ShotEvents = copiedShotEvents; TriggerTimeline = copiedTriggerTimeline; Artifacts = copiedArtifacts; FailureReason = failureReason; UsedCache = usedCache; OriginalWaypointCount = originalWaypointCount; PlannedWaypointCount = plannedWaypointCount; DenseJointTrajectory = copiedDenseJointTrajectory; } /// /// Gets the source program name. /// [JsonPropertyName("programName")] public string ProgramName { get; } /// /// Gets the method that produced the result. /// [JsonPropertyName("method")] public PlanningMethod Method { get; } /// /// Gets a value indicating whether the result can be executed. /// [JsonPropertyName("isValid")] public bool IsValid { get; } /// /// Gets the final trajectory duration. /// [JsonPropertyName("duration")] public TimeSpan Duration { get; } /// /// Gets the sampled shot events exported for monitoring and reports. /// [JsonPropertyName("shotEvents")] public IReadOnlyList ShotEvents { get; } /// /// Gets the trigger timeline that the runtime will inject into servo execution. /// [JsonPropertyName("triggerTimeline")] public IReadOnlyList TriggerTimeline { get; } /// /// Gets the exported trajectory artifacts associated with the result. /// [JsonPropertyName("artifacts")] public IReadOnlyList Artifacts { get; } /// /// Gets the failure reason when the result is invalid. /// [JsonPropertyName("failureReason")] public string? FailureReason { get; } /// /// Gets a value indicating whether the result reused cached planning data. /// [JsonPropertyName("usedCache")] public bool UsedCache { get; } /// /// Gets the teach waypoint count before adaptive point insertion. /// [JsonPropertyName("originalWaypointCount")] public int OriginalWaypointCount { get; } /// /// Gets the final waypoint count after planner preprocessing. /// [JsonPropertyName("plannedWaypointCount")] public int PlannedWaypointCount { get; } /// /// Gets the dense joint trajectory samples where each row is [time, j1, j2, ...]. /// Null when dense sampling was not performed (e.g. simulation fallback). /// [JsonPropertyName("denseJointTrajectory")] public IReadOnlyList>? DenseJointTrajectory { get; } } /// /// Describes a trigger event that the runtime must inject into the servo timeline. /// public sealed class TrajectoryDoEvent { /// /// Initializes a validated runtime trigger event. /// public TrajectoryDoEvent(int waypointIndex, double triggerTime, int offsetCycles, int holdCycles, IoAddressGroup addressGroup) { if (waypointIndex < 0) { throw new ArgumentOutOfRangeException(nameof(waypointIndex), "Waypoint index must be zero or positive."); } if (triggerTime < 0.0) { throw new ArgumentOutOfRangeException(nameof(triggerTime), "Trigger time must be zero or positive."); } if (holdCycles < 0) { throw new ArgumentOutOfRangeException(nameof(holdCycles), "Hold cycles must be zero or positive."); } WaypointIndex = waypointIndex; TriggerTime = triggerTime; OffsetCycles = offsetCycles; HoldCycles = holdCycles; AddressGroup = addressGroup ?? throw new ArgumentNullException(nameof(addressGroup)); } /// /// Gets the original teach-waypoint index that requested the event. /// [JsonPropertyName("waypointIndex")] public int WaypointIndex { get; } /// /// Gets the theoretical trigger time before discretization. /// [JsonPropertyName("triggerTime")] public double TriggerTime { get; } /// /// Gets the configured offset in servo cycles. /// [JsonPropertyName("offsetCycles")] public int OffsetCycles { get; } /// /// Gets the configured hold duration in servo cycles. /// [JsonPropertyName("holdCycles")] public int HoldCycles { get; } /// /// Gets the IO address group to fire. /// [JsonPropertyName("addressGroup")] public IoAddressGroup AddressGroup { get; } } /// /// Describes an exported artifact generated from a planned trajectory. /// public sealed class TrajectoryArtifact { /// /// Initializes a validated trajectory artifact descriptor. /// public TrajectoryArtifact(TrajectoryArtifactKind kind, string logicalName, string relativePath) { if (string.IsNullOrWhiteSpace(logicalName)) { throw new ArgumentException("Logical artifact name is required.", nameof(logicalName)); } if (string.IsNullOrWhiteSpace(relativePath)) { throw new ArgumentException("Relative artifact path is required.", nameof(relativePath)); } Kind = kind; LogicalName = logicalName; RelativePath = relativePath; } /// /// Gets the exported artifact kind. /// [JsonPropertyName("kind")] public TrajectoryArtifactKind Kind { get; } /// /// Gets the logical artifact file name. /// [JsonPropertyName("logicalName")] public string LogicalName { get; } /// /// Gets the artifact path relative to the service output root. /// [JsonPropertyName("relativePath")] public string RelativePath { get; } } /// /// Identifies the exported trajectory artifact category. /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum TrajectoryArtifactKind { /// /// Represents sparse joint teach points. /// JointTeachTrajectory, /// /// Represents sparse Cartesian teach points. /// CartesianTeachTrajectory, /// /// Represents dense joint trajectory samples. /// JointDenseTrajectory, /// /// Represents dense Cartesian trajectory samples. /// CartesianDenseTrajectory, /// /// Represents the exported shot-event mapping. /// ShotEventTimeline }