docs: спек пакета C — массивы и зеркало (linear/circular pattern, mirror)

This commit is contained in:
2026-05-26 23:45:40 +03:00
parent 2a555cafd5
commit d6ab16fd6a
@@ -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<int> featureIds, CoordinateAxis axis, int count, double step, bool geometric, CancellationToken)`
- `CircularPatternAsync(IReadOnlyList<int> featureIds, CoordinateAxis axis, int count, double step, bool reverse, bool geometric, CancellationToken)`
- `MirrorOperationAsync(IReadOnlyList<int> 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` НЕ включает
оригинал или направление шага иное — корректируем по факту, сохранив суть проверки.