refactor: merge duplicated geometry, settings-file and form helpers

Roslyn Bridge flagged these as copy-paste clusters; each pair now shares
one implementation so a fix lands everywhere at once:

- Shape.OffsetInward/OffsetOutward differed only in the target winding.
- BestFitFinder kept its own copy of SpatialQuery's arc-extreme points.
- SplitDrawingForm rebuilt DrawingSplitter's clip-region grid for preview.
- GeometryOptimizer's collinear/coradial finders (one misspelled
  GetCoradialArs) become one generic FindRelated.
- Five settings files repeated the write-with-retry loop, and two also
  repeated the unit parser and unreadable-file filter (SettingsFile).
- ClampsForm and SupportGridForm repeated their row/box builders.
- Vertical/HorizontalRemnantComparer share a RemnantComparer base.

Behavior is unchanged except that Engine/NestStorage settings loads now
also tolerate NotSupported/Security exceptions, matching the other
settings files' never-throw contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-30 12:59:47 -04:00
1 parent 4028dafb26
commit b550640ef0
18 files changed
+291 -517

No files matched your search

+22 -43
View File
@@ -10,14 +10,14 @@ namespace OpenNest.Geometry
public static void Optimize(IList<Arc> 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<Line> 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<T>(
IList<T> items,
Func<IList<T>, T, int, List<T>> findCandidates,
Func<T, T, bool> related,
TryJoin<T> 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<Line> GetCollinearLines(
this IList<Line> lines,
Line line,
int startIndex
/// <summary>
/// The items from <paramref name="startIndex"/> on, other than <paramref name="item"/>
/// itself, that are <paramref name="related"/> to it.
/// </summary>
private static List<T> FindRelated<T>(
IList<T> items,
T item,
int startIndex,
Func<T, T, bool> related
)
where T : class
{
var collinearLines = new List<Line>();
var matches = new List<T>();
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<Arc> GetCoradialArs(this IList<Arc> arcs, Arc arc, int startIndex)
{
var coradialArcs = new List<Arc>();
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;
}
}
}
+12 -51
View File
@@ -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.
/// </summary>
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);
/// <summary>
/// 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.
/// </summary>
internal Shape OffsetInward(double distance)
internal Shape OffsetInward(double distance) =>
OffsetLeftWithWinding(distance, RotationType.CCW);
/// <summary>
/// Offsets Left after normalizing the contour to <paramref name="winding"/>.
/// </summary>
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;
+5 -1
View File
@@ -791,7 +791,11 @@ namespace OpenNest.Geometry
return vertices.ToArray();
}
private static void AddArcExtremeVertices(HashSet<Vector> points, Arc arc)
/// <summary>
/// Adds the cardinal extreme points of an arc (0°, 90°, 180°, 270°)
/// if they fall within the arc's angular span.
/// </summary>
internal static void AddArcExtremeVertices(ICollection<Vector> points, Arc arc)
{
var a1 = arc.StartAngle;
var a2 = arc.EndAngle;
+7 -3
View File
@@ -252,13 +252,17 @@ public static class DrawingSplitter
&& line.Position < bounds.Top - OpenNest.Math.Tolerance.Epsilon;
}
private static List<Box> BuildClipRegions(List<SplitLine> sortedLines, Box bounds)
/// <summary>
/// The grid of pieces the split lines cut <paramref name="bounds"/> into, row by row
/// from the bottom-left.
/// </summary>
public static List<Box> BuildClipRegions(IEnumerable<SplitLine> 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();
+4 -52
View File
@@ -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
/// </summary>
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);
}
/// <summary>The settings as <see cref="Save"/> writes them, for the shared copy.</summary>
@@ -412,35 +395,4 @@ public sealed class ClampSettings
&& double.IsFinite(value)
&& double.IsFinite(value * MillimetersPerInch);
}
/// <summary>
/// Accepts only a defined unit name (case-insensitive); Enum.TryParse would also accept
/// numeric strings and comma-joined names.
/// </summary>
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<Units>())
{
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;
}
+4 -30
View File
@@ -27,20 +27,9 @@ public sealed class EngineSelectionSettings
/// <summary>Missing, unreadable or corrupt settings safely use Default without writing.</summary>
public static EngineSelectionSettings Load(string path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return new();
try
{
var settings = JsonSerializer.Deserialize<EngineSelectionSettings>(
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<EngineSelectionSettings>(path, JsonOptions) ?? new();
settings.EngineName = NormalizeName(settings.EngineName);
return settings;
}
/// <summary>
@@ -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) =>
+1 -17
View File
@@ -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);
}
}
}
}
+4 -53
View File
@@ -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<NestDefaultsDto>(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
/// </summary>
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);
}
/// <summary>The defaults as <see cref="Save"/> writes them, for the shared copy.</summary>
@@ -304,38 +287,6 @@ public sealed class NestDefaults
return JsonSerializer.Serialize(dto, JsonOptions);
}
/// <summary>
/// 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.
/// </summary>
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<Units>())
{
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)
+4 -30
View File
@@ -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<NestStorageSettings>(
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<NestStorageSettings>(path, JsonOptions) ?? new();
settings.ServerUrl = NormalizeUrl(settings.ServerUrl);
return settings;
}
/// <summary>Writes camelCase JSON, creating the parent directory and retrying IO collisions.</summary>
@@ -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('/');
+81
View File
@@ -0,0 +1,81 @@
using System.Text.Json;
namespace OpenNest.Data;
/// <summary>File handling shared by the JSON settings files under %APPDATA%\OpenNest.</summary>
internal static class SettingsFile
{
/// <summary>
/// Writes the file, creating the parent directory, and retries briefly on IO collisions.
/// The last failure propagates; the host handles a persistent write failure.
/// </summary>
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);
}
}
}
/// <summary>
/// Deserializes the file, or returns null when it is missing, unreadable or corrupt.
/// </summary>
public static T? Read<T>(string path, JsonSerializerOptions options)
where T : class
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return null;
try
{
return JsonSerializer.Deserialize<T>(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;
/// <summary>
/// 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.
/// </summary>
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<Units>())
{
if (string.Equals(candidate.ToString(), text, StringComparison.OrdinalIgnoreCase))
{
units = candidate;
return true;
}
}
return false;
}
}
+1 -30
View File
@@ -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;
}
/// <summary>
/// Adds the cardinal extreme points of an arc (0°, 90°, 180°, 270°)
/// if they fall within the arc's angular span.
/// </summary>
private static void AddArcExtremes(List<Vector> 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));
}
/// <summary>
/// Minimum angular separation (radians) between hull-derived rotation candidates.
/// Tessellated arcs produce many hull edges with nearly identical angles;
@@ -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.
/// </summary>
public class HorizontalRemnantComparer : IFillComparer
public class HorizontalRemnantComparer : RemnantComparer
{
public bool IsBetter(List<Part> candidate, List<Part> 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<Part> 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;
}
}
+59
View File
@@ -0,0 +1,59 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.Fill
{
/// <summary>
/// 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.
/// </summary>
public abstract class RemnantComparer : IFillComparer
{
/// <summary>The lower edge of a bounding box along the ranked axis.</summary>
protected abstract double Low(Box box);
/// <summary>The upper edge of a bounding box along the ranked axis.</summary>
protected abstract double High(Box box);
public bool IsBetter(List<Part> candidate, List<Part> 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<Part> 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;
}
}
}
@@ -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.
/// </summary>
public class VerticalRemnantComparer : IFillComparer
public class VerticalRemnantComparer : RemnantComparer
{
public bool IsBetter(List<Part> candidate, List<Part> 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<Part> 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;
}
}
+12 -40
View File
@@ -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
/// <summary>The edited rail; the caller applies it to the nest after OK.</summary>
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();
}
+39
View File
@@ -0,0 +1,39 @@
using System.Windows.Forms;
namespace OpenNest.Forms;
/// <summary>
/// Label / value box / unit rows shared by the machine-settings dialogs built in code
/// (Clamps, Support Grid).
/// </summary>
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;
}
/// <summary>The value clamped to the box's range; non-finite values become the minimum.</summary>
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) });
}
}
+2 -33
View File
@@ -581,39 +581,8 @@ public partial class SplitDrawingForm : Form
Color.FromArgb(40, 128, 222, 234),
};
private List<Box> 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<double> { _drawingBounds.Left };
xEdges.AddRange(verticals.Select(v => v.Position));
xEdges.Add(_drawingBounds.Right);
var yEdges = new List<double> { _drawingBounds.Bottom };
yEdges.AddRange(horizontals.Select(h => h.Position));
yEdges.Add(_drawingBounds.Top);
var regions = new List<Box>();
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<Box> BuildPreviewRegions() =>
DrawingSplitter.BuildClipRegions(_splitLines, _drawingBounds);
// --- OK/Cancel ---
+28 -56
View File
@@ -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) });
}
}