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(); 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(); 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 ReadLines(object doc, string spaceName) { var result = new List(); 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 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 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(); 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 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(Func 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 }; }