fix: правки hole по ревью Codex
- направление сверления — по факту убыли объёма (Update=TRUE даже при сверлении в воздух на сквозном отверстии); TryDirection + CurrentVolume (API5 МЦХ) — P2-баг, вскрыт тестом - валидация double.IsFinite для координат/диаметра/глубины (Infinity проходил мимо >0) — P2 - откат точки-сироты и объекта отверстия при неуспехе (IFeature7.Delete) + RebuildDocument — P2 - тест Hole_through_bottom_face_uses_direction_fallback (нижняя грань) — P3 - 104 теста зелёных
This commit is contained in:
@@ -38,11 +38,18 @@ IHoleDisposal d = (IHoleDisposal)hole; // QI
|
||||
d.BaseSurface = (IModelObject)face; // обязательна грань
|
||||
d.Perpendicular = true;
|
||||
d.AssociationVertex = (IModelObject)pt; // центр = созданная точка
|
||||
d.Direction = true; // в тело; при FALSE — пробуем обратное
|
||||
hole.Update(); // повтор с Direction=false при отказе
|
||||
doc3d.RebuildDocument();
|
||||
d.Direction = true; // подбор по факту (см. ниже)
|
||||
hole.Update(); doc3d.RebuildDocument(); // проверяем убыль объёма; иначе Direction=false
|
||||
```
|
||||
|
||||
**Направление сверления (важно):** нормаль грани ориентирована непредсказуемо (тот же класс
|
||||
проблем, что у boss/cut в OPEN_QUESTIONS), а при **сквозном** отверстии `Update()` возвращает
|
||||
`TRUE` даже когда сверлит «в воздух» (наружу тела → удалено 0 материала). Поэтому направление
|
||||
подбираем **по факту удаления материала**: строим с `Direction=true`, перестраиваем, сравниваем
|
||||
объём (API5 `CalcMassInertiaProperties`, согласован с API7-геометрией после `RebuildDocument`);
|
||||
если объём не убыл — `Direction=false` и повтор; если оба не убавили — ошибка. Только успех
|
||||
Update недостаточен.
|
||||
|
||||
**Размещение** (главная неочевидность, решена): у `IHole3D` нет свойства позиции — оно в
|
||||
`IHoleDisposal` (`BaseSurface` = грань + `AssociationVertex` = точка/вершина). Самый надёжный
|
||||
способ задать центр по мировым координатам — создать `IPoint3D` (`ksPParamCoord` + X/Y/Z) и
|
||||
@@ -65,8 +72,9 @@ doc3d.RebuildDocument();
|
||||
## Реализация
|
||||
|
||||
- **`src/Kompas.Mcp.Core/Modeling/HoleService.cs`** (новый): `HoleAsync(x,y,z,diameter,depth,throughAll,ct)`
|
||||
на STA-потоке; валидация `diameter>0`, `depth>0` (если не throughAll); `FindFaceAtPoint`
|
||||
(копия паттерна из `FaceEditService`); авто-направление с повтором.
|
||||
на STA-потоке; валидация `double.IsFinite` для координат/диаметра/глубины + `>0`; `FindFaceAtPoint`
|
||||
(копия паттерна из `FaceEditService`); подбор направления по убыли объёма (`TryDirection`);
|
||||
при неуспехе — откат точки и объекта отверстия (`IFeature7.Delete`), затем `RebuildDocument`.
|
||||
- **DI:** `AddSingleton<HoleService>()` в `Program.cs`.
|
||||
- **Инструмент** `hole` в `FeatureTools.cs` (конструктор получает `HoleService`).
|
||||
- Транзитные RCW не освобождаем точечно — консистентно (долг v2-2).
|
||||
@@ -77,3 +85,5 @@ doc3d.RebuildDocument();
|
||||
удалён цилиндр π·25·20 ≈ 1570.8 мм³; `InRange(before-after, ±5%)`. ✓
|
||||
2. **Глухое**: та же коробка → `hole(0,0,20, Ø8, depth=10)` → удалён π·16·10 ≈ 502.7 мм³;
|
||||
`InRange(before-after, ±10%)` (торец плоский, погрешность модели). ✓
|
||||
3. **Нижняя грань (подбор направления)**: та же коробка → `hole(0,0,0, Ø10, throughAll)` со стороны
|
||||
нижней грани → корректное направление внутрь (+Z) подбирается по убыли объёма; удалён π·25·20. ✓
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
using KompasAPI7;
|
||||
|
||||
@@ -31,8 +32,12 @@ public sealed class HoleService
|
||||
|
||||
private void HoleCore(double x, double y, double z, double diameter, double depth, bool throughAll)
|
||||
{
|
||||
if (!(diameter > 0)) throw new ArgumentOutOfRangeException(nameof(diameter), "Диаметр должен быть > 0.");
|
||||
if (!throughAll && !(depth > 0)) throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть > 0 (или throughAll).");
|
||||
if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z))
|
||||
throw new ArgumentException("Координаты точки отверстия должны быть конечными числами.");
|
||||
if (!double.IsFinite(diameter) || diameter <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(diameter), "Диаметр должен быть конечным и > 0.");
|
||||
if (!throughAll && (!double.IsFinite(depth) || depth <= 0))
|
||||
throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть конечной и > 0 (или throughAll).");
|
||||
|
||||
if (_session.Application.ActiveDocument is not IKompasDocument3D doc3d)
|
||||
throw new InvalidOperationException("Активный документ не является 3D-документом.");
|
||||
@@ -54,30 +59,68 @@ public sealed class HoleService
|
||||
if (!pt.Update())
|
||||
throw new InvalidOperationException("Update() точки размещения вернул FALSE.");
|
||||
|
||||
var hole = container.Holes3D.Add()
|
||||
?? throw new InvalidOperationException("Holes3D.Add() вернул null.");
|
||||
hole.HoleType = ksHoleTypeEnum.ksHTBase;
|
||||
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;
|
||||
var before = CurrentVolume();
|
||||
|
||||
var disp = (IHoleDisposal)hole;
|
||||
disp.BaseSurface = (IModelObject)face;
|
||||
disp.Perpendicular = true;
|
||||
disp.AssociationVertex = (IModelObject)pt;
|
||||
disp.Direction = true;
|
||||
|
||||
if (!hole.Update())
|
||||
// Если отверстие не построится — удаляем уже созданную точку (и сам объект отверстия),
|
||||
// чтобы не засорять дерево «сиротами» при неуспехе.
|
||||
IHole3D? hole = null;
|
||||
try
|
||||
{
|
||||
disp.Direction = false;
|
||||
if (!hole.Update())
|
||||
throw new InvalidOperationException("Update() отверстия вернул FALSE для обоих направлений.");
|
||||
}
|
||||
hole = container.Holes3D.Add()
|
||||
?? throw new InvalidOperationException("Holes3D.Add() вернул null.");
|
||||
hole.HoleType = ksHoleTypeEnum.ksHTBase;
|
||||
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;
|
||||
|
||||
var disp = (IHoleDisposal)hole;
|
||||
disp.BaseSurface = (IModelObject)face;
|
||||
disp.Perpendicular = true;
|
||||
disp.AssociationVertex = (IModelObject)pt;
|
||||
|
||||
// Направление сверления подбираем ПО ФАКТУ удаления материала: нормаль грани может
|
||||
// смотреть в любую сторону, а при сквозном отверстии Update() возвращает TRUE даже когда
|
||||
// сверлит «в воздух» (наружу тела) — поэтому одного флага успеха Update недостаточно.
|
||||
if (!TryDirection(hole, disp, doc3d, true, before) &&
|
||||
!TryDirection(hole, disp, doc3d, false, before))
|
||||
throw new InvalidOperationException(
|
||||
"Отверстие не удалило материал ни в одном направлении (точка вне тела или диаметр/глубина несовместимы?).");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort откат: сначала объект отверстия, затем точка размещения.
|
||||
try { hole?.Owner?.Delete(); } catch { /* лучшее усилие */ }
|
||||
try { pt.Owner?.Delete(); } catch { /* лучшее усилие */ }
|
||||
doc3d.RebuildDocument();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Построить отверстие в заданном направлении и подтвердить, что материал убыл
|
||||
/// (объём уменьшился). Возвращает true только при фактическом удалении материала.</summary>
|
||||
private bool TryDirection(IHole3D hole, IHoleDisposal disp, IKompasDocument3D doc3d, bool direction, double before)
|
||||
{
|
||||
disp.Direction = direction;
|
||||
if (!hole.Update()) return false;
|
||||
doc3d.RebuildDocument();
|
||||
return CurrentVolume() < before - 1e-6;
|
||||
}
|
||||
|
||||
/// <summary>Текущий объём детали (мм³) через API5 МЦХ — согласован с геометрией API7 после
|
||||
/// RebuildDocument (как в QueryService).</summary>
|
||||
private double CurrentVolume()
|
||||
{
|
||||
const int mixMmKg = 0x1 | 0x10; // ST_MIX_MM | ST_MIX_KG
|
||||
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа (API5).");
|
||||
var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
|
||||
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
|
||||
var mass = part.CalcMassInertiaProperties(mixMmKg) as ksMassInertiaParam
|
||||
?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null.");
|
||||
return mass.v;
|
||||
}
|
||||
|
||||
private static IFace? FindFaceAtPoint(IPart7 part, double x, double y, double z)
|
||||
|
||||
@@ -71,4 +71,29 @@ public sealed class HoleTests
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Hole_through_bottom_face_uses_direction_fallback()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Коробка 40×40×20, нижняя грань на Z=0 (нормаль смотрит наружу, −Z).
|
||||
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; // 32000
|
||||
|
||||
// Сверлим со стороны НИЖНЕЙ грани (0,0,0): корректное направление в тело — внутрь (+Z),
|
||||
// т.е. противоположно нормали → проверяет авто-подбор направления (fallback).
|
||||
await _holes.HoleAsync(0, 0, 0, diameter: 10, depth: 0, throughAll: true);
|
||||
var after = (await _query.GetPartInfoAsync()).Volume;
|
||||
|
||||
var removed = Math.PI * 25 * 20;
|
||||
Assert.True(after < before, "Отверстие с нижней грани должно построиться (fallback направления).");
|
||||
Assert.InRange(before - after, removed * 0.95, removed * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user