feat(feature): операция Оболочка (shell)

ShellAsync (o3d_shellOperation): удаление граней по индексам + толщина стенки,
thinType=!outward. SelectFaceByIndex в ядре, инструмент shell в FeatureTools.
Валидация: thickness>0, дедупликация индексов, проверка диапазона до мутации.
Integration-тест: коробка 40x30x20 → оболочка 2мм → 7152 мм³ (±5%).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 22:28:03 +03:00
parent b6ee39a914
commit 115fcb8759
6 changed files with 291 additions and 5 deletions
@@ -196,6 +196,40 @@ public sealed partial class PartModeler
return id;
}
/// <summary>Превратить тело в оболочку: удалить (открыть) грани с индексами faceIndices,
/// задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется),
/// true — наружу. Возвращает id операции.</summary>
public Task<int> ShellAsync(IReadOnlyList<int> faceIndices, double thickness, bool outward = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
if (thickness <= 0) throw new ArgumentOutOfRangeException(nameof(thickness), "Толщина должна быть > 0.");
ArgumentNullException.ThrowIfNull(faceIndices);
var indices = faceIndices.Distinct().ToList();
if (indices.Count == 0) throw new ArgumentException("Нужна хотя бы одна удаляемая грань.", nameof(faceIndices));
var part = GetTopPart();
// Проверяем диапазон ВСЕХ индексов до мутации модели.
var faces = indices.Select(i => SelectFaceByIndex(part, i)).ToList();
var entity = part.NewEntity((short)Obj3dType.o3d_shellOperation) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_shellOperation) вернул null.");
var def = entity.GetDefinition() as ksShellDefinition
?? throw new InvalidOperationException("GetDefinition() оболочки вернул не ksShellDefinition.");
var arr = def.FaceArray() as ksEntityCollection
?? throw new InvalidOperationException("FaceArray() оболочки вернул не ksEntityCollection.");
foreach (var f in faces) arr.Add(f);
def.thickness = thickness;
def.thinType = !outward; // true=внутрь, false=наружу
if (!entity.Create())
throw new InvalidOperationException(
"Create() оболочки вернул FALSE (толщина больше локального радиуса/стенки или несовместимая топология?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// <summary>Перестроить документ.</summary>
public Task RebuildAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>