@
feat(sketch): эскиз на выбранной грани + МЦХ через API5 sketch_create_on_face: OpenSketchOnFaceAsync выбирает плоскую грань по точке (ksPart.EntityCollection(o3d_face).SelectByPoint) и создаёт на ней эскиз — ключевой шаг итеративного цикла «построил → выбрал грань → новый эскиз». Общее ядро создания эскиза вынесено в CreateSketchOn. QueryService переписан на API5 ksPart.CalcMassInertiaProperties(ST_MIX_MM|ST_MIX_KG): - API7 IMassInertiaParam7.Calculate() непригоден — флаг Actual «залипает» после первого расчёта, повторный запрос отдавал устаревший объём (баг кэша); - API5-маршрут считает по запросу и согласован с построением через ksPart; - единицы (мм³/мм²/кг/мм) задаются битовым вектором. Тест Sketch_on_face_adds_boss_on_top: запрос → штифт R5×H10 на верхней грани цилиндра → повторный запрос видит прирост ≈785 мм³ (проверяет и грань, и кэш МЦХ). Презентация: 20 инструментов, 15 тестов; OPEN_QUESTIONS обновлён. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> @
This commit is contained in:
+14
-9
@@ -69,15 +69,20 @@ stateful-сессии? Пока полагаемся на последовате
|
|||||||
передавать через `ImageContentBlock.FromBytes(bytes, mimeType)` — он сам кодирует. Прямое
|
передавать через `ImageContentBlock.FromBytes(bytes, mimeType)` — он сам кодирует. Прямое
|
||||||
присваивание `Data = rawBytes` портит изображение на проводе. Исправлено в VisionTools.
|
присваивание `Data = rawBytes` портит изображение на проводе. Исправлено в VisionTools.
|
||||||
|
|
||||||
### ✅ МЦХ (get_part_info): единицы измерения и поведение Calculate()
|
### ✅ МЦХ (get_part_info): API5 `CalcMassInertiaProperties`, а не API7
|
||||||
`IMassInertiaParam7` по умолчанию возвращает значения в **единицах отображения МЦХ** документа
|
Сначала пробовали API7 `IMassInertiaParam7` (от `TopPart`). Две проблемы:
|
||||||
(по умолчанию см³/см²/г, плотность г/см³) — то есть зависит от настроек, а не фиксированные СИ.
|
1. **Единицы** — отдаёт в единицах отображения МЦХ документа (по умолчанию см³/см²/г), зависит от настроек.
|
||||||
Поэтому `QueryService` принудительно выставляет `LengthUnits = ksLUnMM` и `MassUnits = ksMUnKG`
|
2. **Залипающий кэш** — после первого расчёта свойство `Actual` остаётся `TRUE` и не сбрасывается
|
||||||
перед чтением → детерминированные **мм³, мм², кг, мм**. Проверено тестом (цилиндр R10×H20:
|
при изменении геометрии через API5; `Calculate()` возвращает `FALSE` и не пересчитывает.
|
||||||
V≈6283 мм³, S≈1885 мм², m≈0.0494 кг, Zc=10 мм).
|
Сценарий «запрос → достройка → запрос» отдавал **устаревшее** значение. Сброс через
|
||||||
Тонкость: `Calculate()` возвращает **FALSE даже при успехе** (когда расчёт уже актуален после
|
`RebuildModel`, переключение `MassSettingMode` — не помогли.
|
||||||
`RebuildDocument`), поэтому опираемся на свойство `Actual`, а не на возвращаемое значение.
|
|
||||||
Материал по умолчанию — сталь (ρ≈7.856 г/см³); масса зависит от назначенного материала.
|
**Решение (✅ в коде):** считаем через API5 **`ksPart.CalcMassInertiaProperties(bitVector)`** →
|
||||||
|
`ksMassInertiaParam` (`v`/`m`/`F`/`xc`/`yc`/`zc`). Считает «по запросу» (без залипания), единицы
|
||||||
|
задаются битовым вектором: `ST_MIX_MM (0x1) | ST_MIX_KG (0x10)` → **мм³, мм², кг, мм**.
|
||||||
|
Согласовано с тем, что деталь строится через API5 `ksPart`. Проверено двумя тестами:
|
||||||
|
цилиндр R10×H20 (V≈6283 мм³, S≈1885 мм², m≈0.0494 кг, Zc=10 мм) и «запрос → штифт на грани → запрос»
|
||||||
|
(виден корректный прирост ≈785 мм³). Материал по умолчанию — сталь (ρ≈7.856 г/см³).
|
||||||
|
|
||||||
### ✅ Снимок 3D: рендер через файл, а не resultArrayBytes
|
### ✅ Снимок 3D: рендер через файл, а не resultArrayBytes
|
||||||
`ksDocument3D.SaveAsToRasterFormat(fileName, param)` при **непустом** `fileName` возвращает TRUE,
|
`ksDocument3D.SaveAsToRasterFormat(fileName, param)` при **непустом** `fileName` возвращает TRUE,
|
||||||
|
|||||||
@@ -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 (19 инструментов)</span>
|
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v1 работает — цикл эскиз→выдавливание→снимок через MCP (20 инструментов)</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 работает · 14 тестов зелёные · доки и ревью</span></div>
|
<div class="progrow"><span>Общий прогресс</span><span>v1 + эскиз на грани · 15 тестов зелёные · доки и ревью</span></div>
|
||||||
<div class="bar"><i style="width:74%"></i></div>
|
<div class="bar"><i style="width:76%"></i></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="todo-grid" style="margin-top:26px">
|
<div class="todo-grid" style="margin-top:26px">
|
||||||
@@ -282,11 +282,11 @@
|
|||||||
<!-- 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">19 инструментов, отдаются по MCP-протоколу</p>
|
<p class="meta">20 инструментов, отдаются по 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 · add line/circle/rectangle · 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 · close</span></li>
|
||||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Features</code>: extrude_boss · extrude_cut · rebuild</span></li>
|
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Features</code>: extrude_boss · extrude_cut · 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -43,24 +43,52 @@ public sealed class PartModeler : IDisposable
|
|||||||
=> _dispatcher.InvokeAsync(() =>
|
=> _dispatcher.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
var part = GetTopPart();
|
var part = GetTopPart();
|
||||||
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.");
|
|
||||||
var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity
|
var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity
|
||||||
?? throw new InvalidOperationException("Не удалось получить базовую плоскость.");
|
?? throw new InvalidOperationException("Не удалось получить базовую плоскость.");
|
||||||
|
return CreateSketchOn(part, planeEntity);
|
||||||
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;
|
|
||||||
}, ct);
|
}, 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>Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование.</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>
|
/// <summary>Добавить отрезок в открытый эскиз.</summary>
|
||||||
public Task AddLineAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
|
public Task AddLineAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
|
||||||
=> _dispatcher.InvokeAsync(() =>
|
=> _dispatcher.InvokeAsync(() =>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using Kompas.Mcp.Core.Threading;
|
using Kompas.Mcp.Core.Threading;
|
||||||
using Kompas6Constants;
|
using Kompas6API5;
|
||||||
using KompasAPI7;
|
using Kompas6Constants3D;
|
||||||
|
|
||||||
namespace Kompas.Mcp.Core.Query;
|
namespace Kompas.Mcp.Core.Query;
|
||||||
|
|
||||||
@@ -10,6 +10,10 @@ namespace Kompas.Mcp.Core.Query;
|
|||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
public sealed class QueryService
|
public sealed class QueryService
|
||||||
{
|
{
|
||||||
|
// bitVector для CalcMassInertiaProperties: ST_MIX_MM (0x1, длина — мм) | ST_MIX_KG (0x10, масса — кг).
|
||||||
|
// Иначе КОМПАС вернёт значения в единицах по умолчанию (см/г).
|
||||||
|
private const int MixMmKg = 0x1 | 0x10;
|
||||||
|
|
||||||
private readonly KompasSession _session;
|
private readonly KompasSession _session;
|
||||||
private readonly KompasDispatcher _dispatcher;
|
private readonly KompasDispatcher _dispatcher;
|
||||||
|
|
||||||
@@ -20,41 +24,39 @@ public sealed class QueryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Рассчитать и вернуть МЦХ активной детали (объём, масса, площадь, центр масс).</summary>
|
/// <summary>Рассчитать и вернуть МЦХ активной детали (объём, масса, площадь, центр масс).</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Используется API5 <c>ksPart.CalcMassInertiaProperties</c> — он считает МЦХ «по запросу»
|
||||||
|
/// и согласован с построением детали через API5. Путь API7 <c>IMassInertiaParam7.Calculate()</c>
|
||||||
|
/// здесь непригоден: его флаг <c>Actual</c> «залипает» после первого расчёта и не сбрасывается
|
||||||
|
/// при изменении геометрии через API5 (повторный запрос отдавал бы устаревшее значение).
|
||||||
|
/// </remarks>
|
||||||
public Task<PartInfo> GetPartInfoAsync(CancellationToken ct = default)
|
public Task<PartInfo> GetPartInfoAsync(CancellationToken ct = default)
|
||||||
=> _dispatcher.InvokeAsync(() =>
|
=> _dispatcher.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
var doc3d = _session.Application.ActiveDocument as IKompasDocument3D
|
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||||
?? throw new InvalidOperationException("Активный документ не является 3D-моделью.");
|
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||||
var part = doc3d.TopPart
|
var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент.");
|
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||||
|
|
||||||
|
ksMassInertiaParam? mass = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var mass = (IMassInertiaParam7)part;
|
mass = part.CalcMassInertiaProperties(MixMmKg) as ksMassInertiaParam
|
||||||
|
?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null (модель пуста или не перестроена?).");
|
||||||
// Детерминированные единицы: длина — мм, масса — кг. Иначе КОМПАС вернёт
|
|
||||||
// значения в единицах отображения МЦХ (по умолчанию см/г), что зависит от настроек.
|
|
||||||
mass.LengthUnits = ksLengthUnitsEnum.ksLUnMM;
|
|
||||||
mass.MassUnits = ksMassUnitsEnum.ksMUnKG;
|
|
||||||
|
|
||||||
// Calculate() возвращает FALSE и при успехе (когда расчёт уже актуален),
|
|
||||||
// поэтому ориентируемся на свойство Actual, а не на возвращаемое значение.
|
|
||||||
mass.Calculate();
|
|
||||||
if (!mass.Actual)
|
|
||||||
throw new InvalidOperationException("МЦХ не рассчитаны (модель пуста или не перестроена?).");
|
|
||||||
|
|
||||||
return new PartInfo
|
return new PartInfo
|
||||||
{
|
{
|
||||||
Volume = mass.Volume,
|
Volume = mass.v, // объём, мм³
|
||||||
Mass = mass.Mass,
|
Mass = mass.m, // масса, кг
|
||||||
Area = mass.Area,
|
Area = mass.F, // площадь, мм²
|
||||||
CenterX = mass.Xc,
|
CenterX = mass.xc, // центр масс в глобальной СК, мм
|
||||||
CenterY = mass.Yc,
|
CenterY = mass.yc,
|
||||||
CenterZ = mass.Zc,
|
CenterZ = mass.zc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// part и его QI-приведение mass — один RCW; release один раз.
|
if (mass is not null && Marshal.IsComObject(mass)) Marshal.ReleaseComObject(mass);
|
||||||
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
|
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
|
||||||
}
|
}
|
||||||
}, ct);
|
}, ct);
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ public sealed class SketchTools(KompasSession session, PartModeler modeler)
|
|||||||
return $"Эскиз создан, id={id}. Добавьте геометрию (sketch_add_*) и завершите sketch_close.";
|
return $"Эскиз создан, id={id}. Добавьте геометрию (sketch_add_*) и завершите sketch_close.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[McpServerTool(Name = "sketch_create_on_face")]
|
||||||
|
[Description("Создать эскиз на плоской грани детали, выбранной по точке (мировые координаты модели x,y,z в мм), и открыть его на редактирование. Используйте для построения «на грани» поверх уже созданной геометрии: сделайте model_snapshot, прикиньте координаты точки на нужной грани, затем вызовите этот инструмент. Возвращает id эскиза.")]
|
||||||
|
public async Task<string> CreateOnFace(double x, double y, double z)
|
||||||
|
{
|
||||||
|
await session.ConnectAsync();
|
||||||
|
var id = await modeler.OpenSketchOnFaceAsync(x, y, z);
|
||||||
|
return $"Эскиз создан на грани в точке ({x}, {y}, {z}), id={id}. Координаты геометрии — в локальной плоскости эскиза.";
|
||||||
|
}
|
||||||
|
|
||||||
[McpServerTool(Name = "sketch_add_circle")]
|
[McpServerTool(Name = "sketch_add_circle")]
|
||||||
[Description("Добавить окружность в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
|
[Description("Добавить окружность в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
|
||||||
public async Task<string> AddCircle(int sketchId, double centerX, double centerY, double radius)
|
public async Task<string> AddCircle(int sketchId, double centerX, double centerY, double radius)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Kompas.Mcp.Core.Documents;
|
using Kompas.Mcp.Core.Documents;
|
||||||
using Kompas.Mcp.Core.Modeling;
|
using Kompas.Mcp.Core.Modeling;
|
||||||
|
using Kompas.Mcp.Core.Query;
|
||||||
using Kompas.Mcp.Core.Vision;
|
using Kompas.Mcp.Core.Vision;
|
||||||
|
|
||||||
namespace Kompas.Mcp.Tests.Integration;
|
namespace Kompas.Mcp.Tests.Integration;
|
||||||
@@ -12,12 +13,14 @@ public sealed class ModelingTests
|
|||||||
private readonly DocumentService _docs;
|
private readonly DocumentService _docs;
|
||||||
private readonly PartModeler _modeler;
|
private readonly PartModeler _modeler;
|
||||||
private readonly SnapshotService _snap;
|
private readonly SnapshotService _snap;
|
||||||
|
private readonly QueryService _query;
|
||||||
|
|
||||||
public ModelingTests(KompasFixture fx)
|
public ModelingTests(KompasFixture fx)
|
||||||
{
|
{
|
||||||
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
||||||
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
|
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
|
||||||
_snap = new SnapshotService(fx.Session, fx.Dispatcher);
|
_snap = new SnapshotService(fx.Session, fx.Dispatcher);
|
||||||
|
_query = new QueryService(fx.Session, fx.Dispatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -53,6 +56,42 @@ public sealed class ModelingTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Sketch_on_face_adds_boss_on_top()
|
||||||
|
{
|
||||||
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Цилиндр R10×H20 на XOY → верхняя грань на Z=20.
|
||||||
|
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 before = await _query.GetPartInfoAsync();
|
||||||
|
|
||||||
|
// Эскиз на верхней грани (точка (0,0,20)) → штифт R5×H10 наружу.
|
||||||
|
var s2 = await _modeler.OpenSketchOnFaceAsync(0, 0, 20);
|
||||||
|
await _modeler.AddCircleAsync(s2, 0, 0, 5);
|
||||||
|
await _modeler.CloseSketchAsync(s2);
|
||||||
|
var boss = await _modeler.ExtrudeAsync(s2, depth: 10);
|
||||||
|
Assert.True(boss > 0);
|
||||||
|
await _modeler.RebuildAsync();
|
||||||
|
|
||||||
|
var after = await _query.GetPartInfoAsync();
|
||||||
|
|
||||||
|
// Прирост объёма ≈ объём штифта π·5²·10 ≈ 785.4 мм³.
|
||||||
|
var deltaExpected = Math.PI * 25 * 10;
|
||||||
|
Assert.InRange(after.Volume - before.Volume, deltaExpected * 0.9, deltaExpected * 1.1);
|
||||||
|
}
|
||||||
|
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