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