refactor(core): PartModeler в partial class (ядро/эскизы/операции)
Разбит на PartModeler.cs (ядро: реестр, helpers, NewParam, reset/dispose), PartModeler.Sketch.cs (эскизы и 2D-примитивы), PartModeler.Features.cs (выдавливание/вращение/скругление/фаска). Поведение не изменено, регрессия ModelingTests 7/7 зелёная. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
|
||||
/// <summary>PartModeler: формообразующие операции (выдавливание, вращение, скругление, фаска).</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed partial class PartModeler
|
||||
{
|
||||
/// <summary>
|
||||
/// Выдавить эскиз. <paramref name="cut"/> — вырезание вместо добавления материала.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
/// <param name="throughAll">Сквозное выдавливание/вырез через всю деталь в обе стороны
|
||||
/// (для отверстий — гарантированно пробивает материал; <paramref name="depth"/> игнорируется).</param>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Вращать эскиз вокруг его осевой линии (добавьте её через AddAxis). <paramref name="cut"/> —
|
||||
/// вырезание вместо добавления материала. Угол <paramref name="angle"/> — в градусах.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Скруглить ребро детали, найденное по точке (мировые координаты, мм), радиусом <paramref name="radius"/>.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>Скруглить ребро детали по индексу из list_edges радиусом <paramref name="radius"/>.</summary>
|
||||
public Task<int> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Снять фаску с ребра детали, найденного по точке (мировые координаты, мм), катетом <paramref name="distance"/>.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>Снять фаску с ребра детали по индексу из list_edges катетом <paramref name="distance"/>.</summary>
|
||||
public Task<int> 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;
|
||||
}
|
||||
|
||||
/// <summary>Перестроить документ.</summary>
|
||||
public Task RebuildAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc3d = ActiveDoc3D();
|
||||
doc3d.RebuildDocument();
|
||||
}, ct);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
|
||||
/// <summary>PartModeler: создание эскизов и добавление 2D-примитивов.</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed partial class PartModeler
|
||||
{
|
||||
/// <summary>Создать эскиз на базовой плоскости и открыть его на редактирование. Возвращает id эскиза.</summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Создать эскиз на плоской грани детали, найденной по точке <paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>
|
||||
/// (мировые координаты модели, мм), и открыть его на редактирование. Возвращает id эскиза.
|
||||
/// Это ключ к итеративному циклу «построил → выбрал грань → новый эскиз на ней».
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Создать эскиз на грани детали, заданной индексом из list_faces, и открыть на редактирование.
|
||||
/// Индекс стабилен, пока геометрия не меняется. Возвращает id эскиза.
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Добавить отрезок в открытый эскиз.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Добавить окружность в открытый эскиз.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Добавить прямоугольник (4 отрезка) по двум углам.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Добавить осевую линию (стиль «осевая») в эскиз — ось для операции вращения.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Завершить редактирование эскиза.</summary>
|
||||
public Task CloseSketchAsync(int sketchId, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => CloseSketchCore(RequireSketch(sketchId)), ct);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
@@ -10,9 +11,11 @@ namespace Kompas.Mcp.Core.Modeling;
|
||||
/// Построение 3D-детали через API5 (<c>ksPart</c>): эскизы и формообразующие операции.
|
||||
/// Эскизы и операции регистрируются под целочисленными id, чтобы инструменты MCP могли
|
||||
/// ссылаться на них в цикле «эскиз → операция → эскиз».
|
||||
/// <para>Класс разбит на partial-файлы: ядро (этот файл), <c>PartModeler.Sketch.cs</c>
|
||||
/// (эскизы и 2D-примитивы), <c>PartModeler.Features.cs</c> (формообразующие операции).</para>
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class PartModeler : IDisposable
|
||||
public sealed partial class PartModeler : IDisposable
|
||||
{
|
||||
private const int MainLineStyle = 1; // системный стиль «Основная»
|
||||
private const int AxisLineStyle = 3; // системный стиль «Осевая» (ось вращения)
|
||||
@@ -37,324 +40,6 @@ public sealed class PartModeler : IDisposable
|
||||
public ksDocument2D? Editor { get; set; } // не null, пока эскиз открыт на редактирование
|
||||
}
|
||||
|
||||
// ---- Эскизы ------------------------------------------------------------
|
||||
|
||||
/// <summary>Создать эскиз на базовой плоскости и открыть его на редактирование. Возвращает id эскиза.</summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Создать эскиз на плоской грани детали, найденной по точке <paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>
|
||||
/// (мировые координаты модели, мм), и открыть его на редактирование. Возвращает id эскиза.
|
||||
/// Это ключ к итеративному циклу «построил → выбрал грань → новый эскиз на ней».
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Создать эскиз на грани детали, заданной индексом из list_faces, и открыть на редактирование.
|
||||
/// Индекс стабилен, пока геометрия не меняется. Возвращает id эскиза.
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Добавить отрезок в открытый эскиз.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Добавить окружность в открытый эскиз.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Добавить прямоугольник (4 отрезка) по двум углам.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Добавить осевую линию (стиль «осевая») в эскиз — ось для операции вращения.</summary>
|
||||
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);
|
||||
|
||||
/// <summary>Завершить редактирование эскиза.</summary>
|
||||
public Task CloseSketchAsync(int sketchId, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => CloseSketchCore(RequireSketch(sketchId)), ct);
|
||||
|
||||
// ---- Операции ----------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Выдавить эскиз. <paramref name="cut"/> — вырезание вместо добавления материала.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
/// <param name="throughAll">Сквозное выдавливание/вырез через всю деталь в обе стороны
|
||||
/// (для отверстий — гарантированно пробивает материал; <paramref name="depth"/> игнорируется).</param>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Вращать эскиз вокруг его осевой линии (добавьте её через AddAxis). <paramref name="cut"/> —
|
||||
/// вырезание вместо добавления материала. Угол <paramref name="angle"/> — в градусах.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Скруглить ребро детали, найденное по точке (мировые координаты, мм), радиусом <paramref name="radius"/>.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>Скруглить ребро детали по индексу из list_edges радиусом <paramref name="radius"/>.</summary>
|
||||
public Task<int> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Снять фаску с ребра детали, найденного по точке (мировые координаты, мм), катетом <paramref name="distance"/>.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> 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);
|
||||
|
||||
/// <summary>Снять фаску с ребра детали по индексу из list_edges катетом <paramref name="distance"/>.</summary>
|
||||
public Task<int> 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;
|
||||
}
|
||||
|
||||
/// <summary>Перестроить документ.</summary>
|
||||
public Task RebuildAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc3d = ActiveDoc3D();
|
||||
doc3d.RebuildDocument();
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Сбросить реестр эскизов/операций и освободить их COM-объекты. Вызывать при смене
|
||||
/// активного документа (создание/открытие/закрытие) — id больше не действительны.
|
||||
@@ -364,6 +49,16 @@ public sealed class PartModeler : IDisposable
|
||||
|
||||
// ---- Внутреннее --------------------------------------------------------
|
||||
|
||||
/// <summary>Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.
|
||||
/// При неудачном приведении освобождает сырой RCW, чтобы не утекал COM-объект.</summary>
|
||||
private T NewParam<T>(StructType2DEnum kind) where T : class
|
||||
{
|
||||
var raw = _session.Kompas.GetParamStruct((short)kind);
|
||||
if (raw is T typed) return typed;
|
||||
ReleaseCom(raw);
|
||||
throw new InvalidOperationException($"GetParamStruct({kind}) вернул не {typeof(T).Name}.");
|
||||
}
|
||||
|
||||
private ksDocument3D ActiveDoc3D()
|
||||
=> _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
|
||||
Reference in New Issue
Block a user