Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4bdcc160e | ||
|
|
5a15b552a3 | ||
|
|
f59c5bd32e | ||
|
|
7a4a35da55 | ||
|
|
af07946a79 |
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using PepApi.Core.Models;
|
using PepApi.Core.Models;
|
||||||
using PepLib.Data;
|
using PepLib.Data;
|
||||||
|
using PepLib.IO;
|
||||||
|
|
||||||
namespace PepApi.Core.Controllers;
|
namespace PepApi.Core.Controllers;
|
||||||
|
|
||||||
@@ -147,6 +148,38 @@ public class DrawingsController : ControllerBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export a drawing as a DXF file.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{name}/dxf")]
|
||||||
|
public async Task<IActionResult> GetDrawingDxf(string name)
|
||||||
|
{
|
||||||
|
var drawing = await _db.Drawings
|
||||||
|
.Where(d => d.Name.ToUpper() == name.ToUpper())
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (drawing == null)
|
||||||
|
return NotFound(new { message = "Drawing not found in database" });
|
||||||
|
|
||||||
|
var filePath = drawing.Path + drawing.File;
|
||||||
|
|
||||||
|
if (!System.IO.File.Exists(filePath))
|
||||||
|
return NotFound(new { message = "Drawing file not found on disk" });
|
||||||
|
|
||||||
|
PepLib.Models.Drawing pep;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pep = PepLib.Models.Drawing.Load(filePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { message = $"Failed to load drawing: {ex.Message}" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = DrawingDxfExporter.Export(pep);
|
||||||
|
return File(stream, "application/dxf", $"{name}.dxf");
|
||||||
|
}
|
||||||
|
|
||||||
private static DrawingDetails ConvertToDetails(Drawing drawing)
|
private static DrawingDetails ConvertToDetails(Drawing drawing)
|
||||||
{
|
{
|
||||||
return new DrawingDetails
|
return new DrawingDetails
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Enable Windows Service hosting
|
// Enable Windows Service hosting (only on Windows)
|
||||||
builder.Host.UseWindowsService();
|
if (OperatingSystem.IsWindows())
|
||||||
|
builder.Host.UseWindowsService();
|
||||||
|
|
||||||
// Add services to the container
|
// Add services to the container
|
||||||
builder.Services.AddControllers()
|
builder.Services.AddControllers()
|
||||||
|
|||||||
+14
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepLib.Core", "PepLib.Core\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepMcp", "PepMcp\PepMcp.csproj", "{28899B7B-2185-4141-B348-E227DFB355E9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepMcp", "PepMcp\PepMcp.csproj", "{28899B7B-2185-4141-B348-E227DFB355E9}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepLib.Core.Tests", "PepLib.Core.Tests\PepLib.Core.Tests.csproj", "{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -55,6 +57,18 @@ Global
|
|||||||
{28899B7B-2185-4141-B348-E227DFB355E9}.Release|x64.Build.0 = Release|Any CPU
|
{28899B7B-2185-4141-B348-E227DFB355E9}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{28899B7B-2185-4141-B348-E227DFB355E9}.Release|x86.ActiveCfg = Release|Any CPU
|
{28899B7B-2185-4141-B348-E227DFB355E9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{28899B7B-2185-4141-B348-E227DFB355E9}.Release|x86.Build.0 = Release|Any CPU
|
{28899B7B-2185-4141-B348-E227DFB355E9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using PepLib.Codes;
|
||||||
|
using PepLib.Enums;
|
||||||
|
using PepLib.Geometry;
|
||||||
|
using PepLib.IO;
|
||||||
|
using PepLib.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PepLib.Core.Tests.IO;
|
||||||
|
|
||||||
|
public class DrawingDxfExporterTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Export_RectangularLoop_ProducesFourLines()
|
||||||
|
{
|
||||||
|
// Arrange: a square 10x10 in incremental mode
|
||||||
|
var drawing = new Drawing();
|
||||||
|
drawing.Info = new DrawingInfo { Name = "TEST" };
|
||||||
|
var loop = new Loop { Name = "TEST.loop-000" };
|
||||||
|
// Mode defaults to Incremental in Loop constructor
|
||||||
|
loop.Add(new RapidMove(new Vector(0, 0))); // position at (0,0)
|
||||||
|
loop.Add(new LinearMove(new Vector(10, 0))); // → (10,0)
|
||||||
|
loop.Add(new LinearMove(new Vector(0, 10))); // → (10,10)
|
||||||
|
loop.Add(new LinearMove(new Vector(-10, 0))); // → (0,10)
|
||||||
|
loop.Add(new LinearMove(new Vector(0, -10))); // → (0,0)
|
||||||
|
drawing.Loops.Add(loop);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
using var stream = DrawingDxfExporter.Export(drawing);
|
||||||
|
|
||||||
|
// Assert: stream is non-empty and contains 4 line entities
|
||||||
|
Assert.True(stream.Length > 0);
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
using var reader = new ACadSharp.IO.DxfReader(stream);
|
||||||
|
var doc = reader.Read();
|
||||||
|
var lines = doc.Entities.OfType<ACadSharp.Entities.Line>().ToList();
|
||||||
|
var arcs = doc.Entities.OfType<ACadSharp.Entities.Arc>().ToList();
|
||||||
|
Assert.Equal(4, lines.Count);
|
||||||
|
Assert.Empty(arcs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Export_LoopWithArc_ProducesLineAndArc()
|
||||||
|
{
|
||||||
|
var drawing = new Drawing();
|
||||||
|
drawing.Info = new DrawingInfo { Name = "TEST" };
|
||||||
|
var loop = new Loop { Name = "TEST.loop-000" };
|
||||||
|
loop.Add(new RapidMove(new Vector(5, 0))); // start at (5,0)
|
||||||
|
loop.Add(new LinearMove(new Vector(5, 0))); // → (10,0)
|
||||||
|
loop.Add(new CircularMove(new Vector(-5, 5), new Vector(-5, 0), RotationType.CCW)); // arc: end=(5,5), center=(5,0)
|
||||||
|
drawing.Loops.Add(loop);
|
||||||
|
|
||||||
|
using var stream = DrawingDxfExporter.Export(drawing);
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
using var reader = new ACadSharp.IO.DxfReader(stream);
|
||||||
|
var doc = reader.Read();
|
||||||
|
var lines = doc.Entities.OfType<ACadSharp.Entities.Line>().ToList();
|
||||||
|
var arcs = doc.Entities.OfType<ACadSharp.Entities.Arc>().ToList();
|
||||||
|
Assert.Single(lines);
|
||||||
|
Assert.Single(arcs);
|
||||||
|
var arc = doc.Entities.OfType<ACadSharp.Entities.Arc>().Single();
|
||||||
|
// Angles must be in degrees (0..360), not radians (0..6.28)
|
||||||
|
Assert.InRange(arc.StartAngle, 0, 360);
|
||||||
|
Assert.InRange(arc.EndAngle, 0, 360);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PepLib.Core\PepLib.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using ACadSharp;
|
||||||
|
using ACadSharp.IO;
|
||||||
|
using ACadSharp.Tables;
|
||||||
|
using CSMath;
|
||||||
|
using PepLib.Codes;
|
||||||
|
using PepLib.Enums;
|
||||||
|
using PepLib.Geometry;
|
||||||
|
using PepLib.Models;
|
||||||
|
using AcadArc = ACadSharp.Entities.Arc;
|
||||||
|
using AcadCircle = ACadSharp.Entities.Circle;
|
||||||
|
using AcadLine = ACadSharp.Entities.Line;
|
||||||
|
|
||||||
|
namespace PepLib.IO;
|
||||||
|
|
||||||
|
public static class DrawingDxfExporter
|
||||||
|
{
|
||||||
|
public static Stream Export(Drawing drawing)
|
||||||
|
{
|
||||||
|
var doc = new CadDocument();
|
||||||
|
|
||||||
|
foreach (var loop in drawing.Loops)
|
||||||
|
{
|
||||||
|
var layer = new Layer(loop.Name);
|
||||||
|
doc.Layers.Add(layer);
|
||||||
|
|
||||||
|
var pos = new Vector();
|
||||||
|
|
||||||
|
foreach (var code in loop)
|
||||||
|
{
|
||||||
|
switch (code.CodeType())
|
||||||
|
{
|
||||||
|
case CodeType.RapidMove:
|
||||||
|
pos = Advance(pos, ((RapidMove)code).EndPoint, loop.Mode);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CodeType.LinearMove:
|
||||||
|
var lm = (LinearMove)code;
|
||||||
|
var lineEnd = Advance(pos, lm.EndPoint, loop.Mode);
|
||||||
|
doc.Entities.Add(new AcadLine
|
||||||
|
{
|
||||||
|
StartPoint = ToXYZ(pos),
|
||||||
|
EndPoint = ToXYZ(lineEnd),
|
||||||
|
Layer = layer
|
||||||
|
});
|
||||||
|
pos = lineEnd;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CodeType.CircularMove:
|
||||||
|
var cm = (CircularMove)code;
|
||||||
|
var arcEnd = Advance(pos, cm.EndPoint, loop.Mode);
|
||||||
|
var arcCenter = Advance(pos, cm.CenterPoint, loop.Mode);
|
||||||
|
|
||||||
|
var dx = pos.X - arcCenter.X;
|
||||||
|
var dy = pos.Y - arcCenter.Y;
|
||||||
|
var radius = Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
var startAngle = Math.Atan2(pos.Y - arcCenter.Y, pos.X - arcCenter.X);
|
||||||
|
var endAngle = Math.Atan2(arcEnd.Y - arcCenter.Y, arcEnd.X - arcCenter.X);
|
||||||
|
|
||||||
|
if (cm.Rotation == RotationType.CW)
|
||||||
|
{
|
||||||
|
(startAngle, endAngle) = (endAngle, startAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.Abs(startAngle - endAngle) < 1e-10) // full circle
|
||||||
|
{
|
||||||
|
doc.Entities.Add(new AcadCircle
|
||||||
|
{
|
||||||
|
Center = ToXYZ(arcCenter),
|
||||||
|
Radius = radius,
|
||||||
|
Layer = layer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
doc.Entities.Add(new AcadArc
|
||||||
|
{
|
||||||
|
Center = ToXYZ(arcCenter),
|
||||||
|
Radius = radius,
|
||||||
|
StartAngle = startAngle * (180.0 / Math.PI),
|
||||||
|
EndAngle = endAngle * (180.0 / Math.PI),
|
||||||
|
Layer = layer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = arcEnd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new MemoryStream();
|
||||||
|
using (var writer = new DxfWriter(buffer, doc, false))
|
||||||
|
{
|
||||||
|
writer.Write();
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = new MemoryStream(buffer.ToArray());
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector Advance(Vector current, Vector offset, ProgrammingMode mode) =>
|
||||||
|
mode == ProgrammingMode.Incremental ? current + offset : offset;
|
||||||
|
|
||||||
|
private static XYZ ToXYZ(Vector v) => new XYZ(v.X, v.Y, 0);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@@ -12,4 +12,10 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ public class PepTools
|
|||||||
BaseAddress = new Uri("http://localhost:8085")
|
BaseAddress = new Uri("http://localhost:8085")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static string LaserQuoteBaseUrl { get; set; } = "http://localhost:5260";
|
||||||
|
private static readonly HttpClient _laserQuoteClient = new();
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
@@ -571,4 +574,48 @@ public class PepTools
|
|||||||
return $"Error calling PEP API: {ex.Message}";
|
return $"Error calling PEP API: {ex.Message}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description("Compare the geometry of a part between PEP (nesting software) and LaserQuote (quoting software). Returns bounding box dimensions, cut length, entity counts, pixel diff %, and alignment strategy. Use this to detect when part drawings have drifted between systems.")]
|
||||||
|
public static async Task<string> CompareGeometry(
|
||||||
|
[Description("The part number to compare (e.g., 'SULLYS-003')")] string partNumber)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"{LaserQuoteBaseUrl}/api/geometry/compare?partNumber={Uri.EscapeDataString(partNumber)}";
|
||||||
|
var response = await _laserQuoteClient.GetAsync(url);
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var result = System.Text.Json.JsonSerializer.Deserialize<GeometryCompareResult>(json, _jsonOptions);
|
||||||
|
|
||||||
|
if (result?.Error != null)
|
||||||
|
return $"Note: {result.Error}";
|
||||||
|
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.AppendLine($"Geometry Comparison: {partNumber}");
|
||||||
|
sb.AppendLine($"Result: {(result?.IsMatch == true ? "MATCH ✓" : "MISMATCH ✗")}");
|
||||||
|
sb.AppendLine($"Pixel Diff: {result?.PixelDiffPercent:F3}%");
|
||||||
|
sb.AppendLine($"Alignment: {result?.AlignmentLabel}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Metric | LaserQuote | PEP |");
|
||||||
|
sb.AppendLine("|---------------|-----------------|-----------------|");
|
||||||
|
sb.AppendLine($"| Width (in) | {result?.LaserQuote?.Width,15:F4} | {result?.Pep?.Width,15:F4} |");
|
||||||
|
sb.AppendLine($"| Height (in) | {result?.LaserQuote?.Height,15:F4} | {result?.Pep?.Height,15:F4} |");
|
||||||
|
sb.AppendLine($"| Cut Length | {result?.LaserQuote?.CutLength,15:F3} | {result?.Pep?.CutLength,15:F3} |");
|
||||||
|
sb.AppendLine($"| Entity Count | {result?.LaserQuote?.EntityCount,15} | {result?.Pep?.EntityCount,15} |");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return $"Error calling LaserQuote API: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record GeometryMetricDto(double Width, double Height, double CutLength, int EntityCount);
|
||||||
|
private record GeometryCompareResult(
|
||||||
|
bool IsMatch,
|
||||||
|
GeometryMetricDto? LaserQuote,
|
||||||
|
GeometryMetricDto? Pep,
|
||||||
|
double PixelDiffPercent,
|
||||||
|
string? AlignmentLabel,
|
||||||
|
string? Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,7 @@ builder.Services
|
|||||||
.WithTools<PepTools>();
|
.WithTools<PepTools>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
PepTools.LaserQuoteBaseUrl = builder.Configuration["LaserQuoteBaseUrl"] ?? "http://localhost:5260";
|
||||||
|
|
||||||
await app.RunAsync();
|
await app.RunAsync();
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"LaserQuoteBaseUrl": "http://localhost:5260"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user