Files
OpenNest/OpenNest.Core/Diagnostics/OverlapPairPresentation.cs
T
ajandClaude Opus 5.5 4aa035af2f feat(diagnostics): mark plate-edge findings on the plate and say "outside the work area"
The Layout label counted parts off the plate or inside the edge spacing, but
nothing showed which part or where, so the user had to hunt for it. Each edge
finding now gets a canvas marker like the overlap and spacing ones: a red
crosshair where a part leaves the sheet and an orange ring at the gap to the
edge, in every visible display mode, with hover text that carries the finding's
message. Markers are retained across edits like the other overlay findings
(only a changed part's marker drops), and a sheet size, edge spacing or outline
change clears them. Validate Nest already located these findings through
PostVerificationFinding.Location.

"Near edge" / "inside the edge spacing" read as a spacing rule rather than a
placement problem, so the label, overlay messages, PlateEdgeReview findings
and the Validate Nest category now say the part is outside the work area. Off
the plate keeps its wording. The PostVerificationKind.EdgeSpacing name stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:28:52 -05:00

89 lines
4.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using OpenNest.Geometry;
namespace OpenNest.Diagnostics;
/// <summary>Read-only pair presentation; no geometry preparation or collision queries.</summary>
public static class OverlapPairPresentation
{
/// <summary>True for part-vs-clamp findings carried on the report (negative clamp slot).</summary>
public static bool IsClampFinding(PlateOverlapPair pair) => pair.PartBId < 0;
public static string Label(PlateOverlapPair pair) =>
IsClampFinding(pair) ? $"{pair.PartAId + 1}/clamp" : $"{pair.PartAId + 1}/{pair.PartBId + 1}";
public static string Details(PlateOverlapPair pair, Units capturedUnits, IFormatProvider provider = null)
{
var units = UnitsHelper.GetShortString(capturedUnits);
var head = IsClampFinding(pair)
? $"Clamp overlap: {pair.PartAName} / {pair.PartBName}"
: $"Pair {Label(pair)}: {pair.PartAName} / {pair.PartBName}";
return head + "\n"
+ $"Shared area ≈ {Number(pair.Area, provider)} {units}²\n"
+ $"Marker: ({Number(pair.Marker.X, provider)}, {Number(pair.Marker.Y, provider)}) {units}";
}
public static string Label(PlateSpacingPair pair, IFormatProvider provider = null) =>
$"{pair.PartAId + 1}/{pair.PartBId + 1} gap {Number(pair.Gap, provider)}";
public static string Details(PlateSpacingPair pair, Units capturedUnits, IFormatProvider provider = null)
{
var units = UnitsHelper.GetShortString(capturedUnits);
return $"Part spacing {pair.PartAId + 1}/{pair.PartBId + 1}: {pair.PartAName} / {pair.PartBName}\n"
+ $"Gap ≈ {Number(pair.Gap, provider)} {units}\n"
+ $"Marker: ({Number(pair.Marker.X, provider)}, {Number(pair.Marker.Y, provider)}) {units}";
}
/// <summary>A short canvas label: the part's sequence number and what is wrong.</summary>
public static string Label(PlateEdgeFinding finding) =>
$"{finding.PartId + 1} " + (finding.OffPlate ? "off plate" : "outside work area");
public static string Details(PlateEdgeFinding finding, Units capturedUnits, IFormatProvider provider = null)
{
var units = UnitsHelper.GetShortString(capturedUnits);
return (finding.OffPlate ? "Off the plate" : "Outside work area") + $": part {finding.PartId + 1}\n"
+ finding.Message + "\n"
+ $"Marker: ({Number(finding.Marker.X, provider)}, {Number(finding.Marker.Y, provider)}) {units}";
}
// Significant figures adapt to scale: a positive sliver must never read as zero.
public static string Number(double value, IFormatProvider provider = null) =>
value.ToString("G6", provider ?? CultureInfo.CurrentCulture);
public static double MarkerHalfSize(int deviceDpi) => 6.0 * deviceDpi / 96;
public static double HitRadius(int deviceDpi) => 10.0 * deviceDpi / 96;
/// <summary>
/// Projects cached markers into a screen-pixel coordinate space. The pointer must
/// use that same space; view zoom never scales the DPI-adjusted hit radius.
/// Coincident or nearby markers return every matching pair in stable ID order.
/// </summary>
public static IReadOnlyList<PlateOverlapPair> HitTest(IEnumerable<PlateOverlapPair> pairs,
Func<Vector, Vector> worldToScreen, Vector pointer, int deviceDpi) =>
pairs.Where(pair => Hits(worldToScreen(pair.Marker), pointer, deviceDpi))
.OrderBy(pair => pair.PartAId).ThenBy(pair => pair.PartBId).ToArray();
/// <inheritdoc cref="HitTest(IEnumerable{PlateOverlapPair}, Func{Vector, Vector}, Vector, int)"/>
public static IReadOnlyList<PlateSpacingPair> HitTest(IEnumerable<PlateSpacingPair> pairs,
Func<Vector, Vector> worldToScreen, Vector pointer, int deviceDpi) =>
pairs.Where(pair => Hits(worldToScreen(pair.Marker), pointer, deviceDpi))
.OrderBy(pair => pair.PartAId).ThenBy(pair => pair.PartBId).ToArray();
/// <inheritdoc cref="HitTest(IEnumerable{PlateOverlapPair}, Func{Vector, Vector}, Vector, int)"/>
public static IReadOnlyList<PlateEdgeFinding> HitTest(IEnumerable<PlateEdgeFinding> findings,
Func<Vector, Vector> worldToScreen, Vector pointer, int deviceDpi) =>
findings.Where(finding => Hits(worldToScreen(finding.Marker), pointer, deviceDpi))
.OrderBy(finding => finding.PartId).ToArray();
private static bool Hits(Vector center, Vector pointer, int deviceDpi)
{
var radius = HitRadius(deviceDpi);
var dx = pointer.X - center.X;
var dy = pointer.Y - center.Y;
return dx * dx + dy * dy <= radius * radius;
}
}