diff --git a/OpenNest.Core/Geometry/GeometryOptimizer.cs b/OpenNest.Core/Geometry/GeometryOptimizer.cs index c2b60701..e9ed0ef5 100644 --- a/OpenNest.Core/Geometry/GeometryOptimizer.cs +++ b/OpenNest.Core/Geometry/GeometryOptimizer.cs @@ -10,14 +10,14 @@ namespace OpenNest.Geometry public static void Optimize(IList arcs) => MergePass( arcs, - (list, item, i) => list.GetCoradialArs(item, i), + (a, b) => a.IsCoradialTo(b), (Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined) ); public static void Optimize(IList lines) => MergePass( lines, - (list, item, i) => list.GetCollinearLines(item, i), + (a, b) => a.IsCollinearTo(b), (Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined) ); @@ -61,7 +61,7 @@ namespace OpenNest.Geometry private static void MergePass( IList items, - Func, T, int, List> findCandidates, + Func related, TryJoin tryJoin ) where T : class @@ -69,7 +69,7 @@ namespace OpenNest.Geometry for (var i = 0; i < items.Count; ++i) { var item = items[i]; - var candidates = findCandidates(items, item, i); + var candidates = FindRelated(items, item, i, related); var index = 0; while (index < candidates.Count) @@ -214,62 +214,41 @@ namespace OpenNest.Geometry return true; } - private static List GetCollinearLines( - this IList lines, - Line line, - int startIndex + /// + /// The items from on, other than + /// itself, that are to it. + /// + private static List FindRelated( + IList items, + T item, + int startIndex, + Func related ) + where T : class { - var collinearLines = new List(); + var matches = new List(); Parallel.For( startIndex, - lines.Count, + items.Count, index => { - var compareLine = lines[index]; + var candidate = items[index]; - if (Object.ReferenceEquals(line, compareLine)) + if (Object.ReferenceEquals(item, candidate)) return; - if (!line.IsCollinearTo(compareLine)) + if (!related(item, candidate)) return; - lock (collinearLines) + lock (matches) { - collinearLines.Add(compareLine); + matches.Add(candidate); } } ); - return collinearLines; - } - - private static List GetCoradialArs(this IList arcs, Arc arc, int startIndex) - { - var coradialArcs = new List(); - - Parallel.For( - startIndex, - arcs.Count, - index => - { - var compareArc = arcs[index]; - - if (Object.ReferenceEquals(arc, compareArc)) - return; - - if (!arc.IsCoradialTo(compareArc)) - return; - - lock (coradialArcs) - { - coradialArcs.Add(compareArc); - } - } - ); - - return coradialArcs; + return matches; } } } diff --git a/OpenNest.Core/Geometry/Shape.cs b/OpenNest.Core/Geometry/Shape.cs index e4cf126a..0791efee 100644 --- a/OpenNest.Core/Geometry/Shape.cs +++ b/OpenNest.Core/Geometry/Shape.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -750,64 +750,25 @@ namespace OpenNest.Geometry /// Normalizes to CW winding before offsetting Left (which is outward for CW), /// making the method independent of the original contour winding direction. /// - internal Shape OffsetOutward(double distance) - { - var poly = ToPolygon(); - - if ( - poly == null - || poly.Vertices.Count < 3 - || poly.RotationDirection() == RotationType.CW - ) - return OffsetEntity(distance, OffsetSide.Left) as Shape; - - // Shape is CCW — reverse to CW so Left offset goes outward. - var copy = new Shape(); - - for (var i = Entities.Count - 1; i >= 0; i--) - { - switch (Entities[i]) - { - case Line l: - copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer }); - break; - case Arc a: - copy.Entities.Add( - new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) - { - Layer = a.Layer, - } - ); - break; - case Circle c: - copy.Entities.Add( - new Circle(c.Center, c.Radius) - { - Layer = c.Layer, - Rotation = RotationType.CW, - } - ); - break; - } - } - - return copy.OffsetEntity(distance, OffsetSide.Left) as Shape; - } + internal Shape OffsetOutward(double distance) => + OffsetLeftWithWinding(distance, RotationType.CW); /// /// Offsets the shape inward by the given distance. /// Normalizes to CCW winding before offsetting Left (which is inward for CCW), /// making the method independent of the original contour winding direction. /// - internal Shape OffsetInward(double distance) + internal Shape OffsetInward(double distance) => + OffsetLeftWithWinding(distance, RotationType.CCW); + + /// + /// Offsets Left after normalizing the contour to . + /// + private Shape OffsetLeftWithWinding(double distance, RotationType winding) { var poly = ToPolygon(); - if ( - poly == null - || poly.Vertices.Count < 3 - || poly.RotationDirection() == RotationType.CCW - ) + if (poly == null || poly.Vertices.Count < 3 || poly.RotationDirection() == winding) return OffsetEntity(distance, OffsetSide.Left) as Shape; // Create a reversed copy to avoid mutating shared entity objects. @@ -833,7 +794,7 @@ namespace OpenNest.Geometry new Circle(c.Center, c.Radius) { Layer = c.Layer, - Rotation = RotationType.CCW, + Rotation = winding, } ); break; diff --git a/OpenNest.Core/Geometry/SpatialQuery.cs b/OpenNest.Core/Geometry/SpatialQuery.cs index ed9921e8..17d854b5 100644 --- a/OpenNest.Core/Geometry/SpatialQuery.cs +++ b/OpenNest.Core/Geometry/SpatialQuery.cs @@ -791,7 +791,11 @@ namespace OpenNest.Geometry return vertices.ToArray(); } - private static void AddArcExtremeVertices(HashSet points, Arc arc) + /// + /// Adds the cardinal extreme points of an arc (0°, 90°, 180°, 270°) + /// if they fall within the arc's angular span. + /// + internal static void AddArcExtremeVertices(ICollection points, Arc arc) { var a1 = arc.StartAngle; var a2 = arc.EndAngle; diff --git a/OpenNest.Core/Splitting/DrawingSplitter.cs b/OpenNest.Core/Splitting/DrawingSplitter.cs index bf347e1b..643f5b67 100644 --- a/OpenNest.Core/Splitting/DrawingSplitter.cs +++ b/OpenNest.Core/Splitting/DrawingSplitter.cs @@ -252,13 +252,17 @@ public static class DrawingSplitter && line.Position < bounds.Top - OpenNest.Math.Tolerance.Epsilon; } - private static List BuildClipRegions(List sortedLines, Box bounds) + /// + /// The grid of pieces the split lines cut into, row by row + /// from the bottom-left. + /// + public static List BuildClipRegions(IEnumerable lines, Box bounds) { - var verticals = sortedLines + var verticals = lines .Where(l => l.Axis == CutOffAxis.Vertical) .OrderBy(l => l.Position) .ToList(); - var horizontals = sortedLines + var horizontals = lines .Where(l => l.Axis == CutOffAxis.Horizontal) .OrderBy(l => l.Position) .ToList(); diff --git a/OpenNest.Data/ClampSettings.cs b/OpenNest.Data/ClampSettings.cs index 31629e84..269b4dfe 100644 --- a/OpenNest.Data/ClampSettings.cs +++ b/OpenNest.Data/ClampSettings.cs @@ -216,7 +216,7 @@ public sealed class ClampSettings { json = File.ReadAllText(path); } - catch (Exception ex) when (IsUnreadableFile(ex)) + catch (Exception ex) when (SettingsFile.IsUnreadableFile(ex)) { status = ClampSettingsStatus.Invalid; return Fallback; @@ -247,7 +247,7 @@ public sealed class ClampSettings foreach (var property in document.RootElement.EnumerateObject()) values[property.Name] = property.Value.Clone(); } - catch (Exception ex) when (IsUnreadableFile(ex)) + catch (Exception ex) when (SettingsFile.IsUnreadableFile(ex)) { status = ClampSettingsStatus.Invalid; return settings; @@ -265,7 +265,7 @@ public sealed class ClampSettings if (values.TryGetValue("units", out var units)) { - if (TryParseUnits(units, out var parsed)) + if (SettingsFile.TryParseUnits(units, out var parsed)) settings.Units = parsed; else ignored = true; @@ -347,24 +347,7 @@ public sealed class ClampSettings /// public void Save(string path, int maxRetries = 3) { - var json = ToJson(); - - var directory = Path.GetDirectoryName(Path.GetFullPath(path)); - if (!string.IsNullOrEmpty(directory)) - Directory.CreateDirectory(directory); - - for (var attempt = 0; attempt < maxRetries; attempt++) - { - try - { - File.WriteAllText(path, json); - return; - } - catch (IOException) when (attempt < maxRetries - 1) - { - Thread.Sleep(100); - } - } + SettingsFile.Write(path, ToJson(), maxRetries); } /// The settings as writes them, for the shared copy. @@ -412,35 +395,4 @@ public sealed class ClampSettings && double.IsFinite(value) && double.IsFinite(value * MillimetersPerInch); } - - /// - /// Accepts only a defined unit name (case-insensitive); Enum.TryParse would also accept - /// numeric strings and comma-joined names. - /// - private static bool TryParseUnits(JsonElement element, out Units units) - { - units = default; - if (element.ValueKind != JsonValueKind.String) - return false; - - var text = element.GetString(); - foreach (var candidate in Enum.GetValues()) - { - if (string.Equals(candidate.ToString(), text, StringComparison.OrdinalIgnoreCase)) - { - units = candidate; - return true; - } - } - - return false; - } - - private static bool IsUnreadableFile(Exception ex) => - ex - is JsonException - or IOException - or UnauthorizedAccessException - or NotSupportedException - or System.Security.SecurityException; } diff --git a/OpenNest.Data/EngineSelectionSettings.cs b/OpenNest.Data/EngineSelectionSettings.cs index 30789ba3..d16de080 100644 --- a/OpenNest.Data/EngineSelectionSettings.cs +++ b/OpenNest.Data/EngineSelectionSettings.cs @@ -27,20 +27,9 @@ public sealed class EngineSelectionSettings /// Missing, unreadable or corrupt settings safely use Default without writing. public static EngineSelectionSettings Load(string path) { - if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) - return new(); - - try - { - var settings = JsonSerializer.Deserialize( - File.ReadAllText(path), JsonOptions) ?? new(); - settings.EngineName = NormalizeName(settings.EngineName); - return settings; - } - catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException) - { - return new(); - } + var settings = SettingsFile.Read(path, JsonOptions) ?? new(); + settings.EngineName = NormalizeName(settings.EngineName); + return settings; } /// @@ -81,22 +70,7 @@ public sealed class EngineSelectionSettings { var json = JsonSerializer.Serialize( new EngineSelectionSettings { EngineName = NormalizeName(EngineName) }, JsonOptions); - var directory = Path.GetDirectoryName(Path.GetFullPath(path)); - - for (var attempt = 0; attempt < 3; attempt++) - { - try - { - if (!string.IsNullOrEmpty(directory)) - Directory.CreateDirectory(directory); - File.WriteAllText(path, json); - return; - } - catch (IOException) when (attempt < 2) - { - Thread.Sleep(100); - } - } + SettingsFile.Write(path, json); } private static string NormalizeName(string? name) => diff --git a/OpenNest.Data/LocalJsonProvider.cs b/OpenNest.Data/LocalJsonProvider.cs index 84dabf10..de9c8f9e 100644 --- a/OpenNest.Data/LocalJsonProvider.cs +++ b/OpenNest.Data/LocalJsonProvider.cs @@ -42,7 +42,7 @@ public class LocalJsonProvider : IDataProvider { var json = JsonSerializer.Serialize(machine, JsonOptions); var path = GetPath(machine.Id); - WriteWithRetry(path, json); + SettingsFile.Write(path, json); } public void DeleteMachine(Guid id) @@ -97,20 +97,4 @@ public class LocalJsonProvider : IDataProvider SaveMachine(config); } - - private static void WriteWithRetry(string path, string json, int maxRetries = 3) - { - for (var attempt = 0; attempt < maxRetries; attempt++) - { - try - { - File.WriteAllText(path, json); - return; - } - catch (IOException) when (attempt < maxRetries - 1) - { - Thread.Sleep(100); - } - } - } } diff --git a/OpenNest.Data/NestDefaults.cs b/OpenNest.Data/NestDefaults.cs index 5967f3d5..cf4b78e0 100644 --- a/OpenNest.Data/NestDefaults.cs +++ b/OpenNest.Data/NestDefaults.cs @@ -106,7 +106,7 @@ public sealed class NestDefaults { json = File.ReadAllText(path); } - catch (Exception ex) when (IsUnreadableFile(ex)) + catch (Exception ex) when (SettingsFile.IsUnreadableFile(ex)) { // A file that exists but cannot be read (locked, access-denied) // must never block creating a nest. @@ -129,7 +129,7 @@ public sealed class NestDefaults { dto = JsonSerializer.Deserialize(json ?? "", JsonOptions); } - catch (Exception ex) when (IsUnreadableFile(ex)) + catch (Exception ex) when (SettingsFile.IsUnreadableFile(ex)) { // Defaults that cannot be parsed must never block creating a nest. status = NestDefaultsStatus.Invalid; @@ -144,7 +144,7 @@ public sealed class NestDefaults status = NestDefaultsStatus.Ok; - if (TryParseUnits(dto.Units, out var units)) + if (SettingsFile.TryParseUnits(dto.Units, out var units)) defaults.Units = units; if ( @@ -261,24 +261,7 @@ public sealed class NestDefaults /// public void Save(string path, int maxRetries = 3) { - var json = ToJson(); - - var directory = Path.GetDirectoryName(Path.GetFullPath(path)); - if (!string.IsNullOrEmpty(directory)) - Directory.CreateDirectory(directory); - - for (var attempt = 0; attempt < maxRetries; attempt++) - { - try - { - File.WriteAllText(path, json); - return; - } - catch (IOException) when (attempt < maxRetries - 1) - { - Thread.Sleep(100); - } - } + SettingsFile.Write(path, ToJson(), maxRetries); } /// The defaults as writes them, for the shared copy. @@ -304,38 +287,6 @@ public sealed class NestDefaults return JsonSerializer.Serialize(dto, JsonOptions); } - /// - /// Accepts only a defined unit name (case-insensitive). Enum.TryParse - /// would also accept numeric strings ("7") and comma-joined names, - /// producing undefined or unintended values. - /// - private static bool TryParseUnits(JsonElement? element, out Units units) - { - units = default; - if (element is not { ValueKind: JsonValueKind.String } value) - return false; - - var text = value.GetString(); - foreach (var candidate in Enum.GetValues()) - { - if (string.Equals(candidate.ToString(), text, StringComparison.OrdinalIgnoreCase)) - { - units = candidate; - return true; - } - } - - return false; - } - - private static bool IsUnreadableFile(Exception ex) => - ex - is JsonException - or IOException - or UnauthorizedAccessException - or NotSupportedException - or System.Security.SecurityException; - private static bool IsValidSize(double width, double length) => !double.IsNaN(width) && !double.IsNaN(length) diff --git a/OpenNest.Data/NestStorageSettings.cs b/OpenNest.Data/NestStorageSettings.cs index f15cf193..917c2fef 100644 --- a/OpenNest.Data/NestStorageSettings.cs +++ b/OpenNest.Data/NestStorageSettings.cs @@ -51,20 +51,9 @@ public sealed class NestStorageSettings public static NestStorageSettings Load(string path) { - if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) - return new(); - - try - { - var settings = JsonSerializer.Deserialize( - File.ReadAllText(path), JsonOptions) ?? new(); - settings.ServerUrl = NormalizeUrl(settings.ServerUrl); - return settings; - } - catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException) - { - return new(); - } + var settings = SettingsFile.Read(path, JsonOptions) ?? new(); + settings.ServerUrl = NormalizeUrl(settings.ServerUrl); + return settings; } /// Writes camelCase JSON, creating the parent directory and retrying IO collisions. @@ -73,22 +62,7 @@ public sealed class NestStorageSettings var json = JsonSerializer.Serialize( new NestStorageSettings { Mode = Mode, ServerUrl = NormalizeUrl(ServerUrl) }, JsonOptions); - var directory = Path.GetDirectoryName(Path.GetFullPath(path)); - - for (var attempt = 0; attempt < 3; attempt++) - { - try - { - if (!string.IsNullOrEmpty(directory)) - Directory.CreateDirectory(directory); - File.WriteAllText(path, json); - return; - } - catch (IOException) when (attempt < 2) - { - Thread.Sleep(100); - } - } + SettingsFile.Write(path, json); } private static string NormalizeUrl(string? url) => (url ?? "").Trim().TrimEnd('/'); diff --git a/OpenNest.Data/SettingsFile.cs b/OpenNest.Data/SettingsFile.cs new file mode 100644 index 00000000..d34dbcd4 --- /dev/null +++ b/OpenNest.Data/SettingsFile.cs @@ -0,0 +1,81 @@ +using System.Text.Json; + +namespace OpenNest.Data; + +/// File handling shared by the JSON settings files under %APPDATA%\OpenNest. +internal static class SettingsFile +{ + /// + /// Writes the file, creating the parent directory, and retries briefly on IO collisions. + /// The last failure propagates; the host handles a persistent write failure. + /// + public static void Write(string path, string json, int maxRetries = 3) + { + var directory = Path.GetDirectoryName(Path.GetFullPath(path)); + + for (var attempt = 0; attempt < maxRetries; attempt++) + { + try + { + if (!string.IsNullOrEmpty(directory)) + Directory.CreateDirectory(directory); + File.WriteAllText(path, json); + return; + } + catch (IOException) when (attempt < maxRetries - 1) + { + Thread.Sleep(100); + } + } + } + + /// + /// Deserializes the file, or returns null when it is missing, unreadable or corrupt. + /// + public static T? Read(string path, JsonSerializerOptions options) + where T : class + { + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + return null; + + try + { + return JsonSerializer.Deserialize(File.ReadAllText(path), options); + } + catch (Exception ex) when (IsUnreadableFile(ex)) + { + return null; + } + } + + public static bool IsUnreadableFile(Exception ex) => + ex + is JsonException + or IOException + or UnauthorizedAccessException + or NotSupportedException + or System.Security.SecurityException; + + /// + /// Accepts only a defined unit name (case-insensitive). Enum.TryParse would also accept + /// numeric strings ("7") and comma-joined names, producing undefined or unintended values. + /// + public static bool TryParseUnits(JsonElement? element, out Units units) + { + units = default; + if (element is not { ValueKind: JsonValueKind.String } value) + return false; + + var text = value.GetString(); + foreach (var candidate in Enum.GetValues()) + { + if (string.Equals(candidate.ToString(), text, StringComparison.OrdinalIgnoreCase)) + { + units = candidate; + return true; + } + } + + return false; + } +} diff --git a/OpenNest.Engine/BestFit/BestFitFinder.cs b/OpenNest.Engine/BestFit/BestFitFinder.cs index fdc2b535..29da0a74 100644 --- a/OpenNest.Engine/BestFit/BestFitFinder.cs +++ b/OpenNest.Engine/BestFit/BestFitFinder.cs @@ -191,7 +191,7 @@ namespace OpenNest.Engine.BestFit { points.Add(arc.StartPoint()); points.Add(arc.EndPoint()); - AddArcExtremes(points, arc); + SpatialQuery.AddArcExtremeVertices(points, arc); } } } @@ -220,35 +220,6 @@ namespace OpenNest.Engine.BestFit return hullAngles; } - /// - /// Adds the cardinal extreme points of an arc (0°, 90°, 180°, 270°) - /// if they fall within the arc's angular span. - /// - private static void AddArcExtremes(List points, Arc arc) - { - var a1 = arc.StartAngle; - var a2 = arc.EndAngle; - - if (arc.IsReversed) - Generic.Swap(ref a1, ref a2); - - // Right (0°) - if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2)) - points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y)); - - // Top (90°) - if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2)) - points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius)); - - // Left (180°) - if (Angle.IsBetweenRad(System.Math.PI, a1, a2)) - points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y)); - - // Bottom (270°) - if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2)) - points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius)); - } - /// /// Minimum angular separation (radians) between hull-derived rotation candidates. /// Tessellated arcs produce many hull edges with nearly identical angles; diff --git a/OpenNest.Engine/Fill/HorizontalRemnantComparer.cs b/OpenNest.Engine/Fill/HorizontalRemnantComparer.cs index 510de13d..0d981a8c 100644 --- a/OpenNest.Engine/Fill/HorizontalRemnantComparer.cs +++ b/OpenNest.Engine/Fill/HorizontalRemnantComparer.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using OpenNest.Geometry; -using OpenNest.Math; namespace OpenNest.Engine.Fill { @@ -8,44 +6,10 @@ namespace OpenNest.Engine.Fill /// Ranks fill results to minimize Y-extent (preserve top-side horizontal remnant). /// Tiebreak chain: count > smallest Y-extent > highest density. /// - public class HorizontalRemnantComparer : IFillComparer + public class HorizontalRemnantComparer : RemnantComparer { - public bool IsBetter(List candidate, List current, Box workArea) - { - if (candidate == null || candidate.Count == 0) - return false; + protected override double Low(Box box) => box.Bottom; - if (current == null || current.Count == 0) - return true; - - if (candidate.Count != current.Count) - return candidate.Count > current.Count; - - var candExtent = YExtent(candidate); - var currExtent = YExtent(current); - - if (!candExtent.IsEqualTo(currExtent)) - return candExtent < currExtent; - - return FillScore.Compute(candidate, workArea).Density - > FillScore.Compute(current, workArea).Density; - } - - private static double YExtent(List parts) - { - var minY = double.MaxValue; - var maxY = double.MinValue; - - foreach (var part in parts) - { - var bb = part.BoundingBox; - if (bb.Bottom < minY) - minY = bb.Bottom; - if (bb.Top > maxY) - maxY = bb.Top; - } - - return maxY - minY; - } + protected override double High(Box box) => box.Top; } } diff --git a/OpenNest.Engine/Fill/RemnantComparer.cs b/OpenNest.Engine/Fill/RemnantComparer.cs new file mode 100644 index 00000000..cfcd5bad --- /dev/null +++ b/OpenNest.Engine/Fill/RemnantComparer.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest.Engine.Fill +{ + /// + /// Ranks fill results to keep one side of the work area free for a remnant. + /// Tiebreak chain: count > smallest extent along the ranked axis > highest density. + /// + public abstract class RemnantComparer : IFillComparer + { + /// The lower edge of a bounding box along the ranked axis. + protected abstract double Low(Box box); + + /// The upper edge of a bounding box along the ranked axis. + protected abstract double High(Box box); + + public bool IsBetter(List candidate, List current, Box workArea) + { + if (candidate == null || candidate.Count == 0) + return false; + + if (current == null || current.Count == 0) + return true; + + if (candidate.Count != current.Count) + return candidate.Count > current.Count; + + var candExtent = Extent(candidate); + var currExtent = Extent(current); + + if (!candExtent.IsEqualTo(currExtent)) + return candExtent < currExtent; + + return FillScore.Compute(candidate, workArea).Density + > FillScore.Compute(current, workArea).Density; + } + + private double Extent(List parts) + { + var min = double.MaxValue; + var max = double.MinValue; + + foreach (var part in parts) + { + var bb = part.BoundingBox; + var low = Low(bb); + var high = High(bb); + if (low < min) + min = low; + if (high > max) + max = high; + } + + return max - min; + } + } +} diff --git a/OpenNest.Engine/Fill/VerticalRemnantComparer.cs b/OpenNest.Engine/Fill/VerticalRemnantComparer.cs index 534b9187..3c1e30f5 100644 --- a/OpenNest.Engine/Fill/VerticalRemnantComparer.cs +++ b/OpenNest.Engine/Fill/VerticalRemnantComparer.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using OpenNest.Geometry; -using OpenNest.Math; namespace OpenNest.Engine.Fill { @@ -8,44 +6,10 @@ namespace OpenNest.Engine.Fill /// Ranks fill results to minimize X-extent (preserve right-side vertical remnant). /// Tiebreak chain: count > smallest X-extent > highest density. /// - public class VerticalRemnantComparer : IFillComparer + public class VerticalRemnantComparer : RemnantComparer { - public bool IsBetter(List candidate, List current, Box workArea) - { - if (candidate == null || candidate.Count == 0) - return false; + protected override double Low(Box box) => box.Left; - if (current == null || current.Count == 0) - return true; - - if (candidate.Count != current.Count) - return candidate.Count > current.Count; - - var candExtent = XExtent(candidate); - var currExtent = XExtent(current); - - if (!candExtent.IsEqualTo(currExtent)) - return candExtent < currExtent; - - return FillScore.Compute(candidate, workArea).Density - > FillScore.Compute(current, workArea).Density; - } - - private static double XExtent(List parts) - { - var minX = double.MaxValue; - var maxX = double.MinValue; - - foreach (var part in parts) - { - var bb = part.BoundingBox; - if (bb.Left < minX) - minX = bb.Left; - if (bb.Right > maxX) - maxX = bb.Right; - } - - return maxX - minX; - } + protected override double High(Box box) => box.Right; } } diff --git a/OpenNest/Forms/ClampsForm.cs b/OpenNest/Forms/ClampsForm.cs index 395870f3..e04dd0a0 100644 --- a/OpenNest/Forms/ClampsForm.cs +++ b/OpenNest/Forms/ClampsForm.cs @@ -83,12 +83,12 @@ public sealed class ClampsForm : Form ); var size = new TableLayoutPanel { AutoSize = true, ColumnCount = 3, Dock = DockStyle.Top }; - widthBox = NewLengthBox(Editor.Width, minimum: 0.0001m); - depthBox = NewLengthBox(Editor.Depth, minimum: 0.0001m); - removalMinutesBox = NewLengthBox(Editor.RemovalMinutes, minimum: 0m); - AddRow(size, "Clamp width (along the edge):", widthBox, unit); - AddRow(size, "Clamp depth (into the plate):", depthBox, unit); - AddRow(size, "Minutes to remove and replace one clamp:", removalMinutesBox, "min"); + widthBox = SettingsRows.NewLengthBox(Editor.Width, minimum: 0.0001m); + depthBox = SettingsRows.NewLengthBox(Editor.Depth, minimum: 0.0001m); + removalMinutesBox = SettingsRows.NewLengthBox(Editor.RemovalMinutes, minimum: 0m); + SettingsRows.AddRow(size, "Clamp width (along the edge):", widthBox, unit); + SettingsRows.AddRow(size, "Clamp depth (into the plate):", depthBox, unit); + SettingsRows.AddRow(size, "Minutes to remove and replace one clamp:", removalMinutesBox, "min"); ownClearanceBox = new CheckBox { Text = "Clearance from parts:", @@ -96,7 +96,7 @@ public sealed class ClampsForm : Form Checked = Editor.Clearance.HasValue, Padding = new Padding(0, 3, 6, 0), }; - clearanceBox = NewLengthBox(Editor.Clearance ?? 0, minimum: 0m); + clearanceBox = SettingsRows.NewLengthBox(Editor.Clearance ?? 0, minimum: 0m); clearanceBox.Enabled = ownClearanceBox.Checked; ownClearanceBox.CheckedChanged += (_, _) => clearanceBox.Enabled = ownClearanceBox.Checked; size.Controls.Add(ownClearanceBox); @@ -117,7 +117,7 @@ public sealed class ClampsForm : Form AutoSize = true, WrapContents = false, }; - centerBox = NewLengthBox(0, minimum: 0m); + centerBox = SettingsRows.NewLengthBox(0, minimum: 0m); edgeBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 120 }; foreach (var edge in ClampEdgeText.All) edgeBox.Items.Add(ClampEdgeText.Name(edge)); @@ -209,34 +209,6 @@ public sealed class ClampsForm : Form /// The edited rail; the caller applies it to the nest after OK. internal ClampRailEditor Editor { get; } - private static NumericUpDown NewLengthBox(double value, decimal minimum) - { - var box = new NumericUpDown - { - DecimalPlaces = 4, - Minimum = minimum, - Maximum = 100000, - Increment = 0.125m, - Width = 110, - }; - box.Value = ToBoxValue(box, value); - return box; - } - - private static decimal ToBoxValue(NumericUpDown box, double value) - { - if (!double.IsFinite(value) || value < (double)box.Minimum) - return box.Minimum; - return value > (double)box.Maximum ? box.Maximum : (decimal)value; - } - - private static void AddRow(TableLayoutPanel table, string label, Control box, string unit) - { - table.Controls.Add(new Label { Text = label, AutoSize = true, Padding = new Padding(0, 6, 6, 0) }); - table.Controls.Add(box); - table.Controls.Add(new Label { Text = unit, AutoSize = true, Padding = new Padding(0, 6, 0, 0) }); - } - private void ReadSizes() { Editor.Width = (double)widthBox.Value; @@ -282,11 +254,11 @@ public sealed class ClampsForm : Form private void LoadMachineDefaults() { Editor.LoadMachineDefaults(machineDefaults); - widthBox.Value = ToBoxValue(widthBox, Editor.Width); - depthBox.Value = ToBoxValue(depthBox, Editor.Depth); - removalMinutesBox.Value = ToBoxValue(removalMinutesBox, Editor.RemovalMinutes); + widthBox.Value = SettingsRows.ToBoxValue(widthBox, Editor.Width); + depthBox.Value = SettingsRows.ToBoxValue(depthBox, Editor.Depth); + removalMinutesBox.Value = SettingsRows.ToBoxValue(removalMinutesBox, Editor.RemovalMinutes); ownClearanceBox.Checked = Editor.Clearance.HasValue; - clearanceBox.Value = ToBoxValue(clearanceBox, Editor.Clearance ?? 0); + clearanceBox.Value = SettingsRows.ToBoxValue(clearanceBox, Editor.Clearance ?? 0); applyToNewNestsBox.Checked = Editor.ApplyToNewNests; RefreshCenters(); } diff --git a/OpenNest/Forms/SettingsRows.cs b/OpenNest/Forms/SettingsRows.cs new file mode 100644 index 00000000..7f338784 --- /dev/null +++ b/OpenNest/Forms/SettingsRows.cs @@ -0,0 +1,39 @@ +using System.Windows.Forms; + +namespace OpenNest.Forms; + +/// +/// Label / value box / unit rows shared by the machine-settings dialogs built in code +/// (Clamps, Support Grid). +/// +internal static class SettingsRows +{ + public static NumericUpDown NewLengthBox(double value, decimal minimum) + { + var box = new NumericUpDown + { + DecimalPlaces = 4, + Minimum = minimum, + Maximum = 100000, + Increment = 0.125m, + Width = 110, + }; + box.Value = ToBoxValue(box, value); + return box; + } + + /// The value clamped to the box's range; non-finite values become the minimum. + public static decimal ToBoxValue(NumericUpDown box, double value) + { + if (!double.IsFinite(value) || value < (double)box.Minimum) + return box.Minimum; + return value > (double)box.Maximum ? box.Maximum : (decimal)value; + } + + public static void AddRow(TableLayoutPanel table, string label, Control box, string unit) + { + table.Controls.Add(new Label { Text = label, AutoSize = true, Padding = new Padding(0, 6, 6, 0) }); + table.Controls.Add(box); + table.Controls.Add(new Label { Text = unit, AutoSize = true, Padding = new Padding(0, 6, 0, 0) }); + } +} diff --git a/OpenNest/Forms/SplitDrawingForm.cs b/OpenNest/Forms/SplitDrawingForm.cs index 11e1e639..9b480bbb 100644 --- a/OpenNest/Forms/SplitDrawingForm.cs +++ b/OpenNest/Forms/SplitDrawingForm.cs @@ -581,39 +581,8 @@ public partial class SplitDrawingForm : Form Color.FromArgb(40, 128, 222, 234), }; - private List BuildPreviewRegions() - { - var verticals = _splitLines - .Where(l => l.Axis == CutOffAxis.Vertical) - .OrderBy(l => l.Position) - .ToList(); - var horizontals = _splitLines - .Where(l => l.Axis == CutOffAxis.Horizontal) - .OrderBy(l => l.Position) - .ToList(); - - var xEdges = new List { _drawingBounds.Left }; - xEdges.AddRange(verticals.Select(v => v.Position)); - xEdges.Add(_drawingBounds.Right); - - var yEdges = new List { _drawingBounds.Bottom }; - yEdges.AddRange(horizontals.Select(h => h.Position)); - yEdges.Add(_drawingBounds.Top); - - var regions = new List(); - for (var yi = 0; yi < yEdges.Count - 1; yi++) - for (var xi = 0; xi < xEdges.Count - 1; xi++) - regions.Add( - new Box( - xEdges[xi], - yEdges[yi], - xEdges[xi + 1] - xEdges[xi], - yEdges[yi + 1] - yEdges[yi] - ) - ); - - return regions; - } + private List BuildPreviewRegions() => + DrawingSplitter.BuildClipRegions(_splitLines, _drawingBounds); // --- OK/Cancel --- diff --git a/OpenNest/Forms/SupportGridForm.cs b/OpenNest/Forms/SupportGridForm.cs index 0d7dcddc..7c429ca3 100644 --- a/OpenNest/Forms/SupportGridForm.cs +++ b/OpenNest/Forms/SupportGridForm.cs @@ -80,34 +80,34 @@ public sealed class SupportGridForm : Form 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); + pitchXBox = SettingsRows.NewLengthBox(initial.PitchX, 0.0001m); + pitchYBox = SettingsRows.NewLengthBox(initial.PitchY, 0.0001m); + offsetXBox = SettingsRows.NewLengthBox(initial.OffsetX, -100000m); + offsetYBox = SettingsRows.NewLengthBox(initial.OffsetY, -100000m); + SettingsRows.AddRow(gridValues, "Pitch along X:", pitchXBox, unit); + SettingsRows.AddRow(gridValues, "Pitch along Y:", pitchYBox, unit); + SettingsRows.AddRow(gridValues, "First point, X from the origin:", offsetXBox, unit); + SettingsRows.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); + slatPitchBox = SettingsRows.NewLengthBox(initial.SlatPitch, 0.0001m); + slatThicknessBox = SettingsRows.NewLengthBox(initial.SlatThickness, 0m); + slatOffsetBox = SettingsRows.NewLengthBox(initial.SlatOffset, -100000m); + SettingsRows.AddRow(slatValues, "Slats run:", slatAxisBox, ""); + SettingsRows.AddRow(slatValues, "Pitch between slats:", slatPitchBox, unit); + SettingsRows.AddRow(slatValues, "Slat thickness:", slatThicknessBox, unit); + SettingsRows.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(sharedValues, "Registration tolerance:", toleranceBox, unit); - AddRow(sharedValues, "Stability margin:", marginBox, unit); + toleranceBox = SettingsRows.NewLengthBox(initial.RegistrationTolerance, 0m); + marginBox = SettingsRows.NewLengthBox(initial.StabilityMargin, 0m); + SettingsRows.AddRow(sharedValues, "Registration tolerance:", toleranceBox, unit); + SettingsRows.AddRow(sharedValues, "Stability margin:", marginBox, unit); layout.Controls.Add(sharedValues); var defaultsGroup = new GroupBox @@ -211,18 +211,18 @@ public sealed class SupportGridForm : Form private void ShowValues(SupportGridSettings settings) { - pitchXBox.Value = ToBoxValue(pitchXBox, settings.PitchX); - pitchYBox.Value = ToBoxValue(pitchYBox, settings.PitchY); - offsetXBox.Value = ToBoxValue(offsetXBox, settings.OffsetX); - offsetYBox.Value = ToBoxValue(offsetYBox, settings.OffsetY); + pitchXBox.Value = SettingsRows.ToBoxValue(pitchXBox, settings.PitchX); + pitchYBox.Value = SettingsRows.ToBoxValue(pitchYBox, settings.PitchY); + offsetXBox.Value = SettingsRows.ToBoxValue(offsetXBox, settings.OffsetX); + offsetYBox.Value = SettingsRows.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); + slatPitchBox.Value = SettingsRows.ToBoxValue(slatPitchBox, settings.SlatPitch); + slatThicknessBox.Value = SettingsRows.ToBoxValue(slatThicknessBox, settings.SlatThickness); + slatOffsetBox.Value = SettingsRows.ToBoxValue(slatOffsetBox, settings.SlatOffset); + toleranceBox.Value = SettingsRows.ToBoxValue(toleranceBox, settings.RegistrationTolerance); + marginBox.Value = SettingsRows.ToBoxValue(marginBox, settings.StabilityMargin); applyToNewNestsBox.Checked = settings.Enabled; } @@ -266,32 +266,4 @@ public sealed class SupportGridForm : Form } DialogResult = DialogResult.OK; } - - private static NumericUpDown NewLengthBox(double value, decimal minimum) - { - var box = new NumericUpDown - { - DecimalPlaces = 4, - Minimum = minimum, - Maximum = 100000, - Increment = 0.125m, - Width = 110, - }; - box.Value = ToBoxValue(box, value); - return box; - } - - private static decimal ToBoxValue(NumericUpDown box, double value) - { - if (!double.IsFinite(value) || value < (double)box.Minimum) - return box.Minimum; - return value > (double)box.Maximum ? box.Maximum : (decimal)value; - } - - private static void AddRow(TableLayoutPanel table, string label, Control box, string unit) - { - table.Controls.Add(new Label { Text = label, AutoSize = true, Padding = new Padding(0, 6, 6, 0) }); - table.Controls.Add(box); - table.Controls.Add(new Label { Text = unit, AutoSize = true, Padding = new Padding(0, 6, 0, 0) }); - } }