mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-01 15:51:37 -05:00
The overlap crosshair sat on the pair's area centroid. For a split,
holed or concave overlap, that point can be off the red material:
between two patches, in a hole, or inside a curve.
PlateOverlapPair.Marker is a point inside the pair's largest island.
It is the island's pole of inaccessibility (PolyLabel), searched to a
tenth of the island's area/perimeter within a 4096-cell budget. If
that bounded search does not land inside (a long thin sliver), the
marker falls back to the midpoint of the widest interior run of a
horizontal scan line placed between vertex heights.
The overlay crosshair, marker hit testing, hover coordinates
("Marker:") and the cutting-inspection ring now use the marker.
Centroid stays the true area centroid. View > Overlap Check > Display
> Centroids is now labelled Markers.
Before the marker existed, its call sites used the centroid. Two
patches, a covered frame, a half annulus and a thin crescent then put
it off the material; now all four land on it. Disabling the scan-line
fallback fails its test. On the 389-plate corpus, markers add about
0.4 ms per overlapping pair; pair and area results are unchanged.
54 lines
2.4 KiB
C#
54 lines
2.4 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}";
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
var radius = HitRadius(deviceDpi);
|
|
return pairs.Where(pair =>
|
|
{
|
|
var center = worldToScreen(pair.Marker);
|
|
var dx = pointer.X - center.X;
|
|
var dy = pointer.Y - center.Y;
|
|
return dx * dx + dy * dy <= radius * radius;
|
|
}).OrderBy(pair => pair.PartAId).ThenBy(pair => pair.PartBId).ToArray();
|
|
}
|
|
}
|