From 205a4b02de31f94946fbddd3a9d194ac2e986aa1 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Wed, 27 May 2026 10:23:16 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=83=D0=BA=D0=BB=D0=BE=D0=BD=20=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B5=D0=B9=20(draft)=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20API5=20=E2=80=94=20ksInclineDefinition=20o3d=5Fi?= =?UTF-8?q?ncline=3D42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ВОПРЕКИ прежнему предположению: уклон ЕСТЬ в API5 (операция Incline, не Draft) → стандартный паттерн PartModeler, регистрируется в _features (массивы/зеркало применимы) - DraftAsync: FaceArray().Add(грани) + SetPlane(нейтральная коорд. плоскость) + angle + direction - ЭМПИРИЧЕСКИ: direction=false=расширение, true=сужение (обратно справке) → маппинг !outward - инструмент draft (faceIndices, neutralPlane, angle 0..90, outward); валидация угла и индексов - интеграционный тест: 4 боковые грани коробки → усечённая пирамида (объём ±10%) - 105 тестов; спек docs/superpowers/specs/2026-05-27-draft-design.md --- .../specs/2026-05-27-draft-design.md | 67 +++++++++++++++++++ .../Modeling/PartModeler.Features.cs | 40 +++++++++++ src/Kompas.Mcp.Host/Tools/FeatureTools.cs | 14 ++++ .../Integration/FeatureOpsTests.cs | 32 +++++++++ 4 files changed, 153 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-27-draft-design.md diff --git a/docs/superpowers/specs/2026-05-27-draft-design.md b/docs/superpowers/specs/2026-05-27-draft-design.md new file mode 100644 index 0000000..b530284 --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-draft-design.md @@ -0,0 +1,67 @@ +# Дизайн: уклон граней (draft / incline) через API5 + +**Дата:** 2026-05-27 +**Статус:** реализовано и проверено (тест зелёный); на ревью (ревьюер — Codex) + +## Цель + +Добавить операцию **«Уклон»** (draft) — наклонить грани на угол относительно нейтральной +(опорной) плоскости. Приоритет 1 текущей сессии. + +## Ключевой факт: уклон ЕСТЬ в API5 (вопреки прежнему предположению) + +Память `kompas-formative-features-api5` ошибочно считала draft недоступным в API5 — она искала +тип со словом **Draft**, но операция называется **«Уклон» = Incline**. Рефлексия подтвердила: +`Obj3dType.o3d_incline=42` + **`ksInclineDefinition`** есть в `Kompas6API5.dll`. Это стандартный +API5-паттерн (как shell/rib/fillet) — НЕ нужен API7. Плюс: операция регистрируется в реестре +`_features`, значит массивы/зеркало пакета C к ней применимы. + +> `o3d_DraftFromEdges=644` (API7 `IDraftFromEdges`) — это ДРУГАЯ операция «уклон от базовой линии» +> (v22), без нейтральной плоскости. Для обычного уклона граней используем `IIncline`/`ksInclineDefinition`. + +## Сигнатуры (рефлексия interop + справка) + +``` +Obj3dType.o3d_incline = 42 +ksInclineDefinition: + double angle // угол уклона (градусы) + bool direction // ЭМПИРИЧЕСКИ: false=расширение, true=сужение (обратно справке!) + object FaceArray() // ksEntityCollection уклоняемых граней (Add по одной) + bool SetPlane(object) // нейтральная (опорная) плоскость — ksEntity + object GetPlane() +``` + +**Паттерн:** `NewEntity(o3d_incline)` → `GetDefinition()` → `FaceArray().Add(face)` (по граням) → +`SetPlane(нейтральная)` → `angle` → `direction` → `Create()` → регистрация в `_features`. + +**Нейтральная плоскость** — сечение тела в ней остаётся неизменным; грани уклоняются вокруг +линии пересечения с ней. На первом этапе — базовая координатная плоскость (XOY/XOZ/YOZ). + +**Направление (важно):** в interop `direction=false` даёт **расширение**, `true` — **сужение** +(проверено объёмом; обратно тексту справки). Контракт инструмента: `outward` (true=расширение), +маппинг `def.direction = !outward`. + +## MCP-инструмент (группа Feature) + +| Инструмент | Параметры | Поведение | +|---|---|---| +| `draft` | `faceIndices: int[]`, `neutralPlane: "XOY"\|"XOZ"\|"YOZ"`, `angle: double`, `outward=false` | Уклон граней `faceIndices` (из list_faces) на `angle`° относительно нейтральной координатной плоскости. `outward` — расширение/сужение. Возвращает id. | + +**Решения (YAGNI):** нейтральная плоскость — только координатная (как mirror_operation); уклон от +ребра (`IDraftFromEdges`) — отдельная задача позже. Грани — по стабильному индексу из list_faces. + +## Реализация + +- **Метод** `DraftAsync(IReadOnlyList faceIndices, BasePlane neutralPlane, double angle, bool outward, ct)` + в `PartModeler.Features.cs`. Валидация: `angle` конечен и в (0; 90); `faceIndices` не пуст, + `Distinct`; диапазон всех индексов проверяется до мутации (`SelectFaceByIndex`). +- **Инструмент** `draft` в `FeatureTools.cs` (`neutralPlane` парсится `BasePlanes.Parse`). +- Транзитные RCW не освобождаем точечно — консистентно (долг v2-2). +- `Create()==FALSE` → `InvalidOperationException` (грань не граничит с нейтральной плоскостью?). + +## Тестирование (Integration, `FeatureOpsTests`) + +**Уклон**: коробка 40×40×20 (низ на Z=0) → 4 боковые грани (площадь 800) под уклон 10° внутрь +(`outward=false`) относительно XOY → усечённая пирамида: верх ≈ 40−2·20·tan10°=32.95; +`V = (20/3)(1600+1085.5+√(1600·1085.5)) ≈ 26689` мм³; `InRange(after, ±10%)` и `after < before`. ✓ +Боковые грани выбираются из `ListFaces` по площади 800 (тип plane). diff --git a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs index 1299f7c..ae05c09 100644 --- a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs +++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs @@ -480,6 +480,46 @@ public sealed partial class PartModeler 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(() => + { + if (!double.IsFinite(angle) || angle <= 0 || angle >= 90) + throw new ArgumentOutOfRangeException(nameof(angle), "Угол уклона должен быть в (0; 90) градусов."); + 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(() => diff --git a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs index 004c874..6e196cd 100644 --- a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs +++ b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs @@ -198,6 +198,20 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler, Hol return $"Отверстие Ø{diameter} ({kind}) создано на грани в точке ({x}, {y}, {z})."; } + [McpServerTool(Name = "draft")] + [Description("Уклон граней: наклонить грани faceIndices (из list_faces) на угол angle (градусы, 0..90) относительно нейтральной (опорной) координатной плоскости neutralPlane (XOY|XOZ|YOZ) — сечение тела в этой плоскости остаётся неизменным, грани уклоняются вокруг линии пересечения с ней. outward=true — расширение (материал добавляется), false — сужение. Грани должны граничить/пересекаться с нейтральной плоскостью. Возвращает id операции.")] + public async Task Draft( + [Description("Индексы уклоняемых граней из list_faces")] int[] faceIndices, + [Description("Нейтральная (опорная) плоскость: XOY|XOZ|YOZ")] string neutralPlane, + [Description("Угол уклона, градусы (0..90)")] double angle, + [Description("Расширение (true) или сужение (false)")] bool outward = false) + { + await session.ConnectAsync(); + var id = await modeler.DraftAsync(faceIndices, BasePlanes.Parse(neutralPlane), angle, outward); + var count = faceIndices?.Distinct().Count() ?? 0; + return $"Уклон {angle}° создан на {count} гранях (нейтральная плоскость {neutralPlane}), id={id}."; + } + [McpServerTool(Name = "rebuild")] [Description("Перестроить активный документ.")] public async Task Rebuild() diff --git a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs index 76f931b..a9f7a74 100644 --- a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs +++ b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs @@ -282,6 +282,38 @@ public sealed class FeatureOpsTests finally { await _docs.CloseAsync(save: false); } } + [Fact] + public async Task Draft_tapers_box_sides_into_frustum() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Коробка 40×40×20 (центр 0,0; низ на Z=0, верх на Z=20). + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddRectangleAsync(s, -20, -20, 20, 20); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: 20); + var before = (await _query.GetPartInfoAsync()).Volume; // 32000 + + // 4 боковые грани (площадь 40×20=800) под уклон 10° внутрь относительно XOY (низ остаётся 40×40). + var faces = await _query.ListFacesAsync(); + var sides = faces.Where(f => f.Type == "plane" && Math.Abs(f.Area - 800) < 1).Select(f => f.Index).ToArray(); + Assert.Equal(4, sides.Length); + + var id = await _modeler.DraftAsync(sides, BasePlane.XOY, angle: 10, outward: false); + Assert.True(id > 0); + await _modeler.RebuildAsync(); + + // Сужение к верху → усечённая пирамида. Верх ≈ 40−2·20·tan10° = 32.95; A2≈1085.5. + // V = (20/3)(1600 + 1085.5 + √(1600·1085.5)) ≈ 26689 мм³. + var after = (await _query.GetPartInfoAsync()).Volume; + var expected = (20.0 / 3) * (1600 + 1085.5 + Math.Sqrt(1600 * 1085.5)); + Assert.True(after < before, "Сужающий уклон должен уменьшить объём."); + Assert.InRange(after, expected * 0.9, expected * 1.1); + } + finally { await _docs.CloseAsync(save: false); } + } + [Fact] public async Task Loft_two_squares_makes_solid() {