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