@
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:
@@ -43,24 +43,52 @@ public sealed class PartModeler : IDisposable
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
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
|
||||
?? throw new InvalidOperationException("Не удалось получить базовую плоскость.");
|
||||
|
||||
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;
|
||||
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>Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование.</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(() =>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6Constants;
|
||||
using KompasAPI7;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Query;
|
||||
|
||||
@@ -10,6 +10,10 @@ namespace Kompas.Mcp.Core.Query;
|
||||
[SupportedOSPlatform("windows")]
|
||||
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 KompasDispatcher _dispatcher;
|
||||
|
||||
@@ -20,41 +24,39 @@ public sealed class QueryService
|
||||
}
|
||||
|
||||
/// <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)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc3d = _session.Application.ActiveDocument as IKompasDocument3D
|
||||
?? throw new InvalidOperationException("Активный документ не является 3D-моделью.");
|
||||
var part = doc3d.TopPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент.");
|
||||
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).");
|
||||
|
||||
ksMassInertiaParam? mass = null;
|
||||
try
|
||||
{
|
||||
var mass = (IMassInertiaParam7)part;
|
||||
|
||||
// Детерминированные единицы: длина — мм, масса — кг. Иначе КОМПАС вернёт
|
||||
// значения в единицах отображения МЦХ (по умолчанию см/г), что зависит от настроек.
|
||||
mass.LengthUnits = ksLengthUnitsEnum.ksLUnMM;
|
||||
mass.MassUnits = ksMassUnitsEnum.ksMUnKG;
|
||||
|
||||
// Calculate() возвращает FALSE и при успехе (когда расчёт уже актуален),
|
||||
// поэтому ориентируемся на свойство Actual, а не на возвращаемое значение.
|
||||
mass.Calculate();
|
||||
if (!mass.Actual)
|
||||
throw new InvalidOperationException("МЦХ не рассчитаны (модель пуста или не перестроена?).");
|
||||
mass = part.CalcMassInertiaProperties(MixMmKg) as ksMassInertiaParam
|
||||
?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null (модель пуста или не перестроена?).");
|
||||
|
||||
return new PartInfo
|
||||
{
|
||||
Volume = mass.Volume,
|
||||
Mass = mass.Mass,
|
||||
Area = mass.Area,
|
||||
CenterX = mass.Xc,
|
||||
CenterY = mass.Yc,
|
||||
CenterZ = mass.Zc,
|
||||
Volume = mass.v, // объём, мм³
|
||||
Mass = mass.m, // масса, кг
|
||||
Area = mass.F, // площадь, мм²
|
||||
CenterX = mass.xc, // центр масс в глобальной СК, мм
|
||||
CenterY = mass.yc,
|
||||
CenterZ = mass.zc,
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
// part и его QI-приведение mass — один RCW; release один раз.
|
||||
if (mass is not null && Marshal.IsComObject(mass)) Marshal.ReleaseComObject(mass);
|
||||
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
|
||||
}
|
||||
}, ct);
|
||||
|
||||
@@ -18,6 +18,15 @@ public sealed class SketchTools(KompasSession session, PartModeler modeler)
|
||||
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")]
|
||||
[Description("Добавить окружность в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddCircle(int sketchId, double centerX, double centerY, double radius)
|
||||
|
||||
Reference in New Issue
Block a user