Files
mech-ai/backend-csharp/Agent4.Api/DwgDraftApi.cs
T
2026-07-17 17:45:56 +08:00

1535 lines
57 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.Win32;
static class DwgDraftApi
{
public static IServiceCollection AddDwgDraft(this IServiceCollection services)
{
services.AddSingleton<DwgDraftService>();
return services;
}
public static WebApplication MapDwgDraftApi(this WebApplication app)
{
var group = app.MapGroup("/api/dwg-draft");
group.MapGet("/installation", (DwgDraftService dwg) => Results.Ok(dwg.DiscoverInstallation()));
group.MapGet("/bridge", (DwgDraftService dwg) => Results.Ok(dwg.ReadBridgeStatus()));
group.MapPost("/extract", (DwgDraftExtractRequest request, DwgDraftService dwg) =>
{
try
{
return Results.Ok(dwg.Extract(request));
}
catch (Exception ex)
{
return Results.Problem($"AutoCAD DWG 提取失败:{ex.Message}", statusCode: 500);
}
});
group.MapPost("/open", (DwgDraftExtractRequest request, DwgDraftService dwg) =>
{
try
{
return Results.Ok(dwg.Extract(request));
}
catch (Exception ex)
{
return Results.Problem($"AutoCAD DWG 打开/提取失败:{ex.Message}", statusCode: 500);
}
});
group.MapGet("/latest-extraction", (DwgDraftService dwg) =>
{
try
{
return Results.Ok(dwg.ReadLatestExtraction());
}
catch (Exception ex)
{
return Results.Problem($"读取 DWG 提取结果失败:{ex.Message}", statusCode: 500);
}
});
group.MapGet("/latest-extraction/category/{category}", (string category, DwgDraftService dwg) =>
{
try
{
var result = dwg.ReadCategoryExtraction(category);
return result.Found ? Results.Ok(result) : Results.NotFound(result);
}
catch (Exception ex)
{
return Results.Problem($"读取 DWG 分类提取结果失败:{ex.Message}", statusCode: 500);
}
});
return app;
}
}
sealed class DwgDraftService
{
const string DefaultProgId = "AutoCAD.Application.23.1";
static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
};
static readonly string[] ProgIdCandidates =
[
"AutoCAD.Application.23.1",
"AutoCAD.Application.23",
"AutoCAD.Application"
];
readonly AppPaths _paths;
readonly object _lock = new();
public DwgDraftService(AppPaths paths)
{
_paths = paths;
}
public DwgDraftInstallation DiscoverInstallation()
{
var progIds = Registry.ClassesRoot.GetSubKeyNames()
.Where(name => name.StartsWith("AutoCAD.Application", StringComparison.OrdinalIgnoreCase))
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
.Select(ReadProgIdInfo)
.ToList();
var exeCandidates = new[]
{
@"D:\Program Files\Autodesk\AutoCAD 2020\acad.exe",
@"C:\Program Files\Autodesk\AutoCAD 2020\acad.exe",
@"C:\Program Files\Autodesk\AutoCAD 2021\acad.exe",
@"C:\Program Files\Autodesk\AutoCAD 2022\acad.exe"
};
return new DwgDraftInstallation
{
Found = progIds.Count > 0 || exeCandidates.Any(File.Exists),
DefaultProgId = DefaultProgId,
ProgIds = progIds,
AcadExeCandidates = exeCandidates.Where(File.Exists).ToList(),
Notes =
[
"DWG 通过 AutoCAD COM 读取,后续二维图查错以 AutoCAD 路径为主。",
"AutoCAD COM 调用可能在软件忙碌时返回 RPC_E_CALL_REJECTED,后端已对属性读取和集合遍历做重试。"
]
};
}
public DwgDraftBridgeStatus ReadBridgeStatus()
{
var bridgeDir = ResolveBridgeDir();
var heartbeatPath = Path.Combine(bridgeDir, "heartbeat.json");
var alive = IsBridgeAlive(heartbeatPath);
JsonElement? heartbeat = null;
if (File.Exists(heartbeatPath))
{
using var doc = JsonDocument.Parse(File.ReadAllText(heartbeatPath));
heartbeat = doc.RootElement.Clone();
}
return new DwgDraftBridgeStatus
{
Ok = true,
Alive = alive,
BridgeDir = bridgeDir,
HeartbeatPath = heartbeatPath,
LastWriteTimeUtc = File.Exists(heartbeatPath) ? File.GetLastWriteTimeUtc(heartbeatPath) : null,
Heartbeat = heartbeat,
Message = alive ? "AutoCAD DWG bridge is online." : "AutoCAD DWG bridge is not online."
};
}
public DwgDraftExtractionResult Extract(DwgDraftExtractRequest request)
{
lock (_lock)
{
return RunExternalExtractor(request);
}
}
DwgDraftExtractionResult RunExternalExtractor(DwgDraftExtractRequest request)
{
TraceExternalExtractor("start");
ApplyDrawingPathBase64(request);
if (string.IsNullOrWhiteSpace(request.DrawingPath))
throw new ArgumentException("drawing_path 不能为空。", nameof(request));
var drawingPath = Path.GetFullPath(request.DrawingPath.Trim().Trim('"'));
TraceExternalExtractor($"drawing_path={drawingPath}");
if (!File.Exists(drawingPath))
throw new FileNotFoundException($"DWG 文件不存在:{drawingPath}");
var progId = string.IsNullOrWhiteSpace(request.ProgId) ? ResolveProgId() : request.ProgId.Trim();
TraceExternalExtractor($"prog_id={progId}");
var outputDir = ResolveOutputDir(request.OutputDir);
Directory.CreateDirectory(outputDir);
TraceExternalExtractor($"output_dir={outputDir}");
var extractorDll = Path.Combine(
_paths.Agent4Root,
"tools",
"drawing-diagnostics",
"AutoCadDwgExtractor",
"bin",
"Debug",
"net10.0",
"AutoCadDwgExtractor.dll");
TraceExternalExtractor($"extractor_dll={extractorDll}; exists={File.Exists(extractorDll)}");
if (!File.Exists(extractorDll))
throw new FileNotFoundException($"AutoCAD DWG 提取器尚未编译:{extractorDll}");
if (request.UseBridge &&
TryRunBridgeExtractor(request, drawingPath, progId, outputDir, out var bridgeResult))
{
return bridgeResult;
}
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = _paths.Agent4Root,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
};
startInfo.ArgumentList.Add(extractorDll);
startInfo.ArgumentList.Add("--drawing-path");
startInfo.ArgumentList.Add(drawingPath);
startInfo.ArgumentList.Add("--output-dir");
startInfo.ArgumentList.Add(outputDir);
startInfo.ArgumentList.Add("--prog-id");
startInfo.ArgumentList.Add(progId);
startInfo.ArgumentList.Add("--visible");
startInfo.ArgumentList.Add(request.Visible ? "true" : "false");
startInfo.ArgumentList.Add("--close-after-extract");
startInfo.ArgumentList.Add(request.CloseAfterExtract ? "true" : "false");
startInfo.ArgumentList.Add("--max-entities");
startInfo.ArgumentList.Add((request.MaxEntities <= 0 ? 100_000 : request.MaxEntities).ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrWhiteSpace(request.MainViewSeedMode))
{
startInfo.ArgumentList.Add("--main-view-seed-mode");
startInfo.ArgumentList.Add(request.MainViewSeedMode.Trim());
}
if (!string.IsNullOrWhiteSpace(request.ReuseExtractionDir))
{
startInfo.ArgumentList.Add("--reuse-extraction-dir");
startInfo.ArgumentList.Add(Path.GetFullPath(request.ReuseExtractionDir.Trim().Trim('"')));
}
TraceExternalExtractor("process_start");
using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start AutoCadDwgExtractor.");
TraceExternalExtractor($"process_started pid={process.Id}");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
TraceExternalExtractor($"process_exit exit_code={process.ExitCode}; stdout_len={stdout.Length}; stderr_len={stderr.Length}");
if (process.ExitCode != 0)
throw new InvalidOperationException($"AutoCadDwgExtractor failed. exit={process.ExitCode}\n{stdout}\n{stderr}");
var manifestPath = Path.Combine(outputDir, "manifest.json");
TraceExternalExtractor($"manifest={manifestPath}; exists={File.Exists(manifestPath)}");
if (!File.Exists(manifestPath))
throw new FileNotFoundException($"AutoCadDwgExtractor did not write manifest: {manifestPath}\n{stdout}\n{stderr}");
using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath));
var root = doc.RootElement;
var entityCount = root.TryGetProperty("entity_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number
? countElement.GetInt32()
: 0;
var typeCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
if (root.TryGetProperty("type_counts", out var typeCountsElement) && typeCountsElement.ValueKind == JsonValueKind.Object)
{
foreach (var item in typeCountsElement.EnumerateObject())
{
if (item.Value.ValueKind == JsonValueKind.Number)
typeCounts[item.Name] = item.Value.GetInt32();
}
}
return new DwgDraftExtractionResult
{
Ok = true,
Source = "AutoCAD COM external extractor",
ProgId = progId,
Started = root.TryGetProperty("started_auto_cad", out var startedElement) && startedElement.ValueKind is JsonValueKind.True,
DrawingPath = drawingPath,
ExportDir = outputDir,
ManifestPath = manifestPath,
EntityCount = entityCount,
TypeCounts = typeCounts,
CategoryFiles = ReadCategoryFiles(root, outputDir),
Message = $"已通过 AutoCAD COM 外部提取器提取 DWG{Path.GetFileName(drawingPath)}"
};
}
bool TryRunBridgeExtractor(
DwgDraftExtractRequest request,
string drawingPath,
string progId,
string outputDir,
out DwgDraftExtractionResult result)
{
result = new DwgDraftExtractionResult();
var bridgeDir = ResolveBridgeDir();
var heartbeatPath = Path.Combine(bridgeDir, "heartbeat.json");
if (!IsBridgeAlive(heartbeatPath))
{
TraceExternalExtractor($"bridge_not_available heartbeat={heartbeatPath}");
return false;
}
var jobsDir = Path.Combine(bridgeDir, "jobs");
var resultsDir = Path.Combine(bridgeDir, "results");
Directory.CreateDirectory(jobsDir);
Directory.CreateDirectory(resultsDir);
var jobId = $"{DateTime.UtcNow:yyyyMMddHHmmssfff}-{Guid.NewGuid():N}";
var jobPath = Path.Combine(jobsDir, $"{jobId}.request.json");
var tempJobPath = jobPath + ".tmp";
var resultPath = Path.Combine(resultsDir, $"{jobId}.result.json");
var job = new DwgBridgeJob
{
JobId = jobId,
DrawingPath = drawingPath,
OutputDir = outputDir,
ProgId = progId,
Visible = request.Visible,
CloseAfterExtract = request.CloseAfterExtract,
MaxEntities = request.MaxEntities <= 0 ? 100_000 : request.MaxEntities,
MainViewSeedMode = request.MainViewSeedMode,
ReuseExtractionDir = string.IsNullOrWhiteSpace(request.ReuseExtractionDir) ? "" : Path.GetFullPath(request.ReuseExtractionDir.Trim().Trim('"'))
};
File.WriteAllText(tempJobPath, JsonSerializer.Serialize(job, JsonOptions));
File.Move(tempJobPath, jobPath, overwrite: true);
TraceExternalExtractor($"bridge_job_submitted job_id={jobId}; bridge_dir={bridgeDir}");
var timeout = TimeSpan.FromSeconds(request.BridgeTimeoutSeconds <= 0 ? 600 : request.BridgeTimeoutSeconds);
var deadline = DateTime.UtcNow.Add(timeout);
while (DateTime.UtcNow < deadline)
{
if (File.Exists(resultPath))
{
var bridgeResult = JsonSerializer.Deserialize<DwgBridgeJobResult>(File.ReadAllText(resultPath), JsonOptions)
?? throw new InvalidOperationException($"Invalid bridge result: {resultPath}");
TraceExternalExtractor($"bridge_job_result job_id={jobId}; ok={bridgeResult.Ok}; exit={bridgeResult.ExitCode}");
if (!bridgeResult.Ok)
{
throw new InvalidOperationException(
$"AutoCAD bridge extraction failed. job={jobId}; exit={bridgeResult.ExitCode}\n{bridgeResult.Error}\n{bridgeResult.Stdout}\n{bridgeResult.Stderr}");
}
var manifestPath = string.IsNullOrWhiteSpace(bridgeResult.ManifestPath)
? Path.Combine(outputDir, "manifest.json")
: bridgeResult.ManifestPath;
if (!File.Exists(manifestPath))
throw new FileNotFoundException($"AutoCAD bridge did not write manifest: {manifestPath}");
result = ReadExternalManifest(
manifestPath,
outputDir,
drawingPath,
progId,
"AutoCAD COM bridge extractor",
$"Read DWG through AutoCAD bridge: {Path.GetFileName(drawingPath)}");
return true;
}
Thread.Sleep(500);
}
throw new TimeoutException($"AutoCAD bridge extraction timed out. job={jobId}; timeout={timeout.TotalSeconds:0}s");
}
bool IsBridgeAlive(string heartbeatPath)
{
if (!File.Exists(heartbeatPath))
return false;
return DateTime.UtcNow - File.GetLastWriteTimeUtc(heartbeatPath) <= TimeSpan.FromSeconds(30);
}
string ResolveBridgeDir()
{
var configured = Environment.GetEnvironmentVariable("AGENT4_DWG_BRIDGE_DIR");
if (!string.IsNullOrWhiteSpace(configured))
return Path.GetFullPath(configured.Trim().Trim('"'));
return Path.Combine(_paths.RuntimeDir, "dwg-draft", "bridge");
}
DwgDraftExtractionResult ReadExternalManifest(
string manifestPath,
string outputDir,
string drawingPath,
string progId,
string source,
string message)
{
using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath));
var root = doc.RootElement;
var entityCount = root.TryGetProperty("entity_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number
? countElement.GetInt32()
: 0;
var typeCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
if (root.TryGetProperty("type_counts", out var typeCountsElement) && typeCountsElement.ValueKind == JsonValueKind.Object)
{
foreach (var item in typeCountsElement.EnumerateObject())
{
if (item.Value.ValueKind == JsonValueKind.Number)
typeCounts[item.Name] = item.Value.GetInt32();
}
}
return new DwgDraftExtractionResult
{
Ok = true,
Source = source,
ProgId = progId,
Started = root.TryGetProperty("started_auto_cad", out var startedElement) && startedElement.ValueKind is JsonValueKind.True,
DrawingPath = drawingPath,
ExportDir = outputDir,
ManifestPath = manifestPath,
EntityCount = entityCount,
TypeCounts = typeCounts,
CategoryFiles = ReadCategoryFiles(root, outputDir),
Message = message
};
}
void TraceExternalExtractor(string message)
{
try
{
var dir = Path.Combine(_paths.RuntimeDir, "dwg-draft");
Directory.CreateDirectory(dir);
File.AppendAllText(
Path.Combine(dir, "extractor-dispatch.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}");
}
catch
{
// Diagnostics must not affect extraction.
}
}
DwgDraftExtractionResult ExtractOnSta(DwgDraftExtractRequest request)
{
ApplyDrawingPathBase64(request);
if (string.IsNullOrWhiteSpace(request.DrawingPath))
throw new ArgumentException("drawing_path 不能为空。", nameof(request));
var drawingPath = Path.GetFullPath(request.DrawingPath.Trim().Trim('"'));
if (!File.Exists(drawingPath))
throw new FileNotFoundException($"DWG 文件不存在:{drawingPath}");
var progId = string.IsNullOrWhiteSpace(request.ProgId) ? ResolveProgId() : request.ProgId.Trim();
var outputDir = ResolveOutputDir(request.OutputDir);
Directory.CreateDirectory(outputDir);
var app = ConnectOrCreate(progId, request.Visible, out var started);
object? doc = null;
try
{
WaitForQuiescent(app, TimeSpan.FromSeconds(20));
doc = OpenDocument(app, drawingPath);
WaitForQuiescent(app, TimeSpan.FromSeconds(20));
var extraction = ExtractDocument(doc, request.MaxEntities <= 0 ? 100_000 : request.MaxEntities);
var manifest = WriteExtractionFiles(outputDir, drawingPath, progId, started, extraction);
if (request.CloseAfterExtract)
TryInvoke(doc, "Close", [false]);
return new DwgDraftExtractionResult
{
Ok = true,
Source = "AutoCAD COM",
ProgId = progId,
Started = started,
DrawingPath = drawingPath,
ExportDir = outputDir,
ManifestPath = Path.Combine(outputDir, "manifest.json"),
EntityCount = extraction.EntitiesCommon.Count,
TypeCounts = extraction.TypeCounts,
CategoryFiles = manifest.CategoryFiles,
Message = $"已通过 AutoCAD COM 提取 DWG{Path.GetFileName(drawingPath)}"
};
}
finally
{
if (doc != null)
ReleaseCom(doc);
ReleaseCom(app);
}
}
DwgDraftDocumentExtraction ExtractDocument(object doc, int maxEntities)
{
var extraction = new DwgDraftDocumentExtraction();
ExtractSpace(doc, "ModelSpace", extraction, maxEntities);
ExtractSpace(doc, "PaperSpace", extraction, maxEntities);
return extraction;
}
void ExtractSpace(object doc, string spaceName, DwgDraftDocumentExtraction extraction, int maxEntities)
{
var space = GetProperty(doc, spaceName);
if (space == null)
return;
try
{
var count = ToInt(GetProperty(space, "Count")) ?? 0;
if (string.Equals(spaceName, "ModelSpace", StringComparison.OrdinalIgnoreCase))
extraction.ModelSpaceCount = count;
else
extraction.PaperSpaceCount = count;
for (var i = 0; i < count && extraction.EntitiesCommon.Count < maxEntities; i++)
{
var entity = GetCollectionItem(space, i);
if (entity == null)
continue;
try
{
ExtractEntity(entity, spaceName, i, extraction);
}
finally
{
ReleaseCom(entity);
}
}
}
finally
{
ReleaseCom(space);
}
}
void ExtractEntity(object entity, string spaceName, int index, DwgDraftDocumentExtraction extraction)
{
var objectName = ReadString(entity, "ObjectName");
if (string.IsNullOrWhiteSpace(objectName))
objectName = ReadString(entity, "EntityName");
if (string.IsNullOrWhiteSpace(objectName))
objectName = entity.GetType().Name;
extraction.TypeCounts[objectName] = extraction.TypeCounts.TryGetValue(objectName, out var current) ? current + 1 : 1;
var common = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["space"] = spaceName,
["index"] = index,
["object_name"] = objectName,
["handle"] = ReadString(entity, "Handle"),
["layer"] = ReadString(entity, "Layer"),
["linetype"] = ReadString(entity, "Linetype"),
["color"] = ToInt(GetProperty(entity, "Color")),
["lineweight"] = ToInt(GetProperty(entity, "Lineweight"))
};
extraction.EntitiesCommon.Add(common);
var details = ExtractDetails(entity, objectName);
var item = Merge(common, details);
if (IsDimension(objectName))
{
extraction.Dimensions.Add(item);
var tolerance = ExtractDimensionalTolerance(common, details);
if (tolerance != null)
extraction.DimensionalTolerances.Add(tolerance);
}
else if (IsGeometry(objectName))
{
extraction.Geometry.Add(item);
}
else if (IsAnnotation(objectName))
{
extraction.Annotations.Add(item);
}
else
{
extraction.UnknownEntities.Add(item);
}
var text = details.TryGetValue("text", out var t) ? Convert.ToString(t, CultureInfo.InvariantCulture) ?? "" : "";
var blockName = details.TryGetValue("effective_name", out var b) ? Convert.ToString(b, CultureInfo.InvariantCulture) ?? "" : "";
if (LooksLikeRoughness(text, blockName))
extraction.Roughness.Add(Merge(common, details));
if (LooksLikeGeometricTolerance(text, blockName))
extraction.GeometricTolerances.Add(Merge(common, details));
if (LooksLikeDatum(text, blockName))
extraction.Datums.Add(Merge(common, details));
}
static Dictionary<string, object?> ExtractDetails(object entity, string objectName)
{
var details = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
if (objectName.Contains("Line", StringComparison.OrdinalIgnoreCase))
{
AddPoint(details, "start", GetProperty(entity, "StartPoint"));
AddPoint(details, "end", GetProperty(entity, "EndPoint"));
}
else if (objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase))
{
AddPoint(details, "center", GetProperty(entity, "Center"));
var radius = ToDouble(GetProperty(entity, "Radius"));
details["radius"] = radius;
details["diameter"] = radius == null ? null : radius * 2;
}
else if (objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase))
{
AddPoint(details, "center", GetProperty(entity, "Center"));
details["radius"] = ToDouble(GetProperty(entity, "Radius"));
details["start_angle"] = ToDouble(GetProperty(entity, "StartAngle"));
details["end_angle"] = ToDouble(GetProperty(entity, "EndAngle"));
}
else if (objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase))
{
AddPoint(details, "start", GetProperty(entity, "StartPoint"));
AddPoint(details, "end", GetProperty(entity, "EndPoint"));
details["degree"] = ToInt(GetProperty(entity, "Degree"));
details["closed"] = ToBool(GetProperty(entity, "Closed"));
}
else if (objectName.Contains("Text", StringComparison.OrdinalIgnoreCase))
{
var raw = ReadString(entity, "TextString");
details["text"] = CleanAcadText(raw);
details["raw_text"] = raw;
details["height"] = ToDouble(GetProperty(entity, "Height"));
details["rotation"] = ToDouble(GetProperty(entity, "Rotation"));
AddPoint(details, "insertion_point", GetProperty(entity, "InsertionPoint"));
}
else if (objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase))
{
var textOverride = ReadString(entity, "TextOverride");
details["measurement"] = ToDouble(GetProperty(entity, "Measurement"));
details["text_override"] = textOverride;
details["text"] = CleanAcadText(textOverride);
var missingPointFields = new List<string>();
AddPointFromProperties(details, "text_position", entity, missingPointFields, "TextPosition");
AddPointFromProperties(details, "xline1_point", entity, missingPointFields, "XLine1Point", "ExtLine1Point", "ExtensionLine1Point");
AddPointFromProperties(details, "xline2_point", entity, missingPointFields, "XLine2Point", "ExtLine2Point", "ExtensionLine2Point");
AddPointFromProperties(details, "dim_line_point", entity, missingPointFields, "DimLinePoint", "DimLineLocation");
AddPointFromProperties(details, "center", entity, missingPointFields, "Center");
AddPointFromProperties(details, "chord_point", entity, missingPointFields, "ChordPoint");
AddPointFromProperties(details, "far_chord_point", entity, missingPointFields, "FarChordPoint");
if (missingPointFields.Count > 0)
details["dimension_point_missing_fields"] = missingPointFields;
}
else if (objectName.Contains("BlockReference", StringComparison.OrdinalIgnoreCase))
{
details["name"] = ReadString(entity, "Name");
details["effective_name"] = ReadString(entity, "EffectiveName");
details["rotation"] = ToDouble(GetProperty(entity, "Rotation"));
details["x_scale_factor"] = ToDouble(GetProperty(entity, "XScaleFactor"));
details["y_scale_factor"] = ToDouble(GetProperty(entity, "YScaleFactor"));
AddPoint(details, "insertion_point", GetProperty(entity, "InsertionPoint"));
var attributes = ReadBlockAttributes(entity);
if (attributes.Count > 0)
{
details["attributes"] = attributes;
details["text"] = string.Join(" ", attributes.Select(a => a.TryGetValue("value", out var value) ? value : null));
}
}
else if (objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase))
{
details["pattern_name"] = ReadString(entity, "PatternName");
details["pattern_scale"] = ToDouble(GetProperty(entity, "PatternScale"));
}
else if (objectName.Contains("RasterImage", StringComparison.OrdinalIgnoreCase))
{
details["image_file"] = ReadString(entity, "ImageFile");
}
return details;
}
static List<Dictionary<string, object?>> ReadBlockAttributes(object entity)
{
var result = new List<Dictionary<string, object?>>();
var hasAttributes = ToBool(GetProperty(entity, "HasAttributes")) ?? false;
if (!hasAttributes)
return result;
var raw = TryInvoke(entity, "GetAttributes", []);
if (raw == null)
return result;
if (raw is Array array)
{
foreach (var item in array)
{
if (item == null)
continue;
result.Add(new Dictionary<string, object?>
{
["tag"] = ReadString(item, "TagString"),
["value"] = CleanAcadText(ReadString(item, "TextString")),
["raw_value"] = ReadString(item, "TextString")
});
ReleaseCom(item);
}
}
return result;
}
static Dictionary<string, object?>? ExtractDimensionalTolerance(
Dictionary<string, object?> common,
Dictionary<string, object?> details)
{
var raw = details.TryGetValue("text_override", out var rawValue) ? Convert.ToString(rawValue, CultureInfo.InvariantCulture) ?? "" : "";
var text = details.TryGetValue("text", out var textValue) ? Convert.ToString(textValue, CultureInfo.InvariantCulture) ?? "" : "";
if (!DimensionalToleranceParser.LooksLikeTolerance(raw, text))
return null;
var measurement = details.TryGetValue("measurement", out var measurementValue)
? ToDouble(measurementValue)
: null;
var objectName = common.TryGetValue("object_name", out var objectNameValue)
? Convert.ToString(objectNameValue, CultureInfo.InvariantCulture) ?? ""
: "";
var parsed = DimensionalToleranceParser.Parse(text, raw, measurement, objectName);
var result = Merge(Merge(common, details), parsed);
result["measurement"] = measurementValue;
result["numeric_values"] = ExtractNumbers(text);
return result;
}
DwgDraftManifest WriteExtractionFiles(
string outputDir,
string drawingPath,
string progId,
bool started,
DwgDraftDocumentExtraction extraction)
{
var files = new List<DwgDraftCategoryFile>
{
WriteCategory(outputDir, "entities-common", "entities-common.json", extraction.EntitiesCommon),
WriteCategory(outputDir, "geometry", "geometry.json", extraction.Geometry),
WriteCategory(outputDir, "annotations", "annotations.json", extraction.Annotations),
WriteCategory(outputDir, "dimensions", "dimensions.json", extraction.Dimensions),
WriteCategory(outputDir, "dimensional-tolerances", "dimensional-tolerances.json", extraction.DimensionalTolerances),
WriteCategory(outputDir, "roughness", "roughness.json", extraction.Roughness),
WriteCategory(outputDir, "geometric-tolerances", "geometric-tolerances.json", extraction.GeometricTolerances),
WriteCategory(outputDir, "datums", "datums.json", extraction.Datums),
WriteCategory(outputDir, "unknown-entities", "unknown-entities.json", extraction.UnknownEntities)
};
var manifest = new DwgDraftManifest
{
Ok = true,
Schema = "agent4.dwg-draft.extraction.v1",
Source = "AutoCAD COM",
ProgId = progId,
StartedAutoCad = started,
DrawingPath = drawingPath,
ExportedAtUtc = DateTime.UtcNow,
ModelSpaceCount = extraction.ModelSpaceCount,
PaperSpaceCount = extraction.PaperSpaceCount,
EntityCount = extraction.EntitiesCommon.Count,
TypeCounts = extraction.TypeCounts,
CategoryFiles = files
};
WriteJson(Path.Combine(outputDir, "manifest.json"), manifest);
return manifest;
}
public DwgDraftLatestExtractionResult ReadLatestExtraction()
{
var path = ResolveManifestPath("");
if (!File.Exists(path))
{
return new DwgDraftLatestExtractionResult
{
Ok = false,
Found = false,
ExportDir = Path.GetDirectoryName(path) ?? "",
ManifestPath = path,
Message = "DWG extraction manifest was not found. Call POST /api/dwg-draft/extract first."
};
}
using var doc = JsonDocument.Parse(File.ReadAllText(path));
var root = doc.RootElement.Clone();
var files = ReadCategoryFiles(root, Path.GetDirectoryName(path) ?? "");
return new DwgDraftLatestExtractionResult
{
Ok = true,
Found = true,
ExportDir = Path.GetDirectoryName(path) ?? "",
ManifestPath = path,
LastWriteTimeUtc = File.GetLastWriteTimeUtc(path),
Manifest = root,
CategoryFiles = files,
Message = "Read the latest AutoCAD DWG extraction manifest."
};
}
public DwgDraftCategoryExtractionResult ReadCategoryExtraction(string category)
{
if (string.IsNullOrWhiteSpace(category))
throw new ArgumentException("category is required.", nameof(category));
var manifestPath = ResolveManifestPath("");
if (!File.Exists(manifestPath))
{
return new DwgDraftCategoryExtractionResult
{
Ok = false,
Found = false,
Category = category,
ManifestPath = manifestPath,
Message = "DWG extraction manifest was not found."
};
}
using var manifestDoc = JsonDocument.Parse(File.ReadAllText(manifestPath));
var exportDir = Path.GetDirectoryName(manifestPath) ?? "";
var files = ReadCategoryFiles(manifestDoc.RootElement, exportDir);
var selected = files.FirstOrDefault(file =>
string.Equals(file.Category, category, StringComparison.OrdinalIgnoreCase) ||
string.Equals(Path.GetFileNameWithoutExtension(file.File), category, StringComparison.OrdinalIgnoreCase));
if (selected == null || !File.Exists(selected.FullPath))
{
return new DwgDraftCategoryExtractionResult
{
Ok = false,
Found = false,
Category = category,
ManifestPath = manifestPath,
ExportDir = exportDir,
AvailableCategories = files.Select(file => file.Category).ToList(),
Message = $"DWG extraction category was not found: {category}"
};
}
using var categoryDoc = JsonDocument.Parse(File.ReadAllText(selected.FullPath));
return new DwgDraftCategoryExtractionResult
{
Ok = true,
Found = true,
Category = selected.Category,
ManifestPath = manifestPath,
ExportDir = exportDir,
CategoryPath = selected.FullPath,
LastWriteTimeUtc = File.GetLastWriteTimeUtc(selected.FullPath),
ItemCount = selected.ItemCount,
Data = categoryDoc.RootElement.Clone(),
AvailableCategories = files.Select(file => file.Category).ToList(),
Message = $"Read DWG extraction category: {selected.Category}"
};
}
string ResolveOutputDir(string requested)
{
if (!string.IsNullOrWhiteSpace(requested))
return Path.GetFullPath(requested.Trim().Trim('"'));
var configured = Environment.GetEnvironmentVariable("AGENT4_DWG_EXPORT_DIR");
if (!string.IsNullOrWhiteSpace(configured))
return Path.GetFullPath(configured.Trim().Trim('"'));
return Path.Combine(_paths.RuntimeDir, "dwg-draft", "latest-extraction");
}
static void ApplyDrawingPathBase64(DwgDraftExtractRequest request)
{
if (string.IsNullOrWhiteSpace(request.DrawingPathBase64))
return;
try
{
request.DrawingPath = Encoding.UTF8.GetString(Convert.FromBase64String(request.DrawingPathBase64.Trim()));
}
catch (Exception ex)
{
throw new ArgumentException($"drawing_path_base64 不是有效的 UTF-8 Base64 路径:{ex.Message}", nameof(request));
}
}
string ResolveManifestPath(string requestedDir)
{
var dir = ResolveOutputDir(requestedDir);
return Path.Combine(dir, "manifest.json");
}
static DwgDraftCategoryFile WriteCategory(string outputDir, string category, string file, object data)
{
var path = Path.Combine(outputDir, file);
WriteJson(path, data);
var itemCount = data is System.Collections.ICollection collection ? collection.Count : 0;
return new DwgDraftCategoryFile
{
Category = category,
File = file,
FullPath = path,
ItemCount = itemCount,
Exists = true
};
}
static void WriteJson(string path, object data)
{
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? ".");
File.WriteAllText(path, JsonSerializer.Serialize(data, JsonOptions));
}
static List<DwgDraftCategoryFile> ReadCategoryFiles(JsonElement manifest, string exportDir)
{
if (!manifest.TryGetProperty("category_files", out var filesElement) || filesElement.ValueKind != JsonValueKind.Array)
return [];
var result = new List<DwgDraftCategoryFile>();
foreach (var item in filesElement.EnumerateArray())
{
var category = item.TryGetProperty("category", out var categoryElement) ? categoryElement.GetString() ?? "" : "";
var file = item.TryGetProperty("file", out var fileElement) ? fileElement.GetString() ?? "" : "";
var itemCount = item.TryGetProperty("item_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number
? countElement.GetInt32()
: 0;
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(file))
continue;
var fullPath = Path.GetFullPath(Path.Combine(exportDir, file));
var root = Path.GetFullPath(exportDir);
if (!fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase))
continue;
result.Add(new DwgDraftCategoryFile
{
Category = category,
File = file,
FullPath = fullPath,
ItemCount = itemCount,
Exists = File.Exists(fullPath)
});
}
return result;
}
static string ResolveProgId()
{
foreach (var progId in ProgIdCandidates)
{
if (Type.GetTypeFromProgID(progId, throwOnError: false) != null)
return progId;
}
return DefaultProgId;
}
static object ConnectOrCreate(string progId, bool visible, out bool started)
{
var app = TryGetActiveComObject(progId, out _);
started = false;
if (app == null)
{
var type = Type.GetTypeFromProgID(progId, throwOnError: true)
?? throw new InvalidOperationException($"找不到 AutoCAD COM ProgID{progId}");
app = Activator.CreateInstance(type)
?? throw new InvalidOperationException($"创建 AutoCAD COM 对象失败:{progId}");
started = true;
}
TrySetProperty(app, "Visible", visible);
return app;
}
static object OpenDocument(object app, string drawingPath)
{
var documents = GetProperty(app, "Documents") ?? throw new InvalidOperationException("AutoCAD.Application.Documents 不可用。");
try
{
var doc = TryInvoke(documents, "Open", [drawingPath, true])
?? TryInvoke(documents, "Open", [drawingPath])
?? throw new InvalidOperationException($"AutoCAD 打开 DWG 失败:{drawingPath}");
return doc;
}
finally
{
ReleaseCom(documents);
}
}
static void WaitForQuiescent(object app, TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
try
{
var state = TryInvoke(app, "GetAcadState", []);
if (state == null)
return;
var isQuiescent = ToBool(GetProperty(state, "IsQuiescent")) ?? true;
ReleaseCom(state);
if (isQuiescent)
return;
}
catch
{
return;
}
Thread.Sleep(250);
}
}
static object? GetProperty(object obj, string name) => RetryCom(() =>
{
try
{
return obj.GetType().InvokeMember(
name,
BindingFlags.GetProperty,
binder: null,
target: obj,
args: null,
culture: CultureInfo.InvariantCulture);
}
catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
{
throw;
}
catch (COMException ex) when (IsRetryableCom(ex))
{
throw;
}
catch
{
return null;
}
});
static object? GetCollectionItem(object obj, int index) => RetryCom(() =>
{
foreach (var flags in new[] { BindingFlags.GetProperty, BindingFlags.InvokeMethod })
{
try
{
return obj.GetType().InvokeMember(
"Item",
flags,
binder: null,
target: obj,
args: [index],
culture: CultureInfo.InvariantCulture);
}
catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
{
throw;
}
catch (COMException ex) when (IsRetryableCom(ex))
{
throw;
}
catch
{
// Try the next dispatch style.
}
}
return null;
});
static object? TryInvoke(object obj, string name, object?[] args) => RetryCom(() =>
{
try
{
return obj.GetType().InvokeMember(
name,
BindingFlags.InvokeMethod,
binder: null,
target: obj,
args: args,
culture: CultureInfo.InvariantCulture);
}
catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
{
throw;
}
catch (COMException ex) when (IsRetryableCom(ex))
{
throw;
}
catch
{
return null;
}
});
static void TrySetProperty(object obj, string name, object? value)
{
RetryCom(() =>
{
try
{
obj.GetType().InvokeMember(
name,
BindingFlags.SetProperty,
binder: null,
target: obj,
args: [value],
culture: CultureInfo.InvariantCulture);
}
catch
{
// Optional property.
}
return (object?)null;
});
}
static T RunSta<T>(Func<T> action)
{
T? result = default;
Exception? error = null;
var thread = new Thread(() =>
{
try
{
result = action();
}
catch (Exception ex)
{
error = ex;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (error != null)
throw error;
return result!;
}
static object? RetryCom(Func<object?> action)
{
Exception? last = null;
for (var attempt = 0; attempt < 120; attempt++)
{
try
{
return action();
}
catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException))
{
last = ex.InnerException;
Thread.Sleep(100);
}
catch (COMException ex) when (IsRetryableCom(ex))
{
last = ex;
Thread.Sleep(100);
}
}
if (last != null)
throw last;
return action();
}
static bool IsRetryableCom(Exception? ex)
{
if (ex is not COMException com)
return false;
return unchecked((uint)com.HResult) is 0x80010001 or 0x8001010A;
}
static Dictionary<string, object?> Merge(
Dictionary<string, object?> common,
Dictionary<string, object?> details)
{
var result = new Dictionary<string, object?>(common, StringComparer.OrdinalIgnoreCase);
foreach (var item in details)
result[item.Key] = item.Value;
return result;
}
static bool IsGeometry(string objectName)
{
return objectName.Contains("Line", StringComparison.OrdinalIgnoreCase) ||
objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase) ||
objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase) ||
objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase) ||
objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase) ||
objectName.Contains("Wipeout", StringComparison.OrdinalIgnoreCase) ||
objectName.Contains("RasterImage", StringComparison.OrdinalIgnoreCase);
}
static bool IsDimension(string objectName) => objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase);
static bool IsAnnotation(string objectName)
{
return objectName.Contains("Text", StringComparison.OrdinalIgnoreCase) ||
objectName.Contains("BlockReference", StringComparison.OrdinalIgnoreCase);
}
static bool LooksLikeRoughness(string text, string blockName)
{
var merged = text + " " + blockName;
return Regex.IsMatch(merged, @"\bR[az]\s*\d", RegexOptions.IgnoreCase) ||
merged.Contains("粗糙", StringComparison.OrdinalIgnoreCase) ||
merged.Contains("其余", StringComparison.OrdinalIgnoreCase) ||
merged.Contains("rough", StringComparison.OrdinalIgnoreCase);
}
static bool LooksLikeGeometricTolerance(string text, string blockName)
{
var merged = text + " " + blockName;
return merged.Contains("位置度", StringComparison.OrdinalIgnoreCase) ||
merged.Contains("同轴度", StringComparison.OrdinalIgnoreCase) ||
merged.Contains("平行度", StringComparison.OrdinalIgnoreCase) ||
merged.Contains("垂直度", StringComparison.OrdinalIgnoreCase) ||
merged.Contains("圆跳动", StringComparison.OrdinalIgnoreCase) ||
merged.Contains("geotol", StringComparison.OrdinalIgnoreCase) ||
Regex.IsMatch(merged, @"[⊥∥⌖◎⌯⌭]");
}
static bool LooksLikeDatum(string text, string blockName)
{
var merged = text + " " + blockName;
return merged.Contains("基准", StringComparison.OrdinalIgnoreCase) ||
merged.Contains("datum", StringComparison.OrdinalIgnoreCase);
}
static List<double> ExtractNumbers(string text)
{
return Regex.Matches(text, @"[+-]?\d+(?:\.\d+)?")
.Select(match => double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : (double?)null)
.Where(value => value != null)
.Select(value => value!.Value)
.ToList();
}
static string CleanAcadText(string text)
{
if (string.IsNullOrWhiteSpace(text))
return "";
var value = text.Replace("%%C", "Φ", StringComparison.OrdinalIgnoreCase)
.Replace("%%D", "°", StringComparison.OrdinalIgnoreCase)
.Replace("%%P", "±", StringComparison.OrdinalIgnoreCase);
value = Regex.Replace(value, @"\\A\d+;", "");
value = Regex.Replace(value, @"\\H[^;]+;", "");
value = Regex.Replace(value, @"\\S([^;]+)\^([^;]*);", "$1/$2");
value = Regex.Replace(value, @"\\S([^;]+);", "$1");
value = Regex.Replace(value, @"[{}]", "");
value = value.Replace(@"\P", " ");
return Regex.Replace(value, @"\s+", " ").Trim();
}
static void AddPoint(Dictionary<string, object?> details, string name, object? value)
{
var point = ToPoint(value);
if (point != null)
details[name] = point;
}
static void AddPointFromProperties(
Dictionary<string, object?> details,
string name,
object entity,
List<string> missingFields,
params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
{
var point = ToPoint(GetProperty(entity, propertyName));
if (point == null)
continue;
details[name] = point;
details[$"{name}_source_property"] = propertyName;
return;
}
missingFields.Add(name);
}
static List<double>? ToPoint(object? value)
{
if (value is Array array)
{
var result = new List<double>();
foreach (var item in array)
{
if (ToDouble(item) is { } number)
result.Add(number);
}
return result.Count > 0 ? result : null;
}
return null;
}
static string ReadString(object obj, string name)
{
return Convert.ToString(GetProperty(obj, name), CultureInfo.InvariantCulture) ?? "";
}
static int? ToInt(object? value)
{
try
{
return value == null ? null : Convert.ToInt32(value, CultureInfo.InvariantCulture);
}
catch
{
return null;
}
}
static double? ToDouble(object? value)
{
try
{
return value == null ? null : Convert.ToDouble(value, CultureInfo.InvariantCulture);
}
catch
{
return null;
}
}
static bool? ToBool(object? value)
{
try
{
return value == null ? null : Convert.ToBoolean(value, CultureInfo.InvariantCulture);
}
catch
{
return null;
}
}
static void ReleaseCom(object obj)
{
try
{
if (Marshal.IsComObject(obj))
Marshal.FinalReleaseComObject(obj);
}
catch
{
// Best-effort COM cleanup.
}
}
static DwgDraftProgIdInfo ReadProgIdInfo(string progId)
{
var clsid = ReadDefaultValue($@"{progId}\CLSID");
return new DwgDraftProgIdInfo
{
ProgId = progId,
Description = ReadDefaultValue(progId),
Clsid = clsid,
LocalServer32 = string.IsNullOrWhiteSpace(clsid) ? "" : ReadDefaultValue($@"CLSID\{clsid}\LocalServer32")
};
}
static string ReadDefaultValue(string subKey)
{
using var key = Registry.ClassesRoot.OpenSubKey(subKey);
return key?.GetValue("")?.ToString() ?? "";
}
static object? TryGetActiveComObject(string progId, out string error)
{
error = "";
try
{
var hr = CLSIDFromProgID(progId, out var clsid);
if (hr < 0)
Marshal.ThrowExceptionForHR(hr);
return GetActiveObject(ref clsid, IntPtr.Zero);
}
catch (Exception ex)
{
error = ex.Message;
return null;
}
}
[DllImport("ole32.dll", CharSet = CharSet.Unicode)]
static extern int CLSIDFromProgID(string progId, out Guid clsid);
[DllImport("oleaut32.dll", PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
static extern object GetActiveObject(ref Guid clsid, IntPtr reserved);
}
sealed class DwgDraftExtractRequest
{
public string DrawingPath { get; set; } = "";
public string DrawingPathBase64 { get; set; } = "";
public string ProgId { get; set; } = "";
public bool Visible { get; set; } = true;
public bool CloseAfterExtract { get; set; }
public string OutputDir { get; set; } = "";
public int MaxEntities { get; set; } = 100_000;
public string MainViewSeedMode { get; set; } = "";
public string ReuseExtractionDir { get; set; } = "";
public bool UseBridge { get; set; } = true;
public int BridgeTimeoutSeconds { get; set; } = 600;
}
sealed class DwgBridgeJob
{
public string JobId { get; set; } = "";
public string DrawingPath { get; set; } = "";
public string OutputDir { get; set; } = "";
public string ProgId { get; set; } = "";
public bool Visible { get; set; } = true;
public bool CloseAfterExtract { get; set; }
public int MaxEntities { get; set; } = 100_000;
public string MainViewSeedMode { get; set; } = "";
public string ReuseExtractionDir { get; set; } = "";
}
sealed class DwgBridgeJobResult
{
public bool Ok { get; set; }
public string JobId { get; set; } = "";
public int ExitCode { get; set; }
public string Stdout { get; set; } = "";
public string Stderr { get; set; } = "";
public string OutputDir { get; set; } = "";
public string ManifestPath { get; set; } = "";
public string Error { get; set; } = "";
}
sealed class DwgDraftBridgeStatus
{
public bool Ok { get; set; }
public bool Alive { get; set; }
public string BridgeDir { get; set; } = "";
public string HeartbeatPath { get; set; } = "";
public DateTime? LastWriteTimeUtc { get; set; }
public JsonElement? Heartbeat { get; set; }
public string Message { get; set; } = "";
}
sealed class DwgDraftDocumentExtraction
{
public int ModelSpaceCount { get; set; }
public int PaperSpaceCount { get; set; }
public Dictionary<string, int> TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<Dictionary<string, object?>> EntitiesCommon { get; set; } = new();
public List<Dictionary<string, object?>> Geometry { get; set; } = new();
public List<Dictionary<string, object?>> Annotations { get; set; } = new();
public List<Dictionary<string, object?>> Dimensions { get; set; } = new();
public List<Dictionary<string, object?>> DimensionalTolerances { get; set; } = new();
public List<Dictionary<string, object?>> Roughness { get; set; } = new();
public List<Dictionary<string, object?>> GeometricTolerances { get; set; } = new();
public List<Dictionary<string, object?>> Datums { get; set; } = new();
public List<Dictionary<string, object?>> UnknownEntities { get; set; } = new();
}
sealed class DwgDraftInstallation
{
public bool Found { get; set; }
public string DefaultProgId { get; set; } = "";
public List<DwgDraftProgIdInfo> ProgIds { get; set; } = new();
public List<string> AcadExeCandidates { get; set; } = new();
public List<string> Notes { get; set; } = new();
}
sealed class DwgDraftProgIdInfo
{
public string ProgId { get; set; } = "";
public string Description { get; set; } = "";
public string Clsid { get; set; } = "";
public string LocalServer32 { get; set; } = "";
}
sealed class DwgDraftExtractionResult
{
public bool Ok { get; set; }
public string Source { get; set; } = "";
public string ProgId { get; set; } = "";
public bool Started { get; set; }
public string DrawingPath { get; set; } = "";
public string ExportDir { get; set; } = "";
public string ManifestPath { get; set; } = "";
public int EntityCount { get; set; }
public Dictionary<string, int> TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<DwgDraftCategoryFile> CategoryFiles { get; set; } = new();
public string Message { get; set; } = "";
}
sealed class DwgDraftManifest
{
public bool Ok { get; set; }
public string Schema { get; set; } = "";
public string Source { get; set; } = "";
public string ProgId { get; set; } = "";
public bool StartedAutoCad { get; set; }
public string DrawingPath { get; set; } = "";
public DateTime ExportedAtUtc { get; set; }
public int ModelSpaceCount { get; set; }
public int PaperSpaceCount { get; set; }
public int EntityCount { get; set; }
public Dictionary<string, int> TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<DwgDraftCategoryFile> CategoryFiles { get; set; } = new();
}
sealed class DwgDraftCategoryFile
{
public string Category { get; set; } = "";
public string File { get; set; } = "";
public string FullPath { get; set; } = "";
public int ItemCount { get; set; }
public bool Exists { get; set; }
}
sealed class DwgDraftLatestExtractionResult
{
public bool Ok { get; set; }
public bool Found { get; set; }
public string ExportDir { get; set; } = "";
public string ManifestPath { get; set; } = "";
public DateTime? LastWriteTimeUtc { get; set; }
public JsonElement? Manifest { get; set; }
public List<DwgDraftCategoryFile> CategoryFiles { get; set; } = new();
public string Message { get; set; } = "";
}
sealed class DwgDraftCategoryExtractionResult
{
public bool Ok { get; set; }
public bool Found { get; set; }
public string Category { get; set; } = "";
public string ExportDir { get; set; } = "";
public string ManifestPath { get; set; } = "";
public string CategoryPath { get; set; } = "";
public DateTime? LastWriteTimeUtc { get; set; }
public int ItemCount { get; set; }
public JsonElement? Data { get; set; }
public List<string> AvailableCategories { get; set; } = new();
public string Message { get; set; } = "";
}