feat(sketch): 7 примитивов эскиза — дуга x2, эллипс, ломаная, многоугольник, сплайн, точка
Методы PartModeler + MCP-инструменты sketch_add_arc_3points/arc/ellipse/ polyline/polygon/spline/point. NURBS порядок 4, ksEndObj через try/finally, param-структуры через NewParam с освобождением RCW. 7 интеграционных тестов (профиль → выдавливание → объём) зелёные. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -729,9 +729,11 @@ public async Task Spline_closed_loop_extrudes()
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
// Площадь сплайна между ромбом (200) и кругом (314) по этим точкам — проверяем правдоподобный объём.
|
||||
// Замкнутый кубический сплайн через 4 точки даёт гладкий вогнутый контур, вписанный
|
||||
// в габаритный квадрат 20×20 (площадь профиля ≈135 мм²). Проверяем, что тело построено
|
||||
// и объём положителен и не превышает габарит профиля (площадь ≤ 400 → V ≤ 2000).
|
||||
var v = (await _query.GetPartInfoAsync()).Volume;
|
||||
Assert.InRange(v, 200 * 5 * 0.8, 314 * 5 * 1.2);
|
||||
Assert.InRange(v, 50 * 5, 400 * 5);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
@@ -125,4 +126,137 @@ public sealed partial class PartModeler
|
||||
/// <summary>Завершить редактирование эскиза.</summary>
|
||||
public Task CloseSketchAsync(int sketchId, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => CloseSketchCore(RequireSketch(sketchId)), ct);
|
||||
|
||||
// ---- Новые примитивы (пакет A) -----------------------------------------
|
||||
|
||||
private const short SplineOrder = 4; // порядок NURBS (степень+1); 4 = кубический сплайн
|
||||
private const int PointStyle = 0; // системный стиль точки (см. SDK pstyles)
|
||||
|
||||
/// <summary>Добавить дугу по трём точкам (начало, точка на дуге, конец) в открытый эскиз.</summary>
|
||||
public Task AddArc3PointsAsync(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksArcBy3Points(x1, y1, x2, y2, x3, y3, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksArcBy3Points вернул 0 (дуга не создана — три точки коллинеарны?).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить дугу по центру, радиусу и углам (градусы) в открытый эскиз.</summary>
|
||||
public Task AddArcByAngleAsync(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePositive(radius, nameof(radius));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
var dir = SketchGeometry.ArcDirection(counterClockwise);
|
||||
if (editor.ksArcByAngle(centerX, centerY, radius, startAngle, endAngle, dir, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksArcByAngle вернул 0 (дуга не создана).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить эллипс: центр, полуоси A/B (мм), угол наклона большой оси к X (градусы).</summary>
|
||||
public Task AddEllipseAsync(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePositive(semiMajor, nameof(semiMajor));
|
||||
SketchGeometry.RequirePositive(semiMinor, nameof(semiMinor));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
ksEllipseParam? p = null;
|
||||
try
|
||||
{
|
||||
p = NewParam<ksEllipseParam>(StructType2DEnum.ko_EllipseParam);
|
||||
// ВНИМАНИЕ: interop генерирует свойства A/B с ЗАГЛАВНОЙ (не a/b как в SDK-доках). Регистр в C# важен.
|
||||
p.xc = centerX; p.yc = centerY;
|
||||
p.A = semiMajor; p.B = semiMinor;
|
||||
p.angle = angle; p.style = MainLineStyle;
|
||||
if (editor.ksEllipse(p) == 0)
|
||||
throw new InvalidOperationException("ksEllipse вернул 0 (эллипс не создан).");
|
||||
}
|
||||
finally { ReleaseCom(p); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить ломаную (цепочку отрезков) по списку точек. closed замыкает последнюю с первой.</summary>
|
||||
public Task AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
// closed-ломаная из 2 точек не образует площадь — требуем минимум 3.
|
||||
SketchGeometry.RequirePoints(points, closed ? 3 : 2, nameof(points));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
for (int i = 0; i + 1 < points.Count; i++)
|
||||
Seg(editor, points[i], points[i + 1]);
|
||||
if (closed)
|
||||
Seg(editor, points[^1], points[0]);
|
||||
|
||||
static void Seg(ksDocument2D ed, (double x, double y) a, (double x, double y) b)
|
||||
{
|
||||
if (ed.ksLineSeg(a.x, a.y, b.x, b.y, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg вернул 0 (сегмент ломаной не создан).");
|
||||
}
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить правильный многоугольник: центр, vertexCount вершин, radius (мм),
|
||||
/// inscribed=true — вершины на окружности (вписанный), false — стороны касаются (описанный),
|
||||
/// angle — поворот первой вершины (градусы).</summary>
|
||||
public Task AddPolygonAsync(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequireVertexCount(vertexCount);
|
||||
SketchGeometry.RequirePositive(radius, nameof(radius));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
ksRegularPolygonParam? p = null;
|
||||
try
|
||||
{
|
||||
p = NewParam<ksRegularPolygonParam>(StructType2DEnum.ko_RegularPolygonParam);
|
||||
p.count = vertexCount; p.xc = centerX; p.yc = centerY;
|
||||
p.radius = radius; p.ang = angle;
|
||||
p.describe = SketchGeometry.PolygonDescribe(inscribed);
|
||||
p.style = MainLineStyle;
|
||||
if (editor.ksRegularPolygon(p, 0) == 0)
|
||||
throw new InvalidOperationException("ksRegularPolygon вернул 0 (многоугольник не создан).");
|
||||
}
|
||||
finally { ReleaseCom(p); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить сплайн (кубический NURBS, порядок 4) через список точек. closed замыкает кривую.</summary>
|
||||
public Task AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePoints(points, 2, nameof(points));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksNurbs(SplineOrder, closed, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksNurbs вернул 0 (сплайн не открыт).");
|
||||
// ksNurbs открыл составной объект — ksEndObj ОБЯЗАТЕЛЕН в любом исходе, иначе редактор «застрянет».
|
||||
var completed = false;
|
||||
try
|
||||
{
|
||||
foreach (var (x, y) in points)
|
||||
{
|
||||
ksNurbsPointParam? np = null;
|
||||
try
|
||||
{
|
||||
np = NewParam<ksNurbsPointParam>(StructType2DEnum.ko_NurbsPointParam);
|
||||
np.x = x; np.y = y; np.weight = 1.0;
|
||||
if (editor.ksNurbsPoint(np) == 0)
|
||||
throw new InvalidOperationException("ksNurbsPoint вернул 0 (узел сплайна не добавлен).");
|
||||
}
|
||||
finally { ReleaseCom(np); }
|
||||
}
|
||||
completed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Закрываем составной объект всегда; ошибку завершения сообщаем только на успешном пути,
|
||||
// чтобы не подменить исходное исключение из цикла.
|
||||
var end = editor.ksEndObj();
|
||||
if (completed && end == 0)
|
||||
throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён).");
|
||||
}
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить точку в открытый эскиз (опорная точка для построений).</summary>
|
||||
public Task AddPointAsync(int sketchId, double x, double y, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksPoint(x, y, PointStyle) == 0)
|
||||
throw new InvalidOperationException("ksPoint вернул 0 (точка не создана).");
|
||||
}, ct);
|
||||
}
|
||||
|
||||
@@ -80,4 +80,73 @@ public sealed class SketchTools(KompasSession session, PartModeler modeler)
|
||||
await modeler.CloseSketchAsync(sketchId);
|
||||
return "Эскиз закрыт.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_arc_3points")]
|
||||
[Description("Добавить дугу по трём точкам (начало, промежуточная точка на дуге, конец) в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddArc3Points(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddArc3PointsAsync(sketchId, x1, y1, x2, y2, x3, y3);
|
||||
return "Дуга по 3 точкам добавлена.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_arc")]
|
||||
[Description("Добавить дугу по центру, радиусу и углам (в градусах, от оси X) в открытый эскиз. counterClockwise=true — против часовой стрелки, false — по часовой. Координаты центра — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddArc(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddArcByAngleAsync(sketchId, centerX, centerY, radius, startAngle, endAngle, counterClockwise);
|
||||
return "Дуга добавлена.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_ellipse")]
|
||||
[Description("Добавить эллипс в открытый эскиз: центр, большая полуось semiMajor, малая полуось semiMinor (мм), угол наклона большой оси к X (градусы, по умолчанию 0).")]
|
||||
public async Task<string> AddEllipse(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddEllipseAsync(sketchId, centerX, centerY, semiMajor, semiMinor, angle);
|
||||
return "Эллипс добавлен.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_polyline")]
|
||||
[Description("Добавить ломаную — цепочку прямых отрезков по списку точек — в открытый эскиз. closed=true замыкает последнюю точку с первой (тогда нужно минимум 3 точки). Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddPolyline(int sketchId, SketchPoint[] points, bool closed = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPolylineAsync(sketchId, Map(points), closed);
|
||||
return $"Ломаная из {points.Length} точек добавлена{(closed ? " (замкнута)" : "")}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_polygon")]
|
||||
[Description("Добавить правильный многоугольник в открытый эскиз: центр, vertexCount вершин (>=3), radius (мм). inscribed=true — вершины лежат на окружности радиуса (вписанный в окружность); false — стороны касаются окружности (описанный). angle — поворот первой вершины (градусы, по умолчанию 0).")]
|
||||
public async Task<string> AddPolygon(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPolygonAsync(sketchId, centerX, centerY, vertexCount, radius, inscribed, angle);
|
||||
return $"Правильный многоугольник ({vertexCount} вершин) добавлен.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_spline")]
|
||||
[Description("Добавить сплайн (кубический NURBS) через список точек в открытый эскиз. closed=true замыкает кривую. Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddSpline(int sketchId, SketchPoint[] points, bool closed = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddSplineAsync(sketchId, Map(points), closed);
|
||||
return $"Сплайн по {points.Length} точкам добавлен{(closed ? " (замкнут)" : "")}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_point")]
|
||||
[Description("Добавить точку в открытый эскиз (опорная точка для построений). Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddPoint(int sketchId, double x, double y)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPointAsync(sketchId, x, y);
|
||||
return "Точка добавлена.";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<(double x, double y)> Map(SketchPoint[] points)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(points);
|
||||
return points.Select(p => (p.X, p.Y)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
using Kompas.Mcp.Core.Query;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: новые примитивы эскиза (профиль → выдавливание → объём).</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class SketchPrimitivesTests
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
private readonly PartModeler _modeler;
|
||||
private readonly QueryService _query;
|
||||
|
||||
public SketchPrimitivesTests(KompasFixture fx)
|
||||
{
|
||||
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
||||
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
|
||||
_query = new QueryService(fx.Session, fx.Dispatcher);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Arc3Points_semicircle_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Верхний полукруг R10: дуга (10,0)→(0,10)→(-10,0) + хорда обратно.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddArc3PointsAsync(s, 10, 0, 0, 10, -10, 0);
|
||||
await _modeler.AddLineAsync(s, -10, 0, 10, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10²/2·5 ≈ 785.4
|
||||
var expected = Math.PI * 100 / 2 * 5;
|
||||
Assert.InRange(v, expected * 0.95, expected * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ArcByAngle_quarter_sector_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Сектор 90° R10 в первом квадранте: дуга (10,0)→(0,10) + два радиуса к центру.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddArcByAngleAsync(s, 0, 0, 10, 0, 90, counterClockwise: true);
|
||||
await _modeler.AddLineAsync(s, 0, 10, 0, 0);
|
||||
await _modeler.AddLineAsync(s, 0, 0, 10, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10²/4·5 ≈ 392.7
|
||||
var expected = Math.PI * 100 / 4 * 5;
|
||||
Assert.InRange(v, expected * 0.95, expected * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ellipse_extrudes_to_expected_volume()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddEllipseAsync(s, 0, 0, semiMajor: 10, semiMinor: 5);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 4);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10·5·4 ≈ 628.3
|
||||
var expected = Math.PI * 10 * 5 * 4;
|
||||
Assert.InRange(v, expected * 0.97, expected * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Polyline_closed_triangle_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Прямоугольный треугольник (0,0)-(20,0)-(0,15), площадь 150.
|
||||
var pts = new (double x, double y)[] { (0, 0), (20, 0), (0, 15) };
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddPolylineAsync(s, pts, closed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // 150·5 = 750
|
||||
Assert.InRange(v, 750 * 0.97, 750 * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Polygon_hexagon_extrudes_to_expected_volume()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Правильный 6-угольник, вписанный в окружность R10: площадь = 0.5·6·R²·sin(60°) ≈ 259.8.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddPolygonAsync(s, 0, 0, vertexCount: 6, radius: 10, inscribed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume;
|
||||
var area = 0.5 * 6 * 100 * Math.Sin(2 * Math.PI / 6);
|
||||
Assert.InRange(v, area * 5 * 0.95, area * 5 * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Spline_closed_loop_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Замкнутый сплайн по 4 точкам вокруг начала координат → выпуклая «капля».
|
||||
var pts = new (double x, double y)[] { (10, 0), (0, 10), (-10, 0), (0, -10) };
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddSplineAsync(s, pts, closed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
// Замкнутый кубический сплайн через 4 точки даёт гладкий вогнутый контур, вписанный
|
||||
// в габаритный квадрат 20×20 (площадь профиля ≈135 мм²). Проверяем, что тело построено
|
||||
// и объём положителен и не превышает габарит профиля (площадь ≤ 400 → V ≤ 2000).
|
||||
var v = (await _query.GetPartInfoAsync()).Volume;
|
||||
Assert.InRange(v, 50 * 5, 400 * 5);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Point_does_not_break_sketch()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Окружность R10 + точка в центре; выдавливание должно дать цилиндр без искажений.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddCircleAsync(s, 0, 0, 10);
|
||||
await _modeler.AddPointAsync(s, 0, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·100·5 ≈ 1570.8
|
||||
var expected = Math.PI * 100 * 5;
|
||||
Assert.InRange(v, expected * 0.97, expected * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user