Compare commits
10
Commits
617de2e854
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f3beebe85 | ||
|
|
1d4f64726b | ||
|
|
85f6a50f1a | ||
|
|
5e0856b5d1 | ||
|
|
9fcc9df209 | ||
|
|
b4bdcc160e | ||
|
|
5a15b552a3 | ||
|
|
f59c5bd32e | ||
|
|
7a4a35da55 | ||
|
|
af07946a79 |
@@ -0,0 +1,4 @@
|
||||
**/.git
|
||||
**/obj
|
||||
**/bin
|
||||
*.md
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Build PepApi image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "PepApi.Core/**"
|
||||
- "PepLib.Core/**"
|
||||
- "Dockerfile"
|
||||
- ".gitea/workflows/build-pepapi.yml"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Gitea container registry
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.thecozycat.net -u "${{ gitea.actor }}" --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t git.thecozycat.net/${{ gitea.repository_owner }}/pepapi:latest -t git.thecozycat.net/${{ gitea.repository_owner }}/pepapi:${{ gitea.sha }} .
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push git.thecozycat.net/${{ gitea.repository_owner }}/pepapi:latest
|
||||
docker push git.thecozycat.net/${{ gitea.repository_owner }}/pepapi:${{ gitea.sha }}
|
||||
+19
@@ -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"]
|
||||
@@ -4,4 +4,5 @@ public class PepSettings
|
||||
{
|
||||
public string NestDirectory { get; set; } = string.Empty;
|
||||
public string MaterialsFile { get; set; } = string.Empty;
|
||||
public string DrawingsDirectory { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PepApi.Core.Configuration;
|
||||
using PepApi.Core.Models;
|
||||
using PepLib.Data;
|
||||
using PepLib.IO;
|
||||
|
||||
namespace PepApi.Core.Controllers;
|
||||
|
||||
@@ -10,10 +13,12 @@ namespace PepApi.Core.Controllers;
|
||||
public class DrawingsController : ControllerBase
|
||||
{
|
||||
private readonly PepDB _db;
|
||||
private readonly PepSettings _settings;
|
||||
|
||||
public DrawingsController(PepDB db)
|
||||
public DrawingsController(PepDB db, IOptions<PepSettings> settings)
|
||||
{
|
||||
_db = db;
|
||||
_settings = settings.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -80,7 +85,7 @@ public class DrawingsController : ControllerBase
|
||||
/// <summary>
|
||||
/// Get a drawing by exact name.
|
||||
/// </summary>
|
||||
[HttpGet("name/{name}")]
|
||||
[HttpGet("by-name/{name}")]
|
||||
public async Task<ActionResult<DrawingDetails>> GetDrawingByName(string name)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return new DrawingDetails
|
||||
|
||||
@@ -101,7 +101,7 @@ public class NestsController : ControllerBase
|
||||
return File(bytes, mimeType, fileName);
|
||||
}
|
||||
|
||||
[HttpGet("{nestName}/download")]
|
||||
[HttpGet("{nestName:regex(^(?!\\d{{4}}$).+)}/download")]
|
||||
public async Task<IActionResult> DownloadFile(string nestName)
|
||||
{
|
||||
var filePath = await GetNestPathAsync(nestName);
|
||||
@@ -131,7 +131,7 @@ public class NestsController : ControllerBase
|
||||
return Ok(combined);
|
||||
}
|
||||
|
||||
[HttpGet("{nestName}/plates")]
|
||||
[HttpGet("{nestName:regex(^(?!\\d{{4}}$).+)}/plates")]
|
||||
public async Task<ActionResult<List<Plate>>> GetPlates(string nestName)
|
||||
{
|
||||
var nestFile = await GetNestPathAsync(nestName);
|
||||
@@ -346,13 +346,22 @@ public class NestsController : ControllerBase
|
||||
var pepPath = Path.Combine(_nestDirectory, nestName + ".pep");
|
||||
if (System.IO.File.Exists(pepPath))
|
||||
return pepPath;
|
||||
|
||||
var pepMatch = FindFileByPrefix(_nestDirectory, nestName, ".pep");
|
||||
if (pepMatch != null)
|
||||
return pepMatch;
|
||||
}
|
||||
|
||||
// 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))
|
||||
return zipPath;
|
||||
|
||||
var zipMatch = FindFileByPrefix(yearDir, nestName, ".zip");
|
||||
if (zipMatch != null)
|
||||
return zipMatch;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -361,33 +370,51 @@ public class NestsController : ControllerBase
|
||||
if (System.IO.File.Exists(flatPepPath))
|
||||
return flatPepPath;
|
||||
|
||||
var flatPepMatch = FindFileByPrefix(_nestDirectory, nestName, ".pep");
|
||||
if (flatPepMatch != null)
|
||||
return flatPepMatch;
|
||||
|
||||
// Fall back to year-based directory lookup for older nests
|
||||
var upperName = nestName.ToUpper();
|
||||
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)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (nestHeader == null)
|
||||
return null;
|
||||
|
||||
var fullName = nestHeader.NestName;
|
||||
var dbYear = nestHeader.DateProgrammed!.Value.Year;
|
||||
|
||||
// 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))
|
||||
return yearZipPath;
|
||||
|
||||
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)
|
||||
{
|
||||
var nest = Nest.Load(nestFilePath);
|
||||
var dir = Path.GetDirectoryName(nestFilePath) + "\\";
|
||||
var name = Path.GetFileNameWithoutExtension(nestFilePath).ToUpper();
|
||||
|
||||
var info = await _db.NestHeaders
|
||||
.FirstOrDefaultAsync(n => n.NestName.ToUpper() == name && dir == n.Path);
|
||||
.Where(n => n.NestName.ToUpper() == name)
|
||||
.OrderByDescending(n => n.DateProgrammed)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (info == null)
|
||||
throw new Exception("Nest header not found in database");
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace PepApi.Core.Models
|
||||
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();
|
||||
nests = nests.Where(n => n.Status.ToUpper() == x);
|
||||
|
||||
@@ -6,8 +6,9 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
||||
|
||||
// Enable Windows Service hosting
|
||||
builder.Host.UseWindowsService();
|
||||
// Enable Windows Service hosting (only on Windows)
|
||||
if (OperatingSystem.IsWindows())
|
||||
builder.Host.UseWindowsService();
|
||||
|
||||
// Add services to the container
|
||||
builder.Services.AddControllers()
|
||||
|
||||
+14
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepLib.Core", "PepLib.Core\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepMcp", "PepMcp\PepMcp.csproj", "{28899B7B-2185-4141-B348-E227DFB355E9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepLib.Core.Tests", "PepLib.Core.Tests\PepLib.Core.Tests.csproj", "{FF7E4CB2-8825-4A03-9D0C-CC713BD93A18}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
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,33 @@
|
||||
using PepLib.IO;
|
||||
using Xunit;
|
||||
|
||||
namespace PepLib.Core.Tests.IO;
|
||||
|
||||
public class NestReaderTests
|
||||
{
|
||||
[Fact]
|
||||
public void Read_ReadOnlyFile_DoesNotRequestWriteAccess()
|
||||
{
|
||||
// Arrange: a file on a read-only mount denies write-access opens (EROFS on Linux,
|
||||
// UnauthorizedAccessException on Windows when the read-only attribute is set).
|
||||
// NestReader only ever reads nest files, so it must never request FileAccess.Write.
|
||||
var path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
File.SetAttributes(path, FileAttributes.ReadOnly);
|
||||
|
||||
var reader = new NestReader();
|
||||
|
||||
// Act / Assert: opening must succeed (i.e. not throw because of the read-only
|
||||
// attribute). Parsing the empty/invalid content is expected to fail separately.
|
||||
var ex = Record.Exception(() => reader.Read(path));
|
||||
|
||||
Assert.IsNotType<UnauthorizedAccessException>(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.SetAttributes(path, FileAttributes.Normal);
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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);
|
||||
}
|
||||
@@ -34,7 +34,8 @@ namespace PepLib.IO
|
||||
|
||||
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.ProgrammedBy = ReadString(0x40, ref stream);
|
||||
Info.CreatedBy = ReadString(0x40, ref stream);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using PepLib.Models;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -38,6 +37,11 @@ namespace PepLib.IO
|
||||
LoadInfo(memstream);
|
||||
memstream.Close();
|
||||
continue;
|
||||
|
||||
case ".loop-info":
|
||||
case ".cadsnapshot":
|
||||
memstream.Close();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Regex.IsMatch(extension, "loop-\\d\\d\\d"))
|
||||
@@ -47,6 +51,9 @@ namespace PepLib.IO
|
||||
}
|
||||
}
|
||||
|
||||
if (Drawing.Info == null)
|
||||
Drawing.Info = DeriveInfoFromLoops();
|
||||
|
||||
Drawing.ResolveLoops();
|
||||
}
|
||||
|
||||
@@ -62,7 +69,7 @@ namespace PepLib.IO
|
||||
|
||||
try
|
||||
{
|
||||
stream = new FileStream(nestFile, FileMode.Open);
|
||||
stream = new FileStream(nestFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
Read(stream);
|
||||
}
|
||||
finally
|
||||
@@ -74,15 +81,22 @@ namespace PepLib.IO
|
||||
|
||||
private void LoadInfo(Stream stream)
|
||||
{
|
||||
try
|
||||
Drawing.Info = DrawingInfo.Load(stream);
|
||||
}
|
||||
|
||||
private DrawingInfo DeriveInfoFromLoops()
|
||||
{
|
||||
var info = new DrawingInfo();
|
||||
|
||||
if (Drawing.Loops.Count > 0)
|
||||
{
|
||||
Drawing.Info = DrawingInfo.Load(stream);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.WriteLine(exception.Message);
|
||||
Debug.WriteLine(exception.StackTrace);
|
||||
// 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)
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace PepLib.IO
|
||||
|
||||
try
|
||||
{
|
||||
stream = new FileStream(nestFile, FileMode.Open);
|
||||
stream = new FileStream(nestFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
Read(stream);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace PepLib.Models
|
||||
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'));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace PepLib.Utilities
|
||||
var nameList = new List<string>();
|
||||
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))
|
||||
{
|
||||
foreach (var entry in zip.Entries)
|
||||
@@ -52,7 +52,7 @@ namespace PepLib.Utilities
|
||||
/// <returns></returns>
|
||||
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))
|
||||
{
|
||||
foreach (var entry in zip.Entries)
|
||||
|
||||
@@ -12,4 +12,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+48
-1
@@ -10,6 +10,9 @@ public class PepTools
|
||||
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()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
@@ -530,7 +533,7 @@ public class PepTools
|
||||
{
|
||||
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)
|
||||
return $"Error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
|
||||
@@ -571,4 +574,48 @@ public class PepTools
|
||||
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>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
PepTools.LaserQuoteBaseUrl = builder.Configuration["LaserQuoteBaseUrl"] ?? "http://localhost:5260";
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"LaserQuoteBaseUrl": "http://localhost:5260"
|
||||
}
|
||||
Reference in New Issue
Block a user