@
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:
@@ -125,7 +125,7 @@
|
|||||||
<!-- HERO -->
|
<!-- HERO -->
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v1 работает — цикл эскиз→выдавливание/вращение→снимок через MCP (23 инструмента)</span>
|
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v1 работает — цикл эскиз→операции→снимок через MCP (25 инструментов)</span>
|
||||||
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
|
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
|
||||||
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
|
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
|
||||||
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
|
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
|
||||||
@@ -246,8 +246,8 @@
|
|||||||
впереди.</p>
|
впереди.</p>
|
||||||
|
|
||||||
<div class="prog">
|
<div class="prog">
|
||||||
<div class="progrow"><span>Общий прогресс</span><span>v1 + эскиз на грани + вращение · 16 тестов зелёные · доки и ревью</span></div>
|
<div class="progrow"><span>Общий прогресс</span><span>v2: грань · вращение · скругление/фаска · 17 тестов зелёные</span></div>
|
||||||
<div class="bar"><i style="width:78%"></i></div>
|
<div class="bar"><i style="width:80%"></i></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="todo-grid" style="margin-top:26px">
|
<div class="todo-grid" style="margin-top:26px">
|
||||||
@@ -282,12 +282,12 @@
|
|||||||
<!-- TOOLS -->
|
<!-- TOOLS -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Инструменты v1 <span class="tag done">ГОТОВО</span></h3>
|
<h3>Инструменты v1 <span class="tag done">ГОТОВО</span></h3>
|
||||||
<p class="meta">23 инструмента, отдаются по MCP-протоколу</p>
|
<p class="meta">25 инструментов, отдаются по MCP-протоколу</p>
|
||||||
<ul class="list">
|
<ul class="list">
|
||||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>System</code>: connect · status · set_visible</span></li>
|
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>System</code>: connect · status · set_visible</span></li>
|
||||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Documents</code>: create · open · save · save_as · close · active</span></li>
|
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Documents</code>: create · open · save · save_as · close · active</span></li>
|
||||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Sketch</code>: create · create_on_face · add line/circle/rectangle/axis · close</span></li>
|
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Sketch</code>: create · create_on_face · add line/circle/rectangle/axis · close</span></li>
|
||||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Features</code>: extrude_boss/cut · revolve_boss/cut 🆕 · rebuild</span></li>
|
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Features</code>: extrude · revolve · fillet_edge/chamfer_edge 🆕 · rebuild</span></li>
|
||||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Vision</code>: model_snapshot 👁️</span></li>
|
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Vision</code>: model_snapshot 👁️</span></li>
|
||||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Query</code>: get_part_info (объём/масса/площадь/центр масс)</span></li>
|
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Query</code>: get_part_info (объём/масса/площадь/центр масс)</span></li>
|
||||||
<li><span class="mark todo"></span><span><code>Query</code>: select_face · list_bodies (далее)</span></li>
|
<li><span class="mark todo"></span><span><code>Query</code>: select_face · list_bodies (далее)</span></li>
|
||||||
@@ -319,7 +319,7 @@
|
|||||||
черчению и сборкам.</p>
|
черчению и сборкам.</p>
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">v1 · готово ✓</div><h4>Цикл моделирования</h4><p>Документы, эскизы, выдавливание/вырез, снимок модели — работает</p></div>
|
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">v1 · готово ✓</div><h4>Цикл моделирования</h4><p>Документы, эскизы, выдавливание/вырез, снимок модели — работает</p></div>
|
||||||
<div class="tl cur"><div class="ph">v2 · в работе</div><h4>Операции и выборка</h4><p>Эскиз на грани ✓ · вращение ✓ · далее: скругление/фаска, массивы, список тел</p></div>
|
<div class="tl cur"><div class="ph">v2 · в работе</div><h4>Операции и выборка</h4><p>Эскиз на грани ✓ · вращение ✓ · скругление/фаска ✓ · далее: массивы, список тел</p></div>
|
||||||
<div class="tl"><div class="ph">v3</div><h4>Параметрика</h4><p>Переменные <code>IVariable7</code>, свойства <code>IPropertyMng</code>, формулы</p></div>
|
<div class="tl"><div class="ph">v3</div><h4>Параметрика</h4><p>Переменные <code>IVariable7</code>, свойства <code>IPropertyMng</code>, формулы</p></div>
|
||||||
<div class="tl"><div class="ph">v4</div><h4>2D-черчение</h4><p>Виды, размеры, тексты, штриховки; спецификации</p></div>
|
<div class="tl"><div class="ph">v4</div><h4>2D-черчение</h4><p>Виды, размеры, тексты, штриховки; спецификации</p></div>
|
||||||
<div class="tl"><div class="ph">v5</div><h4>Сборки и экспорт</h4><p>Компоненты, сопряжения, конвертеры (STEP), HTTP/SSE-транспорт</p></div>
|
<div class="tl"><div class="ph">v5</div><h4>Сборки и экспорт</h4><p>Компоненты, сопряжения, конвертеры (STEP), HTTP/SSE-транспорт</p></div>
|
||||||
|
|||||||
@@ -246,6 +246,64 @@ public sealed class PartModeler : IDisposable
|
|||||||
return id;
|
return id;
|
||||||
}, ct);
|
}, 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>
|
/// <summary>Перестроить документ.</summary>
|
||||||
public Task RebuildAsync(CancellationToken ct = default)
|
public Task RebuildAsync(CancellationToken ct = default)
|
||||||
=> _dispatcher.InvokeAsync(() =>
|
=> _dispatcher.InvokeAsync(() =>
|
||||||
@@ -271,6 +329,19 @@ public sealed class PartModeler : IDisposable
|
|||||||
=> ActiveDoc3D().GetPart((short)Part_Type.pTop_Part) as ksPart
|
=> ActiveDoc3D().GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (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)
|
private SketchEntry RequireSketch(int id)
|
||||||
=> _sketches.TryGetValue(id, out var s) ? s
|
=> _sketches.TryGetValue(id, out var s) ? s
|
||||||
: throw new KeyNotFoundException($"Эскиз с id={id} не найден.");
|
: throw new KeyNotFoundException($"Эскиз с id={id} не найден.");
|
||||||
|
|||||||
@@ -55,6 +55,24 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler)
|
|||||||
return $"Вырез вращением (cut) создан, id={id}.";
|
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")]
|
[McpServerTool(Name = "rebuild")]
|
||||||
[Description("Перестроить активный документ.")]
|
[Description("Перестроить активный документ.")]
|
||||||
public async Task<string> Rebuild()
|
public async Task<string> Rebuild()
|
||||||
|
|||||||
@@ -120,6 +120,41 @@ public sealed class ModelingTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Fillet_and_chamfer_edges()
|
||||||
|
{
|
||||||
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Цилиндр R10×H20 → верхнее ребро на (10,0,20), нижнее на (10,0,0).
|
||||||
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||||
|
await _modeler.AddCircleAsync(s, 0, 0, 10);
|
||||||
|
await _modeler.CloseSketchAsync(s);
|
||||||
|
await _modeler.ExtrudeAsync(s, depth: 20);
|
||||||
|
var cylinderVolume = (await _query.GetPartInfoAsync()).Volume;
|
||||||
|
|
||||||
|
// Скругление верхнего ребра R3 и фаска нижнего 2×2.
|
||||||
|
var fil = await _modeler.FilletEdgeAsync(10, 0, 20, 3);
|
||||||
|
Assert.True(fil > 0);
|
||||||
|
var cha = await _modeler.ChamferEdgeAsync(10, 0, 0, 2);
|
||||||
|
Assert.True(cha > 0);
|
||||||
|
await _modeler.RebuildAsync();
|
||||||
|
|
||||||
|
var path = TestPaths.NewFile("_fillet.png");
|
||||||
|
var snap = await _snap.CaptureActiveModelAsync(saveToFile: path);
|
||||||
|
Assert.True(snap.Bytes.Length > 1000);
|
||||||
|
|
||||||
|
// Скругление и фаска снимают материал с рёбер → объём уменьшается, но незначительно.
|
||||||
|
var after = (await _query.GetPartInfoAsync()).Volume;
|
||||||
|
Assert.True(after < cylinderVolume, "После скругления/фаски объём должен уменьшиться.");
|
||||||
|
Assert.InRange(after, cylinderVolume * 0.9, cylinderVolume);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _docs.CloseAsync(save: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Reset_invalidates_sketch_ids()
|
public async Task Reset_invalidates_sketch_ids()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user