a4cc755fb4
Три способа получить форму там, где раньше был только замкнутый контур из отрезков и дуг. 1. Примитив эскиза type=text: ksTextEx + ksConvertTextToCurve. Пока текст остаётся текстом, это оформление, и операция его не видит; после конвертации глифы становятся обычным сечением. Ответ возвращает фактическую длину строки — иначе ширину шрифта до выдавливания не узнать. 2. extrude(thinThickness, thinSide): контур трактуется как стенка заданной толщины. outward по замкнутому контуру даёт кайму-эквидистанту вокруг него — так надпись получает подложку без 2D-эквидистанты. Толщину при dtReverse КОМПАС читает из reverseThickness: положить её в normalThickness значит молча получить СПЛОШНОЕ сечение вместо рамки. 3. Инструмент primitive: элементарные тела API7 (block, cylinder, sphere, cone) с result=new|union|subtract|intersect. Карман и паз строятся вычитанием тела, без эскиза и выреза. Update() возвращает TRUE даже когда вычитание прошло мимо тела, поэтому сервис сверяет объём до и после и откатывает операцию, если ничего не изменилось.
181 lines
7.8 KiB
C#
181 lines
7.8 KiB
C#
using Kompas.Mcp.Core.Documents;
|
|
using Kompas.Mcp.Core.Modeling;
|
|
using Kompas.Mcp.Core.Query;
|
|
|
|
namespace Kompas.Mcp.Tests.Integration;
|
|
|
|
/// <summary>
|
|
/// Интеграция: механики, на которых держатся слитые инструменты — пакет примитивов эскиза
|
|
/// за один заход, мост «индекс грани → точка на ней», скругление/фаска пачкой рёбер и upsert
|
|
/// переменной. Каждая цепочка проверяется на живом КОМПАС: слияние инструментов не должно
|
|
/// менять результат построения.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
[Collection(KompasCollection.Name)]
|
|
public sealed class MergedToolPathsTests : IntegrationTestBase
|
|
{
|
|
private readonly DocumentService _docs;
|
|
private readonly PartModeler _modeler;
|
|
private readonly QueryService _query;
|
|
private readonly ModelInspectionService _inspection;
|
|
private readonly VariableService _variables;
|
|
|
|
public MergedToolPathsTests(KompasFixture fx) : base(fx)
|
|
{
|
|
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
|
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
|
|
_query = new QueryService(fx.Session, fx.Dispatcher);
|
|
_inspection = new ModelInspectionService(fx.Session, fx.Dispatcher);
|
|
_variables = new VariableService(fx.Session, fx.Dispatcher);
|
|
}
|
|
|
|
private static SketchEntity Rect(double x1, double y1, double x2, double y2) => new()
|
|
{
|
|
Kind = SketchEntityKind.Rectangle,
|
|
Points = [(x1, y1), (x2, y2)],
|
|
};
|
|
|
|
private static SketchEntity Circle(double cx, double cy, double r) => new()
|
|
{
|
|
Kind = SketchEntityKind.Circle,
|
|
CenterX = cx, CenterY = cy, Radius = r,
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Batch_builds_the_whole_contour_in_one_call()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
// Плита 40×20 с четырьмя отверстиями Ø4 — раньше это было 6 вызовов, теперь один.
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
var added = await _modeler.AddEntitiesAsync(s,
|
|
[
|
|
Rect(0, 0, 40, 20),
|
|
Circle(5, 5, 2), Circle(35, 5, 2), Circle(5, 15, 2), Circle(35, 15, 2),
|
|
], autoClose: true);
|
|
Assert.Equal(5, added.Count);
|
|
|
|
await _modeler.ExtrudeAsync(s, depth: 10);
|
|
await _modeler.RebuildAsync();
|
|
|
|
// Объём плиты минус четыре цилиндра.
|
|
var expected = (40 * 20 - 4 * Math.PI * 4) * 10;
|
|
var v = (await _query.GetPartInfoAsync()).Volume;
|
|
Assert.InRange(v, expected * 0.98, expected * 1.02);
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Batch_reports_the_failing_position()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
var bad = new SketchEntity { Kind = SketchEntityKind.Circle, CenterX = 0, CenterY = 0, Radius = -1 };
|
|
|
|
var ex = await Assert.ThrowsAnyAsync<Exception>(
|
|
() => _modeler.AddEntitiesAsync(s, [Rect(0, 0, 10, 10), Circle(2, 2, 1), bad]));
|
|
|
|
Assert.Contains("[2]", ex.Message, StringComparison.Ordinal);
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Face_center_point_lands_on_the_face()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
await _modeler.AddEntitiesAsync(s, [Rect(0, 0, 40, 20)], autoClose: true);
|
|
await _modeler.ExtrudeAsync(s, depth: 10);
|
|
await _modeler.RebuildAsync();
|
|
|
|
// Мост «индекс → точка»: точка обязана лежать на грани, иначе выбор по ней провалится.
|
|
var faces = await _query.ListFacesAsync();
|
|
Assert.NotEmpty(faces);
|
|
|
|
foreach (var f in faces)
|
|
{
|
|
var (x, y, z) = await _inspection.FaceCenterPointAsync(f.Index);
|
|
Assert.True(double.IsFinite(x) && double.IsFinite(y) && double.IsFinite(z),
|
|
$"грань [{f.Index}]: точка не вычислена");
|
|
|
|
// Точка внутри габарита детали (с допуском на округление).
|
|
Assert.InRange(x, -0.01, 40.01);
|
|
Assert.InRange(y, -0.01, 20.01);
|
|
Assert.InRange(z, -0.01, 10.01);
|
|
}
|
|
|
|
// И по такой точке эскиз на грани действительно создаётся.
|
|
var top = faces.First(f => f.Type == "plane");
|
|
var (px, py, pz) = await _inspection.FaceCenterPointAsync(top.Index);
|
|
var onFace = await _modeler.OpenSketchOnFaceAsync(px, py, pz);
|
|
await _modeler.CloseSketchAsync(onFace);
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Fillet_and_chamfer_take_several_edges_at_once()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
await _modeler.AddEntitiesAsync(s, [Rect(0, 0, 40, 20)], autoClose: true);
|
|
await _modeler.ExtrudeAsync(s, depth: 10);
|
|
await _modeler.RebuildAsync();
|
|
|
|
var before = (await _query.GetPartInfoAsync()).Volume;
|
|
var vertical = (await _query.ListEdgesAsync()).Where(e => e.Type == "line").Take(4).Select(e => e.Index).ToList();
|
|
Assert.Equal(4, vertical.Count);
|
|
|
|
// Одна операция на все рёбра: материал убывает, дерево не растёт на четыре узла.
|
|
var featuresBefore = (await _inspection.ListFeaturesAsync()).Count;
|
|
await _modeler.FilletEdgesAsync(vertical, radius: 2);
|
|
await _modeler.RebuildAsync();
|
|
|
|
var after = (await _query.GetPartInfoAsync()).Volume;
|
|
var featuresAfter = (await _inspection.ListFeaturesAsync()).Count;
|
|
|
|
Assert.True(after < before, $"объём не убыл: {before} → {after}");
|
|
Assert.Equal(featuresBefore + 1, featuresAfter);
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Set_variable_creates_then_updates()
|
|
{
|
|
await _docs.CreateAsync(KompasDocumentType.Part);
|
|
try
|
|
{
|
|
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
|
await _modeler.AddEntitiesAsync(s, [Rect(0, 0, 40, 20)], autoClose: true);
|
|
await _modeler.ExtrudeAsync(s, depth: 10);
|
|
|
|
var (created, wasCreated) = await _variables.SetOrCreateVariableAsync("width", "40", "ширина плиты");
|
|
Assert.True(wasCreated);
|
|
Assert.Equal(40, created, 3);
|
|
|
|
// Повторный вызов — уже изменение, и формула со ссылкой считается.
|
|
var (updated, wasCreatedAgain) = await _variables.SetOrCreateVariableAsync("width", "50");
|
|
Assert.False(wasCreatedAgain);
|
|
Assert.Equal(50, updated, 3);
|
|
|
|
var (derived, _) = await _variables.SetOrCreateVariableAsync("height", "width/2");
|
|
Assert.Equal(25, derived, 3);
|
|
|
|
var vars = await _inspection.ListVariablesAsync();
|
|
Assert.Contains(vars, v => v.Name == "width" && Math.Abs(v.Value - 50) < 1e-6);
|
|
Assert.Contains(vars, v => v.Name == "height");
|
|
}
|
|
finally { await _docs.CloseAsync(save: false); }
|
|
}
|
|
}
|