diff --git a/docs/superpowers/specs/2026-05-26-pattern-mirror-design.md b/docs/superpowers/specs/2026-05-26-pattern-mirror-design.md new file mode 100644 index 0000000..3376526 --- /dev/null +++ b/docs/superpowers/specs/2026-05-26-pattern-mirror-design.md @@ -0,0 +1,114 @@ +# Дизайн: пакет 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` (шаг между соседними — интуитивнее для пользователя). +- Круговой `step`: задаём явный угловой шаг (°). Дефолт инструмента не подставляем — пусть + агент передаёт (например 90 для 4 шт). `factor=false`. +- `mirror_body` использует `ksMirrorCopyAllDefinition` без `ChooseBodies` (по умолчанию все тела). +- `geomArray` дефолт `false` (полное копирование операции — надёжнее для несложных тел). + +## Реализация + +- **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, 4, 90)` → + 4 цилиндра по кругу R20 → V≈4·283=1131 (`±5%`); габарит X≈Y≈46 (от −23 до 23). +3. **mirror_operation**: цилиндр R3×H10 центр (20,0) → `mirror_operation([cyl], YOZ)` → копия в + (−20,0) → V≈2·283=565 (`±5%`); габарит X≈46. +4. **mirror_body**: то же тело → `mirror_body(YOZ)` → симметричная пара, габарит X≈46, V≈2·283. + +Ожидаемые объёмы считаем аналитически, сверяем `InRange ±5%`. Если `count` НЕ включает +оригинал или направление шага иное — корректируем по факту, сохранив суть проверки.