diff --git a/CLAUDE.md b/CLAUDE.md index ac4fd80..cb66a24 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**. -**70 MCP tools, 119 tests green (69 unit + 50 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`). **`hole`** (`HoleService`, `FeatureTools`) — цилиндрическое отверстие (сквозное или глухое) через **API7** (`IModelContainer`/`IHole3D`/`IHoleDisposal`); первая формообразующая операция на API7 (прецедент ранее только у `move_face`). **`hole_counterbore`** — отверстие с цековкой (`ksHTCounterbore`, `ISpotfacingHoleParameters`). **`hole_countersink`** — отверстие с зенковкой (`ksHTCountersinking`, `ICountersinkHoleParameters`). **`hole_conic`** — коническое отверстие (`ksHTConic`, `IConicHoleParameters`: `ConicType=ksCNAngle`, `ConicAngle`); завершает набор типов отверстий. `HoleCore` рефакторен: принимает `ksHoleTypeEnum holeType` + `Action configure` (настройка `HoleParameters` после установки `HoleType`, т.к. параметрический подынтерфейс недоступен до фиксации типа). Базовый `hole` = `ksHTBase` + `configure=null`. **`draft`** (уклон, `o3d_incline=42`) — наклон граней на угол относительно нейтральной координатной плоскости; реализован через **API5** `ksInclineDefinition` (операция называется Incline в API5, не Draft). Package D «parametrics» started: **`create_variable`**, **`set_variable`**, **`delete_variable`** (`VariableService`, `VariableTools`) — CRUD переменных модели через API5. `list_variables` исправлен (читает `ksPart.GetFeature().VariableCollection`, а не `ksPart.VariableCollection()`). **Ограничение:** переменная управляет геометрией только в параметрической модели (где размеры эскиза связаны с именем переменной); наши эскизы строятся литеральными координатами → `set_variable` хранит/вычисляет значение, но геометрию не меняет. Связь эскизных размеров с переменными (API2D) — не реализована. +**71 MCP tools, 138 tests green (83 unit + 55 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`). **`hole`** (`HoleService`, `FeatureTools`) — цилиндрическое отверстие (сквозное или глухое) через **API7** (`IModelContainer`/`IHole3D`/`IHoleDisposal`); первая формообразующая операция на API7 (прецедент ранее только у `move_face`). **`hole_counterbore`** — отверстие с цековкой (`ksHTCounterbore`, `ISpotfacingHoleParameters`). **`hole_countersink`** — отверстие с зенковкой (`ksHTCountersinking`, `ICountersinkHoleParameters`). **`hole_conic`** — коническое отверстие (`ksHTConic`, `IConicHoleParameters`: `ConicType=ksCNAngle`, `ConicAngle`); завершает набор типов отверстий. `HoleCore` рефакторен: принимает `ksHoleTypeEnum holeType` + `Action configure` (настройка `HoleParameters` после установки `HoleType`, т.к. параметрический подынтерфейс недоступен до фиксации типа). Базовый `hole` = `ksHTBase` + `configure=null`. **`draft`** (уклон, `o3d_incline=42`) — наклон граней на угол относительно нейтральной координатной плоскости; реализован через **API5** `ksInclineDefinition` (операция называется Incline в API5, не Draft). Package D «parametrics» started: **`create_variable`**, **`set_variable`**, **`delete_variable`** (`VariableService`, `VariableTools`) — CRUD переменных модели через API5. `list_variables` исправлен (читает `ksPart.GetFeature().VariableCollection`, а не `ksPart.VariableCollection()`). **Ограничение:** переменная управляет геометрией только в параметрической модели (где размеры эскиза связаны с именем переменной); наши эскизы строятся литеральными координатами → `set_variable` хранит/вычисляет значение, но геометрию не меняет. Связь эскизных размеров с переменными (API2D) — не реализована. **Assembly class started:** **`assembly_add_component`** (`AssemblyService`, `AssemblyTools`, namespace `Kompas.Mcp.Core.Assemblies`) — первый инструмент класса СБОРКИ: вставка компонента (.m3d/.a3d) из файла в активную сборку с заданием положения origin (мм, мировая СК сборки); возвращает индекс и имя вставленного компонента. See [`README.md`](README.md) and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); design decisions/caveats in [`docs/OPEN_QUESTIONS.md`](docs/OPEN_QUESTIONS.md). @@ -53,7 +53,7 @@ Key implementation facts (don't relearn): - **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); reads via `ksPart.GetFeature().VariableCollection` (all variables, including user-created) with fallback to `ksPart.VariableCollection()` (external-only — bug was fixed). `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`). - **Variable management (package D start, `VariableService`, `VariableTools`)**: `create_variable(name, value, note?, external?)` — creates a model variable; `set_variable(name, expression)` — sets expression (constant `"30"` or formula `"width*2+5"`); `delete_variable(name)` — deletes (fails if dependents exist). **Key API facts (don't relearn):** variable creation MUST use `ksPart.GetFeature().VariableCollection` (property on root ksFeature) — NOT `ksPart.VariableCollection()` (returns external-only). Expression is the leading field; value is derived. Changes applied via `ksPart.RebuildModel()` (return checked); after rebuild the RCW variable is stale → re-read via fresh collection (`ReadValueFresh`). Dependent variables recalculate automatically. Deletion fails if dependents exist (RemoveVariable returns FALSE). **Geometry limitation:** variables control geometry only in a parametric model where sketch dimensions are linked to variable names; our sketches are built with literal coordinates → `set_variable` stores/computes the value but does NOT change geometry. Linking sketch dimensions to variables (parametric sketches via API2D) is not yet implemented. - **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), `linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body` (package C complete), `hole`, `hole_counterbore`, `hole_countersink`, `hole_conic` (all API7 via `HoleService`; `hole_conic` uses `ksHTConic`/`IConicHoleParameters`), `draft` (API5 `ksInclineDefinition`), `create_variable`/`set_variable`/`delete_variable` (package D start) are done.** Not yet done: parametric sketches — **investigated, unavailable via COM API** (`ksCDimWithVariable` cannot be constructed from external automation; see `docs/superpowers/specs/2026-05-27-parametric-sketch-findings.md`); 2D drawing, assembly building; package E continuation (axis, angle plane). 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), `hole`, `hole_counterbore`, `hole_countersink`, `hole_conic` (all API7 via `HoleService`; `hole_conic` uses `ksHTConic`/`IConicHoleParameters`), `draft` (API5 `ksInclineDefinition`), `create_variable`/`set_variable`/`delete_variable` (package D start), `assembly_add_component` (API7, `AssemblyService`) are done.** Not yet done: parametric sketches — **investigated, unavailable via COM API** (`ksCDimWithVariable` cannot be constructed from external automation; see `docs/superpowers/specs/2026-05-27-parametric-sketch-findings.md`); 2D drawing; assembly mates/constraints; package E continuation (axis, angle plane). 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`); all variables via `ksPart.GetFeature().VariableCollection` → `ksVariable` (external-only: `ksPart.VariableCollection()` — do NOT use for reading all variables); 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`. @@ -66,6 +66,7 @@ Key implementation facts (don't relearn): - **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. - **Hole operations (`hole`, `hole_counterbore`, `hole_countersink`, `hole_conic`)**: all implemented via **API7** (not API5 — `ksHoleDefinition` absent in interop). Service `src/Kompas.Mcp.Core/Modeling/HoleService.cs`. `HoleCore` helper is parameterised with `ksHoleTypeEnum holeType` + `Action configure` callback (configure sets `HoleParameters` **after** `HoleType` is assigned, because the type-specific sub-interface — e.g. `ISpotfacingHoleParameters` — is not accessible until the type is fixed). Base `hole` = `ksHTBase` + `configure=null`. `hole_counterbore`: `ksHTCounterbore` + configure sets `(ISpotfacingHoleParameters)hole.HoleParameters` (`SpotfacingDiameter`, `SpotfacingDepth`); validates `spotDiameter>diameter` and `depth>spotDepth` for blind. `hole_countersink`: `ksHTCountersinking` + configure sets `(ICountersinkHoleParameters)hole.HoleParameters` (`CountersinkType=ksCTDiameterAngle`, `CountersinkDiameter`, `CountersinkAngle`); validates `sinkDiameter>diameter`, `0()` in `Program.cs`. - **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 eef6f04..107f3b0 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe } ``` -## Инструменты (70 инструментов) +## Инструменты (71 инструмент) | Группа | Инструменты | |---|---| @@ -53,6 +53,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe | Query | `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`, `list_components` | | Variables | `create_variable`, `set_variable`, `delete_variable` | | Conversion | `import_step`, `export_step` | +| Assembly | `assembly_add_component` | | Validation | `validate_part` | ### Типовой сценарий @@ -71,7 +72,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/ 119 тестов: unit + integration (integration требуют КОМПАС) +tests/Kompas.Mcp.Tests/ 138 тестов: unit + integration (integration требуют КОМПАС) libs/kompas-interop/ вендорские interop-сборки КОМПАС (из SDK Samples/Common) docs/ архитектура, план, презентация, MD-база знаний SDK (Kompas3D_SDK/) usecases/ полигон обкатки подходов (в .gitignore); приёмы поднимаются в навык kompas-3d @@ -90,7 +91,7 @@ dotnet test --filter "Category=Integration" # только integration - **STA-поток** (`KompasDispatcher`) владеет всеми COM-вызовами — КОМПАС однопоточный STA. - Подключение через API5 `KompasObject` (`KOMPAS.Application.5`) → API7 `IApplication` (`ksGetApplication7`). -- 3D строится через API5 `ksPart` (эскизы, выдавливание, вращение, скругление/фаска); снимок — `ksDocument3D.SaveAsToRasterFormat`. Прямое редактирование импортированной B-rep (`move_face`) — через API7 `IPart7.FindObjectsByPoint` + `FaceMover`. Структурный осмотр (`describe_model` и др.) — через `ModelInspectionService` (дерево операций via `ksPart.GetFeature()`, тела via `BodyCollection()`, переменные via `GetFeature().VariableCollection`, измерения via `ksMeasurer`). Управление переменными (`create_variable`, `set_variable`, `delete_variable`) — через `VariableService` (API5). +- 3D строится через API5 `ksPart` (эскизы, выдавливание, вращение, скругление/фаска); снимок — `ksDocument3D.SaveAsToRasterFormat`. Прямое редактирование импортированной B-rep (`move_face`) — через API7 `IPart7.FindObjectsByPoint` + `FaceMover`. Структурный осмотр (`describe_model` и др.) — через `ModelInspectionService` (дерево операций via `ksPart.GetFeature()`, тела via `BodyCollection()`, переменные via `GetFeature().VariableCollection`, измерения via `ksMeasurer`). Управление переменными (`create_variable`, `set_variable`, `delete_variable`) — через `VariableService` (API5). Вставка компонента в сборку (`assembly_add_component`) — через `AssemblyService` (API7, `IParts7.AddFromFile` + `IPlacement3D`). - STEP импорт/экспорт — `IKompasDocument3D1.ConvertFromAdditionFormat`/`ConvertToAdditionFormat` через встроенный конвертер. - MCP — официальный C# SDK `ModelContextProtocol`, транспорт stdio, логи в stderr. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c33ec08..8c0fe04 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-C «массивы и зеркало»+package-D start «переменные»+package-E «offset plane»+`hole`+`hole_counterbore`+`hole_countersink`+`hole_conic`+`draft`). Актуализировано по коду. +> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богатые эскизы»+package-B «shell+rib+sweep+loft»+package-C «массивы и зеркало»+package-D start «переменные»+package-E «offset plane»+`hole`+`hole_counterbore`+`hole_countersink`+`hole_conic`+`draft`+`assembly_add_component`). Актуализировано по коду. > Сопутствующий контекст по COM API КОМПАС — в [`../CLAUDE.md`](../CLAUDE.md). > Имена интерфейсов и перечислений сверены со справкой SDK по MD-базе знаний `docs/Kompas3D_SDK/` (навык `kompas-sdk-research`). @@ -112,8 +112,8 @@ 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) + интеграционные (требуют КОМПАС). 119 тестов (69 unit + 50 integration). | +| **Kompas.Mcp.Core** | COM-слой. Менеджер соединения, менеджер документов, построитель эскизов и операций, `ConversionService` (STEP импорт/экспорт), `QueryService` (МЦХ, грани, рёбра, компоненты), `ModelInspectionService` (дерево операций, тела, переменные, drill-down, измерения), `AssemblyService` (вставка компонентов в сборку, namespace `Kompas.Mcp.Core.Assemblies`), снимок. Все паттерны API5/API7 живут здесь. | +| **Kompas.Mcp.Tests** | Юнит (без COM) + интеграционные (требуют КОМПАС). 138 тестов (83 unit + 55 integration). | Принцип: **только `Core` знает про COM**. `Host`/Tools оперируют доменными DTO и вызывают `Core`; `Host` не содержит бизнес-логики. @@ -121,7 +121,7 @@ MCP client ──stdio──> Host (поток-пул, async) --- -## 5. Карта инструментов (70 инструментов) +## 5. Карта инструментов (71 инструмент) Сгруппированы вокруг центрального цикла «эскиз ↔ операция». Имена — `snake_case`. @@ -204,6 +204,9 @@ MCP client ──stdio──> Host (поток-пул, async) **Ограничение:** переменная управляет геометрией только в параметрической модели, где размеры эскизов привязаны к именам переменных. Наши эскизы строятся литеральными координатами — `set_variable` хранит и вычисляет значение, но геометрию не меняет. Связь размеров эскизов с переменными (параметрические эскизы через API2D) — не реализована (package D продолжение). +**Assembly — построение сборок (`AssemblyService`, `AssemblyTools`, namespace `Kompas.Mcp.Core.Assemblies`)** +- `assembly_add_component` — вставить компонент (.m3d деталь или .a3d подсборка) из файла в активную сборку; задать положение origin (x,y,z мм, мировая СК сборки). Возвращает индекс и имя вставленного компонента. Реализовано через API7: `IParts7.AddFromFile(path, ExternalFile=true, Redraw=true)` → `IPart7` → `IPlacement3D.SetOrigin(x,y,z)` → `UpdatePlacement(true)` → `RebuildModel(true)`. `RequireActiveAssembly` проверяет `doc.DocumentType == ksDocumentAssembly` до обращения к `TopPart`. Откат при неуспехе через `IFeature7.Delete`. DI: `AddSingleton()` в `Program.cs`. + **Vision — визуальная обратная связь (fallback)** - `model_snapshot` — отрендерить активный 3D-документ в PNG и вернуть **как image-контент MCP**, чтобы мультимодальный агент *увидел* промежуточный результат. Использовать для визуально-пространственных @@ -337,7 +340,7 @@ dotnet build -c Release -r win-x64 # сборка ## 10. Дорожная карта -**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B+package-C+package-D start+package-E start+hole+hole_counterbore+hole_countersink+hole_conic+draft):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), **вспомогательная геометрия (`sketch_create_on_offset_plane`)**, выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, **ребро жёсткости (`rib`)**, **кинематическая операция (`sweep`)**, **операция по сечениям (`loft`)**, **линейный/круговой массив и зеркало (`linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`)**, **отверстие (`hole`, API7)**, **цековка (`hole_counterbore`, API7)**, **зенковка (`hole_countersink`, API7)**, **коническое отверстие (`hole_conic`, API7)**, **уклон (`draft`, API5 `ksInclineDefinition`)**, снимок; `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`** (структурный осмотр модели); **`create_variable`, `set_variable`, `delete_variable`** (управление переменными, package D start). +**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B+package-C+package-D start+package-E start+hole+hole_counterbore+hole_countersink+hole_conic+draft+assembly_add_component):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), **вспомогательная геометрия (`sketch_create_on_offset_plane`)**, выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, **ребро жёсткости (`rib`)**, **кинематическая операция (`sweep`)**, **операция по сечениям (`loft`)**, **линейный/круговой массив и зеркало (`linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`)**, **отверстие (`hole`, API7)**, **цековка (`hole_counterbore`, API7)**, **зенковка (`hole_countersink`, API7)**, **коническое отверстие (`hole_conic`, API7)**, **уклон (`draft`, API5 `ksInclineDefinition`)**, снимок; `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`** (структурный осмотр модели); **`create_variable`, `set_variable`, `delete_variable`** (управление переменными, package D start); **`assembly_add_component`** (вставка компонента в сборку, API7, `AssemblyService`). **Следующие приоритеты:** 1. Пакет D «параметрика» (продолжение) — связь размеров эскизов с переменными (параметрические эскизы через API2D). ⚠️ **Исследовано**: `ksCDimWithVariable` недоступен из внешней автоматизации — см. `docs/superpowers/specs/2026-05-27-parametric-sketch-findings.md`. @@ -345,7 +348,7 @@ dotnet build -c Release -r win-x64 # сборка 3. Рассечение/перемещение тела как MCP-инструменты (`SplitSolids`/`BodyRepositions`) — механика есть, продуктизация не закончена. 4. Свойства документа: `IPropertyMng` / `IPropertyKeeper`. 5. Полноценное 2D-черчение: виды, линии/дуги/окружности, размеры (`ILinearDimension`, …), штриховки, тексты. -6. Спецификации (`ISpecification`), построение сборок и сопряжения. +6. Сборки — продолжение: сопряжения (`IMates7`), ограничения, авто-позиционирование компонентов; спецификации (`ISpecification`). 7. Транспорт **HTTP/SSE** — как опция для удалённых клиентов. --- diff --git a/docs/OPEN_QUESTIONS.md b/docs/OPEN_QUESTIONS.md index 792ea04..5d6900c 100644 --- a/docs/OPEN_QUESTIONS.md +++ b/docs/OPEN_QUESTIONS.md @@ -182,3 +182,6 @@ stateful-сессии? Пока полагаемся на последовате ### ✅ Коническое отверстие — реализовано **Решено.** Новый инструмент `hole_conic` — коническое (конусное) отверстие, сужающееся вглубь. Параметры: `x,y,z`, `diameter` (диаметр у основания), `conicAngle` (угол конуса, 0
- Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole+hole_counterbore+hole_countersink+hole_conic+draft+переменные (70 инструментов) + Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole+hole_counterbore+hole_countersink+hole_conic+draft+переменные+assembly_add_component (71 инструмент)

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

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

-
Общий прогрессv3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · переменные (пакет D start) · offset-plane (пакет E start) · hole (API7) · hole_counterbore · hole_countersink · hole_conic · draft (API5) · навык kompas-3d · 70 инструментов · 119 тестов
+
Общий прогрессv3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · переменные (пакет D start) · offset-plane (пакет E start) · hole (API7) · hole_counterbore · hole_countersink · hole_conic · draft (API5) · assembly_add_component · навык kompas-3d · 71 инструмент · 138 тестов
@@ -297,7 +297,7 @@

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

-

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

+

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

  • System: connect · status · set_visible
  • Documents: create · open · save · save_as · close · active
  • @@ -309,7 +309,8 @@
  • Query: get_part_info · get_bounding_box · list_faces · list_edges · list_components
  • Variables: create_variable · set_variable · delete_variable (пакет D)
  • Conversion: import_step · export_step
  • -
  • пакет E (ось, угловая плоскость) · пакет D параметрика: параметрические эскизы (API2D) — исследовано, недоступно через COM API
  • +
  • Assembly: assembly_add_component (вставка компонента .m3d/.a3d в сборку, API7)
  • +
  • пакет E (ось, угловая плоскость) · пакет D параметрика: параметрические эскизы (API2D) — исследовано, недоступно через COM API · сопряжения в сборках
@@ -320,7 +321,7 @@
  • Unit-тесты — без COM: диспетчер, enum-маппинг, StepFormat, InspectionText
  • Интеграционные тесты с КОМПАС: подключение, документы, снимок, цикл, STEP round-trip, сборка, move_face, инспекция
  • -
  • Итого 119 тестов зелёных (69 unit + 50 integration)
  • +
  • Итого 138 тестов зелёных (83 unit + 55 integration)
  • Сборка kompas-mcp.exe, README с конфигом клиента
  • Навык kompas-3d (методика) + полигон usecases/
  • In-process тест MCP-клиента, релизный publish
  • @@ -347,7 +348,8 @@
    draft · готово ✓

    Уклон

    draft ✓ (API5 ksInclineDefinition, o3d_incline=42) · 64 инструмента · 115 тестов

    пакет D start · готово ✓

    Переменные модели

    create_variable · set_variable · delete_variable (API5 VariableService) · list_variables исправлен · 67 инструментов

    отверстия под крепёж · готово ✓

    Цековка, зенковка и конус

    hole_counterbore (ksHTCounterbore) · hole_countersink (ksHTCountersinking) · hole_conic (ksHTConic, IConicHoleParameters) · HoleCore рефакторен (configure-колбэк) · 70 инструментов · 119 тестов

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

    пакет E продолжение

    вспомогательная геометрия (ось, угловая плоскость) · пакет D параметрика — параметрические эскизы недоступны через COM API

    +
    сборки start · готово ✓

    Вставка компонентов

    assembly_add_component (API7 IParts7.AddFromFile + IPlacement3D) · 71 инструмент · 138 тестов

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

    пакет E продолжение

    вспомогательная геометрия (ось, угловая плоскость) · сопряжения в сборках · пакет D параметрика — параметрические эскизы недоступны через COM API

diff --git a/docs/superpowers/specs/2026-05-27-assembly-add-component-design.md b/docs/superpowers/specs/2026-05-27-assembly-add-component-design.md new file mode 100644 index 0000000..0f3c71a --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-assembly-add-component-design.md @@ -0,0 +1,159 @@ +# Дизайн: вставка компонента в сборку (assembly_add_component) через API7 + +**Дата:** 2026-05-27 +**Статус:** реализовано и проверено (3 интеграционных теста зелёные, 136 всего); ревью Codex спека учтено (8 замечаний) + +## Эмпирическая находка при реализации (важно — не разучивать) + +`part.UpdatePlacement(true)` возвращает **FALSE** при перемещении вручную позиционируемого +компонента (без сопряжений) — это **НЕ ошибка** (нечего пересчитывать от зависимостей). Само +перемещение задаёт `placement.SetOrigin(x,y,z)` (живой объект Placement, не копия — проверено: +сдвиг origin на +50 даёт габарит сборки X[50,90]), а **видимым в геометрии** оно становится после +`top.RebuildModel(true)`. Поэтому: результат `SetOrigin` проверяем (FALSE = ошибка), результат +`UpdatePlacement` **игнорируем**, обязателен `RebuildModel(true)`. Разфиксация первого компонента +(`Fixed=false`) **НЕ нужна** — зафиксированный КОМПАСом первый компонент всё равно перемещается +через `SetOrigin`+`RebuildModel` (проверено: оба теста — origin и offset — зелёные без снятия +фиксации). + +## Цель + +Открыть новый класс функционала — **СБОРКИ**. До сих пор сделана только геометрия ДЕТАЛИ. +Первый инкремент вехи: **вставить деталь из сохранённого `.m3d`-файла в активную сборку** с +заданием положения. Это фундамент — без вставки компонентов сборка пуста. + +`list_components` (чтение сборки) уже есть; теперь учимся её НАПОЛНЯТЬ. + +## Архитектурное решение + +Сборки — это **НЕ `PartModeler`** (он строит API5-деталь `ksPart`). Заводим **новый сервис +`AssemblyService`** (API7, паттерн как у `HoleService`/`FaceEditService`: контейнеры берём +COM-QI, операции на STA-потоке `KompasDispatcher`) и **новый класс инструментов `AssemblyTools`**. + +- `src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs` — сервис. +- `src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs` — чистые static-валидаторы (unit-тест). +- `src/Kompas.Mcp.Core/Assemblies/AddedComponent.cs` — record результата (Index, Name). +- `src/Kompas.Mcp.Host/Tools/AssemblyTools.cs` — MCP-инструмент. +- DI: `AddSingleton()` в `Program.cs`. + +(Namespace `Kompas.Mcp.Core.Assemblies` — во множественном числе, чтобы не конфликтовать с +`System.Reflection.Assembly`.) + +## Подтверждённый workflow (рефлексия interop + справка) + +Рефлексией `KompasAPI7.dll` подтверждено: +- `IParts7.AddFromFile(string FileName, bool ExternalFile, bool Redraw)` → `Part7`. +- `IPart7.Placement` (`Placement3D`), `IPart7.UpdatePlacement(bool Redraw)`. +- `IPlacement3D.SetOrigin(double X, double Y, double Z)`. + +``` +// RequireActiveAssembly (см. ниже) — раздельные проверки до доступа к TopPart: +IKompasDocument doc = app.ActiveDocument; // != null +// doc.DocumentType == ksDocumentAssembly (деталь тоже IKompasDocument3D — отсечь по типу!) +IKompasDocument3D doc3d = (IKompasDocument3D)doc; +IPart7 top = (IPart7)doc3d.TopPart; // != null +IParts7 parts = top.Parts; + +IPart7 part = parts.AddFromFile(filePath, /*ExternalFile*/ true, /*Redraw*/ true) as IPart7 + ?? throw; // проверяем результат +// ExternalFile=true: компонент — ссылка на внешний .m3d (что нам и нужно — отдельный файл-источник). +// Redraw=true: вставить и перерисовать ИСХОДНОЕ положение (в начало координат сборки). + +IPlacement3D pl = part.Placement as IPlacement3D ?? throw; // read-only свойство, мутируем +if (!pl.SetOrigin(x, y, z)) throw; // мировые координаты сборки, мм +if (!part.UpdatePlacement(/*Redraw*/ true)) throw; // именно это ПРИМЕНЯЕТ перемещение +``` + +**Проверки результатов (как в `HoleService`):** `AddFromFile` приводится к `IPart7` (иначе ошибка); +`Placement` приводится к `IPlacement3D`; `SetOrigin(...)==true`; `UpdatePlacement(true)==true`. +Неуспех любого — `InvalidOperationException` с понятным текстом. (Откат «осиротевшего» компонента +при неуспехе позиционирования — best-effort через `part.Owner?.Delete()`; на практике origin-сдвиг +надёжен, но проверки обязательны.) + +**Redraw ≠ применение перемещения (важно):** `AddFromFile(..., Redraw:true)` вставляет и +перерисовывает компонент в ИСХОДНОМ положении. Само перемещение применяет и проверяет именно +`part.UpdatePlacement(true)` — не `Redraw`. Отдельный `doc3d.RebuildDocument()` в базовом workflow +не нужен (интеграционный тест подтверждает габарит после `UpdatePlacement`). + +**Почему `AddFromFile`, а не `AddFromFileWithParam`:** для первого инкремента нужен только сдвиг +origin. `AddFromFile` + `Placement.SetOrigin` + `UpdatePlacement` — минимальный надёжный путь +(подтверждён рефлексией: все три члена есть в interop). `AddFromFileWithParam` требует получить +`IPlacement3D`/`IInsertPartParameters` через `IKompasDocument1.GetInterface(код)` до вставки — это +лишняя неочевидность (коды 11223/13196 не проверены рефлексией) и YAGNI на этом шаге. Ориентацию +(`SetVector`/углы Эйлера) и параметры вставки добавим в следующих инкрементах при необходимости. + +**Единицы:** `x/y/z` — миллиметры в мировой СК сборки. По умолчанию (0,0,0) компонент встаёт в +начало координат сборки. + +**Фиксация:** через API первый компонент НЕ фиксируется автоматически. На этом шаге фиксацию НЕ +выставляем (YAGNI) — для теста объёмом/габаритом она не нужна; добавим параметр `fixed` позже, +если понадобится. + +## MCP-инструмент (новая группа Assembly) + +| Инструмент | Параметры | Поведение | +|---|---|---| +| `assembly_add_component` | `filePath: string`, `x=0`, `y=0`, `z=0` | Вставить деталь/подсборку из `.m3d`/`.a3d`-файла в активную сборку; origin компонента — (x,y,z) мм. Возвращает индекс и имя вставленного компонента. | + +Контракт: активный документ должен быть **сборкой** (иначе понятная ошибка); файл должен +существовать. Возврат — текст вида «Компонент '<имя>' вставлен в сборку, индекс N, позиция (x,y,z)». + +## Валидация (чистые static, unit-тест) + +`AssemblyValidation`: +- `NormalizeComponentPath(string? path) → string` — `Trim` → проверка непустоты → проверка + расширения (`.m3d`/`.a3d`, case-insensitive; иначе `ArgumentException`) → `Path.GetFullPath` + (вернуть абсолютный путь). Чистая (не трогает ФС) → unit-тест. +- `RequireFinitePosition(double x, double y, double z)` — координаты конечны (`double.IsFinite`; + отсекает `Infinity`/`NaN`, как в `HoleService`). + +Проверка существования файла (`File.Exists` на нормализованном пути) — в сервисе (не чистая, не +unit-тест), даёт `FileNotFoundException` с абсолютным путём. + +## Проверка активного документа (helper в сервисе) + +`RequireActiveAssembly()` — раздельные ошибки (Codex #2): +1. `ActiveDocument == null` → «Нет активного документа». +2. `doc.DocumentType != ksDocumentAssembly` → «Активный документ не сборка (тип: …). Создайте + сборку: document_create assembly». (Деталь — тоже `IKompasDocument3D`, поэтому проверка по типу + обязательна ДО доступа к `TopPart`.) +3. `doc is not IKompasDocument3D` → «Активный документ не 3D». +4. `doc3d.TopPart is not IPart7` → «Не удалось получить TopPart сборки». + +## Реализация + +- `AssemblyService.AddComponentAsync(string filePath, double x, double y, double z, ct)` — + на STA-потоке: валидация → проверка типа активного документа (сборка) → `AddFromFile` → + `Placement.SetOrigin` → `UpdatePlacement` → вернуть `AddedComponent(Index, Name)`. + Индекс = `parts.Count - 1` после вставки; имя = `part.Name`. +- Транзитные RCW не освобождаем точечно — консистентно с остальным слоем (долг v2-2). + +## Тестирование + +### Unit (`AssemblyValidationTests`) +- `NormalizeComponentPath` бросает на null/пустой/пробельный; бросает на чужом расширении + (`.step`/`.txt`); пропускает `.m3d`/`.a3d` (в т.ч. с пробелами и в разном регистре), возвращает + абсолютный путь. +- `RequireFinitePosition` бросает на NaN/Infinity по каждой координате; пропускает конечные. + +### Integration (`AssemblyTests`, Collection KompasCollection) +Паттерн: построить деталь → `SaveAsAsync(.scratch/*.m3d)` → **проверить `File.Exists` + размер>0** → +`CloseAsync` → создать сборку → `AddComponentAsync` → проверить → `finally CloseAsync`. +**Геометрия детали фиксирована от нуля** (эскиз 0..40 × 0..40, выдавить 0..20 ⇒ локальный габарит +X[0,40] Y[0,40] Z[0,20]) — чтобы абсолютные ожидания по габариту были детерминированы (Codex #7). + +1. **Вставка в начало координат:** сохранить деталь-коробку, проверить файл, закрыть. Создать + сборку. `before = list_components.Count`. `var c = AddComponentAsync(file, 0,0,0)`. + - `list_components.Count == before + 1` (Codex #6); компонент по индексу `c.Index`: + `SizeX≈40, SizeY≈40, SizeZ≈20` (±допуск); `c.Name` непуст. + - `get_bounding_box` сборки: X∈[0,40], Y∈[0,40], Z∈[0,20] (±допуск). +2. **Позиционирование:** та же деталь, `AddComponentAsync(file, 50, 0, 0)`. + - `get_bounding_box` сборки: X∈[50,90] (сдвиг origin на +50 подтверждает работу позиции). + +Объёмом сборку тоже можно проверить (`get_part_info` на TopPart агрегирует компоненты) — +но габарит надёжнее и прямо проверяет позицию. Артефакты `.m3d` — в gitignored `.scratch/`. + +## Дальнейшие инкременты (вне этого спека) +2. `assembly_add_mate` — сопряжения (`IMateConstraints3D.Add(mc_Coincidence/mc_Distance/…)` + + `BaseObject1/2` + `Update`). Типы: совпадение/параллельность/перпендикулярность/касательность/ + концентричность/расстояние/угол/симметрия. +3. Перемещение/массив/фиксация компонентов — по необходимости. diff --git a/src/Kompas.Mcp.Core/Assemblies/AddedComponent.cs b/src/Kompas.Mcp.Core/Assemblies/AddedComponent.cs new file mode 100644 index 0000000..208aacc --- /dev/null +++ b/src/Kompas.Mcp.Core/Assemblies/AddedComponent.cs @@ -0,0 +1,11 @@ +namespace Kompas.Mcp.Core.Assemblies; + +/// Результат вставки компонента в сборку. +public sealed record AddedComponent +{ + /// Индекс вставленного компонента в коллекции компонентов сборки. + public required int Index { get; init; } + + /// Имя вставленного компонента в дереве сборки. + public required string Name { get; init; } +} diff --git a/src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs b/src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs new file mode 100644 index 0000000..5393bcb --- /dev/null +++ b/src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs @@ -0,0 +1,127 @@ +using System.Runtime.Versioning; +using Kompas.Mcp.Core.Threading; +using Kompas6Constants; +using KompasAPI7; + +namespace Kompas.Mcp.Core.Assemblies; + +/// +/// Операции со сборками через API7 (IParts7). Аналогично HoleService/FaceEditService: +/// контейнеры берутся COM-QI, все вызовы — на STA-потоке . +/// +[SupportedOSPlatform("windows")] +public sealed class AssemblyService +{ + private readonly KompasSession _session; + private readonly KompasDispatcher _dispatcher; + + public AssemblyService(KompasSession session, KompasDispatcher dispatcher) + { + _session = session ?? throw new ArgumentNullException(nameof(session)); + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + } + + /// + /// Вставить компонент (деталь .m3d или подсборку .a3d) из файла в активную сборку, разместив + /// его origin в мировой точке (,,) мм. + /// + public Task AddComponentAsync(string filePath, double x, double y, double z, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => AddComponentCore(filePath, x, y, z), ct); + + private AddedComponent AddComponentCore(string filePath, double x, double y, double z) + { + var path = AssemblyValidation.NormalizeComponentPath(filePath); + AssemblyValidation.RequireFinitePosition(x, y, z); + if (!File.Exists(path)) + throw new FileNotFoundException($"Файл компонента не найден: {path}", path); + + var top = RequireActiveAssembly(); + var parts = top.Parts + ?? throw new InvalidOperationException("Не удалось получить коллекцию компонентов сборки (Parts)."); + + // Фиксируем число компонентов ДО вставки — для верифицируемого отката (откатываем всё, что + // появилось сверх beforeCount), не полагаясь на возможный null-возврат AddFromFile. + int beforeCount = parts.Count; + + // ExternalFile=true: компонент ссылается на внешний .m3d/.a3d. Redraw=true: вставить и + // перерисовать в исходном положении (само перемещение применит SetOrigin+RebuildModel ниже). + var part = parts.AddFromFile(path, true, true) as IPart7; + if (part is null) + { + // AddFromFile мог вставить компонент и всё равно вернуть null/не-IPart7 — почистим, если + // число компонентов выросло, чтобы не оставить «осиротевший» компонент в сборке. + RollbackInsertion(top, parts, beforeCount); + throw new InvalidOperationException( + $"Не удалось вставить компонент из файла (AddFromFile вернул null или не IPart7): {path}"); + } + + try + { + var placement = part.Placement as IPlacement3D + ?? throw new InvalidOperationException("Placement компонента не приводится к IPlacement3D."); + if (!placement.SetOrigin(x, y, z)) + throw new InvalidOperationException("SetOrigin вернул FALSE — не удалось задать положение компонента."); + + // UpdatePlacement применяет изменение Placement; для вручную позиционируемого компонента + // (без сопряжений/зависимостей) он может вернуть FALSE — это НЕ ошибка (нечего + // пересчитывать). Фактически положение становится видимым в геометрии после RebuildModel. + part.UpdatePlacement(true); + if (!top.RebuildModel(true)) + throw new InvalidOperationException("RebuildModel вернул FALSE — сборка не перестроена после вставки компонента."); + + return new AddedComponent { Index = parts.Count - 1, Name = SafeName(part) }; + } + catch + { + // Верифицируемый откол: удаляем всё, что добавилось сверх beforeCount, и проверяем. + RollbackInsertion(top, parts, beforeCount); + throw; + } + } + + /// + /// Откатить вставку: удалить компоненты, появившиеся сверх (обычно + /// один — только что вставленный). Best-effort, но с верификацией: компонент удаляется через + /// IFeature7.Delete() своего владельца, затем сборка перестраивается. + /// + private static void RollbackInsertion(IPart7 top, IParts7 parts, int beforeCount) + { + try + { + // Удаляем с конца, пока число не вернётся к исходному (защита от частичной вставки). + for (int guard = 0; parts.Count > beforeCount && guard < 8; guard++) + { + if (parts.Part[parts.Count - 1] is IPart7 last && last.Owner is { } owner) + owner.Delete(); + else + break; // не удалось получить владельца — выходим, чтобы не зациклиться + } + top.RebuildModel(true); + } + catch { /* лучшее усилие: исходное исключение важнее ошибки отката */ } + } + + /// + /// Получить верхний компонент активной сборки, проверив тип документа. Раздельные ошибки: + /// нет документа / документ не сборка / не 3D / нет TopPart. Деталь — тоже IKompasDocument3D, + /// поэтому отсекаем по DocumentType ДО доступа к TopPart. + /// + private IPart7 RequireActiveAssembly() + { + var doc = _session.Application.ActiveDocument + ?? throw new InvalidOperationException("Нет активного документа. Создайте сборку: document_create assembly."); + if (doc.DocumentType != DocumentTypeEnum.ksDocumentAssembly) + throw new InvalidOperationException( + $"Активный документ не является сборкой (тип: {doc.DocumentType}). Создайте сборку: document_create assembly."); + if (doc is not IKompasDocument3D doc3d) + throw new InvalidOperationException("Активный документ не является 3D-документом."); + return doc3d.TopPart as IPart7 + ?? throw new InvalidOperationException("Не удалось получить TopPart сборки."); + } + + private static string SafeName(IPart7 part) + { + try { return part.Name ?? string.Empty; } + catch { return string.Empty; } + } +} diff --git a/src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs b/src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs new file mode 100644 index 0000000..7bbafc3 --- /dev/null +++ b/src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs @@ -0,0 +1,38 @@ +namespace Kompas.Mcp.Core.Assemblies; + +/// +/// Чистые валидаторы операций сборки (без COM) — выделены для unit-тестов. +/// +public static class AssemblyValidation +{ + /// Допустимые расширения файла-источника компонента: деталь (.m3d) и сборка (.a3d). + private static readonly HashSet AllowedExtensions = + new(StringComparer.OrdinalIgnoreCase) { ".m3d", ".a3d" }; + + /// + /// Нормализовать путь к файлу компонента: обрезать пробелы, проверить непустоту и расширение + /// (.m3d/.a3d, без учёта регистра), вернуть абсолютный путь. Существование файла НЕ проверяет + /// (это делает сервис на реальной ФС). + /// + public static string NormalizeComponentPath(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("Путь к файлу компонента не задан.", nameof(path)); + + var trimmed = path.Trim(); + var ext = Path.GetExtension(trimmed); + if (!AllowedExtensions.Contains(ext)) + throw new ArgumentException( + $"Недопустимое расширение файла компонента '{ext}'. Ожидается .m3d (деталь) или .a3d (сборка).", + nameof(path)); + + return Path.GetFullPath(trimmed); + } + + /// Проверить, что координаты вставки конечны (отсекает NaN/Infinity). + public static void RequireFinitePosition(double x, double y, double z) + { + if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z)) + throw new ArgumentException("Координаты вставки компонента должны быть конечными числами (не NaN/Infinity)."); + } +} diff --git a/src/Kompas.Mcp.Host/Program.cs b/src/Kompas.Mcp.Host/Program.cs index dd7150d..7b17e3a 100644 --- a/src/Kompas.Mcp.Host/Program.cs +++ b/src/Kompas.Mcp.Host/Program.cs @@ -1,4 +1,5 @@ using Kompas.Mcp.Core; +using Kompas.Mcp.Core.Assemblies; using Kompas.Mcp.Core.Conversion; using Kompas.Mcp.Core.Documents; using Kompas.Mcp.Core.Editing; @@ -23,6 +24,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Kompas.Mcp.Host/Tools/AssemblyTools.cs b/src/Kompas.Mcp.Host/Tools/AssemblyTools.cs new file mode 100644 index 0000000..d192e99 --- /dev/null +++ b/src/Kompas.Mcp.Host/Tools/AssemblyTools.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using Kompas.Mcp.Core; +using Kompas.Mcp.Core.Assemblies; +using ModelContextProtocol.Server; + +namespace Kompas.Mcp.Host.Tools; + +/// Инструменты работы со сборками (вставка/сопряжение компонентов). +[McpServerToolType] +public sealed class AssemblyTools(KompasSession session, AssemblyService assembly) +{ + [McpServerTool(Name = "assembly_add_component")] + [Description("Вставить компонент (деталь .m3d или подсборку .a3d) из файла в активную сборку. " + + "Активный документ должен быть сборкой (document_create assembly). origin компонента — точка (x,y,z) в мм " + + "мировой СК сборки (по умолчанию начало координат). Деталь-источник должна быть сохранена в файл " + + "(document_save_as). Возвращает индекс и имя вставленного компонента.")] + public async Task AddComponent( + [Description("Путь к файлу-источнику компонента (.m3d/.a3d)")] string filePath, + [Description("X положения origin компонента, мм")] double x = 0, + [Description("Y положения origin компонента, мм")] double y = 0, + [Description("Z положения origin компонента, мм")] double z = 0) + { + await session.ConnectAsync(); + var c = await assembly.AddComponentAsync(filePath, x, y, z); + return $"Компонент '{c.Name}' вставлен в сборку, индекс {c.Index}, позиция ({x}, {y}, {z})."; + } +} diff --git a/tests/Kompas.Mcp.Tests/AssemblyValidationTests.cs b/tests/Kompas.Mcp.Tests/AssemblyValidationTests.cs new file mode 100644 index 0000000..4c56243 --- /dev/null +++ b/tests/Kompas.Mcp.Tests/AssemblyValidationTests.cs @@ -0,0 +1,44 @@ +using Kompas.Mcp.Core.Assemblies; + +namespace Kompas.Mcp.Tests; + +[Trait("Category", "Unit")] +public sealed class AssemblyValidationTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void NormalizeComponentPath_throws_on_empty(string? path) + => Assert.Throws(() => AssemblyValidation.NormalizeComponentPath(path)); + + [Theory] + [InlineData(@"C:\parts\bolt.step")] + [InlineData(@"C:\parts\bolt.txt")] + [InlineData(@"C:\parts\bolt")] + public void NormalizeComponentPath_throws_on_wrong_extension(string path) + => Assert.Throws(() => AssemblyValidation.NormalizeComponentPath(path)); + + [Theory] + [InlineData(@"C:\parts\bolt.m3d", ".m3d")] + [InlineData(@"C:\parts\frame.a3d", ".a3d")] + [InlineData(@"C:\parts\bolt.M3D", ".m3d")] // регистр не важен + [InlineData(" C:\\parts\\bolt.a3d ", ".a3d")] // пробелы обрезаются + public void NormalizeComponentPath_accepts_m3d_and_a3d(string path, string expectedExt) + { + var result = AssemblyValidation.NormalizeComponentPath(path); + Assert.True(Path.IsPathFullyQualified(result), "Должен вернуться абсолютный путь."); + Assert.EndsWith(expectedExt, result, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData(double.NaN, 0, 0)] + [InlineData(0, double.PositiveInfinity, 0)] + [InlineData(0, 0, double.NegativeInfinity)] + public void RequireFinitePosition_throws_on_non_finite(double x, double y, double z) + => Assert.Throws(() => AssemblyValidation.RequireFinitePosition(x, y, z)); + + [Fact] + public void RequireFinitePosition_accepts_finite() + => Assert.Null(Record.Exception(() => AssemblyValidation.RequireFinitePosition(50, -10, 0))); +} diff --git a/tests/Kompas.Mcp.Tests/Integration/AssemblyTests.cs b/tests/Kompas.Mcp.Tests/Integration/AssemblyTests.cs new file mode 100644 index 0000000..0c28a0b --- /dev/null +++ b/tests/Kompas.Mcp.Tests/Integration/AssemblyTests.cs @@ -0,0 +1,156 @@ +using Kompas.Mcp.Core.Assemblies; +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Modeling; +using Kompas.Mcp.Core.Query; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: вставка компонента в сборку (API7 IParts7.AddFromFile + Placement). +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class AssemblyTests +{ + private readonly DocumentService _docs; + private readonly PartModeler _modeler; + private readonly AssemblyService _assembly; + private readonly QueryService _query; + + public AssemblyTests(KompasFixture fx) + { + _docs = new DocumentService(fx.Session, fx.Dispatcher); + _modeler = new PartModeler(fx.Session, fx.Dispatcher); + _assembly = new AssemblyService(fx.Session, fx.Dispatcher); + _query = new QueryService(fx.Session, fx.Dispatcher); + } + + /// Построить деталь-коробку от нуля (габарит X[0,w] Y[0,d] Z[0,h]), сохранить в .m3d, + /// проверить файл, закрыть. Возвращает абсолютный путь к сохранённому файлу. + private async Task BuildAndSaveBoxAsync(double w, double d, double h) + { + var path = TestPaths.NewFile(".m3d"); + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddRectangleAsync(s, 0, 0, w, d); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: h); + await _modeler.RebuildAsync(); + await _docs.SaveAsAsync(path); + } + finally { await _docs.CloseAsync(save: false); } + + // Отделяем проблемы сохранения от проблем вставки: файл должен реально записаться. + Assert.True(File.Exists(path), $"Файл детали не сохранён: {path}"); + Assert.True(new FileInfo(path).Length > 0, "Файл детали пуст."); + return path; + } + + [Fact] + public async Task AddComponent_inserts_one_component_at_origin() + { + var partFile = await BuildAndSaveBoxAsync(40, 40, 20); + + await _docs.CreateAsync(KompasDocumentType.Assembly); + try + { + var before = (await _query.ListComponentsAsync()).Count; + + var added = await _assembly.AddComponentAsync(partFile, 0, 0, 0); + + var comps = await _query.ListComponentsAsync(); + Assert.Equal(before + 1, comps.Count); + Assert.False(string.IsNullOrWhiteSpace(added.Name), "Имя вставленного компонента не должно быть пустым."); + + var c = comps[added.Index]; + Assert.InRange(c.SizeX, 40 * 0.95, 40 * 1.05); + Assert.InRange(c.SizeY, 40 * 0.95, 40 * 1.05); + Assert.InRange(c.SizeZ, 20 * 0.95, 20 * 1.05); + + // Габарит сборки совпадает с деталью, вставленной в начало координат. + var bb = await _query.GetBoundingBoxAsync(); + Assert.InRange(bb.MinX, -1, 1); + Assert.InRange(bb.MaxX, 39, 41); + Assert.InRange(bb.MinY, -1, 1); + Assert.InRange(bb.MaxY, 39, 41); + Assert.InRange(bb.MinZ, -1, 1); + Assert.InRange(bb.MaxZ, 19, 21); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task AddComponent_positions_component_at_offset() + { + var partFile = await BuildAndSaveBoxAsync(40, 40, 20); + + await _docs.CreateAsync(KompasDocumentType.Assembly); + try + { + // Вставка со сдвигом origin на +50 по X → габарит сборки X[50,90]. + await _assembly.AddComponentAsync(partFile, 50, 0, 0); + + var bb = await _query.GetBoundingBoxAsync(); + Assert.InRange(bb.MinX, 49, 51); + Assert.InRange(bb.MaxX, 89, 91); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task AddComponent_into_non_empty_assembly_increments_and_indexes() + { + // Вставка ВТОРОГО компонента в непустую сборку: проверяет инкремент count и что + // возвращённый Index указывает именно на вставленный компонент (а не «последний по случаю»). + var partFile = await BuildAndSaveBoxAsync(30, 30, 10); + + await _docs.CreateAsync(KompasDocumentType.Assembly); + try + { + await _assembly.AddComponentAsync(partFile, 0, 0, 0); // первый + var before = (await _query.ListComponentsAsync()).Count; + Assert.Equal(1, before); + + var added = await _assembly.AddComponentAsync(partFile, 60, 0, 0); // второй, со сдвигом + + var comps = await _query.ListComponentsAsync(); + Assert.Equal(before + 1, comps.Count); + Assert.Equal(comps.Count - 1, added.Index); + // Компонент по возвращённому индексу — деталь нужного габарита. + Assert.InRange(comps[added.Index].SizeX, 30 * 0.95, 30 * 1.05); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task AddComponent_missing_file_throws_and_keeps_assembly_empty() + { + var missing = Path.Combine(TestPaths.Scratch, $"no_such_{Guid.NewGuid():N}.m3d"); + + await _docs.CreateAsync(KompasDocumentType.Assembly); + try + { + await Assert.ThrowsAsync( + () => _assembly.AddComponentAsync(missing, 0, 0, 0)); + + // Сборка не должна получить «осиротевший» компонент от неуспешной вставки. + Assert.Empty(await _query.ListComponentsAsync()); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task AddComponent_rejects_non_assembly_document() + { + // Активный документ — деталь (не сборка): должна быть понятная ошибка. + var partFile = await BuildAndSaveBoxAsync(20, 20, 10); + + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + await Assert.ThrowsAsync( + () => _assembly.AddComponentAsync(partFile, 0, 0, 0)); + } + finally { await _docs.CloseAsync(save: false); } + } +}