refactor(core): унификация COM-хелперов и классификаторов топологии
Вынесены общие ComHelper (Release + Safe-геттеры) и GeomClassifiers (FaceType/EdgeType), устранены дубли в 5+ сервисах. Освобождение doc3d RCW в GetTopPart, void-возвраты вместо фиктивных bool, HashSet для OpKinds, DocumentTypeName в паспорте модели, FileExtension без аллокаций. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6Constants;
|
||||
using KompasAPI7;
|
||||
@@ -39,7 +39,7 @@ public sealed class ConversionService
|
||||
CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => ImportCore(path, docType, createComponentFiles), ct);
|
||||
|
||||
private bool ImportCore(string path, KompasDocumentType docType, bool createComponentFiles)
|
||||
private void ImportCore(string path, KompasDocumentType docType, bool createComponentFiles)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"STEP-файл не найден: {path}", path);
|
||||
@@ -63,19 +63,22 @@ public sealed class ConversionService
|
||||
prm.Format = (ksKOMPASConverterEnum)StepFormats.ImportConverterCode;
|
||||
if (createComponentFiles)
|
||||
{
|
||||
// NeedCreateComponentsFiles может отсутствовать в Home-редакции КОМПАС —
|
||||
// ловим COM-исключения; не критично, если свойство недоступно.
|
||||
try { prm.NeedCreateComponentsFiles = true; }
|
||||
catch { /* свойство может быть недоступно — не критично */ }
|
||||
catch (System.Runtime.InteropServices.COMException) { }
|
||||
catch (System.Runtime.InteropServices.InvalidComObjectException) { }
|
||||
catch (NotImplementedException) { }
|
||||
}
|
||||
|
||||
if (!doc3d1.ConvertFromAdditionFormat(path, (AdditionConvertParameters)prm))
|
||||
throw new InvalidOperationException("ConvertFromAdditionFormat вернул FALSE (импорт STEP не удался).");
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseCom(prm);
|
||||
ReleaseCom(doc);
|
||||
ReleaseCom(conv);
|
||||
ComHelper.Release(prm);
|
||||
ComHelper.Release(doc);
|
||||
ComHelper.Release(conv);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +86,7 @@ public sealed class ConversionService
|
||||
public Task ExportStepAsync(string path, StepFormat format = StepFormat.Auto, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => ExportCore(path, format), ct);
|
||||
|
||||
private bool ExportCore(string path, StepFormat format)
|
||||
private void ExportCore(string path, StepFormat format)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Путь экспорта не задан.", nameof(path));
|
||||
@@ -109,13 +112,12 @@ public sealed class ConversionService
|
||||
|
||||
if (!doc3d1.ConvertToAdditionFormat(path, (AdditionConvertParameters)prm))
|
||||
throw new InvalidOperationException("ConvertToAdditionFormat вернул FALSE (экспорт STEP не удался).");
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseCom(prm);
|
||||
ReleaseCom(active);
|
||||
ReleaseCom(conv);
|
||||
ComHelper.Release(prm);
|
||||
ComHelper.Release(active);
|
||||
ComHelper.Release(conv);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,13 +133,4 @@ public sealed class ConversionService
|
||||
throw new InvalidOperationException(
|
||||
$"get_Converter({formatCode}) вернул null — STEP-конвертер недоступен в этой редакции КОМПАС.");
|
||||
}
|
||||
|
||||
private static void ReleaseCom(object? comObject)
|
||||
{
|
||||
if (comObject is not null && Marshal.IsComObject(comObject))
|
||||
{
|
||||
try { Marshal.ReleaseComObject(comObject); }
|
||||
catch { /* освобождение не критично */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6Constants;
|
||||
using KompasAPI7;
|
||||
@@ -26,7 +26,7 @@ public sealed class DocumentService
|
||||
var docs = _session.Application.Documents;
|
||||
var doc = (IKompasDocument)docs.Add(DocumentTypes.ToEnum(type), visible);
|
||||
try { return ToInfo(doc); }
|
||||
finally { ReleaseCom(doc); ReleaseCom(docs); }
|
||||
finally { ComHelper.Release(doc); ComHelper.Release(docs); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Открыть документ с диска.</summary>
|
||||
@@ -38,7 +38,7 @@ public sealed class DocumentService
|
||||
if (doc is null)
|
||||
throw new InvalidOperationException($"Не удалось открыть документ: {path}");
|
||||
try { return ToInfo(doc); }
|
||||
finally { ReleaseCom(doc); ReleaseCom(docs); }
|
||||
finally { ComHelper.Release(doc); ComHelper.Release(docs); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Сводка по активному документу или <c>null</c>, если документов нет.</summary>
|
||||
@@ -48,7 +48,7 @@ public sealed class DocumentService
|
||||
var doc = _session.Application.ActiveDocument;
|
||||
if (doc is null) return null;
|
||||
try { return ToInfo(doc); }
|
||||
finally { ReleaseCom(doc); }
|
||||
finally { ComHelper.Release(doc); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Сохранить активный документ (для уже сохранённого — без диалога).</summary>
|
||||
@@ -72,7 +72,7 @@ public sealed class DocumentService
|
||||
var doc = _session.Application.ActiveDocument
|
||||
?? throw new InvalidOperationException("Нет активного документа.");
|
||||
try { return func(doc); }
|
||||
finally { ReleaseCom(doc); }
|
||||
finally { ComHelper.Release(doc); }
|
||||
}
|
||||
|
||||
private static DocumentInfo ToInfo(IKompasDocument doc) => new()
|
||||
@@ -81,13 +81,4 @@ public sealed class DocumentService
|
||||
PathName = doc.PathName ?? string.Empty,
|
||||
Type = DocumentTypes.FromEnum(doc.DocumentType),
|
||||
};
|
||||
|
||||
private static void ReleaseCom(object? comObject)
|
||||
{
|
||||
if (comObject is not null && Marshal.IsComObject(comObject))
|
||||
{
|
||||
try { Marshal.ReleaseComObject(comObject); }
|
||||
catch { /* освобождение не критично */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class FaceEditService
|
||||
public Task MoveFaceAsync(double x, double y, double z, double distance, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => MoveFaceCore(x, y, z, distance), ct);
|
||||
|
||||
private bool MoveFaceCore(double x, double y, double z, double distance)
|
||||
private void MoveFaceCore(double x, double y, double z, double distance)
|
||||
{
|
||||
if (distance == 0) throw new ArgumentException("Смещение не может быть нулевым.", nameof(distance));
|
||||
|
||||
@@ -61,7 +61,6 @@ public sealed class FaceEditService
|
||||
|
||||
// Перестраиваем, чтобы состояние ошибки операции стало актуальным (проверяется через validate_part).
|
||||
doc3d.RebuildDocument();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IFace? FindFaceAtPoint(IPart7 part, double x, double y, double z)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Kompas.Mcp.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Вспомогательные методы для работы с COM RCW: безопасное освобождение
|
||||
/// и safe-геттеры для свойств, которые могут бросить исключение.
|
||||
/// <para>
|
||||
/// Метод <see cref="Release"/> идентично реализован в каждом сервисе — вынесен сюда.
|
||||
/// Safe-геттеры заменяют разрозненные <c>SafeStr</c>/<c>SafeGet</c>/<c>SafeBool</c>/<c>SafeNum</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class ComHelper
|
||||
{
|
||||
/// <summary>Освободить COM RCW: null-гард + IsComObject-проверка + try/catch.</summary>
|
||||
internal static void Release(object? com)
|
||||
{
|
||||
if (com is not null && Marshal.IsComObject(com))
|
||||
try { Marshal.ReleaseComObject(com); } catch { }
|
||||
}
|
||||
|
||||
/// <summary>Безопасно получить строку: возвращает <c>""</c> при любом исключении.</summary>
|
||||
internal static string SafeStr(Func<string?> f) { try { return f() ?? ""; } catch { return ""; } }
|
||||
|
||||
/// <summary>Безопасно получить число: возвращает <c>0</c> при любом исключении.</summary>
|
||||
internal static double SafeNum(Func<double> f) { try { return f(); } catch { return 0; } }
|
||||
|
||||
/// <summary>Безопасно получить флаг: возвращает <c>false</c> при любом исключении.</summary>
|
||||
internal static bool SafeBool(Func<bool> f) { try { return f(); } catch { return false; } }
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public sealed class KompasSession : IDisposable
|
||||
private KompasConnectionInfo ConnectCore(bool visible)
|
||||
{
|
||||
if (_app is not null)
|
||||
return LastInfo ??= BuildInfo(LastProgId);
|
||||
return LastInfo ??= BuildInfo(_lastProgId);
|
||||
|
||||
// 1) attach к уже запущенному экземпляру
|
||||
foreach (var id in ProgIds)
|
||||
@@ -73,7 +73,7 @@ public sealed class KompasSession : IDisposable
|
||||
if (NativeMethods.GetActiveObject(id) is KompasObject ko)
|
||||
{
|
||||
_kompas = ko;
|
||||
LastProgId = id;
|
||||
_lastProgId = id;
|
||||
_launchedByUs = false;
|
||||
_log.LogInformation("Присоединились к запущенному КОМПАС ({ProgId})", id);
|
||||
break;
|
||||
@@ -92,7 +92,7 @@ public sealed class KompasSession : IDisposable
|
||||
if (Activator.CreateInstance(type) is KompasObject ko)
|
||||
{
|
||||
_kompas = ko;
|
||||
LastProgId = id;
|
||||
_lastProgId = id;
|
||||
_launchedByUs = true;
|
||||
_log.LogInformation("Запустили новый КОМПАС ({ProgId})", id);
|
||||
break;
|
||||
@@ -114,10 +114,10 @@ public sealed class KompasSession : IDisposable
|
||||
|
||||
_app.Visible = visible;
|
||||
|
||||
return LastInfo = BuildInfo(LastProgId);
|
||||
return LastInfo = BuildInfo(_lastProgId);
|
||||
}
|
||||
|
||||
private string LastProgId { get; set; } = string.Empty;
|
||||
private string _lastProgId = string.Empty;
|
||||
|
||||
private KompasConnectionInfo BuildInfo(string progId)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
@@ -432,14 +432,7 @@ public sealed class PartModeler : IDisposable
|
||||
_nextId = 1;
|
||||
}
|
||||
|
||||
private static void ReleaseCom(object? comObject)
|
||||
{
|
||||
if (comObject is not null && Marshal.IsComObject(comObject))
|
||||
{
|
||||
try { Marshal.ReleaseComObject(comObject); }
|
||||
catch { /* освобождение не критично */ }
|
||||
}
|
||||
}
|
||||
private static void ReleaseCom(object? comObject) => ComHelper.Release(comObject);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Kompas6API5;
|
||||
|
||||
namespace Kompas.Mcp.Core.Query;
|
||||
|
||||
/// <summary>
|
||||
/// Классификаторы топологических объектов КОМПАС: строковый тип грани и ребра.
|
||||
/// <para>
|
||||
/// Каждый вызов COM-метода обёрнут в отдельный <c>try/catch</c> — на экзотической геометрии
|
||||
/// (NURBS, импортированные STEP-тела) методы класса <c>ksFaceDefinition</c>/<c>ksEdgeDefinition</c>
|
||||
/// могут бросить <c>COMException</c>, что не должно прерывать bulk-перебор. Ранее этот код
|
||||
/// был продублирован в <c>QueryService</c> (без защиты) и <c>ModelInspectionService</c> (с защитой).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class GeomClassifiers
|
||||
{
|
||||
/// <summary>Строковый тип поверхности: plane/cylinder/cone/sphere/torus/nurbs/other.</summary>
|
||||
internal static string FaceType(ksFaceDefinition d)
|
||||
{
|
||||
try { if (d.IsPlanar()) return "plane"; } catch { }
|
||||
try { if (d.IsCylinder()) return "cylinder"; } catch { }
|
||||
try { if (d.IsCone()) return "cone"; } catch { }
|
||||
try { if (d.IsSphere()) return "sphere"; } catch { }
|
||||
try { if (d.IsTorus()) return "torus"; } catch { }
|
||||
try { if (d.IsNurbsSurface()) return "nurbs"; } catch { }
|
||||
return "other";
|
||||
}
|
||||
|
||||
/// <summary>Строковый тип кривой: line/circle/arc/ellipse_arc/ellipse/nurbs/other.</summary>
|
||||
internal static string EdgeType(ksEdgeDefinition d)
|
||||
{
|
||||
try { if (d.IsLineSeg()) return "line"; } catch { }
|
||||
try { if (d.IsCircle()) return "circle"; } catch { }
|
||||
try { if (d.IsArc()) return "arc"; } catch { }
|
||||
try { if (d.IsEllipseArc()) return "ellipse_arc"; } catch { }
|
||||
try { if (d.IsEllipse()) return "ellipse"; } catch { }
|
||||
try { if (d.IsNurbs()) return "nurbs"; } catch { }
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,9 @@ public static class InspectionText
|
||||
}
|
||||
|
||||
// Формообразующие операции, наличие которых означает параметрическую историю.
|
||||
private static readonly string[] OpKinds =
|
||||
{ "sketch", "extrusion", "cut_extrusion", "revolve", "cut_revolve", "fillet", "chamfer" };
|
||||
// HashSet<string> даёт O(1) Contains вместо O(n) по массиву при bulk-проверке граней.
|
||||
private static readonly HashSet<string> OpKinds =
|
||||
new(StringComparer.Ordinal) { "sketch", "extrusion", "cut_extrusion", "revolve", "cut_revolve", "fillet", "chamfer" };
|
||||
|
||||
/// <summary>
|
||||
/// Модель — «голый» импорт без истории построения (типично для STEP): в дереве только начало
|
||||
@@ -39,18 +40,21 @@ public static class InspectionText
|
||||
public static string GroupTypes(IReadOnlyList<string> types)
|
||||
{
|
||||
if (types.Count == 0) return "—";
|
||||
// Count материализуем один раз: IGrouping<K,V> — IEnumerable без кеша,
|
||||
// двойной вызов g.Count() итерирует группу дважды.
|
||||
return string.Join(", ", types
|
||||
.GroupBy(t => t)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.ThenBy(g => g.Key, StringComparer.Ordinal)
|
||||
.Select(g => $"{g.Count()} {g.Key}"));
|
||||
.Select(g => (Key: g.Key, Count: g.Count()))
|
||||
.OrderByDescending(x => x.Count)
|
||||
.ThenBy(x => x.Key, StringComparer.Ordinal)
|
||||
.Select(x => $"{x.Count} {x.Key}"));
|
||||
}
|
||||
|
||||
/// <summary>Отрендерить «паспорт» модели в читаемый текст.</summary>
|
||||
public static string Render(ModelDescription d)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Документ: «{d.Name}» (деталь)");
|
||||
sb.AppendLine($"Документ: «{d.Name}» ({d.DocumentTypeName})");
|
||||
|
||||
if (d.IsEmpty)
|
||||
{
|
||||
|
||||
@@ -38,4 +38,10 @@ public sealed record ModelDescription
|
||||
|
||||
/// <summary>Переменные модели.</summary>
|
||||
public required IReadOnlyList<VariableInfo> Variables { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Локализованное название типа документа для вывода: «деталь», «сборка», «чертёж» и т.д.
|
||||
/// По умолчанию «деталь».
|
||||
/// </summary>
|
||||
public string DocumentTypeName { get; init; } = "деталь";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
@@ -34,7 +36,7 @@ public sealed class ModelInspectionService
|
||||
var part = GetTopPart();
|
||||
try
|
||||
{
|
||||
var name = ActiveDocName();
|
||||
var (name, typeName) = ActiveDocInfo();
|
||||
|
||||
bool hasGab = part.GetGabarit(false, false,
|
||||
out double x1, out double y1, out double z1, out double x2, out double y2, out double z2);
|
||||
@@ -48,6 +50,7 @@ public sealed class ModelInspectionService
|
||||
Bodies = Array.Empty<BodyInfo>(), FaceTypes = Array.Empty<string>(),
|
||||
EdgeTypes = Array.Empty<string>(), Features = Array.Empty<FeatureInfo>(),
|
||||
Imported = false, Variables = Array.Empty<VariableInfo>(),
|
||||
DocumentTypeName = typeName,
|
||||
};
|
||||
|
||||
var bodies = ReadBodies(part);
|
||||
@@ -67,9 +70,10 @@ public sealed class ModelInspectionService
|
||||
Features = features,
|
||||
Imported = InspectionText.IsImported(features, bodies.Count),
|
||||
Variables = variables,
|
||||
DocumentTypeName = typeName,
|
||||
};
|
||||
}
|
||||
finally { Release(part); }
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Перечислить узлы дерева построения по порядку.</summary>
|
||||
@@ -78,7 +82,7 @@ public sealed class ModelInspectionService
|
||||
{
|
||||
var part = GetTopPart();
|
||||
try { return ReadFeatures(part); }
|
||||
finally { Release(part); }
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Перечислить тела детали.</summary>
|
||||
@@ -87,7 +91,7 @@ public sealed class ModelInspectionService
|
||||
{
|
||||
var part = GetTopPart();
|
||||
try { return ReadBodies(part); }
|
||||
finally { Release(part); }
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Перечислить переменные модели.</summary>
|
||||
@@ -96,7 +100,7 @@ public sealed class ModelInspectionService
|
||||
{
|
||||
var part = GetTopPart();
|
||||
try { return ReadVariables(part); }
|
||||
finally { Release(part); }
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Детали грани по индексу из list_faces: тип, площадь, нормаль, радиус, число рёбер.</summary>
|
||||
@@ -117,14 +121,14 @@ public sealed class ModelInspectionService
|
||||
return new FaceDetail
|
||||
{
|
||||
Index = index,
|
||||
Type = FaceType(def),
|
||||
Type = GeomClassifiers.FaceType(def),
|
||||
Area = area,
|
||||
Normal = FaceNormal(def),
|
||||
Radius = radius,
|
||||
EdgeCount = edgeCount,
|
||||
};
|
||||
}
|
||||
finally { Release(part); }
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Детали ребра по индексу из list_edges: тип, длина, смежные грани, концевые вершины.</summary>
|
||||
@@ -139,13 +143,13 @@ public sealed class ModelInspectionService
|
||||
|
||||
double len = 0; try { len = def.GetLength(MixMm); } catch { }
|
||||
string? f1 = null, f2 = null;
|
||||
try { if (def.GetAdjacentFace(true) is ksFaceDefinition a) f1 = FaceType(a); } catch { }
|
||||
try { if (def.GetAdjacentFace(false) is ksFaceDefinition b) f2 = FaceType(b); } catch { }
|
||||
try { if (def.GetAdjacentFace(true) is ksFaceDefinition a) f1 = GeomClassifiers.FaceType(a); } catch { }
|
||||
try { if (def.GetAdjacentFace(false) is ksFaceDefinition b) f2 = GeomClassifiers.FaceType(b); } catch { }
|
||||
|
||||
return new EdgeDetail
|
||||
{
|
||||
Index = index,
|
||||
Type = EdgeType(def),
|
||||
Type = GeomClassifiers.EdgeType(def),
|
||||
Length = len,
|
||||
AdjacentFace1 = f1,
|
||||
AdjacentFace2 = f2,
|
||||
@@ -153,7 +157,7 @@ public sealed class ModelInspectionService
|
||||
End = Vertex(def, false),
|
||||
};
|
||||
}
|
||||
finally { Release(part); }
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
@@ -186,7 +190,7 @@ public sealed class ModelInspectionService
|
||||
AngleValid = angleValid,
|
||||
};
|
||||
}
|
||||
finally { Release(part); }
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
// ── чтение данных (на STA-потоке) ─────────────────────────────────────
|
||||
@@ -264,11 +268,11 @@ public sealed class ModelInspectionService
|
||||
if (vc.GetByIndex(i) is not ksVariable v) continue;
|
||||
list.Add(new VariableInfo
|
||||
{
|
||||
Name = SafeStr(() => v.name),
|
||||
Expression = SafeStr(() => v.Expression),
|
||||
Value = SafeNum(() => v.value),
|
||||
External = SafeBool(() => v.external),
|
||||
Information = SafeBool(() => v.Information),
|
||||
Name = ComHelper.SafeStr(() => v.name),
|
||||
Expression = ComHelper.SafeStr(() => v.Expression),
|
||||
Value = ComHelper.SafeNum(() => v.value),
|
||||
External = ComHelper.SafeBool(() => v.external),
|
||||
Information = ComHelper.SafeBool(() => v.Information),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
@@ -280,10 +284,12 @@ public sealed class ModelInspectionService
|
||||
var edgeTypes = new List<string>();
|
||||
if (Faces(part) is { } faces)
|
||||
for (int i = 0; i < faces.GetCount(); i++)
|
||||
if (faces.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksFaceDefinition d) faceTypes.Add(FaceType(d));
|
||||
if (faces.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksFaceDefinition d)
|
||||
faceTypes.Add(GeomClassifiers.FaceType(d));
|
||||
if (Edges(part) is { } edges)
|
||||
for (int i = 0; i < edges.GetCount(); i++)
|
||||
if (edges.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksEdgeDefinition d) edgeTypes.Add(EdgeType(d));
|
||||
if (edges.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksEdgeDefinition d)
|
||||
edgeTypes.Add(GeomClassifiers.EdgeType(d));
|
||||
return (faceTypes, edgeTypes);
|
||||
}
|
||||
|
||||
@@ -349,16 +355,43 @@ public sealed class ModelInspectionService
|
||||
{
|
||||
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
return doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
// Освобождаем RCW документа сразу после получения ksPart — part имеет собственный ref.
|
||||
try
|
||||
{
|
||||
return doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
}
|
||||
finally { ComHelper.Release(doc3d); }
|
||||
}
|
||||
|
||||
private string ActiveDocName()
|
||||
/// <summary>Имя и локализованный тип активного документа. Отдельный API7-вызов от GetTopPart.</summary>
|
||||
private (string name, string typeName) ActiveDocInfo()
|
||||
{
|
||||
var doc = _session.Application.ActiveDocument;
|
||||
try { return doc?.Name ?? ""; }
|
||||
catch { return ""; }
|
||||
finally { Release(doc); }
|
||||
try
|
||||
{
|
||||
string name = ComHelper.SafeStr(() => doc?.Name);
|
||||
string typeName = "деталь";
|
||||
if (doc is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
typeName = DocumentTypes.FromEnum(doc.DocumentType) switch
|
||||
{
|
||||
KompasDocumentType.Assembly => "сборка",
|
||||
KompasDocumentType.Drawing => "чертёж",
|
||||
KompasDocumentType.Fragment => "фрагмент",
|
||||
KompasDocumentType.Specification => "спецификация",
|
||||
KompasDocumentType.Text => "текст",
|
||||
_ => "деталь",
|
||||
};
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return (name, typeName);
|
||||
}
|
||||
catch { return ("", "деталь"); }
|
||||
finally { ComHelper.Release(doc); }
|
||||
}
|
||||
|
||||
private static ksEntityCollection? Faces(ksPart part) => part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection;
|
||||
@@ -382,44 +415,11 @@ public sealed class ModelInspectionService
|
||||
?? throw new InvalidOperationException($"Ребро[{index}] не приводится к ksEdgeDefinition.");
|
||||
}
|
||||
|
||||
private static string FaceType(ksFaceDefinition d)
|
||||
{
|
||||
try { if (d.IsPlanar()) return "plane"; } catch { }
|
||||
try { if (d.IsCylinder()) return "cylinder"; } catch { }
|
||||
try { if (d.IsCone()) return "cone"; } catch { }
|
||||
try { if (d.IsSphere()) return "sphere"; } catch { }
|
||||
try { if (d.IsTorus()) return "torus"; } catch { }
|
||||
try { if (d.IsNurbsSurface()) return "nurbs"; } catch { }
|
||||
return "other";
|
||||
}
|
||||
|
||||
private static string EdgeType(ksEdgeDefinition d)
|
||||
{
|
||||
try { if (d.IsLineSeg()) return "line"; } catch { }
|
||||
try { if (d.IsCircle()) return "circle"; } catch { }
|
||||
try { if (d.IsArc()) return "arc"; } catch { }
|
||||
try { if (d.IsEllipseArc()) return "ellipse_arc"; } catch { }
|
||||
try { if (d.IsEllipse()) return "ellipse"; } catch { }
|
||||
try { if (d.IsNurbs()) return "nurbs"; } catch { }
|
||||
return "other";
|
||||
}
|
||||
|
||||
private static BoundingBox Box(double x1, double y1, double z1, double x2, double y2, double z2) => new()
|
||||
{
|
||||
MinX = Math.Min(x1, x2), MinY = Math.Min(y1, y2), MinZ = Math.Min(z1, z2),
|
||||
MaxX = Math.Max(x1, x2), MaxY = Math.Max(y1, y2), MaxZ = Math.Max(z1, z2),
|
||||
};
|
||||
|
||||
private static string SafeStr(Func<string?> g) { try { return g() ?? ""; } catch { return ""; } }
|
||||
private static double SafeNum(Func<double> g) { try { return g(); } catch { return 0; } }
|
||||
private static bool SafeBool(Func<bool> g) { try { return g(); } catch { return false; } }
|
||||
private static string Inv(FormattableString fs) => FormattableString.Invariant(fs);
|
||||
|
||||
private static void Release(object? com)
|
||||
{
|
||||
if (com is not null && Marshal.IsComObject(com))
|
||||
{
|
||||
try { Marshal.ReleaseComObject(com); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
@@ -14,6 +15,9 @@ public sealed class QueryService
|
||||
// Иначе КОМПАС вернёт значения в единицах по умолчанию (см/г).
|
||||
private const int MixMmKg = 0x1 | 0x10;
|
||||
|
||||
// ST_MIX_MM (0x1) — площади граней / длины рёбер в мм.
|
||||
private const uint MixMm = 0x1;
|
||||
|
||||
private readonly KompasSession _session;
|
||||
private readonly KompasDispatcher _dispatcher;
|
||||
|
||||
@@ -33,11 +37,7 @@ public sealed class QueryService
|
||||
public Task<PartInfo> GetPartInfoAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
|
||||
var part = GetTopPart();
|
||||
ksMassInertiaParam? mass = null;
|
||||
try
|
||||
{
|
||||
@@ -56,8 +56,8 @@ public sealed class QueryService
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (mass is not null && Marshal.IsComObject(mass)) Marshal.ReleaseComObject(mass);
|
||||
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
|
||||
ComHelper.Release(mass);
|
||||
ComHelper.Release(part);
|
||||
}
|
||||
}, ct);
|
||||
|
||||
@@ -83,13 +83,22 @@ public sealed class QueryService
|
||||
bool gab = false;
|
||||
try { gab = p.GetGabarit(false, false, out x1, out y1, out z1, out x2, out y2, out z2); }
|
||||
catch { /* габарит может быть недоступен у отдельных компонентов */ }
|
||||
|
||||
// Inline safe-геттеры: избегаем лямбда-аллокаций в горячем цикле по компонентам.
|
||||
string name = "", marking = "", fileName = "";
|
||||
bool isDetail = false;
|
||||
try { name = p.Name ?? ""; } catch { }
|
||||
try { marking = p.Marking ?? ""; } catch { }
|
||||
try { fileName = p.FileName ?? ""; } catch { }
|
||||
try { isDetail = p.Detail; } catch { }
|
||||
|
||||
list.Add(new ComponentInfo
|
||||
{
|
||||
Index = i,
|
||||
Name = SafeGet(() => p.Name),
|
||||
Marking = SafeGet(() => p.Marking),
|
||||
FileName = SafeGet(() => p.FileName),
|
||||
IsDetail = SafeBool(() => p.Detail),
|
||||
Name = name,
|
||||
Marking = marking,
|
||||
FileName = fileName,
|
||||
IsDetail = isDetail,
|
||||
SizeX = gab ? Math.Abs(x2 - x1) : 0,
|
||||
SizeY = gab ? Math.Abs(y2 - y1) : 0,
|
||||
SizeZ = gab ? Math.Abs(z2 - z1) : 0,
|
||||
@@ -98,27 +107,11 @@ public sealed class QueryService
|
||||
return list;
|
||||
}, ct);
|
||||
|
||||
private static string SafeGet(Func<string?> get)
|
||||
{
|
||||
try { return get() ?? string.Empty; } catch { return string.Empty; }
|
||||
}
|
||||
|
||||
private static bool SafeBool(Func<bool> get)
|
||||
{
|
||||
try { return get(); } catch { return false; }
|
||||
}
|
||||
|
||||
// ST_MIX_MM (0x1) — площади граней в мм².
|
||||
private const uint MixMm = 0x1;
|
||||
|
||||
/// <summary>Перечислить грани активной детали: индекс, тип поверхности и площадь (мм²).</summary>
|
||||
public Task<IReadOnlyList<FaceInfo>> ListFacesAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync<IReadOnlyList<FaceInfo>>(() =>
|
||||
{
|
||||
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
var part = GetTopPart();
|
||||
try
|
||||
{
|
||||
var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
|
||||
@@ -133,37 +126,20 @@ public sealed class QueryService
|
||||
list.Add(new FaceInfo
|
||||
{
|
||||
Index = i,
|
||||
Type = FaceType(def),
|
||||
Type = GeomClassifiers.FaceType(def),
|
||||
Area = def.GetArea(MixMm),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
|
||||
}
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
private static string FaceType(ksFaceDefinition def)
|
||||
{
|
||||
if (def.IsPlanar()) return "plane";
|
||||
if (def.IsCylinder()) return "cylinder";
|
||||
if (def.IsCone()) return "cone";
|
||||
if (def.IsSphere()) return "sphere";
|
||||
if (def.IsTorus()) return "torus";
|
||||
if (def.IsNurbsSurface()) return "nurbs";
|
||||
return "other";
|
||||
}
|
||||
|
||||
/// <summary>Перечислить рёбра активной детали: индекс, тип кривой и длину (мм).</summary>
|
||||
public Task<IReadOnlyList<EdgeInfo>> ListEdgesAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync<IReadOnlyList<EdgeInfo>>(() =>
|
||||
{
|
||||
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
var part = GetTopPart();
|
||||
try
|
||||
{
|
||||
var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection
|
||||
@@ -178,37 +154,20 @@ public sealed class QueryService
|
||||
list.Add(new EdgeInfo
|
||||
{
|
||||
Index = i,
|
||||
Type = EdgeType(def),
|
||||
Type = GeomClassifiers.EdgeType(def),
|
||||
Length = def.GetLength(MixMm),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
|
||||
}
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
private static string EdgeType(ksEdgeDefinition def)
|
||||
{
|
||||
if (def.IsLineSeg()) return "line";
|
||||
if (def.IsCircle()) return "circle";
|
||||
if (def.IsArc()) return "arc";
|
||||
if (def.IsEllipseArc()) return "ellipse_arc";
|
||||
if (def.IsEllipse()) return "ellipse";
|
||||
if (def.IsNurbs()) return "nurbs";
|
||||
return "other";
|
||||
}
|
||||
|
||||
/// <summary>Габаритный параллелепипед активной детали (по телам), мм.</summary>
|
||||
public Task<BoundingBox> GetBoundingBoxAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
var part = GetTopPart();
|
||||
try
|
||||
{
|
||||
// full=false → только тела (без вспомогательной геометрии); customizable игнорируется.
|
||||
@@ -223,9 +182,24 @@ public sealed class QueryService
|
||||
MaxX = Math.Max(x1, x2), MaxY = Math.Max(y1, y2), MaxZ = Math.Max(z1, z2),
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
|
||||
}
|
||||
finally { ComHelper.Release(part); }
|
||||
}, ct);
|
||||
|
||||
// ── низкоуровневые помощники ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Получить вершинный компонент активной детали. Освобождает RCW doc3d сразу после
|
||||
/// GetPart — ksPart имеет собственный COM-счётчик ссылок.
|
||||
/// </summary>
|
||||
private ksPart GetTopPart()
|
||||
{
|
||||
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
try
|
||||
{
|
||||
return doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
}
|
||||
finally { ComHelper.Release(doc3d); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,18 @@ public static class RasterImageFormats
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
|
||||
/// <summary>Расширение файла без точки — постоянная строка без аллокации (в отличие от ToString().ToLower()).</summary>
|
||||
public static string FileExtension(this RasterImageFormat format) => format switch
|
||||
{
|
||||
RasterImageFormat.Bmp => "bmp",
|
||||
RasterImageFormat.Gif => "gif",
|
||||
RasterImageFormat.Jpg => "jpg",
|
||||
RasterImageFormat.Png => "png",
|
||||
RasterImageFormat.Tif => "tif",
|
||||
RasterImageFormat.Tga => "tga",
|
||||
_ => format.ToString().ToLowerInvariant(),
|
||||
};
|
||||
|
||||
public static RasterImageFormat Parse(string value) => value.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"png" => RasterImageFormat.Png,
|
||||
|
||||
@@ -64,7 +64,7 @@ public sealed class SnapshotService
|
||||
param.returnResultAsArrayBytes = false;
|
||||
|
||||
var outFile = saveToFile ?? Path.Combine(
|
||||
Path.GetTempPath(), $"kompas-snap-{Guid.NewGuid():N}.{format.ToString().ToLowerInvariant()}");
|
||||
Path.GetTempPath(), $"kompas-snap-{Guid.NewGuid():N}.{format.FileExtension()}");
|
||||
var deleteAfter = saveToFile is null;
|
||||
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user