Compare commits

..
8 Commits
Author SHA1 Message Date
ajandClaude Sonnet 4.6 85f6a50f1a feat: improve DXF export and cross-platform file handling
- PepSettings: add DrawingsDirectory property for DXF endpoint config
- DrawingDxfExporter: support subprogram calls, skip non-cut linear moves,
  remove spurious radians-to-degrees conversion on arc angles, set AC1018 DXF version
- DrawingReader: handle .loop-info/.cadsnapshot entries gracefully, derive
  DrawingInfo from loop names when .info entry is absent, use FileShare.Read
- DrawingInfoReader: use TryParse instead of Parse for material number
- Drawing: make GetLoopName public (used by DrawingDxfExporter)
- ZipHelper: use FileShare.Read to allow concurrent readers on Windows/Linux
- NestFilterData: skip status filter when value is "All"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 07:23:12 -04:00
ajandClaude Sonnet 4.6 5e0856b5d1 fix: resolve API route name collisions
- DrawingsController: rename 'name/{name}' to 'by-name/{name}' to
  eliminate ambiguous match with '{name}/dxf' on path /drawings/name/dxf
- NestsController: add regex constraint to '{nestName}/download' and
  '{nestName}/plates' to prevent collision with year-parameterized routes
  when nestName is a 4-digit integer
- PepMcp/PepTools.cs: update GetDrawingByName URL to match renamed route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 07:20:53 -04:00
aj 9fcc9df209 feat: add Dockerfile for Linux container build 2026-06-25 22:17:43 -04:00
aj b4bdcc160e feat: make PepApi cross-platform for Linux Docker 2026-06-25 22:15:20 -04:00
aj 5a15b552a3 feat: add compare_geometry MCP tool to PepMcp
- Add LaserQuoteBaseUrl config property to PepTools
- Implement CompareGeometry() MCP tool to compare part geometry between PEP and LaserQuote
- Read LaserQuoteBaseUrl from appsettings.json in Program.cs
- Add appsettings.json with LaserQuoteBaseUrl config
- Update PepMcp.csproj to copy appsettings.json to output directory
2026-06-23 07:17:22 -04:00
aj f59c5bd32e feat: add GET /drawings/{name}/dxf endpoint to export PEP drawings as DXF 2026-06-23 06:57:08 -04:00
ajandClaude Sonnet 4.6 7a4a35da55 fix: convert arc angles from radians to degrees in DrawingDxfExporter
ACadSharp Arc.StartAngle/EndAngle store values in degrees (DXF spec groups
50/51). The previous code passed Math.Atan2 results (radians) directly,
causing silent geometry corruption. Also hardened the arc export test to
assert angles are in degree range (0..360).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 06:53:17 -04:00
aj af07946a79 feat: add DrawingDxfExporter to convert PEP Drawing to DXF stream 2026-06-23 06:49:50 -04:00
20 changed files with 442 additions and 24 deletions
+4
View File
@@ -0,0 +1,4 @@
**/.git
**/obj
**/bin
*.md
+19
View File
@@ -0,0 +1,19 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY PepApi.Core/PepApi.Core.csproj PepApi.Core/
COPY PepLib.Core/PepLib.Core.csproj PepLib.Core/
RUN dotnet restore PepApi.Core/PepApi.Core.csproj
COPY PepApi.Core/ PepApi.Core/
COPY PepLib.Core/ PepLib.Core/
RUN dotnet publish PepApi.Core/PepApi.Core.csproj -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8085
ENTRYPOINT ["dotnet", "PepApi.Core.dll"]
+1
View File
@@ -4,4 +4,5 @@ public class PepSettings
{ {
public string NestDirectory { get; set; } = string.Empty; public string NestDirectory { get; set; } = string.Empty;
public string MaterialsFile { get; set; } = string.Empty; public string MaterialsFile { get; set; } = string.Empty;
public string DrawingsDirectory { get; set; } = string.Empty;
} }
+42 -2
View File
@@ -1,7 +1,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using PepApi.Core.Configuration;
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;
@@ -10,10 +13,12 @@ namespace PepApi.Core.Controllers;
public class DrawingsController : ControllerBase public class DrawingsController : ControllerBase
{ {
private readonly PepDB _db; private readonly PepDB _db;
private readonly PepSettings _settings;
public DrawingsController(PepDB db) public DrawingsController(PepDB db, IOptions<PepSettings> settings)
{ {
_db = db; _db = db;
_settings = settings.Value;
} }
/// <summary> /// <summary>
@@ -80,7 +85,7 @@ public class DrawingsController : ControllerBase
/// <summary> /// <summary>
/// Get a drawing by exact name. /// Get a drawing by exact name.
/// </summary> /// </summary>
[HttpGet("name/{name}")] [HttpGet("by-name/{name}")]
public async Task<ActionResult<DrawingDetails>> GetDrawingByName(string name) public async Task<ActionResult<DrawingDetails>> GetDrawingByName(string name)
{ {
var drawing = await _db.Drawings var drawing = await _db.Drawings
@@ -147,6 +152,41 @@ 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" });
if (string.IsNullOrEmpty(_settings.DrawingsDirectory))
return StatusCode(503, new { message = "DrawingsDirectory is not configured" });
var filePath = Path.Combine(_settings.DrawingsDirectory, drawing.File);
if (!System.IO.File.Exists(filePath))
return NotFound(new { message = $"Drawing file not found: {drawing.File}" });
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
+31 -5
View File
@@ -101,7 +101,7 @@ public class NestsController : ControllerBase
return File(bytes, mimeType, fileName); return File(bytes, mimeType, fileName);
} }
[HttpGet("{nestName}/download")] [HttpGet("{nestName:regex(^(?!\\d{{4}}$).+)}/download")]
public async Task<IActionResult> DownloadFile(string nestName) public async Task<IActionResult> DownloadFile(string nestName)
{ {
var filePath = await GetNestPathAsync(nestName); var filePath = await GetNestPathAsync(nestName);
@@ -131,7 +131,7 @@ public class NestsController : ControllerBase
return Ok(combined); return Ok(combined);
} }
[HttpGet("{nestName}/plates")] [HttpGet("{nestName:regex(^(?!\\d{{4}}$).+)}/plates")]
public async Task<ActionResult<List<Plate>>> GetPlates(string nestName) public async Task<ActionResult<List<Plate>>> GetPlates(string nestName)
{ {
var nestFile = await GetNestPathAsync(nestName); var nestFile = await GetNestPathAsync(nestName);
@@ -346,13 +346,22 @@ public class NestsController : ControllerBase
var pepPath = Path.Combine(_nestDirectory, nestName + ".pep"); var pepPath = Path.Combine(_nestDirectory, nestName + ".pep");
if (System.IO.File.Exists(pepPath)) if (System.IO.File.Exists(pepPath))
return pepPath; return pepPath;
var pepMatch = FindFileByPrefix(_nestDirectory, nestName, ".pep");
if (pepMatch != null)
return pepMatch;
} }
// Older nests use .zip in year subdirectory // Older nests use .zip in year subdirectory
var zipPath = Path.Combine(_nestDirectory, year.Value.ToString(), nestName + ".zip"); var yearDir = Path.Combine(_nestDirectory, year.Value.ToString());
var zipPath = Path.Combine(yearDir, nestName + ".zip");
if (System.IO.File.Exists(zipPath)) if (System.IO.File.Exists(zipPath))
return zipPath; return zipPath;
var zipMatch = FindFileByPrefix(yearDir, nestName, ".zip");
if (zipMatch != null)
return zipMatch;
return null; return null;
} }
@@ -361,25 +370,42 @@ public class NestsController : ControllerBase
if (System.IO.File.Exists(flatPepPath)) if (System.IO.File.Exists(flatPepPath))
return flatPepPath; return flatPepPath;
var flatPepMatch = FindFileByPrefix(_nestDirectory, nestName, ".pep");
if (flatPepMatch != null)
return flatPepMatch;
// Fall back to year-based directory lookup for older nests // Fall back to year-based directory lookup for older nests
var upperName = nestName.ToUpper();
var nestHeader = await _db.NestHeaders var nestHeader = await _db.NestHeaders
.Where(n => n.NestName.ToUpper() == nestName.ToUpper() && n.DateProgrammed != null) .Where(n => (n.NestName.ToUpper() == upperName || n.NestName.ToUpper().StartsWith(upperName + "-"))
&& n.DateProgrammed != null)
.OrderByDescending(n => n.DateProgrammed) .OrderByDescending(n => n.DateProgrammed)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (nestHeader == null) if (nestHeader == null)
return null; return null;
var fullName = nestHeader.NestName;
var dbYear = nestHeader.DateProgrammed!.Value.Year; var dbYear = nestHeader.DateProgrammed!.Value.Year;
// Older nests used .zip extension // Older nests used .zip extension
var yearZipPath = Path.Combine(_nestDirectory, dbYear.ToString(), nestName + ".zip"); var yearZipPath = Path.Combine(_nestDirectory, dbYear.ToString(), fullName + ".zip");
if (System.IO.File.Exists(yearZipPath)) if (System.IO.File.Exists(yearZipPath))
return yearZipPath; return yearZipPath;
return null; return null;
} }
private static string? FindFileByPrefix(string directory, string prefix, string extension)
{
if (!Directory.Exists(directory))
return null;
var pattern = prefix + "-*" + extension;
var matches = Directory.GetFiles(directory, pattern);
return matches.Length > 0 ? matches.Order().First() : null;
}
private async Task<NestDetails> GetNestDetailsAsync(string nestFilePath) private async Task<NestDetails> GetNestDetailsAsync(string nestFilePath)
{ {
var nest = Nest.Load(nestFilePath); var nest = Nest.Load(nestFilePath);
+1 -1
View File
@@ -77,7 +77,7 @@ namespace PepApi.Core.Models
nests = nests.Where(n => n.DateCreated <= this.EndDate); nests = nests.Where(n => n.DateCreated <= this.EndDate);
} }
if (this.Status != null) if (this.Status != null && !this.Status.Equals("All", StringComparison.OrdinalIgnoreCase))
{ {
var x = this.Status.ToUpper(); var x = this.Status.ToUpper();
nests = nests.Where(n => n.Status.ToUpper() == x); nests = nests.Where(n => n.Status.ToUpper() == x);
+3 -2
View File
@@ -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
View File
@@ -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>
+148
View File
@@ -0,0 +1,148 @@
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();
doc.Header.Version = ACadVersion.AC1018;
// Collect loop IDs that are called as subprograms so we skip drawing them as top-level
var subprogramIds = new HashSet<int>();
foreach (var loop in drawing.Loops)
{
foreach (var code in loop)
{
if (code.CodeType() == CodeType.SubProgramCall)
subprogramIds.Add(((SubProgramCall)code).LoopId);
}
}
foreach (var loop in drawing.Loops)
{
var loopNumber = GetLoopNumber(loop.Name);
if (subprogramIds.Contains(loopNumber))
continue; // drawn inline at SubProgramCall positions
var layer = new Layer(loop.Name);
doc.Layers.Add(layer);
DrawLoop(doc, drawing, loop, layer, new Vector());
}
var buffer = new MemoryStream();
using (var writer = new DxfWriter(buffer, doc, false))
{
writer.Write();
}
return new MemoryStream(buffer.ToArray());
}
private static Vector DrawLoop(CadDocument doc, Drawing drawing, Loop loop, Layer layer, Vector startPos)
{
var pos = startPos;
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;
if (lm.Type == EntityType.Cut)
{
var lineEnd = Advance(pos, lm.EndPoint, loop.Mode);
doc.Entities.Add(new AcadLine
{
StartPoint = ToXYZ(pos),
EndPoint = ToXYZ(lineEnd),
Layer = layer
});
pos = lineEnd;
}
else
{
pos = Advance(pos, lm.EndPoint, loop.Mode);
}
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)
{
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,
EndAngle = endAngle,
Layer = layer
});
}
pos = arcEnd;
break;
case CodeType.SubProgramCall:
var call = (SubProgramCall)code;
var subLoop = drawing.Loops.FirstOrDefault(l => l.Name == drawing.GetLoopName(call.LoopId));
if (subLoop != null)
DrawLoop(doc, drawing, subLoop, layer, pos);
// pos unchanged — subprograms are closed shapes that return to start
break;
}
}
return pos;
}
private static int GetLoopNumber(string loopName)
{
var idx = loopName.LastIndexOf(".loop-", StringComparison.OrdinalIgnoreCase);
if (idx < 0) return -1;
return int.TryParse(loopName.Substring(idx + 6), out var n) ? n : -1;
}
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);
}
+2 -1
View File
@@ -34,7 +34,8 @@ namespace PepLib.IO
stream.Seek(0x9, SeekOrigin.Current); stream.Seek(0x9, SeekOrigin.Current);
Info.MaterialNumber = int.Parse(ReadString(0x40, ref stream)); int.TryParse(ReadString(0x40, ref stream), out var materialNumber);
Info.MaterialNumber = materialNumber;
Info.MaterialGrade = ReadString(0x10, ref stream); Info.MaterialGrade = ReadString(0x10, ref stream);
Info.ProgrammedBy = ReadString(0x40, ref stream); Info.ProgrammedBy = ReadString(0x40, ref stream);
Info.CreatedBy = ReadString(0x40, ref stream); Info.CreatedBy = ReadString(0x40, ref stream);
+21 -7
View File
@@ -1,5 +1,4 @@
using PepLib.Models; using PepLib.Models;
using System.Diagnostics;
using System.IO.Compression; using System.IO.Compression;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -38,6 +37,11 @@ namespace PepLib.IO
LoadInfo(memstream); LoadInfo(memstream);
memstream.Close(); memstream.Close();
continue; continue;
case ".loop-info":
case ".cadsnapshot":
memstream.Close();
continue;
} }
if (Regex.IsMatch(extension, "loop-\\d\\d\\d")) if (Regex.IsMatch(extension, "loop-\\d\\d\\d"))
@@ -47,6 +51,9 @@ namespace PepLib.IO
} }
} }
if (Drawing.Info == null)
Drawing.Info = DeriveInfoFromLoops();
Drawing.ResolveLoops(); Drawing.ResolveLoops();
} }
@@ -62,7 +69,7 @@ namespace PepLib.IO
try try
{ {
stream = new FileStream(nestFile, FileMode.Open); stream = new FileStream(nestFile, FileMode.Open, FileAccess.Read, FileShare.Read);
Read(stream); Read(stream);
} }
finally finally
@@ -73,16 +80,23 @@ namespace PepLib.IO
} }
private void LoadInfo(Stream stream) private void LoadInfo(Stream stream)
{
try
{ {
Drawing.Info = DrawingInfo.Load(stream); Drawing.Info = DrawingInfo.Load(stream);
} }
catch (Exception exception)
private DrawingInfo DeriveInfoFromLoops()
{ {
Debug.WriteLine(exception.Message); var info = new DrawingInfo();
Debug.WriteLine(exception.StackTrace);
if (Drawing.Loops.Count > 0)
{
// Loop names follow the pattern "{DrawingName}.loop-XXX"
var loopName = Drawing.Loops[0].Name;
var suffix = loopName.LastIndexOf(".loop-", StringComparison.OrdinalIgnoreCase);
info.Name = suffix >= 0 ? loopName.Substring(0, suffix) : loopName;
} }
return info;
} }
private Loop ReadLoop(string name, Stream stream) private Loop ReadLoop(string name, Stream stream)
+1 -1
View File
@@ -76,7 +76,7 @@ namespace PepLib.Models
return null; return null;
} }
private string GetLoopName(int loopId) public string GetLoopName(int loopId)
{ {
return string.Format("{0}.loop-{1}", Info.Name, loopId.ToString().PadLeft(3, '0')); return string.Format("{0}.loop-{1}", Info.Name, loopId.ToString().PadLeft(3, '0'));
} }
+1
View File
@@ -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>
+2 -2
View File
@@ -18,7 +18,7 @@ namespace PepLib.Utilities
var nameList = new List<string>(); var nameList = new List<string>();
var streamList = new List<Stream>(); var streamList = new List<Stream>();
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read)) using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var zip = new ZipArchive(fileStream, ZipArchiveMode.Read)) using (var zip = new ZipArchive(fileStream, ZipArchiveMode.Read))
{ {
foreach (var entry in zip.Entries) foreach (var entry in zip.Entries)
@@ -52,7 +52,7 @@ namespace PepLib.Utilities
/// <returns></returns> /// <returns></returns>
public static bool ExtractByExtension(string file, string extension, out string name, out Stream stream) public static bool ExtractByExtension(string file, string extension, out string name, out Stream stream)
{ {
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read)) using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var zip = new ZipArchive(fileStream, ZipArchiveMode.Read)) using (var zip = new ZipArchive(fileStream, ZipArchiveMode.Read))
{ {
foreach (var entry in zip.Entries) foreach (var entry in zip.Entries)
+6
View File
@@ -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>
+48 -1
View File
@@ -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,
@@ -530,7 +533,7 @@ public class PepTools
{ {
try try
{ {
var response = await _httpClient.GetAsync($"/drawings/name/{Uri.EscapeDataString(name)}"); var response = await _httpClient.GetAsync($"/drawings/by-name/{Uri.EscapeDataString(name)}");
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
return $"Error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}"; return $"Error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
@@ -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);
} }
+3
View File
@@ -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();
+3
View File
@@ -0,0 +1,3 @@
{
"LaserQuoteBaseUrl": "http://localhost:5260"
}