diff --git a/OpenNest.Core/Support/SlatAxis.cs b/OpenNest.Core/Support/SlatAxis.cs
new file mode 100644
index 00000000..93ffe3e2
--- /dev/null
+++ b/OpenNest.Core/Support/SlatAxis.cs
@@ -0,0 +1,12 @@
+namespace OpenNest.Support
+{
+ /// The axis a bed's slats run along, the full length or width of the table.
+ public enum SlatAxis
+ {
+ /// Slats run along X, the table's full length; they repeat along Y.
+ X,
+
+ /// Slats run along Y, the table's full width; they repeat along X.
+ Y,
+ }
+}
diff --git a/OpenNest.Core/Support/SupportMaterial.cs b/OpenNest.Core/Support/SupportMaterial.cs
index 40f4c16d..1388574e 100644
--- a/OpenNest.Core/Support/SupportMaterial.cs
+++ b/OpenNest.Core/Support/SupportMaterial.cs
@@ -182,6 +182,25 @@ namespace OpenNest.Support
return spans;
}
+ ///
+ /// The spans of the vertical line at that lie in material, in
+ /// ascending order, on the same half-open rule as .
+ ///
+ public IReadOnlyList<(double Start, double End)> SpansAtX(double x)
+ {
+ var crossings = new List();
+ 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;
+ }
+
/// Whether lies in material (even-odd over outline and holes).
public bool Contains(Vector point)
{
@@ -205,6 +224,17 @@ namespace OpenNest.Support
}
}
+ private static void AddCrossingsAtX(Vector[] ring, double x, List 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;
diff --git a/OpenNest.Core/Support/SupportSlats.cs b/OpenNest.Core/Support/SupportSlats.cs
new file mode 100644
index 00000000..65eb4fc5
--- /dev/null
+++ b/OpenNest.Core/Support/SupportSlats.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using OpenNest.Geometry;
+
+namespace OpenNest.Support
+{
+ ///
+ /// A bed of straight slats, as on a plasma table: each slat a rectangle
+ /// wide running the full length of the table along , one every
+ /// across it, the first centered 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.
+ ///
+ 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; }
+
+ /// The distance between neighboring slats' center lines.
+ public double Pitch { get; }
+
+ /// How wide each slat's top edge is; zero treats a slat as a line.
+ public double Thickness { get; }
+
+ /// The first slat's center line, measured from the plate origin into the sheet.
+ public double Offset { get; }
+
+ public override IReadOnlyList Contacts(SupportMaterial material, int quadrant, Vector shift,
+ bool extremesOnly = false)
+ {
+ ArgumentNullException.ThrowIfNull(material);
+ var contacts = new List();
+ 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;
+ }
+
+ ///
+ /// The slats within (world coordinates) on a sheet in
+ /// , each cut off to the region, for display; empty when there
+ /// would be more than .
+ ///
+ public IReadOnlyList 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();
+
+ var slats = new List((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 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 ring, bool alongX, double baseline, double half,
+ List 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);
+ }
+ }
+ }
+}
diff --git a/OpenNest.Data/SupportGridSettings.cs b/OpenNest.Data/SupportGridSettings.cs
index 71676bb7..e3a696ab 100644
--- a/OpenNest.Data/SupportGridSettings.cs
+++ b/OpenNest.Data/SupportGridSettings.cs
@@ -4,8 +4,9 @@ using OpenNest.Support;
namespace OpenNest.Data;
///
-/// The machine's laser support grid, persisted to a single JSON file (by default
-/// %APPDATA%\OpenNest\support-grid.json). Off by default: until 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; picks one. Off by default: until 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.
///
@@ -31,6 +32,15 @@ public sealed class SupportGridSettings
public double PitchY { get; set; } = 2;
public double OffsetX { get; set; }
public double OffsetY { get; set; }
+
+ /// Whether the bed is slats rather than the grid of points.
+ 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"
);
- /// Settings holding (or the defaults when null) in .
- public static SupportGridSettings FromGrid(SupportGrid grid, Units units, bool enabled)
+ ///
+ /// Settings holding in ; the other layout's
+ /// values come from (or the defaults when null).
+ ///
+ 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;
}
- /// The grid these settings describe in , or null when it is invalid.
- public SupportGrid? GridFor(Units units)
+ /// The bed these settings describe in , or null when it is invalid.
+ 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,
};
}
- /// Gives a newly created nest this grid when enabled (after its units are set).
+ /// Gives a newly created nest this bed when enabled (after its units are set).
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);
}
///
@@ -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(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,
};
diff --git a/OpenNest.IO.Tests/SupportGridPersistenceTests.cs b/OpenNest.IO.Tests/SupportGridPersistenceTests.cs
index 7b62e62c..cc02cf73 100644
--- a/OpenNest.IO.Tests/SupportGridPersistenceTests.cs
+++ b/OpenNest.IO.Tests/SupportGridPersistenceTests.cs
@@ -80,6 +80,35 @@ public class SupportGridPersistenceTests
Assert.Equal(-0.25, bed["offsetY"]!.GetValue());
}
+ [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());
+ Assert.Equal("y", bed["axis"]!.GetValue());
+ Assert.Equal(0.125, bed["thickness"]!.GetValue());
+ 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()
{
diff --git a/OpenNest.IO/NestFormat.cs b/OpenNest.IO/NestFormat.cs
index 346488a7..59446ffc 100644
--- a/OpenNest.IO/NestFormat.cs
+++ b/OpenNest.IO/NestFormat.cs
@@ -110,8 +110,10 @@ namespace OpenNest.IO
}
///
- /// A support bed layout. names the layout: only grid, a regular
- /// grid of contact points, so far. Lengths are in the nest's units.
+ /// A support bed layout. names the layout: grid, a regular grid
+ /// of contact points (pitch and offset along X and Y), or slats, straight slats
+ /// running along (, ,
+ /// ). Lengths are in the nest's units.
///
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
///
public sealed class LenientSupportBedConverter : JsonConverter
{
- 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;
diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs
index 4590f407..5d954e1e 100644
--- a/OpenNest.IO/NestReader.cs
+++ b/OpenNest.IO/NestReader.cs
@@ -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)
{
diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs
index 5719b43d..2d2ff045 100644
--- a/OpenNest.IO/NestWriter.cs
+++ b/OpenNest.IO/NestWriter.cs
@@ -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 BuildDrawingDtos()
{
diff --git a/OpenNest.Tests/Data/SupportGridSettingsTests.cs b/OpenNest.Tests/Data/SupportGridSettingsTests.cs
index fcc45031..e28a187e 100644
--- a/OpenNest.Tests/Data/SupportGridSettingsTests.cs
+++ b/OpenNest.Tests/Data/SupportGridSettingsTests.cs
@@ -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(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(settings.BedFor(Units.Inches));
+ }
+
[Fact]
public void InvalidValues_KeepTheirDefaults_AndBadJsonLoadsAsOff()
{
diff --git a/OpenNest.Tests/Support/SupportSlatsTests.cs b/OpenNest.Tests/Support/SupportSlatsTests.cs
new file mode 100644
index 00000000..3ed10ca7
--- /dev/null
+++ b/OpenNest.Tests/Support/SupportSlatsTests.cs
@@ -0,0 +1,124 @@
+using OpenNest.Geometry;
+using OpenNest.Support;
+
+namespace OpenNest.Tests.Support;
+
+///
+/// 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.
+///
+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(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(slats, new SupportSlats(SlatAxis.X, 4, 0.125, 1, 0.125, 0.25));
+ Assert.NotEqual(slats, new SupportGrid(4, 4, 1, 1, 0.125, 0.25));
+ Assert.Throws(() => new SupportSlats(SlatAxis.Y, 0, 0.125));
+ Assert.Throws(() => new SupportSlats(SlatAxis.Y, 4, -0.125));
+ }
+}
diff --git a/OpenNest/Controls/SupportOverlay.cs b/OpenNest/Controls/SupportOverlay.cs
index 579a0ce9..ac346530 100644
--- a/OpenNest/Controls/SupportOverlay.cs
+++ b/OpenNest/Controls/SupportOverlay.cs
@@ -8,8 +8,8 @@ using OpenNest.Support;
namespace OpenNest.Controls
{
///
- /// View > 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 > 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.
diff --git a/OpenNest/Forms/MachineSettings.cs b/OpenNest/Forms/MachineSettings.cs
index 0b898b70..3bebc018 100644
--- a/OpenNest/Forms/MachineSettings.cs
+++ b/OpenNest/Forms/MachineSettings.cs
@@ -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);
}
///
diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs
index 846b2a44..cb245b4f 100644
--- a/OpenNest/Forms/MainForm.Designer.cs
+++ b/OpenNest/Forms/MainForm.Designer.cs
@@ -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
diff --git a/OpenNest/Forms/MainForm.Nest.cs b/OpenNest/Forms/MainForm.Nest.cs
index cd767953..6de50e37 100644
--- a/OpenNest/Forms/MainForm.Nest.cs
+++ b/OpenNest/Forms/MainForm.Nest.cs
@@ -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();
}
diff --git a/OpenNest/Forms/SupportGridForm.cs b/OpenNest/Forms/SupportGridForm.cs
index c6f216db..22ab9edb 100644
--- a/OpenNest/Forms/SupportGridForm.cs
+++ b/OpenNest/Forms/SupportGridForm.cs
@@ -8,8 +8,9 @@ using OpenNest.Support;
namespace OpenNest.Forms;
///
-/// Nest > 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 > 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.
///
@@ -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();
}
- /// The grid the nest's sheets rest on after OK, or null for none.
- public SupportGrid Grid { get; private set; }
+ /// The bed the nest's sheets rest on after OK, or null for none.
+ 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;
}
diff --git a/docs/nest-file-format.md b/docs/nest-file-format.md
index f4eddbfa..852512bc 100644
--- a/docs/nest-file-format.md
+++ b/docs/nest-file-format.md
@@ -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
diff --git a/docs/support-tabs.md b/docs/support-tabs.md
index b16ca49a..50e21c30 100644
--- a/docs/support-tabs.md
+++ b/docs/support-tabs.md
@@ -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).