2244 lines
99 KiB
C#
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);
|