using System.Runtime.Versioning;
using Kompas6API5;
using Kompas6Constants3D;
namespace Kompas.Mcp.Core.Modeling;
/// PartModeler: формообразующие операции (выдавливание, вращение, скругление, фаска).
[SupportedOSPlatform("windows")]
public sealed partial class PartModeler
{
///
/// Выдавить эскиз. — вырезание вместо добавления материала.
/// Возвращает id операции.
///
/// Сквозное выдавливание/вырез через всю деталь в обе стороны
/// (для отверстий — гарантированно пробивает материал; игнорируется).
public Task ExtrudeAsync(
int sketchId, double depth, bool cut = false, bool forward = true,
double draftAngle = 0, bool throughAll = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
if (!throughAll && depth <= 0)
throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть > 0 (или используйте throughAll).");
var sketch = RequireSketch(sketchId);
CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией
var part = GetTopPart();
var type = cut ? Obj3dType.o3d_cutExtrusion : Obj3dType.o3d_bossExtrusion;
var entity = part.NewEntity((short)type) as ksEntity
?? throw new InvalidOperationException("NewEntity операции выдавливания вернул null.");
// Конкретные типы определений (без dynamic): ошибки связывания — на этапе компиляции.
if (cut)
{
var def = entity.GetDefinition() as ksCutExtrusionDefinition
?? throw new InvalidOperationException("GetDefinition() выреза вернул не ksCutExtrusionDefinition.");
if (throughAll)
{
def.directionType = (short)Direction_Type.dtBoth;
def.SetSideParam(true, (short)End_Type.etThroughAll, depth, draftAngle, false);
def.SetSideParam(false, (short)End_Type.etThroughAll, depth, draftAngle, false);
}
else
{
def.directionType = (short)Direction_Type.dtNormal;
def.SetSideParam(forward, (short)End_Type.etBlind, depth, draftAngle, false);
}
def.SetSketch(sketch.Entity);
}
else
{
if (throughAll)
throw new ArgumentException("throughAll поддерживается только для выреза (cut).", nameof(throughAll));
var def = entity.GetDefinition() as ksBossExtrusionDefinition
?? throw new InvalidOperationException("GetDefinition() выдавливания вернул не ksBossExtrusionDefinition.");
def.directionType = (short)Direction_Type.dtNormal;
def.SetSideParam(forward, (short)End_Type.etBlind, depth, draftAngle, false);
def.SetSketch(sketch.Entity);
}
if (!entity.Create())
throw new InvalidOperationException("Create() операции выдавливания вернул FALSE.");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
///
/// Вращать эскиз вокруг его осевой линии (добавьте её через AddAxis). —
/// вырезание вместо добавления материала. Угол — в градусах.
/// Возвращает id операции.
///
public Task RevolveAsync(
int sketchId, double angle = 360, bool cut = false, bool forward = true,
CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
if (angle is <= 0 or > 360)
throw new ArgumentOutOfRangeException(nameof(angle), "Угол вращения должен быть в (0; 360].");
var sketch = RequireSketch(sketchId);
CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией
var part = GetTopPart();
var type = cut ? Obj3dType.o3d_cutRotated : Obj3dType.o3d_bossRotated;
var entity = part.NewEntity((short)type) as ksEntity
?? throw new InvalidOperationException("NewEntity операции вращения вернул null.");
if (cut)
{
var def = entity.GetDefinition() as ksCutRotatedDefinition
?? throw new InvalidOperationException("GetDefinition() выреза вращением вернул не ksCutRotatedDefinition.");
def.directionType = (short)Direction_Type.dtNormal;
def.SetThinParam(false, 0, 0, 0);
def.SetSideParam(forward, angle);
def.SetSketch(sketch.Entity);
}
else
{
var def = entity.GetDefinition() as ksBossRotatedDefinition
?? throw new InvalidOperationException("GetDefinition() вращения вернул не ksBossRotatedDefinition.");
def.directionType = (short)Direction_Type.dtNormal;
def.toroidShapeType = false; // сфероид (сплошное тело), не тороид
def.SetThinParam(false, 0, 0, 0);
def.SetSideParam(forward, angle);
def.SetSketch(sketch.Entity);
}
if (!entity.Create())
throw new InvalidOperationException("Create() операции вращения вернул FALSE.");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
///
/// Скруглить ребро детали, найденное по точке (мировые координаты, мм), радиусом .
/// Возвращает id операции.
///
public Task FilletEdgeAsync(double x, double y, double z, double radius, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
return CreateFillet(part, SelectEdgeByPoint(part, x, y, z), radius);
}, ct);
/// Скруглить ребро детали по индексу из list_edges радиусом .
public Task FilletEdgeIndexAsync(int edgeIndex, double radius, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
return CreateFillet(part, SelectEdgeByIndex(part, edgeIndex), radius);
}, ct);
private int CreateFillet(ksPart part, ksEntity edge, double radius)
{
if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус скругления должен быть > 0.");
var entity = part.NewEntity((short)Obj3dType.o3d_fillet) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_fillet) вернул null.");
var def = entity.GetDefinition() as ksFilletDefinition
?? throw new InvalidOperationException("GetDefinition() скругления вернул не ksFilletDefinition.");
def.radius = radius;
def.tangent = false;
var arr = def.array() as ksEntityCollection
?? throw new InvalidOperationException("array() скругления вернул не ksEntityCollection.");
arr.Add(edge);
if (!entity.Create())
throw new InvalidOperationException("Create() скругления вернул FALSE (радиус слишком велик для ребра?).");
var id = _nextId++;
_features[id] = entity;
return id;
}
///
/// Снять фаску с ребра детали, найденного по точке (мировые координаты, мм), катетом .
/// Возвращает id операции.
///
public Task ChamferEdgeAsync(double x, double y, double z, double distance, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
return CreateChamfer(part, SelectEdgeByPoint(part, x, y, z), distance);
}, ct);
/// Снять фаску с ребра детали по индексу из list_edges катетом .
public Task ChamferEdgeIndexAsync(int edgeIndex, double distance, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
return CreateChamfer(part, SelectEdgeByIndex(part, edgeIndex), distance);
}, ct);
private int CreateChamfer(ksPart part, ksEntity edge, double distance)
{
if (distance <= 0) throw new ArgumentOutOfRangeException(nameof(distance), "Катет фаски должен быть > 0.");
var entity = part.NewEntity((short)Obj3dType.o3d_chamfer) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_chamfer) вернул null.");
var def = entity.GetDefinition() as ksChamferDefinition
?? throw new InvalidOperationException("GetDefinition() фаски вернул не ksChamferDefinition.");
def.tangent = false;
def.SetChamferParam(true, distance, distance);
var arr = def.array() as ksEntityCollection
?? throw new InvalidOperationException("array() фаски вернул не ksEntityCollection.");
arr.Add(edge);
if (!entity.Create())
throw new InvalidOperationException("Create() фаски вернул FALSE (катет слишком велик для ребра?).");
var id = _nextId++;
_features[id] = entity;
return id;
}
/// Превратить тело в оболочку: удалить (открыть) грани с индексами faceIndices,
/// задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется),
/// true — наружу. Возвращает id операции.
public Task ShellAsync(IReadOnlyList 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);
/// Построить ребро жёсткости от разомкнутого контура эскиза до тела.
/// thickness — толщина стенки (мм); side — направление достройки (left|right|up|down);
/// symmetric — толщина симметрична плоскости эскиза; angle — уклон стенок (градусы).
/// Возвращает id операции.
public Task RibAsync(int sketchId, double thickness, string side = "up", bool symmetric = true, double angle = 0, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
SketchGeometry.RequirePositive(thickness, nameof(thickness)); // ловит и NaN (!(NaN>0))
if (!double.IsFinite(angle)) throw new ArgumentException("Угол уклона должен быть конечным числом.", nameof(angle));
var sideCode = SketchGeometry.RibSide(side);
var sketch = RequireSketch(sketchId);
CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией
var part = GetTopPart();
var entity = part.NewEntity((short)Obj3dType.o3d_ribOperation) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_ribOperation) вернул null.");
var def = entity.GetDefinition() as ksRibDefinition
?? throw new InvalidOperationException("GetDefinition() ребра вернул не ksRibDefinition.");
def.SetSketch(sketch.Entity);
def.index = 0; // первый сегмент контура задаёт уклон
def.angle = angle;
def.side = sideCode;
if (symmetric)
def.SetThinParam((short)Direction_Type.dtBoth, thickness / 2, thickness / 2);
else
def.SetThinParam((short)Direction_Type.dtNormal, thickness, 0);
if (!entity.Create())
throw new InvalidOperationException(
"Create() ребра вернул FALSE (контур не пересекает тело или неверное направление side?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// Кинематическая операция (sweep): переместить профиль (эскиз profileSketchId)
/// вдоль траектории (эскиз pathSketchId), образуя сплошное тело. Возвращает id операции.
public Task SweepAsync(int profileSketchId, int pathSketchId, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
if (profileSketchId == pathSketchId)
throw new ArgumentException("Профиль и траектория должны быть разными эскизами.", nameof(pathSketchId));
var profile = RequireSketch(profileSketchId);
var path = RequireSketch(pathSketchId);
CloseSketchCore(profile); // оба эскиза должны быть закрыты перед операцией
CloseSketchCore(path);
var part = GetTopPart();
var entity = part.NewEntity((short)Obj3dType.o3d_baseEvolution) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_baseEvolution) вернул null.");
var def = entity.GetDefinition() as ksBaseEvolutionDefinition
?? throw new InvalidOperationException("GetDefinition() кинематической операции вернул не ksBaseEvolutionDefinition.");
def.SetSketch(profile.Entity);
var pathArr = def.PathPartArray() as ksEntityCollection
?? throw new InvalidOperationException("PathPartArray() вернул не ksEntityCollection.");
pathArr.Add(path.Entity);
def.sketchShiftType = 0; // перенос сечения с сохранением угла
def.SetThinParam(false, 0, 0, 0); // сплошное тело
if (!entity.Create())
throw new InvalidOperationException(
"Create() кинематической операции вернул FALSE (начало траектории не на плоскости профиля или несовместимая геометрия?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// Операция по сечениям (loft): построить сплошное тело по ≥2 закрытым эскизам-сечениям
/// (в порядке списка). closed — замкнуть сечения в кольцо. Возвращает id операции.
public Task LoftAsync(IReadOnlyList sketchIds, bool closed = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
ArgumentNullException.ThrowIfNull(sketchIds);
var ids = sketchIds.Distinct().ToList();
if (ids.Count < 2)
throw new ArgumentException("Нужно минимум 2 сечения для операции по сечениям.", nameof(sketchIds));
var sketches = ids.Select(RequireSketch).ToList(); // проверяем все id до мутации
foreach (var s in sketches) CloseSketchCore(s);
var part = GetTopPart();
var entity = part.NewEntity((short)Obj3dType.o3d_baseLoft) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_baseLoft) вернул null.");
var def = entity.GetDefinition() as ksBaseLoftDefinition
?? throw new InvalidOperationException("GetDefinition() операции по сечениям вернул не ksBaseLoftDefinition.");
var arr = def.Sketchs() as ksEntityCollection
?? throw new InvalidOperationException("Sketchs() вернул не ksEntityCollection.");
foreach (var s in sketches) arr.Add(s.Entity);
def.SetLoftParam(closed, false, true); // closed, flipVertex=false, autoPath=true
def.SetThinParam(false, 0, 0, 0); // сплошное тело
if (!entity.Create())
throw new InvalidOperationException(
"Create() операции по сечениям вернул FALSE (несовместимые сечения или неверный порядок?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// Линейный массив (по сетке вдоль одной координатной оси): размножить операции
/// featureIds вдоль оси axis, count экземпляров всего (включая исходный, ≥2), шаг step (мм)
/// между соседними. geometric=true — быстрое геометрическое копирование. Возвращает id операции.
public Task LinearPatternAsync(IReadOnlyList featureIds, CoordinateAxis axis, int count, double step, bool geometric = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
ArgumentNullException.ThrowIfNull(featureIds);
var ids = featureIds.Distinct().ToList();
if (ids.Count == 0) throw new ArgumentException("Нужна хотя бы одна операция для массива.", nameof(featureIds));
SketchGeometry.RequireMin(count, 2, nameof(count)); // count включает исходный
SketchGeometry.RequirePositive(step, nameof(step)); // ловит и NaN
// Собираем источники до мутации модели (проверка существования всех id).
var features = ids.Select(RequireFeature).ToList();
var part = GetTopPart();
var axisEntity = part.GetDefaultEntity((short)CoordinateAxes.ToObj3dType(axis)) as ksEntity
?? throw new InvalidOperationException("Не удалось получить координатную ось.");
var entity = part.NewEntity((short)Obj3dType.o3d_meshCopy) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_meshCopy) вернул null.");
var def = entity.GetDefinition() as ksMeshCopyDefinition
?? throw new InvalidOperationException("GetDefinition() массива вернул не ksMeshCopyDefinition.");
def.SetAxis1(axisEntity);
def.SetCopyParamAlongAxis(true, 0, count, step, false); // ось 1: angle=0, factor=false (шаг между соседними)
def.count2 = 1; // второе направление выключено (1D-массив)
def.geomArray = geometric;
var arr = def.OperationArray() as ksEntityCollection
?? throw new InvalidOperationException("OperationArray() массива вернул не ksEntityCollection.");
foreach (var f in features) arr.Add(f);
if (!entity.Create())
throw new InvalidOperationException(
"Create() линейного массива вернул FALSE (копии самопересекаются или несовместимая геометрия?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// Круговой массив: размножить операции featureIds вокруг координатной оси axis,
/// count экземпляров всего (включая исходный, ≥2), угловой шаг step (градусы) между соседними.
/// reverse — обратное направление вращения. geometric=true — быстрое геометрическое копирование.
/// Возвращает id операции.
public Task CircularPatternAsync(IReadOnlyList featureIds, CoordinateAxis axis, int count, double step, bool reverse = false, bool geometric = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
ArgumentNullException.ThrowIfNull(featureIds);
var ids = featureIds.Distinct().ToList();
if (ids.Count == 0) throw new ArgumentException("Нужна хотя бы одна операция для массива.", nameof(featureIds));
SketchGeometry.RequireMin(count, 2, nameof(count)); // count включает исходный
SketchGeometry.RequirePositive(step, nameof(step)); // угловой шаг > 0, ловит NaN
// Собираем источники до мутации модели (проверка существования всех id).
var features = ids.Select(RequireFeature).ToList();
var part = GetTopPart();
var axisEntity = part.GetDefaultEntity((short)CoordinateAxes.ToObj3dType(axis)) as ksEntity
?? throw new InvalidOperationException("Не удалось получить координатную ось.");
var entity = part.NewEntity((short)Obj3dType.o3d_circularCopy) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_circularCopy) вернул null.");
var def = entity.GetDefinition() as ksCircularCopyDefinition
?? throw new InvalidOperationException("GetDefinition() массива вернул не ksCircularCopyDefinition.");
def.SetAxis(axisEntity);
// Чистый круговой массив: радиальное направление выключено (count1=1),
// экземпляры идут по кольцу (count2=count) с угловым шагом step° между соседними.
def.count1 = 1;
def.step1 = 0;
def.factor1 = false;
def.count2 = count;
def.step2 = step;
def.factor2 = false; // шаг между соседними (не общий угол)
def.inverce = reverse;
def.geomArray = geometric;
var arr = def.GetOperationArray() as ksEntityCollection
?? throw new InvalidOperationException("GetOperationArray() массива вернул не ksEntityCollection.");
foreach (var f in features) arr.Add(f);
if (!entity.Create())
throw new InvalidOperationException(
"Create() кругового массива вернул FALSE (копии самопересекаются или неверная ось?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// Зеркальная копия операций featureIds относительно координатной плоскости plane.
/// Возвращает id операции.
public Task MirrorOperationAsync(IReadOnlyList featureIds, BasePlane plane, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
ArgumentNullException.ThrowIfNull(featureIds);
var ids = featureIds.Distinct().ToList();
if (ids.Count == 0) throw new ArgumentException("Нужна хотя бы одна операция для зеркала.", nameof(featureIds));
// Собираем источники до мутации модели (проверка существования всех id).
var features = ids.Select(RequireFeature).ToList();
var part = GetTopPart();
var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity
?? throw new InvalidOperationException("Не удалось получить координатную плоскость.");
var entity = part.NewEntity((short)Obj3dType.o3d_mirrorOperation) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_mirrorOperation) вернул null.");
var def = entity.GetDefinition() as ksMirrorCopyDefinition
?? throw new InvalidOperationException("GetDefinition() зеркала вернул не ksMirrorCopyDefinition.");
def.SetPlane(planeEntity);
var arr = def.GetOperationArray() as ksEntityCollection
?? throw new InvalidOperationException("GetOperationArray() зеркала вернул не ksEntityCollection.");
foreach (var f in features) arr.Add(f);
if (!entity.Create())
throw new InvalidOperationException(
"Create() зеркальной копии вернул FALSE (копия пересекает исходную геометрию?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// Зеркально отразить всё тело относительно координатной плоскости plane (получить
/// симметричную деталь: оригинал + зеркало). Возвращает id операции.
public Task MirrorBodyAsync(BasePlane plane, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity
?? throw new InvalidOperationException("Не удалось получить координатную плоскость.");
var entity = part.NewEntity((short)Obj3dType.o3d_mirrorAllOperation) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_mirrorAllOperation) вернул null.");
var def = entity.GetDefinition() as ksMirrorCopyAllDefinition
?? throw new InvalidOperationException("GetDefinition() зеркала тела вернул не ksMirrorCopyAllDefinition.");
def.SetPlane(planeEntity);
// ChooseBodies() в этом interop не приводится к ksChooseBodies (даёт null), а вызов и не
// требуется: o3d_mirrorAllOperation по умолчанию отражает ВСЕ тела детали, сохраняя
// оригинал (проверено тестом — объём удваивается). Поэтому выбор тел не настраиваем.
if (!entity.Create())
throw new InvalidOperationException(
"Create() зеркального отражения тела вернул FALSE (тело пересекает плоскость симметрии?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// Уклон граней: наклонить грани faceIndices на угол angle° относительно нейтральной
/// (опорной) координатной плоскости neutralPlane — сечение тела в этой плоскости остаётся
/// неизменным. outward=true — расширение (материал добавляется), false — сужение. Грани должны
/// граничить/пересекаться с нейтральной плоскостью. Возвращает id операции.
public Task DraftAsync(IReadOnlyList faceIndices, BasePlane neutralPlane, double angle, bool outward = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
SketchGeometry.RequireDraftAngle(angle, nameof(angle));
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 planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(neutralPlane)) as ksEntity
?? throw new InvalidOperationException("Не удалось получить координатную плоскость.");
var entity = part.NewEntity((short)Obj3dType.o3d_incline) as ksEntity
?? throw new InvalidOperationException("NewEntity(o3d_incline) вернул null.");
var def = entity.GetDefinition() as ksInclineDefinition
?? throw new InvalidOperationException("GetDefinition() уклона вернул не ksInclineDefinition.");
var arr = def.FaceArray() as ksEntityCollection
?? throw new InvalidOperationException("FaceArray() уклона вернул не ksEntityCollection.");
foreach (var f in faces) arr.Add(f);
def.SetPlane(planeEntity);
def.angle = angle;
def.direction = !outward; // эмпирически: direction=false → расширение, true → сужение
if (!entity.Create())
throw new InvalidOperationException(
"Create() уклона вернул FALSE (грань не граничит с нейтральной плоскостью или несовместимая геометрия?).");
var id = _nextId++;
_features[id] = entity;
return id;
}, ct);
/// Перестроить документ.
public Task RebuildAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var doc3d = ActiveDoc3D();
doc3d.RebuildDocument();
}, ct);
}