diff --git a/src/Kompas.Mcp.Core/Conversion/ConversionService.cs b/src/Kompas.Mcp.Core/Conversion/ConversionService.cs
index 58052e9..0358f0a 100644
--- a/src/Kompas.Mcp.Core/Conversion/ConversionService.cs
+++ b/src/Kompas.Mcp.Core/Conversion/ConversionService.cs
@@ -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 { /* освобождение не критично */ }
- }
- }
}
diff --git a/src/Kompas.Mcp.Core/Documents/DocumentService.cs b/src/Kompas.Mcp.Core/Documents/DocumentService.cs
index 9eba22e..c3f1363 100644
--- a/src/Kompas.Mcp.Core/Documents/DocumentService.cs
+++ b/src/Kompas.Mcp.Core/Documents/DocumentService.cs
@@ -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);
/// Открыть документ с диска.
@@ -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);
/// Сводка по активному документу или null, если документов нет.
@@ -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);
/// Сохранить активный документ (для уже сохранённого — без диалога).
@@ -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 { /* освобождение не критично */ }
- }
- }
}
diff --git a/src/Kompas.Mcp.Core/Editing/FaceEditService.cs b/src/Kompas.Mcp.Core/Editing/FaceEditService.cs
index 0fcd200..243a7ad 100644
--- a/src/Kompas.Mcp.Core/Editing/FaceEditService.cs
+++ b/src/Kompas.Mcp.Core/Editing/FaceEditService.cs
@@ -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)
diff --git a/src/Kompas.Mcp.Core/Interop/ComHelper.cs b/src/Kompas.Mcp.Core/Interop/ComHelper.cs
new file mode 100644
index 0000000..deb569c
--- /dev/null
+++ b/src/Kompas.Mcp.Core/Interop/ComHelper.cs
@@ -0,0 +1,30 @@
+using System.Runtime.InteropServices;
+
+namespace Kompas.Mcp.Core.Interop;
+
+///
+/// Вспомогательные методы для работы с COM RCW: безопасное освобождение
+/// и safe-геттеры для свойств, которые могут бросить исключение.
+///
+/// Метод идентично реализован в каждом сервисе — вынесен сюда.
+/// Safe-геттеры заменяют разрозненные SafeStr/SafeGet/SafeBool/SafeNum.
+///
+///
+internal static class ComHelper
+{
+ /// Освободить COM RCW: null-гард + IsComObject-проверка + try/catch.
+ internal static void Release(object? com)
+ {
+ if (com is not null && Marshal.IsComObject(com))
+ try { Marshal.ReleaseComObject(com); } catch { }
+ }
+
+ /// Безопасно получить строку: возвращает "" при любом исключении.
+ internal static string SafeStr(Func f) { try { return f() ?? ""; } catch { return ""; } }
+
+ /// Безопасно получить число: возвращает 0 при любом исключении.
+ internal static double SafeNum(Func f) { try { return f(); } catch { return 0; } }
+
+ /// Безопасно получить флаг: возвращает false при любом исключении.
+ internal static bool SafeBool(Func f) { try { return f(); } catch { return false; } }
+}
diff --git a/src/Kompas.Mcp.Core/KompasSession.cs b/src/Kompas.Mcp.Core/KompasSession.cs
index b219a04..08962f9 100644
--- a/src/Kompas.Mcp.Core/KompasSession.cs
+++ b/src/Kompas.Mcp.Core/KompasSession.cs
@@ -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)
{
diff --git a/src/Kompas.Mcp.Core/Modeling/PartModeler.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
index bf769f6..d5bdb29 100644
--- a/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
+++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
@@ -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()
{
diff --git a/src/Kompas.Mcp.Core/Query/GeomClassifiers.cs b/src/Kompas.Mcp.Core/Query/GeomClassifiers.cs
new file mode 100644
index 0000000..2991fd4
--- /dev/null
+++ b/src/Kompas.Mcp.Core/Query/GeomClassifiers.cs
@@ -0,0 +1,39 @@
+using Kompas6API5;
+
+namespace Kompas.Mcp.Core.Query;
+
+///
+/// Классификаторы топологических объектов КОМПАС: строковый тип грани и ребра.
+///
+/// Каждый вызов COM-метода обёрнут в отдельный try/catch — на экзотической геометрии
+/// (NURBS, импортированные STEP-тела) методы класса ksFaceDefinition/ksEdgeDefinition
+/// могут бросить COMException, что не должно прерывать bulk-перебор. Ранее этот код
+/// был продублирован в QueryService (без защиты) и ModelInspectionService (с защитой).
+///
+///
+internal static class GeomClassifiers
+{
+ /// Строковый тип поверхности: plane/cylinder/cone/sphere/torus/nurbs/other.
+ 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";
+ }
+
+ /// Строковый тип кривой: line/circle/arc/ellipse_arc/ellipse/nurbs/other.
+ 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";
+ }
+}
diff --git a/src/Kompas.Mcp.Core/Query/InspectionText.cs b/src/Kompas.Mcp.Core/Query/InspectionText.cs
index 9cf8fc8..e7b963c 100644
--- a/src/Kompas.Mcp.Core/Query/InspectionText.cs
+++ b/src/Kompas.Mcp.Core/Query/InspectionText.cs
@@ -23,8 +23,9 @@ public static class InspectionText
}
// Формообразующие операции, наличие которых означает параметрическую историю.
- private static readonly string[] OpKinds =
- { "sketch", "extrusion", "cut_extrusion", "revolve", "cut_revolve", "fillet", "chamfer" };
+ // HashSet даёт O(1) Contains вместо O(n) по массиву при bulk-проверке граней.
+ private static readonly HashSet OpKinds =
+ new(StringComparer.Ordinal) { "sketch", "extrusion", "cut_extrusion", "revolve", "cut_revolve", "fillet", "chamfer" };
///
/// Модель — «голый» импорт без истории построения (типично для STEP): в дереве только начало
@@ -39,18 +40,21 @@ public static class InspectionText
public static string GroupTypes(IReadOnlyList types)
{
if (types.Count == 0) return "—";
+ // Count материализуем один раз: IGrouping — 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}"));
}
/// Отрендерить «паспорт» модели в читаемый текст.
public static string Render(ModelDescription d)
{
var sb = new StringBuilder();
- sb.AppendLine($"Документ: «{d.Name}» (деталь)");
+ sb.AppendLine($"Документ: «{d.Name}» ({d.DocumentTypeName})");
if (d.IsEmpty)
{
diff --git a/src/Kompas.Mcp.Core/Query/ModelDescription.cs b/src/Kompas.Mcp.Core/Query/ModelDescription.cs
index 16603a7..6b7c92c 100644
--- a/src/Kompas.Mcp.Core/Query/ModelDescription.cs
+++ b/src/Kompas.Mcp.Core/Query/ModelDescription.cs
@@ -38,4 +38,10 @@ public sealed record ModelDescription
/// Переменные модели.
public required IReadOnlyList Variables { get; init; }
+
+ ///
+ /// Локализованное название типа документа для вывода: «деталь», «сборка», «чертёж» и т.д.
+ /// По умолчанию «деталь».
+ ///
+ public string DocumentTypeName { get; init; } = "деталь";
}
diff --git a/src/Kompas.Mcp.Core/Query/ModelInspectionService.cs b/src/Kompas.Mcp.Core/Query/ModelInspectionService.cs
index ddcc484..0e565fb 100644
--- a/src/Kompas.Mcp.Core/Query/ModelInspectionService.cs
+++ b/src/Kompas.Mcp.Core/Query/ModelInspectionService.cs
@@ -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(), FaceTypes = Array.Empty(),
EdgeTypes = Array.Empty(), Features = Array.Empty(),
Imported = false, Variables = Array.Empty(),
+ 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);
/// Перечислить узлы дерева построения по порядку.
@@ -78,7 +82,7 @@ public sealed class ModelInspectionService
{
var part = GetTopPart();
try { return ReadFeatures(part); }
- finally { Release(part); }
+ finally { ComHelper.Release(part); }
}, ct);
/// Перечислить тела детали.
@@ -87,7 +91,7 @@ public sealed class ModelInspectionService
{
var part = GetTopPart();
try { return ReadBodies(part); }
- finally { Release(part); }
+ finally { ComHelper.Release(part); }
}, ct);
/// Перечислить переменные модели.
@@ -96,7 +100,7 @@ public sealed class ModelInspectionService
{
var part = GetTopPart();
try { return ReadVariables(part); }
- finally { Release(part); }
+ finally { ComHelper.Release(part); }
}, ct);
/// Детали грани по индексу из list_faces: тип, площадь, нормаль, радиус, число рёбер.
@@ -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);
/// Детали ребра по индексу из list_edges: тип, длина, смежные грани, концевые вершины.
@@ -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);
///
@@ -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();
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()
+ /// Имя и локализованный тип активного документа. Отдельный API7-вызов от GetTopPart.
+ 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 g) { try { return g() ?? ""; } catch { return ""; } }
- private static double SafeNum(Func g) { try { return g(); } catch { return 0; } }
- private static bool SafeBool(Func 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 { }
- }
- }
}
diff --git a/src/Kompas.Mcp.Core/Query/QueryService.cs b/src/Kompas.Mcp.Core/Query/QueryService.cs
index b575770..6032731 100644
--- a/src/Kompas.Mcp.Core/Query/QueryService.cs
+++ b/src/Kompas.Mcp.Core/Query/QueryService.cs
@@ -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 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 get)
- {
- try { return get() ?? string.Empty; } catch { return string.Empty; }
- }
-
- private static bool SafeBool(Func get)
- {
- try { return get(); } catch { return false; }
- }
-
- // ST_MIX_MM (0x1) — площади граней в мм².
- private const uint MixMm = 0x1;
-
/// Перечислить грани активной детали: индекс, тип поверхности и площадь (мм²).
public Task> ListFacesAsync(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
{
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";
- }
-
/// Перечислить рёбра активной детали: индекс, тип кривой и длину (мм).
public Task> ListEdgesAsync(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
{
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";
- }
-
/// Габаритный параллелепипед активной детали (по телам), мм.
public Task 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);
+
+ // ── низкоуровневые помощники ──────────────────────────────────────────
+
+ ///
+ /// Получить вершинный компонент активной детали. Освобождает RCW doc3d сразу после
+ /// GetPart — ksPart имеет собственный COM-счётчик ссылок.
+ ///
+ 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); }
+ }
}
diff --git a/src/Kompas.Mcp.Core/Vision/RasterImageFormat.cs b/src/Kompas.Mcp.Core/Vision/RasterImageFormat.cs
index 27357fc..f1cedcb 100644
--- a/src/Kompas.Mcp.Core/Vision/RasterImageFormat.cs
+++ b/src/Kompas.Mcp.Core/Vision/RasterImageFormat.cs
@@ -25,6 +25,18 @@ public static class RasterImageFormats
_ => "application/octet-stream",
};
+ /// Расширение файла без точки — постоянная строка без аллокации (в отличие от ToString().ToLower()).
+ 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,
diff --git a/src/Kompas.Mcp.Core/Vision/SnapshotService.cs b/src/Kompas.Mcp.Core/Vision/SnapshotService.cs
index d839ca7..00942d0 100644
--- a/src/Kompas.Mcp.Core/Vision/SnapshotService.cs
+++ b/src/Kompas.Mcp.Core/Vision/SnapshotService.cs
@@ -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