658 lines
23 KiB
C#
658 lines
23 KiB
C#
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text.Json;
|
|
using Microsoft.Win32;
|
|
|
|
var options = CliOptions.Parse(args);
|
|
if (!options.HasReferenceCenter && (string.IsNullOrWhiteSpace(options.ReferenceDwgPath) || !File.Exists(options.ReferenceDwgPath)))
|
|
throw new FileNotFoundException($"参考 DWG 不存在: {options.ReferenceDwgPath}");
|
|
if (string.IsNullOrWhiteSpace(options.StudentDwgPath) || !File.Exists(options.StudentDwgPath))
|
|
throw new FileNotFoundException($"学生 DWG 不存在: {options.StudentDwgPath}");
|
|
if (string.IsNullOrWhiteSpace(options.OutputDwgPath))
|
|
throw new ArgumentException("缺少 --output-dwg。为避免破坏原始学生图,必须输出到副本。");
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(options.OutputDwgPath) ?? Environment.CurrentDirectory);
|
|
var result = StaRunner.Run(() => DwgSheetFrameAligner.Align(options));
|
|
var reportPath = Path.Combine(
|
|
Path.GetDirectoryName(options.OutputDwgPath) ?? Environment.CurrentDirectory,
|
|
Path.GetFileNameWithoutExtension(options.OutputDwgPath) + ".align-report.json");
|
|
File.WriteAllText(reportPath, JsonSerializer.Serialize(result, JsonDefaults.Options));
|
|
Console.WriteLine(JsonSerializer.Serialize(result, JsonDefaults.Options));
|
|
|
|
sealed class DwgSheetFrameAligner
|
|
{
|
|
const double AxisTolerance = 0.05;
|
|
const double FrameTolerance = 1.0;
|
|
|
|
[DllImport("ole32.dll", CharSet = CharSet.Unicode)]
|
|
private static extern int CLSIDFromProgID(string progId, out Guid clsid);
|
|
|
|
[DllImport("oleaut32.dll", PreserveSig = false)]
|
|
[return: MarshalAs(UnmanagedType.IUnknown)]
|
|
private static extern object GetActiveObject(ref Guid clsid, IntPtr reserved);
|
|
|
|
public static AlignResult Align(CliOptions options)
|
|
{
|
|
var started = DateTimeOffset.Now;
|
|
var app = ConnectOrCreate(options.ProgId, options.Visible, out _);
|
|
object? referenceDoc = null;
|
|
object? studentDoc = null;
|
|
try
|
|
{
|
|
FrameRect referenceFrame;
|
|
if (options.HasReferenceCenter)
|
|
{
|
|
referenceFrame = FrameRect.FromCenter(options.ReferenceCenterX!.Value, options.ReferenceCenterY!.Value);
|
|
}
|
|
else
|
|
{
|
|
WaitForQuiescent(app, TimeSpan.FromSeconds(30));
|
|
referenceDoc = OpenDocument(app, options.ReferenceDwgPath, readOnly: true);
|
|
referenceFrame = DetectInnerFrame(referenceDoc);
|
|
TryInvoke(referenceDoc, "Close", [false]);
|
|
ReleaseCom(referenceDoc);
|
|
referenceDoc = null;
|
|
}
|
|
|
|
File.Copy(options.StudentDwgPath, options.OutputDwgPath, overwrite: true);
|
|
WaitForQuiescent(app, TimeSpan.FromSeconds(30));
|
|
studentDoc = OpenDocument(app, options.OutputDwgPath, readOnly: false);
|
|
var studentFrameBefore = DetectInnerFrame(studentDoc);
|
|
|
|
var dx = referenceFrame.CenterX - studentFrameBefore.CenterX;
|
|
var dy = referenceFrame.CenterY - studentFrameBefore.CenterY;
|
|
var moved = MoveAllEntities(studentDoc, dx, dy);
|
|
TryInvoke(studentDoc, "Save", []);
|
|
|
|
var studentFrameAfter = DetectInnerFrame(studentDoc);
|
|
if (options.CloseAfterAlign)
|
|
TryInvoke(studentDoc, "Close", [false]);
|
|
|
|
return new AlignResult
|
|
{
|
|
Ok = true,
|
|
StartedAt = started,
|
|
CompletedAt = DateTimeOffset.Now,
|
|
ReferenceDwgPath = options.ReferenceDwgPath,
|
|
StudentDwgPath = options.StudentDwgPath,
|
|
OutputDwgPath = options.OutputDwgPath,
|
|
ReferenceInnerFrame = referenceFrame,
|
|
StudentInnerFrameBefore = studentFrameBefore,
|
|
StudentInnerFrameAfter = studentFrameAfter,
|
|
DeltaX = dx,
|
|
DeltaY = dy,
|
|
MovedEntityCount = moved,
|
|
Message = "已按图框内圈中心整体平移学生 DWG 副本。"
|
|
};
|
|
}
|
|
finally
|
|
{
|
|
if (referenceDoc != null)
|
|
{
|
|
TryInvoke(referenceDoc, "Close", [false]);
|
|
ReleaseCom(referenceDoc);
|
|
}
|
|
if (studentDoc != null)
|
|
ReleaseCom(studentDoc);
|
|
ReleaseCom(app);
|
|
}
|
|
}
|
|
|
|
static FrameRect DetectInnerFrame(object doc)
|
|
{
|
|
var lines = new List<Line2d>();
|
|
lines.AddRange(ReadLines(doc, "ModelSpace"));
|
|
lines.AddRange(ReadLines(doc, "PaperSpace"));
|
|
if (lines.Count == 0)
|
|
throw new InvalidOperationException("DWG 中没有可用于识别图框的直线。");
|
|
|
|
var horizontal = lines
|
|
.Where(line => Math.Abs(line.Y1 - line.Y2) <= AxisTolerance)
|
|
.Select(line => line.Normalized())
|
|
.ToList();
|
|
var vertical = lines
|
|
.Where(line => Math.Abs(line.X1 - line.X2) <= AxisTolerance)
|
|
.Select(line => line.Normalized())
|
|
.ToList();
|
|
|
|
var maxHorizontal = horizontal.Select(line => line.Length).DefaultIfEmpty(0).Max();
|
|
var maxVertical = vertical.Select(line => line.Length).DefaultIfEmpty(0).Max();
|
|
var longHorizontal = horizontal
|
|
.Where(line => line.Length >= maxHorizontal * 0.65)
|
|
.OrderByDescending(line => line.Length)
|
|
.Take(20)
|
|
.ToList();
|
|
var longVertical = vertical
|
|
.Where(line => line.Length >= maxVertical * 0.65)
|
|
.OrderByDescending(line => line.Length)
|
|
.Take(20)
|
|
.ToList();
|
|
|
|
var rectangles = new List<FrameRect>();
|
|
for (var hi = 0; hi < longHorizontal.Count; hi++)
|
|
{
|
|
for (var hj = hi + 1; hj < longHorizontal.Count; hj++)
|
|
{
|
|
var bottom = longHorizontal[hi].Y1 <= longHorizontal[hj].Y1 ? longHorizontal[hi] : longHorizontal[hj];
|
|
var top = ReferenceEquals(bottom, longHorizontal[hi]) ? longHorizontal[hj] : longHorizontal[hi];
|
|
for (var vi = 0; vi < longVertical.Count; vi++)
|
|
{
|
|
for (var vj = vi + 1; vj < longVertical.Count; vj++)
|
|
{
|
|
var left = longVertical[vi].X1 <= longVertical[vj].X1 ? longVertical[vi] : longVertical[vj];
|
|
var right = ReferenceEquals(left, longVertical[vi]) ? longVertical[vj] : longVertical[vi];
|
|
if (!SpansX(bottom, left.X1, right.X1) || !SpansX(top, left.X1, right.X1))
|
|
continue;
|
|
if (!SpansY(left, bottom.Y1, top.Y1) || !SpansY(right, bottom.Y1, top.Y1))
|
|
continue;
|
|
|
|
rectangles.Add(new FrameRect
|
|
{
|
|
MinX = left.X1,
|
|
MinY = bottom.Y1,
|
|
MaxX = right.X1,
|
|
MaxY = top.Y1,
|
|
SourceHandles =
|
|
[
|
|
bottom.Handle,
|
|
top.Handle,
|
|
left.Handle,
|
|
right.Handle
|
|
]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var unique = rectangles
|
|
.GroupBy(rect => $"{Math.Round(rect.MinX, 3)}|{Math.Round(rect.MinY, 3)}|{Math.Round(rect.MaxX, 3)}|{Math.Round(rect.MaxY, 3)}")
|
|
.Select(group => group.First())
|
|
.Where(rect => rect.Width > 0 && rect.Height > 0)
|
|
.OrderByDescending(rect => rect.Area)
|
|
.ToList();
|
|
|
|
if (unique.Count == 0)
|
|
throw new InvalidOperationException("未能识别图框矩形。");
|
|
|
|
// 工程图一般有外框和内框;面积最大的是外框,第二大的是内框。
|
|
return unique.Count >= 2 ? unique[1] : unique[0];
|
|
}
|
|
|
|
static bool SpansX(Line2d line, double left, double right) =>
|
|
line.MinX <= left + FrameTolerance && line.MaxX >= right - FrameTolerance;
|
|
|
|
static bool SpansY(Line2d line, double bottom, double top) =>
|
|
line.MinY <= bottom + FrameTolerance && line.MaxY >= top - FrameTolerance;
|
|
|
|
static List<Line2d> ReadLines(object doc, string spaceName)
|
|
{
|
|
var result = new List<Line2d>();
|
|
var space = GetProperty(doc, spaceName);
|
|
if (space == null)
|
|
return result;
|
|
|
|
try
|
|
{
|
|
var count = ToInt(GetProperty(space, "Count")) ?? 0;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var entity = GetCollectionItem(space, i);
|
|
if (entity == null)
|
|
continue;
|
|
try
|
|
{
|
|
var objectName = ReadString(entity, "ObjectName");
|
|
if (!string.Equals(objectName, "AcDbLine", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
var start = DoubleList(GetProperty(entity, "StartPoint"));
|
|
var end = DoubleList(GetProperty(entity, "EndPoint"));
|
|
if (start.Count < 2 || end.Count < 2)
|
|
continue;
|
|
|
|
result.Add(new Line2d(
|
|
start[0],
|
|
start[1],
|
|
end[0],
|
|
end[1],
|
|
ReadString(entity, "Handle"),
|
|
spaceName));
|
|
}
|
|
finally
|
|
{
|
|
ReleaseCom(entity);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ReleaseCom(space);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int MoveAllEntities(object doc, double dx, double dy)
|
|
{
|
|
var moved = 0;
|
|
moved += MoveSpaceEntities(doc, "ModelSpace", dx, dy);
|
|
moved += MoveSpaceEntities(doc, "PaperSpace", dx, dy);
|
|
return moved;
|
|
}
|
|
|
|
static int MoveSpaceEntities(object doc, string spaceName, double dx, double dy)
|
|
{
|
|
var moved = 0;
|
|
var space = GetProperty(doc, spaceName);
|
|
if (space == null)
|
|
return moved;
|
|
|
|
try
|
|
{
|
|
var count = ToInt(GetProperty(space, "Count")) ?? 0;
|
|
var from = new double[] { 0, 0, 0 };
|
|
var to = new double[] { dx, dy, 0 };
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var entity = GetCollectionItem(space, i);
|
|
if (entity == null)
|
|
continue;
|
|
try
|
|
{
|
|
TryInvoke(entity, "Move", [from, to]);
|
|
moved++;
|
|
}
|
|
finally
|
|
{
|
|
ReleaseCom(entity);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ReleaseCom(space);
|
|
}
|
|
|
|
return moved;
|
|
}
|
|
|
|
static object ConnectOrCreate(string progId, bool visible, out DateTimeOffset started)
|
|
{
|
|
started = DateTimeOffset.Now;
|
|
object? app = null;
|
|
try
|
|
{
|
|
var type = Type.GetTypeFromProgID(progId);
|
|
if (type != null)
|
|
app = GetActiveObjectFromProgId(progId);
|
|
}
|
|
catch
|
|
{
|
|
app = null;
|
|
}
|
|
|
|
if (app == null)
|
|
{
|
|
StartAutoCadFromRegistry(progId);
|
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(60);
|
|
while (DateTime.UtcNow < deadline && app == null)
|
|
{
|
|
try { app = GetActiveObjectFromProgId(progId); }
|
|
catch { Thread.Sleep(500); }
|
|
}
|
|
}
|
|
|
|
if (app == null)
|
|
throw new InvalidOperationException($"AutoCAD 启动或 COM 连接失败: {progId}");
|
|
|
|
TrySetProperty(app, "Visible", visible);
|
|
return app;
|
|
}
|
|
|
|
static object GetActiveObjectFromProgId(string progId)
|
|
{
|
|
var hr = CLSIDFromProgID(progId, out var clsid);
|
|
if (hr < 0)
|
|
Marshal.ThrowExceptionForHR(hr);
|
|
return GetActiveObject(ref clsid, IntPtr.Zero);
|
|
}
|
|
|
|
static void StartAutoCadFromRegistry(string progId)
|
|
{
|
|
var clsid = ReadDefaultValue($@"{progId}\CLSID");
|
|
var command = string.IsNullOrWhiteSpace(clsid) ? "" : ReadDefaultValue($@"CLSID\{clsid}\LocalServer32");
|
|
var exe = ExtractExePath(command);
|
|
if (string.IsNullOrWhiteSpace(exe) || !File.Exists(exe))
|
|
exe = @"D:\Program Files\Autodesk\AutoCAD 2020\acad.exe";
|
|
if (!File.Exists(exe))
|
|
throw new FileNotFoundException($"找不到 acad.exe。ProgID={progId}, LocalServer32={command}");
|
|
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = exe,
|
|
Arguments = "/Automation",
|
|
UseShellExecute = true,
|
|
WorkingDirectory = Path.GetDirectoryName(exe) ?? Environment.CurrentDirectory
|
|
});
|
|
}
|
|
|
|
static string ReadDefaultValue(string subKey)
|
|
{
|
|
using var key = Registry.ClassesRoot.OpenSubKey(subKey);
|
|
return key?.GetValue("")?.ToString() ?? "";
|
|
}
|
|
|
|
static string ExtractExePath(string command)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(command))
|
|
return "";
|
|
var trimmed = command.Trim();
|
|
if (trimmed.StartsWith('"'))
|
|
{
|
|
var end = trimmed.IndexOf('"', 1);
|
|
return end > 1 ? trimmed[1..end] : "";
|
|
}
|
|
var exeIndex = trimmed.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
|
|
return exeIndex >= 0 ? trimmed[..(exeIndex + 4)] : trimmed;
|
|
}
|
|
|
|
static object OpenDocument(object app, string drawingPath, bool readOnly)
|
|
{
|
|
var documents = GetProperty(app, "Documents") ?? throw new InvalidOperationException("AutoCAD.Application.Documents 不可用。");
|
|
try
|
|
{
|
|
return TryInvoke(documents, "Open", [drawingPath, readOnly])
|
|
?? TryInvoke(documents, "Open", [drawingPath])
|
|
?? throw new InvalidOperationException($"AutoCAD 打开 DWG 失败: {drawingPath}");
|
|
}
|
|
finally
|
|
{
|
|
ReleaseCom(documents);
|
|
}
|
|
}
|
|
|
|
static void WaitForQuiescent(object app, TimeSpan timeout)
|
|
{
|
|
var deadline = DateTime.UtcNow + timeout;
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
var state = TryInvoke(app, "GetAcadState", []);
|
|
if (state == null)
|
|
return;
|
|
try
|
|
{
|
|
if (ToBool(GetProperty(state, "IsQuiescent")) ?? true)
|
|
return;
|
|
}
|
|
finally
|
|
{
|
|
ReleaseCom(state);
|
|
}
|
|
Thread.Sleep(250);
|
|
}
|
|
}
|
|
|
|
static object? GetProperty(object obj, string name) => RetryCom(() =>
|
|
{
|
|
try
|
|
{
|
|
return obj.GetType().InvokeMember(name, BindingFlags.GetProperty, null, obj, null, 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, null, obj, [index], CultureInfo.InvariantCulture); }
|
|
catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException)) { throw; }
|
|
catch (COMException ex) when (IsRetryableCom(ex)) { throw; }
|
|
catch { }
|
|
}
|
|
return null;
|
|
});
|
|
|
|
static object? TryInvoke(object obj, string name, object?[] args) => RetryCom(() =>
|
|
{
|
|
try { return obj.GetType().InvokeMember(name, BindingFlags.InvokeMethod, null, obj, args, 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, null, obj, [value], CultureInfo.InvariantCulture); }
|
|
catch { }
|
|
return null;
|
|
});
|
|
}
|
|
|
|
static object? RetryCom(Func<object?> action)
|
|
{
|
|
Exception? last = null;
|
|
for (var i = 0; i < 180; i++)
|
|
{
|
|
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) =>
|
|
ex is COMException com && unchecked((uint)com.HResult) is 0x80010001 or 0x8001010A;
|
|
|
|
static string ReadString(object obj, string name) => Convert.ToString(GetProperty(obj, name), CultureInfo.InvariantCulture) ?? "";
|
|
|
|
static int? ToInt(object? value)
|
|
{
|
|
if (value == null)
|
|
return null;
|
|
return int.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
|
? parsed
|
|
: null;
|
|
}
|
|
|
|
static bool? ToBool(object? value)
|
|
{
|
|
if (value == null)
|
|
return null;
|
|
if (value is bool b)
|
|
return b;
|
|
return bool.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), out var parsed) ? parsed : null;
|
|
}
|
|
|
|
static List<double> DoubleList(object? value)
|
|
{
|
|
if (value == null)
|
|
return [];
|
|
if (value is double[] doubles)
|
|
return doubles.ToList();
|
|
if (value is object[] objects)
|
|
return objects.SelectMany(DoubleList).ToList();
|
|
if (value is Array array)
|
|
{
|
|
var result = new List<double>();
|
|
foreach (var item in array)
|
|
{
|
|
if (double.TryParse(Convert.ToString(item, CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
|
|
result.Add(parsed);
|
|
}
|
|
return result;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
static void ReleaseCom(object? obj)
|
|
{
|
|
if (obj != null && Marshal.IsComObject(obj))
|
|
Marshal.ReleaseComObject(obj);
|
|
}
|
|
}
|
|
|
|
sealed class CliOptions
|
|
{
|
|
public string ReferenceDwgPath { get; set; } = "";
|
|
public string StudentDwgPath { get; set; } = "";
|
|
public string OutputDwgPath { get; set; } = "";
|
|
public string ProgId { get; set; } = "AutoCAD.Application.23.1";
|
|
public double? ReferenceCenterX { get; set; }
|
|
public double? ReferenceCenterY { get; set; }
|
|
public bool HasReferenceCenter => ReferenceCenterX != null && ReferenceCenterY != null;
|
|
public bool Visible { get; set; } = true;
|
|
public bool CloseAfterAlign { get; set; } = true;
|
|
|
|
public static CliOptions Parse(string[] args)
|
|
{
|
|
var options = new CliOptions();
|
|
for (var i = 0; i < args.Length; i++)
|
|
{
|
|
var key = args[i];
|
|
var value = i + 1 < args.Length ? args[i + 1] : "";
|
|
switch (key)
|
|
{
|
|
case "--reference-dwg":
|
|
options.ReferenceDwgPath = Path.GetFullPath(value.Trim().Trim('"'));
|
|
i++;
|
|
break;
|
|
case "--student-dwg":
|
|
options.StudentDwgPath = Path.GetFullPath(value.Trim().Trim('"'));
|
|
i++;
|
|
break;
|
|
case "--output-dwg":
|
|
options.OutputDwgPath = Path.GetFullPath(value.Trim().Trim('"'));
|
|
i++;
|
|
break;
|
|
case "--reference-center-x":
|
|
options.ReferenceCenterX = double.Parse(value, CultureInfo.InvariantCulture);
|
|
i++;
|
|
break;
|
|
case "--reference-center-y":
|
|
options.ReferenceCenterY = double.Parse(value, CultureInfo.InvariantCulture);
|
|
i++;
|
|
break;
|
|
case "--prog-id":
|
|
options.ProgId = value;
|
|
i++;
|
|
break;
|
|
case "--visible":
|
|
options.Visible = bool.TryParse(value, out var visible) ? visible : options.Visible;
|
|
i++;
|
|
break;
|
|
case "--close-after-align":
|
|
options.CloseAfterAlign = bool.TryParse(value, out var close) ? close : options.CloseAfterAlign;
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
return options;
|
|
}
|
|
}
|
|
|
|
sealed record Line2d(double X1, double Y1, double X2, double Y2, string Handle, string SpaceName)
|
|
{
|
|
public double Length => Math.Sqrt(Math.Pow(X2 - X1, 2) + Math.Pow(Y2 - Y1, 2));
|
|
public double MinX => Math.Min(X1, X2);
|
|
public double MaxX => Math.Max(X1, X2);
|
|
public double MinY => Math.Min(Y1, Y2);
|
|
public double MaxY => Math.Max(Y1, Y2);
|
|
|
|
public Line2d Normalized()
|
|
{
|
|
if (Math.Abs(Y1 - Y2) <= Math.Abs(X1 - X2))
|
|
return X1 <= X2 ? this : this with { X1 = X2, Y1 = Y2, X2 = X1, Y2 = Y1 };
|
|
return Y1 <= Y2 ? this : this with { X1 = X2, Y1 = Y2, X2 = X1, Y2 = Y1 };
|
|
}
|
|
}
|
|
|
|
sealed class FrameRect
|
|
{
|
|
public double MinX { get; set; }
|
|
public double MinY { get; set; }
|
|
public double MaxX { get; set; }
|
|
public double MaxY { get; set; }
|
|
public double Width => MaxX - MinX;
|
|
public double Height => MaxY - MinY;
|
|
public double Area => Width * Height;
|
|
public double CenterX => (MinX + MaxX) / 2.0;
|
|
public double CenterY => (MinY + MaxY) / 2.0;
|
|
public List<string> SourceHandles { get; set; } = [];
|
|
|
|
public static FrameRect FromCenter(double x, double y)
|
|
{
|
|
return new FrameRect
|
|
{
|
|
MinX = x,
|
|
MaxX = x,
|
|
MinY = y,
|
|
MaxY = y,
|
|
SourceHandles = ["explicit_reference_center"]
|
|
};
|
|
}
|
|
}
|
|
|
|
sealed class AlignResult
|
|
{
|
|
public bool Ok { get; set; }
|
|
public DateTimeOffset StartedAt { get; set; }
|
|
public DateTimeOffset CompletedAt { get; set; }
|
|
public string ReferenceDwgPath { get; set; } = "";
|
|
public string StudentDwgPath { get; set; } = "";
|
|
public string OutputDwgPath { get; set; } = "";
|
|
public FrameRect ReferenceInnerFrame { get; set; } = new();
|
|
public FrameRect StudentInnerFrameBefore { get; set; } = new();
|
|
public FrameRect StudentInnerFrameAfter { get; set; } = new();
|
|
public double DeltaX { get; set; }
|
|
public double DeltaY { get; set; }
|
|
public int MovedEntityCount { get; set; }
|
|
public string Message { get; set; } = "";
|
|
}
|
|
|
|
static class StaRunner
|
|
{
|
|
public static T Run<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 class JsonDefaults
|
|
{
|
|
public static readonly JsonSerializerOptions Options = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
|
};
|
|
}
|