using System.Runtime.InteropServices; using System.Runtime.Versioning; using Kompas.Mcp.Core.Threading; using Kompas6API5; using Kompas6Constants3D; namespace Kompas.Mcp.Core.Modeling; /// /// Построение 3D-детали через API5 (ksPart): эскизы и формообразующие операции. /// Эскизы и операции регистрируются под целочисленными id, чтобы инструменты MCP могли /// ссылаться на них в цикле «эскиз → операция → эскиз». /// [SupportedOSPlatform("windows")] public sealed class PartModeler : IDisposable { private const int MainLineStyle = 1; // системный стиль «Основная» private const int AxisLineStyle = 3; // системный стиль «Осевая» (ось вращения) private readonly KompasSession _session; private readonly KompasDispatcher _dispatcher; private int _nextId = 1; private readonly Dictionary _sketches = new(); private readonly Dictionary _features = new(); public PartModeler(KompasSession session, KompasDispatcher dispatcher) { _session = session ?? throw new ArgumentNullException(nameof(session)); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); } private sealed class SketchEntry { public required ksEntity Entity { get; init; } public required ksSketchDefinition Definition { get; init; } public ksDocument2D? Editor { get; set; } // не null, пока эскиз открыт на редактирование } // ---- Эскизы ------------------------------------------------------------ /// Создать эскиз на базовой плоскости и открыть его на редактирование. Возвращает id эскиза. public Task OpenSketchAsync(BasePlane plane, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var part = GetTopPart(); var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity ?? throw new InvalidOperationException("Не удалось получить базовую плоскость."); return CreateSketchOn(part, planeEntity); }, ct); /// /// Создать эскиз на плоской грани детали, найденной по точке ,, /// (мировые координаты модели, мм), и открыть его на редактирование. Возвращает id эскиза. /// Это ключ к итеративному циклу «построил → выбрал грань → новый эскиз на ней». /// public Task OpenSketchOnFaceAsync(double x, double y, double z, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var part = GetTopPart(); var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection ?? throw new InvalidOperationException("Не удалось получить коллекцию граней."); // Оставляем в коллекции только грани, проходящие через указанную точку. faces.SelectByPoint(x, y, z); if (faces.GetCount() == 0) throw new InvalidOperationException( $"В точке ({x}, {y}, {z}) не найдено грани. Сделайте снимок и сверьте координаты."); var face = faces.GetByIndex(0) as ksEntity ?? throw new InvalidOperationException("Грань из коллекции не приводится к ksEntity."); return CreateSketchOn(part, face); }, ct); /// /// Создать эскиз на грани детали, заданной индексом из list_faces, и открыть на редактирование. /// Индекс стабилен, пока геометрия не меняется. Возвращает id эскиза. /// public Task OpenSketchOnFaceIndexAsync(int faceIndex, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var part = GetTopPart(); var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection ?? throw new InvalidOperationException("Не удалось получить коллекцию граней."); if (faceIndex < 0 || faceIndex >= faces.GetCount()) throw new ArgumentOutOfRangeException(nameof(faceIndex), $"Индекс грани вне диапазона [0; {faces.GetCount() - 1}]. Сверьтесь с list_faces."); var face = faces.GetByIndex(faceIndex) as ksEntity ?? throw new InvalidOperationException("Грань по индексу не приводится к ksEntity."); return CreateSketchOn(part, face); }, ct); /// Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование. private int CreateSketchOn(ksPart part, ksEntity planeEntity) { var entity = part.NewEntity((short)Obj3dType.o3d_sketch) as ksEntity ?? throw new InvalidOperationException("NewEntity(o3d_sketch) вернул null."); var def = entity.GetDefinition() as ksSketchDefinition ?? throw new InvalidOperationException("GetDefinition() эскиза вернул не ksSketchDefinition."); def.SetPlane(planeEntity); entity.Create(); var editor = def.BeginEdit() as ksDocument2D ?? throw new InvalidOperationException("BeginEdit() не вернул ksDocument2D."); var id = _nextId++; _sketches[id] = new SketchEntry { Entity = entity, Definition = def, Editor = editor }; return id; } /// Добавить отрезок в открытый эскиз. public Task AddLineAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var editor = RequireOpenSketch(sketchId).Editor!; if (editor.ksLineSeg(x1, y1, x2, y2, MainLineStyle) == 0) throw new InvalidOperationException("ksLineSeg вернул 0 (отрезок не создан)."); }, ct); /// Добавить окружность в открытый эскиз. public Task AddCircleAsync(int sketchId, double centerX, double centerY, double radius, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус должен быть > 0."); var editor = RequireOpenSketch(sketchId).Editor!; if (editor.ksCircle(centerX, centerY, radius, MainLineStyle) == 0) throw new InvalidOperationException("ksCircle вернул 0 (окружность не создана)."); }, ct); /// Добавить прямоугольник (4 отрезка) по двум углам. public Task AddRectangleAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var editor = RequireOpenSketch(sketchId).Editor!; void Seg(double ax, double ay, double bx, double by) { if (editor.ksLineSeg(ax, ay, bx, by, MainLineStyle) == 0) throw new InvalidOperationException("ksLineSeg вернул 0 (сторона прямоугольника не создана)."); } Seg(x1, y1, x2, y1); Seg(x2, y1, x2, y2); Seg(x2, y2, x1, y2); Seg(x1, y2, x1, y1); }, ct); /// Добавить осевую линию (стиль «осевая») в эскиз — ось для операции вращения. public Task AddAxisAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var editor = RequireOpenSketch(sketchId).Editor!; if (editor.ksLineSeg(x1, y1, x2, y2, AxisLineStyle) == 0) throw new InvalidOperationException("ksLineSeg (ось) вернул 0 (ось не создана)."); }, ct); /// Завершить редактирование эскиза. public Task CloseSketchAsync(int sketchId, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => CloseSketchCore(RequireSketch(sketchId)), ct); // ---- Операции ---------------------------------------------------------- /// /// Выдавить эскиз. — вырезание вместо добавления материала. /// Возвращает id операции. /// /// Сквозное выдавливание/вырез через всю деталь в обе стороны /// (для отверстий — гарантированно пробивает материал; игнорируется). public Task ExtrudeAsync( int sketchId, double depth, bool cut = false, bool forward = true, double draftAngle = 0, bool throughAll = false, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { if (!throughAll && depth <= 0) throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть > 0 (или используйте throughAll)."); var sketch = RequireSketch(sketchId); CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией var part = GetTopPart(); var type = cut ? Obj3dType.o3d_cutExtrusion : Obj3dType.o3d_bossExtrusion; var entity = part.NewEntity((short)type) as ksEntity ?? throw new InvalidOperationException("NewEntity операции выдавливания вернул null."); // Конкретные типы определений (без dynamic): ошибки связывания — на этапе компиляции. if (cut) { var def = entity.GetDefinition() as ksCutExtrusionDefinition ?? throw new InvalidOperationException("GetDefinition() выреза вернул не ksCutExtrusionDefinition."); if (throughAll) { def.directionType = (short)Direction_Type.dtBoth; def.SetSideParam(true, (short)End_Type.etThroughAll, depth, draftAngle, false); def.SetSideParam(false, (short)End_Type.etThroughAll, depth, draftAngle, false); } else { def.directionType = (short)Direction_Type.dtNormal; def.SetSideParam(forward, (short)End_Type.etBlind, depth, draftAngle, false); } def.SetSketch(sketch.Entity); } else { if (throughAll) throw new ArgumentException("throughAll поддерживается только для выреза (cut).", nameof(throughAll)); var def = entity.GetDefinition() as ksBossExtrusionDefinition ?? throw new InvalidOperationException("GetDefinition() выдавливания вернул не ksBossExtrusionDefinition."); def.directionType = (short)Direction_Type.dtNormal; def.SetSideParam(forward, (short)End_Type.etBlind, depth, draftAngle, false); def.SetSketch(sketch.Entity); } if (!entity.Create()) throw new InvalidOperationException("Create() операции выдавливания вернул FALSE."); var id = _nextId++; _features[id] = entity; return id; }, ct); /// /// Вращать эскиз вокруг его осевой линии (добавьте её через AddAxis). — /// вырезание вместо добавления материала. Угол — в градусах. /// Возвращает id операции. /// public Task RevolveAsync( int sketchId, double angle = 360, bool cut = false, bool forward = true, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { if (angle is <= 0 or > 360) throw new ArgumentOutOfRangeException(nameof(angle), "Угол вращения должен быть в (0; 360]."); var sketch = RequireSketch(sketchId); CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией var part = GetTopPart(); var type = cut ? Obj3dType.o3d_cutRotated : Obj3dType.o3d_bossRotated; var entity = part.NewEntity((short)type) as ksEntity ?? throw new InvalidOperationException("NewEntity операции вращения вернул null."); if (cut) { var def = entity.GetDefinition() as ksCutRotatedDefinition ?? throw new InvalidOperationException("GetDefinition() выреза вращением вернул не ksCutRotatedDefinition."); def.directionType = (short)Direction_Type.dtNormal; def.SetThinParam(false, 0, 0, 0); def.SetSideParam(forward, angle); def.SetSketch(sketch.Entity); } else { var def = entity.GetDefinition() as ksBossRotatedDefinition ?? throw new InvalidOperationException("GetDefinition() вращения вернул не ksBossRotatedDefinition."); def.directionType = (short)Direction_Type.dtNormal; def.toroidShapeType = false; // сфероид (сплошное тело), не тороид def.SetThinParam(false, 0, 0, 0); def.SetSideParam(forward, angle); def.SetSketch(sketch.Entity); } if (!entity.Create()) throw new InvalidOperationException("Create() операции вращения вернул FALSE."); var id = _nextId++; _features[id] = entity; return id; }, ct); /// /// Скруглить ребро детали, найденное по точке (мировые координаты, мм), радиусом . /// Возвращает id операции. /// public Task FilletEdgeAsync(double x, double y, double z, double radius, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var part = GetTopPart(); return CreateFillet(part, SelectEdgeByPoint(part, x, y, z), radius); }, ct); /// Скруглить ребро детали по индексу из list_edges радиусом . public Task FilletEdgeIndexAsync(int edgeIndex, double radius, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var part = GetTopPart(); return CreateFillet(part, SelectEdgeByIndex(part, edgeIndex), radius); }, ct); private int CreateFillet(ksPart part, ksEntity edge, double radius) { if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус скругления должен быть > 0."); var entity = part.NewEntity((short)Obj3dType.o3d_fillet) as ksEntity ?? throw new InvalidOperationException("NewEntity(o3d_fillet) вернул null."); var def = entity.GetDefinition() as ksFilletDefinition ?? throw new InvalidOperationException("GetDefinition() скругления вернул не ksFilletDefinition."); def.radius = radius; def.tangent = false; var arr = def.array() as ksEntityCollection ?? throw new InvalidOperationException("array() скругления вернул не ksEntityCollection."); arr.Add(edge); if (!entity.Create()) throw new InvalidOperationException("Create() скругления вернул FALSE (радиус слишком велик для ребра?)."); var id = _nextId++; _features[id] = entity; return id; } /// /// Снять фаску с ребра детали, найденного по точке (мировые координаты, мм), катетом . /// Возвращает id операции. /// public Task ChamferEdgeAsync(double x, double y, double z, double distance, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var part = GetTopPart(); return CreateChamfer(part, SelectEdgeByPoint(part, x, y, z), distance); }, ct); /// Снять фаску с ребра детали по индексу из list_edges катетом . public Task ChamferEdgeIndexAsync(int edgeIndex, double distance, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var part = GetTopPart(); return CreateChamfer(part, SelectEdgeByIndex(part, edgeIndex), distance); }, ct); private int CreateChamfer(ksPart part, ksEntity edge, double distance) { if (distance <= 0) throw new ArgumentOutOfRangeException(nameof(distance), "Катет фаски должен быть > 0."); var entity = part.NewEntity((short)Obj3dType.o3d_chamfer) as ksEntity ?? throw new InvalidOperationException("NewEntity(o3d_chamfer) вернул null."); var def = entity.GetDefinition() as ksChamferDefinition ?? throw new InvalidOperationException("GetDefinition() фаски вернул не ksChamferDefinition."); def.tangent = false; def.SetChamferParam(true, distance, distance); var arr = def.array() as ksEntityCollection ?? throw new InvalidOperationException("array() фаски вернул не ksEntityCollection."); arr.Add(edge); if (!entity.Create()) throw new InvalidOperationException("Create() фаски вернул FALSE (катет слишком велик для ребра?)."); var id = _nextId++; _features[id] = entity; return id; } /// Перестроить документ. public Task RebuildAsync(CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { var doc3d = ActiveDoc3D(); doc3d.RebuildDocument(); }, ct); /// /// Сбросить реестр эскизов/операций и освободить их COM-объекты. Вызывать при смене /// активного документа (создание/открытие/закрытие) — id больше не действительны. /// public Task ResetAsync(CancellationToken ct = default) => _dispatcher.InvokeAsync(ResetCore, ct); // ---- Внутреннее -------------------------------------------------------- private ksDocument3D ActiveDoc3D() => _session.Kompas.ActiveDocument3D() as ksDocument3D ?? throw new InvalidOperationException("Нет активного 3D-документа."); private ksPart GetTopPart() => ActiveDoc3D().GetPart((short)Part_Type.pTop_Part) as ksPart ?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart)."); /// Выбрать ребро детали по точке (мировые координаты). Берёт первое подходящее. private static ksEntity SelectEdgeByPoint(ksPart part, double x, double y, double z) { var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection ?? throw new InvalidOperationException("Не удалось получить коллекцию рёбер."); edges.SelectByPoint(x, y, z); if (edges.GetCount() == 0) throw new InvalidOperationException( $"В точке ({x}, {y}, {z}) не найдено ребра. Сделайте снимок и сверьте координаты точки на ребре."); return edges.GetByIndex(0) as ksEntity ?? throw new InvalidOperationException("Ребро из коллекции не приводится к ksEntity."); } /// Выбрать ребро детали по индексу из list_edges. private static ksEntity SelectEdgeByIndex(ksPart part, int edgeIndex) { var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection ?? throw new InvalidOperationException("Не удалось получить коллекцию рёбер."); if (edgeIndex < 0 || edgeIndex >= edges.GetCount()) throw new ArgumentOutOfRangeException(nameof(edgeIndex), $"Индекс ребра вне диапазона [0; {edges.GetCount() - 1}]. Сверьтесь с list_edges."); return edges.GetByIndex(edgeIndex) as ksEntity ?? throw new InvalidOperationException("Ребро по индексу не приводится к ksEntity."); } private SketchEntry RequireSketch(int id) => _sketches.TryGetValue(id, out var s) ? s : throw new KeyNotFoundException($"Эскиз с id={id} не найден."); private SketchEntry RequireOpenSketch(int id) { var s = RequireSketch(id); if (s.Editor is null) throw new InvalidOperationException($"Эскиз id={id} уже закрыт — добавление геометрии невозможно."); return s; } private static void CloseSketchCore(SketchEntry s) { if (s.Editor is null) return; s.Definition.EndEdit(); ReleaseCom(s.Editor); // редактор после EndEdit больше не нужен s.Editor = null; } private void ResetCore() { foreach (var s in _sketches.Values) { ReleaseCom(s.Editor); ReleaseCom(s.Definition); ReleaseCom(s.Entity); } foreach (var f in _features.Values) ReleaseCom(f); _sketches.Clear(); _features.Clear(); _nextId = 1; } private static void ReleaseCom(object? comObject) { if (comObject is not null && Marshal.IsComObject(comObject)) { try { Marshal.ReleaseComObject(comObject); } catch { /* освобождение не критично */ } } } public void Dispose() { // Освобождаем COM на STA-потоке; не виснем на старте, если поток занят. try { _dispatcher.InvokeAsync(ResetCore).Wait(TimeSpan.FromSeconds(5)); } catch { /* при остановке диспетчера COM освободит GC */ } } }