1535 lines
57 KiB
C#
1535 lines
57 KiB
C#
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; } = "";
|
||
}
|