# Дизайн: пакет C — массивы и зеркало (linear/circular pattern, mirror) **Дата:** 2026-05-26 **Статус:** на ревью (автономный режим; ревьюер — Codex) ## Цель Добавить класс операций «размножение»: **линейный массив** (по сетке вдоль координатной оси), **круговой массив** (вокруг координатной оси) и **зеркальное отражение** (операций относительно координатной плоскости; и тела целиком). Самый ценный недостающий класс (NEXT-SESSION приоритет 1). Реализуется тремя инкрементами, по одной операции за раз. Все три операции в API5 строятся одним паттерном: `NewEntity(o3d_*)` → `GetDefinition()` → задать **источник** (коллекция копируемых операций / тел) + **направление** (ось/плоскость) → `Create()` → регистрация в `_features`. Источник копирования — `ksEntity` ранее созданных операций, на которые ссылаемся по их id из реестра `_features` (те же id, что возвращают extrude/revolve/fillet/…). ## Сигнатуры COM (рефлексия interop + справка SDK) ``` Obj3dType.o3d_meshCopy = 35 // массив по сетке (линейный) Obj3dType.o3d_circularCopy = 36 // круговой массив Obj3dType.o3d_mirrorOperation = 48 // зеркальная копия выбранных операций Obj3dType.o3d_mirrorAllOperation = 49 // зеркально отразить тело(а) целиком Obj3dType.o3d_axisOX/OY/OZ = 71/72/73 // координатные оси (GetDefaultEntity) Obj3dType.o3d_planeXOY/XOZ/YOZ = 1/2/3 // координатные плоскости ksMeshCopyDefinition: // линейный (по сетке) bool SetAxis1(object axisOrEdge) // ось 1 (коорд. ось или прямое ребро) bool SetAxis2(object axisOrEdge) // ось 2 (для 2D-сетки) bool SetCopyParamAlongAxis(bool firstAxis, double angle, int count, double step, bool factor) int count1/count2; double step1/step2; double angle1/angle2; bool factor1/factor2 bool geomArray; bool insideFlag object OperationArray() // ksEntityCollection источников ksCircularCopyDefinition: // круговой bool SetAxis(object axis) // ось вращения bool SetCopyParamAlongDir(int count, double step, bool factor, bool dir) int count1/count2; double step1/step2; bool factor1/factor2; bool geomArray; bool inverce object GetOperationArray() // ksEntityCollection источников ksMirrorCopyDefinition: // зеркало выбранных операций bool SetPlane(object plane) // плоскость симметрии (коорд. или плоская грань) object GetOperationArray() // ksEntityCollection источников ksMirrorCopyAllDefinition: // зеркало тела(а) целиком bool SetPlane(object plane) object ChooseBodies() // ksChooseBodies (необяз.: все тела по умолчанию) ``` **Семантика (из справки, проверить эмпирически):** - `count` — количество **всех** экземпляров, **включая исходный** (count=3 → +2 копии). - линейный `step` — мм между **соседними** при `factor=false`; круговой `step` — **угол в градусах** между соседними при `factor=false`. - `count` включает оригинал → требуем `count >= 2`. ## Новые MCP-инструменты (группа Feature) | Инструмент | Параметры | Поведение | |---|---|---| | `linear_pattern` | `featureIds: int[]`, `axis: "X"\|"Y"\|"Z"`, `count: int`, `step: double`, `geometric=false` | Линейный массив: размножить операции `featureIds` вдоль координатной оси `axis`, `count` экз. всего (вкл. исходную, ≥2), шаг `step` мм между соседними. `geometric` → быстрое геом. копирование. Возвращает id. | | `circular_pattern` | `featureIds: int[]`, `axis: "X"\|"Y"\|"Z"`, `count: int`, `step=` (угол, °), `reverse=false`, `geometric=false` | Круговой массив вокруг координатной оси `axis`, `count` экз. всего (≥2), угловой шаг `step` ° между соседними (по умолчанию 360/count для равномерного — см. ниже). `reverse` — обратное направление. Возвращает id. | | `mirror_operation` | `featureIds: int[]`, `plane: "XOY"\|"XOZ"\|"YOZ"` | Зеркальная копия операций `featureIds` относительно координатной плоскости `plane`. Возвращает id. | | `mirror_body` | `plane: "XOY"\|"XOZ"\|"YOZ"` | Зеркально отразить всё тело относительно плоскости (получить симметричную деталь). Возвращает id. | **Решения (YAGNI / инкрементальность):** - **Ось/плоскость — только координатные** (X/Y/Z, XOY/XOZ/YOZ) на первом этапе: детерминированно, легко тестируется, покрывает большинство случаев. Выбор по ребру/грани — отдельная задача позже. - **Линейный массив — 1D** (одна ось): `SetAxis1` + `SetCopyParamAlongAxis(true,…)`, `count2=1` (второе направление выключено). 2D-сетку добавим позже при необходимости. - `factor=false` (шаг между соседними — интуитивнее для пользователя). - **Круговой — задаём свойства напрямую** (`SetCopyParamAlongDir` неоднозначен: `dir`/radial vs annular): `count1=1` (радиально — один «ряд»), `count2=count` (кольцевое — N вокруг оси), `step2=step` (°), `factor2=false`, `inverce=reverse`. Угловой шаг задаёт агент (например 90). - `mirror_body` использует `ksMirrorCopyAllDefinition` + `SetPlane`. В API5-interop у определения **нет** `SaveInitialObjects` (по рефлексии — только `ChooseBodies/Get/SetPlane`); поведение «оригинал + зеркало» проверяем эмпирически. `ChooseBodies` дорефлексировать в инкременте 3 (для односложного тела, вероятно, не требуется; если `Create` упадёт — выставить `ksAllBodies`). - `geomArray` дефолт `false` (полное копирование операции — надёжнее для несложных тел). ### Открытые API-тонкости (проверяются интеграционными тестами) - `count` включает оригинал — подтверждается тестом (count=3 → 3 экз.). Если нет — поправить. - `count2=1` действительно выключает 2-е направление линейного массива. - Круговой: `count2=count`/`count1=1` даёт чистый круговой массив (без радиального дубля). - `mirror_body` оставляет исходное тело (а не заменяет его). ## Реализация - **Enum** `CoordinateAxis { X, Y, Z }` + `CoordinateAxes.ToObj3dType/Parse` — новый файл `Core/Modeling/CoordinateAxis.cs` (по аналогии с `BasePlane.cs`). - **Методы** в `PartModeler.Features.cs`: - `LinearPatternAsync(IReadOnlyList featureIds, CoordinateAxis axis, int count, double step, bool geometric, CancellationToken)` - `CircularPatternAsync(IReadOnlyList featureIds, CoordinateAxis axis, int count, double step, bool reverse, bool geometric, CancellationToken)` - `MirrorOperationAsync(IReadOnlyList featureIds, BasePlane plane, CancellationToken)` - `MirrorBodyAsync(BasePlane plane, CancellationToken)` - **Хелпер** `RequireFeature(int id)` в `PartModeler.cs` (по аналогии с `RequireSketch`): `_features.TryGetValue` → `ksEntity` или `KeyNotFoundException`. - **Валидация** (чистая, `SketchGeometry`): `RequireMin(int value, int min, string)` для `count>=2`; `RequirePositive(step, …)` (ловит NaN). Списки `featureIds`: not null, `Distinct`, не пуст, все id существуют (проверка до мутации модели — собрать `ksEntity` заранее). - **Инструменты** в `FeatureTools.cs`: `linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`; `axis`/`plane` парсятся из строки (`CoordinateAxes.Parse`/`BasePlanes.Parse`). - Имена методов коллекции в interop различаются: MeshCopy — `OperationArray()`; Circular и Mirror — `GetOperationArray()` (по рефлексии). Все → `as ksEntityCollection ?? throw`. - Транзитные RCW не освобождаем точечно — консистентно (долг v2-2). - `Create()==FALSE` → `InvalidOperationException` с подсказкой (несовместимая геометрия / шаг ведёт к самопересечению / неверная ось). ## Тестирование (Integration, `FeatureOpsTests`) Паттерн: `CreateAsync(Part)` → построить базовую операцию (запомнить её id) → массив/зеркало → `RebuildAsync` → проверить объём (`GetPartInfoAsync`) и габарит (`GetBoundingBoxAsync`); `finally CloseAsync`. 1. **linear_pattern**: цилиндр R5×H10 в центре (V₁≈785 мм³) → `linear_pattern([cyl], X, 3, 30)` → 3 непересекающихся цилиндра → V≈3·785=2356 (`InRange ±5%`); габарит X≈70 (от −5 до 65). 2. **circular_pattern**: цилиндр R3×H10 со смещением (центр (20,0)) → `circular_pattern([cyl], Z, 3, 90)` → 3 цилиндра в (20,0),(0,20),(−20,0) (count=3, несимметрично → осмысленный поворот, не «полный круг») → V≈3·283=849 (`±5%`); габарит X≈46 (от −23 до 23), SizeY≈26 (от −3 до 23). 3. **mirror_operation** (две исходные операции — проверяет заполнение `ksEntityCollection`): цилиндр R3 центр (20,0) + цилиндр R2 центр (10,0), оба H10 → `mirror_operation([c1,c2], YOZ)` → копии в (−20,0),(−10,0) → V≈2·(283+125.7)=817 (`±5%`); габарит X≈46. 4. **mirror_body**: цилиндр R3×H10 центр (20,0) → `mirror_body(YOZ)` → оригинал + зеркало в (−20,0) → V≈2·283=565 (`±5%`); габарит X≈46. Ожидаемые объёмы считаем аналитически, сверяем `InRange ±5%`. Открытые тонкости (count вкл. оригинал; count2=1; круговой count1=1/count2=N; mirror_body оставляет оригинал) — проверяются этими тестами; при расхождении корректируем по факту, сохранив суть проверки.