csharp
复制代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using View = SolidWorks.Interop.sldworks.View;
namespace tools
{
/// <summary>
/// 工程图螺纹/孔:在零件中识别目标特征,将模型整圆边映射到视图可见边并创建直径尺寸。
/// 支持选中「视图」或「零件/组件」(装配体工程图内点选零件);首次使用请看控制台 TypeName2 列表并调整 <see cref="TargetFeatureTypeNames"/>。
/// 装饰螺纹(CosmeticThread)通常 <c>GetFaces()</c> 为 0,圆边优先来自 <see cref="CosmeticThreadFeatureData"/> 的 <c>Edge</c>;
/// 若定义边读不到,可在工程图中用 <c>SelectByID2(..., \"CTHREAD\", ...)</c> 选中装饰螺纹再 <c>AddDimension2</c>(与录制宏一致)。
/// 与 <see cref="benddim"/> 一致:创建尺寸前对关联零件调用全局明细注释文字格式;面数统计与圆边收集使用 <see cref="EnumerateFeatureFaces"/>,兼容 COM 仅返回单个 <see cref="Face"/> 的情形。
/// 孔向导类特征(如 HoleWzd)名称中含 <c>M5</c>、<c>M8x1.25</c> 等时,可在直径尺寸创建后将显示文字改为该规格串(见 <see cref="ReplaceHoleWizardDiameterTextWithThreadLabel"/>)。
/// </summary>
public static class threadhol_dim
{
/// <summary>为 true 时仅打印特征 TypeName2 统计与明细,不创建尺寸。</summary>
public static bool OnlyPrintFeatureTypeNames = false;
/// <summary>为 true 时,TypeName2 包含子串 "thread"(忽略大小写)的特征也视为目标(便于覆盖本地化名称)。</summary>
public static bool MatchTypeNameContainsThread = true;
/// <summary>
/// 为 true 时:对从孔向导类特征(HoleWzd / AdvHole / AdvancedHole)收集到的圆边创建直径尺寸后,
/// 若特征名称中能解析出螺纹规格(如 <c>M5 螺纹孔1</c> → <c>M5</c>),则用 <see cref="DisplayDimension.SetText"/> 将尺寸显示改为该字符串(如由数值 Ø4.2 改为 <c>M5</c>)。
/// </summary>
public static bool ReplaceHoleWizardDiameterTextWithThreadLabel = true;
/// <summary>
/// 视为「螺纹孔相关」的特征 TypeName2(SolidWorks 英文环境常见值,请按控制台打印结果增删)。
/// 常见:CosmeticThread、HoleWzd(孔向导)、AdvancedHole 等。
/// </summary>
public static readonly HashSet<string> TargetFeatureTypeNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"CosmeticThread",
"HoleWzd",
"AdvHole",
"AdvancedHole",
"Thread",
};
const double DimensionTextPerpBaseOffsetM = 0.003;
const double DimensionTextPerpStaggerStepM = 0.0025;
const double CircleRadiusAbsTolM = 0.00015;
const double CircleRadiusRelTol = 0.004;
const double CircleCenterTolM = 0.00035;
const int MaxSubFeatureWalkSteps = 50_000;
/// <summary>圆弧视为「整圆孔边」的最小参数跨度(弧度),略小于 2π 以包容拓扑缝隙。</summary>
const double MinCircleEdgeParamSpan = 5.5;
/// <summary>从特征名称解析螺纹规格(如 M5、M8x1.25),用于孔向导类特征尺寸显示改写。</summary>
static readonly Regex HoleFeatureThreadCalloutFromName = new(
@"\b(M\s*[\d.]+(?:\s*[x×]\s*[\d.]+)?)\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
/// <summary>
/// 兼容旧名:等价于 <see cref="DimensionThreadHolesFromDrawingSelection"/>。
/// </summary>
public static void DimensionSelectedViewThreadHoles(ISldWorks swApp) => DimensionThreadHolesFromDrawingSelection(swApp);
/// <summary>
/// 在工程图中根据当前选择标注螺纹/孔直径:可选中「视图」或「零件/组件」(装配体图内点选零件);亦支持通过面/边选择解析到零件实例。
/// 螺纹特征由 <see cref="TargetFeatureTypeNames"/> / <see cref="MatchTypeNameContainsThread"/> 筛选;几何优先来自特征面(孔向导等),装饰螺纹则补充 <c>CosmeticThreadFeatureData.Edge</c>。
/// </summary>
public static void DimensionThreadHolesFromDrawingSelection(ISldWorks swApp)
{
bool restoreInputDimValOnCreate = false;
bool prevInputDimValOnCreate = false;
try
{
var swModel = (ModelDoc2)swApp.ActiveDoc;
if (swModel == null)
{
Console.WriteLine("[threadhol_dim] 没有活动文档");
return;
}
var swSelMgr = (SelectionMgr)swModel.SelectionManager;
if (swSelMgr.GetSelectedObjectCount() < 1)
{
Console.WriteLine("[threadhol_dim] 请先选择:工程图视图,或装配体视图中的零件/组件(亦可选中零件上的面或圆边)。");
return;
}
if (!TryResolveThreadHoleDrawingSelection(swModel, swSelMgr, out var view, out var partDoc, out var restrictComp, out var resolveMsg))
{
Console.WriteLine("[threadhol_dim] " + resolveMsg);
return;
}
ApplyGlobalAnnotationTextHeight((ModelDoc2)partDoc);
prevInputDimValOnCreate = swApp.GetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate);
swApp.SetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate, false);
restoreInputDimValOnCreate = true;
swApp.CommandInProgress = true;
ExitDrawingSketchIfActive(swModel);
var typeCounts = new Dictionary<string, int>(StringComparer.Ordinal);
var lines = new List<string>();
WalkPartFeatureTree((Feature?)partDoc.FirstFeature(), typeCounts, lines, 0);
Console.WriteLine("========== [threadhol_dim] 关联零件中特征 TypeName2(去重统计)==========");
var sortedTypes = new List<string>(typeCounts.Keys);
sortedTypes.Sort(StringComparer.Ordinal);
foreach (var t in sortedTypes)
Console.WriteLine($" {t} ×{typeCounts[t]}");
Console.WriteLine("========== [threadhol_dim] 明细(深度 前缀 TypeName2 · 名称)==========");
foreach (var line in lines)
Console.WriteLine(line);
if (OnlyPrintFeatureTypeNames)
{
LogTargetFeaturesThreadFaceSummary((ModelDoc2)partDoc, partDoc, view, restrictComp);
Console.WriteLine("[threadhol_dim] OnlyPrintFeatureTypeNames=true,已跳过标注。需要出尺寸时请设为 false 并调整 TargetFeatureTypeNames。");
Console.WriteLine("[threadhol_dim] 提示:孔向导等从特征面扫整圆模型边,再映射视图可见边出直径(非 CTHREAD);装饰螺纹定义边读不到时才用下方「CTHREAD 候选名」SelectByID2,可与录制宏第一参数对照。");
return;
}
var candidates = new List<Feature>();
CollectTargetFeaturesFromPart(partDoc, candidates);
if (candidates.Count == 0)
{
Console.WriteLine("[threadhol_dim] 未找到匹配 TargetFeatureTypeNames / Thread 子串的特征,请根据上方 TypeName 列表调整筛选条件。");
return;
}
Console.WriteLine($"[threadhol_dim] 匹配到 {candidates.Count} 个候选特征,开始收集圆边...");
var partModel = (ModelDoc2)partDoc;
var circleDimTargets = BuildThreadHoleCircleDimTargets(partModel, candidates);
Console.WriteLine($"[threadhol_dim] 去重后模型圆边 {circleDimTargets.Count} 条");
double textStagger = 0;
int ok = 0;
int fail = 0;
var drawingDoc = (DrawingDoc)swModel;
foreach (var feat in candidates)
{
if (!IsCosmeticThreadType(feat))
continue;
if (TryHasUsableCosmeticThreadDefinitionCircleEdge(partModel, feat))
continue;
if (TryAddCosmeticThreadDiameterViaCthreadSelect(
swModel, drawingDoc, view, partDoc, feat, restrictComp, ref textStagger))
{
ok++;
}
else
{
fail++;
Console.WriteLine($"[threadhol_dim] 装饰螺纹 CTHREAD 捷径未成功: {feat.Name}(可对照宏中 SelectByID2 全名字符串)");
}
}
foreach (var t in circleDimTargets)
{
var vis = FindVisibleCircleEdge(view, t.ModelEdge, restrictComp);
if (vis == null)
{
fail++;
LogCircleSkip(t.ModelEdge, restrictComp != null ? "可见边中未匹配到同圆(已限定零件实例)" : "可见边中未匹配到同圆");
continue;
}
if (!TryAddDiameterDimension(swApp, swModel, view, t.ModelEdge, vis, t.ThreadCalloutDisplay, ref textStagger))
{
fail++;
continue;
}
ok++;
}
Console.WriteLine($"[threadhol_dim] 完成:成功 {ok},失败 {fail}。");
}
finally
{
if (restoreInputDimValOnCreate)
{
try
{
swApp.SetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate, prevInputDimValOnCreate);
}
catch
{
// ignored
}
}
try
{
swApp.CommandInProgress = false;
}
catch
{
// ignored
}
}
}
/// <summary>
/// 解析工程图选择:返回目标 <see cref="PartDoc"/>、用于几何投影的 <see cref="View"/>,以及在装配体视图中限定可见边搜索范围的组件(零件图时为 null)。
/// </summary>
static bool TryResolveThreadHoleDrawingSelection(
ModelDoc2 swModel,
SelectionMgr swSelMgr,
out View view,
out PartDoc partDoc,
out Component2? restrictComp,
out string resolveMsg)
{
view = null!;
partDoc = null!;
restrictComp = null;
resolveMsg = "";
int selType = swSelMgr.GetSelectedObjectType3(1, -1);
if (selType == (int)swSelectType_e.swSelDRAWINGVIEWS)
{
var v = (View)swSelMgr.GetSelectedObject(1);
var refDoc = v.ReferencedDocument;
if (refDoc == null)
{
resolveMsg = "无法获取视图关联文档。";
return false;
}
if (refDoc is not PartDoc pd)
{
resolveMsg = "当前选中视图的引用模型不是零件。装配体工程图请在视图中选中「零件/组件」,或使用显示单个零件的视图。";
return false;
}
view = v;
partDoc = pd;
restrictComp = null;
return true;
}
if (selType == (int)swSelectType_e.swSelCOMPONENTS)
{
var comp = (Component2)swSelMgr.GetSelectedObject(1);
return TryResolveFromDrawingComponent(swModel, swSelMgr, comp, 1, out view, out partDoc, out restrictComp, out resolveMsg);
}
if (selType == (int)swSelectType_e.swSelFACES
|| selType == (int)swSelectType_e.swSelEDGES)
{
Component2? comp = null;
try
{
comp = swSelMgr.GetSelectedObjectsComponent3(1, -1);
}
catch
{
comp = null;
}
if (comp == null)
{
resolveMsg = "无法从当前面/边选择解析到零件组件,请改为在视图中选中零件实例。";
return false;
}
return TryResolveFromDrawingComponent(swModel, swSelMgr, comp, 1, out view, out partDoc, out restrictComp, out resolveMsg);
}
resolveMsg = $"不支持的选中类型 ({(swSelectType_e)selType})。请选择视图、零件/组件,或零件上的面/圆边。";
return false;
}
static bool TryResolveFromDrawingComponent(
ModelDoc2 swModel,
SelectionMgr swSelMgr,
Component2 comp,
int selectionIndex,
out View view,
out PartDoc partDoc,
out Component2? restrictComp,
out string resolveMsg)
{
view = null!;
partDoc = null!;
restrictComp = comp;
resolveMsg = "";
ModelDoc2? modelDoc = null;
try
{
modelDoc = comp.GetModelDoc2() as ModelDoc2;
}
catch
{
modelDoc = null;
}
if (modelDoc is not PartDoc pd)
{
resolveMsg = "选中项不是零件级组件(例如子装配体)。请展开后选择具体零件。";
return false;
}
partDoc = pd;
View? v = null;
try
{
v = swSelMgr.GetSelectedObjectsDrawingView2(selectionIndex, -1) as View;
}
catch
{
v = null;
}
if (v == null && swModel is DrawingDoc)
v = TryFindViewContainingComponent(swModel, comp);
if (v == null)
{
resolveMsg = "无法确定该零件所在的工程图视图(可尝试在图形区域从该视图中重新选择零件)。";
return false;
}
view = v;
return true;
}
static View? TryFindViewContainingComponent(ModelDoc2 swModel, Component2 target)
{
try
{
var dd = (DrawingDoc)swModel;
var sheet = (Sheet)dd.GetCurrentSheet();
if (sheet == null)
return null;
var views = (object[])sheet.GetViews();
if (views == null)
return null;
foreach (View v in views)
{
if (v == null)
continue;
if (ViewShowsComponent(v, target))
return v;
}
}
catch
{
// ignored
}
return null;
}
static bool ViewShowsComponent(View view, Component2 target)
{
object? visObj = null;
try
{
visObj = view.GetVisibleComponents();
}
catch
{
return false;
}
if (visObj is not object[] roots)
return false;
foreach (object? o in roots)
{
if (o is Component2 root && ComponentSubtreeContains(root, target))
return true;
}
return false;
}
static bool ComponentSubtreeContains(Component2 root, Component2 target)
{
if (root == null || target == null)
return false;
if (NamesEqualInstance(root, target))
return true;
object? chObj = null;
try
{
chObj = root.GetChildren();
}
catch
{
return false;
}
if (chObj is not object[] children)
return false;
foreach (object? c in children)
{
if (c is Component2 child && ComponentSubtreeContains(child, target))
return true;
}
return false;
}
static bool NamesEqualInstance(Component2 a, Component2 b)
{
string na = "";
string nb = "";
try
{
na = a.Name2 ?? "";
}
catch
{
na = "";
}
try
{
nb = b.Name2 ?? "";
}
catch
{
nb = "";
}
return string.Equals(na, nb, StringComparison.Ordinal);
}
static void LogTargetFeaturesThreadFaceSummary(
ModelDoc2 partModel,
PartDoc partDoc,
View view,
Component2? restrictComp)
{
var candidates = new List<Feature>();
CollectTargetFeaturesFromPart(partDoc, candidates);
Console.WriteLine("========== [threadhol_dim] 螺纹/孔候选特征:面数与几何来源 ==========");
if (candidates.Count == 0)
{
Console.WriteLine(" (无候选特征)");
return;
}
foreach (var feat in candidates)
{
int faceCount;
try
{
faceCount = EnumerateFeatureFaces(feat).Count();
}
catch
{
faceCount = -1;
}
string tn = "";
try
{
tn = feat.GetTypeName2() ?? "";
}
catch
{
tn = "?";
}
string nm = "";
try
{
nm = feat.Name ?? "";
}
catch
{
nm = "";
}
string extra = "";
if (string.Equals(tn, "CosmeticThread", StringComparison.OrdinalIgnoreCase))
extra = " " + DescribeCosmeticThreadDefinitionEdge(partModel, feat);
else if (IsHoleLikeThreadSourceFeature(feat))
extra = " " + DescribeHoleLikeFeatureCircleEdgesFromFaces(feat);
Console.WriteLine($" {tn} · {nm} → GetFaces 面数: {faceCount}{extra}");
if (string.Equals(tn, "CosmeticThread", StringComparison.OrdinalIgnoreCase)
&& !TryHasUsableCosmeticThreadDefinitionCircleEdge(partModel, feat))
{
var ids = BuildCthreadSelectByIdNameCandidates(feat, view, partDoc, restrictComp);
if (ids.Count > 0)
{
Console.WriteLine(" → CTHREAD SelectByID2 候选(第二参 Type 填 CTHREAD):");
foreach (var id in ids)
Console.WriteLine(" \"" + id + "\"");
}
else
{
Console.WriteLine(" → CTHREAD:当前规则未生成候选全名(请检查视图/引用零件)。");
}
}
}
}
/// <summary>装饰螺纹不返回 BREP 面时,说明其依附圆边是否可读。</summary>
static string DescribeCosmeticThreadDefinitionEdge(ModelDoc2 partModel, Feature feat)
{
object? defObj = null;
try
{
defObj = feat.GetDefinition();
}
catch
{
return "· CosmeticThread 定义: 无法 GetDefinition";
}
if (defObj is not CosmeticThreadFeatureData ctData)
return "· CosmeticThread 定义: 类型非 CosmeticThreadFeatureData";
Edge? edge = TryGetCosmeticThreadAttachmentEdge(partModel, ctData);
if (edge == null)
return "· CosmeticThread 定义 Edge: 未能读取(标注阶段将尝试 CTHREAD/SelectByID2 捷径)";
if (!IsNearFullCircleEdge(edge, out var curve) || curve == null)
return "· CosmeticThread 定义 Edge: 存在但非整圆(或参数跨度不足)";
try
{
var cp = (double[])curve.CircleParams;
if (cp != null && cp.Length >= 7)
return $"· 定义整圆边 R={cp[6] * 1000:F3} mm";
}
catch
{
// ignored
}
return "· CosmeticThread 定义 Edge: 整圆(半径未读)";
}
/// <summary>孔向导类特征有 BREP 面时,与标注阶段一致地从面收集整圆边,便于与「仅 CTHREAD」区分。</summary>
static string DescribeHoleLikeFeatureCircleEdgesFromFaces(Feature feat)
{
var edges = new List<Edge>();
CollectNearFullCircleEdgesFromFeature(feat, edges);
if (edges.Count == 0)
return "· 从特征面收集整圆边: 0 条(标注阶段无可用圆边)";
var radiiMm = new List<double>(edges.Count);
foreach (var edge in edges)
{
if (!IsNearFullCircleEdge(edge, out var curve) || curve == null)
continue;
try
{
var cp = (double[])curve.CircleParams;
if (cp != null && cp.Length >= 7)
radiiMm.Add(cp[6] * 1000.0);
}
catch
{
// ignored
}
}
string tail = "";
if (radiiMm.Count > 0)
{
radiiMm.Sort();
const int maxShow = 16;
var parts = new List<string>(Math.Min(radiiMm.Count, maxShow));
for (int i = 0; i < radiiMm.Count && i < maxShow; i++)
parts.Add(radiiMm[i].ToString("F3"));
tail = " · R(mm)=" + string.Join(", ", parts);
if (radiiMm.Count > maxShow)
tail += "...";
}
return $"· 从特征面收集整圆边: {edges.Count} 条(模型边→视图可见边,不用 CTHREAD){tail}";
}
/// <summary>从孔/螺纹类特征收集用于标直径的模型整圆边:实体面边 + 装饰螺纹定义边。</summary>
static void CollectThreadHoleCircleEdgesFromFeature(ModelDoc2 partModel, Feature feat, List<Edge> sink)
{
CollectNearFullCircleEdgesFromFeature(feat, sink);
TryCollectCosmeticThreadCircleEdgeFromDefinition(partModel, feat, sink);
}
/// <summary>特征名称是否视为装饰螺纹线计数用(含「装饰螺纹线」及常见误写「孔螺蚊线」)。</summary>
static bool NameLooksLikeDecorativeCosmeticThreadLine(Feature f)
{
string n = "";
try
{
n = f.Name ?? "";
}
catch
{
return false;
}
return n.IndexOf("装饰螺纹线", StringComparison.OrdinalIgnoreCase) >= 0
|| n.IndexOf("孔螺蚊线", StringComparison.OrdinalIgnoreCase) >= 0;
}
/// <summary>在孔向导子树中数 CosmeticThread 且名称匹配 <see cref="NameLooksLikeDecorativeCosmeticThreadLine"/>;子树为 0 时回退为候选中名称含「孔螺蚊线」的个数,仍为 0 再数「装饰螺纹线」。</summary>
static int CountHoleQuantityFromDecorativeCosmeticThreadLine(Feature holeLike, List<Feature> candidates)
{
int sub = CountDecorativeCosmeticThreadLineUnderFeature(holeLike);
if (sub > 0)
return sub;
int nk = CountCosmeticCandidatesNameContains(candidates, "孔螺蚊线");
if (nk > 0)
return nk;
return CountCosmeticCandidatesNameContains(candidates, "装饰螺纹线");
}
static int CountCosmeticCandidatesNameContains(List<Feature> cand, string fragment)
{
int n = 0;
foreach (var c in cand)
{
if (!IsCosmeticThreadType(c))
continue;
string name = "";
try
{
name = c.Name ?? "";
}
catch
{
continue;
}
if (name.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0)
n++;
}
return n;
}
static int CountDecorativeCosmeticThreadInSubtreeRecursive(Feature feat, ref int steps)
{
if (feat == null || steps >= MaxSubFeatureWalkSteps)
return 0;
steps++;
int n = 0;
try
{
if (string.Equals(feat.GetTypeName2() ?? "", "CosmeticThread", StringComparison.OrdinalIgnoreCase)
&& NameLooksLikeDecorativeCosmeticThreadLine(feat))
n++;
}
catch
{
// ignored
}
Feature? ch = (Feature?)feat.GetFirstSubFeature();
while (ch != null && steps < MaxSubFeatureWalkSteps)
{
n += CountDecorativeCosmeticThreadInSubtreeRecursive(ch, ref steps);
ch = (Feature?)ch.GetNextSubFeature();
}
return n;
}
/// <summary>在 <paramref name="root"/> 的直接子特征及其子树中,统计名称含「装饰螺纹线」或「孔螺蚊线」的 CosmeticThread 个数。</summary>
static int CountDecorativeCosmeticThreadLineUnderFeature(Feature root)
{
int steps = 0;
int n = 0;
Feature? sub = (Feature?)root.GetFirstSubFeature();
while (sub != null && steps < MaxSubFeatureWalkSteps)
{
n += CountDecorativeCosmeticThreadInSubtreeRecursive(sub, ref steps);
sub = (Feature?)sub.GetNextSubFeature();
}
return n;
}
/// <summary>
/// 每个候选特征最多一条圆边尺寸(首条整圆边);存在孔向导类候选时不标装饰螺纹定义圆边,以免与孔向导重复。
/// 多个孔向导特征名称解析出<strong>同一规格</strong>(如均为 <c>M5</c>)时只标第一次遇到的。
/// 孔向导尺寸显示:子树或候选中名称含「装饰螺纹线」(及常见误写「孔螺蚊线」)的 CosmeticThread 个数,大于 1 时显示 <c>4-M5</c>。
/// </summary>
static List<(Edge ModelEdge, string? ThreadCalloutDisplay)> BuildThreadHoleCircleDimTargets(
ModelDoc2 partModel,
List<Feature> candidates)
{
bool anyHoleLike = false;
foreach (var f in candidates)
{
if (IsHoleLikeThreadSourceFeature(f))
{
anyHoleLike = true;
break;
}
}
var seenHoleCallout = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var pairs = new List<(Edge e, string? c)>();
foreach (var feat in candidates)
{
var edges = new List<Edge>();
if (anyHoleLike && IsCosmeticThreadType(feat))
CollectNearFullCircleEdgesFromFeature(feat, edges);
else
CollectThreadHoleCircleEdgesFromFeature(partModel, feat, edges);
string? callout = ReplaceHoleWizardDiameterTextWithThreadLabel
? TryParseThreadCalloutFromHoleLikeFeature(feat)
: null;
if (IsHoleLikeThreadSourceFeature(feat) && !string.IsNullOrWhiteSpace(callout))
{
if (seenHoleCallout.Contains(callout))
continue;
seenHoleCallout.Add(callout);
}
Edge? pick = null;
foreach (var e in edges)
{
if (IsNearFullCircleEdge(e, out _))
{
pick = e;
break;
}
}
if (pick == null)
continue;
string? threadDisplay = callout;
if (IsHoleLikeThreadSourceFeature(feat) && !string.IsNullOrWhiteSpace(callout))
{
int qty = CountHoleQuantityFromDecorativeCosmeticThreadLine(feat, candidates);
if (qty > 1)
threadDisplay = $"{qty}-{callout}";
}
pairs.Add((pick, threadDisplay));
}
var merged = new List<(Edge ModelEdge, string? ThreadCalloutDisplay)>();
foreach (var (e, c) in pairs)
{
int idx = -1;
for (int i = 0; i < merged.Count; i++)
{
if (ReferenceEquals(merged[i].ModelEdge, e))
{
idx = i;
break;
}
}
if (idx < 0)
{
merged.Add((e, c));
continue;
}
if (c != null && merged[idx].ThreadCalloutDisplay == null)
merged[idx] = (e, c);
}
return merged;
}
static bool IsHoleLikeThreadSourceFeature(Feature feat)
{
if (feat == null)
return false;
string tn;
try
{
tn = feat.GetTypeName2() ?? "";
}
catch
{
return false;
}
return string.Equals(tn, "HoleWzd", StringComparison.OrdinalIgnoreCase)
|| string.Equals(tn, "AdvHole", StringComparison.OrdinalIgnoreCase)
|| string.Equals(tn, "AdvancedHole", StringComparison.OrdinalIgnoreCase);
}
/// <summary>从孔向导类特征名称提取 <c>M5</c>、<c>M8x1.25</c> 等(忽略大小写与空格)。</summary>
static string? TryParseThreadCalloutFromHoleLikeFeature(Feature feat)
{
if (!IsHoleLikeThreadSourceFeature(feat))
return null;
string name;
try
{
name = feat.Name ?? "";
}
catch
{
return null;
}
if (string.IsNullOrWhiteSpace(name))
return null;
var m = HoleFeatureThreadCalloutFromName.Match(name);
if (!m.Success)
return null;
string raw = m.Groups[1].Value;
if (string.IsNullOrWhiteSpace(raw))
return null;
string compact = raw.Replace(" ", "").Replace("×", "x");
return string.IsNullOrWhiteSpace(compact) ? null : compact;
}
/// <summary>
/// SolidWorks 中 <c>CosmeticThread</c> 常无 <c>GetFaces()</c>,依附孔口圆边保存在 <see cref="CosmeticThreadFeatureData.Edge"/>。
/// </summary>
static void TryCollectCosmeticThreadCircleEdgeFromDefinition(ModelDoc2 partModel, Feature feat, List<Edge> sink)
{
if (partModel == null)
return;
string tn = "";
try
{
tn = feat.GetTypeName2() ?? "";
}
catch
{
return;
}
if (!string.Equals(tn, "CosmeticThread", StringComparison.OrdinalIgnoreCase))
return;
object? defObj = null;
try
{
defObj = feat.GetDefinition();
}
catch
{
return;
}
if (defObj is not CosmeticThreadFeatureData ctData)
return;
Edge? edge = TryGetCosmeticThreadAttachmentEdge(partModel, ctData);
if (edge == null)
return;
if (!IsNearFullCircleEdge(edge, out _))
return;
if (!ContainsEdgeByReference(sink, edge))
sink.Add(edge);
}
static Edge? TryGetCosmeticThreadAttachmentEdge(ModelDoc2 partModel, CosmeticThreadFeatureData ctData)
{
try
{
var e = ctData.Edge;
if (e != null)
return e;
}
catch
{
// 部分版本需 AccessSelections 后才可读 Edge
}
try
{
try
{
partModel.ClearSelection2(true);
}
catch
{
// ignored
}
if (!ctData.AccessSelections(partModel, null))
return null;
try
{
return ctData.Edge;
}
finally
{
try
{
ctData.ReleaseSelectionAccess();
}
catch
{
// ignored
}
}
}
catch
{
return null;
}
}
const string DrawingCosmeticThreadSelectType = "CTHREAD";
static bool IsCosmeticThreadType(Feature feat)
{
try
{
return string.Equals(feat.GetTypeName2() ?? "", "CosmeticThread", StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
/// <summary>装饰螺纹是否已能通过零件定义边走「圆边→视图边」标注路径。</summary>
static bool TryHasUsableCosmeticThreadDefinitionCircleEdge(ModelDoc2 partModel, Feature feat)
{
if (!IsCosmeticThreadType(feat))
return false;
object? defObj = null;
try
{
defObj = feat.GetDefinition();
}
catch
{
return false;
}
if (defObj is not CosmeticThreadFeatureData ctData)
return false;
Edge? edge = TryGetCosmeticThreadAttachmentEdge(partModel, ctData);
return edge != null && IsNearFullCircleEdge(edge, out _);
}
/// <summary>
/// 工程图宏常用路径:<c>ActivateView</c> 后用 <c>SelectByID2(全名, CTHREAD, x,y,z)</c> 选中装饰螺纹,再 <c>AddDimension2</c>。
/// 全名常见形式:<c>特征名@零件名-配置名@视图名</c>(零件图)或 <c>特征名@Component.Name2@视图名</c>(装配图)。
/// </summary>
static bool TryAddCosmeticThreadDiameterViaCthreadSelect(
ModelDoc2 drwModel,
DrawingDoc drawingDoc,
View view,
PartDoc partDoc,
Feature cosmeticFeat,
Component2? restrictComp,
ref double textStaggerM)
{
ExitDrawingSketchIfActive(drwModel);
try
{
drwModel.ClearSelection2(true);
}
catch
{
// ignored
}
TryActivateDrawingSheetAndView(drawingDoc, view);
var names = BuildCthreadSelectByIdNameCandidates(cosmeticFeat, view, partDoc, restrictComp);
if (names.Count == 0)
return false;
double[] pickZs = { 0, 500, -500 };
double[] outline = Array.Empty<double>();
try
{
var o = (double[])view.GetOutline();
if (o != null && o.Length >= 4)
outline = o;
}
catch
{
outline = Array.Empty<double>();
}
double cx = 0, cy = 0;
bool haveCenter = false;
if (outline.Length >= 4)
{
cx = (outline[0] + outline[2]) * 0.5;
cy = (outline[1] + outline[3]) * 0.5;
haveCenter = true;
}
foreach (string id in names)
{
foreach (double pz in pickZs)
{
foreach (var pick in EnumerateCthreadPickPoints(haveCenter, cx, cy, pz))
{
try
{
drwModel.ClearSelection2(true);
}
catch
{
// ignored
}
bool sel;
try
{
sel = drwModel.Extension.SelectByID2(
id,
DrawingCosmeticThreadSelectType,
pick.x,
pick.y,
pick.z,
false,
0,
null,
0);
}
catch
{
sel = false;
}
if (!sel)
continue;
if (!TryGetCthreadDimensionLeaderSheetXY(view, ref textStaggerM, out var dx, out var dy))
{
try
{
drwModel.ClearSelection2(true);
}
catch
{
// ignored
}
continue;
}
object? dim = null;
try
{
dim = drwModel.AddDimension2(dx, dy, 0);
}
catch
{
dim = null;
}
try
{
drwModel.ClearSelection2(true);
}
catch
{
// ignored
}
if (dim != null)
{
Console.WriteLine($"[threadhol_dim] CTHREAD 捷径成功: {id}");
return true;
}
}
}
}
return false;
}
static IEnumerable<(double x, double y, double z)> EnumerateCthreadPickPoints(bool haveCenter, double cx, double cy, double pz)
{
if (haveCenter)
{
yield return (cx, cy, pz);
yield return (cx + 0.0005, cy + 0.0005, pz);
yield return (cx - 0.0005, cy - 0.0005, pz);
}
yield return (0, 0, pz);
}
static void TryActivateDrawingSheetAndView(DrawingDoc drawingDoc, View view)
{
try
{
var sheet = view.Sheet as Sheet;
if (sheet != null)
{
string sn = sheet.GetName();
if (!string.IsNullOrWhiteSpace(sn))
drawingDoc.ActivateSheet(sn);
}
}
catch
{
// ignored
}
try
{
drawingDoc.ActivateView(view.GetName2());
}
catch
{
try
{
drawingDoc.ActivateView(view.Name);
}
catch
{
// ignored
}
}
}
static List<string> BuildCthreadSelectByIdNameCandidates(
Feature feat,
View view,
PartDoc partDoc,
Component2? restrictComp)
{
var set = new HashSet<string>(StringComparer.Ordinal);
string featName = "";
try
{
featName = feat.Name ?? "";
}
catch
{
featName = "";
}
if (string.IsNullOrWhiteSpace(featName))
return new List<string>();
var viewNames = new List<string>();
void addViewName(string? s)
{
if (string.IsNullOrWhiteSpace(s))
return;
s = s.Trim();
if (!viewNames.Contains(s, StringComparer.Ordinal))
viewNames.Add(s);
}
try
{
addViewName(view.GetName2());
}
catch
{
// ignored
}
try
{
addViewName(view.Name);
}
catch
{
// ignored
}
if (viewNames.Count == 0)
return new List<string>();
string cfg = "";
try
{
cfg = view.ReferencedConfiguration ?? "";
}
catch
{
cfg = "";
}
cfg = cfg.Trim();
var refDoc = view.ReferencedDocument as ModelDoc2;
if (refDoc == null)
refDoc = (ModelDoc2)partDoc;
string path = "";
try
{
path = refDoc?.GetPathName() ?? "";
}
catch
{
path = "";
}
string baseName = "";
if (!string.IsNullOrWhiteSpace(path))
baseName = Path.GetFileNameWithoutExtension(path.Trim());
if (string.IsNullOrWhiteSpace(baseName) && refDoc != null)
{
try
{
baseName = TrimSolidWorksModelTitle(refDoc.GetTitle());
}
catch
{
baseName = "";
}
}
foreach (string vn in viewNames)
{
if (restrictComp != null)
{
string compName = "";
try
{
compName = restrictComp.Name2 ?? "";
}
catch
{
compName = "";
}
if (!string.IsNullOrWhiteSpace(compName))
set.Add($"{featName}@{compName}@{vn}");
}
else
{
if (!string.IsNullOrWhiteSpace(baseName))
{
if (!string.IsNullOrWhiteSpace(cfg) && !string.Equals(cfg, "默认", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(cfg, "Default", StringComparison.OrdinalIgnoreCase))
{
set.Add($"{featName}@{baseName}-{cfg}@{vn}");
}
set.Add($"{featName}@{baseName}@{vn}");
}
}
}
return set.ToList();
}
static string TrimSolidWorksModelTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
return "";
string t = title.Trim();
foreach (var ext in new[] { ".sldprt", ".SLDPRT", ".sldasm", ".SLDASM" })
{
if (t.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
return t.Substring(0, t.Length - ext.Length);
}
return t;
}
static bool TryGetCthreadDimensionLeaderSheetXY(
View view,
ref double staggerAlongM,
out double sheetX,
out double sheetY)
{
sheetX = sheetY = 0;
try
{
var o = (double[])view.GetOutline();
if (o == null || o.Length < 4)
return false;
double mx = (o[0] + o[2]) * 0.5;
double my = (o[1] + o[3]) * 0.5;
double offBase = DimensionTextPerpBaseOffsetM;
sheetX = mx + staggerAlongM;
sheetY = my + offBase;
staggerAlongM += DimensionTextPerpStaggerStepM;
return true;
}
catch
{
return false;
}
}
static bool IsTargetFeature(Feature feat)
{
string tn = feat.GetTypeName2() ?? "";
if (TargetFeatureTypeNames.Contains(tn))
return true;
if (MatchTypeNameContainsThread && tn.IndexOf("thread", StringComparison.OrdinalIgnoreCase) >= 0)
return true;
return false;
}
static void CollectTargetFeaturesFromPart(PartDoc partDoc, List<Feature> sink)
{
int steps = 0;
Feature? f = (Feature?)partDoc.FirstFeature();
while (f != null && steps < MaxSubFeatureWalkSteps)
{
CollectTargetFeaturesFromNode(f, sink, ref steps);
f = (Feature?)f.GetNextFeature();
}
}
static void CollectTargetFeaturesFromNode(Feature feat, List<Feature> sink, ref int steps)
{
if (steps >= MaxSubFeatureWalkSteps)
return;
steps++;
if (IsTargetFeature(feat))
sink.Add(feat);
Feature? sub = (Feature?)feat.GetFirstSubFeature();
while (sub != null && steps < MaxSubFeatureWalkSteps)
{
CollectTargetFeaturesFromNode(sub, sink, ref steps);
sub = (Feature?)sub.GetNextSubFeature();
}
}
static void WalkPartFeatureTree(Feature? top, Dictionary<string, int> typeCounts, List<string> lines, int depth)
{
int steps = 0;
Feature? f = top;
while (f != null && steps < MaxSubFeatureWalkSteps)
{
WalkFeatureNode(f, typeCounts, lines, depth, ref steps);
f = (Feature?)f.GetNextFeature();
}
}
static void WalkFeatureNode(Feature feat, Dictionary<string, int> typeCounts, List<string> lines, int depth, ref int steps)
{
if (steps >= MaxSubFeatureWalkSteps)
return;
steps++;
string tn = feat.GetTypeName2() ?? "";
if (!typeCounts.ContainsKey(tn))
typeCounts[tn] = 0;
typeCounts[tn]++;
string pad = new string(' ', depth * 2);
lines.Add($"{pad}[{depth}] {tn} · {feat.Name}");
Feature? sub = (Feature?)feat.GetFirstSubFeature();
while (sub != null && steps < MaxSubFeatureWalkSteps)
{
WalkFeatureNode(sub, typeCounts, lines, depth + 1, ref steps);
sub = (Feature?)sub.GetNextSubFeature();
}
}
static void CollectNearFullCircleEdgesFromFeature(Feature feat, List<Edge> sink)
{
foreach (Face face in EnumerateFeatureFaces(feat))
{
object? edgesObj = null;
try
{
edgesObj = face.GetEdges();
}
catch
{
continue;
}
if (edgesObj is not object[] edgeArr)
continue;
foreach (object? eo in edgeArr)
{
if (eo is not Edge edge)
continue;
if (!IsNearFullCircleEdge(edge, out _))
continue;
if (!ContainsEdgeByReference(sink, edge))
sink.Add(edge);
}
}
}
static bool ContainsEdgeByReference(List<Edge> list, Edge e)
{
foreach (var x in list)
{
if (ReferenceEquals(x, e))
return true;
}
return false;
}
static bool IsNearFullCircleEdge(Edge edge, out Curve? curveOut)
{
curveOut = null;
try
{
var curve = (Curve)edge.GetCurve();
if (curve == null || !curve.IsCircle())
return false;
curve.GetEndParams(out var t0, out var t1, out _, out _);
double span = Math.Abs(t1 - t0);
if (span < MinCircleEdgeParamSpan)
return false;
curveOut = curve;
return true;
}
catch
{
return false;
}
}
static Edge? FindVisibleCircleEdge(View view, Edge modelEdge, Component2? restrictToComponent)
{
var mCurve = (Curve)modelEdge.GetCurve();
if (mCurve == null || !mCurve.IsCircle())
return null;
var mCp = (double[])mCurve.CircleParams;
if (mCp == null || mCp.Length < 7)
return null;
double rM = mCp[6];
double cxM = mCp[0], cyM = mCp[1], czM = mCp[2];
double radTol = Math.Max(CircleRadiusAbsTolM, rM * CircleRadiusRelTol);
var visibleComps = (object[])view.GetVisibleComponents();
if (visibleComps == null)
return null;
Edge? best = null;
double bestScore = double.MaxValue;
foreach (Component2 comp in visibleComps)
{
if (comp == null)
continue;
if (restrictToComponent != null && !ComponentSubtreeContains(comp, restrictToComponent))
continue;
var visibleEdges = (object[])view.GetVisibleEntities(
comp, (int)swViewEntityType_e.swViewEntityType_Edge);
if (visibleEdges == null)
continue;
foreach (object obj in visibleEdges)
{
if (obj is not Edge visEdge)
continue;
if (ReferenceEquals(visEdge, modelEdge))
return visEdge;
try
{
var vCurve = (Curve)visEdge.GetCurve();
if (vCurve == null || !vCurve.IsCircle())
continue;
var vCp = (double[])vCurve.CircleParams;
if (vCp == null || vCp.Length < 7)
continue;
double rV = vCp[6];
if (Math.Abs(rV - rM) > radTol)
continue;
double dx = vCp[0] - cxM;
double dy = vCp[1] - cyM;
double dz = vCp[2] - czM;
double dCtr = Math.Sqrt(dx * dx + dy * dy + dz * dz);
if (dCtr > CircleCenterTolM)
continue;
double score = dCtr + Math.Abs(rV - rM);
if (score < bestScore)
{
bestScore = score;
best = visEdge;
}
}
catch
{
// continue
}
}
}
return best;
}
static void LogCircleSkip(Edge modelEdge, string reason)
{
try
{
var c = (Curve)modelEdge.GetCurve();
var cp = c != null ? (double[])c.CircleParams : null;
if (cp != null && cp.Length >= 7)
{
Console.WriteLine(
$"[threadhol_dim] 跳过:{reason} · R={cp[6] * 1000:F3} mm 中心(mm)({cp[0] * 1000:F2},{cp[1] * 1000:F2},{cp[2] * 1000:F2})");
}
else
{
Console.WriteLine($"[threadhol_dim] 跳过:{reason}");
}
}
catch
{
Console.WriteLine($"[threadhol_dim] 跳过:{reason}");
}
}
static bool TryApplyThreadCalloutToDisplayDimension(DisplayDimension disp, string text)
{
if (disp == null || string.IsNullOrWhiteSpace(text))
return false;
try
{
disp.SetText((int)swDimensionTextParts_e.swDimensionTextAll, text);
return true;
}
catch
{
// ignored
}
try
{
disp.SetText((int)swDimensionTextParts_e.swDimensionTextCalloutAbove, text);
return true;
}
catch
{
// ignored
}
return false;
}
static bool TryAddDiameterDimension(
ISldWorks swApp,
ModelDoc2 swModel,
View view,
Edge modelCircleEdge,
Edge visibleCircleEdge,
string? threadCalloutDisplay,
ref double textStaggerM)
{
ExitDrawingSketchIfActive(swModel);
if (!TryCircleDiameterOppositePoints(modelCircleEdge, out var pA, out var pB))
{
Console.WriteLine("[threadhol_dim] 无法取圆直径端点,跳过。");
return false;
}
if (!TryGetDimensionLeaderSheetXYFromTwoModelPoints(swApp, view, pA, pB, ref textStaggerM, out var x, out var y))
{
swModel.ClearSelection2(true);
Console.WriteLine("[threadhol_dim] 尺寸文字位置计算失败。");
return false;
}
var selMgr = (SelectionMgr)swModel.SelectionManager;
var selData = selMgr.CreateSelectData();
selData.View = view;
((Entity)visibleCircleEdge).Select4(true, selData);
var dimObj = swModel.AddDimension2(x, y, 0);
swModel.ClearSelection2(true);
if (dimObj is not DisplayDimension disp)
{
Console.WriteLine("[threadhol_dim] AddDimension2 返回空(可检查视图是否显示该圆边)。");
return false;
}
if (!string.IsNullOrWhiteSpace(threadCalloutDisplay))
{
if (TryApplyThreadCalloutToDisplayDimension(disp, threadCalloutDisplay.Trim()))
Console.WriteLine($"[threadhol_dim] 孔向导直径显示已改为: {threadCalloutDisplay.Trim()}");
else
Console.WriteLine($"[threadhol_dim] 孔向导直径显示改写未成功,保留数值显示: {threadCalloutDisplay.Trim()}");
}
return true;
}
static bool TryCircleDiameterOppositePoints(Edge edge, out double[] a, out double[] b)
{
a = b = Array.Empty<double>();
try
{
var curve = (Curve)edge.GetCurve();
if (curve == null || !curve.IsCircle())
return false;
var cp = (double[])curve.CircleParams;
if (cp == null || cp.Length < 7)
return false;
double cx = cp[0], cy = cp[1], cz = cp[2];
double ax = cp[3], ay = cp[4], az = cp[5];
double r = cp[6];
double alen = Math.Sqrt(ax * ax + ay * ay + az * az);
if (alen < 1e-12)
return false;
ax /= alen;
ay /= alen;
az /= alen;
double ux = 1, uy = 0, uz = 0;
double cxu = Math.Abs(ax * ux + ay * uy + az * uz);
if (cxu > 0.95)
{
ux = 0;
uy = 1;
uz = 0;
}
double px = ay * uz - az * uy;
double py = az * ux - ax * uz;
double pz = ax * uy - ay * ux;
double plen = Math.Sqrt(px * px + py * py + pz * pz);
if (plen < 1e-12)
return false;
px /= plen;
py /= plen;
pz /= plen;
a = new[] { cx + px * r, cy + py * r, cz + pz * r };
b = new[] { cx - px * r, cy - py * r, cz - pz * r };
return true;
}
catch
{
return false;
}
}
static bool TryModelPointToSheetXY(ISldWorks swApp, View view, double[] model3, out double sheetX, out double sheetY)
{
sheetX = sheetY = 0;
try
{
var math = swApp.IGetMathUtility();
if (math == null)
return false;
var mp = (MathPoint)math.CreatePoint(model3);
if (mp == null)
return false;
var modelToSheet = (MathTransform)view.ModelToViewTransform;
if (modelToSheet == null)
return false;
mp = (MathPoint)mp.MultiplyTransform(modelToSheet);
var arr = (double[])mp.ArrayData;
if (arr == null || arr.Length < 2)
return false;
sheetX = arr[0];
sheetY = arr[1];
return true;
}
catch
{
return false;
}
}
static bool TryGetDimensionLeaderSheetXYFromTwoModelPoints(
ISldWorks swApp,
View view,
double[] modelA,
double[] modelB,
ref double perpStaggerM,
out double sheetX,
out double sheetY)
{
sheetX = sheetY = 0;
if (!TryModelPointToSheetXY(swApp, view, modelA, out var ax, out var ay))
return false;
if (!TryModelPointToSheetXY(swApp, view, modelB, out var bx, out var by))
return false;
double mx = (ax + bx) * 0.5;
double my = (ay + by) * 0.5;
double vx = bx - ax;
double vy = by - ay;
double len = Math.Sqrt(vx * vx + vy * vy);
double nx, ny;
if (len > 1e-12)
{
nx = -vy / len;
ny = vx / len;
}
else
{
nx = 0;
ny = 1;
}
double off = DimensionTextPerpBaseOffsetM + perpStaggerM;
perpStaggerM += DimensionTextPerpStaggerStepM;
sheetX = mx + nx * off;
sheetY = my + ny * off;
return true;
}
/// <summary>
/// <c>Feature.GetFaces()</c> 可能为 null;COM 在仅一面时有时返回单个 <see cref="Face"/> 而非 object[](与 <see cref="benddim"/> 一致)。
/// </summary>
static IEnumerable<Face> EnumerateFeatureFaces(Feature feat)
{
if (feat == null) yield break;
object raw;
try
{
raw = feat.GetFaces();
}
catch
{
yield break;
}
if (raw == null) yield break;
if (raw is object[] arr)
{
foreach (var o in arr)
{
if (o is Face f) yield return f;
}
yield break;
}
if (raw is Face one) yield return one;
}
/// <summary>
/// 使用零件文档全局明细注释文字格式,与折弯标注 <see cref="benddim"/> 一致,避免新建尺寸文字被局部样式托管覆盖。
/// </summary>
static void ApplyGlobalAnnotationTextHeight(ModelDoc2 partModel)
{
try
{
var myTextFormat = partModel.Extension.GetUserPreferenceTextFormat(
(int)swUserPreferenceTextFormat_e.swDetailingAnnotationTextFormat, 0) as TextFormat;
if (myTextFormat == null) return;
myTextFormat.CharHeight = 0.0035;
bool boolstatus = partModel.Extension.SetUserPreferenceTextFormat(
(int)swUserPreferenceTextFormat_e.swDetailingAnnotationTextFormat, 0, myTextFormat);
Console.WriteLine($"[threadhol_dim] 全局注释文字高度 0.0035 m,SetUserPreferenceTextFormat: {boolstatus}");
}
catch (Exception ex)
{
Console.WriteLine($"[threadhol_dim] 设置全局注释文字高度失败:{ex.Message}");
}
}
static void ExitDrawingSketchIfActive(ModelDoc2 swModel)
{
try
{
try
{
swModel.SketchManager.AddToDB = false;
}
catch
{
// ignored
}
for (int i = 0; i < 6; i++)
{
try
{
if (swModel.SketchManager.ActiveSketch == null)
break;
swModel.SketchManager.InsertSketch(false);
}
catch
{
break;
}
}
try
{
swModel.ClearSelection2(true);
}
catch
{
// ignored
}
}
catch
{
// ignored
}
}
}
}