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); }