diff --git a/CLAUDE.md b/CLAUDE.md index a5517e7..6e920e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ The base is committed to the repo (canonical) — no regeneration step. It cover **v1 + v2 + STEP/assembly + direct B-rep edit + structural model inspection implemented and working** (full sketch→feature→inspect→STEP round-trip + move_face + describe_model validated end-to-end). Stack: **.NET 8 (`net8.0-windows`, x64), C#**, MCP via the official `ModelContextProtocol` SDK over **stdio**. -**58 MCP tools, 89 tests green (53 unit + 36 integration).** v2 added: sketch-on-face (by point and by index), revolve (boss/cut + sketch axis), fillet/chamfer (edge-by-point and by index), and Query tools `get_part_info` (МЦХ), `get_bounding_box`, `list_faces`, `list_edges`. After v2: `import_step`, `export_step` (`ConversionService`, `ConversionTools`), `list_components` (assembly traversal via `TopPart → IParts7`). `move_face` (`FaceEditService`, `EditTools`) — direct B-rep face editing. After move_face: **structural model inspection** — `describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure` (`ModelInspectionService`, `InspectionTools`). `describe_model` is the preferred first call over `model_snapshot` (no token cost for image context; gives feature tree, bodies, variables, topology summary, МЦХ). Package A «richer sketches»: 7 new sketch primitives — `sketch_add_arc`, `sketch_add_arc_3points`, `sketch_add_ellipse`, `sketch_add_polyline`, `sketch_add_polygon`, `sketch_add_spline`, `sketch_add_point`. `PartModeler` refactored into partial classes: `PartModeler.cs` (core), `PartModeler.Sketch.cs` (2D primitives), `PartModeler.Features.cs` (extrude/revolve/fillet/chamfer). Static `SketchGeometry` class in `Core/Modeling`. Package B «forming ops» complete: **`shell`** (оболочка, `o3d_shellOperation=43`) + **`rib`** (ребро жёсткости, `o3d_ribOperation=44`) + **`sweep`** (кинематическая операция, `o3d_baseEvolution=45`) + **`loft`** (по сечениям, `o3d_baseLoft=30`). Auxiliary geometry (start of package E): **`sketch_create_on_offset_plane`** (смещённая плоскость, `o3d_planeOffset=14`) — unlocked `loft`. Additional Edit tools: **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`**. +**62 MCP tools, 101 tests green (60 unit + 41 integration).** v2 added: sketch-on-face (by point and by index), revolve (boss/cut + sketch axis), fillet/chamfer (edge-by-point and by index), and Query tools `get_part_info` (МЦХ), `get_bounding_box`, `list_faces`, `list_edges`. After v2: `import_step`, `export_step` (`ConversionService`, `ConversionTools`), `list_components` (assembly traversal via `TopPart → IParts7`). `move_face` (`FaceEditService`, `EditTools`) — direct B-rep face editing. After move_face: **structural model inspection** — `describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure` (`ModelInspectionService`, `InspectionTools`). `describe_model` is the preferred first call over `model_snapshot` (no token cost for image context; gives feature tree, bodies, variables, topology summary, МЦХ). Package A «richer sketches»: 7 new sketch primitives — `sketch_add_arc`, `sketch_add_arc_3points`, `sketch_add_ellipse`, `sketch_add_polyline`, `sketch_add_polygon`, `sketch_add_spline`, `sketch_add_point`. `PartModeler` refactored into partial classes: `PartModeler.cs` (core), `PartModeler.Sketch.cs` (2D primitives), `PartModeler.Features.cs` (extrude/revolve/fillet/chamfer). Static `SketchGeometry` class in `Core/Modeling`. Package B «forming ops» complete: **`shell`** (оболочка, `o3d_shellOperation=43`) + **`rib`** (ребро жёсткости, `o3d_ribOperation=44`) + **`sweep`** (кинематическая операция, `o3d_baseEvolution=45`) + **`loft`** (по сечениям, `o3d_baseLoft=30`). Auxiliary geometry (start of package E): **`sketch_create_on_offset_plane`** (смещённая плоскость, `o3d_planeOffset=14`) — unlocked `loft`. Additional Edit tools: **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`**. Package C «patterns & mirror» complete: **`linear_pattern`** (линейный массив, `o3d_meshCopy=35`) + **`circular_pattern`** (круговой массив, `o3d_circularCopy=36`) + **`mirror_operation`** (зеркальная копия операций, `o3d_mirrorOperation=48`) + **`mirror_body`** (зеркало тела, `o3d_mirrorAllOperation=49`). See [`README.md`](README.md) and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); design decisions/caveats in [`docs/OPEN_QUESTIONS.md`](docs/OPEN_QUESTIONS.md). @@ -52,7 +52,7 @@ Key implementation facts (don't relearn): - Logs go to **stderr** (stdout is the MCP channel). Integration tests reuse one КОМПАС via `KompasFixture`; artifacts land in gitignored `.scratch/`. - **Structural model inspection** (`ModelInspectionService`, `InspectionTools`): `describe_model` — one-call structural "passport" (bounding box + МЦХ + bodies + topology summary + feature tree + variables); preferred over snapshot. `list_features` — feature tree with params (depth/radius/legs). `list_bodies` — list solid/surface bodies with face count. `list_variables` — model variables (name/expression/value, external/info flags). `describe_face` — drill-down by index (type, area, normal, radius, edge count). `describe_edge` — drill-down by index (type, length, adjacent faces, vertices). `measure` — distance/angle between two objects (face|edge|vertex by index, unit `ST_MIX_MM`). - **Feature tree API** (don't relearn): read via API5 `ksPart.GetFeature()` → `(ksFeature).SubFeatureCollection(true,false)` → `ksFeatureCollection`. Cast `(ksFeature)part` does NOT work (RCW QI returns null) — must use `GetFeature()`. `ksFeature.type` returns only coarse `o3d_entity` and does NOT distinguish operations; precise type and params come from `ksFeature.GetObject()` → `ksEntity.GetDefinition()` and matching definition type (`ksBossExtrusionDefinition.GetSideParam`, `ksFilletDefinition.radius`, `ksChamferDefinition.GetChamferParam`, …). Node name ("Элемент выдавливания:1", "Скругление:1") is localized by КОМПАС itself. -- **STEP import/export, `list_components`, `move_face`, `split_solid_by_plane`, `move_body`, `boolean_union`, full inspection layer, richer sketch primitives (package A), `shell`, `rib`, `sweep`, `loft` (package B complete), `sketch_create_on_offset_plane` (package E start) are done.** Not yet done: `draft` (no API5, requires API7), `hole`; patterns/arrays (package C), parametrics (package D), 2D drawing, assembly building. Known caveat: boss/cut direction on a selected face depends on face-normal orientation (`forward` may need flipping — agent picks via snapshot or `describe_face` normal). See `docs/OPEN_QUESTIONS.md` → «Ревью v2». +- **STEP import/export, `list_components`, `move_face`, `split_solid_by_plane`, `move_body`, `boolean_union`, full inspection layer, richer sketch primitives (package A), `shell`, `rib`, `sweep`, `loft` (package B complete), `sketch_create_on_offset_plane` (package E start), `linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body` (package C complete) are done.** Not yet done: `draft` (no API5, requires API7), `hole`; parametrics (package D), 2D drawing, assembly building. Known caveat: boss/cut direction on a selected face depends on face-normal orientation (`forward` may need flipping — agent picks via snapshot or `describe_face` normal). See `docs/OPEN_QUESTIONS.md` → «Ревью v2». - **"STEP import without history"** detected structurally: `bodyCount > 0 && no formative features && features.Count <= bodyCount+1` (only origin + body). STEP with edits (move_face: "Смещённая плоскость", "Разрезать", "Переместить грани", "Булева операция") — that already has history. - **Topology/measure API**: `ksFaceDefinition.EdgeCollection` / `GetCylinderParam` / `GetSurface().GetNormal` + `normalOrientation`; `ksEdgeDefinition.GetAdjacentFace(bool)` / `GetVertex(bool)` → `ksVertexDefinition.GetPoint`; bodies via `ksPart.BodyCollection()` → `ksBody` (`IsSolid` / `FaceCollection`); variables via `ksPart.VariableCollection()` → `ksVariable`; measurements via `ksPart.GetMeasurer()` → `ksMeasurer` (`SetObject1/2`, `unit=ST_MIX_MM`, `Calc`, `distance`/`MinDistance`/`angle` in degrees / `IsAngleValid`). - **STEP import COM pattern** (verified): `IApplication.get_Converter((object)(int)ksConverterFromSTEP=-3)` (pass format code, not DLL path) → `IConverter.ConverterParameters(cmd)` → set `IAdditionConvertParameters.Format=ksConverterFromSTEP` → `IKompasDocument3D1.ConvertFromAdditionFormat(path, prm)`. Export: `ConvertToAdditionFormat` with format codes for AP203/AP214/AP242. Param co-classes (AdditionConvertParameters etc.) are NOT CoCreatable — only via converter factories. Assembly traversal: `IKompasDocument3D.TopPart → IPart7.Parts (IParts7)`. Extract component: `prm.NeedCreateComponentsFiles=true` (writes .m3d files next to STEP) + `IPart7.OpenSourceDocument`. @@ -62,6 +62,7 @@ Key implementation facts (don't relearn): - **Sweep operation (`sweep`, package B)**: `ksBaseEvolutionDefinition` via `ksPart.NewEntity(o3d_baseEvolution=45)`. Params: `profileSketchId` (closed profile sketch), `pathSketchId` (open/closed path sketch on a different plane, typically perpendicular). `SetSketch(profile)` → `PathPartArray().Add(path)` → `sketchShiftType=0` → `SetThinParam(false)` → `Create()`. Validation: `profileSketchId != pathSketchId`. Profile and path must be on different (usually perpendicular) planes. - **Offset plane (`sketch_create_on_offset_plane`, package E start)**: `OpenSketchOnOffsetPlaneAsync` in `PartModeler.Sketch.cs`. `ksPart.NewEntity(o3d_planeOffset=14)` → `ksPlaneOffsetDefinition` (`SetPlane(basePlane)` + `offset` mm + `direction`) → `Create()` → sketch on that plane. Used to place sketches at arbitrary heights — enables `loft` with parallel sections. `entity.Create()` result is checked (reliability fix applied to all `CreateSketchOn*` paths). - **Loft operation (`loft`, package B)**: `ksBaseLoftDefinition` via `ksPart.NewEntity(o3d_baseLoft=30)`. Params: `sketchIds[]` (≥2 closed profile sketches on different parallel planes). `Sketchs().Add(each sketch)` → `SetLoftParam(closed=false, false, true)` → `SetThinParam(false)` → `Create()`. Requires parallel section sketches at different heights — use `sketch_create_on_offset_plane` for the non-base sections. +- **Patterns & mirror (package C)**: `linear_pattern` — `ksMeshCopyDefinition` via `ksPart.NewEntity(o3d_meshCopy=35)`; `SetAxis1(axis)` + `SetCopyParamAlongAxis(true,0,count,step,false)` + `count2=1` (disables 2nd direction → 1D linear); features added via `OperationArray()` (no Get). `circular_pattern` — `ksCircularCopyDefinition` via `NewEntity(o3d_circularCopy=36)`; `SetAxis(axis)`; properties set directly: `count1=1` (radial off), `count2=count` (ring), `step2=angle°` between neighbours, `factor2=false`, `inverce=reverse`; `GetOperationArray().Add(feature)`. `mirror_operation` — `ksMirrorCopyDefinition` via `NewEntity(o3d_mirrorOperation=48)`; `SetPlane(basePlane)` + `GetOperationArray().Add(feature)`. `mirror_body` — `ksMirrorCopyAllDefinition` via `NewEntity(o3d_mirrorAllOperation=49)`; `SetPlane(basePlane)`; `ChooseBodies()` does NOT cast to `ksChooseBodies` (returns null) and is not needed — `o3d_mirrorAllOperation` mirrors all bodies keeping the original (volume doubles). Source features retrieved by id from `_features` registry via new private `RequireFeature` helper (analogous to `RequireSketch`). New `src/Kompas.Mcp.Core/Modeling/CoordinateAxis.cs` — enum `CoordinateAxis {X,Y,Z}` + `CoordinateAxes.ToObj3dType/Parse` (→ `o3d_axisOX/OY/OZ = 71/72/73`), analogous to `BasePlane.cs`. `SketchGeometry.RequireMin(int value, int min, string paramName)` — validator for `count>=2`. Empirical: `count` includes the original instance (count=3 → 3 bodies); `count2=1` disables 2nd direction for linear; `step2` is angle° between neighbours for circular. - **Richer sketch primitives (package A, ksDocument2D API5 wrappers)**: `ksArcBy3Points` (3-point arc), `ksArcByAngle` (centre/radius/start-end angles in degrees, counterClockwise flag), `ksEllipse` via `ksEllipseParam` — struct obtained via `KompasObject.GetParamStruct(ko_EllipseParam=22)`; **property names are `A`/`B` (uppercase) in the interop**; `ksRegularPolygon` via `ksRegularPolygonParam` (`ko_RegularPolygonParam=92`) — `describe = !inscribed`; NURBS spline via `ksNurbs(order=4)` + `ksNurbsPoint` loop + `ksEndObj`; `ksPoint`. Param structs are released after use. `PartModeler` split into partial classes: `PartModeler.cs` (registry/helpers/NewParam/reset), `PartModeler.Sketch.cs` (all 2D primitives), `PartModeler.Features.cs` (extrude/revolve/fillet/chamfer). Static `SketchGeometry` class (`Core/Modeling`) holds enum mappings (`ArcDirection`, `PolygonDescribe`) and validators (`RequirePositive`, `RequireVertexCount`, `RequirePoints`). Polyline/spline point lists use `record SketchPoint(X, Y)` with JSON names `x`/`y`. ## КОМПАС-3D API architecture (the critical context) diff --git a/README.md b/README.md index d400738..3e74743 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe } ``` -## Инструменты (58 инструментов) +## Инструменты (62 инструмента) | Группа | Инструменты | |---|---| | System | `kompas_connect`, `kompas_status`, `kompas_set_visible` | | Documents | `document_create`, `document_open`, `document_save`, `document_save_as`, `document_close`, `document_active` | | Sketch | `sketch_create`, `sketch_create_on_face`, `sketch_create_on_face_index`, `sketch_create_on_offset_plane`, `sketch_add_line`, `sketch_add_circle`, `sketch_add_rectangle`, `sketch_add_axis`, `sketch_add_arc`, `sketch_add_arc_3points`, `sketch_add_ellipse`, `sketch_add_polyline`, `sketch_add_polygon`, `sketch_add_spline`, `sketch_add_point`, `sketch_close` | -| Features | `extrude_boss`, `extrude_cut`, `revolve_boss`, `revolve_cut`, `fillet_edge`, `chamfer_edge`, `fillet_edge_index`, `chamfer_edge_index`, `shell`, `rib`, `sweep`, `loft`, `rebuild` | +| Features | `extrude_boss`, `extrude_cut`, `revolve_boss`, `revolve_cut`, `fillet_edge`, `chamfer_edge`, `fillet_edge_index`, `chamfer_edge_index`, `shell`, `rib`, `sweep`, `loft`, `linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`, `rebuild` | | Edit | `move_face`, `split_solid_by_plane`, `move_body`, `boolean_union` | | Inspection | `describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure` | | Vision | `model_snapshot` (PNG-снимок; fallback — сначала `describe_model`) | @@ -70,7 +70,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe ``` src/Kompas.Mcp.Core/ COM-слой: STA-диспетчер, подключение, документы, эскизы/операции, конвертация, снимок, инспекция модели src/Kompas.Mcp.Host/ MCP-сервер (stdio) + определения инструментов -tests/Kompas.Mcp.Tests/ 89 тестов: unit + integration (integration требуют КОМПАС) +tests/Kompas.Mcp.Tests/ 101 тест: unit + integration (integration требуют КОМПАС) libs/kompas-interop/ вендорские interop-сборки КОМПАС (из SDK Samples/Common) docs/ архитектура, план, презентация, MD-база знаний SDK (Kompas3D_SDK/) usecases/ полигон обкатки подходов (в .gitignore); приёмы поднимаются в навык kompas-3d diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8fa9894..31a70b9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Архитектура MCP-сервера КОМПАС-3D (предлагаемый вариант) -> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богатые эскизы»+package-B «shell+rib+sweep+loft»+package-E «offset plane»). Актуализировано по коду. +> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богатые эскизы»+package-B «shell+rib+sweep+loft»+package-C «массивы и зеркало»+package-E «offset plane»). Актуализировано по коду. > Сопутствующий контекст по COM API КОМПАС — в [`../CLAUDE.md`](../CLAUDE.md). > Имена интерфейсов и перечислений сверены со справкой SDK по MD-базе знаний `docs/Kompas3D_SDK/` (навык `kompas-sdk-research`). @@ -113,7 +113,7 @@ MCP client ──stdio──> Host (поток-пул, async) |---|---| | **Kompas.Mcp.Host** | Точка входа. Сборка MCP-сервера (stdio), DI (в т.ч. `ConversionService`), логи в stderr, старт и владение STA-потоком, graceful shutdown. Определения инструментов по категориям (System / Documents / Sketch / Features / Query / Conversion). **Тонкий слой**: валидация аргументов → постановка задачи на STA-поток → форматирование результата/ошибки в MCP-ответ. | | **Kompas.Mcp.Core** | COM-слой. Менеджер соединения, менеджер документов, построитель эскизов и операций, `ConversionService` (STEP импорт/экспорт), `QueryService` (МЦХ, грани, рёбра, компоненты), `ModelInspectionService` (дерево операций, тела, переменные, drill-down, измерения), снимок. Все паттерны API5/API7 живут здесь. | -| **Kompas.Mcp.Tests** | Юнит (без COM) + интеграционные (требуют КОМПАС). 89 тестов (53 unit + 36 integration). | +| **Kompas.Mcp.Tests** | Юнит (без COM) + интеграционные (требуют КОМПАС). 101 тест (60 unit + 41 integration). | Принцип: **только `Core` знает про COM**. `Host`/Tools оперируют доменными DTO и вызывают `Core`; `Host` не содержит бизнес-логики. @@ -121,7 +121,7 @@ MCP client ──stdio──> Host (поток-пул, async) --- -## 5. Карта инструментов (58 инструментов) +## 5. Карта инструментов (62 инструмента) Сгруппированы вокруг центрального цикла «эскиз ↔ операция». Имена — `snake_case`. @@ -143,6 +143,7 @@ MCP client ──stdio──> Host (поток-пул, async) - Жизненный цикл: `NewEntity(o3d_sketch)` → `SetPlane(plane|face)` → `BeginEdit()` → `ksLineSeg` / `ksCircle` / `ksArcBy3Points` / `ksArcByAngle` / `ksEllipse` / `ksRegularPolygon` / `ksNurbs` / … → `EndEdit()`. - `PartModeler` split into partial classes: `PartModeler.cs` (core), `PartModeler.Sketch.cs`, `PartModeler.Features.cs`. Static `SketchGeometry` class holds mappings and validators. Point lists use `record SketchPoint(X, Y)` (JSON `x`/`y`). All angles in degrees. +- `CoordinateAxis.cs` (`src/Kompas.Mcp.Core/Modeling/`) — enum `CoordinateAxis {X,Y,Z}` + `CoordinateAxes.ToObj3dType/Parse` (→ `o3d_axisOX/OY/OZ = 71/72/73`), analogous to `BasePlane.cs`. **Features** (3D-операции) - `extrude_boss` / `extrude_cut` — sketch, depth, direction, endType. @@ -151,6 +152,10 @@ MCP client ──stdio──> Host (поток-пул, async) - `rib` — построить ребро жёсткости от разомкнутого контура эскиза до тела. Параметры: `sketchId`, `thickness` (мм), `side` (`left|right|up|down` → 0/1/2/3), `symmetric` (толщина симметрична плоскости), `angle` (уклон стенок, °). Реализовано через `ksPart.NewEntity(o3d_ribOperation=44)` → `ksRibDefinition` (`SetSketch`, `SetThinParam`). `symmetric=true`: `SetThinParam(dtBoth, t/2, t/2)`; иначе `SetThinParam(dtNormal, t, 0)`. Контур должен не касаться тела концами — КОМПАС дотягивает полотно сам. - `sweep` — кинематическая операция: перемещение замкнутого профиля (эскиз `profileSketchId`) вдоль траектории (эскиз `pathSketchId`), образуя сплошное тело. Профиль и траектория на разных (обычно перпендикулярных) плоскостях. Реализовано через `ksPart.NewEntity(o3d_baseEvolution=45)` → `ksBaseEvolutionDefinition` (`SetSketch(профиль)` → `PathPartArray().Add(траектория)` → `sketchShiftType=0` → `SetThinParam(false)`) → `Create()`. Валидация: `profileSketchId != pathSketchId`. - `loft` — операция по сечениям: построить сплошное тело по ≥2 закрытым эскизам-сечениям на параллельных плоскостях. Реализовано через `ksPart.NewEntity(o3d_baseLoft=30)` → `ksBaseLoftDefinition` (`Sketchs().Add(каждое сечение)` → `SetLoftParam(closed=false,false,true)` → `SetThinParam(false)`) → `Create()`. Для сечений не на базовых плоскостях использовать `sketch_create_on_offset_plane`. +- `linear_pattern` — линейный массив операций вдоль координатной оси (`o3d_meshCopy=35`, `ksMeshCopyDefinition`); параметры: `featureIds[]`, `axis` (X/Y/Z), `count` (включает исходный, ≥2), `step` (мм). `SetAxis1` + `SetCopyParamAlongAxis(true,0,count,step,false)` + `count2=1` (выключает 2-е направление). +- `circular_pattern` — круговой массив вокруг координатной оси (`o3d_circularCopy=36`, `ksCircularCopyDefinition`); параметры: `featureIds[]`, `axis`, `count`, `step` (угол° между соседними), `reverse`, `geometric`. Свойства `count1=1`/`count2=count`/`step2=angle°`/`factor2=false`/`inverce=reverse` задаются напрямую; `GetOperationArray()`. +- `mirror_operation` — зеркальная копия операций (`o3d_mirrorOperation=48`, `ksMirrorCopyDefinition`); параметры: `featureIds[]`, `plane` (XOY/XOZ/YOZ). `SetPlane` + `GetOperationArray()`. +- `mirror_body` — зеркало всего тела (`o3d_mirrorAllOperation=49`, `ksMirrorCopyAllDefinition`); параметр: `plane`. `ChooseBodies()` не приводится к `ksChooseBodies` и не нужен — операция отражает все тела, сохраняя оригинал (объём удваивается). - `rebuild` — перестроить деталь. **Edit** (прямое редактирование) @@ -316,12 +321,11 @@ dotnet build -c Release -r win-x64 # сборка ## 10. Дорожная карта -**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B+package-E start):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), **вспомогательная геометрия (`sketch_create_on_offset_plane`)**, выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, **ребро жёсткости (`rib`)**, **кинематическая операция (`sweep`)**, **операция по сечениям (`loft`)**, снимок; `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`; `import_step`, `export_step`, `list_components`; **`move_face`**, **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`** (прямое редактирование и булевы операции); **`describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure`** (структурный осмотр модели). +**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B+package-C+package-E start):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), **вспомогательная геометрия (`sketch_create_on_offset_plane`)**, выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, **ребро жёсткости (`rib`)**, **кинематическая операция (`sweep`)**, **операция по сечениям (`loft`)**, **линейный/круговой массив и зеркало (`linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`)**, снимок; `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`; `import_step`, `export_step`, `list_components`; **`move_face`**, **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`** (прямое редактирование и булевы операции); **`describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure`** (структурный осмотр модели). **Следующие приоритеты:** 1. `draft` (уклон, требует API7), `hole`. -2. Пакет C «массивы» — `ILinearPattern`, `ICircularPattern`. -3. Пакет E «вспомогательная геометрия» (продолжение) — ось, точка, плоскость по трём точкам / по углу. +2. Пакет E «вспомогательная геометрия» (продолжение) — ось, точка, плоскость по трём точкам / по углу. 4. Рассечение/перемещение тела как MCP-инструменты (`SplitSolids`/`BodyRepositions`) — механика есть, продуктизация не закончена. 5. Свойства документа: `IPropertyMng` / `IPropertyKeeper`. 6. Полноценное 2D-черчение: виды, линии/дуги/окружности, размеры (`ILinearDimension`, …), штриховки, тексты. diff --git a/docs/OPEN_QUESTIONS.md b/docs/OPEN_QUESTIONS.md index a2eb2fc..c9b6d2c 100644 --- a/docs/OPEN_QUESTIONS.md +++ b/docs/OPEN_QUESTIONS.md @@ -143,4 +143,4 @@ stateful-сессии? Пока полагаемся на последовате - `describe_face` / `describe_edge` — drill-down по индексу. - `measure` — расстояние/угол между объектами (`ksMeasurer`). -Ключевые паттерны: дерево — `ksPart.GetFeature()` → `SubFeatureCollection(true,false)`; тела — `BodyCollection()` → `ksBody`; переменные — `VariableCollection()` → `ksVariable`. Детект «импорт без истории»: `bodyCount>0 && нет формообразующих операций && features.Count<=bodyCount+1`. Итог: **43 инструмента, 60 тестов.** +Ключевые паттерны: дерево — `ksPart.GetFeature()` → `SubFeatureCollection(true,false)`; тела — `BodyCollection()` → `ksBody`; переменные — `VariableCollection()` → `ksVariable`. Детект «импорт без истории»: `bodyCount>0 && нет формообразующих операций && features.Count<=bodyCount+1`. Итог: **62 инструмента, 101 тест.** diff --git a/docs/presentation.html b/docs/presentation.html index b5a4bea..2e481f7 100644 --- a/docs/presentation.html +++ b/docs/presentation.html @@ -98,7 +98,7 @@ .progrow{display:flex;justify-content:space-between;font-size:13px;color:var(--muted);margin-bottom:8px} /* roadmap timeline */ - .timeline{display:grid;grid-template-columns:repeat(6,1fr);gap:14px;margin-top:8px} + .timeline{display:grid;grid-template-columns:repeat(7,1fr);gap:14px;margin-top:8px} .tl{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px 16px;position:relative} .tl .ph{font-size:12px;font-weight:700;color:var(--accent)} .tl h4{font-size:15px;margin:8px 0 6px} @@ -111,7 +111,7 @@ .links{display:flex;gap:20px;flex-wrap:wrap;margin-top:10px} @media(max-width:1020px){ - .timeline{grid-template-columns:repeat(3,1fr)} + .timeline{grid-template-columns:repeat(4,1fr)} } @media(max-width:880px){ .grid,.stack{grid-template-columns:1fr 1fr} @@ -128,7 +128,7 @@
- Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+offset-plane (58 инструментов) + Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало (62 инструмента)

КОМПАС-3D MCP-сервер
управление CAD языком LLM

MCP-сервер, который превращает операции КОМПАС-3D — создание документов, эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом — @@ -261,7 +261,7 @@ впереди.

-
Общий прогрессv3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · offset-plane (пакет E start) · навык kompas-3d · 58 инструментов · 89 тестов
+
Общий прогрессv3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · offset-plane (пакет E start) · навык kompas-3d · 62 инструмента · 101 тест
@@ -297,18 +297,18 @@

Инструменты v3+ ГОТОВО

-

58 инструментов, отдаются по MCP-протоколу

+

62 инструмента, отдаются по MCP-протоколу

  • System: connect · status · set_visible
  • Documents: create · open · save · save_as · close · active
  • Sketch: create · on_face (точка/индекс) · on_offset_plane (смещённая плоскость) · add line/circle/arc/arc_3points/rectangle/axis/ellipse/polyline/polygon/spline/point · close
  • -
  • Features: extrude · revolve · fillet_edge/chamfer_edge (точка + индекс) · shell (оболочка) · rib (ребро жёсткости) · sweep (кинематическая) · loft (по сечениям) · rebuild
  • +
  • Features: extrude · revolve · fillet_edge/chamfer_edge (точка + индекс) · shell (оболочка) · rib (ребро жёсткости) · sweep (кинематическая) · loft (по сечениям) · linear_pattern · circular_pattern · mirror_operation · mirror_body · rebuild
  • Edit: move_face · split_solid_by_plane · move_body · boolean_union
  • Inspection: describe_model · list_features · list_bodies · list_variables · describe_face · describe_edge · measure
  • Vision: model_snapshot 👁️ (fallback)
  • Query: get_part_info · get_bounding_box · list_faces · list_edges · list_components
  • Conversion: import_step · export_step
  • -
  • draft (API7) · hole · пакет C (массивы) · пакет E (продолжение)
  • +
  • draft (API7) · hole · пакет E (продолжение)
@@ -319,7 +319,7 @@
  • Unit-тесты — без COM: диспетчер, enum-маппинг, StepFormat, InspectionText
  • Интеграционные тесты с КОМПАС: подключение, документы, снимок, цикл, STEP round-trip, сборка, move_face, инспекция
  • -
  • Итого 89 тестов зелёных (53 unit + 36 integration)
  • +
  • Итого 101 тест зелёный (60 unit + 41 integration)
  • Сборка kompas-mcp.exe, README с конфигом клиента
  • Навык kompas-3d (методика) + полигон usecases/
  • In-process тест MCP-клиента, релизный publish
  • @@ -342,7 +342,8 @@
    v3 · готово ✓

    B-rep + структурный осмотр

    move_face (сдвиг грани на N мм) · describe_model · list_features · list_bodies · list_variables · describe_face · describe_edge · measure

    пакет A · готово ✓

    Богатые эскизы

    arc · arc_3points · ellipse · polyline · polygon · spline · point (+7 инструментов); PartModeler разбит на partial-классы; SketchGeometry

    пакет B · готово ✓

    Формообразующие операции

    shell ✓ · rib ✓ · sweep ✓ · loft ✓ (по сечениям) · offset-plane ✓ (пакет E); draft · hole — впереди

    -
    далее · в работе

    draft · hole · пакет C

    draft (API7) · hole · пакет C (массивы: linear/circular) · пакет E продолжение (ось, угловая плоскость)

    +
    пакет C · готово ✓

    Массивы и зеркало

    linear_pattern ✓ · circular_pattern ✓ · mirror_operation ✓ · mirror_body

    +
    далее · в работе

    draft · hole · пакет E

    draft (API7) · hole · пакет E продолжение (ось, угловая плоскость)

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..0f2bebd --- /dev/null +++ b/docs/superpowers/specs/2026-05-26-pattern-mirror-design.md @@ -0,0 +1,128 @@ +# Дизайн: пакет 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 оставляет оригинал) — проверяются +этими тестами; при расхождении корректируем по факту, сохранив суть проверки. diff --git a/src/Kompas.Mcp.Core/Modeling/CoordinateAxis.cs b/src/Kompas.Mcp.Core/Modeling/CoordinateAxis.cs new file mode 100644 index 0000000..18a9042 --- /dev/null +++ b/src/Kompas.Mcp.Core/Modeling/CoordinateAxis.cs @@ -0,0 +1,30 @@ +using Kompas6Constants3D; + +namespace Kompas.Mcp.Core.Modeling; + +/// Базовая координатная ось (для направления массива / оси вращения). +public enum CoordinateAxis +{ + X, + Y, + Z, +} + +public static class CoordinateAxes +{ + public static Obj3dType ToObj3dType(CoordinateAxis axis) => axis switch + { + CoordinateAxis.X => Obj3dType.o3d_axisOX, + CoordinateAxis.Y => Obj3dType.o3d_axisOY, + CoordinateAxis.Z => Obj3dType.o3d_axisOZ, + _ => throw new ArgumentOutOfRangeException(nameof(axis), axis, "Неизвестная ось."), + }; + + public static CoordinateAxis Parse(string value) => value?.Trim().ToUpperInvariant() switch + { + "X" => CoordinateAxis.X, + "Y" => CoordinateAxis.Y, + "Z" => CoordinateAxis.Z, + _ => throw new ArgumentException($"Неизвестная ось '{value}'. Допустимо: X, Y, Z.", nameof(value)), + }; +} diff --git a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs index b8dbd0e..1299f7c 100644 --- a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs +++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs @@ -331,6 +331,155 @@ public sealed partial class PartModeler return id; }, ct); + /// Линейный массив (по сетке вдоль одной координатной оси): размножить операции + /// featureIds вдоль оси axis, count экземпляров всего (включая исходный, ≥2), шаг step (мм) + /// между соседними. geometric=true — быстрое геометрическое копирование. Возвращает id операции. + public Task LinearPatternAsync(IReadOnlyList featureIds, CoordinateAxis axis, int count, double step, bool geometric = false, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + ArgumentNullException.ThrowIfNull(featureIds); + var ids = featureIds.Distinct().ToList(); + if (ids.Count == 0) throw new ArgumentException("Нужна хотя бы одна операция для массива.", nameof(featureIds)); + SketchGeometry.RequireMin(count, 2, nameof(count)); // count включает исходный + SketchGeometry.RequirePositive(step, nameof(step)); // ловит и NaN + + // Собираем источники до мутации модели (проверка существования всех id). + var features = ids.Select(RequireFeature).ToList(); + + var part = GetTopPart(); + var axisEntity = part.GetDefaultEntity((short)CoordinateAxes.ToObj3dType(axis)) as ksEntity + ?? throw new InvalidOperationException("Не удалось получить координатную ось."); + + var entity = part.NewEntity((short)Obj3dType.o3d_meshCopy) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_meshCopy) вернул null."); + var def = entity.GetDefinition() as ksMeshCopyDefinition + ?? throw new InvalidOperationException("GetDefinition() массива вернул не ksMeshCopyDefinition."); + def.SetAxis1(axisEntity); + def.SetCopyParamAlongAxis(true, 0, count, step, false); // ось 1: angle=0, factor=false (шаг между соседними) + def.count2 = 1; // второе направление выключено (1D-массив) + def.geomArray = geometric; + var arr = def.OperationArray() as ksEntityCollection + ?? throw new InvalidOperationException("OperationArray() массива вернул не ksEntityCollection."); + foreach (var f in features) arr.Add(f); + + if (!entity.Create()) + throw new InvalidOperationException( + "Create() линейного массива вернул FALSE (копии самопересекаются или несовместимая геометрия?)."); + + var id = _nextId++; + _features[id] = entity; + return id; + }, ct); + + /// Круговой массив: размножить операции featureIds вокруг координатной оси axis, + /// count экземпляров всего (включая исходный, ≥2), угловой шаг step (градусы) между соседними. + /// reverse — обратное направление вращения. geometric=true — быстрое геометрическое копирование. + /// Возвращает id операции. + public Task CircularPatternAsync(IReadOnlyList featureIds, CoordinateAxis axis, int count, double step, bool reverse = false, bool geometric = false, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + ArgumentNullException.ThrowIfNull(featureIds); + var ids = featureIds.Distinct().ToList(); + if (ids.Count == 0) throw new ArgumentException("Нужна хотя бы одна операция для массива.", nameof(featureIds)); + SketchGeometry.RequireMin(count, 2, nameof(count)); // count включает исходный + SketchGeometry.RequirePositive(step, nameof(step)); // угловой шаг > 0, ловит NaN + + // Собираем источники до мутации модели (проверка существования всех id). + var features = ids.Select(RequireFeature).ToList(); + + var part = GetTopPart(); + var axisEntity = part.GetDefaultEntity((short)CoordinateAxes.ToObj3dType(axis)) as ksEntity + ?? throw new InvalidOperationException("Не удалось получить координатную ось."); + + var entity = part.NewEntity((short)Obj3dType.o3d_circularCopy) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_circularCopy) вернул null."); + var def = entity.GetDefinition() as ksCircularCopyDefinition + ?? throw new InvalidOperationException("GetDefinition() массива вернул не ksCircularCopyDefinition."); + def.SetAxis(axisEntity); + // Чистый круговой массив: радиальное направление выключено (count1=1), + // экземпляры идут по кольцу (count2=count) с угловым шагом step° между соседними. + def.count1 = 1; + def.step1 = 0; + def.factor1 = false; + def.count2 = count; + def.step2 = step; + def.factor2 = false; // шаг между соседними (не общий угол) + def.inverce = reverse; + def.geomArray = geometric; + var arr = def.GetOperationArray() as ksEntityCollection + ?? throw new InvalidOperationException("GetOperationArray() массива вернул не ksEntityCollection."); + foreach (var f in features) arr.Add(f); + + if (!entity.Create()) + throw new InvalidOperationException( + "Create() кругового массива вернул FALSE (копии самопересекаются или неверная ось?)."); + + var id = _nextId++; + _features[id] = entity; + return id; + }, ct); + + /// Зеркальная копия операций featureIds относительно координатной плоскости plane. + /// Возвращает id операции. + public Task MirrorOperationAsync(IReadOnlyList featureIds, BasePlane plane, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + ArgumentNullException.ThrowIfNull(featureIds); + var ids = featureIds.Distinct().ToList(); + if (ids.Count == 0) throw new ArgumentException("Нужна хотя бы одна операция для зеркала.", nameof(featureIds)); + + // Собираем источники до мутации модели (проверка существования всех id). + var features = ids.Select(RequireFeature).ToList(); + + var part = GetTopPart(); + var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity + ?? throw new InvalidOperationException("Не удалось получить координатную плоскость."); + + var entity = part.NewEntity((short)Obj3dType.o3d_mirrorOperation) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_mirrorOperation) вернул null."); + var def = entity.GetDefinition() as ksMirrorCopyDefinition + ?? throw new InvalidOperationException("GetDefinition() зеркала вернул не ksMirrorCopyDefinition."); + def.SetPlane(planeEntity); + var arr = def.GetOperationArray() as ksEntityCollection + ?? throw new InvalidOperationException("GetOperationArray() зеркала вернул не ksEntityCollection."); + foreach (var f in features) arr.Add(f); + + if (!entity.Create()) + throw new InvalidOperationException( + "Create() зеркальной копии вернул FALSE (копия пересекает исходную геометрию?)."); + + var id = _nextId++; + _features[id] = entity; + return id; + }, ct); + + /// Зеркально отразить всё тело относительно координатной плоскости plane (получить + /// симметричную деталь: оригинал + зеркало). Возвращает id операции. + public Task MirrorBodyAsync(BasePlane plane, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + var part = GetTopPart(); + var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity + ?? throw new InvalidOperationException("Не удалось получить координатную плоскость."); + + var entity = part.NewEntity((short)Obj3dType.o3d_mirrorAllOperation) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_mirrorAllOperation) вернул null."); + var def = entity.GetDefinition() as ksMirrorCopyAllDefinition + ?? throw new InvalidOperationException("GetDefinition() зеркала тела вернул не ksMirrorCopyAllDefinition."); + def.SetPlane(planeEntity); + // ChooseBodies() в этом interop не приводится к ksChooseBodies (даёт null), а вызов и не + // требуется: o3d_mirrorAllOperation по умолчанию отражает ВСЕ тела детали, сохраняя + // оригинал (проверено тестом — объём удваивается). Поэтому выбор тел не настраиваем. + + 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.Core/Modeling/PartModeler.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs index 353fb93..67de4ef 100644 --- a/src/Kompas.Mcp.Core/Modeling/PartModeler.cs +++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs @@ -108,6 +108,11 @@ public sealed partial class PartModeler : IDisposable => _sketches.TryGetValue(id, out var s) ? s : throw new KeyNotFoundException($"Эскиз с id={id} не найден."); + /// Получить ksEntity ранее созданной операции по её id (источник для массива/зеркала). + private ksEntity RequireFeature(int id) + => _features.TryGetValue(id, out var f) ? f + : throw new KeyNotFoundException($"Операция с id={id} не найдена."); + private SketchEntry RequireOpenSketch(int id) { var s = RequireSketch(id); diff --git a/src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs b/src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs index e849f38..1907fc6 100644 --- a/src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs +++ b/src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs @@ -34,6 +34,13 @@ public static class SketchGeometry throw new ArgumentOutOfRangeException(nameof(count), count, "Число вершин должно быть >= 3."); } + /// Требовать целое значение не меньше (например, число экземпляров массива). + public static void RequireMin(int value, int min, string paramName) + { + if (value < min) + throw new ArgumentOutOfRangeException(paramName, value, $"Значение должно быть >= {min}."); + } + /// Валидировать список точек: не null, минимум , все координаты конечны. public static void RequirePoints(IReadOnlyList<(double x, double y)> points, int min, string paramName) { diff --git a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs index 8772cbf..654b0a2 100644 --- a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs +++ b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs @@ -134,6 +134,56 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler) return $"Операция по сечениям создана ({sections} сечений), id={id}."; } + [McpServerTool(Name = "linear_pattern")] + [Description("Линейный массив: размножить ранее созданные операции featureIds (их id из extrude/revolve/fillet/...) вдоль координатной оси axis (X|Y|Z). count — количество экземпляров ВСЕГО, включая исходный (>=2). step — шаг между соседними экземплярами (мм). geometric=true — быстрое геометрическое копирование (может не строиться на скруглениях/фасках кривых поверхностей). Возвращает id операции.")] + public async Task LinearPattern( + [Description("Id операций-источников (из ранее созданных операций)")] int[] featureIds, + [Description("Координатная ось направления: X|Y|Z")] string axis, + [Description("Число экземпляров всего, включая исходный (>=2)")] int count, + [Description("Шаг между соседними экземплярами, мм")] double step, + [Description("Геометрическое копирование (быстрее)")] bool geometric = false) + { + await session.ConnectAsync(); + var id = await modeler.LinearPatternAsync(featureIds, CoordinateAxes.Parse(axis), count, step, geometric); + return $"Линейный массив создан ({count} экз. вдоль {axis}, шаг {step} мм), id={id}."; + } + + [McpServerTool(Name = "circular_pattern")] + [Description("Круговой массив: размножить ранее созданные операции featureIds вокруг координатной оси axis (X|Y|Z). count — количество экземпляров ВСЕГО, включая исходный (>=2). step — угловой шаг между соседними экземплярами, градусы (например 90 для 4 шт через 90°, или 120 для 3 шт). reverse=true — обратное направление вращения. geometric=true — быстрое геометрическое копирование. Возвращает id операции.")] + public async Task CircularPattern( + [Description("Id операций-источников")] int[] featureIds, + [Description("Координатная ось вращения: X|Y|Z")] string axis, + [Description("Число экземпляров всего, включая исходный (>=2)")] int count, + [Description("Угловой шаг между соседними, градусы")] double step, + [Description("Обратное направление вращения")] bool reverse = false, + [Description("Геометрическое копирование (быстрее)")] bool geometric = false) + { + await session.ConnectAsync(); + var id = await modeler.CircularPatternAsync(featureIds, CoordinateAxes.Parse(axis), count, step, reverse, geometric); + return $"Круговой массив создан ({count} экз. вокруг {axis}, шаг {step}°), id={id}."; + } + + [McpServerTool(Name = "mirror_operation")] + [Description("Зеркальная копия операций: отразить ранее созданные операции featureIds относительно координатной плоскости plane (XOY|XOZ|YOZ). Добавляет зеркальные копии (оригинал сохраняется). Возвращает id операции.")] + public async Task MirrorOperation( + [Description("Id операций-источников")] int[] featureIds, + [Description("Плоскость симметрии: XOY|XOZ|YOZ")] string plane) + { + await session.ConnectAsync(); + var id = await modeler.MirrorOperationAsync(featureIds, BasePlanes.Parse(plane)); + return $"Зеркальная копия операций создана (плоскость {plane}), id={id}."; + } + + [McpServerTool(Name = "mirror_body")] + [Description("Зеркально отразить всё тело детали относительно координатной плоскости plane (XOY|XOZ|YOZ) — получить симметричную деталь (оригинал + зеркало). Удобно для моделирования половины с последующим отражением. Возвращает id операции.")] + public async Task MirrorBody( + [Description("Плоскость симметрии: XOY|XOZ|YOZ")] string plane) + { + await session.ConnectAsync(); + var id = await modeler.MirrorBodyAsync(BasePlanes.Parse(plane)); + return $"Тело зеркально отражено (плоскость {plane}), id={id}."; + } + [McpServerTool(Name = "rebuild")] [Description("Перестроить активный документ.")] public async Task Rebuild() diff --git a/tests/Kompas.Mcp.Tests/CoordinateAxisTests.cs b/tests/Kompas.Mcp.Tests/CoordinateAxisTests.cs new file mode 100644 index 0000000..764fde4 --- /dev/null +++ b/tests/Kompas.Mcp.Tests/CoordinateAxisTests.cs @@ -0,0 +1,21 @@ +using Kompas.Mcp.Core.Modeling; + +namespace Kompas.Mcp.Tests; + +[Trait("Category", "Unit")] +public sealed class CoordinateAxisTests +{ + [Theory] + [InlineData("X", CoordinateAxis.X)] + [InlineData("y", CoordinateAxis.Y)] + [InlineData(" Z ", CoordinateAxis.Z)] + public void Parse_maps_known(string value, CoordinateAxis expected) + => Assert.Equal(expected, CoordinateAxes.Parse(value)); + + [Theory] + [InlineData("W")] + [InlineData("")] + [InlineData(null)] + public void Parse_throws_on_unknown(string? value) + => Assert.Throws(() => CoordinateAxes.Parse(value!)); +} diff --git a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs index e74662f..76f931b 100644 --- a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs +++ b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs @@ -4,7 +4,7 @@ using Kompas.Mcp.Core.Query; namespace Kompas.Mcp.Tests.Integration; -/// Интеграция: формообразующие операции пакета B. +/// Интеграция: формообразующие операции пакетов B и C (массивы, зеркало). [Trait("Category", "Integration")] [Collection(KompasCollection.Name)] public sealed class FeatureOpsTests @@ -136,6 +136,152 @@ public sealed class FeatureOpsTests finally { await _docs.CloseAsync(save: false); } } + [Fact] + public async Task LinearPattern_replicates_cylinder_along_X() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Цилиндр R5×H10 в центре (объём π·25·10 ≈ 785.4 мм³). + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddCircleAsync(s, 0, 0, 5); + await _modeler.CloseSketchAsync(s); + var cyl = await _modeler.ExtrudeAsync(s, depth: 10); + var one = (await _query.GetPartInfoAsync()).Volume; + + // Линейный массив: 3 экземпляра вдоль X с шагом 30 (R5 → не перекрываются). + var id = await _modeler.LinearPatternAsync(new[] { cyl }, CoordinateAxis.X, count: 3, step: 30); + Assert.True(id > 0); + await _modeler.RebuildAsync(); + + // count=3 включает исходный → суммарный объём ≈ 3 цилиндра. + var v = (await _query.GetPartInfoAsync()).Volume; + Assert.InRange(v, one * 3 * 0.95, one * 3 * 1.05); + + // Габарит вдоль X: копии в 0/30/60, R5 → от −5 до 65 → размер ≈70 (знак шага не важен). + var box = await _query.GetBoundingBoxAsync(); + Assert.InRange(box.SizeX, 70 * 0.95, 70 * 1.05); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task CircularPattern_replicates_cylinder_around_Z() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Цилиндр R3×H10 со смещением: центр (20,0) (объём π·9·10 ≈ 282.7 мм³). + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddCircleAsync(s, 20, 0, 3); + await _modeler.CloseSketchAsync(s); + var cyl = await _modeler.ExtrudeAsync(s, depth: 10); + var one = (await _query.GetPartInfoAsync()).Volume; + + // Круговой массив вокруг Z: 3 экз. с шагом 90° → центры (20,0),(0,20),(-20,0). + var id = await _modeler.CircularPatternAsync(new[] { cyl }, CoordinateAxis.Z, count: 3, step: 90); + Assert.True(id > 0); + await _modeler.RebuildAsync(); + + var v = (await _query.GetPartInfoAsync()).Volume; + Assert.InRange(v, one * 3 * 0.95, one * 3 * 1.05); + + // Габарит: X от -23 до 23 → 46; Y от -3 до 23 → 26 (поворот реально произошёл). + var box = await _query.GetBoundingBoxAsync(); + Assert.InRange(box.SizeX, 46 * 0.95, 46 * 1.05); + Assert.InRange(box.SizeY, 26 * 0.95, 26 * 1.05); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task CircularPattern_reverse_flips_rotation_direction() + { + // count=2 (исходный + 1 копия на 90°): forward и reverse уводят копию в РАЗНЫЕ + // стороны по Y, поэтому MaxY габарита заметно различается (проводка def.inverce). + async Task CopyMaxY(bool reverse) + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddCircleAsync(s, 20, 0, 3); // цилиндр R3 центр (20,0) + await _modeler.CloseSketchAsync(s); + var cyl = await _modeler.ExtrudeAsync(s, depth: 10); + await _modeler.CircularPatternAsync(new[] { cyl }, CoordinateAxis.Z, count: 2, step: 90, reverse: reverse); + await _modeler.RebuildAsync(); + return (await _query.GetBoundingBoxAsync()).MaxY; + } + finally { await _docs.CloseAsync(save: false); } + } + + var fwd = await CopyMaxY(reverse: false); + var rev = await CopyMaxY(reverse: true); + // Одно направление: копия в (0,+20) → MaxY≈23; другое: в (0,−20) → MaxY≈3. Разница ~20. + Assert.True(Math.Abs(fwd - rev) > 10, $"reverse не меняет направление вращения: fwd MaxY={fwd:F1}, rev MaxY={rev:F1}"); + } + + [Fact] + public async Task MirrorOperation_mirrors_two_features_across_YOZ() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Две отдельные операции (проверяем заполнение коллекции несколькими источниками): + // цилиндр R3 центр (20,0) и цилиндр R2 центр (10,0), оба H10. + var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddCircleAsync(s1, 20, 0, 3); + await _modeler.CloseSketchAsync(s1); + var c1 = await _modeler.ExtrudeAsync(s1, depth: 10); + + var s2 = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddCircleAsync(s2, 10, 0, 2); + await _modeler.CloseSketchAsync(s2); + var c2 = await _modeler.ExtrudeAsync(s2, depth: 10); + var before = (await _query.GetPartInfoAsync()).Volume; // π·9·10 + π·4·10 ≈ 408.4 + + // Зеркало обеих операций относительно YOZ (x=0) → копии в (-20,0),(-10,0). + var id = await _modeler.MirrorOperationAsync(new[] { c1, c2 }, BasePlane.YOZ); + Assert.True(id > 0); + await _modeler.RebuildAsync(); + + var v = (await _query.GetPartInfoAsync()).Volume; // удвоение + Assert.InRange(v, before * 2 * 0.95, before * 2 * 1.05); + + // Габарит X симметричен: от -23 до 23 → 46. + var box = await _query.GetBoundingBoxAsync(); + Assert.InRange(box.SizeX, 46 * 0.95, 46 * 1.05); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task MirrorBody_makes_symmetric_part_across_YOZ() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Цилиндр R3×H10 центр (20,0) (объём ≈ 282.7 мм³). + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddCircleAsync(s, 20, 0, 3); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: 10); + var before = (await _query.GetPartInfoAsync()).Volume; + + // Зеркально отразить тело относительно YOZ → оригинал + зеркало в (-20,0). + var id = await _modeler.MirrorBodyAsync(BasePlane.YOZ); + Assert.True(id > 0); + await _modeler.RebuildAsync(); + + var v = (await _query.GetPartInfoAsync()).Volume; + Assert.InRange(v, before * 2 * 0.95, before * 2 * 1.05); + + var box = await _query.GetBoundingBoxAsync(); + Assert.InRange(box.SizeX, 46 * 0.95, 46 * 1.05); // от -23 до 23 + } + finally { await _docs.CloseAsync(save: false); } + } + [Fact] public async Task Loft_two_squares_makes_solid() { diff --git a/tests/Kompas.Mcp.Tests/SketchGeometryTests.cs b/tests/Kompas.Mcp.Tests/SketchGeometryTests.cs index 75631c6..a5e41ef 100644 --- a/tests/Kompas.Mcp.Tests/SketchGeometryTests.cs +++ b/tests/Kompas.Mcp.Tests/SketchGeometryTests.cs @@ -45,6 +45,14 @@ public sealed class SketchGeometryTests Assert.Null(Record.Exception(() => SketchGeometry.RequireVertexCount(3))); } + [Fact] + public void RequireMin_throws_below_min() + { + Assert.Throws(() => SketchGeometry.RequireMin(1, 2, "count")); + Assert.Null(Record.Exception(() => SketchGeometry.RequireMin(2, 2, "count"))); + Assert.Null(Record.Exception(() => SketchGeometry.RequireMin(5, 2, "count"))); + } + [Fact] public void RequirePoints_throws_on_null() => Assert.Throws(