feat(support): add slat support beds alongside the grid

Slats are strips of a set thickness running the full length or width of
the table, one every pitch. A part's contacts are the corners of where its
material crosses a slat, so the support check and support tabs work as on
the grid. Nest > Support Bed... picks grid or slats; nest files store
type "slats" and support-grid.json keeps both layouts' values. Draw
Support shows the slats cut off to the sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-24 15:33:12 -04:00
1 parent cd1961e26b
commit 48863d51c8
17 files changed
+715 -106

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
namespace OpenNest.Support
{
/// <summary>The axis a <see cref="SupportSlats"/> bed's slats run along, the full length or width of the table.</summary>
public enum SlatAxis
{
/// <summary>Slats run along X, the table's full length; they repeat along Y.</summary>
X,
/// <summary>Slats run along Y, the table's full width; they repeat along X.</summary>
Y,
}
}
+30
View File
@@ -182,6 +182,25 @@ namespace OpenNest.Support
return spans;
}
/// <summary>
/// The spans of the vertical line at <paramref name="x"/> that lie in material, in
/// ascending order, on the same half-open rule as <see cref="SpansAtY"/>.
/// </summary>
public IReadOnlyList<(double Start, double End)> SpansAtX(double x)
{
var crossings = new List<double>();
if (!IsComplete || x < bounds.Left || x > bounds.Right)
return Array.Empty<(double, double)>();
AddCrossingsAtX(outline, x, crossings);
foreach (var hole in holes)
AddCrossingsAtX(hole, x, crossings);
crossings.Sort();
var spans = new List<(double, double)>(crossings.Count / 2);
for (var i = 0; i + 1 < crossings.Count; i += 2)
spans.Add((crossings[i], crossings[i + 1]));
return spans;
}
/// <summary>Whether <paramref name="point"/> lies in material (even-odd over outline and holes).</summary>
public bool Contains(Vector point)
{
@@ -205,6 +224,17 @@ namespace OpenNest.Support
}
}
private static void AddCrossingsAtX(Vector[] ring, double x, List<double> crossings)
{
for (int i = 0, j = ring.Length - 1; i < ring.Length; j = i++)
{
var a = ring[j];
var b = ring[i];
if ((a.X > x) != (b.X > x))
crossings.Add(a.Y + (x - a.X) * (b.Y - a.Y) / (b.X - a.X));
}
}
private static bool Inside(Vector[] ring, Vector point)
{
var inside = false;
+152
View File
@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.Support
{
/// <summary>
/// A bed of straight slats, as on a plasma table: each slat a rectangle <see cref="Thickness"/>
/// wide running the full length of the table along <see cref="Axis"/>, one every
/// <see cref="Pitch"/> across it, the first centered <see cref="Offset"/> from the plate origin
/// into the sheet. A sheet in another quadrant sees the same slats mirrored. A part rests on
/// the region where its material crosses a slat; the corners of that region (where the
/// outline crosses a slat's edges, and outline vertices on the slat) are its contacts.
/// Lengths are in the nest's units.
/// </summary>
public sealed class SupportSlats : SupportBed
{
public SupportSlats(SlatAxis axis, double pitch, double thickness, double offset = 0,
double registrationTolerance = 0, double stabilityMargin = 0)
: base(registrationTolerance, stabilityMargin)
{
if (axis is not (SlatAxis.X or SlatAxis.Y))
throw new ArgumentOutOfRangeException(nameof(axis), "Slats run along X or Y.");
if (!double.IsFinite(pitch) || pitch <= 0)
throw new ArgumentOutOfRangeException(nameof(pitch), "Slat pitch must be finite and positive.");
if (!double.IsFinite(thickness) || thickness < 0)
throw new ArgumentOutOfRangeException(nameof(thickness), "Slat thickness must be finite and zero or more.");
if (!double.IsFinite(offset))
throw new ArgumentOutOfRangeException(nameof(offset), "Slat offset must be finite.");
Axis = axis;
Pitch = pitch;
Thickness = thickness;
Offset = offset;
}
public SlatAxis Axis { get; }
/// <summary>The distance between neighboring slats' center lines.</summary>
public double Pitch { get; }
/// <summary>How wide each slat's top edge is; zero treats a slat as a line.</summary>
public double Thickness { get; }
/// <summary>The first slat's center line, measured from the plate origin into the sheet.</summary>
public double Offset { get; }
public override IReadOnlyList<Vector> Contacts(SupportMaterial material, int quadrant, Vector shift,
bool extremesOnly = false)
{
ArgumentNullException.ThrowIfNull(material);
var contacts = new List<Vector>();
if (!material.IsComplete)
return contacts;
// Work across the slats: Y for slats along X, X for slats along Y.
var alongX = Axis == SlatAxis.X;
var baseline = alongX ? SignY(quadrant) * Offset + shift.Y : SignX(quadrant) * Offset + shift.X;
var bounds = material.Bounds;
var low = alongX ? bounds.Bottom : bounds.Left;
var high = alongX ? bounds.Top : bounds.Right;
var half = Thickness / 2;
var first = (long)System.Math.Ceiling((low - half - baseline) / Pitch);
var last = (long)System.Math.Floor((high + half - baseline) / Pitch);
if (first > last)
return contacts;
// Where the outline crosses each slat's edges.
for (var slat = first; slat <= last; slat++)
{
var center = baseline + slat * Pitch;
AddEdge(material, alongX, center - half, contacts);
if (half > 0)
AddEdge(material, alongX, center + half, contacts);
}
// Outline and hole vertices that sit on a slat: every vertex on one slat's top.
AddVertices(material.Outline, alongX, baseline, half, contacts);
foreach (var hole in material.Holes)
AddVertices(hole, alongX, baseline, half, contacts);
return contacts;
}
/// <summary>
/// The slats within <paramref name="region"/> (world coordinates) on a sheet in
/// <paramref name="quadrant"/>, each cut off to the region, for display; empty when there
/// would be more than <paramref name="limit"/>.
/// </summary>
public IReadOnlyList<Box> SlatsIn(Box region, int quadrant, int limit = 5000)
{
ArgumentNullException.ThrowIfNull(region);
var alongX = Axis == SlatAxis.X;
var baseline = alongX ? SignY(quadrant) * Offset : SignX(quadrant) * Offset;
var low = alongX ? region.Bottom : region.Left;
var high = alongX ? region.Top : region.Right;
var half = Thickness / 2;
var first = System.Math.Ceiling((low - half - baseline) / Pitch);
var last = System.Math.Floor((high + half - baseline) / Pitch);
var count = last - first + 1;
if (!double.IsFinite(count) || count <= 0 || count > limit)
return Array.Empty<Box>();
var slats = new List<Box>((int)count);
for (var slat = first; slat <= last; slat++)
{
var center = baseline + slat * Pitch;
var start = System.Math.Max(low, center - half);
var end = System.Math.Min(high, center + half);
slats.Add(alongX
? new Box(region.Left, start, region.Length, end - start)
: new Box(start, region.Bottom, end - start, region.Width));
}
return slats;
}
public override SupportBed Scaled(double factor) =>
new SupportSlats(Axis, Pitch * factor, Thickness * factor, Offset * factor,
RegistrationTolerance * factor, StabilityMargin * factor);
public override bool Equals(SupportBed other) =>
other is SupportSlats slats
&& slats.Axis == Axis && slats.Pitch == Pitch
&& slats.Thickness == Thickness && slats.Offset == Offset
&& slats.RegistrationTolerance == RegistrationTolerance
&& slats.StabilityMargin == StabilityMargin;
public override int GetHashCode() =>
HashCode.Combine(Axis, Pitch, Thickness, Offset, RegistrationTolerance, StabilityMargin);
private static void AddEdge(SupportMaterial material, bool alongX, double across, List<Vector> contacts)
{
var spans = alongX ? material.SpansAtY(across) : material.SpansAtX(across);
foreach (var (start, end) in spans)
{
contacts.Add(alongX ? new Vector(start, across) : new Vector(across, start));
contacts.Add(alongX ? new Vector(end, across) : new Vector(across, end));
}
}
private void AddVertices(IReadOnlyList<Vector> ring, bool alongX, double baseline, double half,
List<Vector> contacts)
{
foreach (var vertex in ring)
{
var across = alongX ? vertex.Y : vertex.X;
// The nearest slat center; the vertex is on that slat when within half its thickness.
var center = baseline + System.Math.Round((across - baseline) / Pitch) * Pitch;
if (System.Math.Abs(across - center) <= half)
contacts.Add(vertex);
}
}
}
}
+67 -19
View File
@@ -4,8 +4,9 @@ using OpenNest.Support;
namespace OpenNest.Data;
/// <summary>
/// The machine's laser support grid, persisted to a single JSON file (by default
/// %APPDATA%\OpenNest\support-grid.json). Off by default: until <see cref="Enabled"/> is set,
/// The machine's support bed, a laser grid of points or a plasma table's slats, persisted to a
/// single JSON file (by default %APPDATA%\OpenNest\support-grid.json). Both layouts' values are
/// kept; <see cref="UseSlats"/> picks one. Off by default: until <see cref="Enabled"/> is set,
/// new nests get no support bed. Loading never throws; an invalid value falls back to its
/// default and a file that cannot be read loads as off.
/// </summary>
@@ -31,6 +32,15 @@ public sealed class SupportGridSettings
public double PitchY { get; set; } = 2;
public double OffsetX { get; set; }
public double OffsetY { get; set; }
/// <summary>Whether the bed is slats rather than the grid of points.</summary>
public bool UseSlats { get; set; }
public SlatAxis SlatAxis { get; set; } = SlatAxis.Y;
public double SlatPitch { get; set; } = 4;
public double SlatThickness { get; set; } = 0.125;
public double SlatOffset { get; set; }
public double RegistrationTolerance { get; set; } = 0.125;
public double StabilityMargin { get; set; } = 0.25;
@@ -42,31 +52,50 @@ public sealed class SupportGridSettings
"support-grid.json"
);
/// <summary>Settings holding <paramref name="grid"/> (or the defaults when null) in <paramref name="units"/>.</summary>
public static SupportGridSettings FromGrid(SupportGrid grid, Units units, bool enabled)
/// <summary>
/// Settings holding <paramref name="bed"/> in <paramref name="units"/>; the other layout's
/// values come from <paramref name="others"/> (or the defaults when null).
/// </summary>
public static SupportGridSettings FromBed(SupportBed bed, Units units, bool enabled, SupportGridSettings others = null)
{
var settings = new SupportGridSettings().ConvertedTo(units);
var settings = (others ?? new SupportGridSettings()).ConvertedTo(units);
settings.Enabled = enabled;
if (grid != null)
if (bed != null)
{
settings.PitchX = grid.PitchX;
settings.PitchY = grid.PitchY;
settings.OffsetX = grid.OffsetX;
settings.OffsetY = grid.OffsetY;
settings.RegistrationTolerance = grid.RegistrationTolerance;
settings.StabilityMargin = grid.StabilityMargin;
settings.RegistrationTolerance = bed.RegistrationTolerance;
settings.StabilityMargin = bed.StabilityMargin;
}
switch (bed)
{
case SupportGrid grid:
settings.UseSlats = false;
settings.PitchX = grid.PitchX;
settings.PitchY = grid.PitchY;
settings.OffsetX = grid.OffsetX;
settings.OffsetY = grid.OffsetY;
break;
case SupportSlats slats:
settings.UseSlats = true;
settings.SlatAxis = slats.Axis;
settings.SlatPitch = slats.Pitch;
settings.SlatThickness = slats.Thickness;
settings.SlatOffset = slats.Offset;
break;
}
return settings;
}
/// <summary>The grid these settings describe in <paramref name="units"/>, or null when it is invalid.</summary>
public SupportGrid? GridFor(Units units)
/// <summary>The bed these settings describe in <paramref name="units"/>, or null when it is invalid.</summary>
public SupportBed? BedFor(Units units)
{
var converted = ConvertedTo(units);
try
{
return new SupportGrid(converted.PitchX, converted.PitchY, converted.OffsetX, converted.OffsetY,
converted.RegistrationTolerance, converted.StabilityMargin);
return UseSlats
? new SupportSlats(converted.SlatAxis, converted.SlatPitch, converted.SlatThickness,
converted.SlatOffset, converted.RegistrationTolerance, converted.StabilityMargin)
: new SupportGrid(converted.PitchX, converted.PitchY, converted.OffsetX, converted.OffsetY,
converted.RegistrationTolerance, converted.StabilityMargin);
}
catch (ArgumentOutOfRangeException)
{
@@ -86,17 +115,22 @@ public sealed class SupportGridSettings
PitchY = PitchY * scale,
OffsetX = OffsetX * scale,
OffsetY = OffsetY * scale,
UseSlats = UseSlats,
SlatAxis = SlatAxis,
SlatPitch = SlatPitch * scale,
SlatThickness = SlatThickness * scale,
SlatOffset = SlatOffset * scale,
RegistrationTolerance = RegistrationTolerance * scale,
StabilityMargin = StabilityMargin * scale,
};
}
/// <summary>Gives a newly created nest this grid when enabled (after its units are set).</summary>
/// <summary>Gives a newly created nest this bed when enabled (after its units are set).</summary>
public void ApplyToNewNest(Nest nest)
{
ArgumentNullException.ThrowIfNull(nest);
if (Enabled && GridFor(nest.Units) is { } grid)
nest.SetSupportBed(grid);
if (Enabled && BedFor(nest.Units) is { } bed)
nest.SetSupportBed(bed);
}
/// <summary>
@@ -128,6 +162,15 @@ public sealed class SupportGridSettings
settings.PitchY = Length(root, "pitchY", settings.PitchY, positive: true);
settings.OffsetX = Length(root, "offsetX", settings.OffsetX, positive: false, signed: true);
settings.OffsetY = Length(root, "offsetY", settings.OffsetY, positive: false, signed: true);
if (root.TryGetProperty("layout", out var layout) && layout.ValueKind == JsonValueKind.String)
settings.UseSlats = string.Equals(layout.GetString(), "slats", StringComparison.OrdinalIgnoreCase);
if (root.TryGetProperty("slatAxis", out var axis) && axis.ValueKind == JsonValueKind.String
&& Enum.TryParse<SlatAxis>(axis.GetString(), ignoreCase: true, out var slatAxis)
&& Enum.IsDefined(slatAxis))
settings.SlatAxis = slatAxis;
settings.SlatPitch = Length(root, "slatPitch", settings.SlatPitch, positive: true);
settings.SlatThickness = Length(root, "slatThickness", settings.SlatThickness, positive: false);
settings.SlatOffset = Length(root, "slatOffset", settings.SlatOffset, positive: false, signed: true);
settings.RegistrationTolerance = Length(root, "registrationTolerance", settings.RegistrationTolerance, positive: false);
settings.StabilityMargin = Length(root, "stabilityMargin", settings.StabilityMargin, positive: false);
return settings;
@@ -151,6 +194,11 @@ public sealed class SupportGridSettings
["pitchY"] = PitchY,
["offsetX"] = OffsetX,
["offsetY"] = OffsetY,
["layout"] = UseSlats ? "slats" : "grid",
["slatAxis"] = SlatAxis.ToString().ToLowerInvariant(),
["slatPitch"] = SlatPitch,
["slatThickness"] = SlatThickness,
["slatOffset"] = SlatOffset,
["registrationTolerance"] = RegistrationTolerance,
["stabilityMargin"] = StabilityMargin,
};
@@ -80,6 +80,35 @@ public class SupportGridPersistenceTests
Assert.Equal(-0.25, bed["offsetY"]!.GetValue<double>());
}
[Fact]
public void SlatsRoundTripWithTheirTypeAndAxis()
{
var slats = new SupportSlats(SlatAxis.Y, 4, 0.125, 1.5, 0.125, 0.25);
var file = Write(NestWith(slats));
var bed = NestJson(file)["plateDefaults"]!["supportBed"]!;
var reopened = new NestReader(file).Read();
Assert.Equal("slats", bed["type"]!.GetValue<string>());
Assert.Equal("y", bed["axis"]!.GetValue<string>());
Assert.Equal(0.125, bed["thickness"]!.GetValue<double>());
Assert.False(bed.AsObject().ContainsKey("pitchX"));
Assert.Equal(slats, reopened.SupportBed);
Assert.Same(reopened.SupportBed, Assert.Single(reopened.Plates).SupportBed);
}
[Theory]
[InlineData("""{"type":"slats","axis":"z","pitch":4,"thickness":0.125}""")]
[InlineData("""{"type":"slats","axis":"x","pitch":0,"thickness":0.125}""")]
[InlineData("""{"type":"slats","axis":"x","pitch":4}""")]
[InlineData("""{"type":"slats","axis":"x","pitch":4,"thickness":0.125,"offset":"one"}""")]
public void MalformedSlats_LoadAsNone(string bed)
{
var file = Rewrite(Write(NestWith(Grid)), json => json["plateDefaults"]!["supportBed"] = JsonNode.Parse(bed));
Assert.Null(new NestReader(file).Read().SupportBed);
}
[Fact]
public void NestWithoutGrid_WritesNoKey()
{
+68 -21
View File
@@ -110,8 +110,10 @@ namespace OpenNest.IO
}
/// <summary>
/// A support bed layout. <see cref="Type"/> names the layout: only <c>grid</c>, a regular
/// grid of contact points, so far. Lengths are in the nest's units.
/// A support bed layout. <see cref="Type"/> names the layout: <c>grid</c>, a regular grid
/// of contact points (pitch and offset along X and Y), or <c>slats</c>, straight slats
/// running along <see cref="Axis"/> (<see cref="Pitch"/>, <see cref="Thickness"/>,
/// <see cref="Offset"/>). Lengths are in the nest's units.
/// </summary>
public record SupportBedDto
{
@@ -120,6 +122,10 @@ namespace OpenNest.IO
public double PitchY { get; init; }
public double OffsetX { get; init; }
public double OffsetY { get; init; }
public string Axis { get; init; }
public double Pitch { get; init; }
public double Thickness { get; init; }
public double Offset { get; init; }
public double RegistrationTolerance { get; init; }
public double StabilityMargin { get; init; }
}
@@ -131,7 +137,9 @@ namespace OpenNest.IO
/// </summary>
public sealed class LenientSupportBedConverter : JsonConverter<SupportBedDto>
{
private static readonly string[] Optional = { "offsetX", "offsetY", "registrationTolerance", "stabilityMargin" };
private static readonly string[] Shared = { "registrationTolerance", "stabilityMargin" };
private static readonly string[] GridOffsets = { "offsetX", "offsetY" };
private static readonly string[] SlatOffset = { "offset" };
public override SupportBedDto Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
@@ -140,37 +148,76 @@ namespace OpenNest.IO
var bed = document.RootElement;
if (bed.ValueKind != JsonValueKind.Object
|| !bed.TryGetProperty("type", out var type) || type.ValueKind != JsonValueKind.String
|| type.GetString() != "grid"
|| !TryNumber(bed, "pitchX", out var pitchX) || !TryNumber(bed, "pitchY", out var pitchY))
|| !TryOptional(bed, Shared, out var shared))
return null;
var values = new double[Optional.Length];
for (var i = 0; i < Optional.Length; i++)
if (bed.TryGetProperty(Optional[i], out _) && !TryNumber(bed, Optional[i], out values[i]))
return null;
return new SupportBedDto
switch (type.GetString())
{
PitchX = pitchX,
PitchY = pitchY,
OffsetX = values[0],
OffsetY = values[1],
RegistrationTolerance = values[2],
StabilityMargin = values[3],
};
case "grid":
if (!TryNumber(bed, "pitchX", out var pitchX) || !TryNumber(bed, "pitchY", out var pitchY)
|| !TryOptional(bed, GridOffsets, out var offsets))
return null;
return new SupportBedDto
{
PitchX = pitchX,
PitchY = pitchY,
OffsetX = offsets[0],
OffsetY = offsets[1],
RegistrationTolerance = shared[0],
StabilityMargin = shared[1],
};
case "slats":
if (!bed.TryGetProperty("axis", out var axis) || axis.ValueKind != JsonValueKind.String
|| !TryNumber(bed, "pitch", out var pitch) || !TryNumber(bed, "thickness", out var thickness)
|| !TryOptional(bed, SlatOffset, out var offset))
return null;
return new SupportBedDto
{
Type = "slats",
Axis = axis.GetString(),
Pitch = pitch,
Thickness = thickness,
Offset = offset[0],
RegistrationTolerance = shared[0],
StabilityMargin = shared[1],
};
default:
return null;
}
}
public override void Write(Utf8JsonWriter writer, SupportBedDto value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("type", value.Type);
writer.WriteNumber("pitchX", value.PitchX);
writer.WriteNumber("pitchY", value.PitchY);
writer.WriteNumber("offsetX", value.OffsetX);
writer.WriteNumber("offsetY", value.OffsetY);
if (value.Type == "slats")
{
writer.WriteString("axis", value.Axis);
writer.WriteNumber("pitch", value.Pitch);
writer.WriteNumber("thickness", value.Thickness);
writer.WriteNumber("offset", value.Offset);
}
else
{
writer.WriteNumber("pitchX", value.PitchX);
writer.WriteNumber("pitchY", value.PitchY);
writer.WriteNumber("offsetX", value.OffsetX);
writer.WriteNumber("offsetY", value.OffsetY);
}
writer.WriteNumber("registrationTolerance", value.RegistrationTolerance);
writer.WriteNumber("stabilityMargin", value.StabilityMargin);
writer.WriteEndObject();
}
// Optional lengths default to zero; one that is present must be a number.
private static bool TryOptional(JsonElement bed, string[] names, out double[] values)
{
values = new double[names.Length];
for (var i = 0; i < names.Length; i++)
if (bed.TryGetProperty(names[i], out _) && !TryNumber(bed, names[i], out values[i]))
return false;
return true;
}
private static bool TryNumber(JsonElement bed, string name, out double value)
{
value = 0;
+13 -2
View File
@@ -481,8 +481,19 @@ namespace OpenNest.IO
return null;
try
{
return new SupportGrid(dto.PitchX, dto.PitchY, dto.OffsetX, dto.OffsetY,
dto.RegistrationTolerance, dto.StabilityMargin);
if (dto.Type != "slats")
return new SupportGrid(dto.PitchX, dto.PitchY, dto.OffsetX, dto.OffsetY,
dto.RegistrationTolerance, dto.StabilityMargin);
SlatAxis? axis = dto.Axis switch
{
"x" => SlatAxis.X,
"y" => SlatAxis.Y,
_ => null,
};
return axis is { } along
? new SupportSlats(along, dto.Pitch, dto.Thickness, dto.Offset,
dto.RegistrationTolerance, dto.StabilityMargin)
: null;
}
catch (ArgumentOutOfRangeException)
{
+16 -4
View File
@@ -159,8 +159,9 @@ namespace OpenNest.IO
}
private static SupportBedDto ToDto(SupportBed bed) =>
bed is SupportGrid grid
? new SupportBedDto
bed switch
{
SupportGrid grid => new SupportBedDto
{
PitchX = grid.PitchX,
PitchY = grid.PitchY,
@@ -168,8 +169,19 @@ namespace OpenNest.IO
OffsetY = grid.OffsetY,
RegistrationTolerance = grid.RegistrationTolerance,
StabilityMargin = grid.StabilityMargin,
}
: null;
},
SupportSlats slats => new SupportBedDto
{
Type = "slats",
Axis = slats.Axis == SlatAxis.X ? "x" : "y",
Pitch = slats.Pitch,
Thickness = slats.Thickness,
Offset = slats.Offset,
RegistrationTolerance = slats.RegistrationTolerance,
StabilityMargin = slats.StabilityMargin,
},
_ => null,
};
private List<DrawingDto> BuildDrawingDtos()
{
@@ -60,6 +60,48 @@ public class SupportGridSettingsTests : IDisposable
Assert.Equal(6.35, grid.StabilityMargin, 9);
}
[Fact]
public void SavedSlatsRoundTrip_AndKeepTheGridValuesForSwitchingBack()
{
new SupportGridSettings
{
Enabled = true,
PitchX = 3,
UseSlats = true,
SlatAxis = SlatAxis.X,
SlatPitch = 4,
SlatThickness = 0.125,
SlatOffset = 2,
}.Save(_path);
var settings = SupportGridSettings.Load(_path);
var nest = new Nest { Units = Units.Millimeters };
settings.ApplyToNewNest(nest);
var slats = Assert.IsType<SupportSlats>(nest.SupportBed);
Assert.Equal(SlatAxis.X, slats.Axis);
Assert.Equal(101.6, slats.Pitch, 9);
Assert.Equal(3.175, slats.Thickness, 9);
Assert.Equal(50.8, slats.Offset, 9);
Assert.Equal(3, settings.PitchX);
var fromBed = SupportGridSettings.FromBed(slats, Units.Millimeters, enabled: true, settings);
Assert.True(fromBed.UseSlats);
Assert.Equal(76.2, fromBed.PitchX, 9);
}
[Fact]
public void OlderFile_WithoutALayout_IsAGrid()
{
File.WriteAllText(_path, """{"enabled":true,"pitchX":2,"pitchY":2}""");
var settings = SupportGridSettings.Load(_path);
Assert.False(settings.UseSlats);
Assert.Equal(0.125, settings.SlatThickness);
Assert.IsType<SupportGrid>(settings.BedFor(Units.Inches));
}
[Fact]
public void InvalidValues_KeepTheirDefaults_AndBadJsonLoadsAsOff()
{
+124
View File
@@ -0,0 +1,124 @@
using OpenNest.Geometry;
using OpenNest.Support;
namespace OpenNest.Tests.Support;
/// <summary>
/// Whether a cut part stays level on a slat bed: each slat a strip of its thickness running the
/// table's full length or width, and a part's contacts the corners of where its material crosses one.
/// </summary>
public class SupportSlatsTests
{
private static SupportMaterial Rectangle(double x1, double y1, double x2, double y2) =>
SupportMaterial.FromPolygons(
new[] { new Vector(x1, y1), new Vector(x1, y2), new Vector(x2, y2), new Vector(x2, y1) });
[Fact]
public void Square_AcrossTwoSlats_RestsOnTheSlatsOuterEdges()
{
var slats = new SupportSlats(SlatAxis.Y, pitch: 2, thickness: 0.125, offset: 1);
var support = SupportAnalyzer.Analyze(slats, 1, Rectangle(0, 0, 4, 4));
Assert.True(support.IsComplete);
Assert.Equal(8, support.Contacts.Count);
Assert.Contains(new Vector(0.9375, 0), support.Contacts);
Assert.Contains(new Vector(3.0625, 4), support.Contacts);
Assert.Equal(1.0625, support.Margin, 12);
Assert.True(support.IsSupported);
}
[Fact]
public void Strip_AcrossOneSlat_BalancesOnItsThicknessButTipsUnderTheStabilityMargin()
{
var slats = new SupportSlats(SlatAxis.Y, pitch: 20, thickness: 0.125, offset: 5, stabilityMargin: 0.25);
var support = SupportAnalyzer.Analyze(slats, 1, Rectangle(0, 0, 10, 1));
Assert.Equal(4, support.Contacts.Count);
Assert.Equal(0.0625, support.Margin, 12);
Assert.False(support.IsSupported);
}
[Fact]
public void Strip_BetweenSlatsAlongItsLength_FallsThrough()
{
var slats = new SupportSlats(SlatAxis.X, pitch: 2, thickness: 0.125, offset: 1);
var support = SupportAnalyzer.Analyze(slats, 1, Rectangle(0, 1.25, 10, 1.75));
Assert.Empty(support.Contacts);
Assert.Equal(double.NegativeInfinity, support.Margin);
}
[Fact]
public void Part_NarrowerThanASlat_RestsOnItsOwnCorners()
{
var slats = new SupportSlats(SlatAxis.X, pitch: 4, thickness: 0.5, offset: 1);
var support = SupportAnalyzer.Analyze(slats, 1, Rectangle(0, 0.8, 3, 1.2));
Assert.Equal(4, support.Contacts.Distinct().Count());
Assert.Contains(new Vector(0, 0.8), support.Contacts);
Assert.Contains(new Vector(3, 1.2), support.Contacts);
Assert.Equal(0.2, support.Margin, 12);
}
[Fact]
public void Vertex_RestingOnASlat_IsAContact()
{
var slats = new SupportSlats(SlatAxis.X, pitch: 10, thickness: 0.125, offset: 0.05);
var triangle = SupportMaterial.FromPolygons(new[] { new Vector(0, 0), new Vector(4, 5), new Vector(8, 0) });
var contacts = slats.Contacts(triangle, 1, Vector.Zero);
Assert.Contains(new Vector(0, 0), contacts);
Assert.Contains(new Vector(8, 0), contacts);
// The bottom edge lies on the slat, so only the slat's top edge crosses the outline.
Assert.Contains(contacts, c => System.Math.Abs(c.Y - 0.1125) < 1e-12);
}
[Theory]
[InlineData(1, 1)]
[InlineData(3, -1)]
public void Quadrant_MeasuresTheSlatsFromTheOriginIntoTheSheet(int quadrant, double sign)
{
var slats = new SupportSlats(SlatAxis.X, pitch: 2, thickness: 0.125, offset: 0.5);
var sheet = new Box(sign < 0 ? -10 : 0, sign < 0 ? -4 : 0, 10, 4);
var strips = slats.SlatsIn(sheet, quadrant);
Assert.Equal(2, strips.Count);
Assert.Contains(strips, s => System.Math.Abs(s.Center.Y - sign * 0.5) < 1e-12);
Assert.All(strips, s => Assert.Equal(10, s.Length, 12));
Assert.All(strips, s => Assert.Equal(0.125, s.Width, 12));
}
[Fact]
public void SlatsIn_CutsTheEdgeSlatsOffToTheSheet()
{
var slats = new SupportSlats(SlatAxis.Y, pitch: 4, thickness: 0.5, offset: 0);
var strips = slats.SlatsIn(new Box(0, 0, 8, 3), 1);
Assert.Equal(3, strips.Count);
Assert.Equal(0.25, strips[0].Length, 12);
Assert.Equal(0.5, strips[1].Length, 12);
Assert.Equal(0.25, strips[2].Length, 12);
Assert.Equal(3, strips[0].Width, 12);
}
[Fact]
public void ScaledAndEqual_CompareEveryLength()
{
var slats = new SupportSlats(SlatAxis.Y, 4, 0.125, 1, 0.125, 0.25);
var scaled = Assert.IsType<SupportSlats>(slats.Scaled(2));
Assert.Equal(new SupportSlats(SlatAxis.Y, 8, 0.25, 2, 0.25, 0.5), scaled);
Assert.Equal(slats, new SupportSlats(SlatAxis.Y, 4, 0.125, 1, 0.125, 0.25));
Assert.NotEqual<SupportBed>(slats, new SupportSlats(SlatAxis.X, 4, 0.125, 1, 0.125, 0.25));
Assert.NotEqual<SupportBed>(slats, new SupportGrid(4, 4, 1, 1, 0.125, 0.25));
Assert.Throws<ArgumentOutOfRangeException>(() => new SupportSlats(SlatAxis.Y, 0, 0.125));
Assert.Throws<ArgumentOutOfRangeException>(() => new SupportSlats(SlatAxis.Y, 4, -0.125));
}
}
+42 -5
View File
@@ -8,8 +8,8 @@ using OpenNest.Support;
namespace OpenNest.Controls
{
/// <summary>
/// View &gt; Draw Support: the plate's support grid and, for each part, the contacts it rests
/// on and its center of gravity, green where the grid holds it level and red where it can
/// View &gt; Draw Support: the plate's support grid or slats and, for each part, the contacts it
/// rests on and its center of gravity, green where the bed holds it level and red where it can
/// tip; a red part's best tab point is marked with an orange diamond. Display only: it
/// takes part in no hit test, selection or sequencing. Analyses are cached per part until
/// its program, pose, the bed or the quadrant changes.
@@ -23,6 +23,7 @@ namespace OpenNest.Controls
private const float GridPointSize = 3f;
private static readonly Color GridColor = Color.FromArgb(110, 120, 120, 120);
private static readonly Color SlatColor = Color.FromArgb(90, 120, 120, 120);
private static readonly Color ContactColor = Color.FromArgb(220, 30, 150, 60);
private static readonly Color SupportedColor = Color.FromArgb(230, 20, 140, 50);
private static readonly Color TippingColor = Color.FromArgb(240, 210, 30, 30);
@@ -41,6 +42,8 @@ namespace OpenNest.Controls
if (bed is SupportGrid grid)
DrawGrid(g, view, plate, grid);
else if (bed is SupportSlats slats)
DrawSlats(g, view, plate, slats);
using var contactBrush = new SolidBrush(ContactColor);
using var supportedPen = new Pen(SupportedColor, 2f);
@@ -76,13 +79,47 @@ namespace OpenNest.Controls
return support;
}
private static Box Sheet(Plate plate)
{
var left = plate.Quadrant is 2 or 3 ? -plate.Size.Length : 0;
var bottom = plate.Quadrant is 3 or 4 ? -plate.Size.Width : 0;
return new Box(left, bottom, plate.Size.Length, plate.Size.Width);
}
// Each slat a strip cut off to the sheet, at least a pixel wide however far out the view is.
private static void DrawSlats(Graphics g, PlateView view, Plate plate, SupportSlats slats)
{
if (view.LengthWorldToGui(slats.Pitch) < MinimumGridSpacing)
return;
using var brush = new SolidBrush(SlatColor);
const float minimum = 1f;
foreach (var slat in slats.SlatsIn(Sheet(plate), plate.Quadrant))
{
var a = view.PointWorldToGraph(new Vector(slat.Left, slat.Top));
var b = view.PointWorldToGraph(new Vector(slat.Right, slat.Bottom));
var x = System.Math.Min(a.X, b.X);
var y = System.Math.Min(a.Y, b.Y);
var width = System.Math.Abs(b.X - a.X);
var height = System.Math.Abs(b.Y - a.Y);
if (width < minimum)
{
x -= (minimum - width) / 2;
width = minimum;
}
if (height < minimum)
{
y -= (minimum - height) / 2;
height = minimum;
}
g.FillRectangle(brush, x, y, width, height);
}
}
private static void DrawGrid(Graphics g, PlateView view, Plate plate, SupportGrid grid)
{
if (view.LengthWorldToGui(System.Math.Min(grid.PitchX, grid.PitchY)) < MinimumGridSpacing)
return;
var left = plate.Quadrant is 2 or 3 ? -plate.Size.Length : 0;
var bottom = plate.Quadrant is 3 or 4 ? -plate.Size.Width : 0;
var sheet = new Box(left, bottom, plate.Size.Length, plate.Size.Width);
var sheet = Sheet(plate);
using var brush = new SolidBrush(GridColor);
// Anti-aliased dots smear differently at each subpixel offset; whole-pixel squares
// drawn without smoothing are all the same size.
+1 -1
View File
@@ -114,7 +114,7 @@ namespace OpenNest.Forms
{
nest.Machine = machine == null ? null : new NestMachine(machine.Id, machine.Name);
var grid = LoadSupportGridSettings(machine);
nest.SetSupportBed(grid.Enabled ? grid.GridFor(nest.Units) : null);
nest.SetSupportBed(grid.Enabled ? grid.BedFor(nest.Units) : null);
}
/// <summary>
+1 -1
View File
@@ -632,7 +632,7 @@ namespace OpenNest.Forms
//
mnuNestSupportGrid.Name = "mnuNestSupportGrid";
mnuNestSupportGrid.Size = new System.Drawing.Size(205, 22);
mnuNestSupportGrid.Text = "Support Grid...";
mnuNestSupportGrid.Text = "Support Bed...";
mnuNestSupportGrid.Click += SupportGrid_Click;
//
// mnuNestImportDrawing
+2 -2
View File
@@ -159,7 +159,7 @@ namespace OpenNest.Forms
if (IsPlateWorkBusy(views))
{
MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.",
"Support Grid", MessageBoxButtons.OK, MessageBoxIcon.Information);
"Support Bed", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
@@ -172,7 +172,7 @@ namespace OpenNest.Forms
if (form.ShowDialog(this) != DialogResult.OK)
return;
nest.SetSupportBed(form.Grid);
nest.SetSupportBed(form.Bed);
foreach (var view in views)
view.Invalidate();
}
+81 -31
View File
@@ -8,8 +8,9 @@ using OpenNest.Support;
namespace OpenNest.Forms;
/// <summary>
/// Nest &gt; Support Grid...: edits the laser grid of contact points every sheet of the current
/// nest rests on, used to judge whether cut parts stay level and where support tabs go (see
/// Nest &gt; Support Bed...: edits what every sheet of the current nest rests on, a laser grid of
/// contact points or a plasma table's slats, used to judge whether cut parts stay level and where
/// support tabs go (see
/// docs/support-tabs.md). OK applies it to the nest; Cancel discards the edits. Save as Machine
/// Defaults writes the grid to the nest's machine configuration immediately and is not undone by Cancel.
/// </summary>
@@ -23,6 +24,12 @@ public sealed class SupportGridForm : Form
private readonly NumericUpDown pitchYBox;
private readonly NumericUpDown offsetXBox;
private readonly NumericUpDown offsetYBox;
private readonly RadioButton gridButton;
private readonly RadioButton slatsButton;
private readonly ComboBox slatAxisBox;
private readonly NumericUpDown slatPitchBox;
private readonly NumericUpDown slatThicknessBox;
private readonly NumericUpDown slatOffsetBox;
private readonly NumericUpDown toleranceBox;
private readonly NumericUpDown marginBox;
private readonly CheckBox applyToNewNestsBox;
@@ -38,14 +45,14 @@ public sealed class SupportGridForm : Form
this.units = units;
this.machineDefaults = machineDefaults ?? new SupportGridSettings();
this.saveMachineDefaults = saveMachineDefaults;
var grid = bed as SupportGrid;
// A nest without a grid starts from the machine defaults, so enabling it is one click.
var initial = grid != null
? SupportGridSettings.FromGrid(grid, units, this.machineDefaults.Enabled)
// A nest without a bed starts from the machine defaults, so enabling it is one click; the
// layout it does not use keeps the defaults' values.
var initial = bed != null
? SupportGridSettings.FromBed(bed, units, this.machineDefaults.Enabled, this.machineDefaults)
: this.machineDefaults.ConvertedTo(units);
var unit = UnitsHelper.GetShortString(units);
Text = "Support Grid";
Text = "Support Bed";
StartPosition = FormStartPosition.CenterParent;
AutoSize = true;
AutoSizeMode = AutoSizeMode.GrowAndShrink;
@@ -66,30 +73,55 @@ public sealed class SupportGridForm : Form
AutoSize = true,
MaximumSize = new Size(430, 0),
Padding = new Padding(0, 0, 0, 8),
Text = "The grid of contact points cut parts rest on, one every pitch along X and Y, the first "
+ "at the offset from the plate origin into the sheet. A part whose center of gravity "
+ "is not inside its contacts by the stability margin can tip; with \"Tab parts the "
+ "grid cannot support\" in Cutting Settings, Plan Cutting tabs it where the tab "
+ "steadies it. The registration tolerance allows for a sheet loaded off its place.",
Text = "What cut parts rest on: a grid of contact points, one every pitch along X and Y, the "
+ "first at the offset from the plate origin into the sheet; or slats, each a strip of "
+ "the slat thickness running the full length or width of the table, one every pitch, "
+ "the first centered at the offset. A part whose center of gravity is not inside its "
+ "contacts by the stability margin can tip; with \"Tab parts the support grid cannot "
+ "hold level\" in Cutting Settings, Plan Cutting tabs it where the tab steadies it. "
+ "The registration tolerance allows for a sheet loaded off its place.",
});
useGridBox = new CheckBox { Text = "This nest's sheets rest on a support grid", AutoSize = true, Checked = grid != null };
useGridBox = new CheckBox { Text = "This nest's sheets rest on a support bed", AutoSize = true, Checked = bed != null };
layout.Controls.Add(useGridBox);
var values = new TableLayoutPanel { AutoSize = true, ColumnCount = 3 };
var kinds = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Padding = new Padding(16, 0, 0, 0) };
gridButton = new RadioButton { Text = "Grid of points", AutoSize = true, Checked = !initial.UseSlats };
slatsButton = new RadioButton { Text = "Slats", AutoSize = true, Checked = initial.UseSlats };
kinds.Controls.Add(gridButton);
kinds.Controls.Add(slatsButton);
layout.Controls.Add(kinds);
var gridValues = new TableLayoutPanel { AutoSize = true, ColumnCount = 3 };
pitchXBox = NewLengthBox(initial.PitchX, 0.0001m);
pitchYBox = NewLengthBox(initial.PitchY, 0.0001m);
offsetXBox = NewLengthBox(initial.OffsetX, -100000m);
offsetYBox = NewLengthBox(initial.OffsetY, -100000m);
AddRow(gridValues, "Pitch along X:", pitchXBox, unit);
AddRow(gridValues, "Pitch along Y:", pitchYBox, unit);
AddRow(gridValues, "First point, X from the origin:", offsetXBox, unit);
AddRow(gridValues, "First point, Y from the origin:", offsetYBox, unit);
layout.Controls.Add(gridValues);
var slatValues = new TableLayoutPanel { AutoSize = true, ColumnCount = 3 };
slatAxisBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 200 };
slatAxisBox.Items.AddRange(new object[] { "Along X (the full length)", "Along Y (the full width)" });
slatAxisBox.SelectedIndex = initial.SlatAxis == SlatAxis.X ? 0 : 1;
slatPitchBox = NewLengthBox(initial.SlatPitch, 0.0001m);
slatThicknessBox = NewLengthBox(initial.SlatThickness, 0m);
slatOffsetBox = NewLengthBox(initial.SlatOffset, -100000m);
AddRow(slatValues, "Slats run:", slatAxisBox, "");
AddRow(slatValues, "Pitch between slats:", slatPitchBox, unit);
AddRow(slatValues, "Slat thickness:", slatThicknessBox, unit);
AddRow(slatValues, "First slat's center from the origin:", slatOffsetBox, unit);
layout.Controls.Add(slatValues);
var sharedValues = new TableLayoutPanel { AutoSize = true, ColumnCount = 3 };
toleranceBox = NewLengthBox(initial.RegistrationTolerance, 0m);
marginBox = NewLengthBox(initial.StabilityMargin, 0m);
AddRow(values, "Pitch along X:", pitchXBox, unit);
AddRow(values, "Pitch along Y:", pitchYBox, unit);
AddRow(values, "First point, X from the origin:", offsetXBox, unit);
AddRow(values, "First point, Y from the origin:", offsetYBox, unit);
AddRow(values, "Registration tolerance:", toleranceBox, unit);
AddRow(values, "Stability margin:", marginBox, unit);
layout.Controls.Add(values);
AddRow(sharedValues, "Registration tolerance:", toleranceBox, unit);
AddRow(sharedValues, "Stability margin:", marginBox, unit);
layout.Controls.Add(sharedValues);
var defaultsGroup = new GroupBox
{
@@ -106,7 +138,7 @@ public sealed class SupportGridForm : Form
};
applyToNewNestsBox = new CheckBox
{
Text = "Give nests on this machine this grid",
Text = "Give nests on this machine this bed",
AutoSize = true,
Checked = this.machineDefaults.Enabled,
};
@@ -143,14 +175,15 @@ public sealed class SupportGridForm : Form
CancelButton = cancel;
useGridBox.CheckedChanged += (_, _) => UpdateEnabled();
slatsButton.CheckedChanged += (_, _) => UpdateEnabled();
loadDefaultsButton.Click += (_, _) => ShowValues(this.machineDefaults.ConvertedTo(units));
saveDefaultsButton.Click += (_, _) => SaveMachineDefaults();
ok.Click += (_, _) => Accept();
UpdateEnabled();
}
/// <summary>The grid the nest's sheets rest on after OK, or null for none.</summary>
public SupportGrid Grid { get; private set; }
/// <summary>The bed the nest's sheets rest on after OK, or null for none.</summary>
public SupportBed Bed { get; private set; }
private SupportGridSettings Read() => new()
{
@@ -160,6 +193,11 @@ public sealed class SupportGridForm : Form
PitchY = (double)pitchYBox.Value,
OffsetX = (double)offsetXBox.Value,
OffsetY = (double)offsetYBox.Value,
UseSlats = slatsButton.Checked,
SlatAxis = slatAxisBox.SelectedIndex == 0 ? SlatAxis.X : SlatAxis.Y,
SlatPitch = (double)slatPitchBox.Value,
SlatThickness = (double)slatThicknessBox.Value,
SlatOffset = (double)slatOffsetBox.Value,
RegistrationTolerance = (double)toleranceBox.Value,
StabilityMargin = (double)marginBox.Value,
};
@@ -170,6 +208,12 @@ public sealed class SupportGridForm : Form
pitchYBox.Value = ToBoxValue(pitchYBox, settings.PitchY);
offsetXBox.Value = ToBoxValue(offsetXBox, settings.OffsetX);
offsetYBox.Value = ToBoxValue(offsetYBox, settings.OffsetY);
gridButton.Checked = !settings.UseSlats;
slatsButton.Checked = settings.UseSlats;
slatAxisBox.SelectedIndex = settings.SlatAxis == SlatAxis.X ? 0 : 1;
slatPitchBox.Value = ToBoxValue(slatPitchBox, settings.SlatPitch);
slatThicknessBox.Value = ToBoxValue(slatThicknessBox, settings.SlatThickness);
slatOffsetBox.Value = ToBoxValue(slatOffsetBox, settings.SlatOffset);
toleranceBox.Value = ToBoxValue(toleranceBox, settings.RegistrationTolerance);
marginBox.Value = ToBoxValue(marginBox, settings.StabilityMargin);
applyToNewNestsBox.Checked = settings.Enabled;
@@ -177,8 +221,13 @@ public sealed class SupportGridForm : Form
private void UpdateEnabled()
{
foreach (var box in new[] { pitchXBox, pitchYBox, offsetXBox, offsetYBox, toleranceBox, marginBox })
box.Enabled = useGridBox.Checked;
var on = useGridBox.Checked;
gridButton.Enabled = slatsButton.Enabled = on;
foreach (var box in new Control[] { pitchXBox, pitchYBox, offsetXBox, offsetYBox })
box.Enabled = on && !slatsButton.Checked;
foreach (var box in new Control[] { slatAxisBox, slatPitchBox, slatThicknessBox, slatOffsetBox })
box.Enabled = on && slatsButton.Checked;
toleranceBox.Enabled = marginBox.Enabled = on;
}
private void SaveMachineDefaults()
@@ -188,8 +237,8 @@ public sealed class SupportGridForm : Form
{
saveMachineDefaults(settings);
savedLabel.Text = settings.Enabled
? $"Saved {DateTime.Now:t}: nests on this machine get this grid. Cancel does not undo this."
: $"Saved {DateTime.Now:t}: nests on this machine get no grid. Cancel does not undo this.";
? $"Saved {DateTime.Now:t}: nests on this machine get this bed. Cancel does not undo this."
: $"Saved {DateTime.Now:t}: nests on this machine get no bed. Cancel does not undo this.";
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException)
{
@@ -200,10 +249,11 @@ public sealed class SupportGridForm : Form
private void Accept()
{
Grid = useGridBox.Checked ? Read().GridFor(units) : null;
if (useGridBox.Checked && Grid == null)
Bed = useGridBox.Checked ? Read().BedFor(units) : null;
if (useGridBox.Checked && Bed == null)
{
MessageBox.Show(this, "Both pitches must be greater than zero.", Text,
MessageBox.Show(this, slatsButton.Checked ? "The slat pitch must be greater than zero."
: "Both pitches must be greater than zero.", Text,
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
+10 -7
View File
@@ -152,13 +152,16 @@ spacing differs from the saved defaults, is still written.
## Support grid
`plateDefaults.supportBed` optionally stores the machine support bed every sheet of
the nest rests on (see [support tabs](support-tabs.md)): `type` (`grid`, the only
layout so far), `pitchX`, `pitchY`, `offsetX`, `offsetY` (the first grid point,
measured from the plate origin into the sheet), `registrationTolerance` and
`stabilityMargin`, all in the nest's units. The key is omitted when the nest has
none, so such files are unchanged. A missing pitch, a non-numeric value, an unknown
`type` or an invalid value (a non-positive pitch, a negative tolerance or margin)
loads as no bed; the rest of the nest loads. Every plate follows the nest's bed.
the nest rests on (see [support tabs](support-tabs.md)): `type`, then for `grid`
`pitchX`, `pitchY`, `offsetX`, `offsetY` (the first grid point, measured from the
plate origin into the sheet), or for `slats` `axis` (`x` or `y`, the axis the slats
run along), `pitch`, `thickness` and `offset` (the first slat's center line); both
end with `registrationTolerance` and `stabilityMargin`. All lengths are in the
nest's units. The key is omitted when the nest has none, so such files are
unchanged. A missing pitch or thickness, a non-numeric value, an unknown `type` or
`axis`, or an invalid value (a non-positive pitch, a negative thickness, tolerance
or margin) loads as no bed; the rest of the nest loads. Versions without slat
support read a `slats` bed as none. Every plate follows the nest's bed.
## Drawing edits
+25 -13
View File
@@ -9,10 +9,20 @@ perimeter point where one tab steadies the part most.
`OpenNest.Support.SupportBed` is the machine's bed, fixed to the machine and measured from the
plate origin into the sheet; a sheet in another quadrant sees it mirrored. The built-in layout
is the laser grid, `SupportGrid`: one contact point every `PitchX` along X and `PitchY` along
Y, the first at (`OffsetX`, `OffsetY`). Lengths are in the nest's units. Straight slats, and
curved slats imported from a CAD layout (as on the plasma table), are planned as further
`SupportBed` subclasses: a layout only reports its contacts under a part's material.
is one of two layouts; a layout only reports its contacts under a part's material. Lengths are
in the nest's units.
- The laser grid, `SupportGrid`: one contact point every `PitchX` along X and `PitchY` along
Y, the first at (`OffsetX`, `OffsetY`).
- Straight slats, `SupportSlats` (as on the plasma table): each slat a strip `Thickness` wide
running the full length (`Axis` X) or width (`Axis` Y) of the table, one every `Pitch`
across it, the first centered `Offset` from the origin. A part rests on the region where its
material crosses a slat, so its contacts are that region's corners: where the outline and
holes cross each slat's two edges, and the outline and hole vertices on a slat. A part
narrower than a slat rests on its own corners; one balanced across a single slat has a margin
of at most half the thickness, which the stability margin rejects.
Curved slats imported from a CAD layout are not modeled yet.
Every bed also carries:
@@ -23,14 +33,15 @@ Every bed also carries:
The bed belongs to the nest (`Nest.SetSupportBed`); every plate follows it, including plates
added later. It is display, analysis and tab placement only: never material, spacing or a
keep-out. Nest > Support Grid... edits it; Save as Machine Defaults writes it to the nest's
machine configuration (`MachineConfig.SupportGrid`), which nests assigned that machine receive
while it is enabled: File > New and BOM import assign the Tools > Nest Defaults machine (the
first by name when none is chosen), and Nest > Machine (or the status bar's machine label) reassigns a nest and replaces its grid
with the new machine's (none when that machine gives nests no grid). A machine with no grid
saved yet uses the legacy shared `%APPDATA%\OpenNest\support-grid.json`. The nest file stores
the bed in `plateDefaults.supportBed` and the machine in `machine` (see
[nest file format](nest-file-format.md)).
keep-out. Nest > Support Bed... edits it and picks the grid or slats; Save as Machine Defaults
writes it to the nest's machine configuration (`MachineConfig.SupportGrid`: both layouts' values
and which one is in use; settings saved before slats existed are a grid), which nests assigned that
machine receive while it is enabled: File > New and BOM import assign the Tools > Nest Defaults
machine (the first by name when none is chosen), and Nest > Machine (or the status bar's machine
label) reassigns a nest and replaces its bed with the new machine's (none when that machine gives
nests no bed). A machine with no bed saved yet uses the legacy shared
`%APPDATA%\OpenNest\support-grid.json`. The nest file stores the bed in
`plateDefaults.supportBed` and the machine in `machine` (see [nest file format](nest-file-format.md)).
## The support check
@@ -61,7 +72,8 @@ off the corner only), and `AcceptsTabAt` rejects closer points. The tie-break ab
the clear points only; points equally near the center line go to the one farthest from an
inside corner. Only when every outline point is too close does the clearance give way.
View > Draw Support shows the grid (while its points are at least 6 pixels apart), each part's
View > Draw Support shows the grid or the slats, cut off to the sheet (while its points or slats
are at least 6 pixels apart), each part's
contacts, and its center of gravity: green when supported, red when it can tip, with an orange
diamond on a red part's best tab point (keeping the plate's tab-size clearance).