using System; using System.Collections.Generic; using System.Linq; namespace OpenNest.Diagnostics; public enum OverlapDisplayMode { Off, Areas, Centroids, Both } public enum OverlapCheckStatus { NotChecked, Checking, Current, Incomplete, Failed, Canceled, Stale } /// UI-thread lifecycle policy, independent of workers, GDI and view transforms. public sealed class OverlapReportState { private OverlapGeometryStamp stamp; private OverlapGeometryStamp displayStamp; private OverlapGeometryStamp observedDisplayStamp; private int uncheckedPartCount; public long Generation { get; private set; } public OverlapCheckStatus Status { get; private set; } = OverlapCheckStatus.NotChecked; public OverlapDisplayMode DisplayMode { get; set; } = OverlapDisplayMode.Areas; public PlateOverlapReport Report { get; private set; } /// Known overlap pairs safe to draw, even while the full layout needs a recheck. public IReadOnlyList DisplayPairs { get; private set; } = Array.Empty(); public IReadOnlyList DisplayCutOffClampFindings { get; private set; } = Array.Empty(); /// Known part-spacing pairs safe to draw, retained like . public IReadOnlyList DisplaySpacingPairs { get; private set; } = Array.Empty(); public bool IsRunning => Status == OverlapCheckStatus.Checking; /// A current, fully checked report with no finding of any kind. public bool IsAllClear => Status == OverlapCheckStatus.Current && Report.Pairs.Count == 0 && Report.ClampFindings.Count == 0 && Report.CutOffClampFindings.Count == 0 && Report.SpacingPairs.Count == 0; /// /// The last published result's message, kept through layout edits and rechecks so the canvas /// stays quiet until the next result; never a claim about the current layout. Null when none. /// public string SettledMessage { get; private set; } /// Whether that last published result was all clear. public bool SettledClear { get; private set; } public string Message => Status switch { OverlapCheckStatus.Checking => "Checking overlaps…", OverlapCheckStatus.Current => CurrentMessage(), OverlapCheckStatus.Incomplete => $"Overlap check incomplete: {Report.Pairs.Count} overlapping pairs; " + (Report.ClampFindings.Count == 0 ? "" : $"{Report.ClampFindings.Count} clamp findings; ") + (Report.CutOffClampFindings.Count == 0 ? "" : $"{Report.CutOffClampFindings.Count} cut-off/clamp contacts; ") + (Report.SpacingPairs.Count == 0 ? "" : $"{Report.SpacingPairs.Count} pairs under part spacing; ") + $"{uncheckedPartCount} parts could not be checked", OverlapCheckStatus.Failed => "Overlap check failed — run Check Overlaps again", OverlapCheckStatus.Canceled => "Overlap check canceled", OverlapCheckStatus.Stale => "Overlap check out of date — run Check Overlaps again", _ => "Overlaps: not checked" }; /// A few steady words for the status bar; has the detail. public string Summary => Status switch { OverlapCheckStatus.Checking => "Layout: checking…", OverlapCheckStatus.Current => IsAllClear ? "Layout: OK" : "Layout: " + Findings(), OverlapCheckStatus.Incomplete => "Layout: check incomplete", OverlapCheckStatus.Failed => "Layout: check failed", OverlapCheckStatus.Canceled => "Layout: check canceled", OverlapCheckStatus.Stale => "Layout: out of date", _ => "Layout: not checked" }; /// True when reports findings or a check that did not finish. public bool NeedsAttention => Status is OverlapCheckStatus.Incomplete or OverlapCheckStatus.Failed || Status == OverlapCheckStatus.Current && !IsAllClear; private string Findings() { var overlaps = Report.Pairs.Count + Report.ClampFindings.Count + Report.CutOffClampFindings.Count; var spacing = Report.SpacingPairs.Count; var words = new List(); if (overlaps != 0) words.Add(overlaps == 1 ? "1 overlap" : $"{overlaps} overlaps"); if (spacing != 0) words.Add($"{spacing} too close"); return string.Join(", ", words); } // Only a fully checked report that required a gap may clear spacing as well as overlaps. private string CurrentMessage() { var spacing = Report.SpacingPairs.Count == 0 ? "" : $"; part spacing: {Report.SpacingPairs.Count} pairs too close"; if (Report.ClampFindings.Count != 0 || Report.CutOffClampFindings.Count != 0) return $"Overlaps: {Report.Pairs.Count} pairs, {Report.ClampFindings.Count} clamps, " + $"{Report.CutOffClampFindings.Count} cut-off/clamp contacts{spacing}"; if (Report.Pairs.Count != 0) return $"Overlaps: {Report.Pairs.Count} pairs{spacing}"; if (spacing.Length != 0) return "No material overlaps" + spacing; return Report.SpacingChecked ? "No material overlaps or part spacing violations detected" : "No material overlaps detected"; } /// /// Starts a request. A manual check from Off shows Areas; an automatic recheck keeps /// the user's display choice, including Off. /// public long Begin(Plate plate, bool automatic = false) { RefreshDisplayPairs(plate); Clear(OverlapCheckStatus.Checking, preserveDisplay: true); stamp = OverlapGeometryStamp.Capture(plate); if (!automatic && DisplayMode == OverlapDisplayMode.Off) DisplayMode = OverlapDisplayMode.Areas; return Generation; } public bool TryPublish(long generation, Plate plate, PlateOverlapReport report) { if (!CanComplete(generation, plate)) return false; Report = report; DisplayPairs = report.ClampFindings.Count == 0 ? report.Pairs : report.Pairs.Concat(report.ClampFindings).ToList().AsReadOnly(); DisplayCutOffClampFindings = report.CutOffClampFindings; DisplaySpacingPairs = report.SpacingPairs; displayStamp = stamp; observedDisplayStamp = stamp; uncheckedPartCount = CountUncheckedParts(report.Issues.Concat(report.SpacingIssues)); Status = report.IsComplete ? OverlapCheckStatus.Current : OverlapCheckStatus.Incomplete; SettledMessage = Message; SettledClear = IsAllClear; return true; } public bool TryFail(long generation, Plate plate) { if (!CanComplete(generation, plate)) return false; Clear(OverlapCheckStatus.Failed); return true; } private bool CanComplete(long generation, Plate plate) => generation == Generation && IsRunning && EnsureFresh(plate); public bool EnsureFresh(Plate plate) { RefreshDisplayPairs(plate); if (stamp == null) return false; if (stamp.Matches(plate)) return true; Invalidate(plate); return false; } /// Layout edit: retain only pairs whose two ordered slots still match exactly. public void Invalidate(Plate plate) { RefreshDisplayPairs(plate); if (Status is OverlapCheckStatus.Checking or OverlapCheckStatus.Current or OverlapCheckStatus.Incomplete) Clear(OverlapCheckStatus.Stale, preserveDisplay: true); } /// In-place geometry edits and teardown must forget every cached display pair. public void Invalidate() { ClearDisplayPairs(); if (Status is OverlapCheckStatus.Checking or OverlapCheckStatus.Current or OverlapCheckStatus.Incomplete) Clear(OverlapCheckStatus.Stale); } private void RefreshDisplayPairs(Plate plate) { if (displayStamp == null || observedDisplayStamp?.Matches(plate) == true) return; var unchanged = displayStamp.UnchangedSlots(plate); var sameZones = displayStamp.SameZones(plate); if (!sameZones || !displayStamp.SameCutOffs(plate)) DisplayCutOffClampFindings = Array.Empty(); var retained = DisplayPairs.Where(pair => unchanged[pair.PartAId] && (pair.PartBId < 0 ? sameZones : unchanged[pair.PartBId])).ToList(); if (retained.Count != DisplayPairs.Count) DisplayPairs = retained.AsReadOnly(); var sameSpacing = displayStamp.SameSpacing(plate); var retainedSpacing = DisplaySpacingPairs.Where(pair => sameSpacing && unchanged[pair.PartAId] && unchanged[pair.PartBId]).ToList(); if (retainedSpacing.Count != DisplaySpacingPairs.Count) DisplaySpacingPairs = retainedSpacing.AsReadOnly(); if (DisplayPairs.Count == 0 && DisplayCutOffClampFindings.Count == 0 && DisplaySpacingPairs.Count == 0) ClearDisplayPairs(); else observedDisplayStamp = OverlapGeometryStamp.Capture(plate); } private void ClearDisplayPairs() { DisplayPairs = Array.Empty(); DisplayCutOffClampFindings = Array.Empty(); DisplaySpacingPairs = Array.Empty(); displayStamp = null; observedDisplayStamp = null; } public void Cancel() { if (IsRunning) Clear(OverlapCheckStatus.Canceled); } public void Reset() => Clear(OverlapCheckStatus.NotChecked); private void Clear(OverlapCheckStatus status, bool preserveDisplay = false) { if (!preserveDisplay) { ClearDisplayPairs(); SettledMessage = null; SettledClear = false; } Generation++; Report = null; uncheckedPartCount = 0; stamp = null; Status = status; } public static int CountUncheckedParts(IEnumerable issues) { var ids = new HashSet(); foreach (var issue in issues) { ids.Add(issue.PartAId); if (issue.PartBId.HasValue) ids.Add(issue.PartBId.Value); } return ids.Count; } }