using System.Globalization; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using SolidWorks.Interop.sldworks; using SolidWorks.Interop.swconst; internal static class Program { [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); private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower }; private static int Main(string[] args) { try { var request = CandidateRequest.Parse(args); request.ModelPath = NormalizePath(request.ModelPath); request.OutputDir = NormalizePath(request.OutputDir); request.DrawingTemplatePath = NormalizePath(request.DrawingTemplatePath); request.RankingJsonPath = NormalizePath(request.RankingJsonPath); request.StudentSignaturePath = NormalizePath(request.StudentSignaturePath); request.ReferenceSignaturePath = NormalizePath(request.ReferenceSignaturePath); request.StudentViewRegionsPath = NormalizePath(request.StudentViewRegionsPath); request.ReferenceViewRegionsPath = NormalizePath(request.ReferenceViewRegionsPath); request.OutputDwgPath = NormalizePath(request.OutputDwgPath); request.SectionCutSymbolsPath = NormalizePath(request.SectionCutSymbolsPath); if (!request.UseActiveModel && (string.IsNullOrWhiteSpace(request.ModelPath) || !File.Exists(request.ModelPath))) throw new FileNotFoundException($"模型文件不存在: {request.ModelPath}"); if (!request.UseActiveModel && !request.ModelPath.EndsWith(".SLDASM", StringComparison.OrdinalIgnoreCase) && !request.ModelPath.EndsWith(".SLDPRT", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("--model 必须是 .SLDASM 或 .SLDPRT 文件。"); if (string.IsNullOrWhiteSpace(request.OutputDir)) request.OutputDir = Path.Combine(System.Environment.CurrentDirectory, "runtime", "solidworks-view-candidates", Path.GetFileNameWithoutExtension(request.ModelPath)); Directory.CreateDirectory(request.OutputDir); var sw = ConnectSolidWorks(); var result = Generate(sw, request); WriteJson(Path.Combine(request.OutputDir, "candidate-result.json"), result); WriteJson(Path.Combine(request.OutputDir, "candidate-views.json"), result.Candidates); Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); return result.Ok ? 0 : 1; } catch (Exception ex) { Console.Error.WriteLine(ex); return 1; } } private static SldWorks ConnectSolidWorks() { try { var hr = CLSIDFromProgID("SldWorks.Application", out var clsid); if (hr < 0) Marshal.ThrowExceptionForHR(hr); var active = (SldWorks)GetActiveObject(ref clsid, IntPtr.Zero); active.Visible = true; return active; } catch { var type = Type.GetTypeFromProgID("SldWorks.Application") ?? throw new InvalidOperationException("无法创建 SolidWorks COM 对象。"); var sw = (SldWorks)(Activator.CreateInstance(type) ?? throw new InvalidOperationException("SolidWorks COM 创建结果为空。")); sw.Visible = true; return sw; } } private static CandidateResult Generate(SldWorks sw, CandidateRequest request) { var started = DateTimeOffset.Now; var model = ResolveModel(sw, request); request.ModelPath = NormalizePath(Safe(() => model.GetPathName(), request.ModelPath)); if (string.IsNullOrWhiteSpace(request.ModelPath)) throw new InvalidOperationException("无法确定模型路径,CreateDrawViewFromModelView3 需要模型文件路径。"); var mathUtility = sw.GetMathUtility() ?? throw new InvalidOperationException("SolidWorks GetMathUtility 返回空。"); if (request.ExportActiveDrawingDwg) { var exportDrawingContext = ResolveDrawing(sw, request); return ExportActiveDrawingDwg(exportDrawingContext, request, started); } if (request.ListDrawingViews) { var listDrawingContext = ResolveDrawing(sw, request); return ListDrawingViews(listDrawingContext, request, started); } if (request.AlignMainViewOrigin) { var alignDrawingContext = ResolveDrawing(sw, request); return AlignPlacedMainViewToSignatureOrigin(alignDrawingContext, request, started); } if (request.AlignProjectedViewsToRegions) { var alignDrawingContext = ResolveDrawing(sw, request); return AlignProjectedViewToDwgRegionCenter(alignDrawingContext, request, started); } if (request.AlignSectionViewsToRegions) { var alignDrawingContext = ResolveDrawing(sw, request); return AlignSectionViewsToDwgRegionCenters(alignDrawingContext, request, started); } if (request.PlaceProjectedView) { var projectedDrawingContext = ResolveDrawing(sw, request); return PlaceProjectedView(projectedDrawingContext, request, started); } if (request.PlaceSectionViews) { var sectionDrawingContext = ResolveDrawing(sw, request); return PlaceSectionViews(sectionDrawingContext, request, started); } if (request.PlaceMainView && request.CandidateIndex == null) request.CandidateIndex = ReadBestCandidateIndex(request.RankingJsonPath); var candidates = BuildCandidates(request.ViewNamePrefix); if (request.CandidateIndex != null) candidates = candidates.Where(candidate => candidate.Index == request.CandidateIndex.Value).ToList(); if (candidates.Count == 0) throw new InvalidOperationException($"没有找到候选视图: index={request.CandidateIndex}"); if (request.PlaceMainView) RenameCandidates(candidates, request.MainViewNamePrefix); CreateNamedViews(model, mathUtility, candidates, request.KeepNamedViews); var drawingContext = ResolveDrawing(sw, request); var drawingModel = drawingContext.Model; var drawing = drawingContext.Drawing; if (request.ExportCandidateDwgsSequential) { CleanupCandidateDrawingViews(drawingModel, drawing, request.ViewNamePrefix); ExportCandidateDwgsSequential(drawingModel, drawing, request.ModelPath, candidates, request); if (!request.KeepNamedViews) DeleteNamedViews(model, candidates); return new CandidateResult { Ok = true, Message = "已串行导出 24 个候选视图 DWG;每次只插入并保存一个候选视图。", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Candidates = candidates, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["candidate_dwg_dir"] = CandidateDwgDirectory(request), ["candidate_dwg_count"] = candidates.Count(candidate => candidate.DwgSaveOk && File.Exists(candidate.DwgPath)).ToString(CultureInfo.InvariantCulture) }, SourceApis = [ "IModelView.Orientation3", "IModelDoc2.NameView", "IDrawingDoc.CreateDrawViewFromModelView3", "IModelDocExtension.SaveAs", "IModelDocExtension.SelectByID2", "IModelDocExtension.DeleteSelection2", "IModelDoc2.DeleteNamedView" ] }; } if (request.PlaceMainView) { var candidate = candidates.Single(); if (request.ReplaceExistingMainView) CleanupCandidateDrawingViews(drawingModel, drawing, request.MainViewNamePrefix); var view = InsertSingleCandidateView(drawing, request.ModelPath, candidate, request) ?? throw new InvalidOperationException($"插入主视图失败: {candidate.Name}"); Safe(() => drawingModel.ForceRebuild3(false), false); candidate.DrawingViewName = Safe(() => view.GetName2(), candidate.DrawingViewName); candidate.OrientationName = Safe(() => view.GetOrientationName(), candidate.OrientationName); candidate.Position = DoubleList(Safe(() => view.Position, null)); candidate.ScaleDecimal = Safe(() => view.ScaleDecimal, 0.0); candidate.UseSheetScale = Safe(() => view.UseSheetScale, 0); return new CandidateResult { Ok = true, Message = "已在当前 SolidWorks 工程图中放置最优主视图。", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Candidates = candidates, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["placed_main_view_name"] = candidate.DrawingViewName, ["placed_orientation_name"] = candidate.OrientationName, ["candidate_index"] = candidate.Index.ToString(CultureInfo.InvariantCulture), ["direction_name"] = candidate.DirectionName, ["roll_degrees"] = candidate.RollDeg.ToString(CultureInfo.InvariantCulture), ["scale_decimal"] = candidate.ScaleDecimal.ToString(CultureInfo.InvariantCulture) }, SourceApis = [ "IModelView.Orientation3", "IModelDoc2.NameView", "IDrawingDoc.CreateDrawViewFromModelView3", "IModelDocExtension.SelectByID2", "IModelDocExtension.DeleteSelection2" ] }; } if (DateTimeOffset.Now.Ticks >= 0) throw new InvalidOperationException("当前流程只支持 --export-candidate-dwgs-sequential:每次插入一个候选视图、另存为一个 DWG、删除该视图,再交给 AutoCAD 提取和排序。旧的 SolidWorks 工程图 polyline 直接签名/排序方法已废弃。"); return new CandidateResult { Ok = true, Message = "已生成 24 个 SolidWorks 候选主视图。", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Candidates = candidates, SourceApis = [ "IModelView.Orientation3", "IModelDoc2.NameView", "IDrawingDoc.CreateDrawViewFromModelView3", "ISldWorks.IGetFirstDocument2", "IModelDoc2.DeleteNamedView" ] }; } private static ModelDoc2 ResolveModel(SldWorks sw, CandidateRequest request) { if (request.UseActiveModel) { var active = sw.IActiveDoc2 as ModelDoc2; if (active != null && IsPartOrAssembly(active)) return active; var openModel = FindOpenModel(sw); if (openModel != null) return openModel; throw new InvalidOperationException("没有找到已打开的零件或装配体。请先打开模型,或使用 --model 指定路径。"); } return OpenModel(sw, request.ModelPath); } private static ModelDoc2 OpenModel(SldWorks sw, string modelPath) { var extension = Path.GetExtension(modelPath); var type = extension.Equals(".SLDASM", StringComparison.OrdinalIgnoreCase) ? (int)swDocumentTypes_e.swDocASSEMBLY : (int)swDocumentTypes_e.swDocPART; var errors = 0; var warnings = 0; var model = sw.OpenDoc6( modelPath, type, (int)swOpenDocOptions_e.swOpenDocOptions_Silent, "", ref errors, ref warnings) as ModelDoc2; if (model == null) throw new InvalidOperationException($"打开模型失败: errors={errors}, warnings={warnings}"); Safe(() => { var activateErrors = 0; sw.ActivateDoc3(model.GetTitle(), false, 0, ref activateErrors); return activateErrors; }, 0); return model; } private static DrawingContext ResolveDrawing(SldWorks sw, CandidateRequest request) { if (request.UseActiveDrawing) { var drawingModel = FindOpenDrawing(sw) ?? throw new InvalidOperationException("没有找到已打开的工程图。请先打开 .SLDDRW,再运行 --use-active-drawing。"); return new DrawingContext( drawingModel, drawingModel as DrawingDoc ?? throw new InvalidOperationException("已打开文档不是 DrawingDoc。"), "", Safe(() => drawingModel.GetPathName(), "")); } var templatePath = ResolveTemplatePath(sw, request); Safe(() => { sw.PreSelectDwgTemplateSize((int)swDwgPaperSizes_e.swDwgPaperA2size, ""); return true; }, false); var drawingModelNew = sw.NewDocument( templatePath, (int)swDwgPaperSizes_e.swDwgPaperA2size, request.SheetWidthM, request.SheetHeightM) as ModelDoc2 ?? throw new InvalidOperationException("SolidWorks NewDocument 未返回工程图。"); var drawing = drawingModelNew as DrawingDoc ?? throw new InvalidOperationException("新建文档不是 DrawingDoc。"); return new DrawingContext(drawingModelNew, drawing, templatePath, Safe(() => drawingModelNew.GetPathName(), "")); } private static ModelDoc2? FindOpenDrawing(SldWorks sw) { var active = sw.IActiveDoc2 as ModelDoc2; if (active != null && Safe(() => active.GetType(), 0) == (int)swDocumentTypes_e.swDocDRAWING) return active; var doc = sw.IGetFirstDocument2(); while (doc != null) { if (Safe(() => doc.GetType(), 0) == (int)swDocumentTypes_e.swDocDRAWING) return doc; doc = Safe(() => doc.IGetNext(), null); } return null; } private static ModelDoc2? FindOpenModel(SldWorks sw) { var doc = sw.IGetFirstDocument2(); while (doc != null) { if (IsPartOrAssembly(doc)) return doc; doc = Safe(() => doc.IGetNext(), null); } return null; } private static bool IsPartOrAssembly(ModelDoc2 doc) { var type = Safe(() => doc.GetType(), 0); return type == (int)swDocumentTypes_e.swDocPART || type == (int)swDocumentTypes_e.swDocASSEMBLY; } private static string ResolveTemplatePath(SldWorks sw, CandidateRequest request) { if (!string.IsNullOrWhiteSpace(request.DrawingTemplatePath)) { if (!File.Exists(request.DrawingTemplatePath)) throw new FileNotFoundException($"工程图模板不存在: {request.DrawingTemplatePath}"); return request.DrawingTemplatePath; } var templatePath = sw.GetDocumentTemplate((int)swDocumentTypes_e.swDocDRAWING, "", 0, request.SheetWidthM, request.SheetHeightM); if (string.IsNullOrWhiteSpace(templatePath) || !File.Exists(templatePath)) throw new FileNotFoundException($"无法解析 SolidWorks 工程图模板: {templatePath}"); return templatePath; } private static List BuildCandidates(string prefix) { var directions = new[] { ("px", new[] { 1.0, 0.0, 0.0 }), ("nx", new[] { -1.0, 0.0, 0.0 }), ("py", new[] { 0.0, 1.0, 0.0 }), ("ny", new[] { 0.0, -1.0, 0.0 }), ("pz", new[] { 0.0, 0.0, 1.0 }), ("nz", new[] { 0.0, 0.0, -1.0 }) }; var rolls = new[] { 0, 90, 180, 270 }; var result = new List(); var index = 0; foreach (var (directionName, direction) in directions) { foreach (var roll in rolls) { index++; var name = $"{prefix}_{index:00}_{directionName}_r{roll}"; var basis = BuildBasis(direction, roll); result.Add(new ViewCandidate { Index = index, Name = name, DirectionName = directionName, RollDeg = roll, ViewDirection = direction.ToList(), BasisU = basis.U.ToList(), BasisV = basis.V.ToList(), BasisD = basis.D.ToList() }); } } return result; } private static void RenameCandidates(List candidates, string prefix) { foreach (var candidate in candidates) candidate.Name = $"{prefix}_{candidate.Index:00}_{candidate.DirectionName}_r{candidate.RollDeg}"; } private static int ReadBestCandidateIndex(string rankingJsonPath) { if (string.IsNullOrWhiteSpace(rankingJsonPath)) throw new ArgumentException("--place-main-view 需要同时传 --candidate-index,或传 --ranking-json 自动读取 rank 1。"); if (!File.Exists(rankingJsonPath)) throw new FileNotFoundException($"排序结果文件不存在: {rankingJsonPath}"); using var document = JsonDocument.Parse(File.ReadAllText(rankingJsonPath)); if (!document.RootElement.TryGetProperty("ranking", out var ranking) || ranking.ValueKind != JsonValueKind.Array || ranking.GetArrayLength() == 0) throw new InvalidOperationException($"排序结果中没有 ranking 数组: {rankingJsonPath}"); var best = ranking.EnumerateArray() .OrderBy(item => item.TryGetProperty("rank", out var rank) && rank.TryGetInt32(out var value) ? value : int.MaxValue) .First(); if (!best.TryGetProperty("index", out var indexElement) || !indexElement.TryGetInt32(out var index)) throw new InvalidOperationException($"排序第一名缺少 index 字段: {rankingJsonPath}"); return index; } private static CandidateResult AlignPlacedMainViewToSignatureOrigin( DrawingContext drawingContext, CandidateRequest request, DateTimeOffset started) { var studentOrigin = ReadSignatureOrigin(request.StudentSignaturePath, "student"); if (string.IsNullOrWhiteSpace(request.ReferenceSignaturePath)) throw new ArgumentException("--align-main-view-origin 只保留 DWG 对比路径,必须提供 --reference-signature。该文件应来自 SW 工程图导出 DWG 后的 AutoCAD 提取结果。"); var referenceOrigin = ReadSignatureOrigin(request.ReferenceSignaturePath, "reference"); var view = FindMainDrawingView(drawingContext.Drawing, request) ?? throw new InvalidOperationException($"没有找到需要平移的主视图。prefix={request.MainViewNamePrefix}, orientation={request.MainViewOrientationName}"); var viewName = Safe(() => Convert.ToString(view.GetName2(), CultureInfo.InvariantCulture) ?? "", ""); var orientationName = Safe(() => Convert.ToString(view.GetOrientationName(), CultureInfo.InvariantCulture) ?? "", ""); var deltaMmX = studentOrigin.X - referenceOrigin.X; var deltaMmY = studentOrigin.Y - referenceOrigin.Y; var appliedDeltaMmX = ShouldApplyAxis(request.AlignmentAxis, "x") ? deltaMmX : 0.0; var appliedDeltaMmY = ShouldApplyAxis(request.AlignmentAxis, "y") ? deltaMmY : 0.0; var deltaM = new[] { appliedDeltaMmX / 1000.0, appliedDeltaMmY / 1000.0 }; var before = DoubleList(Safe(() => view.Position, null)); if (before.Count < 2) throw new InvalidOperationException($"无法读取主视图 Position: {viewName}"); var after = new[] { before[0] + deltaM[0], before[1] + deltaM[1] }; Safe(() => { view.Position = after; return true; }, false); Safe(() => drawingContext.Model.ForceRebuild3(false), false); var actualAfter = DoubleList(Safe(() => view.Position, null)); return new CandidateResult { Ok = true, Message = "已按学生图左侧竖直线最上点整体平移 SolidWorks 主视图。", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["main_view_name"] = viewName, ["main_view_orientation_name"] = orientationName, ["student_origin_mm"] = $"{studentOrigin.X.ToString(CultureInfo.InvariantCulture)},{studentOrigin.Y.ToString(CultureInfo.InvariantCulture)}", ["reference_origin_mm"] = $"{referenceOrigin.X.ToString(CultureInfo.InvariantCulture)},{referenceOrigin.Y.ToString(CultureInfo.InvariantCulture)}", ["reference_origin_source"] = request.ReferenceSignaturePath, ["delta_mm"] = $"{deltaMmX.ToString(CultureInfo.InvariantCulture)},{deltaMmY.ToString(CultureInfo.InvariantCulture)}", ["alignment_axis"] = request.AlignmentAxis, ["applied_delta_mm"] = $"{appliedDeltaMmX.ToString(CultureInfo.InvariantCulture)},{appliedDeltaMmY.ToString(CultureInfo.InvariantCulture)}", ["delta_m"] = $"{deltaM[0].ToString(CultureInfo.InvariantCulture)},{deltaM[1].ToString(CultureInfo.InvariantCulture)}", ["position_before_m"] = $"{before[0].ToString(CultureInfo.InvariantCulture)},{before[1].ToString(CultureInfo.InvariantCulture)}", ["position_after_m"] = actualAfter.Count >= 2 ? $"{actualAfter[0].ToString(CultureInfo.InvariantCulture)},{actualAfter[1].ToString(CultureInfo.InvariantCulture)}" : $"{after[0].ToString(CultureInfo.InvariantCulture)},{after[1].ToString(CultureInfo.InvariantCulture)}" }, SourceApis = [ "IView.Position", "IView.GetOrientationName", "IView.GetName2", "IModelDoc2.ForceRebuild3" ] }; } private static bool ShouldApplyAxis(string axis, string component) { if (string.IsNullOrWhiteSpace(axis)) return true; if (string.Equals(axis, "xy", StringComparison.OrdinalIgnoreCase)) return true; return string.Equals(axis, component, StringComparison.OrdinalIgnoreCase); } private static CandidateResult AlignProjectedViewToDwgRegionCenter( DrawingContext drawingContext, CandidateRequest request, DateTimeOffset started) { if (string.IsNullOrWhiteSpace(request.StudentViewRegionsPath)) throw new ArgumentException("--align-projected-views-to-regions requires --student-view-regions."); if (string.IsNullOrWhiteSpace(request.ReferenceViewRegionsPath)) throw new ArgumentException("--align-projected-views-to-regions requires --reference-view-regions."); var direction = NormalizeProjectedDirection(request.ProjectedViewDirection); var regionKind = ProjectedDirectionToRegionKind(direction); var student = ReadOrthographicViewRegionCenter(request.StudentViewRegionsPath, regionKind, "student"); var reference = ReadOrthographicViewRegionCenter(request.ReferenceViewRegionsPath, regionKind, "reference"); var viewName = FirstNonEmpty( request.ProjectedViewName, direction == "right" ? "agent4_right_projected_view" : "agent4_bottom_projected_view"); var view = FindDrawingView(drawingContext.Drawing, viewName) ?? throw new InvalidOperationException($"Projected drawing view not found: {viewName}"); var deltaMmX = student.CenterX - reference.CenterX; var deltaMmY = student.CenterY - reference.CenterY; var appliedDeltaMmX = ShouldApplyAxis(request.AlignmentAxis, "x") ? deltaMmX : 0.0; var appliedDeltaMmY = ShouldApplyAxis(request.AlignmentAxis, "y") ? deltaMmY : 0.0; var before = DoubleList(Safe(() => view.Position, null)); if (before.Count < 2) throw new InvalidOperationException($"Cannot read projected view Position: {viewName}"); Safe(() => { view.PositionLocked = false; return true; }, false); var after = new[] { before[0] + appliedDeltaMmX / 1000.0, before[1] + appliedDeltaMmY / 1000.0 }; Safe(() => { view.Position = after; return true; }, false); Safe(() => drawingContext.Model.ForceRebuild3(false), false); var actualAfter = DoubleList(Safe(() => view.Position, null)); var reportPath = Path.Combine(request.OutputDir, "aligned-projected-view.json"); WriteJson(reportPath, new Dictionary { ["direction"] = direction, ["region_kind"] = regionKind, ["view_name"] = viewName, ["student_view_regions"] = request.StudentViewRegionsPath, ["reference_view_regions"] = request.ReferenceViewRegionsPath, ["alignment_axis"] = request.AlignmentAxis, ["student_center_mm"] = new[] { student.CenterX, student.CenterY }, ["reference_center_mm"] = new[] { reference.CenterX, reference.CenterY }, ["delta_mm"] = new[] { deltaMmX, deltaMmY }, ["applied_delta_mm"] = new[] { appliedDeltaMmX, appliedDeltaMmY }, ["position_before_m"] = before, ["position_after_m"] = actualAfter.Count >= 2 ? actualAfter : after, ["kept_projection_relation"] = true }); return new CandidateResult { Ok = true, Message = "Aligned projected drawing view to DWG-extracted orthographic region center.", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["student_view_regions"] = request.StudentViewRegionsPath, ["reference_view_regions"] = request.ReferenceViewRegionsPath, ["aligned_projected_view"] = reportPath, ["direction"] = direction, ["region_kind"] = regionKind, ["view_name"] = viewName, ["delta_mm"] = $"{deltaMmX.ToString(CultureInfo.InvariantCulture)},{deltaMmY.ToString(CultureInfo.InvariantCulture)}", ["applied_delta_mm"] = $"{appliedDeltaMmX.ToString(CultureInfo.InvariantCulture)},{appliedDeltaMmY.ToString(CultureInfo.InvariantCulture)}" }, SourceApis = [ "IView.Position", "IView.GetName2", "IModelDoc2.ForceRebuild3" ] }; } private static CandidateResult AlignSectionViewsToDwgRegionCenters( DrawingContext drawingContext, CandidateRequest request, DateTimeOffset started) { if (string.IsNullOrWhiteSpace(request.StudentViewRegionsPath)) throw new ArgumentException("--align-section-views-to-regions requires --student-view-regions."); if (string.IsNullOrWhiteSpace(request.ReferenceViewRegionsPath)) throw new ArgumentException("--align-section-views-to-regions requires --reference-view-regions."); var studentCenters = ReadSectionViewRegionCenters(request.StudentViewRegionsPath, "student"); var referenceCenters = ReadSectionViewRegionCenters(request.ReferenceViewRegionsPath, "reference"); if (!string.IsNullOrWhiteSpace(request.SectionLabel)) { var requestedLabel = NormalizeSectionRegionLabel(request.SectionLabel); studentCenters = studentCenters .Where(item => string.Equals(NormalizeSectionRegionLabel(item.Label), requestedLabel, StringComparison.OrdinalIgnoreCase)) .ToList(); referenceCenters = referenceCenters .Where(item => string.Equals(NormalizeSectionRegionLabel(item.Label), requestedLabel, StringComparison.OrdinalIgnoreCase)) .ToList(); } var referenceByLabel = referenceCenters .GroupBy(item => NormalizeSectionRegionLabel(item.Label), StringComparer.OrdinalIgnoreCase) .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); var matches = studentCenters .Select(student => new { Student = student, Reference = referenceByLabel.TryGetValue(NormalizeSectionRegionLabel(student.Label), out var reference) ? reference : null }) .Where(item => item.Reference != null) .ToList(); if (matches.Count == 0) throw new InvalidOperationException($"No matching section view labels between {request.StudentViewRegionsPath} and {request.ReferenceViewRegionsPath}"); var averageDeltaMmX = matches.Average(item => item.Student.CenterX - item.Reference!.CenterX); var averageDeltaMmY = matches.Average(item => item.Student.CenterY - item.Reference!.CenterY); var deltasByLabel = matches.ToDictionary( item => NormalizeSectionRegionLabel(item.Student.Label), item => { var deltaMmX = item.Student.CenterX - item.Reference!.CenterX; var deltaMmY = item.Student.CenterY - item.Reference.CenterY; var appliedDeltaMmX = ShouldApplyAxis(request.AlignmentAxis, "x") ? deltaMmX : 0.0; var appliedDeltaMmY = ShouldApplyAxis(request.AlignmentAxis, "y") ? deltaMmY : 0.0; return new SectionRegionDelta(deltaMmX, deltaMmY, appliedDeltaMmX, appliedDeltaMmY); }, StringComparer.OrdinalIgnoreCase); var matchedLabelKeys = matches .Select(item => NormalizeSectionRegionLabel(item.Student.Label)) .ToHashSet(StringComparer.OrdinalIgnoreCase); var moved = new List>(); var viewObj = drawingContext.Drawing.GetFirstView(); while (viewObj is View view) { var viewName = Safe(() => view.GetName2(), ""); var type = Safe(() => view.Type, 0); var matchedLabel = matchedLabelKeys.FirstOrDefault(label => IsLikelySectionViewForLabel(viewName, SectionViewLabelToken(label))); if (type == (int)swDrawingViewTypes_e.swDrawingSectionView && !string.IsNullOrWhiteSpace(matchedLabel) && deltasByLabel.TryGetValue(matchedLabel, out var delta)) { var before = DoubleList(Safe(() => view.Position, null)); if (before.Count >= 2) { var after = new[] { before[0] + delta.AppliedDeltaMmX / 1000.0, before[1] + delta.AppliedDeltaMmY / 1000.0 }; Safe(() => { view.PositionLocked = false; return true; }, false); var removedAlignment = Safe(() => { view.RemoveAlignment(); return true; }, false); Safe(() => { view.Position = after; return true; }, false); var actualAfter = DoubleList(Safe(() => view.Position, null)); moved.Add(new Dictionary { ["label"] = matchedLabel, ["view_name"] = viewName, ["delta_mm"] = new[] { delta.DeltaMmX, delta.DeltaMmY }, ["applied_delta_mm"] = new[] { delta.AppliedDeltaMmX, delta.AppliedDeltaMmY }, ["remove_alignment_called"] = removedAlignment, ["position_before_m"] = before, ["position_after_m"] = actualAfter.Count >= 2 ? actualAfter : after }); } } viewObj = view.GetNextView(); } if (moved.Count == 0) throw new InvalidOperationException($"No SolidWorks section drawing views matched labels: {string.Join(",", matchedLabelKeys)}"); Safe(() => drawingContext.Model.ForceRebuild3(false), false); var reportPath = Path.Combine(request.OutputDir, "aligned-section-views.json"); WriteJson(reportPath, new Dictionary { ["student_view_regions"] = request.StudentViewRegionsPath, ["reference_view_regions"] = request.ReferenceViewRegionsPath, ["alignment_axis"] = request.AlignmentAxis, ["matched_section_count"] = matches.Count, ["average_delta_mm"] = new[] { averageDeltaMmX, averageDeltaMmY }, ["matched_regions"] = matches.Select(item => new Dictionary { ["label"] = item.Student.Label, ["student_center_mm"] = new[] { item.Student.CenterX, item.Student.CenterY }, ["reference_center_mm"] = new[] { item.Reference!.CenterX, item.Reference.CenterY }, ["delta_mm"] = new[] { item.Student.CenterX - item.Reference.CenterX, item.Student.CenterY - item.Reference.CenterY } }).ToList(), ["moved_views"] = moved }); return new CandidateResult { Ok = true, Message = "Aligned SolidWorks section drawing views to DWG-extracted section region centers.", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["student_view_regions"] = request.StudentViewRegionsPath, ["reference_view_regions"] = request.ReferenceViewRegionsPath, ["aligned_section_views"] = reportPath, ["matched_section_count"] = matches.Count.ToString(CultureInfo.InvariantCulture), ["moved_view_count"] = moved.Count.ToString(CultureInfo.InvariantCulture), ["average_delta_mm"] = $"{averageDeltaMmX.ToString(CultureInfo.InvariantCulture)},{averageDeltaMmY.ToString(CultureInfo.InvariantCulture)}" }, SourceApis = [ "IView.Position", "IView.GetName2", "IView.Type", "IModelDoc2.ForceRebuild3" ] }; } private static List ReadSectionViewRegionCenters(string path, string label) { if (!File.Exists(path)) throw new FileNotFoundException($"{label} view-regions file not found: {path}"); using var document = JsonDocument.Parse(File.ReadAllText(path)); var root = document.RootElement; if (!root.TryGetProperty("section_views", out var sectionViews) || sectionViews.ValueKind != JsonValueKind.Array) throw new InvalidOperationException($"{label} view-regions missing section_views array: {path}"); var result = new List(); foreach (var item in sectionViews.EnumerateArray()) { if (!ReadBool(item, "ok")) continue; var sectionLabel = ReadString(item, "label"); if (string.IsNullOrWhiteSpace(sectionLabel)) continue; if (!item.TryGetProperty("bbox", out var bbox) || !TryReadJsonDouble(bbox, "center_x", out var centerX) || !TryReadJsonDouble(bbox, "center_y", out var centerY)) continue; result.Add(new SectionRegionCenter(sectionLabel, centerX, centerY)); } if (result.Count == 0) throw new InvalidOperationException($"{label} view-regions has no ok section view centers: {path}"); return result; } private static OrthographicRegionCenter ReadOrthographicViewRegionCenter(string path, string kind, string label) { if (!File.Exists(path)) throw new FileNotFoundException($"{label} view-regions file not found: {path}"); using var document = JsonDocument.Parse(File.ReadAllText(path)); var root = document.RootElement; if (!root.TryGetProperty("orthographic_views", out var views) || views.ValueKind != JsonValueKind.Array) throw new InvalidOperationException($"{label} view-regions missing orthographic_views array: {path}"); foreach (var item in views.EnumerateArray()) { if (!ReadBool(item, "ok")) continue; if (!string.Equals(ReadString(item, "kind"), kind, StringComparison.OrdinalIgnoreCase)) continue; if (!item.TryGetProperty("bbox", out var bbox) || !TryReadJsonDouble(bbox, "center_x", out var centerX) || !TryReadJsonDouble(bbox, "center_y", out var centerY)) continue; return new OrthographicRegionCenter(kind, centerX, centerY); } throw new InvalidOperationException($"{label} view-regions has no ok orthographic region kind={kind}: {path}"); } private static string NormalizeProjectedDirection(string value) { var direction = string.IsNullOrWhiteSpace(value) ? "down" : value.Trim().ToLowerInvariant(); return direction switch { "down" or "bottom" or "俯视图" => "down", "right" or "右视图" => "right", _ => throw new ArgumentException($"unsupported projected view direction: {value}") }; } private static string ProjectedDirectionToRegionKind(string direction) => NormalizeProjectedDirection(direction) == "right" ? "right_of_main_view_candidate" : "bottom_view_candidate"; private static bool TryReadJsonDouble(JsonElement element, string propertyName, out double value) { value = 0.0; return element.ValueKind == JsonValueKind.Object && element.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.Number && property.TryGetDouble(out value); } private static string NormalizeSectionRegionLabel(string label) { var cleaned = Regex.Replace(label.Trim().ToUpperInvariant(), @"\s+", "", RegexOptions.CultureInvariant); var match = Regex.Match(cleaned, @"^([A-Z0-9]{1,4})(?:-\1)?$", RegexOptions.CultureInvariant); return match.Success ? $"{match.Groups[1].Value}-{match.Groups[1].Value}" : cleaned; } private static string SectionViewLabelToken(string normalizedLabel) { var index = normalizedLabel.IndexOf('-', StringComparison.Ordinal); return index > 0 ? normalizedLabel[..index] : normalizedLabel; } private static CandidateResult ListDrawingViews( DrawingContext drawingContext, CandidateRequest request, DateTimeOffset started) { var views = new List>(); var viewObj = drawingContext.Drawing.GetFirstView(); while (viewObj is View view) { var name = Safe(() => view.GetName2(), ""); var orientationName = Safe(() => view.GetOrientationName(), ""); views.Add(new Dictionary { ["name"] = name, ["orientation_name"] = orientationName, ["type"] = Safe(() => view.Type, 0), ["position_m"] = DoubleList(Safe(() => view.Position, null)), ["outline_m"] = DoubleList(Safe(() => view.GetOutline(), null)), ["scale_decimal"] = Safe(() => view.ScaleDecimal, 0.0), ["use_sheet_scale"] = Safe(() => view.UseSheetScale, 0) }); viewObj = view.GetNextView(); } var outputPath = Path.Combine(request.OutputDir, "drawing-views.json"); WriteJson(outputPath, views); return new CandidateResult { Ok = true, Message = "Listed drawing views from the current SolidWorks drawing.", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["drawing_views"] = outputPath, ["drawing_view_count"] = views.Count.ToString(CultureInfo.InvariantCulture) }, SourceApis = [ "IDrawingDoc.GetFirstView", "IView.GetName2", "IView.GetOrientationName", "IView.Position", "IView.GetOutline" ] }; } private static CandidateResult ExportActiveDrawingDwg( DrawingContext drawingContext, CandidateRequest request, DateTimeOffset started) { var dwgPath = request.OutputDwgPath; if (string.IsNullOrWhiteSpace(dwgPath)) { var baseName = !string.IsNullOrWhiteSpace(drawingContext.DrawingPath) ? Path.GetFileNameWithoutExtension(drawingContext.DrawingPath) : "active-solidworks-drawing"; dwgPath = Path.Combine(request.OutputDir, SanitizeFileName(baseName) + ".dwg"); } var rebuildAttempt = 0; var maxRebuildAttempts = Math.Max(0, request.CandidateRebuildRetryCount); var save = new SaveResult(false, 0, 0); while (maxRebuildAttempts == 0 || rebuildAttempt < maxRebuildAttempts) { rebuildAttempt++; Safe(() => drawingContext.Model.ForceRebuild3(false), false); WaitBeforeNextCandidateAttempt(request); save = SaveAsWithRetries(drawingContext.Model, dwgPath, request.SaveRetryCount, request.SaveRetryDelayMs); if (save.Ok) break; Console.Error.WriteLine( $"当前工程图 DWG 导出失败,准备重建后重试:attempt={rebuildAttempt}, errors={save.Errors}, warnings={save.Warnings}"); } return new CandidateResult { Ok = save.Ok, Message = save.Ok ? "已导出当前 SolidWorks 工程图 DWG。" : "当前 SolidWorks 工程图 DWG 导出失败。", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["dwg_path"] = dwgPath, ["dwg_save_ok"] = save.Ok.ToString(CultureInfo.InvariantCulture), ["dwg_save_errors"] = save.Errors.ToString(CultureInfo.InvariantCulture), ["dwg_save_warnings"] = save.Warnings.ToString(CultureInfo.InvariantCulture), ["dwg_rebuild_attempts"] = rebuildAttempt.ToString(CultureInfo.InvariantCulture), ["dwg_rebuild_retry_limit"] = maxRebuildAttempts == 0 ? "unlimited" : maxRebuildAttempts.ToString(CultureInfo.InvariantCulture) }, SourceApis = [ "IModelDocExtension.SaveAs", "IModelDoc2.SaveAs4", "IModelDoc2.ForceRebuild3" ] }; } private static CandidateResult PlaceProjectedView( DrawingContext drawingContext, CandidateRequest request, DateTimeOffset started) { if (string.IsNullOrWhiteSpace(request.MainViewOrientationName)) throw new ArgumentException("--place-projected-view requires --main-view-orientation-name with the parent drawing view name or orientation name."); if (request.ProjectedViewXM == null || request.ProjectedViewYM == null) throw new ArgumentException("--place-projected-view requires --projected-view-x-m and --projected-view-y-m."); var parentView = FindMainDrawingView(drawingContext.Drawing, request) ?? throw new InvalidOperationException($"Parent drawing view not found: {request.MainViewOrientationName}"); var parentName = Safe(() => parentView.GetName2(), ""); var parentOrientation = Safe(() => parentView.GetOrientationName(), ""); var parentPosition = DoubleList(Safe(() => parentView.Position, null)); if (request.ReplaceExistingProjectedView && !string.IsNullOrWhiteSpace(request.ProjectedViewName)) { DeleteDrawingView(drawingContext.Model, new ViewCandidate { DrawingViewName = request.ProjectedViewName, OrientationName = request.ProjectedViewName, Name = request.ProjectedViewName }); } drawingContext.Model.ClearSelection2(true); var selected = Safe(() => drawingContext.Model.Extension.SelectByID2(parentName, "DRAWINGVIEW", 0, 0, 0, false, 0, null, 0), false); if (!selected) selected = Safe(() => drawingContext.Model.Extension.SelectByID2(request.MainViewOrientationName, "DRAWINGVIEW", 0, 0, 0, false, 0, null, 0), false); if (!selected) throw new InvalidOperationException($"Failed to select parent drawing view: {parentName}"); var view = drawingContext.Drawing.CreateUnfoldedViewAt3( request.ProjectedViewXM.Value, request.ProjectedViewYM.Value, 0, false) as View ?? throw new InvalidOperationException("CreateUnfoldedViewAt3 returned null."); if (!string.IsNullOrWhiteSpace(request.ProjectedViewName)) Safe(() => view.SetName2(request.ProjectedViewName), false); Safe(() => drawingContext.Model.ForceRebuild3(false), false); var viewName = Safe(() => view.GetName2(), ""); var viewOrientation = Safe(() => view.GetOrientationName(), ""); var viewPosition = DoubleList(Safe(() => view.Position, null)); return new CandidateResult { Ok = true, Message = "Created a projected drawing view from the current main view.", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["parent_view_name"] = parentName, ["parent_view_orientation_name"] = parentOrientation, ["parent_view_position_m"] = parentPosition.Count >= 2 ? $"{parentPosition[0].ToString(CultureInfo.InvariantCulture)},{parentPosition[1].ToString(CultureInfo.InvariantCulture)}" : "", ["projected_view_name"] = viewName, ["projected_view_orientation_name"] = viewOrientation, ["projected_view_type"] = Safe(() => view.Type, 0).ToString(CultureInfo.InvariantCulture), ["projected_view_position_m"] = viewPosition.Count >= 2 ? $"{viewPosition[0].ToString(CultureInfo.InvariantCulture)},{viewPosition[1].ToString(CultureInfo.InvariantCulture)}" : "", ["requested_position_m"] = $"{request.ProjectedViewXM.Value.ToString(CultureInfo.InvariantCulture)},{request.ProjectedViewYM.Value.ToString(CultureInfo.InvariantCulture)}", ["projection_direction"] = request.ProjectedViewDirection }, SourceApis = [ "IModelDocExtension.SelectByID2", "IDrawingDoc.CreateUnfoldedViewAt3", "IView.SetName2", "IView.Position", "IModelDoc2.ForceRebuild3" ] }; } private static CandidateResult PlaceSectionViews( DrawingContext drawingContext, CandidateRequest request, DateTimeOffset started) { if (string.IsNullOrWhiteSpace(request.SectionCutSymbolsPath)) throw new ArgumentException("--place-section-views requires --section-cut-symbols."); if (!File.Exists(request.SectionCutSymbolsPath)) throw new FileNotFoundException($"Section cut symbols file not found: {request.SectionCutSymbolsPath}"); var symbols = ReadSectionCutSymbols(request.SectionCutSymbolsPath) .Where(symbol => symbol.Ok && string.Equals(symbol.Kind, "section_cut_symbol_pair", StringComparison.OrdinalIgnoreCase)) .Where(symbol => string.IsNullOrWhiteSpace(request.SectionLabel) || string.Equals(symbol.Label, request.SectionLabel, StringComparison.OrdinalIgnoreCase)) .ToList(); if (symbols.Count == 0) throw new InvalidOperationException($"No section cut symbols to place. path={request.SectionCutSymbolsPath}, label={request.SectionLabel}"); var placed = new List>(); foreach (var symbol in symbols) { var parentView = ResolveSectionParentView(drawingContext.Drawing, request, symbol) ?? throw new InvalidOperationException($"Parent drawing view not found for section {symbol.Label}, view_kind={symbol.ViewKind}"); var parentName = Safe(() => parentView.GetName2(), ""); var parentOrientation = Safe(() => parentView.GetOrientationName(), ""); var viewName = BuildSectionViewName(request, symbol); if (request.ReplaceExistingSectionViews) { DeleteExistingSectionViewsByLabel(drawingContext.Model, drawingContext.Drawing, symbol.Label); DeleteDrawingView(drawingContext.Model, new ViewCandidate { DrawingViewName = viewName, OrientationName = viewName, Name = viewName }); } drawingContext.Model.ClearSelection2(true); var activated = Safe(() => drawingContext.Drawing.ActivateView(parentName), false); if (!activated && !string.IsNullOrWhiteSpace(parentOrientation)) activated = Safe(() => drawingContext.Drawing.ActivateView(parentOrientation), false); if (!activated) throw new InvalidOperationException($"Failed to activate parent drawing view: {parentName}"); var sketchLine = BuildSectionSketchLine(parentView, symbol); var start = sketchLine.StartMm; var end = sketchLine.EndMm; var startM = sketchLine.StartM; var endM = sketchLine.EndM; var segment = drawingContext.Model.SketchManager.CreateLine(startM[0], startM[1], 0, endM[0], endM[1], 0) ?? throw new InvalidOperationException($"Failed to create section line for {symbol.Label} in {parentName}"); var selected = Safe(() => segment.Select2(false, 0), false); if (!selected) selected = Safe(() => segment.Select(false), false); if (!selected) throw new InvalidOperationException($"Failed to select section line for {symbol.Label}"); var placementM = ComputeSectionViewPlacement(symbol, request.SectionViewOffsetM); var options = BuildSectionViewOptions(symbol, request); var view = drawingContext.Drawing.CreateSectionViewAt5( placementM[0], placementM[1], 0, symbol.Label, options, null, request.SectionDepthM) as View ?? throw new InvalidOperationException($"CreateSectionViewAt5 returned null for {symbol.Label}"); Safe(() => view.SetName2(viewName), false); Safe(() => drawingContext.Model.ForceRebuild3(false), false); var actualName = Safe(() => view.GetName2(), viewName); var actualPosition = DoubleList(Safe(() => view.Position, null)); placed.Add(new Dictionary { ["label"] = symbol.Label, ["source_parent_handle"] = symbol.ParentHandle, ["view_kind"] = symbol.ViewKind, ["parent_view_name"] = parentName, ["parent_view_orientation_name"] = parentOrientation, ["section_view_name"] = actualName, ["cut_line_mm"] = new Dictionary { ["start"] = start, ["end"] = end, ["center"] = sketchLine.CenterMm, ["direction"] = symbol.CutLine.Direction, ["source"] = sketchLine.Source }, ["cut_line_m"] = new Dictionary { ["start"] = new[] { start[0] / 1000.0, start[1] / 1000.0 }, ["end"] = new[] { end[0] / 1000.0, end[1] / 1000.0 }, ["center"] = new[] { sketchLine.CenterMm[0] / 1000.0, sketchLine.CenterMm[1] / 1000.0 } }, ["sketch_line_m"] = new Dictionary { ["start"] = startM, ["end"] = endM, ["center"] = sketchLine.CenterM, ["coordinate_system"] = "active_parent_drawing_view", ["expected_default_arrow_direction"] = ComputeSectionLineDefaultArrowDirection(startM, endM) }, ["arrow_direction"] = symbol.AverageArrowDirection, ["requested_position_m"] = placementM, ["actual_position_m"] = actualPosition, ["options"] = options }); } WriteJson(Path.Combine(request.OutputDir, "placed-section-views.json"), placed); return new CandidateResult { Ok = true, Message = "Created SolidWorks section views from extracted student DWG section cut symbols.", ModelPath = request.ModelPath, DrawingTemplatePath = drawingContext.TemplatePath, DrawingPath = drawingContext.DrawingPath, UsedActiveDrawing = request.UseActiveDrawing, OutputDir = request.OutputDir, StartedAt = started, CompletedAt = DateTimeOffset.Now, Artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["section_cut_symbols"] = request.SectionCutSymbolsPath, ["section_count"] = placed.Count.ToString(CultureInfo.InvariantCulture), ["placed_section_views"] = Path.Combine(request.OutputDir, "placed-section-views.json") }, SourceApis = [ "IDrawingDoc.ActivateView", "ISketchManager.CreateLine", "ISketchSegment.Select2", "IDrawingDoc.CreateSectionViewAt5", "IView.SetName2", "IModelDoc2.ForceRebuild3" ] }; } private static void DeleteExistingSectionViewsByLabel(ModelDoc2 drawingModel, DrawingDoc drawing, string label) { if (string.IsNullOrWhiteSpace(label)) return; var toDelete = new List(); var viewObj = drawing.GetFirstView(); while (viewObj is View view) { var name = Safe(() => view.GetName2(), ""); var type = Safe(() => view.Type, 0); if (type == (int)swDrawingViewTypes_e.swDrawingSectionView && IsLikelySectionViewForLabel(name, label)) { toDelete.Add(name); } viewObj = view.GetNextView(); } foreach (var name in toDelete.Distinct(StringComparer.OrdinalIgnoreCase)) { DeleteDrawingView(drawingModel, new ViewCandidate { DrawingViewName = name, OrientationName = name, Name = name }); } } private static bool IsLikelySectionViewForLabel(string viewName, string label) { if (string.IsNullOrWhiteSpace(viewName) || string.IsNullOrWhiteSpace(label)) return false; var normalized = Regex.Replace(viewName, @"\s+", "", RegexOptions.CultureInvariant).ToUpperInvariant(); var token = label.Trim().ToUpperInvariant(); return normalized.Contains($"{token}-{token}", StringComparison.OrdinalIgnoreCase) || normalized.EndsWith(token, StringComparison.OrdinalIgnoreCase); } private static View? ResolveSectionParentView(DrawingDoc drawing, CandidateRequest request, SectionCutSymbol symbol) { if (string.Equals(symbol.ViewKind, "bottom_view_candidate", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(request.ProjectedViewName)) { return FindDrawingView(drawing, request.ProjectedViewName); } if (string.Equals(symbol.ViewKind, "main_view", StringComparison.OrdinalIgnoreCase)) return FindMainDrawingView(drawing, request); return FindMainDrawingView(drawing, request) ?? (!string.IsNullOrWhiteSpace(request.ProjectedViewName) ? FindDrawingView(drawing, request.ProjectedViewName) : null); } private static View? FindDrawingView(DrawingDoc drawing, string nameOrOrientation) { var viewObj = drawing.GetFirstView(); while (viewObj is View view) { var name = Safe(() => view.GetName2(), ""); var orientationName = Safe(() => view.GetOrientationName(), ""); if (string.Equals(name, nameOrOrientation, StringComparison.OrdinalIgnoreCase) || string.Equals(orientationName, nameOrOrientation, StringComparison.OrdinalIgnoreCase)) return view; viewObj = view.GetNextView(); } return null; } private static string BuildSectionViewName(CandidateRequest request, SectionCutSymbol symbol) { var label = string.IsNullOrWhiteSpace(symbol.Label) ? "section" : symbol.Label.Trim(); return $"{request.SectionViewNamePrefix}_{label}"; } private static SectionSketchLine BuildSectionSketchLine(View parentView, SectionCutSymbol symbol) { var center = symbol.CutLine.Center.Count >= 2 ? symbol.CutLine.Center : [ (symbol.CutLine.Start[0] + symbol.CutLine.End[0]) / 2.0, (symbol.CutLine.Start[1] + symbol.CutLine.End[1]) / 2.0 ]; var centerM = new[] { center[0] / 1000.0, center[1] / 1000.0 }; var parentPosition = DoubleList(Safe(() => parentView.Position, null)); var parentX = parentPosition.Count >= 2 ? parentPosition[0] : 0.0; var parentY = parentPosition.Count >= 2 ? parentPosition[1] : 0.0; var outline = DoubleList(Safe(() => parentView.GetOutline(), null)); if (outline.Count >= 4) { var minX = Math.Min(outline[0], outline[2]); var maxX = Math.Max(outline[0], outline[2]); var minY = Math.Min(outline[1], outline[3]); var maxY = Math.Max(outline[1], outline[3]); if (string.Equals(symbol.CutLine.Direction, "horizontal", StringComparison.OrdinalIgnoreCase)) { var sheetStart = new[] { minX, centerM[1] }; var sheetEnd = new[] { maxX, centerM[1] }; return BuildSectionSketchLineFromSheetPoints( sheetStart, sheetEnd, center, centerM, parentX, parentY, symbol, "cut_line_center_through_parent_view_outline_parent_local"); } var verticalSheetStart = new[] { centerM[0], maxY }; var verticalSheetEnd = new[] { centerM[0], minY }; return BuildSectionSketchLineFromSheetPoints( verticalSheetStart, verticalSheetEnd, center, centerM, parentX, parentY, symbol, "cut_line_center_through_parent_view_outline_parent_local"); } return BuildSectionSketchLineFromSheetPoints( [symbol.CutLine.Start[0] / 1000.0, symbol.CutLine.Start[1] / 1000.0], [symbol.CutLine.End[0] / 1000.0, symbol.CutLine.End[1] / 1000.0], center, centerM, parentX, parentY, symbol, "fallback_cut_line_start_end_parent_local"); } private static SectionSketchLine BuildSectionSketchLineFromSheetPoints( double[] sheetStartM, double[] sheetEndM, List centerMm, double[] centerM, double parentX, double parentY, SectionCutSymbol symbol, string source) { var oriented = OrientSectionSketchLineByDwgArrow(sheetStartM, sheetEndM, symbol); var start = oriented.StartM; var end = oriented.EndM; return new SectionSketchLine( [start[0] * 1000.0, start[1] * 1000.0], [end[0] * 1000.0, end[1] * 1000.0], centerMm, [start[0] - parentX, start[1] - parentY], [end[0] - parentX, end[1] - parentY], [centerM[0] - parentX, centerM[1] - parentY], oriented.Reversed ? $"{source}_reversed_to_match_dwg_arrow" : source); } private static (double[] StartM, double[] EndM, bool Reversed) OrientSectionSketchLineByDwgArrow( double[] startM, double[] endM, SectionCutSymbol symbol) { var arrow = Normalize2(symbol.AverageArrowDirection); if (arrow == null) return (startM, endM, false); var defaultDirection = ComputeSectionLineDefaultArrowDirection(startM, endM); if (defaultDirection == null) return (startM, endM, false); var dot = defaultDirection[0] * arrow[0] + defaultDirection[1] * arrow[1]; return dot < 0 ? (endM, startM, true) : (startM, endM, false); } private static double[]? ComputeSectionLineDefaultArrowDirection(double[] startM, double[] endM) { var dx = endM[0] - startM[0]; var dy = endM[1] - startM[1]; var length = Math.Sqrt(dx * dx + dy * dy); if (length <= 1e-9) return null; // SolidWorks follows the section line orientation; use the left normal // as the automatic direction and reverse the line when DWG arrows disagree. return [-dy / length, dx / length]; } private static double[]? Normalize2(IReadOnlyList vector) { if (vector.Count < 2) return null; var x = vector[0]; var y = vector[1]; var length = Math.Sqrt(x * x + y * y); return length <= 1e-9 ? null : [x / length, y / length]; } private static double[] ComputeSectionViewPlacement(SectionCutSymbol symbol, double offsetM) { var center = symbol.CutLine.Center.Count >= 2 ? symbol.CutLine.Center : [ (symbol.CutLine.Start[0] + symbol.CutLine.End[0]) / 2.0, (symbol.CutLine.Start[1] + symbol.CutLine.End[1]) / 2.0 ]; var midX = center[0] / 1000.0; var midY = center[1] / 1000.0; var arrow = symbol.AverageArrowDirection; var ax = arrow.Count >= 2 ? arrow[0] : 0.0; var ay = arrow.Count >= 2 ? arrow[1] : 0.0; if (string.Equals(symbol.CutLine.Direction, "vertical", StringComparison.OrdinalIgnoreCase)) return [midX + Math.Sign(ax == 0 ? 1.0 : ax) * offsetM, midY]; if (string.Equals(symbol.CutLine.Direction, "horizontal", StringComparison.OrdinalIgnoreCase)) return [midX, midY + Math.Sign(ay == 0 ? 1.0 : ay) * offsetM]; var length = Math.Sqrt(ax * ax + ay * ay); if (length <= 1e-9) return [midX + offsetM, midY]; return [midX + ax / length * offsetM, midY + ay / length * offsetM]; } private static int BuildSectionViewOptions(SectionCutSymbol symbol, CandidateRequest request) { var options = (int)swCreateSectionViewAtOptions_e.swCreateSectionView_ScaleWithModel; if (request.SectionViewNotAligned) options |= (int)swCreateSectionViewAtOptions_e.swCreateSectionView_NotAligned; if (ShouldChangeSectionDirection(symbol, request)) options |= (int)swCreateSectionViewAtOptions_e.swCreateSectionView_ChangeDirection; return options; } private static bool ShouldChangeSectionDirection(SectionCutSymbol symbol, CandidateRequest request) { if (request.SectionViewChangeDirection != null) return request.SectionViewChangeDirection.Value; return false; } private static List ReadSectionCutSymbols(string path) { using var document = JsonDocument.Parse(File.ReadAllText(path)); if (document.RootElement.ValueKind != JsonValueKind.Array) throw new InvalidOperationException($"Section cut symbols JSON root must be an array: {path}"); var result = new List(); foreach (var element in document.RootElement.EnumerateArray()) { var symbol = new SectionCutSymbol { Ok = ReadBool(element, "ok"), Kind = ReadString(element, "kind"), Label = ReadString(element, "label"), ParentHandle = ReadString(element, "parent_handle"), ViewKind = ReadString(element, "view_kind"), CutLine = ReadSectionCutLine(element) }; symbol.AverageArrowDirection = ReadAverageArrowDirection(element); result.Add(symbol); } return result; } private static SectionCutLine ReadSectionCutLine(JsonElement element) { if (!element.TryGetProperty("cut_line", out var cutLine)) throw new InvalidOperationException("Section cut symbol is missing cut_line."); return new SectionCutLine { Start = ReadPoint(cutLine, "start"), End = ReadPoint(cutLine, "end"), Center = ReadPoint(cutLine, "center"), Direction = ReadString(cutLine, "direction") }; } private static List ReadAverageArrowDirection(JsonElement element) { if (!element.TryGetProperty("markers", out var markers) || markers.ValueKind != JsonValueKind.Array) return [0.0, 0.0]; var vectors = markers.EnumerateArray() .Where(marker => marker.TryGetProperty("arrow_direction", out var direction) && direction.ValueKind == JsonValueKind.Array) .Select(marker => ReadPoint(marker, "arrow_direction")) .Where(point => point.Count >= 2) .ToList(); if (vectors.Count == 0) return [0.0, 0.0]; return [ vectors.Average(vector => vector[0]), vectors.Average(vector => vector[1]) ]; } private static bool ReadBool(JsonElement element, string propertyName) { return element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.True; } private static string ReadString(JsonElement element, string propertyName) { return element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() ?? "" : ""; } private static List ReadPoint(JsonElement element, string propertyName) { if (!element.TryGetProperty(propertyName, out var value) || value.ValueKind != JsonValueKind.Array) return []; return value.EnumerateArray() .Where(item => item.ValueKind == JsonValueKind.Number) .Select(item => item.GetDouble()) .ToList(); } private static (double X, double Y) ReadSignatureOrigin(string path, string label) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException($"缺少 {label} signature path。"); if (!File.Exists(path)) throw new FileNotFoundException($"{label} signature 不存在: {path}"); using var document = JsonDocument.Parse(File.ReadAllText(path)); if (!document.RootElement.TryGetProperty("relative_line_coordinates", out var relative) || !relative.TryGetProperty("origin", out var origin) || !origin.TryGetProperty("x", out var xElement) || !origin.TryGetProperty("y", out var yElement)) throw new InvalidOperationException($"{label} signature 缺少 relative_line_coordinates.origin: {path}"); return (xElement.GetDouble(), yElement.GetDouble()); } private static View? FindMainDrawingView(DrawingDoc drawing, CandidateRequest request) { var viewObj = drawing.GetFirstView(); while (viewObj is View view) { var name = Safe(() => view.GetName2(), ""); var orientationName = Safe(() => view.GetOrientationName(), ""); if (!string.IsNullOrWhiteSpace(request.MainViewOrientationName)) { if (string.Equals(orientationName, request.MainViewOrientationName, StringComparison.OrdinalIgnoreCase) || string.Equals(name, request.MainViewOrientationName, StringComparison.OrdinalIgnoreCase)) return view; } else if (LooksLikeCandidateView(name, orientationName, request.MainViewNamePrefix)) { return view; } viewObj = view.GetNextView(); } return null; } private static void CreateNamedViews(ModelDoc2 model, object mathUtility, List candidates, bool keepNamedViews) { var activeView = model.IActiveView as ModelView ?? model.ActiveView as ModelView ?? throw new InvalidOperationException("模型没有可用 ActiveView,无法设置 Orientation3。"); foreach (var candidate in candidates) { if (keepNamedViews) Safe(() => model.DeleteNamedView(candidate.Name), false); else Safe(() => model.DeleteNamedView(candidate.Name), false); var transformData = new double[] { candidate.BasisU[0], candidate.BasisU[1], candidate.BasisU[2], candidate.BasisV[0], candidate.BasisV[1], candidate.BasisV[2], candidate.BasisD[0], candidate.BasisD[1], candidate.BasisD[2], 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 }; dynamic utility = mathUtility; MathTransform transform = utility.CreateTransform(transformData); activeView.Orientation3 = transform; model.GraphicsRedraw2(); model.NameView(candidate.Name); } } private static void ExportCandidateDwgsSequential( ModelDoc2 drawingModel, DrawingDoc drawing, string modelPath, List candidates, CandidateRequest request) { var dwgDir = CandidateDwgDirectory(request); Directory.CreateDirectory(dwgDir); foreach (var candidate in candidates) { var dwgPath = Path.Combine(dwgDir, $"{candidate.Index:00}_{SanitizeFileName(candidate.Name)}.dwg"); ExportSingleCandidateDwgUntilSaved(drawingModel, drawing, modelPath, candidate, request, dwgPath); } if (!request.KeepExportedCandidateView) CleanupCandidateDrawingViews(drawingModel, drawing, request.ViewNamePrefix); } private static void ExportSingleCandidateDwgUntilSaved( ModelDoc2 drawingModel, DrawingDoc drawing, string modelPath, ViewCandidate candidate, CandidateRequest request, string dwgPath) { var rebuildAttempt = 0; var maxRebuildAttempts = Math.Max(0, request.CandidateRebuildRetryCount); while (maxRebuildAttempts == 0 || rebuildAttempt < maxRebuildAttempts) { rebuildAttempt++; candidate.DwgRebuildAttempts = rebuildAttempt; candidate.DwgPath = dwgPath; CleanupCandidateDrawingViews(drawingModel, drawing, request.ViewNamePrefix); var view = InsertSingleCandidateView(drawing, modelPath, candidate, request); if (view == null) { candidate.DwgSaveOk = false; candidate.DwgSaveErrors = -1; candidate.DwgSaveWarnings = 0; candidate.DwgSaveMessage = $"第 {rebuildAttempt} 轮重建失败:CreateDrawViewFromModelView3 返回空。"; WaitBeforeNextCandidateAttempt(request); continue; } Safe(() => drawingModel.ForceRebuild3(false), false); WaitBeforeNextCandidateAttempt(request); var saveResult = SaveAsWithRetries(drawingModel, dwgPath, request.SaveRetryCount, request.SaveRetryDelayMs); candidate.DwgSaveOk = saveResult.Ok; candidate.DwgSaveErrors = saveResult.Errors; candidate.DwgSaveWarnings = saveResult.Warnings; if (saveResult.Ok) { candidate.DwgSaveMessage = $"第 {rebuildAttempt} 轮重建后保存成功。"; if (!request.KeepExportedCandidateView) DeleteDrawingView(drawingModel, candidate); return; } candidate.DwgSaveMessage = $"第 {rebuildAttempt} 轮重建后保存失败:errors={saveResult.Errors}, warnings={saveResult.Warnings}。"; Console.Error.WriteLine( $"候选 {candidate.Index:00} 保存失败,准备重建后重试:attempt={rebuildAttempt}, errors={saveResult.Errors}, warnings={saveResult.Warnings}"); if (!request.KeepExportedCandidateView) DeleteDrawingView(drawingModel, candidate); WaitBeforeNextCandidateAttempt(request); } candidate.DwgSaveOk = false; candidate.DwgSaveMessage = $"达到候选视图重建重试上限 {maxRebuildAttempts},仍未保存成功。"; throw new InvalidOperationException($"候选 {candidate.Index:00} DWG 保存失败,已达到重建重试上限 {maxRebuildAttempts}。"); } private static void WaitBeforeNextCandidateAttempt(CandidateRequest request) { if (request.SaveRetryDelayMs > 0) Thread.Sleep(request.SaveRetryDelayMs); } private static View? InsertSingleCandidateView(DrawingDoc drawing, string modelPath, ViewCandidate candidate, CandidateRequest request) { var x = request.SingleViewXM ?? request.SheetWidthM / 2.0; var y = request.SingleViewYM ?? request.SheetHeightM / 2.0; var view = drawing.CreateDrawViewFromModelView3(modelPath, candidate.Name, x, y, 0); if (view == null) { candidate.Inserted = false; candidate.InsertError = "CreateDrawViewFromModelView3 returned null."; return null; } if (request.ViewScale > 0) Safe(() => { view.UseSheetScale = 0; view.ScaleDecimal = request.ViewScale; return true; }, false); candidate.Inserted = true; candidate.DrawingViewName = Safe(() => view.GetName2(), ""); candidate.OrientationName = Safe(() => view.GetOrientationName(), ""); candidate.Position = DoubleList(Safe(() => view.Position, null)); candidate.ScaleDecimal = Safe(() => view.ScaleDecimal, 0.0); candidate.UseSheetScale = Safe(() => view.UseSheetScale, 0); return view; } private static string CandidateDwgDirectory(CandidateRequest request) { return string.IsNullOrWhiteSpace(request.CandidateDwgDir) ? Path.Combine(request.OutputDir, "candidate-dwgs") : request.CandidateDwgDir; } private static void CleanupCandidateDrawingViews(ModelDoc2 drawingModel, DrawingDoc drawing, string prefix) { var toDelete = new List(); var viewObj = drawing.GetFirstView(); while (viewObj is View view) { var name = Safe(() => view.GetName2(), ""); var orientationName = Safe(() => view.GetOrientationName(), ""); if (LooksLikeCandidateView(name, orientationName, prefix)) { toDelete.Add(new ViewCandidate { DrawingViewName = name, OrientationName = orientationName }); } viewObj = view.GetNextView(); } foreach (var candidate in toDelete) DeleteDrawingView(drawingModel, candidate); } private static bool LooksLikeCandidateView(string viewName, string orientationName, string prefix) { return (!string.IsNullOrWhiteSpace(orientationName) && orientationName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) || (!string.IsNullOrWhiteSpace(viewName) && viewName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); } private static bool DeleteDrawingView(ModelDoc2 drawingModel, ViewCandidate candidate) { var names = new[] { candidate.DrawingViewName, candidate.OrientationName, candidate.Name }; foreach (var name in names.Where(item => !string.IsNullOrWhiteSpace(item)).Distinct(StringComparer.OrdinalIgnoreCase)) { drawingModel.ClearSelection2(true); var selected = Safe(() => drawingModel.Extension.SelectByID2(name, "DRAWINGVIEW", 0, 0, 0, false, 0, null, 0), false); if (!selected) continue; var deleted = Safe(() => drawingModel.Extension.DeleteSelection2(0), false); drawingModel.ClearSelection2(true); if (deleted) return true; } return false; } private static SaveResult SaveAs(ModelDoc2 model, string path) { var errors = 0; var warnings = 0; Directory.CreateDirectory(Path.GetDirectoryName(path) ?? System.Environment.CurrentDirectory); var ok = Safe(() => model.SaveAs4( path, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings), false); if (!ok && !File.Exists(path)) { errors = 0; warnings = 0; ok = model.Extension.SaveAs( path, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, null, ref errors, ref warnings); } return new SaveResult(ok || File.Exists(path), errors, warnings); } private static SaveResult SaveAsWithRetries(ModelDoc2 model, string path, int retryCount, int retryDelayMs) { retryCount = Math.Max(1, retryCount); retryDelayMs = Math.Max(0, retryDelayMs); var last = new SaveResult(false, 0, 0); for (var attempt = 1; attempt <= retryCount; attempt++) { Safe(() => model.ForceRebuild3(false), false); if (attempt > 1 && retryDelayMs > 0) Thread.Sleep(retryDelayMs); last = SaveAs(model, path); if (last.Ok) return last; } return last; } private static void DeleteNamedViews(ModelDoc2 model, List candidates) { foreach (var candidate in candidates) Safe(() => model.DeleteNamedView(candidate.Name), false); } private static (double[] U, double[] V, double[] D) BuildBasis(double[] direction, double rollDeg) { var d = Normalize(direction); var worldUp = Math.Abs(Dot(d, [0.0, 1.0, 0.0])) > 0.92 ? new[] { 0.0, 0.0, 1.0 } : new[] { 0.0, 1.0, 0.0 }; var u = Normalize(Cross(worldUp, d)); var v = Normalize(Cross(d, u)); var rad = rollDeg * Math.PI / 180.0; var cos = Math.Cos(rad); var sin = Math.Sin(rad); var rolledU = Add(Scale(u, cos), Scale(v, sin)); var rolledV = Add(Scale(u, -sin), Scale(v, cos)); return (Normalize(rolledU), Normalize(rolledV), d); } private static double[] Normalize(double[] v) { var norm = Math.Sqrt(Dot(v, v)); if (norm <= 1e-12) return [0, 0, 1]; return [v[0] / norm, v[1] / norm, v[2] / norm]; } private static double Dot(double[] a, double[] b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; private static double[] Cross(double[] a, double[] b) { return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; } private static double[] Add(double[] a, double[] b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; private static double[] Scale(double[] a, double k) => [a[0] * k, a[1] * k, a[2] * k]; private 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 (item == null) continue; if (double.TryParse(Convert.ToString(item, CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) result.Add(parsed); } return result; } return []; } private static string NormalizePath(string? path) { if (string.IsNullOrWhiteSpace(path)) return ""; return Path.GetFullPath(System.Environment.ExpandEnvironmentVariables(path.Trim().Trim('"'))); } private static string SanitizeFileName(string name) { var value = Path.GetFileNameWithoutExtension(name); if (string.IsNullOrWhiteSpace(value)) value = "solidworks_view_candidates"; foreach (var c in Path.GetInvalidFileNameChars()) value = value.Replace(c, '_'); return value; } private static string FirstNonEmpty(params string?[] values) { foreach (var value in values) { if (!string.IsNullOrWhiteSpace(value)) return value.Trim(); } return ""; } private static void WriteJson(string path, object data) { Directory.CreateDirectory(Path.GetDirectoryName(path) ?? System.Environment.CurrentDirectory); File.WriteAllText(path, JsonSerializer.Serialize(data, JsonOptions)); } private static T Safe(Func action, T fallback) { try { return action(); } catch { return fallback; } } } internal sealed class CandidateRequest { public string ModelPath { get; set; } = ""; public string DrawingTemplatePath { get; set; } = ""; public string OutputDir { get; set; } = ""; public string CandidateDwgDir { get; set; } = ""; public string RankingJsonPath { get; set; } = ""; public string StudentSignaturePath { get; set; } = ""; public string ReferenceSignaturePath { get; set; } = ""; public string StudentViewRegionsPath { get; set; } = ""; public string ReferenceViewRegionsPath { get; set; } = ""; public string OutputDwgPath { get; set; } = ""; public string SectionCutSymbolsPath { get; set; } = ""; public string ViewNamePrefix { get; set; } = "agent4_candidate"; public string MainViewNamePrefix { get; set; } = "agent4_main_view"; public string MainViewOrientationName { get; set; } = ""; public string AlignmentAxis { get; set; } = "xy"; public string ProjectedViewName { get; set; } = "agent4_bottom_projected_view"; public string ProjectedViewDirection { get; set; } = "down"; public string SectionLabel { get; set; } = ""; public string SectionViewNamePrefix { get; set; } = "agent4_section_view"; public double SheetWidthM { get; set; } = 0.594; public double SheetHeightM { get; set; } = 0.420; public int? CandidateIndex { get; set; } public double ViewScale { get; set; } = 1.0; public double? SingleViewXM { get; set; } public double? SingleViewYM { get; set; } public double? ProjectedViewXM { get; set; } public double? ProjectedViewYM { get; set; } public double SectionViewOffsetM { get; set; } = 0.18; public double SectionDepthM { get; set; } = 0.0; public int SaveRetryCount { get; set; } = 3; public int SaveRetryDelayMs { get; set; } = 1000; public int CandidateRebuildRetryCount { get; set; } = 0; public bool KeepNamedViews { get; set; } public bool UseActiveDrawing { get; set; } public bool UseActiveModel { get; set; } public bool ExportCandidateDwgsSequential { get; set; } public bool ExportActiveDrawingDwg { get; set; } public bool ListDrawingViews { get; set; } public bool KeepExportedCandidateView { get; set; } public bool PlaceMainView { get; set; } public bool PlaceProjectedView { get; set; } public bool PlaceSectionViews { get; set; } public bool AlignMainViewOrigin { get; set; } public bool AlignProjectedViewsToRegions { get; set; } public bool AlignSectionViewsToRegions { get; set; } public bool ReplaceExistingMainView { get; set; } = true; public bool ReplaceExistingProjectedView { get; set; } = true; public bool ReplaceExistingSectionViews { get; set; } = true; public bool SectionViewNotAligned { get; set; } public bool? SectionViewChangeDirection { get; set; } public static CandidateRequest Parse(string[] args) { var request = new CandidateRequest(); for (var i = 0; i < args.Length; i++) { var arg = args[i]; string Next() => i + 1 < args.Length ? args[++i] : throw new ArgumentException($"缺少参数值: {arg}"); switch (arg) { case "--request-json": request = JsonSerializer.Deserialize(File.ReadAllText(Next()), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new CandidateRequest(); break; case "--model": request.ModelPath = Next(); break; case "--template": request.DrawingTemplatePath = Next(); break; case "--output-dir": request.OutputDir = Next(); break; case "--candidate-dwg-dir": request.CandidateDwgDir = Next(); break; case "--ranking-json": request.RankingJsonPath = Next(); break; case "--student-signature": request.StudentSignaturePath = Next(); break; case "--reference-signature": request.ReferenceSignaturePath = Next(); break; case "--student-view-regions": request.StudentViewRegionsPath = Next(); break; case "--reference-view-regions": request.ReferenceViewRegionsPath = Next(); break; case "--dwg": request.OutputDwgPath = Next(); break; case "--section-cut-symbols": request.SectionCutSymbolsPath = Next(); break; case "--view-name-prefix": request.ViewNamePrefix = Next(); break; case "--main-view-name-prefix": request.MainViewNamePrefix = Next(); break; case "--main-view-orientation-name": request.MainViewOrientationName = Next(); break; case "--alignment-axis": request.AlignmentAxis = Next(); break; case "--projected-view-name": request.ProjectedViewName = Next(); break; case "--projected-view-direction": request.ProjectedViewDirection = Next(); break; case "--section-label": request.SectionLabel = Next(); break; case "--section-view-name-prefix": request.SectionViewNamePrefix = Next(); break; case "--sheet-width-m": request.SheetWidthM = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--sheet-height-m": request.SheetHeightM = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--candidate-index": request.CandidateIndex = int.Parse(Next(), CultureInfo.InvariantCulture); break; case "--view-scale": request.ViewScale = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--single-view-x-m": request.SingleViewXM = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--single-view-y-m": request.SingleViewYM = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--projected-view-x-m": request.ProjectedViewXM = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--projected-view-y-m": request.ProjectedViewYM = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--section-view-offset-m": request.SectionViewOffsetM = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--section-depth-m": request.SectionDepthM = double.Parse(Next(), CultureInfo.InvariantCulture); break; case "--save-retry-count": request.SaveRetryCount = int.Parse(Next(), CultureInfo.InvariantCulture); break; case "--save-retry-delay-ms": request.SaveRetryDelayMs = int.Parse(Next(), CultureInfo.InvariantCulture); break; case "--candidate-rebuild-retry-count": request.CandidateRebuildRetryCount = int.Parse(Next(), CultureInfo.InvariantCulture); break; case "--keep-named-views": request.KeepNamedViews = true; break; case "--use-active-drawing": request.UseActiveDrawing = true; break; case "--use-active-model": request.UseActiveModel = true; break; case "--export-candidate-dwgs-sequential": request.ExportCandidateDwgsSequential = true; break; case "--export-active-drawing-dwg": request.ExportActiveDrawingDwg = true; break; case "--list-drawing-views": request.ListDrawingViews = true; break; case "--keep-exported-candidate-view": request.KeepExportedCandidateView = true; break; case "--place-main-view": request.PlaceMainView = true; break; case "--place-projected-view": request.PlaceProjectedView = true; break; case "--place-section-views": request.PlaceSectionViews = true; break; case "--align-main-view-origin": request.AlignMainViewOrigin = true; break; case "--align-projected-views-to-regions": case "--align-orthographic-views-to-regions": request.AlignProjectedViewsToRegions = true; break; case "--align-section-views-to-regions": request.AlignSectionViewsToRegions = true; break; case "--keep-existing-main-view": request.ReplaceExistingMainView = false; break; case "--keep-existing-projected-view": request.ReplaceExistingProjectedView = false; break; case "--keep-existing-section-views": request.ReplaceExistingSectionViews = false; break; case "--section-view-not-aligned": request.SectionViewNotAligned = true; break; case "--section-view-change-direction": request.SectionViewChangeDirection = true; break; case "--section-view-keep-direction": request.SectionViewChangeDirection = false; break; default: throw new ArgumentException($"未知参数: {arg}"); } } return request; } } internal sealed class CandidateResult { public bool Ok { get; set; } public string Message { get; set; } = ""; public string ModelPath { get; set; } = ""; public string DrawingTemplatePath { get; set; } = ""; public string DrawingPath { get; set; } = ""; public bool UsedActiveDrawing { get; set; } public string OutputDir { get; set; } = ""; public DateTimeOffset StartedAt { get; set; } public DateTimeOffset CompletedAt { get; set; } public List Candidates { get; set; } = []; public Dictionary Artifacts { get; set; } = new(StringComparer.OrdinalIgnoreCase); public List SourceApis { get; set; } = []; } internal sealed class ViewCandidate { public int Index { get; set; } public string Name { get; set; } = ""; public string DirectionName { get; set; } = ""; public int RollDeg { get; set; } public List ViewDirection { get; set; } = []; public List BasisU { get; set; } = []; public List BasisV { get; set; } = []; public List BasisD { get; set; } = []; public bool Inserted { get; set; } public string InsertError { get; set; } = ""; public string DrawingViewName { get; set; } = ""; public string OrientationName { get; set; } = ""; public List Position { get; set; } = []; public double ScaleDecimal { get; set; } public int UseSheetScale { get; set; } public string DwgPath { get; set; } = ""; public bool DwgSaveOk { get; set; } public int DwgSaveErrors { get; set; } public int DwgSaveWarnings { get; set; } public int DwgRebuildAttempts { get; set; } public string DwgSaveMessage { get; set; } = ""; } internal sealed record DrawingContext(ModelDoc2 Model, DrawingDoc Drawing, string TemplatePath, string DrawingPath); internal sealed record SaveResult(bool Ok, int Errors, int Warnings); internal sealed class SectionCutSymbol { public bool Ok { get; set; } public string Kind { get; set; } = ""; public string Label { get; set; } = ""; public string ParentHandle { get; set; } = ""; public string ViewKind { get; set; } = ""; public SectionCutLine CutLine { get; set; } = new(); public List AverageArrowDirection { get; set; } = []; } internal sealed class SectionCutLine { public List Start { get; set; } = []; public List End { get; set; } = []; public List Center { get; set; } = []; public string Direction { get; set; } = ""; } internal sealed record SectionRegionCenter(string Label, double CenterX, double CenterY); internal sealed record OrthographicRegionCenter(string Kind, double CenterX, double CenterY); internal sealed record SectionRegionDelta(double DeltaMmX, double DeltaMmY, double AppliedDeltaMmX, double AppliedDeltaMmY); internal sealed record SectionSketchLine( List StartMm, List EndMm, List CenterMm, double[] StartM, double[] EndM, double[] CenterM, string Source);