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 */ }
}
}