Files
kompas3d-mcp/src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs
T
mikhail dc4003810d feat(sketch): 7 примитивов эскиза — дуга x2, эллипс, ломаная, многоугольник, сплайн, точка
Методы PartModeler + MCP-инструменты sketch_add_arc_3points/arc/ellipse/
polyline/polygon/spline/point. NURBS порядок 4, ksEndObj через try/finally,
param-структуры через NewParam с освобождением RCW. 7 интеграционных
тестов (профиль → выдавливание → объём) зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:50:27 +03:00

263 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.Versioning;
using Kompas6API5;
using Kompas6Constants;
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);
// ---- Новые примитивы (пакет A) -----------------------------------------
private const short SplineOrder = 4; // порядок NURBS (степень+1); 4 = кубический сплайн
private const int PointStyle = 0; // системный стиль точки (см. SDK pstyles)
/// <summary>Добавить дугу по трём точкам (начало, точка на дуге, конец) в открытый эскиз.</summary>
public Task AddArc3PointsAsync(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var editor = RequireOpenSketch(sketchId).Editor!;
if (editor.ksArcBy3Points(x1, y1, x2, y2, x3, y3, MainLineStyle) == 0)
throw new InvalidOperationException("ksArcBy3Points вернул 0 (дуга не создана — три точки коллинеарны?).");
}, ct);
/// <summary>Добавить дугу по центру, радиусу и углам (градусы) в открытый эскиз.</summary>
public Task AddArcByAngleAsync(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
SketchGeometry.RequirePositive(radius, nameof(radius));
var editor = RequireOpenSketch(sketchId).Editor!;
var dir = SketchGeometry.ArcDirection(counterClockwise);
if (editor.ksArcByAngle(centerX, centerY, radius, startAngle, endAngle, dir, MainLineStyle) == 0)
throw new InvalidOperationException("ksArcByAngle вернул 0 (дуга не создана).");
}, ct);
/// <summary>Добавить эллипс: центр, полуоси A/B (мм), угол наклона большой оси к X (градусы).</summary>
public Task AddEllipseAsync(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
SketchGeometry.RequirePositive(semiMajor, nameof(semiMajor));
SketchGeometry.RequirePositive(semiMinor, nameof(semiMinor));
var editor = RequireOpenSketch(sketchId).Editor!;
ksEllipseParam? p = null;
try
{
p = NewParam<ksEllipseParam>(StructType2DEnum.ko_EllipseParam);
// ВНИМАНИЕ: interop генерирует свойства A/B с ЗАГЛАВНОЙ (не a/b как в SDK-доках). Регистр в C# важен.
p.xc = centerX; p.yc = centerY;
p.A = semiMajor; p.B = semiMinor;
p.angle = angle; p.style = MainLineStyle;
if (editor.ksEllipse(p) == 0)
throw new InvalidOperationException("ksEllipse вернул 0 (эллипс не создан).");
}
finally { ReleaseCom(p); }
}, ct);
/// <summary>Добавить ломаную (цепочку отрезков) по списку точек. closed замыкает последнюю с первой.</summary>
public Task AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
// closed-ломаная из 2 точек не образует площадь — требуем минимум 3.
SketchGeometry.RequirePoints(points, closed ? 3 : 2, nameof(points));
var editor = RequireOpenSketch(sketchId).Editor!;
for (int i = 0; i + 1 < points.Count; i++)
Seg(editor, points[i], points[i + 1]);
if (closed)
Seg(editor, points[^1], points[0]);
static void Seg(ksDocument2D ed, (double x, double y) a, (double x, double y) b)
{
if (ed.ksLineSeg(a.x, a.y, b.x, b.y, MainLineStyle) == 0)
throw new InvalidOperationException("ksLineSeg вернул 0 (сегмент ломаной не создан).");
}
}, ct);
/// <summary>Добавить правильный многоугольник: центр, vertexCount вершин, radius (мм),
/// inscribed=true — вершины на окружности (вписанный), false — стороны касаются (описанный),
/// angle — поворот первой вершины (градусы).</summary>
public Task AddPolygonAsync(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
SketchGeometry.RequireVertexCount(vertexCount);
SketchGeometry.RequirePositive(radius, nameof(radius));
var editor = RequireOpenSketch(sketchId).Editor!;
ksRegularPolygonParam? p = null;
try
{
p = NewParam<ksRegularPolygonParam>(StructType2DEnum.ko_RegularPolygonParam);
p.count = vertexCount; p.xc = centerX; p.yc = centerY;
p.radius = radius; p.ang = angle;
p.describe = SketchGeometry.PolygonDescribe(inscribed);
p.style = MainLineStyle;
if (editor.ksRegularPolygon(p, 0) == 0)
throw new InvalidOperationException("ksRegularPolygon вернул 0 (многоугольник не создан).");
}
finally { ReleaseCom(p); }
}, ct);
/// <summary>Добавить сплайн (кубический NURBS, порядок 4) через список точек. closed замыкает кривую.</summary>
public Task AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
SketchGeometry.RequirePoints(points, 2, nameof(points));
var editor = RequireOpenSketch(sketchId).Editor!;
if (editor.ksNurbs(SplineOrder, closed, MainLineStyle) == 0)
throw new InvalidOperationException("ksNurbs вернул 0 (сплайн не открыт).");
// ksNurbs открыл составной объект — ksEndObj ОБЯЗАТЕЛЕН в любом исходе, иначе редактор «застрянет».
var completed = false;
try
{
foreach (var (x, y) in points)
{
ksNurbsPointParam? np = null;
try
{
np = NewParam<ksNurbsPointParam>(StructType2DEnum.ko_NurbsPointParam);
np.x = x; np.y = y; np.weight = 1.0;
if (editor.ksNurbsPoint(np) == 0)
throw new InvalidOperationException("ksNurbsPoint вернул 0 (узел сплайна не добавлен).");
}
finally { ReleaseCom(np); }
}
completed = true;
}
finally
{
// Закрываем составной объект всегда; ошибку завершения сообщаем только на успешном пути,
// чтобы не подменить исходное исключение из цикла.
var end = editor.ksEndObj();
if (completed && end == 0)
throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён).");
}
}, ct);
/// <summary>Добавить точку в открытый эскиз (опорная точка для построений).</summary>
public Task AddPointAsync(int sketchId, double x, double y, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var editor = RequireOpenSketch(sketchId).Editor!;
if (editor.ksPoint(x, y, PointStyle) == 0)
throw new InvalidOperationException("ksPoint вернул 0 (точка не создана).");
}, ct);
}