feat: типы отверстий — цековка и зенковка (hole_counterbore, hole_countersink) через API7
- HoleCore отрефакторен: параметр holeType + Action<IHole3D> configure (настройка HoleParameters) - CounterboreHoleAsync (ksHTCounterbore + ISpotfacingHoleParameters: SpotfacingDiameter/Depth) - CountersinkHoleAsync (ksHTCountersinking + ICountersinkHoleParameters: ksCTDiameterAngle, Diameter/Angle) - переиспользуется размещение/подбор направления/откат из базового hole - инструменты hole_counterbore, hole_countersink; интеграционные тесты (объём ±10% / больше базы) - 69 инструментов, 118 тестов; спек docs/superpowers/specs/2026-05-27-hole-types-design.md
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Дизайн: типы отверстий — цековка и зенковка (расширение hole)
|
||||
|
||||
**Дата:** 2026-05-27
|
||||
**Статус:** реализовано и проверено (тесты зелёные); на ревью (ревьюер — Codex)
|
||||
|
||||
## Цель
|
||||
|
||||
Расширить `hole` (простое цилиндрическое, `ksHTBase`) двумя ходовыми типами под крепёж:
|
||||
**цековка** (`hole_counterbore`, под винт с цилиндрической головкой) и **зенковка**
|
||||
(`hole_countersink`, под винт с потайной головкой). Через API7 (как базовое отверстие).
|
||||
|
||||
## Сигнатуры (рефлексия interop)
|
||||
|
||||
```
|
||||
IHole3D.HoleType = ksHoleTypeEnum: ksHTBase=0, ksHTCounterbore=1, ksHTCountersinking=2, ksHTConic=4
|
||||
IHole3D.HoleParameters : IKompasAPIObject // приводится к типу по HoleType (после установки HoleType)
|
||||
ISpotfacingHoleParameters (цековка): SpotfacingDiameter, SpotfacingDepth (мм)
|
||||
ICountersinkHoleParameters (зенковка): CountersinkType (ksCountersinkTypeEnum), CountersinkDiameter, CountersinkAngle (°), CountersinkDepth
|
||||
ksCountersinkTypeEnum: ksCTDiameterAngle=0, ksCTDepthAngle=1, ksCTDiameterDepth=2
|
||||
```
|
||||
|
||||
## MCP-инструменты (группа Feature)
|
||||
|
||||
| Инструмент | Параметры | Поведение |
|
||||
|---|---|---|
|
||||
| `hole_counterbore` | `x,y,z`, `diameter`, `spotfaceDiameter`, `spotfaceDepth`, `depth=0`, `throughAll=false` | Отверстие Ø`diameter` + цилиндрическая цековка Ø`spotfaceDiameter`×`spotfaceDepth` сверху. |
|
||||
| `hole_countersink` | `x,y,z`, `diameter`, `sinkDiameter`, `sinkAngle`, `depth=0`, `throughAll=false` | Отверстие Ø`diameter` + коническая зенковка Ø`sinkDiameter` под углом `sinkAngle`° (тип `ksCTDiameterAngle`). |
|
||||
|
||||
**Решения:** конический тип (`ksHTConic`) и резьба (`ShowThread`/`IThread`) — позже. Зенковка задаётся
|
||||
парой диаметр+угол (`ksCTDiameterAngle`) — самый привычный ввод.
|
||||
|
||||
## Реализация
|
||||
|
||||
- **`HoleCore` отрефакторен**: добавлены параметры `ksHoleTypeEnum holeType` и `Action<IHole3D>? configure`
|
||||
(конфигуратор `HoleParameters` после установки `HoleType`). `HoleAsync` → `ksHTBase, null`.
|
||||
Вся остальная логика (размещение через `IHoleDisposal`+`Points3D`, подбор направления по убыли
|
||||
объёма, откат «сирот») переиспользуется.
|
||||
- **`CounterboreHoleAsync`**: валидация (`spotDiameter>diameter`, `spotDepth>0`, finite) →
|
||||
`HoleCore(..., ksHTCounterbore, hole => set ISpotfacingHoleParameters)`.
|
||||
- **`CountersinkHoleAsync`**: валидация (`sinkDiameter>diameter`, `sinkAngle∈(0;180)`, finite) →
|
||||
`HoleCore(..., ksHTCountersinking, hole => set ICountersinkHoleParameters: type=ksCTDiameterAngle)`.
|
||||
- **Инструменты** `hole_counterbore`, `hole_countersink` в `FeatureTools.cs`.
|
||||
|
||||
## Тестирование (Integration, `HoleTests`)
|
||||
|
||||
- **Цековка**: коробка 40×40×20 → `hole_counterbore(0,0,20, Ø10, spot Ø20×5, throughAll)` →
|
||||
удалено = база (π·25·20≈1571) + кольцо цековки (π·75·5≈1178) ≈ 2749 (`±10%`); и
|
||||
`removed > базовый цилиндр·1.1` (уширение реально вырезано). ✓
|
||||
- **Зенковка**: коробка → `hole_countersink(0,0,20, Ø8, sink Ø16, 90°, throughAll)` →
|
||||
`removed > база Ø8 (π·16·20≈1005)` и `< база·2` (конус добавил материал, в разумных пределах;
|
||||
точный объём зависит от трактовки угла). ✓
|
||||
@@ -28,9 +28,49 @@ public sealed class HoleService
|
||||
/// diameter — диаметр (мм). throughAll — сквозное (depth игнорируется), иначе на глубину depth (мм).
|
||||
/// </summary>
|
||||
public Task HoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => HoleCore(x, y, z, diameter, depth, throughAll), ct);
|
||||
=> _dispatcher.InvokeAsync(() => HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTBase, null), ct);
|
||||
|
||||
private void HoleCore(double x, double y, double z, double diameter, double depth, bool throughAll)
|
||||
/// <summary>Отверстие с цековкой (цилиндрическим уширением сверху, под винт с цилиндрической
|
||||
/// головкой): основное отверстие diameter + цековка spotDiameter×spotDepth.</summary>
|
||||
public Task CounterboreHoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll,
|
||||
double spotDiameter, double spotDepth, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (!double.IsFinite(spotDiameter) || spotDiameter <= diameter)
|
||||
throw new ArgumentOutOfRangeException(nameof(spotDiameter), "Диаметр цековки должен быть больше диаметра отверстия.");
|
||||
if (!double.IsFinite(spotDepth) || spotDepth <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(spotDepth), "Глубина цековки должна быть > 0.");
|
||||
HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTCounterbore, hole =>
|
||||
{
|
||||
var p = hole.HoleParameters as ISpotfacingHoleParameters
|
||||
?? throw new InvalidOperationException("HoleParameters не приводится к ISpotfacingHoleParameters.");
|
||||
p.SpotfacingDiameter = spotDiameter;
|
||||
p.SpotfacingDepth = spotDepth;
|
||||
});
|
||||
}, ct);
|
||||
|
||||
/// <summary>Отверстие с зенковкой (коническим уширением сверху, под винт с потайной головкой):
|
||||
/// основное отверстие diameter + зенковка sinkDiameter под углом sinkAngle (градусы, конус).</summary>
|
||||
public Task CountersinkHoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll,
|
||||
double sinkDiameter, double sinkAngle, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (!double.IsFinite(sinkDiameter) || sinkDiameter <= diameter)
|
||||
throw new ArgumentOutOfRangeException(nameof(sinkDiameter), "Диаметр зенковки должен быть больше диаметра отверстия.");
|
||||
if (!double.IsFinite(sinkAngle) || sinkAngle <= 0 || sinkAngle >= 180)
|
||||
throw new ArgumentOutOfRangeException(nameof(sinkAngle), "Угол зенковки должен быть в (0; 180) градусов.");
|
||||
HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTCountersinking, hole =>
|
||||
{
|
||||
var p = hole.HoleParameters as ICountersinkHoleParameters
|
||||
?? throw new InvalidOperationException("HoleParameters не приводится к ICountersinkHoleParameters.");
|
||||
p.CountersinkType = ksCountersinkTypeEnum.ksCTDiameterAngle; // задаём диаметр + угол
|
||||
p.CountersinkDiameter = sinkDiameter;
|
||||
p.CountersinkAngle = sinkAngle;
|
||||
});
|
||||
}, ct);
|
||||
|
||||
private void HoleCore(double x, double y, double z, double diameter, double depth, bool throughAll,
|
||||
ksHoleTypeEnum holeType, Action<IHole3D>? configure)
|
||||
{
|
||||
if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z))
|
||||
throw new ArgumentException("Координаты точки отверстия должны быть конечными числами.");
|
||||
@@ -68,13 +108,14 @@ public sealed class HoleService
|
||||
{
|
||||
hole = container.Holes3D.Add()
|
||||
?? throw new InvalidOperationException("Holes3D.Add() вернул null.");
|
||||
hole.HoleType = ksHoleTypeEnum.ksHTBase;
|
||||
hole.HoleType = holeType;
|
||||
hole.Diameter = diameter;
|
||||
hole.DepthType = throughAll ? ksDepthTypeEnum.ksDTReachThrough : ksDepthTypeEnum.ksDTValue;
|
||||
if (!throughAll) hole.Depth = depth;
|
||||
hole.EndFaceType = ksEndFaceTypeEnum.ksEFFlat;
|
||||
hole.Axis = false;
|
||||
hole.ShowThread = false;
|
||||
configure?.Invoke(hole); // параметры цековки/зенковки (после установки HoleType)
|
||||
|
||||
var disp = (IHoleDisposal)hole;
|
||||
disp.BaseSurface = (IModelObject)face;
|
||||
|
||||
@@ -212,6 +212,38 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler, Hol
|
||||
return $"Уклон {angle}° создан на {count} гранях (нейтральная плоскость {neutralPlane}), id={id}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "hole_counterbore")]
|
||||
[Description("Отверстие с ЦЕКОВКОЙ (цилиндрическое уширение сверху — под винт с цилиндрической головкой) на грани в точке (x,y,z — центр). diameter — диаметр отверстия (мм). depth/throughAll — как у hole. spotfaceDiameter — диаметр цековки (мм, больше diameter), spotfaceDepth — глубина цековки (мм). Направление в тело — авто.")]
|
||||
public async Task<string> HoleCounterbore(
|
||||
double x, double y, double z,
|
||||
[Description("Диаметр отверстия, мм")] double diameter,
|
||||
[Description("Диаметр цековки, мм (> diameter)")] double spotfaceDiameter,
|
||||
[Description("Глубина цековки, мм")] double spotfaceDepth,
|
||||
[Description("Глубина отверстия, мм (если throughAll=false)")] double depth = 0,
|
||||
[Description("Сквозное отверстие")] bool throughAll = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await holes.CounterboreHoleAsync(x, y, z, diameter, depth, throughAll, spotfaceDiameter, spotfaceDepth);
|
||||
var kind = throughAll ? "сквозное" : $"глубина {depth} мм";
|
||||
return $"Отверстие с цековкой Ø{diameter} (цековка Ø{spotfaceDiameter}×{spotfaceDepth}, {kind}) создано в точке ({x}, {y}, {z}).";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "hole_countersink")]
|
||||
[Description("Отверстие с ЗЕНКОВКОЙ (коническое уширение сверху — под винт с потайной головкой) на грани в точке (x,y,z — центр). diameter — диаметр отверстия (мм). depth/throughAll — как у hole. sinkDiameter — диаметр зенковки (мм, больше diameter), sinkAngle — угол конуса зенковки (градусы, 0..180; типично 90). Направление в тело — авто.")]
|
||||
public async Task<string> HoleCountersink(
|
||||
double x, double y, double z,
|
||||
[Description("Диаметр отверстия, мм")] double diameter,
|
||||
[Description("Диаметр зенковки, мм (> diameter)")] double sinkDiameter,
|
||||
[Description("Угол конуса зенковки, градусы (0..180, типично 90)")] double sinkAngle,
|
||||
[Description("Глубина отверстия, мм (если throughAll=false)")] double depth = 0,
|
||||
[Description("Сквозное отверстие")] bool throughAll = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await holes.CountersinkHoleAsync(x, y, z, diameter, depth, throughAll, sinkDiameter, sinkAngle);
|
||||
var kind = throughAll ? "сквозное" : $"глубина {depth} мм";
|
||||
return $"Отверстие с зенковкой Ø{diameter} (зенковка Ø{sinkDiameter}, угол {sinkAngle}°, {kind}) создано в точке ({x}, {y}, {z}).";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "rebuild")]
|
||||
[Description("Перестроить активный документ.")]
|
||||
public async Task<string> Rebuild()
|
||||
|
||||
@@ -96,4 +96,55 @@ public sealed class HoleTests
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Counterbore_removes_base_plus_spotface()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddRectangleAsync(s, -20, -20, 20, 20);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 20);
|
||||
var before = (await _query.GetPartInfoAsync()).Volume;
|
||||
|
||||
// Ø10 сквозное + цековка Ø20×5 по центру верхней грани.
|
||||
await _holes.CounterboreHoleAsync(0, 0, 20, diameter: 10, depth: 0, throughAll: true,
|
||||
spotDiameter: 20, spotDepth: 5);
|
||||
var removed = before - (await _query.GetPartInfoAsync()).Volume;
|
||||
|
||||
// База: π·25·20 ≈ 1570.8; + кольцо цековки (π·100−π·25)·5 ≈ 1178 → итого ≈ 2749.
|
||||
var baseCyl = Math.PI * 25 * 20;
|
||||
var expected = Math.PI * 25 * 20 + Math.PI * (100 - 25) * 5;
|
||||
Assert.True(removed > baseCyl * 1.1, "Цековка должна удалить больше, чем простое отверстие.");
|
||||
Assert.InRange(removed, expected * 0.9, expected * 1.1);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Countersink_removes_base_plus_cone()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddRectangleAsync(s, -20, -20, 20, 20);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 20);
|
||||
var before = (await _query.GetPartInfoAsync()).Volume;
|
||||
|
||||
// Ø8 сквозное + зенковка Ø16 под 90° по центру верхней грани.
|
||||
await _holes.CountersinkHoleAsync(0, 0, 20, diameter: 8, depth: 0, throughAll: true,
|
||||
sinkDiameter: 16, sinkAngle: 90);
|
||||
var removed = before - (await _query.GetPartInfoAsync()).Volume;
|
||||
|
||||
// База Ø8 сквозь 20 ≈ 1005; зенковка-конус добавляет ~270 (точное зависит от трактовки угла).
|
||||
var baseCyl = Math.PI * 16 * 20;
|
||||
Assert.True(removed > baseCyl, "Зенковка должна удалить больше, чем простое отверстие Ø8.");
|
||||
Assert.True(removed < baseCyl * 2, "Объём зенковки в разумных пределах.");
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user