From bfd740c81e4a6594bad44a380919967e8f463616 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 17 Mar 2026 11:36:26 -0400 Subject: [PATCH] feat(core): add FlangeShape with JSON preset loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FlangeShape generates an outer circle with evenly-spaced bolt holes on a bolt circle pattern. ShapeDefinition.LoadFromJson() provides generic JSON loading for any shape — no separate preset classes needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Shapes/FlangeShape.cs | 38 ++++++++ OpenNest.Core/Shapes/ShapeDefinition.cs | 13 +++ OpenNest.Tests/Shapes/FlangeShapeTests.cs | 105 ++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 OpenNest.Core/Shapes/FlangeShape.cs create mode 100644 OpenNest.Tests/Shapes/FlangeShapeTests.cs diff --git a/OpenNest.Core/Shapes/FlangeShape.cs b/OpenNest.Core/Shapes/FlangeShape.cs new file mode 100644 index 0000000..e9183d7 --- /dev/null +++ b/OpenNest.Core/Shapes/FlangeShape.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest.Shapes +{ + public class FlangeShape : ShapeDefinition + { + public double NominalPipeSize { get; set; } + public double OD { get; set; } + public double HoleDiameter { get; set; } + public double HolePatternDiameter { get; set; } + public int HoleCount { get; set; } + + public override Drawing GetDrawing() + { + var entities = new List(); + + // Outer circle + entities.Add(new Circle(0, 0, OD / 2.0)); + + // Bolt holes evenly spaced on the bolt circle + var boltCircleRadius = HolePatternDiameter / 2.0; + var holeRadius = HoleDiameter / 2.0; + var angleStep = 2.0 * System.Math.PI / HoleCount; + + for (var i = 0; i < HoleCount; i++) + { + var angle = i * angleStep; + var cx = boltCircleRadius * System.Math.Cos(angle); + var cy = boltCircleRadius * System.Math.Sin(angle); + entities.Add(new Circle(cx, cy, holeRadius)); + } + + return CreateDrawing(entities); + } + } +} diff --git a/OpenNest.Core/Shapes/ShapeDefinition.cs b/OpenNest.Core/Shapes/ShapeDefinition.cs index 2e57f1e..4327935 100644 --- a/OpenNest.Core/Shapes/ShapeDefinition.cs +++ b/OpenNest.Core/Shapes/ShapeDefinition.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Text.Json; using OpenNest.Converters; using OpenNest.Geometry; @@ -7,6 +9,11 @@ namespace OpenNest.Shapes { public abstract class ShapeDefinition { + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + public string Name { get; set; } protected ShapeDefinition() @@ -19,6 +26,12 @@ namespace OpenNest.Shapes public abstract Drawing GetDrawing(); + public static List LoadFromJson(string path) where T : ShapeDefinition + { + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize>(json, JsonOptions); + } + protected Drawing CreateDrawing(List entities) { var pgm = ConvertGeometry.ToProgram(entities); diff --git a/OpenNest.Tests/Shapes/FlangeShapeTests.cs b/OpenNest.Tests/Shapes/FlangeShapeTests.cs new file mode 100644 index 0000000..eca87c4 --- /dev/null +++ b/OpenNest.Tests/Shapes/FlangeShapeTests.cs @@ -0,0 +1,105 @@ +using OpenNest.CNC; +using OpenNest.Shapes; + +namespace OpenNest.Tests.Shapes; + +public class FlangeShapeTests +{ + [Fact] + public void GetDrawing_BoundingBoxMatchesOD() + { + var shape = new FlangeShape + { + OD = 10, + HoleDiameter = 1, + HolePatternDiameter = 7, + HoleCount = 4 + }; + var drawing = shape.GetDrawing(); + + var bbox = drawing.Program.BoundingBox(); + Assert.Equal(10, bbox.Width, 0.01); + Assert.Equal(10, bbox.Length, 0.01); + } + + [Fact] + public void GetDrawing_AreaExcludesBoltHoles() + { + var shape = new FlangeShape + { + OD = 10, + HoleDiameter = 1, + HolePatternDiameter = 7, + HoleCount = 4 + }; + var drawing = shape.GetDrawing(); + + // Area = pi * 5^2 - 4 * pi * 0.5^2 = pi * (25 - 1) = pi * 24 + var expectedArea = System.Math.PI * 24; + Assert.Equal(expectedArea, drawing.Area, 0.5); + } + + [Fact] + public void GetDrawing_DefaultName_IsFlange() + { + var shape = new FlangeShape + { + OD = 10, + HoleDiameter = 1, + HolePatternDiameter = 7, + HoleCount = 4 + }; + var drawing = shape.GetDrawing(); + + Assert.Equal("Flange", drawing.Name); + } + + [Fact] + public void LoadFromJson_ProducesCorrectDrawing() + { + var json = """ + [ + { + "Name": "2in-150#", + "NominalPipeSize": 2.0, + "OD": 6.0, + "HoleDiameter": 0.75, + "HolePatternDiameter": 4.75, + "HoleCount": 4 + }, + { + "Name": "2in-300#", + "NominalPipeSize": 2.0, + "OD": 6.5, + "HoleDiameter": 0.75, + "HolePatternDiameter": 5.0, + "HoleCount": 8 + } + ] + """; + + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, json); + + var flanges = ShapeDefinition.LoadFromJson(tempFile); + + Assert.Equal(2, flanges.Count); + + var first = flanges[0]; + Assert.Equal("2in-150#", first.Name); + var drawing = first.GetDrawing(); + var bbox = drawing.Program.BoundingBox(); + Assert.Equal(6, bbox.Width, 0.01); + + var second = flanges[1]; + Assert.Equal("2in-300#", second.Name); + Assert.Equal(8, second.HoleCount); + } + finally + { + File.Delete(tempFile); + } + } +}