Files
kompas3d-mcp/docs/superpowers/specs/2026-05-26-pattern-mirror-design.md
T

12 KiB
Raw Blame History

Дизайн: пакет 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<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.TryGetValueksEntity или 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()==FALSEInvalidOperationException с подсказкой (несовместимая геометрия / шаг ведёт к самопересечению / неверная ось).

Тестирование (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 оставляет оригинал) — проверяются этими тестами; при расхождении корректируем по факту, сохранив суть проверки.