feat(features): скругление и фаска ребра (fillet_edge / chamfer_edge)

PartModeler.FilletEdgeAsync / ChamferEdgeAsync через API5 o3d_fillet/o3d_chamfer:
ребро выбирается по точке (общий хелпер SelectEdgeByPoint поверх
EntityCollection(o3d_edge).SelectByPoint), добавляется в def.array(),
радиус/катет задаются (ksFilletDefinition.radius, ksChamferDefinition.SetChamferParam).
Новые MCP-инструменты: fillet_edge, chamfer_edge.

Тест Fillet_and_chamfer_edges: цилиндр → скругление верхнего ребра R3 + фаска нижнего 2×2,
проверка успеха, уменьшения объёма и снимка (визуально подтверждено).
Презентация: 25 инструментов, 17 тестов.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@
This commit is contained in:
2026-05-26 10:01:26 +03:00
parent 5a689ad461
commit 88dad09ea7
4 changed files with 130 additions and 6 deletions
@@ -246,6 +246,64 @@ public sealed class PartModeler : IDisposable
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(() =>
{
if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус скругления должен быть > 0.");
var part = GetTopPart();
var edge = SelectEdgeByPoint(part, x, y, z);
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;
}, ct);
/// <summary>
/// Снять фаску с ребра детали, найденного по точке (мировые координаты, мм), катетом <paramref name="distance"/>.
/// Возвращает id операции.
/// </summary>
public Task<int> ChamferEdgeAsync(double x, double y, double z, double distance, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
if (distance <= 0) throw new ArgumentOutOfRangeException(nameof(distance), "Катет фаски должен быть > 0.");
var part = GetTopPart();
var edge = SelectEdgeByPoint(part, x, y, z);
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;
}, ct);
/// <summary>Перестроить документ.</summary>
public Task RebuildAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
@@ -271,6 +329,19 @@ public sealed class PartModeler : IDisposable
=> ActiveDoc3D().GetPart((short)Part_Type.pTop_Part) as ksPart
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
/// <summary>Выбрать ребро детали по точке (мировые координаты). Берёт первое подходящее.</summary>
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.");
}
private SketchEntry RequireSketch(int id)
=> _sketches.TryGetValue(id, out var s) ? s
: throw new KeyNotFoundException($"Эскиз с id={id} не найден.");
+18
View File
@@ -55,6 +55,24 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler)
return $"Вырез вращением (cut) создан, id={id}.";
}
[McpServerTool(Name = "fillet_edge")]
[Description("Скруглить ребро детали, выбранное по точке (мировые координаты x,y,z в мм, точка должна лежать на ребре), радиусом radius (мм). Сделайте model_snapshot, прикиньте точку на ребре. Возвращает id операции.")]
public async Task<string> FilletEdge(double x, double y, double z, double radius)
{
await session.ConnectAsync();
var id = await modeler.FilletEdgeAsync(x, y, z, radius);
return $"Скругление R{radius} создано на ребре в точке ({x}, {y}, {z}), id={id}.";
}
[McpServerTool(Name = "chamfer_edge")]
[Description("Снять фаску с ребра детали, выбранного по точке (мировые координаты x,y,z в мм, точка должна лежать на ребре), катетом distance (мм). Сделайте model_snapshot, прикиньте точку на ребре. Возвращает id операции.")]
public async Task<string> ChamferEdge(double x, double y, double z, double distance)
{
await session.ConnectAsync();
var id = await modeler.ChamferEdgeAsync(x, y, z, distance);
return $"Фаска {distance}×{distance} создана на ребре в точке ({x}, {y}, {z}), id={id}.";
}
[McpServerTool(Name = "rebuild")]
[Description("Перестроить активный документ.")]
public async Task<string> Rebuild()