Files
2026-07-17 17:45:56 +08:00

2244 lines
99 KiB
C#

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<string, string>(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<string, string>(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<ViewCandidate> 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<ViewCandidate>();
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<ViewCandidate> 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<object?>(() => 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<object?>(() => 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<string, string>(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<object?>(() => 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<object?>(() => view.Position, null));
var reportPath = Path.Combine(request.OutputDir, "aligned-projected-view.json");
WriteJson(reportPath, new Dictionary<string, object?>
{
["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<string, string>(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<Dictionary<string, object?>>();
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<object?>(() => 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<object?>(() => view.Position, null));
moved.Add(new Dictionary<string, object?>
{
["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<string, object?>
{
["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<string, object?>
{
["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<string, string>(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<SectionRegionCenter> 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<SectionRegionCenter>();
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<Dictionary<string, object?>>();
var viewObj = drawingContext.Drawing.GetFirstView();
while (viewObj is View view)
{
var name = Safe(() => view.GetName2(), "");
var orientationName = Safe(() => view.GetOrientationName(), "");
views.Add(new Dictionary<string, object?>
{
["name"] = name,
["orientation_name"] = orientationName,
["type"] = Safe(() => view.Type, 0),
["position_m"] = DoubleList(Safe<object?>(() => view.Position, null)),
["outline_m"] = DoubleList(Safe<object?>(() => 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<string, string>(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<string, string>(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<object?>(() => 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<object?>(() => 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<string, string>(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<Dictionary<string, object?>>();
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<object?>(() => view.Position, null));
placed.Add(new Dictionary<string, object?>
{
["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<string, object?>
{
["start"] = start,
["end"] = end,
["center"] = sketchLine.CenterMm,
["direction"] = symbol.CutLine.Direction,
["source"] = sketchLine.Source
},
["cut_line_m"] = new Dictionary<string, object?>
{
["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<string, object?>
{
["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<string, string>(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<string>();
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<object?>(() => 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<object?>(() => 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<double> 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<double> 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<SectionCutSymbol> 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<SectionCutSymbol>();
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<double> 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<double> 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<ViewCandidate> 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<ViewCandidate> 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<ViewCandidate>();
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<ViewCandidate> 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<double> DoubleList(object? value)
{
if (value == null)
return [];
if (value is double[] doubles)
return doubles.ToList();
if (value is object[] objects)
return objects.SelectMany(DoubleList).ToList();
if (value is Array array)
{
var result = new List<double>();
foreach (var item in array)
{
if (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<T>(Func<T> 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<CandidateRequest>(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<ViewCandidate> Candidates { get; set; } = [];
public Dictionary<string, string> Artifacts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<string> 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<double> ViewDirection { get; set; } = [];
public List<double> BasisU { get; set; } = [];
public List<double> BasisV { get; set; } = [];
public List<double> 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<double> 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<double> AverageArrowDirection { get; set; } = [];
}
internal sealed class SectionCutLine
{
public List<double> Start { get; set; } = [];
public List<double> End { get; set; } = [];
public List<double> 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<double> StartMm,
List<double> EndMm,
List<double> CenterMm,
double[] StartM,
double[] EndM,
double[] CenterM,
string Source);