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).