diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000..6e2e244
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "csharpier": {
+ "version": "1.3.0",
+ "commands": [
+ "csharpier"
+ ],
+ "rollForward": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..8083617
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,68 @@
+# Unified code style for OpenNest.
+# Canonical formatter: CSharpier (see .config/dotnet-tools.json).
+# dotnet tool restore
+# dotnet csharpier check . # verify before committing
+# dotnet csharpier format . # fix
+# These settings match CSharpier's conventions so IDE auto-formatting
+# (VS / Rider / VS Code) stays consistent with the formatter.
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{csproj,props,targets,xml,config,manifest}]
+indent_style = space
+indent_size = 2
+
+[*.{json,yml,yaml}]
+indent_style = space
+indent_size = 2
+
+[*.{cs,csx}]
+indent_style = space
+indent_size = 4
+# CSharpier wraps code at 100 columns; dotnet format cannot re-wrap, but
+# IDEs surface a visual guide and analyzers can flag hard violations.
+max_line_length = 100
+
+# --- Using directives (System first, outside the namespace) ---
+dotnet_sort_system_directives_first = true
+csharp_using_directive_placement = outside_namespace:warning
+
+# --- Brace placement: Allman (opening brace on its own line) ---
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# --- Spacing ---
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_after_cast = false
+csharp_space_after_comma = true
+csharp_space_before_comma = false
+
+# --- Code style preferences ---
+# Project rule: always use var for locals (see CLAUDE.md).
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+csharp_prefer_braces = true:suggestion
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:silent
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
diff --git a/OpenNest.IO/Bending/BendRepair.cs b/OpenNest.IO/Bending/BendRepair.cs
index 5c039af..9c96ccc 100644
--- a/OpenNest.IO/Bending/BendRepair.cs
+++ b/OpenNest.IO/Bending/BendRepair.cs
@@ -6,7 +6,12 @@ using OpenNest.Geometry;
namespace OpenNest.IO.Bending
{
- public enum BendRepairUnits { Unspecified, Inches, Millimeters }
+ public enum BendRepairUnits
+ {
+ Unspecified,
+ Inches,
+ Millimeters,
+ }
/// Explicit opt-in. Distances are physical millimeters, not drawing coordinates.
public sealed class BendRepairOptions
@@ -15,28 +20,49 @@ namespace OpenNest.IO.Bending
public double MaxEndpointMovementMillimeters { get; set; }
}
- public sealed record BendRepairReport(int BendIndex, string Status, string Reason,
- Vector OriginalStart, Vector OriginalEnd, Vector Start, Vector End);
+ public sealed record BendRepairReport(
+ int BendIndex,
+ string Status,
+ string Reason,
+ Vector OriginalStart,
+ Vector OriginalEnd,
+ Vector Start,
+ Vector End
+ );
/// Conservative, atomic repair. Never edits cut entities or unassociated marks.
public static class BendRepair
{
- public static List Apply(List entities, List bends, BendRepairOptions options)
+ public static List Apply(
+ List entities,
+ List bends,
+ BendRepairOptions options
+ )
{
var reports = new List();
- if (options == null) return reports;
+ if (options == null)
+ return reports;
var scale = options.DrawingUnits == BendRepairUnits.Inches ? 1 / 25.4 : 1.0;
var tolerance = 0.001 * scale;
var limit = options.MaxEndpointMovementMillimeters * scale;
- var valid = (options.DrawingUnits == BendRepairUnits.Inches || options.DrawingUnits == BendRepairUnits.Millimeters)
- && double.IsFinite(limit) && limit > tolerance && options.MaxEndpointMovementMillimeters <= 3.175;
+ var valid =
+ (
+ options.DrawingUnits == BendRepairUnits.Inches
+ || options.DrawingUnits == BendRepairUnits.Millimeters
+ )
+ && double.IsFinite(limit)
+ && limit > tolerance
+ && options.MaxEndpointMovementMillimeters <= 3.175;
var original = bends.Select(b => (b.StartPoint, b.EndPoint)).ToArray();
var marks = entities.OfType().Where(IsMark).ToList();
var cuts = entities.Where(IsCut).ToList();
// ShapeBuilder may reverse/weld its inputs; isolate it from all source geometry.
var shapes = ShapeBuilder.GetShapes(cuts.CloneAll(), tolerance);
var boundaries = shapes.Where(s => s.IsClosed()).SelectMany(s => s.Entities).ToList();
- var polygons = shapes.Where(s => s.IsClosed()).Select(s => s.ToPolygonWithTolerance(tolerance / 10)).ToList();
+ var polygons = shapes
+ .Where(s => s.IsClosed())
+ .Select(s => s.ToPolygonWithTolerance(tolerance / 10))
+ .ToList();
bool InMaterial(Vector point) => polygons.Count(p => p.ContainsPoint(point)) % 2 == 1;
for (var i = 0; i < bends.Count; i++)
@@ -45,55 +71,96 @@ namespace OpenNest.IO.Bending
var (start, end) = original[i];
var reason = "";
var status = "Skipped";
- if (!valid) reason = "Specify drawing units and a finite movement limit above 0.001 and at most 3.175 mm.";
+ if (!valid)
+ reason =
+ "Specify drawing units and a finite movement limit above 0.001 and at most 3.175 mm.";
else if (!Finite(start) || !Finite(end) || start.DistanceTo(end) <= 2 * limit)
reason = "Invalid or too-short bend axis.";
else
{
var axis = (end - start) / start.DistanceTo(end);
- var first = marks.Where(m => Associated(m, start, end, tolerance, scale)).ToList();
- var last = marks.Where(m => Associated(m, end, start, tolerance, scale)).ToList();
+ var first = marks
+ .Where(m => Associated(m, start, end, tolerance, scale))
+ .ToList();
+ var last = marks
+ .Where(m => Associated(m, end, start, tolerance, scale))
+ .ToList();
if (first.Count != 1 || last.Count != 1 || ReferenceEquals(first[0], last[0]))
reason = "Missing or ambiguous collinear ticks at both original endpoints.";
- else if (original.Where((_, j) => j != i).Any(b =>
- Associated(first[0], b.StartPoint, b.EndPoint, tolerance, scale) ||
- Associated(first[0], b.EndPoint, b.StartPoint, tolerance, scale) ||
- Associated(last[0], b.StartPoint, b.EndPoint, tolerance, scale) ||
- Associated(last[0], b.EndPoint, b.StartPoint, tolerance, scale)))
+ else if (
+ original
+ .Where((_, j) => j != i)
+ .Any(b =>
+ Associated(first[0], b.StartPoint, b.EndPoint, tolerance, scale)
+ || Associated(first[0], b.EndPoint, b.StartPoint, tolerance, scale)
+ || Associated(last[0], b.StartPoint, b.EndPoint, tolerance, scale)
+ || Associated(last[0], b.EndPoint, b.StartPoint, tolerance, scale)
+ )
+ )
reason = "Tick is shared with another bend.";
else
{
var probe = new Line(start - axis * limit, end + axis * limit);
var hits = new List();
- var uncertain = shapes.Where(s => !s.IsClosed()).Any(s => s.Intersects(probe));
+ var uncertain = shapes
+ .Where(s => !s.IsClosed())
+ .Any(s => s.Intersects(probe));
foreach (var edge in boundaries)
{
- if (edge is Line line && OnAxis(line.StartPoint, start, axis, tolerance)
+ if (
+ edge is Line line
+ && OnAxis(line.StartPoint, start, axis, tolerance)
&& OnAxis(line.EndPoint, start, axis, tolerance)
- && System.Math.Max(Dot(line.StartPoint - start, axis), Dot(line.EndPoint - start, axis)) >= -limit
- && System.Math.Min(Dot(line.StartPoint - start, axis), Dot(line.EndPoint - start, axis)) <= bend.Length + limit)
+ && System.Math.Max(
+ Dot(line.StartPoint - start, axis),
+ Dot(line.EndPoint - start, axis)
+ ) >= -limit
+ && System.Math.Min(
+ Dot(line.StartPoint - start, axis),
+ Dot(line.EndPoint - start, axis)
+ )
+ <= bend.Length + limit
+ )
uncertain = true;
if (edge.Intersects(probe, out var points))
foreach (var p in points)
- if (Finite(p) && !hits.Any(h => h.DistanceTo(p) <= tolerance)) hits.Add(p);
+ if (Finite(p) && !hits.Any(h => h.DistanceTo(p) <= tolerance))
+ hits.Add(p);
}
var nearStart = hits.Where(p => p.DistanceTo(start) <= limit).ToList();
var nearEnd = hits.Where(p => p.DistanceTo(end) <= limit).ToList();
- if (uncertain || hits.Count != 2 || nearStart.Count != 1 || nearEnd.Count != 1)
- reason = "Missing/ambiguous closed cut boundaries, interior crossing, or movement exceeds limit.";
+ if (
+ uncertain
+ || hits.Count != 2
+ || nearStart.Count != 1
+ || nearEnd.Count != 1
+ )
+ reason =
+ "Missing/ambiguous closed cut boundaries, interior crossing, or movement exceeds limit.";
else
{
// Project the intersection back onto the existing axis; never rotate a bend.
var newStart = start + axis * Dot(nearStart[0] - start, axis);
var newEnd = start + axis * Dot(nearEnd[0] - start, axis);
var sample = axis * (10 * tolerance);
- if (!InMaterial((newStart + newEnd) / 2)
- || !InMaterial(newStart + sample) || !InMaterial(newEnd - sample)
- || InMaterial(newStart - sample) || InMaterial(newEnd + sample))
- reason = "Endpoints do not bound an unambiguous material interval (possible tangent).";
- else if (Dot(newEnd - newStart, axis) <= first[0].Length + last[0].Length + tolerance)
+ if (
+ !InMaterial((newStart + newEnd) / 2)
+ || !InMaterial(newStart + sample)
+ || !InMaterial(newEnd - sample)
+ || InMaterial(newStart - sample)
+ || InMaterial(newEnd + sample)
+ )
+ reason =
+ "Endpoints do not bound an unambiguous material interval (possible tangent).";
+ else if (
+ Dot(newEnd - newStart, axis)
+ <= first[0].Length + last[0].Length + tolerance
+ )
reason = "Repaired ticks would overlap or reverse the bend.";
- else if (newStart.DistanceTo(start) <= tolerance && newEnd.DistanceTo(end) <= tolerance)
+ else if (
+ newStart.DistanceTo(start) <= tolerance
+ && newEnd.DistanceTo(end) <= tolerance
+ )
{
status = "Unchanged";
reason = "Already on cut boundaries.";
@@ -107,7 +174,8 @@ namespace OpenNest.IO.Bending
b.Offset(newEnd - end);
var ai = entities.IndexOf(first[0]);
var bi = entities.IndexOf(last[0]);
- if (ai < 0 || bi < 0) reason = "Tick was already consumed; unchanged.";
+ if (ai < 0 || bi < 0)
+ reason = "Tick was already consumed; unchanged.";
else
{
entities[ai] = a;
@@ -115,40 +183,85 @@ namespace OpenNest.IO.Bending
bend.StartPoint = newStart;
bend.EndPoint = newEnd;
status = "Repaired";
- reason = "Both endpoints snapped along axis; only their two associated ticks replaced.";
+ reason =
+ "Both endpoints snapped along axis; only their two associated ticks replaced.";
}
}
}
}
}
- reports.Add(new BendRepairReport(i, status, reason, start, end, bend.StartPoint, bend.EndPoint));
+ reports.Add(
+ new BendRepairReport(
+ i,
+ status,
+ reason,
+ start,
+ end,
+ bend.StartPoint,
+ bend.EndPoint
+ )
+ );
}
return reports;
}
private static bool Finite(Vector p) => double.IsFinite(p.X) && double.IsFinite(p.Y);
+
private static double Dot(Vector a, Vector b) => a.X * b.X + a.Y * b.Y;
+
private static bool OnAxis(Vector p, Vector origin, Vector axis, double tolerance) =>
System.Math.Abs((p.X - origin.X) * axis.Y - (p.Y - origin.Y) * axis.X) <= tolerance;
- private static bool Continuous(Entity e) => string.IsNullOrEmpty(e.LineTypeName)
+
+ private static bool Continuous(Entity e) =>
+ string.IsNullOrEmpty(e.LineTypeName)
|| string.Equals(e.LineTypeName, "Continuous", StringComparison.OrdinalIgnoreCase)
|| string.Equals(e.LineTypeName, "ByLayer", StringComparison.OrdinalIgnoreCase);
- private static bool IsMark(Entity e) => Continuous(e) &&
- (string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase)
- || string.Equals(e.Layer?.Name, "SCRIBE", StringComparison.OrdinalIgnoreCase));
- private static bool IsCut(Entity e) => Continuous(e) &&
- (e.Layer?.Name == "0" || string.Equals(e.Layer?.Name, "CUT", StringComparison.OrdinalIgnoreCase))
+
+ private static bool IsMark(Entity e) =>
+ Continuous(e)
+ && (
+ string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(e.Layer?.Name, "SCRIBE", StringComparison.OrdinalIgnoreCase)
+ );
+
+ private static bool IsCut(Entity e) =>
+ Continuous(e)
+ && (
+ e.Layer?.Name == "0"
+ || string.Equals(e.Layer?.Name, "CUT", StringComparison.OrdinalIgnoreCase)
+ )
&& (e is Line || e is Arc || e is Circle);
- private static bool Associated(Line mark, Vector endpoint, Vector other, double tolerance, double scale)
+
+ private static bool Associated(
+ Line mark,
+ Vector endpoint,
+ Vector other,
+ double tolerance,
+ double scale
+ )
{
var length = endpoint.DistanceTo(other);
- if (!Finite(endpoint) || !Finite(other) || length <= tolerance || !Finite(mark.StartPoint) || !Finite(mark.EndPoint)
- || mark.Length <= tolerance || mark.Length > 25.4 * scale + tolerance || mark.Length >= length / 3) return false;
+ if (
+ !Finite(endpoint)
+ || !Finite(other)
+ || length <= tolerance
+ || !Finite(mark.StartPoint)
+ || !Finite(mark.EndPoint)
+ || mark.Length <= tolerance
+ || mark.Length > 25.4 * scale + tolerance
+ || mark.Length >= length / 3
+ )
+ return false;
var axis = (other - endpoint) / length;
- if (!OnAxis(mark.StartPoint, endpoint, axis, tolerance) || !OnAxis(mark.EndPoint, endpoint, axis, tolerance)) return false;
+ if (
+ !OnAxis(mark.StartPoint, endpoint, axis, tolerance)
+ || !OnAxis(mark.EndPoint, endpoint, axis, tolerance)
+ )
+ return false;
var a = Dot(mark.StartPoint - endpoint, axis);
var b = Dot(mark.EndPoint - endpoint, axis);
- return System.Math.Abs(System.Math.Min(a, b)) <= tolerance && System.Math.Max(a, b) > tolerance;
+ return System.Math.Abs(System.Math.Min(a, b)) <= tolerance
+ && System.Math.Max(a, b) > tolerance;
}
}
}
diff --git a/README.md b/README.md
index 2f27123..6ac085a 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,15 @@ cd OpenNest
dotnet build OpenNest.sln
```
+### Code formatting
+
+C# sources are formatted with [CSharpier](https://csharpier.com/), pinned in `.config/dotnet-tools.json`; the matching style (4-space indent, Allman braces, System-first usings, 100-column wraps) is mirrored in `.editorconfig` so IDE auto-format agrees. Before committing:
+
+```bash
+dotnet tool restore
+dotnet csharpier format . # apply; use `check` instead of `format` to verify only
+```
+
### Cross-platform engine contract tests
```bash