@
feat(query): перечисление граней + выбор грани по индексу QueryService.ListFacesAsync: обход EntityCollection(o3d_face), для каждой грани — индекс, тип поверхности (plane/cylinder/cone/sphere/torus/nurbs через ksFaceDefinition.Is*) и площадь (GetArea, мм²). Запись FaceInfo. PartModeler.OpenSketchOnFaceIndexAsync: эскиз на грани по индексу из list_faces — надёжнее выбора по точке (агенту не нужно угадывать координаты). Новые инструменты: list_faces, sketch_create_on_face_index. Тест List_faces_and_sketch_by_index: цилиндр → 3 грани (2 plane + 1 cylinder), площади торца ≈314 мм² и бока ≈1257 мм²; эскиз на торце по индексу → бобышка. Презентация: 28 инструментов, 18 тестов. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> @
This commit is contained in:
@@ -125,7 +125,7 @@
|
||||
<!-- HERO -->
|
||||
<header class="hero">
|
||||
<div class="wrap">
|
||||
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v1 работает — цикл эскиз→операции→снимок через MCP (26 инструментов)</span>
|
||||
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v1 работает — цикл эскиз→операции→снимок через MCP (28 инструментов)</span>
|
||||
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
|
||||
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
|
||||
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
|
||||
@@ -246,8 +246,8 @@
|
||||
впереди.</p>
|
||||
|
||||
<div class="prog">
|
||||
<div class="progrow"><span>Общий прогресс</span><span>v2: грань · вращение · скругление/фаска · 17 тестов зелёные</span></div>
|
||||
<div class="bar"><i style="width:80%"></i></div>
|
||||
<div class="progrow"><span>Общий прогресс</span><span>v2: грань · вращение · скругление/фаска · габариты · список граней · 18 тестов</span></div>
|
||||
<div class="bar"><i style="width:82%"></i></div>
|
||||
</div>
|
||||
|
||||
<div class="todo-grid" style="margin-top:26px">
|
||||
@@ -282,15 +282,15 @@
|
||||
<!-- TOOLS -->
|
||||
<div class="panel">
|
||||
<h3>Инструменты v1 <span class="tag done">ГОТОВО</span></h3>
|
||||
<p class="meta">26 инструментов, отдаются по MCP-протоколу</p>
|
||||
<p class="meta">28 инструментов, отдаются по MCP-протоколу</p>
|
||||
<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>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 · 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 · 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>Query</code>: get_part_info · get_bounding_box 🆕</span></li>
|
||||
<li><span class="mark todo"></span><span><code>Query</code>: list_bodies · list_faces (далее)</span></li>
|
||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Query</code>: get_part_info · get_bounding_box · list_faces 🆕</span></li>
|
||||
<li><span class="mark todo"></span><span><code>Query</code>: list_bodies · list_edges (далее)</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
черчению и сборкам.</p>
|
||||
<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 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">v4</div><h4>2D-черчение</h4><p>Виды, размеры, тексты, штриховки; спецификации</p></div>
|
||||
<div class="tl"><div class="ph">v5</div><h4>Сборки и экспорт</h4><p>Компоненты, сопряжения, конвертеры (STEP), HTTP/SSE-транспорт</p></div>
|
||||
|
||||
@@ -71,6 +71,25 @@ public sealed class PartModeler : IDisposable
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Kompas.Mcp.Core.Query;
|
||||
|
||||
/// <summary>Сводка по грани детали для выбора в итеративном цикле построения.</summary>
|
||||
public sealed record FaceInfo
|
||||
{
|
||||
/// <summary>Индекс грани в коллекции (стабилен, пока геометрия не меняется).</summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>Тип поверхности: plane, cylinder, cone, sphere, torus, nurbs, other.</summary>
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>Площадь грани, мм².</summary>
|
||||
public required double Area { get; init; }
|
||||
}
|
||||
@@ -61,6 +61,54 @@ public sealed class QueryService
|
||||
}
|
||||
}, ct);
|
||||
|
||||
// ST_MIX_MM (0x1) — площади граней в мм².
|
||||
private const uint MixMm = 0x1;
|
||||
|
||||
/// <summary>Перечислить грани активной детали: индекс, тип поверхности и площадь (мм²).</summary>
|
||||
public Task<IReadOnlyList<FaceInfo>> ListFacesAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync<IReadOnlyList<FaceInfo>>(() =>
|
||||
{
|
||||
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
try
|
||||
{
|
||||
var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
|
||||
?? throw new InvalidOperationException("Не удалось получить коллекцию граней.");
|
||||
|
||||
var count = faces.GetCount();
|
||||
var list = new List<FaceInfo>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (faces.GetByIndex(i) is not ksEntity entity) continue;
|
||||
if (entity.GetDefinition() is not ksFaceDefinition def) continue;
|
||||
list.Add(new FaceInfo
|
||||
{
|
||||
Index = i,
|
||||
Type = FaceType(def),
|
||||
Area = def.GetArea(MixMm),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
|
||||
}
|
||||
}, ct);
|
||||
|
||||
private static string FaceType(ksFaceDefinition def)
|
||||
{
|
||||
if (def.IsPlanar()) return "plane";
|
||||
if (def.IsCylinder()) return "cylinder";
|
||||
if (def.IsCone()) return "cone";
|
||||
if (def.IsSphere()) return "sphere";
|
||||
if (def.IsTorus()) return "torus";
|
||||
if (def.IsNurbsSurface()) return "nurbs";
|
||||
return "other";
|
||||
}
|
||||
|
||||
/// <summary>Габаритный параллелепипед активной детали (по телам), мм.</summary>
|
||||
public Task<BoundingBox> GetBoundingBoxAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
|
||||
@@ -19,6 +19,17 @@ public sealed class QueryTools(KompasSession session, QueryService query)
|
||||
$"центр масс: ({i.CenterX:F2}, {i.CenterY:F2}, {i.CenterZ:F2}).";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "list_faces")]
|
||||
[Description("Перечислить грани активной детали: индекс, тип поверхности (plane/cylinder/cone/sphere/torus/nurbs) и площадь (мм²). Индекс используйте в sketch_create_on_face_index. Удобно для надёжного выбора грани без угадывания координат.")]
|
||||
public async Task<string> ListFaces()
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
var faces = await query.ListFacesAsync();
|
||||
if (faces.Count == 0) return "Граней нет (модель пуста?).";
|
||||
var lines = faces.Select(f => $" [{f.Index}] {f.Type}, площадь {f.Area:F2} мм²");
|
||||
return $"Граней: {faces.Count}\n" + string.Join("\n", lines);
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "get_bounding_box")]
|
||||
[Description("Габариты активной детали: координаты габаритного параллелепипеда (мм) и его размеры. Помогает оценить протяжённость модели и подобрать точки для выбора граней/рёбер.")]
|
||||
public async Task<string> GetBoundingBox()
|
||||
|
||||
@@ -45,6 +45,15 @@ public sealed class SketchTools(KompasSession session, PartModeler modeler)
|
||||
return "Отрезок добавлен.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_create_on_face_index")]
|
||||
[Description("Создать эскиз на грани детали, заданной индексом из list_faces, и открыть на редактирование. Надёжнее выбора по точке (не нужно угадывать координаты). Возвращает id эскиза.")]
|
||||
public async Task<string> CreateOnFaceIndex([Description("Индекс грани из list_faces")] int faceIndex)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
var id = await modeler.OpenSketchOnFaceIndexAsync(faceIndex);
|
||||
return $"Эскиз создан на грани [{faceIndex}], id={id}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_axis")]
|
||||
[Description("Добавить осевую линию (ось вращения) в открытый эскиз — нужна для revolve_boss/revolve_cut. Две точки в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddAxis(int sketchId, double x1, double y1, double x2, double y2)
|
||||
|
||||
@@ -155,6 +155,46 @@ public sealed class ModelingTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_faces_and_sketch_by_index()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Цилиндр R10×H20: 3 грани — 2 плоские (торцы) + 1 цилиндрическая (бок).
|
||||
var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddCircleAsync(s1, 0, 0, 10);
|
||||
await _modeler.CloseSketchAsync(s1);
|
||||
await _modeler.ExtrudeAsync(s1, depth: 20);
|
||||
|
||||
var faces = await _query.ListFacesAsync();
|
||||
Assert.Equal(3, faces.Count);
|
||||
Assert.Equal(2, faces.Count(f => f.Type == "plane"));
|
||||
Assert.Equal(1, faces.Count(f => f.Type == "cylinder"));
|
||||
|
||||
// Площадь торца ≈ π·10² ≈ 314 мм²; боковой ≈ 2π·10·20 ≈ 1257 мм².
|
||||
var planar = faces.First(f => f.Type == "plane");
|
||||
Assert.InRange(planar.Area, Math.PI * 100 * 0.95, Math.PI * 100 * 1.05);
|
||||
var side = faces.First(f => f.Type == "cylinder");
|
||||
Assert.InRange(side.Area, 2 * Math.PI * 10 * 20 * 0.95, 2 * Math.PI * 10 * 20 * 1.05);
|
||||
|
||||
// Эскиз на плоской грани по индексу → бобышка (проверяем, что выбор по индексу работает).
|
||||
var s2 = await _modeler.OpenSketchOnFaceIndexAsync(planar.Index);
|
||||
await _modeler.AddCircleAsync(s2, 0, 0, 4);
|
||||
await _modeler.CloseSketchAsync(s2);
|
||||
var boss = await _modeler.ExtrudeAsync(s2, depth: 8, forward: false);
|
||||
Assert.True(boss > 0);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
// Модель осталась валидной и опрашиваемой.
|
||||
Assert.True((await _query.GetPartInfoAsync()).Volume > 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _docs.CloseAsync(save: false);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reset_invalidates_sketch_ids()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user