Files
OpenNest/OpenNest.Core/Diagnostics/OverlapGeometryStamp.cs
T
aj 9a293bf221 fix(core): compare clamp cut checks in the plate's quadrant frame
Plate.ClampZones are stored relative to the sheet's lower-left corner,
but cut-off positions, motions and part poses use the signed quadrant
frame. The automatic cutoff planner, manual cut-off contacts, clamp
rapid warnings and whole-plate overlap capture compared the two
directly, so in quadrants 2-4 a cut or rapid through a real rail clamp
was neither avoided nor warned about.

Add ClampZoneFrame.ToPlacement, which maps live zones into the
placement frame exactly like the sheet and engine stock, and use it in
every Slice 5 consumer. It rejects missing or malformed zones (bounds
stay publicly mutable); pre-post verification reports those as an
incomplete clamp check instead of a clear result. Overlap freshness now
treats a size or quadrant change as moving the zones.

Tests use real ClampRail zones in all quadrants; the hand-signed
quadrant fixtures that hid the defect are converted to rails.

Project Memory: opennest 274836d5-fb76-4619-b3cc-0c811e767dd6
2026-10-13 18:15:49 -04:00

133 lines
5.2 KiB
C#

using System;
using OpenNest.CNC;
namespace OpenNest.Diagnostics;
/// <summary>
/// Cheap ordered identity/pose check, not a geometry hash. In-place geometry editors must
/// explicitly invalidate before mutating. Capture and match only on the model's UI thread.
/// </summary>
public sealed class OverlapGeometryStamp
{
private readonly Plate plate;
private readonly Entry[] entries;
private readonly (string Id, long X, long Y, long Length, long Width)[] zones;
private readonly (CutOff Definition, CutOffAxis Axis, long X, long Y, long? Start, long? End)[] cutoffs;
private readonly (long Width, long Length, int Quadrant) sheet;
private OverlapGeometryStamp(Plate plate)
{
this.plate = plate;
sheet = SheetState(plate);
entries = new Entry[plate.Parts.Count];
for (var i = 0; i < entries.Length; i++)
entries[i] = new Entry(plate.Parts[i]);
zones = new (string, long, long, long, long)[plate.ClampZones.Count];
for (var i = 0; i < zones.Length; i++)
{
var zone = plate.ClampZones[i];
var box = zone.Bounds;
zones[i] = (zone.Id, Bits(box.X), Bits(box.Y), Bits(box.Length), Bits(box.Width));
}
cutoffs = new (CutOff, CutOffAxis, long, long, long?, long?)[plate.CutOffs.Count];
for (var i = 0; i < cutoffs.Length; i++)
cutoffs[i] = CutOffState(plate.CutOffs[i]);
}
private static (CutOff, CutOffAxis, long, long, long?, long?) CutOffState(CutOff cut) =>
(cut, cut.Axis, Bits(cut.Position.X), Bits(cut.Position.Y),
cut.StartLimit.HasValue ? Bits(cut.StartLimit.Value) : null,
cut.EndLimit.HasValue ? Bits(cut.EndLimit.Value) : null);
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
private static (long, long, int) SheetState(Plate plate) =>
(Bits(plate.Size.Width), Bits(plate.Size.Length), plate.Quadrant);
public static OverlapGeometryStamp Capture(Plate plate) => new(plate);
internal bool SameZones(Plate current)
{
// Stored zones are sheet-relative: a size or quadrant change moves them in the
// parts' frame without changing their bits.
if (!ReferenceEquals(plate, current) || current.ClampZones.Count != zones.Length
|| sheet != SheetState(current))
return false;
for (var i = 0; i < zones.Length; i++)
{
var zone = current.ClampZones[i];
var box = zone.Bounds;
if (zones[i] != (zone.Id, Bits(box.X), Bits(box.Y), Bits(box.Length), Bits(box.Width)))
return false;
}
return true;
}
internal bool SameCutOffs(Plate current)
{
if (!ReferenceEquals(plate, current) || current.CutOffs.Count != cutoffs.Length
|| sheet != SheetState(current))
return false;
for (var i = 0; i < cutoffs.Length; i++)
if (cutoffs[i] != CutOffState(current.CutOffs[i]))
return false;
return true;
}
public bool Matches(Plate current)
{
if (current.Parts.Count != entries.Length || !SameZones(current) || !SameCutOffs(current))
return false;
for (var i = 0; i < entries.Length; i++)
if (!entries[i].Matches(current.Parts[i]))
return false;
return true;
}
/// <summary>
/// Conservative display reuse: only unchanged ordered slots on the same plate survive.
/// Index-changing edits may hide extra pairs, but cannot attach old geometry to new parts.
/// </summary>
internal bool[] UnchangedSlots(Plate current)
{
var matches = new bool[entries.Length];
if (!ReferenceEquals(plate, current))
return matches;
for (var i = 0; i < entries.Length && i < current.Parts.Count; i++)
matches[i] = entries[i].Matches(current.Parts[i]);
return matches;
}
private readonly struct Entry
{
private readonly Part part;
private readonly Drawing drawing;
private readonly Program placedProgram;
private readonly Program cleanProgram;
private readonly long x, y, rotation;
private readonly bool isCutOff;
public Entry(Part part)
{
this.part = part;
drawing = part.BaseDrawing;
placedProgram = part.Program;
cleanProgram = drawing.Program;
x = BitConverter.DoubleToInt64Bits(part.Location.X);
y = BitConverter.DoubleToInt64Bits(part.Location.Y);
rotation = BitConverter.DoubleToInt64Bits(part.Rotation);
isCutOff = drawing.IsCutOff;
}
public bool Matches(Part current) =>
ReferenceEquals(part, current)
&& ReferenceEquals(drawing, current.BaseDrawing)
&& ReferenceEquals(placedProgram, current.Program)
&& ReferenceEquals(cleanProgram, current.BaseDrawing.Program)
&& x == BitConverter.DoubleToInt64Bits(current.Location.X)
&& y == BitConverter.DoubleToInt64Bits(current.Location.Y)
&& rotation == BitConverter.DoubleToInt64Bits(current.Rotation)
&& isCutOff == current.BaseDrawing.IsCutOff;
}
}