41 Commits

Author SHA1 Message Date
mikhail 4e119afdba docs: замкнуть жизненные циклы CAD-каталога 2026-07-17 02:15:32 +03:00
mikhail b188e1904f docs: уточнить статусы и приоритеты CAD-каталога 2026-07-17 01:57:53 +03:00
mikhail c0d855c171 docs: добавить каталог внешнего CAD-контракта 2026-07-17 01:39:39 +03:00
mikhail 8e56af61b3 docs: спроектировать каталог внешнего CAD-контракта 2026-07-17 01:18:10 +03:00
mikhail 69d037c875 chore: настроить Codex-агент поиска SDK 2026-07-17 01:12:53 +03:00
mikhail 6aa05b1adb docs: спроектировать Codex-агент поиска SDK 2026-07-17 01:06:25 +03:00
mikhail 7cb44a073e docs: handoff — инкремент 9 (выноска) завершён; далее — базы/допуски формы
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:34:18 +03:00
mikhail 16406a34be Merge feat/drawing-leader: линия-выноска с текстом (инкремент 9)
drawing_add_leader через ISymbols2DContainer.Leaders + IBranchs. 83 инструмента,
331 тест зелёный. Ревью: Codex + pi/glm-5.1 + pi/kimi-k2.6.
2026-05-28 00:33:33 +03:00
mikhail 5d51b9e1af docs: выноска — TODO, README, ARCHITECTURE, OPEN_QUESTIONS, presentation, CLAUDE.md
Инкремент 9. 83 инструмента, 331 тест. + заметка о ревью реализации в спеке
(Codex + pi/glm-5.1 + pi/kimi-k2.6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:33:33 +03:00
mikhail beaa220510 review(drawing): правки по ревью реализации (Codex + pi/glm-5.1 + pi/kimi-k2.6)
- проверка возврата AddBranchByPoint/SetBranchTextPosition (bool) — ранняя диагностика
  вместо позднего RPC_E_SERVERFAULT (Codex, glm)
- RU-алиасы в сообщении ShelfDirections.Parse и в описании инструмента (glm, kimi)
- интеграционный тест направления полки параметризован Right/Left/Up/Down — покрывает
  всю поверхность ToKompas на реальном COM (glm, kimi)
- отклонено (обоснование): unit ToKompas (граница проекта), e2e tool-обёртки (тонкая),
  angleDeg (нет Angle у ILeader), default text у AddText (вне объёма)

331 тест зелёный, сборка Release чистая.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:28:04 +03:00
mikhail f89ad951ae feat(drawing): линия-выноска с текстом (drawing_add_leader)
Выноска на виде через API7 ISymbols2DContainer.Leaders. Стрелка указывает в (x,y),
текст на полке у (textX,textY). shelfDirection: auto|right|left|up|down.

- DrawingService.AddLeaderAsync + GetViewLeaderCountAsync
- ShelfDirection (enum + Parse/ToKompas), переиспользует DrawingAnnotationResult
- 13 unit + 7 интеграционных тестов (всего 328 зелёных), сборка Release чистая

КЛЮЧ (спайк): новая выноска имеет 0 ответвлений — сначала (IBranchs)bl.AddBranchByPoint(0,x,y)
(остриё), ИНАЧЕ КОМПАС падает RPC_E_SERVERFAULT; затем SetBranchTextPosition (полка),
(ILeader)bl.TextOnShelf.Str (текст), Update. Ревью спека Codex+pi/glm-5.1+pi/kimi-k2.6 учтено
(QI as?? throw, read-back ?? empty, RequireDistinctPoints, RU-алиасы, критический порядок).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:18:08 +03:00
mikhail 08191db25c docs: handoff — инкремент 8 (формат листа) завершён; ревью-гейт = Codex + pi/glm-5.1 + pi/kimi-k2.6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:35:22 +03:00
mikhail e60c8d48e4 Merge feat/drawing-sheet-format: формат и ориентация листа (инкремент 8)
drawing_set_sheet_format (A0–A5/user + книжная/альбомная) через ILayoutSheet.Format.
82 инструмента, 308 тестов зелёных. Ревью: Codex + pi/glm-5.1 + pi/kimi-k2.6.
2026-05-27 23:34:33 +03:00
mikhail 4937b9e677 docs: формат/ориентация листа — TODO, README, ARCHITECTURE, OPEN_QUESTIONS, presentation, CLAUDE.md
Инкремент 8. 82 инструмента, 308 тестов. + заметка о ревью реализации в спеке
(Codex + pi/glm-5.1 + pi/kimi-k2.6; K1 снят спайком — КОМПАС сам выводит ориентацию user).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:34:15 +03:00
mikhail 24f3531a3c review(drawing): правки по ревью реализации (Codex + pi/glm-5.1 + pi/kimi-k2.6)
- K1 (kimi, Major): спайк подтвердил — для User КОМПАС САМ выводит VerticalOrientation
  из W/H (игнорирует флаг, W/H не свопает); read-back корректен, правка не нужна —
  задокументировано комментарием + тесты на Landscape (500×300→альбомная, 300×500→книжная)
- K2: ValidateFormatDimensions называет точный нарушивший параметр (width/height)
- тесты: A1 в Parse (C3), A4 landscape со свопом 297×210 (G2), sheetNumber=0 (C2/G3)
- отклонено (нит): алиасы в описании (G4), сообщение NaN (G5), тип исключения standard (K5)

308 тестов зелёных, сборка Release чистая.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:28:34 +03:00
mikhail 56ed2cb0cf feat(drawing): формат и ориентация листа (drawing_set_sheet_format)
Задать формат (A0–A5/user) и ориентацию (книжная/альбомная) листа чертежа через
API7 ILayoutSheet.Format (ISheetFormat). Стандартный формат — размеры авто; user —
явные width/height (landscape игнорируется, ориентация из соотношения сторон).

- DrawingService.SetSheetFormatAsync + RequireLayoutSheet
- PaperFormat (enum + Parse/ToKompas/FromKompas), SheetFormatResult
- DrawingValidation.ValidateFormatDimensions (user: >0; стандартный: размеры запрещены)
- 19 unit + 7 интеграционных тестов (всего 304 зелёных), сборка Release чистая

Спайк подтвердил: дефолт A4 portrait; стандартный формат пересчитывает W/H авто
(A3 landscape→420×297); user применяет width/height. Ревью спека pi/glm-5.1 + pi/kimi-k2.6
учтено (FromKompas для дружелюбного имени, запрет размеров у стандартного, sheetNumber>0,
landscape игнорируется для user).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:19:37 +03:00
mikhail adc9da37fa Merge feat/skill-kompas-fdm-design: навык проектирования под FDM (DFM-методика + гео-аудит) 2026-05-27 23:00:00 +03:00
mikhail f490c146c7 fix(skill): kompas-fdm-design — уточнить угол θ_max vs α (финальное ревью) 2026-05-27 22:53:51 +03:00
mikhail a87957582c docs: упомянуть навык kompas-fdm-design (проектирование под FDM) 2026-05-27 22:49:30 +03:00
mikhail 178ca48faf feat(skill): kompas-fdm-design — references/geometry-audit.md (самопроверка + границы) 2026-05-27 22:45:23 +03:00
mikhail 38f2687c8d feat(skill): kompas-fdm-design — references/fdm-rules.md (полный свод DFM) 2026-05-27 22:44:06 +03:00
mikhail 2fb0f8347d feat(skill): kompas-fdm-design — диспетчер SKILL.md (правила DFM + гео-аудит) 2026-05-27 22:33:18 +03:00
mikhail a5de3566ba Update agent definitions (.claude/agents/docs-maintainer.md, kompaz-sdk-research.md) 2026-05-27 22:21:13 +03:00
mikhail df7bf92884 docs(skill): spec + план навыка kompas-fdm-design (проектирование под FDM)
Дизайн и план реализации нового навыка kompas-fdm-design — методика
проектирования деталей под FDM-печать (правила DFM + лёгкий гео-аудит),
отдельный от kompas-3d, без слайсера, экспорт через export_step.

Выверено консультациями pi (glm-5.1, kimi-k2.6) и 6 ревью (spec x3, план x3).
Сам навык (SKILL.md + references/) по плану — РЕАЛИЗАЦИЯ ПРЕДСТОИТ.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:18:27 +03:00
mikhail 7b50ac1a5d docs: handoff — инкремент 7 (ассоциативная привязка) завершён; далее — дуга/угловой/формат
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:01:00 +03:00
mikhail 8f4567732e Merge feat/drawing-associative-dimension: ассоциативная привязка размеров (инкремент 7)
Флаг associate у диаметрального/радиального — привязка к спроецированной окружности
через BaseObject (значение из геометрии). 81 инструмент, 278 тестов зелёных.
2026-05-27 22:00:16 +03:00
mikhail ff5fb27dd5 docs: ассоциативная привязка диам./радиального — TODO, README, ARCHITECTURE, OPEN_QUESTIONS, presentation, CLAUDE.md
Инкремент 7. 81 инструмент (associate — флаг существующих), 278 тестов.
CLAUDE.md также включает структурную чистку (делегирование каталога/дорожной карты/
бэклога в README/ARCHITECTURE/TODO без дублирования).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:57:45 +03:00
mikhail ecf2e22265 docs: сжать и оптимизировать CLAUDE.md (−36% слов)
Каталог инструментов из «Current state» свёрнут в указатель на README
§Инструменты (единый источник правды); дубль-каталог «are done» / «not yet
done» убран в пользу ARCHITECTURE §10 и TODO.md. Уникальные COM-гочи из
него перенесены в структурированный bullet про Drawing. В bullets
«don't relearn» подрезан boilerplate (имена файлов, DI, валидаторы) —
все COM-цепочки и эмпирика сохранены. Исправлена модель субагента
kompas-sdk-research: Sonnet на Haiku (рассинхрон с определением агента).
Включает текущие инкрементальные правки (счётчики тестов, заметка об
ассоциативной привязке размеров).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:55:12 +03:00
mikhail d3b3a7d589 review(drawing): правки по ревью реализации Codex (ассоциативная привязка)
- свободный путь (associate=false) берёт только ISymbols2DContainer (как раньше) —
  IDrawingContainer запрашивается лишь при associate=true (не расширяем поверхность отказа)
- XML-доки диаметрального/радиального уточнены: при associate значение из геометрии

16 тестов привязки+регрессии зелёные, сборка Release чистая.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:52:35 +03:00
mikhail 775128484e feat(drawing): ассоциативная привязка диаметрального/радиального размера к окружности вида
Флаг associate у drawing_add_diametral_dimension/drawing_add_radial_dimension:
- associate=false (по умолчанию) — прежний свободный размер (без изменений);
- associate=true — (xc,yc,radius) как ключ поиска спроецированной окружности
  (IDrawingContainer.Circles), привязка через BaseObject; значение Ø/R читается
  из геометрии и обновляется с моделью.

- CircularObjectMatch.SelectMatchIndex (чистый отбор по центру+радиусу, допуск 1 мм;
  бросает при отсутствии/неоднозначности; концентрические разделяются радиусом)
- RequireViewContainers (вид → ISymbols2DContainer + IDrawingContainer), CollectCircles, FindBaseCircle
- 5 unit + 6 интеграционных тестов (всего 278 зелёных), сборка Release чистая

Спайк подтвердил: BaseObject=circle без задания Xc/Yc/Radius → Valid, NominalValue из
геометрии (Ø=20/R=10 для R10). Объём — только окружности (Circles); дуги/угловой/линейный —
далее. Ревью Codex спека учтено (оба контейнера, guard вокруг блока, доказательство
ассоциативности тестом radius=10.3→Ø20, регресс обоих свободных путей).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:47:17 +03:00
mikhail ecabaa1828 docs: добавить канонический бэклог оставшихся этапов (docs/TODO.md)
Консолидация дорожной карты по классам (2D-чертёж/сборки/деталь) + сквозное/тех.долг
со статусами (☐/▣/). Ссылки из CLAUDE.md и NEXT-SESSION.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:27:49 +03:00
mikhail 23341092cb docs: handoff — инкремент 6 (текстовые обозначения) завершён; далее — привязка/формат/выноски
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:23:50 +03:00
mikhail 679171c5a7 Merge feat/drawing-text-annotations: текстовые обозначения чертежа (инкремент 6)
Шероховатость, свободный текст, технические требования. 81 инструмент, 267 тестов зелёных.
2026-05-27 21:22:43 +03:00
mikhail d8edb18f06 docs: текстовые обозначения чертежа — README, ARCHITECTURE, OPEN_QUESTIONS, presentation, CLAUDE.md
81 инструментов, 267 тестов. 2D-чертёж: добавлены шероховатость, свободный текст,
технические требования (инкремент 6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:22:35 +03:00
mikhail 5272066bef feat(drawing): текстовые обозначения — шероховатость, текст, тех. требования
drawing_add_rough (знак шероховатости: ISymbols2DContainer.Roughs + IRoughParams,
SignType ksRoughSignEnum, значение Ra/Rz через RoughParamText.Str),
drawing_add_text (свободная надпись: IDrawingContainer.DrawingTexts + IText.Str),
drawing_set_technical_requirements (блок тех. требований уровня документа:
IDrawingDocument.TechnicalDemand.Text).

- DrawingService: AddRoughAsync/AddTextAsync/SetTechnicalRequirementsAsync +
  ReadTechnicalRequirementsAsync + счётчики; RequireDrawingContainer/RequireTechnicalDemand
- RoughSignType (enum + Parse/ToKompas), DrawingAnnotationResult (Value read-back из COM)
- RequireNonEmptyText в DrawingValidation
- 13 unit + 13 интеграционных тестов (всего 267 зелёных), сборка Release чистая

Спайк подтвердил: значение шероховатости через IRoughParams.RoughParamText.Str (round-trip);
текст НЕ в IView.ObjectCount (счёт по DrawingTexts.Count); тех. требования IsCreated False→True
при первом Text.Str+Update, многострочно через \n. Ревью Codex спека учтено (height убран —
это высота блока, не шрифт; guards; read-back возврат). Спек — docs/superpowers/specs/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:13:11 +03:00
mikhail 1f153e43ab chore: усилить description делегатов — проактивные триггеры
kompas-sdk-research и docs-maintainer почти не вызывались (по транскриптам:
2 и 1 раз против 40 у general-purpose). Причина — описания ждали явной
реплики пользователя, а в автономном цикле NEXT-SESSION её нет.

- Триггеры переписаны на проактивные, от лица Opus, с привязкой к шагу цикла
  (SDK-лукап — перед кодом; доки — перед коммитом, после реализации).
- kompas-sdk-research: «(Sonnet)» → «(Haiku, read-only)» (соответствие model:),
  снят барьер делегирования (ответ дёшев, перепроверяется рефлексией по interop),
  добавлены негативные границы (не геометрия, не KsAPI).
- docs-maintainer: в охват явно добавлены CLAUDE.md и OPEN_QUESTIONS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:42:29 +03:00
mikhail 328e89a6e7 docs: handoff — инкремент 5 (радиальный/угловой) завершён; далее — привязка/формат/обозначения
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:05:10 +03:00
mikhail 7fa434afae Merge feat/drawing-radial-angular-dimension: радиальный и угловой размеры (инкремент 5)
Завершено базовое семейство размеров 2D-чертежа. 78 инструментов, 241 тест зелёный.
2026-05-27 20:03:41 +03:00
mikhail db9d76a7bd docs: радиальный и угловой размеры — README, ARCHITECTURE, OPEN_QUESTIONS, presentation, CLAUDE.md
78 инструментов, 241 тест. Базовое семейство размеров 2D-чертежа завершено
(линейный, диаметральный, радиальный, угловой).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:03:35 +03:00
mikhail 86a5a52347 review(drawing): правки по ревью реализации Codex (угловой размер)
- описание (x3,y3) и сообщение об ошибке: угол выбирает angleType, не точка дуги
- тест angleType=Max (лучи 0°/45° → 135° тупой супплемент): min=45/max=135/more=315
- спек дополнен находкой о трёх типах угла

241 тест зелёный, сборка Release чистая.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:58:15 +03:00
mikhail 246233727c feat(drawing): радиальный (R) и угловой размеры на виде чертежа
drawing_add_radial_dimension (R окружности/дуги; NominalValue = радиус) и
drawing_add_angular_dimension (угол между двумя сторонами; angleType min/max/more
выбирает измеряемый сектор) через API7 ISymbols2DContainer. Завершают базовое
семейство размеров чертежа (после линейного и диаметрального).

- DrawingService: AddRadialDimensionAsync/AddAngularDimensionAsync + счётчики
- AngleDimensionType (enum + Parse/ToKompas, по образцу DimensionOrientations)
- DrawingDimensionResult.Value — единице-нейтральный (мм / градусы)
- 9 unit + 13 интеграционных тестов (всего 240 зелёных)

Спайк подтвердил: радиальный NominalValue = РАДИУС (не диаметр, вопреки справке);
у углового измеряемый угол выбирает angleType (min=90°/more=270°), а (x3,y3) лишь
позиционирует дугу. Спек и ревью Codex — docs/superpowers/specs/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:51:10 +03:00
49 changed files with 5032 additions and 93 deletions
+3 -10
View File
@@ -1,17 +1,10 @@
---
name: docs-maintainer
description: >
Делегируй этому субагенту (Sonnet) синхронизацию документации проекта с
изменениями в коде — README.md, docs/*.md (ARCHITECTURE, OPEN_QUESTIONS),
docs/presentation.html. Используй ВСЕГДА, когда нужно отразить в доках новые
инструменты/классы/функции, переименования, удаления или закрытые вопросы
OPEN_QUESTIONS — не делай это сам, передавай сводку изменений этому субагенту.
Триггеры: «обнови доки», «синхронизируй документацию», «задокументируй
изменения», «обнови README/ARCHITECTURE».
tools: Read, Edit, Write, Glob, Grep
description: "ОБЯЗАТЕЛЬНО делегируй этому субагенту (Sonnet) ВСЮ синхронизацию документации проекта с кодом — README.md, CLAUDE.md, docs/ARCHITECTURE.md, docs/OPEN_QUESTIONS.md, docs/presentation.html — НЕ правь эти файлы сам. Срабатывает ПРОАКТИВНО как обязательный шаг рабочего цикла, а НЕ по явной команде: как только фича реализована и протестирована, ПЕРЕД коммитом доков передай ему сводку изменений (новые/переименованные/удалённые инструменты, классы, факты реализации, закрытые вопросы OPEN_QUESTIONS, новые счётчики инструментов/тестов для presentation.html). Не жди реплики «обнови доки» — делегируй сам после каждой завершённой единицы работы. Триггеры (включая ТВОИ внутренние): «фича готова, надо отразить в доках», «обнови README/ARCHITECTURE/CLAUDE.md/presentation», «закрыт вопрос в OPEN_QUESTIONS», «синхронизируй документацию», «задокументируй изменения». Передавай сводку текстом — код/тесты он сам не запускает (Bash нет).\n"
tools: "Read, Edit, Write, Glob, Grep"
model: sonnet
color: blue
---
Ты — помощник по документации проекта **kompas3d-mcp** (MCP-сервер для
автоматизации CAD-системы КОМПАС-3D через COM API, .NET 8, C#).
+3 -11
View File
@@ -1,18 +1,10 @@
---
name: kompas-sdk-research
description: >
Делегируй этому субагенту (Sonnet) поиск и чтение справки КОМПАС SDK.
Источник — структурированная MD-база знаний docs/Kompas3D_SDK/ (статьи по
интерфейсам с YAML-тегами, ищется обычным Grep/Read). Используй ВСЕГДА, когда
нужно найти интерфейс/метод/константу COM API КОМПАС, его сигнатуру
(Automation/COM), значения перечисления, единицы измерения или паттерн
использования — не рой справку сам, передавай этому субагенту и получай
готовую выжимку. Триггеры: «найди в справке КОМПАС», «какая сигнатура у…»,
«какие значения enum…», «как через API КОМПАС сделать…», «что возвращает <метод>».
tools: Glob, Grep, Read
description: "ОБЯЗАТЕЛЬНО делегируй этому субагенту (Haiku, read-only) ЛЮБОЙ поиск по справке COM API КОМПАС в MD-базе docs/Kompas3D_SDK/ — НЕ грепай справку сам. Срабатывает ПРОАКТИВНО, без явной просьбы пользователя: как только при планировании или реализации фичи тебе понадобилась сигнатура метода (Automation/COM), значения перечисления (Obj3dType, ksHoleTypeEnum, ST_MIX_*, стили линий…), нужный интерфейс под задачу («чем/каким интерфейсом построить X»), единицы измерения или цепочка вызовов — СНАЧАЛА спроси этого субагента, ПОТОМ пиши код. Это дёшево: ответ Haiku ты всё равно перепроверяешь рефлексией по libs/kompas-interop/*.dll, поэтому ошибка стоит мало, а сэкономленный контекст Opus — много. Триггеры (в т.ч. ТВОИ внутренние, не только реплики пользователя): «нужна сигнатура …», «какие значения enum …», «каким интерфейсом сделать X через API КОМПАС», «что возвращает <метод>», «как через COM API построить …», «найди в справке КОМПАС». НЕ для построения геометрии в КОМПАС (это MCP-инструменты / навык kompas-3d) и НЕ для KsAPI (Qt/C++ — в MD-базе его нет).\n"
tools: "Glob, Grep, Read"
model: haiku
color: cyan
---
Ты — помощник по навигации в справке **КОМПАС SDK** в репозитории
**kompas3d-mcp** (MCP-сервер для КОМПАС-3D через COM API, .NET 8, C#).
Справка — это MD-база знаний `docs/Kompas3D_SDK/`: статьи по интерфейсам с
+116
View File
@@ -0,0 +1,116 @@
---
name: kompas-fdm-design
description: >
Методика проектирования деталей под FDM/FFF 3D-печать в КОМПАС-3D через MCP-сервер этого
проекта: правила DFM (нависания и угол θ_max, толщины стенок n·w, отверстия и teardrop,
посадки/зазоры, ориентация под прочность, elephant foot, бобышки/инсёрты/защёлки) ПЛЮС
лёгкая самопроверка геометрии инструментами осмотра. Используй, когда задача — спроектировать
или ДОВЕСТИ деталь, чтобы она хорошо ПЕЧАТАЛАСЬ на FDM. Триггеры: «сделай деталь
печатнопригодной / под FDM», «спроектируй … под печать», «напечатается ли без поддержек?»,
«подбери зазоры для печатной посадки», «как ориентировать деталь под печать», «почему деталь
плохо печатается / где будут нависания», «доведи деталь под FDM». Строит через навык kompas-3d.
НЕ для: механики построения через MCP (это kompas-3d); слайсинга/нарезки/g-code (вне границ);
поиска по справке SDK (субагент kompas-sdk-research).
---
# kompas-fdm-design — проектирование деталей под FDM-печать
## Что это (и чем НЕ является)
Методический слой **поверх** навыка `kompas-3d`. Отвечает на вопрос **«как спроектировать, чтобы
напечаталось на FDM»**, а не «чем строить».
- **`kompas-3d`** = *чем и как строить* через MCP (эскиз→операция→осмотр→`validate_part`). Этот
навык **опирается** на него для механики.
- Этот навык = *какие правила геометрии* соблюдать, чтобы FDM-печать удалась.
- **Слайсинг — вне границ.** Навык не нарезает и не оценивает g-code.
## Когда применять / когда НЕ применять
**Применять:** «сделай печатнопригодным / под FDM», «спроектируй … под печать», «напечатается без
поддержек?», «подбери зазоры печатной посадки», «как ориентировать под печать», «почему плохо
печатается», «доведи деталь под печать».
**НЕ применять:** чистая механика построения (→ `kompas-3d`); слайсинг/нарезка/g-code (вне границ);
поиск сигнатур/констант в справке SDK (→ субагент `kompas-sdk-research`).
## Калибровка (выполни первым шагом)
Все правила масштабируются от трёх параметров (часть значений — абсолютные эмпирические мм,
помечены «калибровать тестом»):
- **`w` = ширина линии ≈ диаметр сопла.** Дефолт: сопло 0.4 → `w ≈ 0.40.45 мм`. → стенки `n·w`.
- **`h` = высота слоя.** Дефолт `h ≈ 0.5·сопло` (0.2 мм); структурная печать 0.2–0.25.
- **`θ_max` = предельный угол самонесущей поверхности ОТ ВЕРТИКАЛИ.** `θ_max ≈ arctan(w/2h)`
(≈45° при `w`=0.4, `h`=0.2). **Дефолт 45°** (PLA, хороший обдув); **40°** для PETG/ABS
(эмпирическая поправка на провис, не вывод формулы) или для толстого слоя (`h≥0.3` → формула
даёт ~34°). Нависания, teardrop, фаски, зенковки берут угол из `θ_max`.
- **Материал** (PLA / PETG / ABS) — модификатор зазоров/мостов/коробления/`θ_max`. См.
`references/fdm-rules.md`.
Если сопло/слой/материал не заданы — прими дефолты (сопло 0.4, `h`=0.2, PLA) и скажи об этом.
## Два правила (соблюдай всегда)
**1. Ориентация печати — первое проектное решение.**
- **Спроси у пользователя** (если не задано): главное направление рабочей нагрузки и
косметические/критичные грани. **Путь нагрузки из геометрии не выводится** — его задаёт задача.
- Реши постановку на стол (ось Z = рост слоёв). От неё зависит: где нависания; куда смотрят
отверстия (вертикальные → компенсация Ø; горизонтальные → teardrop); путь нагрузки (**держи в
XY**; Z-сжатие можно, Z-растяжение/срез — нет); плоскости сопряжения (на XY-гранях); «лесенка» на
наклонных функциональных поверхностях; если поддержки неизбежны — чтобы опорные грани были
некритичными/скрытыми.
- Зафиксируй ориентацию и проектируй под неё.
**2. Чек-лист печатнопригодности перед выдачей** (ниже). Сначала **`validate_part`** (деталь
*валидна*), затем **FDM-чек-лист** (деталь *печатнопригодна*) — разные проверки. **Гео-аудит
эвристический и не доказывает печатнопригодность** (не ловит путь нагрузки/анизотропию).
## Рабочий цикл
1. **Калибровка**: сопло→`w`; слой→`h``θ_max`; материал→поправки.
2. **Ориентация** (правило 1): опрос (нагрузка/косметика) → постановка, ось слоёв, сопряжения,
«лесенка», поддержки.
3. **Правила эскиза/операции** (строй через `kompas-3d`): стенки `n·w`; нависания → скос под
`θ_max`; горизонтальные отверстия → teardrop; вертикальные → компенсация Ø; фаска у основания;
зазоры посадок (со знаком); заходные фаски; мин. элементы/текст; бобышки/инсёрты/защёлки.
Числа — в `references/fdm-rules.md`.
4. **Гео-аудит** (`references/geometry-audit.md`) — инструментами осмотра.
5. **Предусловия экспорта**: единое тело/манифолд (`boolean_union` при необходимости) →
`validate_part` чисто.
6. **Чек-лист** → экспорт **через `export_step`**.
## Чек-лист печатнопригодности
- [ ] Направление нагрузки и косметические грани **получены от пользователя**; ориентация
зафиксирована; нагрузка в XY (или Z только на сжатие); сопряжения на XY-гранях.
- [ ] Стенки кратны `w` (≥2·w; несущие ≥3·w, т.е. 3 периметра); нет «не кратных `w`» (кроме
функциональных).
- [ ] Нет 90°-полок; нависания ≤`θ_max` или заменены скосами; мосты в пределах пролёта по короткой
стороне (числа — `references/fdm-rules.md`); внутренним поддержкам — доступ.
- [ ] Горизонтальные отверстия — teardrop/D (геометрия из `θ_max`); вертикальные — компенсация Ø;
глухие — дно ≥2–3 мм.
- [ ] Фаска у основания (elephant foot); внутренние углы ≥R0.5; опорная площадка есть.
- [ ] Посадки по таблице со **знаком** (натяг — вычесть); допуск ±0.2 **НЕ** прибавлен к зазору;
заходные фаски на сопряжениях.
- [ ] Мин. элементы/текст ≥ порогов; аспект тонких выступов ≤4–5×.
- [ ] Бобышки/инсёрты (бор отверстия ≤ OD инсёрта, ставить с Z-грани)/резьба/защёлки (изгиб в XY) по правилам.
- [ ] Полости — дренаж/вент; критичные поверхности не под «лесенкой»/поддержкой.
- [ ] Предусловия экспорта: единое тело/манифолд; `validate_part` чисто.
## Гео-аудит (кратко)
Лёгкая самопроверка построенной модели **существующими** инструментами осмотра MCP:
`describe_model` / `list_faces` / `describe_face` (нависания по нормалям нижних граней; цилиндры с
горизонтальной осью → нужен teardrop), `get_bounding_box` (как ось слоёв соотносится с габаритом),
`measure` (номиналы/зазоры), `list_bodies` + `validate_part` (единое тело). **Границы и методика —
`references/geometry-audit.md`.** Аудит эвристический; истинная мин. толщина стенки и полный детект
криволинейных нависаний не решаются — это **не приговор и не доказательство печатнопригодности**.
## Связанное
- Полный численный свод DFM: [`references/fdm-rules.md`](references/fdm-rules.md).
- Рецепты самопроверки и границы: [`references/geometry-audit.md`](references/geometry-audit.md).
- Механика построения через MCP: навык **`kompas-3d`**.
- Поиск по справке SDK: субагент **`kompas-sdk-research`**.
- Дизайн навыка: `docs/superpowers/specs/2026-05-27-kompas-fdm-design-skill-design.md`.
@@ -0,0 +1,146 @@
# Свод правил DFM для FDM-печати
> Выверено 3 ревью (pi/glm-5.1, pi/kimi-k2.6, Codex). Числа для `w≈0.40.45`, `h≈0.2` (сопло 0.4).
> **Зазоры — на сторону (радиальные)**; диаметральный = 2×. **Угол нависания — от вертикали**
> (вертикаль=0°, горизонталь=90°); самонесущие — ≤ `θ_max`. Параметры `w`/`h`/`θ_max` — см.
> SKILL.md → «Калибровка». Ссылки «§N» ниже — на разделы этого файла.
## 1. Стенки и оболочки
- Толщина стенки = **`n · w`**. Мин. конструктивная — **2·w (~0.8 мм)**; несущая — **≥3·w**.
- **Маппинг стенка→периметры:** нужно `N` периметров ⇒ стенка **`N·w`** (при `w`=0.45: 3 → 1.35,
4 → 1.8, 5 → 2.25 мм).
- **Не задавай толщину стенки, не кратную `w`** (напр. 0.6 при `w`=0.45): слайсер оставит зазор
или переэкструдирует. Прыгай на следующий кратный.
- **Caveat:** `n·w` — для конструктивных стенок; внешняя функциональная величина (флексура,
тепловой барьер, посадочный размер) важнее кратности.
- Одиночная стенка `1·w` — только декоративная. Узкий сквозной прорез — **≥2·w (~0.8 мм)**.
## 2. Нависания, полки, мосты (разделять!)
- **Самонесущие — поверхности ≤ `θ_max` от вертикали.** Зона 45–60° (т.е. **выше** `θ_max`=45° при
дефолте) — ещё печатается, но с падением качества; **> `θ_max` существенно — поддержки** →
избегать редизайном.
- **90°-полка (консоль, опора с одной стороны) НЕ печатается ни на какой длине** (миф «≤6 мм»
неверен — провисает с первого слоя). Любую горизонтальную полку: **скос под `θ_max`**, либо
**превратить в мост** (две опоры), либо поддержка.
- **Мост (bridge) — пролёт между двумя опорами на одной высоте.** При достаточном обдуве, `h≈0.2`,
консервативно (для ненастроенного слайсера): **PLA ~1525 мм, PETG ~1015 мм, ABS ~1218 мм**.
Длинные прямоугольные проёмы **ориентировать так, чтобы мост шёл по короткой стороне**; концы —
на сплошных опорах.
- Нижнюю функциональную поверхность моста — припуск **0.20.3 мм** на провис. *Граница:* величину
провиса из CAD не предсказать (обдув/скорость — настройки печати).
- **Внутренние/потолочные нависания хуже наружных** — потолок пазов аркой/шевроном, не плоским
пролётом > 2 мм.
- **Доступ к поддержкам:** окно во внутренней полости **≥810 мм**.
## 3. Отверстия
- **Вертикальные (ось ∥ Z)** печатаются уже номинала → **увеличить диаметр модели** (радиус на
половину): **+0.2 мм (Ø<4)**, **+0.20.3 мм (Ø 410)**, **+0.10.2 мм (Ø>10)**; калибровать,
критичные — рассверливать.
- **Горизонтальные (ось в XY)** → **teardrop** или **D-отверстие** (плоский верх). Мин. Ø **2 мм**.
Круглая часть тоже печатается уже → **+0.10.2 мм** к её Ø.
- **Геометрия teardrop:** боковины касательны окружности под углом `θ_max` к вертикали (с двух
сторон), сходятся в вершине на вертикальной оси. Высота вершины над центром = **`r / sin θ_max`**;
включённый угол при вершине = **`2·θ_max`**. При `θ_max`=45° → `r/sin45° = √2·r ≈ 1.414·r` над
центром (= **0.414·r над верхом окружности**), угол 90°. Низ — оставшаяся дуга окружности.
- **Глухое отверстие:** дно = внутренний мост → **толщина дна ≥2–3 мм** или купольное/
вентилируемое. Сквозные предпочтительнее.
- **Отступ от края** — через остаточную перемычку: стенка между отверстием и краем **≥23·w**
(лёгкая нагрузка) / больше под крепёж.
## 4. Посадки и зазоры (печатная деталь ↔ печатная деталь)
Зазор **на сторону** (радиальный); диаметральный = 2× значения:
| Посадка | Зазор/сторону | Примечание |
|---|---|---|
| Натяг (press) | **−0.05…0 мм** (вычесть из номинала!) | короткий, PLA; иначе snap-fit (§14) |
| Переходная/плотная | 0.05–0.15 мм | |
| Скользящая | 0.150.20 мм | PLA↔PLA; контакт ≥20 мм → 0.20; PETG +0.05 |
| Свободная | 0.250.35 мм | >0.35/сторону — уже очень слабо |
- **Знак:** «натяг» = **отрицательный** зазор → вычесть из номинала (вал +/отверстие −).
Положительное число в строке press — ошибка прочтения.
- **ABS↔ABS:** +0.05/сторону. **PETG:** прессовые со временем «расслабляются».
- **Допуск точности (НЕ прибавлять к посадкам):** общий разброс FDM — **XY ±0.2 мм** (±0.1
калибровано), **Z хуже**. Это точность изготовления, не добавка к зазору.
## 5. Первый слой / стол
- **Elephant foot** — от притирки первого слоя (низкий Z-offset/переэкструзия; НЕ от высоты слоя).
Фаска по нижним рёбрам: **0.3 × 45° (калибровано)** / **0.51.0 × 45° (слабая калибровка)**.
- Внутренние углы у основания — **скругление ≥R0.5**.
- **Опорная площадка:** без «лезвийных» оснований; контакт хотя бы ~3 периметра. Высокие тонкие
детали — **интегральный фланец 1–2 мм** (предпочтительнее brim).
## 6. Ориентация и прочность
- **Z (межслойная) прочность от XY:** PLA ~4055%, PETG ~3550%, **ABS ~2035% (выброс)**. Несущую
нагрузку — в **XY (вдоль слоёв)**.
- **Z-сжатие допустимо** (слои в сжатии не расслаиваются); избегать **Z-растяжения и Z-среза**.
- Изгиб: слои в растяжении/сжатии, не на срез по линии слоя.
- Z-нагрузка неизбежна → **увеличить несущее сечение** ~×2 относительно XY-расчёта.
- Плоскости сопряжения — на **XY-гранях**, не на Z-боковинах.
## 7. Минимальные элементы и текст
- Выступ/штифт/ребро — **≥1·w (≥0.5 мм)**; паз/щель — **≥2·w (~0.8 мм)**.
- **Аспект тонких выступов:** высота ≤ ~4–5× базовой ширины; выше — конусность/раскос/редизайн.
- **Выпуклый** текст: штрих **≥0.5 мм**, высота **≥2·h (~0.4 мм)**, sans-serif bold.
- **Гравированный** текст: штрих **≥2·w (~0.80.9 мм)** (нужно ≥2 периметра; 0.6 мм не влезает),
глубина **≥2·h (~0.4 мм)**. (pt не используем — геометрия в мм.)
## 8. «Лесенка» (staircase) — критерий ориентации
- Наклонные/криволинейные поверхности дают ступени: глубина ≈ **`h / tan(α)`** (α — угол от
**горизонтали**; не путать с `θ_max`, который от вертикали). Пример: α=30°, `h`=0.2 → ~0.35 мм.
- Критичные (скользящие/уплотняющие/оптические) поверхности **ориентировать вертикально или
горизонтально**. Это вход в правило ориентации (SKILL.md, правило 1).
- Большой плоский **верх** без опоры коробит («подушка») — внутренние рёбра каждые ~15–20 мм или
достаточная толщина верха.
## 9. Бобышки, инсёрты, резьба
- **Саморез/винтонарезной** пилот (M3): ~Ø2.5 PLA / Ø2.6 PETG / Ø2.7 ABS; заход ≥3 мм.
- **Термоинсёрт латунный** (M3): бор **по даташиту** (типично ~Ø4.0, ±0.05); **`Ø_bore = OD_инсёрта (0…0.1) мм`** (≤ OD, лёгкий натяг под расплав — НЕ больше OD); стенка бобышки **≥2 мм**; глубина = длина инсёрта + 0.5 мм; **ставить с верхней (Z) грани** (не в боковину).
- **Бобышка под винт:** OD ≥ 2–3× Ø винта; не делать массивный сплошной объём (карман/оболочка).
- **Сквозное под металлический болт:** радиальный зазор 0.2–0.3 → **+0.4–0.6 мм к номиналу** болта.
- **Резьба:** не моделировать <M6 → инсёрты/саморезы. Если моделировать: **≥M6, ось вертикальная**,
зазор +0.1–0.2 мм, профиль крупный/трапецеидальный (не мелкий ISO — вершины-нависания).
## 10. Фаски vs скругления
- **Нижние (у стола) рёбра — фаска** (скругление = нулевой контакт + EF).
- **Верхние рёбра — скругление** (R0.5–2.0); **но** радиус **> ~½ толщины стенки** сам даёт
нависание > `θ_max` → тогда фаска/ступень.
- **Внутренние углы — всегда скругление ≥R0.5**.
## 11. Зенковки / цековки
- **Цековка (counterbore)** — большим Ø/полостью **вверх** (дно по телу, не мостом); глубина +0.3 мм.
- **Зенковка (countersink):** конус **вверх**; включённый угол **≤90°** → стенки ≤45° от вертикали
печатается; **>90°** → стенки-нависание → поддержка или замена цилиндрической цековкой.
## 12. Заходные фаски (assembly relief)
- На штифтах, отверстиях, инсёртах, защёлках, «ласточкиных хвостах» — **заходная фаска** (≈0.51 мм
× 45° или ≈ половина зазора) против задиров при сборке.
## 13. Разбиение детали и сборка из печатных частей
- Конфликт «прочная ориентация vs бесподдержечность», или крупная/коробящаяся деталь → **разбить**
на части с самоустанавливающимися стыками (печатные штифты/шпонки/замки), склейка; стыки на
XY-гранях. Зазор стыка — по §4.
## 14. Защёлки (snap-fit) / живой шарнир
- Консольная защёлка: толщина балки **≈1.0–2.0 мм** (функциональный размер, не привязан к кратности
`w`; тоньше ~1 мм — хрупко), зацеп/возврат **0.30.8 мм**, длина/толщина **≥5:1** (до 10:1),
**скругление в основании ≥R0.5**.
- **Направление слоёв:** балка гнётся **в плоскости XY** (слои перпендикулярны изгибу), **не
поперёк Z** (расслоится с первого нажатия).
- **Живой шарнир** — только PLA/PP-подобные, перемычка **0.30.5 мм**; PETG/ABS не годятся.
## 15. Коробление (геометрия)
- Большие плоскости (>80×80, особенно ABS): скругления углов R3–5 + рёбра/решётка снизу.
- Радиус внешних углов: R2 (ABS) / R1 (PLA/PETG).
- Длинные тонкие пролёты (>60 мм, <2 мм) — рёбра/косынки каждые 30–40 мм; высота ребра ≤5× базы.
- Избегать сплошных кубов/плит → карман/оболочка + рёбра. Усадка: PLA ~0.3%, PETG ~0.5%, ABS ~0.8%.
- Симметрия геометрии уравновешивает усадку.
- *«Мышиные уши» (Ø8–10 мм по углам)* — **крайняя мера адгезии** (по сути brim-геометрия);
предпочтительно интегральный фланец/скругления углов.
- *Граница:* стол/корпус/обдув для ABS — настройки печати, вне навыка; здесь только геометрия.
## 16. Полые детали и гигиена модели
- **Полости:** дренаж Ø3–5 мм у **низшей** точки + вент у **высшей**.
- Допуски/зазоры — **в геометрию** (слайсер читает модель буквально).
- Раздельные тела — зазор ≥0.2 мм (общая CAD-гигиена; перед выдачей объединять — рабочий цикл,
шаг 5 в SKILL.md).
@@ -0,0 +1,33 @@
# Гео-аудит модели под FDM — что проверяемо инструментами осмотра
Лёгкая самопроверка построенной модели **существующими** инструментами осмотра MCP. Запускать на
шаге 4 рабочего цикла (см. SKILL.md), перед чек-листом и экспортом.
## Что проверяемо
| Проверка | Как | Статус |
|---|---|---|
| Нависания (приближённо) | `list_faces`/`describe_face`: для **нижних** граней угол поверхности от вертикали; > `θ_max` → флаг | ✅ плоские; ⚠️ криволинейные грубо |
| Ориентация (геом. прокси) | `get_bounding_box`: как ось слоёв соотносится с габаритом | ⚠️ длинная ось ≠ путь нагрузки |
| Горизонтальные круглые отверстия | `describe_face`: цилиндр с горизонтальной осью → «нужен teardrop» | ✅ |
| Номиналы / зазоры / габариты | `measure` между гранями; `get_bounding_box` | ✅ |
| Тело / манифолд перед выдачей | `list_bodies` (одно тело?), `validate_part` | ✅ |
## Граница честности
- **Угол нависания** мерить в **той же конвенции, что fdm-rules.md** (от вертикали; нижняя грань с
поверхностью > `θ_max` от вертикали = нависание) — не путать с углом нормали от горизонтали.
- **Путь нагрузки агент НЕ выводит из габарита** — берёт из задачи/опроса (правило 1). Длинная ось
≠ несущая.
- **Истинная мин. толщина стенки и полный детект криволинейных нависаний — не решаются** (нет
thickness/overhang-солвера).
- **Аудит эвристический и НЕ доказывает печатнопригодность** (не ловит анизотропию/путь нагрузки).
Вывод — список флагов для решения, не «приговор». Слайсер навык не зовёт намеренно.
## Как применять в цикле
1. После построения и `validate_part` — пройти таблицу выше сверху вниз.
2. Каждый флаг — сверить с соответствующим правилом `fdm-rules.md` и решить: исправить геометрию
или принять осознанно.
3. Путь нагрузки и косметические грани взять из ответа пользователя (правило 1), не из габарита.
4. Затем — чек-лист печатнопригодности (SKILL.md) → экспорт через `export_step`.
+87
View File
@@ -0,0 +1,87 @@
name = "kompas-sdk-research"
model = "gpt-5.6-terra"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
description = "ОБЯЗАТЕЛЬНО делегируй этому read-only субагенту ЛЮБОЙ поиск по справке COM API КОМПАС в MD-базе docs/Kompas3D_SDK/ — НЕ ищи по справке в основной задаче. Срабатывает ПРОАКТИВНО, без явной просьбы пользователя: как только при планировании или реализации фичи понадобилась сигнатура метода (Automation/COM), значения перечисления (Obj3dType, ksHoleTypeEnum, ST_MIX_*, стили линий…), нужный интерфейс под задачу, единицы измерения или цепочка вызовов — СНАЧАЛА делегируй исследование этому агенту, ПОТОМ пиши код. Агент возвращает сжатую выжимку, а основная задача при необходимости перепроверяет спорные детали рефлексией по libs/kompas-interop/*.dll. Триггеры: «нужна сигнатура …», «какие значения enum …», «каким интерфейсом сделать X через API КОМПАС», «что возвращает <метод>», «как через COM API построить …», «найди в справке КОМПАС». НЕ для построения геометрии в КОМПАС и НЕ для KsAPI (Qt/C++ — в MD-базе его нет)."
developer_instructions = '''
Ты — помощник по навигации в справке **КОМПАС SDK** в репозитории
**kompas3d-mcp** (MCP-сервер для КОМПАС-3D через COM API, .NET 8, C#).
Справка — это MD-база знаний `docs/Kompas3D_SDK/`: статьи по интерфейсам с
YAML-фронтматтером (`type`, `api`, `domain`, `tags`, `sources`); методы и
свойства — заголовки `## ` внутри статьи интерфейса.
Тебя вызывают, чтобы основная задача не тратила контекст на поиск:
**вопрос для исследования придёт в промпте от вызывающего**. Прогони Grep/Read
по базе, прочитай нужную статью и верни сжатый ответ (интерфейс, сигнатура,
параметры, значения констант, источник).
## Когда это про тебя
- Нужна сигнатура метода (`SetSideParam`, `GetGabarit`, …) — Automation/COM, параметры, возврат.
- Нужны значения перечисления/констант (`Obj3dType`, `ksMassUnitsEnum`, `ST_MIX_*`, стили линий).
- Нужно найти интерфейс под задачу («чем построить массив», «как задать переменную»).
- Нужен паттерн «как через API сделать X» с цепочкой вызовов.
## Структура MD-базы `docs/Kompas3D_SDK/`
```
index.md — оглавление по 4 категориям со ссылками на статьи
interfaces/*.md — одна статья на интерфейс/тему; все методы и свойства как ## секции
enums/*.md — перечисления (пары «имя | значение | смысл»)
structures/*.md — структуры и параметрические интерфейсы
guides/*.md — концепции и руководства
resources/ — изображения (ссылки в статьях вида ../resources/имя.png)
```
Каждая статья начинается с YAML-фронтматтера: `type` (interface/enum/struct/guide),
`api` (api7/api5), `domain` (3d/2d/sketch/assembly/…), `tags`, `sources` (исходные `*.html`).
Имя файла статьи — имя интерфейса (`iapplication.md`, `ksbossextrusiondefinition.md`)
или читаемый слаг темы (`приложение.md`). Методы/свойства внутри — заголовки `## Имя - Описание`.
> База **хранится в репозитории** (канонический источник справки) — отдельная
> генерация не нужна.
## Инструменты (используй именно их, не веб)
- **Grep** по `docs/Kompas3D_SDK/` — ищи имя интерфейса/метода/свойства, русское
ключевое слово или тег. Имена методов — это заголовки `## SetSideParam - …`,
поэтому поиск по имени метода сразу ведёт в нужную статью.
- **Read** найденного `*.md` — статья интерфейса содержит все его методы/свойства,
блоки `Синтаксис Automation:`/`Синтаксис COM:` (в code-fence), параметры, примечания.
- `docs/Kompas3D_SDK/index.md` — оглавление по категориям, когда нужно осмотреть тему.
Полезные приёмы поиска:
- по перечислению: `Grep "ksMassUnitsEnum"` или загляни в `enums/`;
- фильтр по тегам в фронтматтере: `Grep "domain: \[.*3d"` , `Grep "type: enum"`;
- веер по многим интерфейсам сразу: `Grep "IPart7|IDocument3D|IPart"` по `interfaces/`.
Точные числовые значения констант при необходимости сверяй в заголовках SDK:
`C:\Program Files\ASCON\KOMPAS-3D v24 Home\SDK\Include\*.h` (например `ksConstants*.h`, `ldefin2d.h`).
## Важно — что в базе и чего нет
- База содержит **COM API7/API5** — то, что использует проект (файлы `iapplication*`,
`kspart*`, `ksmassinertiaparam*` и т.п.). Это твой основной источник.
- Кроссплатформенный Qt/C++ **KsAPI** (`ksapi_*`) в базу **не включён** — если задача
явно про KsAPI, скажи об этом (нужен отдельный источник).
## Правила
1. Не угадывай — находи через Grep/Read по `docs/Kompas3D_SDK/` и читай первоисточник.
2. Верни **сжатую выжимку**, а не дамп статьи:
- имя интерфейса и метода/свойства;
- синтаксис Automation (и COM, если важен порядок out-параметров);
- параметры (имя, тип, смысл) и возвращаемое значение;
- для enum/констант — пары «имя = значение» и их смысл;
- важные примечания (единицы измерения, побочные эффекты, версия КОМПАС);
- имя статьи / `sources` из фронтматтера как ссылку на источник.
3. Если у метода несколько перегрузок или связанных интерфейсов — перечисли релевантные.
4. Если в COM-interop имя отличается от справки (частый случай в этом проекте), отметь это
как вероятную тонкость и предложи свериться рефлексией по `libs/kompas-interop/*.dll`.
5. Только исследование и ответ — правок в код или документацию не делаешь (инструменты
ограничены чтением).
## Пример задачи в промпте
«Нужна сигнатура и единицы измерения у метода расчёта МЦХ детали через API5 — что
возвращает, как задать мм/кг.» Или короче: «как построить массив по сетке (pattern)».'''
+51 -33
View File
File diff suppressed because one or more lines are too long
+6 -4
View File
@@ -9,7 +9,9 @@ LLM набор инструментов для создания документ
Принцип: **MCP транслирует возможности SDK КОМПАС в общие инструменты** (не под конкретную задачу).
Поверх MCP — навык **`.claude/skills/kompas-3d/`** с методикой работы (playbook'и, эвристики,
обкатанные в `usecases/`).
обкатанные в `usecases/`). Отдельный навык **`.claude/skills/kompas-fdm-design/`** — методика
проектирования под FDM/FFF 3D-печать (правила DFM + лёгкий гео-аудит инструментами осмотра);
работает поверх `kompas-3d`, экспорт через `export_step`, слайсер не вызывает.
> Подробности: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) · план — [docs/IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md)
> · спорные вопросы — [docs/OPEN_QUESTIONS.md](docs/OPEN_QUESTIONS.md) · обзор — [docs/presentation.html](docs/presentation.html)
@@ -39,7 +41,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe
}
```
## Инструменты (76 инструментов)
## Инструменты (83 инструмента)
| Группа | Инструменты |
|---|---|
@@ -54,7 +56,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe
| Variables | `create_variable`, `set_variable`, `delete_variable` |
| Conversion | `import_step`, `export_step` |
| Assembly | `assembly_add_component`, `assembly_add_mate` |
| Drawing | `drawing_create_standard_views`, `drawing_fill_title_block`, `drawing_add_linear_dimension`, `drawing_add_diametral_dimension` |
| Drawing | `drawing_create_standard_views`, `drawing_fill_title_block`, `drawing_add_linear_dimension`, `drawing_add_diametral_dimension`, `drawing_add_radial_dimension`, `drawing_add_angular_dimension`, `drawing_add_rough`, `drawing_add_text`, `drawing_set_technical_requirements`, `drawing_set_sheet_format`, `drawing_add_leader` |
| Validation | `validate_part` |
### Типовой сценарий
@@ -73,7 +75,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/ 218 тестов: unit + integration (integration требуют КОМПАС)
tests/Kompas.Mcp.Tests/ 331 тест: unit + integration (integration требуют КОМПАС)
libs/kompas-interop/ вендорские interop-сборки КОМПАС (из SDK Samples/Common)
docs/ архитектура, план, презентация, MD-база знаний SDK (Kompas3D_SDK/)
usecases/ полигон обкатки подходов (в .gitignore); приёмы поднимаются в навык kompas-3d
+271
View File
@@ -0,0 +1,271 @@
# Каталог внешнего CAD-контракта для автономного агента
## Назначение и критерий полноты
Этот документ разделяет два слоя: **текущий контракт** из 84 публично зарегистрированных инструментов Host и **целевые расширения**, необходимые автономному агенту. Для существующего метода статус оценивает только поведение, прямо заявленное в его строке: отсутствие соседних `list/update/delete` или явного выбора документа не понижает полностью выполненный метод до частичного. Такие системные разрывы вынесены в отдельные целевые строки.
Контракт считается полным не по числу методов, а когда агент может замкнуть цикл: обнаружить состояние, выбрать адресуемые объекты, изменить модель, проверить результат и сохранить его либо безопасно откатить. Для мутирующих семейств проверяется симметрия `list/describe → create → update/transform → delete → validate`.
Общее текущее ограничение: все документные, модельные, сборочные и чертёжные операции неявно работают только с активным документом. Стабильного идентификатора документа и явного параметра выбора документа нет. Поэтому одновременная надёжная работа с несколькими открытыми документами пока невозможна.
## Легенда
| Поле | Значения |
|---|---|
| Статус | ✅ реализован · 🟡 частично · ⬜ не реализован · ⛔ ограничен платформой |
| Необходимость | `Core` — нужен для замкнутого базового цикла · `Advanced` — нужен для типового профессионального сценария · `Optional` — расширяет охват или удобство |
| Приоритет | `P0` — замыкает автономный цикл · `P1` — снимает существенное профессиональное ограничение · `P2` — расширяет охват; `—` — пробела нет |
`🟡 частично` означает, что имя уже существует, но его результат, адресуемость или набор вариантов недостаточны для заявленного целевого поведения. `⛔ ограничен платформой` означает, что надёжное внешнее поведение не подтверждено и требует отдельного технического исследования либо другого режима исполнения.
## Покрытие сквозных сценариев
| Сценарий | Обнаружить состояние | Изменить | Проверить | Сохранить | Восстановить | Итог и блокирующие пробелы |
|---|---|---|---|---|---|---|
| Создать и сохранить параметрическую 3D-деталь | частично: структура и переменные читаются | частично: операции строятся, но размеры созданных эскизов не связаны с переменными | частично: геометрия и ошибки проверяются, но созданные сущности не имеют полного адресного цикла | частично: только активный документ | неполный: нет управляемого checkpoint lifecycle и отката | **неполный** — нет надёжной параметризации эскиза, изменения/удаления операций и восстановления |
| Открыть или импортировать модель, локально изменить и проверить | частично: тела, грани и рёбра видны по индексам, но из них нельзя получить selector с ревизией | частично: доступны перемещение грани, рассечение, перенос тела и объединение | частично: есть структурная и числовая проверка | частично: только активный документ | неполный | **частичный** — нет полного selector lifecycle, удаления тел, subtract/intersect и безопасного отката |
| Создать сборку, разместить компоненты, наложить сопряжения и проверить структуру | неполный: только верхний уровень, без списка сопряжений | частично: вставка и два типа сопряжений | неполный: нет структурной диагностики решённой сборки; проверка коллизий остаётся Advanced-пробелом | частично: только активный документ | неполный | **неполный** — нет рекурсивной структуры, placement, удаления, полного семейства сопряжений и структурной валидации |
| Создать основные виды чертежа, оформить и сохранить | неполный: нет агрегированного паспорта, инвентаризации листов, видов и аннотаций | частично: основные виды и базовые обозначения создаются | неполный: нет машинной валидации видов, ассоциаций и оформления | частично: только активный документ | неполный | **неполный** — нет describe/validate, CRUD видов и аннотаций, разрезов, баз, допусков, осевых и многолистности |
| Выполнить геометрическую и документную инспекцию без визуального анализа | частично: модель описывается структурно, но нет списка документов и вершин | не требуется | частично: измерение принимает вершины, которые нельзя предварительно перечислить; индексы нельзя преобразовать в устойчивый selector с ревизией | не требуется | не требуется | **частичный** — отсутствуют multi-document discovery, вершины и полный selector lifecycle |
## Методы внешнего контракта
### Сессия и документы
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `kompas_connect` | Подключить CAD-сессию и вернуть сведения о среде | ✅ реализован | Core | — | — |
| `kompas_status` | Получить состояние подключения | ✅ реализован | Core | — | — |
| `kompas_set_visible` | Показать или скрыть пользовательское окно | ✅ реализован | Optional | — | — |
| `document_create` | Создать документ заданного типа и сделать его активным | ✅ реализован | Core | — | — |
| `document_open` | Открыть документ с диска и сделать его активным | ✅ реализован | Core | — | — |
| `document_save_as` | Сохранить активный документ по новому пути | ✅ реализован | Core | — | — |
| `document_save` | Сохранить активный документ | ✅ реализован | Core | — | — |
| `document_close` | Закрыть активный документ с выбранным режимом сохранения | ✅ реализован | Core | — | — |
| `document_active` | Описать активный документ | ✅ реализован | Core | — | — |
| `list_documents` | Перечислить открытые документы с типом, путём, состоянием изменений и `document_id` | ⬜ не реализован | Core | P0 | Требуется основа multi-document контура |
| `document_activate` | Сделать документ активным по `document_id` | ⬜ не реализован | Core | P0 | Без него невозможно надёжно переключать контекст |
| `document_describe` | Прочитать свойства и состояние указанного документа без его активации | ⬜ не реализован | Core | P0 | Нужна документная инспекция вне глобального active state |
| `document_set_properties` | Изменить стандартные и пользовательские свойства документа | ⬜ не реализован | Advanced | P1 | Нет внешнего управления метаданными |
### 2D-геометрия и эскизы
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `sketch_create` | Создать эскиз на базовой плоскости и вернуть сессионный идентификатор | ✅ реализован | Core | — | — |
| `sketch_create_on_face` | Создать эскиз на плоской грани, выбранной точкой | ✅ реализован | Advanced | — | — |
| `sketch_create_on_offset_plane` | Создать эскиз на смещённой плоскости | ✅ реализован | Advanced | — | — |
| `sketch_add_circle` | Добавить окружность в открытый эскиз | ✅ реализован | Core | — | — |
| `sketch_add_line` | Добавить отрезок в открытый эскиз | ✅ реализован | Core | — | — |
| `sketch_create_on_face_index` | Создать эскиз на грани по текущему индексу | ✅ реализован | Core | — | — |
| `sketch_add_axis` | Добавить осевую линию в открытый эскиз | ✅ реализован | Core | — | — |
| `sketch_add_rectangle` | Добавить прямоугольник в открытый эскиз | ✅ реализован | Core | — | — |
| `sketch_close` | Завершить редактирование текущего открытого эскиза | ✅ реализован | Core | — | — |
| `sketch_add_arc_3points` | Добавить дугу по трём точкам | ✅ реализован | Advanced | — | — |
| `sketch_add_arc` | Добавить дугу по центру, радиусу и углам | ✅ реализован | Advanced | — | — |
| `sketch_add_ellipse` | Добавить эллипс | ✅ реализован | Advanced | — | — |
| `sketch_add_polyline` | Добавить цепочку отрезков | ✅ реализован | Core | — | — |
| `sketch_add_polygon` | Добавить правильный многоугольник | ✅ реализован | Advanced | — | — |
| `sketch_add_spline` | Добавить сплайн | ✅ реализован | Advanced | — | — |
| `sketch_add_point` | Добавить опорную точку | ✅ реализован | Advanced | — | — |
| `list_sketches` | Перечислить эскизы документа со стабильными идентификаторами и состоянием | ⬜ не реализован | Core | P0 | Нельзя восстановить контекст после открытия документа |
| `describe_sketch` | Прочитать плоскость, состояние и геометрическую сводку эскиза | ⬜ не реализован | Core | P0 | Нет проверки исходного или созданного эскиза |
| `list_sketch_entities` | Перечислить сущности эскиза с типом, геометрией и `entity_id` | ⬜ не реализован | Core | P0 | Созданные объекты не адресуются |
| `update_sketch_entity` | Изменить геометрию сущности по `entity_id` | ⬜ не реализован | Core | P0 | Нет редактирования без пересоздания эскиза |
| `delete_sketch_entity` | Удалить сущность по `entity_id` | ⬜ не реализован | Core | P0 | Нет симметрии create/delete |
| `delete_sketch` | Удалить эскиз с контролем зависимостей | ⬜ не реализован | Core | P0 | Нельзя исправить ошибочно созданный эскиз |
| `list_sketch_constraints` | Перечислить геометрические связи и размеры | ⬜ не реализован | Advanced | P1 | Нет диагностики степени определённости |
| `sketch_set_constraint` | Создать или изменить геометрическую связь | ⬜ не реализован | Advanced | P1 | Нет управляемых зависимостей геометрии |
| `sketch_delete_constraint` | Удалить связь или размер | ⬜ не реализован | Advanced | P1 | Нет полного жизненного цикла ограничений |
| `sketch_set_driving_dimension` | Связать размер эскиза с числом или переменной | ⛔ ограничен платформой | Core | P0 | Надёжная внешняя связь размера с переменной не подтверждена; требуется отдельное техническое исследование или иной режим исполнения |
### 3D-моделирование
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `extrude_boss` | Добавить материал выдавливанием эскиза | ✅ реализован | Core | — | — |
| `extrude_cut` | Удалить материал выдавливанием эскиза | ✅ реализован | Core | — | — |
| `revolve_boss` | Добавить материал вращением профиля | ✅ реализован | Advanced | — | — |
| `revolve_cut` | Удалить материал вращением профиля | ✅ реализован | Advanced | — | — |
| `fillet_edge` | Скруглить одно ребро, выбранное точкой | ✅ реализован | Advanced | — | — |
| `chamfer_edge` | Снять фаску с одного ребра, выбранного точкой | ✅ реализован | Advanced | — | — |
| `fillet_edge_index` | Скруглить одно ребро по текущему индексу | ✅ реализован | Advanced | — | — |
| `chamfer_edge_index` | Снять фаску с одного ребра по текущему индексу | ✅ реализован | Advanced | — | — |
| `shell` | Создать оболочку с удалением граней по текущим индексам | ✅ реализован | Advanced | — | — |
| `rib` | Создать ребро жёсткости по эскизу | ✅ реализован | Advanced | — | — |
| `sweep` | Построить тело перемещением профиля по траектории | ✅ реализован | Advanced | — | — |
| `loft` | Построить тело по сечениям | ✅ реализован | Advanced | — | — |
| `linear_pattern` | Создать линейный массив операций | ✅ реализован | Advanced | — | — |
| `circular_pattern` | Создать круговой массив операций | ✅ реализован | Advanced | — | — |
| `mirror_operation` | Создать зеркальную копию операций | ✅ реализован | Advanced | — | — |
| `mirror_body` | Зеркально скопировать все тела относительно базовой плоскости | ✅ реализован | Advanced | — | — |
| `hole` | Создать простое отверстие | ✅ реализован | Core | — | — |
| `draft` | Создать уклон граней по текущим индексам | ✅ реализован | Advanced | — | — |
| `hole_counterbore` | Создать отверстие с цилиндрическим углублением | ✅ реализован | Advanced | — | — |
| `hole_countersink` | Создать отверстие с коническим углублением | ✅ реализован | Advanced | — | — |
| `hole_conic` | Создать коническое отверстие | ✅ реализован | Advanced | — | — |
| `rebuild` | Перестроить активную модель | ✅ реализован | Core | — | — |
| `create_variable` | Создать переменную модели | ✅ реализован | Advanced | — | — |
| `set_variable` | Изменить выражение переменной и перестроить модель | ✅ реализован | Advanced | — | — |
| `set_variable_note` | Изменить комментарий переменной | ✅ реализован | Optional | — | — |
| `delete_variable` | Удалить переменную с проверкой зависимостей | ✅ реализован | Advanced | — | — |
| `describe_feature` | Прочитать тип, параметры, состояние и зависимости операции по `feature_id` | ⬜ не реализован | Core | P0 | Текущая инспекция не даёт полного адресного контракта |
| `update_feature` | Изменить параметры существующей операции и перестроить модель | ⬜ не реализован | Core | P0 | Нет исторического редактирования |
| `delete_feature` | Удалить операцию с диагностикой зависимостей | ⬜ не реализован | Core | P0 | Нет исправления ошибочного построения |
| `set_feature_suppressed` | Исключить или вернуть операцию в расчёт | ⬜ не реализован | Advanced | P1 | Нет управления вариантами и диагностикой дерева |
| `datum_point_create` | Создать адресуемую вспомогательную 3D-точку по координатам или выбранной геометрии | ⬜ не реализован | Advanced | P1 | Нет опорных точек для последующих построений и измерений |
| `datum_axis_create` | Создать адресуемую вспомогательную ось по точкам, направлению или выбранной геометрии | ⬜ не реализован | Advanced | P1 | Нет произвольных опорных осей вне координатной системы |
| `datum_plane_create` | Создать адресуемую вспомогательную плоскость по трём точкам, оси с углом или выбранной геометрии | ⬜ не реализован | Advanced | P1 | Сейчас доступна только смещённая базовая плоскость внутри создания эскиза |
### Прямое и историческое редактирование
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `move_face` | Сместить грань, выбранную точкой, вдоль нормали | ✅ реализован | Core | — | — |
| `split_solid_by_plane` | Рассечь тела активной детали базовой смещённой плоскостью | ✅ реализован | Advanced | — | — |
| `move_body` | Задать перенос и ориентацию выбранного тела | 🟡 частично | Advanced | P1 | Сейчас доступен только перенос тела по текущему индексу; поворот и явная ориентация отсутствуют |
| `boolean_union` | Объединить все тела активной детали | ✅ реализован | Advanced | — | — |
| `copy_body` | Копировать выбранное тело с преобразованием | ⬜ не реализован | Advanced | P1 | Нет симметричной операции к перемещению и удалению |
| `delete_body` | Удалить выбранное тело | ⬜ не реализован | Core | P0 | Нет полного жизненного цикла многотельной модели |
| `boolean_subtract` | Вычесть выбранные тела-инструменты из целевого тела | ⬜ не реализован | Advanced | P1 | Отсутствует типовая булева операция |
| `boolean_intersect` | Оставить пересечение выбранных тел | ⬜ не реализован | Advanced | P1 | Отсутствует типовая булева операция |
| `body_check_intersection` | Найти пересечения выбранных тел без изменения модели и вернуть пары с оценкой объёма | ⬜ не реализован | Advanced | P1 | Нет аналитической проверки коллизий внутри многотельной детали |
### Инспекция, выбор объектов и измерения
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `describe_model` | Получить структурный паспорт активной модели | ✅ реализован | Core | — | — |
| `list_features` | Перечислить дерево операций и доступные основные параметры | ✅ реализован | Core | — | — |
| `list_bodies` | Перечислить тела по текущим индексам | ✅ реализован | Core | — | — |
| `list_variables` | Перечислить переменные и вычисленные значения | ✅ реализован | Advanced | — | — |
| `describe_face` | Описать грань по текущему индексу | ✅ реализован | Core | — | — |
| `describe_edge` | Описать ребро по текущему индексу | ✅ реализован | Core | — | — |
| `measure` | Измерить расстояние и угол между объектами по переданным индексам, включая вершины | ✅ реализован | Core | — | — |
| `get_part_info` | Получить объём, массу, площадь и центр масс | ✅ реализован | Core | — | — |
| `list_components` | Перечислить компоненты верхнего уровня сборки | ✅ реализован | Core | — | — |
| `list_faces` | Перечислить грани и их типы по текущим индексам | ✅ реализован | Core | — | — |
| `list_edges` | Перечислить рёбра и их типы по текущим индексам | ✅ реализован | Core | — | — |
| `get_bounding_box` | Получить габарит модели | ✅ реализован | Core | — | — |
| `model_snapshot` | Получить растровый снимок модели | ✅ реализован | Optional | — | — |
| `validate_part` | Найти ошибки и необходимость перестроения детали или сборки | ✅ реализован | Core | — | — |
| `get_model_revision` | Получить монотонную ревизию структуры и топологии модели | ⬜ не реализован | Core | P0 | Нужна для обнаружения устаревших ссылок |
| `list_vertices` | Перечислить вершины с координатами и селекторами | ⬜ не реализован | Core | P0 | Замыкает входной контракт измерения вершин |
| `create_selector` | Создать устойчивый геометрический селектор по типу объекта и текущему индексу, вернув также ревизию модели | ⬜ не реализован | Core | P0 | Нет входной точки от списков граней, рёбер, вершин и тел к resolve/validate lifecycle |
| `resolve_selector` | Найти объект по геометрическому селектору и вернуть совпадение в текущей ревизии | ⬜ не реализован | Core | P0 | Нет устойчивого повторного выбора после перестроения |
| `validate_selector` | Проверить, что ссылка относится к текущей ревизии и однозначному объекту | ⬜ не реализован | Core | P0 | Мутации могут примениться не к тому объекту |
### Сборки
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `assembly_add_component` | Вставить деталь или подсборку и задать исходное положение | ✅ реализован | Core | — | — |
| `assembly_add_mate` | Создать адресуемое сопряжение поддерживаемого типа между компонентами | 🟡 частично | Core | P0 | Сейчас доступны только coincidence/distance, выбор по точкам и результат без `mate_id`; нужны также concentric, parallel, perpendicular, angle и tangency |
| `describe_assembly` | Рекурсивно описать дерево компонентов, документы, состояния и placement | ⬜ не реализован | Core | P0 | Верхнеуровневого списка недостаточно для управления |
| `describe_component` | Прочитать компонент, его placement, подавление и ссылки | ⬜ не реализован | Core | P0 | Нет адресной инспекции компонента |
| `assembly_set_component_placement` | Изменить перенос и поворот компонента по `component_id` | ⬜ не реализован | Core | P0 | Нет управления placement после вставки |
| `assembly_remove_component` | Удалить компонент с диагностикой зависимых сопряжений | ⬜ не реализован | Core | P0 | Нет симметрии add/remove |
| `assembly_set_component_suppressed` | Исключить или вернуть компонент в расчёт | ⬜ не реализован | Advanced | P1 | Нет управления вариантами сборки |
| `list_mates` | Перечислить сопряжения, объекты, параметры и состояние решения | ⬜ не реализован | Core | P0 | Созданные сопряжения не адресуются |
| `assembly_update_mate` | Изменить тип, объекты или параметр сопряжения | ⬜ не реализован | Core | P0 | Нет исправления сопряжения без пересоздания |
| `assembly_remove_mate` | Удалить сопряжение по `mate_id` | ⬜ не реализован | Core | P0 | Нет симметрии add/remove |
| `assembly_check_interference` | Найти пересечения компонентов и оценить объём коллизий | ⬜ не реализован | Advanced | P1 | Нет профессиональной проверки физических коллизий; минимальная структурная проверка остаётся в `validate_assembly` |
| `validate_assembly` | Проверить нерешённые, конфликтующие и избыточные сопряжения | ⬜ не реализован | Core | P0 | Текущая общая проверка не даёт сборочный диагноз |
| `specification_generate` | Создать или полностью обновить спецификацию активной сборки по её текущему составу | ⬜ не реализован | Advanced | P1 | Нет выпуска состава изделия; повторный вызов должен детерминированно заменять ранее сгенерированное содержимое |
| `specification_list_items` | Перечислить позиции спецификации с обозначением, наименованием, количеством и ссылкой на компонент | ⬜ не реализован | Advanced | P1 | Нет read-back для проверки сформированного состава |
### Чертежи и оформление
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `drawing_create_standard_views` | Создать набор основных ассоциативных видов и вернуть их номера | ✅ реализован | Core | — | — |
| `drawing_fill_title_block` | Заполнить обозначение, наименование и материал в штампе | ✅ реализован | Core | — | — |
| `drawing_add_linear_dimension` | Создать свободный линейный размер | ✅ реализован | Core | — | — |
| `drawing_add_diametral_dimension` | Создать свободный или связанный с окружностью диаметральный размер | ✅ реализован | Core | — | — |
| `drawing_add_radial_dimension` | Создать свободный или связанный с окружностью радиальный размер | ✅ реализован | Core | — | — |
| `drawing_add_angular_dimension` | Создать свободный угловой размер | ✅ реализован | Core | — | — |
| `drawing_add_rough` | Создать свободное обозначение шероховатости | ✅ реализован | Advanced | — | — |
| `drawing_add_text` | Создать текстовую надпись | ✅ реализован | Core | — | — |
| `drawing_set_technical_requirements` | Заменить блок технических требований | ✅ реализован | Advanced | — | — |
| `drawing_set_sheet_format` | Задать формат и ориентацию листа по номеру | ✅ реализован | Core | — | — |
| `drawing_add_leader` | Создать свободную выноску с одной ветвью и текстом | ✅ реализован | Advanced | — | — |
| `describe_drawing` | Получить структурный паспорт чертежа: листы, виды, основную надпись, технические требования, аннотации и состояние ассоциаций | ⬜ не реализован | Core | P0 | Нет агрегированного read-back без визуального анализа |
| `validate_drawing` | Диагностировать ошибочные или устаревшие виды, разорванные ассоциации и неполное оформление | ⬜ не реализован | Core | P0 | Нет машинной проверки готовности чертежа |
| `drawing_list_sheets` | Перечислить листы, форматы, стили и состояния | ⬜ не реализован | Core | P0 | Нет основы многолистного управления |
| `drawing_add_sheet` | Добавить лист с форматом и стилем | ⬜ не реализован | Advanced | P1 | Многолистный комплект не создаётся |
| `drawing_delete_sheet` | Удалить лист по `sheet_id` | ⬜ не реализован | Advanced | P1 | Нет полного жизненного цикла листа |
| `drawing_list_views` | Перечислить виды с `view_id`, источником, масштабом и placement | ⬜ не реализован | Core | P0 | Созданные виды нельзя надёжно найти и проверить |
| `drawing_describe_view` | Прочитать параметры, границы и состояние обновления вида | ⬜ не реализован | Core | P0 | Нет адресной проверки вида |
| `drawing_update_view` | Изменить масштаб, placement, ориентацию и видимость вида | ⬜ не реализован | Core | P0 | Нет редактирования вида |
| `drawing_delete_view` | Удалить вид и диагностировать зависимые обозначения | ⬜ не реализован | Core | P0 | Нет симметрии create/delete |
| `drawing_rebuild_views` | Обновить связанные виды и вернуть изменившиеся/ошибочные объекты | ⬜ не реализован | Core | P0 | Нет явной проверки актуальности после изменения модели |
| `drawing_create_section_view` | Создать разрез или сечение по линии | ⬜ не реализован | Advanced | P1 | Нет профессионально необходимого типа вида |
| `drawing_create_detail_view` | Создать выносной элемент | ⬜ не реализован | Advanced | P1 | Нет увеличенного локального представления |
| `drawing_create_local_view` | Создать местный вид | ⬜ не реализован | Advanced | P2 | Требуется техническое исследование устойчивой адресации контура |
| `drawing_list_annotations` | Перечислить размеры, обозначения и тексты с `annotation_id` и связями | ⬜ не реализован | Core | P0 | Все созданные аннотации практически не адресуются |
| `drawing_update_annotation` | Изменить положение, текст, стиль или привязку аннотации | ⬜ не реализован | Core | P0 | Нет общего edit-контура |
| `drawing_delete_annotation` | Удалить аннотацию по `annotation_id` | ⬜ не реализован | Core | P0 | Нет общего delete-контура |
| `drawing_add_datum` | Создать обозначение базы и вернуть `annotation_id` | ⬜ не реализован | Advanced | P1 | Нет оформления баз |
| `drawing_add_geometric_tolerance` | Создать рамку допуска формы или расположения с базами | ⬜ не реализован | Advanced | P1 | Нет оформления допусков |
| `drawing_add_centerline` | Создать осевую линию с привязкой к геометрии | ⬜ не реализован | Advanced | P1 | Нет осевых линий |
| `drawing_add_center_mark` | Создать обозначение центра окружности или дуги | ⬜ не реализован | Advanced | P1 | Нет центровых обозначений |
### Импорт и экспорт
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `import_step` | Импортировать STEP как новую активную деталь или сборку | ✅ реализован | Core | — | — |
| `export_step` | Экспортировать активную 3D-модель в STEP | ✅ реализован | Core | — | — |
| `import_model` | Импортировать нейтральный или собственный 3D-формат, кроме STEP, по явному `format` | ⬜ не реализован | Advanced | P2 | STEP принимается только специализированным `import_step`; остальные форматы требуют поформатной проверки |
| `export_model` | Экспортировать документ или выбранные тела в 3D-формат, кроме STEP | ⬜ не реализован | Advanced | P2 | STEP выпускается только специализированным `export_step`; для остальных форматов нужна проверяемая матрица возможностей |
| `import_drawing` | Импортировать 2D-обменный формат в новый документ | ⬜ не реализован | Advanced | P2 | Нужна поформатная проверка слоёв, масштаба и единиц |
| `export_drawing` | Экспортировать выбранные листы в единый PDF-документ | ⬜ не реализован | Advanced | P1 | Нет выпуска комплекта чертежей для просмотра и печати |
| `export_drawing_exchange` | Экспортировать геометрию выбранного листа в редактируемый 2D-обменный формат | ⬜ не реализован | Advanced | P2 | Нужна отдельная поформатная проверка слоёв, масштаба, шрифтов и единиц |
| `export_mesh` | Экспортировать выбранные тела в полигональный формат с параметрами точности | ⬜ не реализован | Optional | P2 | Требуется проверка доступных форматов и единиц |
### Управление состоянием, восстановление и надёжность агентской работы
| Метод | Назначение | Статус | Необходимость | Приоритет | Пробел / ограничение |
|---|---|---|---|---|---|
| `transaction_begin` | Начать атомарную группу изменений | ⛔ ограничен платформой | Optional | P2 | Надёжная атомарная граница не подтверждена; требуется техническое исследование |
| `transaction_commit` | Зафиксировать атомарную группу изменений | ⛔ ограничен платформой | Optional | P2 | Реалистичность зависит от подтверждения общей транзакционной модели |
| `transaction_rollback` | Откатить атомарную группу изменений | ⛔ ограничен платформой | Optional | P2 | Надёжный полный откат группы не подтверждён; требуется техническое исследование |
| `state_checkpoint` | Создать детерминированный именованный снимок состояния; дубликат имени отклоняется, а `replace=true` явно заменяет его | ⬜ не реализован | Core | P0 | Нужен подтверждаемый рубеж с однозначной политикой имён перед рискованной операцией |
| `state_restore` | Восстановить документ из снимка состояния и вернуть новую ревизию | ⬜ не реализован | Core | P0 | Нет детерминированного fallback после неверной цепочки мутаций |
| `state_list_checkpoints` | Перечислить снимки состояния документа с именем, временем, исходной ревизией и признаком доступности | ⬜ не реализован | Core | P0 | Нельзя обнаружить допустимые точки восстановления |
| `state_delete_checkpoint` | Удалить именованный снимок состояния без изменения документа | ⬜ не реализован | Core | P0 | Нет завершения lifecycle и очистки устаревших снимков |
| `undo` | Отменить последнее изменение указанного документа | ⬜ не реализован | Core | P0 | Нет пошагового исправления |
| `redo` | Повторить отменённое изменение указанного документа | ⬜ не реализован | Core | P0 | Нет симметрии undo/redo |
| `operation_cancel` | Запросить отмену длительной операции и сообщить фактический результат | ⛔ ограничен платформой | Optional | P2 | Надёжная остановка уже начавшейся операции не подтверждена; требуется техническое исследование |
| `session_recover` | Восстановить внешний контекст после сбоя: документы, активность, ревизии и адреса | ⬜ не реализован | Core | P0 | Сессионные идентификаторы теряются при сбросе или переподключении |
## Приоритизированный перечень пробелов
### P0 — замыкание автономного цикла
- Документы: `list_documents`, `document_activate`, `document_describe`.
- Эскизы: `list_sketches`, `describe_sketch`, `list_sketch_entities`, `update_sketch_entity`, `delete_sketch_entity`, `delete_sketch`, `sketch_set_driving_dimension`.
- История и тела: `describe_feature`, `update_feature`, `delete_feature`, `delete_body`.
- Устойчивый выбор: `get_model_revision`, `list_vertices`, `create_selector`, `resolve_selector`, `validate_selector`.
- Сборки: `assembly_add_mate`, `describe_assembly`, `describe_component`, `assembly_set_component_placement`, `assembly_remove_component`, `list_mates`, `assembly_update_mate`, `assembly_remove_mate`, `validate_assembly`.
- Чертёжный CRUD: `describe_drawing`, `validate_drawing`, `drawing_list_sheets`, `drawing_list_views`, `drawing_describe_view`, `drawing_update_view`, `drawing_delete_view`, `drawing_rebuild_views`, `drawing_list_annotations`, `drawing_update_annotation`, `drawing_delete_annotation`.
- Восстановление: `state_checkpoint`, `state_restore`, `state_list_checkpoints`, `state_delete_checkpoint`, `undo`, `redo`, `session_recover`.
### P1 — типовые профессиональные сценарии
- Документы и эскизы: `document_set_properties`, `list_sketch_constraints`, `sketch_set_constraint`, `sketch_delete_constraint`.
- История и многотельность: `set_feature_suppressed`, `move_body`, `copy_body`, `boolean_subtract`, `boolean_intersect`, `body_check_intersection`.
- Вспомогательная 3D-геометрия: `datum_point_create`, `datum_axis_create`, `datum_plane_create`.
- Сборки и спецификации: `assembly_set_component_suppressed`, `assembly_check_interference`, `specification_generate`, `specification_list_items`.
- Профессиональное оформление: `drawing_add_sheet`, `drawing_delete_sheet`, `drawing_create_section_view`, `drawing_create_detail_view`, `drawing_add_datum`, `drawing_add_geometric_tolerance`, `drawing_add_centerline`, `drawing_add_center_mark`.
- Выпуск: `export_drawing`.
### P2 — расширение охвата
- Дополнительный вид: `drawing_create_local_view`.
- Дополнительные форматы: `import_model`, `export_model`, `import_drawing`, `export_drawing_exchange`, `export_mesh`.
- Исследовательские операции состояния: `transaction_begin`, `transaction_commit`, `transaction_rollback`, `operation_cancel`.
## Итоговая оценка
Текущий контракт силён в создании 3D-геометрии, структурном чтении детали, STEP-обмене и первичном создании сборок и чертежей. Он ещё не является полным автономным CAD-контуром: активный документ остаётся глобальным неявным состоянием; созданные сущности эскиза, отверстия, компоненты, сопряжения, виды и аннотации недостаточно адресуемы; индексы топологии не связаны с ревизией; отсутствуют update/delete для большинства семейств и безопасное восстановление. Все пять сквозных сценариев поэтому остаются частичными или неполными.
+14 -7
View File
File diff suppressed because one or more lines are too long
+36 -1
View File
@@ -199,4 +199,39 @@ stateful-сессии? Пока полагаемся на последовате
**Решено.** Новый инструмент `drawing_add_linear_dimension(x1,y1,x2,y2,x3,y3, orientation, viewNumber)` — ставит линейный размер на виде активного чертежа. Паттерн API7: получить вид по `viewNumber` → COM-QI `(ISymbols2DContainer)view` (размер адресуется конкретному виду, а не листу) → `LineDimensions.Add()``ILineDimension``X1,Y1,X2,Y2` (выносные точки в **локальной СК вида**, мм), `X3,Y3` (положение размерной линии), `Orientation` (`ksLineDimensionOrientationEnum`: `ksLinDParallel=0`/`ksLinDHorizontal=1`/`ksLinDVertical=2`), `AutoNominalValue=true``Update()``Valid` → значение `((IDimensionText)dim).NominalValue`. Откат `dim.Delete()` при ошибке или нулевом значении. **Ключевая находка: размеры НЕ входят в `IView.ObjectCount`** (тот считает геометрию `IDrawingContainer`); размеры живут в `ISymbols2DContainer` отдельно — попадание размера в вид проверяется через `ISymbols2DContainer.LineDimensions.Count`. `DrawingViewsResult` расширен полем `ViewNumbers` (номера созданных видов — для адресации при простановке размеров). Свой enum `DimensionOrientation {Horizontal, Vertical, Parallel}` + `DimensionOrientations.Parse/ToKompas`. Валидаторы `DrawingValidation.RequireFiniteCoords` + `RequireDistinctPoints`. Новые файлы: `DimensionOrientation.cs`, `DrawingDimensionResult.cs`; инструмент в `DrawingTools.cs`. Итог: **75 инструментов, 203 теста (131 unit + 72 integration).** Следующий шаг: диаметральные/радиальные/угловые размеры, рамка/формат.
### ✅ Диаметральный размер на виде чертежа — реализован (инкремент 4 вехи «2D-чертёж»)
**Решено.** Новый инструмент `drawing_add_diametral_dimension(xc,yc,radius, angle, viewNumber)` — ставит диаметральный размер (Ø) на виде активного чертежа. Паттерн API7: общий хелпер `RequireSymbols2DContainer(viewNumber)` (переиспользуется с линейным путём) → `DiametralDimensions.Add()``IDiametralDimension` (`Xc`, `Yc`, `Radius` в локальной СК вида, `Angle` в **радианах** через `DimensionAngles.ToRadians`, `AutoNominalValue=true`) → `Update()``Valid``((IDimensionText)dim).NominalValue` = диаметр (2·Radius). Размер «свободный» (без `BaseObject` — ассоциативная привязка отложена). Валидатор `DrawingValidation.RequirePositiveRadius`. `DimensionAngles.ToRadians` (с проверкой переполнения) добавлен в `DimensionOrientation.cs`; `RequirePositiveRadius` — в `DrawingValidation.cs`; инструмент в `DrawingTools.cs`. Итог: **76 инструментов, 218 тестов (141 unit + 77 integration).** Следующий шаг: радиальные/угловые размеры, рамка/формат.
**Решено.** Новый инструмент `drawing_add_diametral_dimension(xc,yc,radius, angle, viewNumber)` — ставит диаметральный размер (Ø) на виде активного чертежа. Паттерн API7: общий хелпер `RequireSymbols2DContainer(viewNumber)` (переиспользуется с линейным путём) → `DiametralDimensions.Add()``IDiametralDimension` (`Xc`, `Yc`, `Radius` в локальной СК вида, `Angle` в **радианах** через `DimensionAngles.ToRadians`, `AutoNominalValue=true`) → `Update()``Valid``((IDimensionText)dim).NominalValue` = диаметр (2·Radius). Размер «свободный» (без `BaseObject` — ассоциативная привязка отложена). Валидатор `DrawingValidation.RequirePositiveRadius`. `DimensionAngles.ToRadians` (с проверкой переполнения) добавлен в `DimensionOrientation.cs`; `RequirePositiveRadius` — в `DrawingValidation.cs`; инструмент в `DrawingTools.cs`. Итог: **76 инструментов, 218 тестов (141 unit + 77 integration).**
### ✅ Радиальный и угловой размеры на виде чертежа — реализованы (инкремент 5 вехи «2D-чертёж»)
**Решено.** Два новых инструмента завершают базовое семейство размеров:
`drawing_add_radial_dimension(xc,yc,radius, angle=0, viewNumber)` — радиальный размер (R). Паттерн API7: `RequireSymbols2DContainer(viewNumber)``RadialDimensions.Add()``IRadialDimension` (`Xc`, `Yc`, `Radius`, `Angle` в радианах, `DimensionType=true`, `AutoNominalValue=true`) → `Update()``Valid``NominalValue` = радиус. Переиспользует `RequirePositiveRadius` и `DimensionAngles.ToRadians`.
`drawing_add_angular_dimension(xc,yc, x1,y1, x2,y2, x3,y3, angleType="min", viewNumber)` — угловой размер. `angleType` = `"min"` (острый) | `"max"` (тупой) | `"more"` (рефлексный >180°). Паттерн API7: `RequireSymbols2DContainer(viewNumber)``AngleDimensions.Add(ksDrADimension)``IAngleDimension` (`Xc`,`Yc`,`X1`,`Y1`,`X2`,`Y2`,`X3`,`Y3`, `DimensionType=ksAngleDimTypeEnum`, `AutoNominalValue=true`) → `Update()``Valid``NominalValue` в градусах. Новый enum `AngleDimensionType {Min,Max,More}` + `AngleDimensionTypes.Parse/ToKompas` в `DimensionOrientation.cs` (+9 unit-тестов для маппинга).
Итог: **78 инструментов, 241 тест (150 unit + 91 integration).** Базовое семейство размеров завершено — виды, штамп, размеры (линейный, диаметральный, радиальный, угловой). Нереализовано: текстовые обозначения (шероховатость, допуски формы, выноски, тех. требования), ассоциативная привязка, рамка/формат.
### ✅ Текстовые обозначения на чертеже — реализованы (инкремент 6 вехи «2D-чертёж»)
**Решено.** Три новых инструмента завершают класс «2D-чертёж» на уровне текстовых аннотаций:
`drawing_add_rough(x,y, value?, signType, angle=0, viewNumber)` — знак шероховатости на виде. Паттерн API7: `ISymbols2DContainer.Roughs.Add()``IRough` (`BranchX0/Y0/Angle`) → `(IRoughParams)rough` (`SignType=ksRoughSignEnum`, `RoughParamText.Str = value`) → `Update()``Valid`. Новый enum `RoughSignType {NoProcessing, DeleteMaterial, WithoutDeleteMaterial}` + `RoughSignTypes.Parse/ToKompas` в `Drawings/RoughSignType.cs`. Возвращает `DrawingAnnotationResult { Value, ViewNumber }`.
`drawing_add_text(x,y, text, angle=0, viewNumber)` — свободная текстовая надпись на виде. Паттерн API7: `(IDrawingContainer)view.DrawingTexts.Add()``IDrawingText` (`X/Y/Angle`) → `(IText)dt.Str = text``Update()``Valid`. ВАЖНО: текст живёт в `IDrawingContainer`, а не `ISymbols2DContainer`; проверяется через `DrawingTexts.Count`. Возвращает `DrawingAnnotationResult { Value, ViewNumber }`.
`drawing_set_technical_requirements(text)` — технические требования документа (уровень документа, не вида). Паттерн API7: `(IDrawingDocument)doc.TechnicalDemand``td.Text.Str = text``td.Update()`. Перезаписывает прежние. Возвращает число строк.
Итог: **81 инструмент, 267 тестов (163 unit + 104 integration).** Нереализовано: ассоциативная привязка размеров и шероховатости к геометрии (`BaseObject`), рамка/формат листа, выноски (`Leaders`), обозначения баз (`Bases`), допуски формы (`Tolerances`).
### ✅ Ассоциативная привязка диаметрального/радиального к окружности — реализована (инкремент 7 вехи «2D-чертёж»)
**Решено (частично).** Параметр `associate=true` добавлен к `drawing_add_diametral_dimension` и `drawing_add_radial_dimension`. Реализация (API7, `DrawingService`): новый класс `CircularObjectMatch` — отбирает окружность-кандидата в `IDrawingContainer.Circles` по центру (xc,yc) + радиусу с допуском 1 мм; бросает при отсутствии или неоднозначности (концентрические окружности различаются по радиусу). Хелпер `RequireViewContainers` получает одновременно `ISymbols2DContainer` и `IDrawingContainer` для вида. `_Circle` реализует `IDrawingObject`, поэтому `dim.BaseObject = circle` без дополнительного приведения. `associate=false` — прежнее «свободное» поведение без изменений. Подтверждено спайком: `BaseObject=circle` без задания Xc/Yc/Radius → `Valid`, `NominalValue` из геометрии (цилиндр R10: Ø=20 / R=10). +5 unit-тестов (`CircularObjectMatchTests`) + 6 интеграционных (`DrawingAssociativeDimensionTests`). Итог: **81 инструмент, 278 тестов (168 unit + 110 integration).**
Нереализовано (следующие инкременты): привязка к дуге (`IDrawingContainer.Arcs`), ассоциативный угловой размер (`BaseObject1/2`, отрезки), ассоциативная шероховатость (`IRough.BaseObject`); рамка/основная надпись по ГОСТ-стилю (`LayoutLibraryFileName`/`LayoutStyleNumber`), несколько листов; выноски (`Leaders`), обозначения баз (`Bases`), допуски формы (`Tolerances`).
### ✅ Формат и ориентация листа чертежа — реализованы (инкремент 8 вехи «2D-чертёж»)
**Решено.** Новый инструмент `drawing_set_sheet_format(format, landscape, width?, height?, sheetNumber=1)` — задаёт формат и ориентацию активного листа чертежа. Паттерн API7: `doc2d.LayoutSheets.ItemByNumber[sheetNumber]``ILayoutSheet.Format` (`ISheetFormat`: `Format=ksDocumentFormatEnum`, `VerticalOrientation=!landscape`, `FormatWidth/Height` только для `format=user`) → `sheet.Update()`. Дефолтный лист нового чертежа — A4 книжный. Новые типы: enum `PaperFormat {A0,A1,A2,A3,A4,A5,User}` + `PaperFormats.Parse/ToKompas/FromKompas`; record `SheetFormatResult`; валидатор `DrawingValidation.ValidateFormatDimensions`; хелпер `RequireLayoutSheet`. Ключевая тонкость: для `format=user` КОМПАС выводит `VerticalOrientation` из соотношения сторон (`height>width`), флаг `landscape` в этом случае игнорируется — результат читается read-back из COM. Для стандартных форматов `width`/`height` запрещены (должны быть 0). +20 unit-тестов (`PaperFormats.Parse`, `ValidateFormatDimensions`) + 10 интеграционных (`DrawingSheetFormatTests`). Итог: **82 инструмента, 308 тестов (188 unit + 120 integration).**
Нереализовано: рамка по ГОСТ-стилю (`LayoutLibraryFileName`/`LayoutStyleNumber`), несколько листов.
### ✅ Линия-выноска с текстом — реализована (инкремент 9 вехи «2D-чертёж»)
**Решено.** Новый инструмент `drawing_add_leader(x,y, textX,textY, text, shelfDirection, viewNumber)` — ставит линию-выноску с надписью на полке на виде активного чертежа. Паттерн API7: `ISymbols2DContainer.Leaders.Add(ksDrLeader)``IBaseLeader``(IBranchs)bl.AddBranchByPoint(0,x,y)` (остриё, ответвление ответвлений — обязательно до `Update`, иначе `RPC_E_SERVERFAULT`) → `SetBranchTextPosition(textX,textY)``(ILeader)bl.TextOnShelf.Str = text` → опционально `ShelfDirection``Update()``Valid`. Новый enum `ShelfDirection {Auto,Right,Left,Up,Down}` + `ShelfDirections.Parse/ToKompas` в `src/Kompas.Mcp.Core/Drawings/ShelfDirection.cs`; переиспользует `DrawingAnnotationResult { Value, ViewNumber }`. +13 unit-тестов (`ShelfDirections.Parse`) + 10 интеграционных (`DrawingLeaderTests`, включая параметризацию направлений полки). Итог: **83 инструмента, 331 тест (201 unit + 130 integration).**
Нереализовано: обозначения баз (`Bases`), допуски формы (`Tolerances`); привязка к дуге (`Arcs`), ассоциативный угловой размер, ассоциативная шероховатость (`IRough.BaseObject`); рамка по ГОСТ-стилю; специальные выноски (позиция, клеймо, маркер).
+76
View File
@@ -0,0 +1,76 @@
# TODO проекта kompas3d-mcp
Канонический бэклог **оставшихся** этапов. Что уже сделано — см. `CLAUDE.md` → «Current state» и
`docs/ARCHITECTURE.md` §10 (дорожная карта). Метод работы над этапом — `docs/superpowers/NEXT-SESSION.md`
(спайк → спек → ревью Codex → TDD → ревью реализации → доки → merge/push).
Статус: ☐ не начато · ▣ частично · ⛔ заблокировано (недостижимо текущим COM API) · ✓ сделано
---
## Веха 2D-ЧЕРТЁЖ — текущий фокус (самый ценный растущий класс)
Готово: стандартные виды, основная надпись (штамп), размеры (линейный/диаметральный/радиальный/угловой),
текстовые обозначения (шероховатость, свободный текст, технические требования).
- ▣ **Ассоциативная привязка размеров и шероховатости к геометрии вида** (`BaseObject`/`BaseObject1/2`).
Обозначение «прилипает» к геометрии вида и обновляется с моделью, а не задаётся координатами.
- ✓ Диаметральный/радиальный → **окружность** (`Circles`): флаг `associate` (инкремент 7); поиск по
центру+радиусу (`CircularObjectMatch`), значение Ø/R из геометрии.
- ☐ Привязка к **дуге** (`Arcs`) — нужен свой спайк (`IArc` как `BaseObject`; дуги одного родителя
неотличимы по центру+радиусу — нужен ключ по углу/точке).
- ☐ **Угловой** → пара отрезков (`BaseObject1/2`, `LineSegments`).
- ☐ **Шероховатость**`IRough.BaseObject` (привязка к контуру/ребру).
- ⛔/☐ **Линейный**у `ILineDimension` нет `BaseObject`; нужен иной механизм (отложено).
- ✓ **Формат/ориентация листа** (A0A5/user, книжная/альбомная) — `drawing_set_sheet_format`
(инкремент 8); `ILayoutSheet.Format`/`ISheetFormat`. Остаётся: ☐ рамка/основная надпись по
конкретному ГОСТ-стилю оформления (`LayoutLibraryFileName`/`LayoutStyleNumber`), несколько листов.
- ✓ **Выноски** (`Leaders`) — `drawing_add_leader` (инкремент 9): остриё + полка с текстом,
`shelfDirection`. Остаётся: ☐ многострочный/под-полкой текст (`TextUnderShelf`), несколько
ответвлений, привязка к геометрии (`IBranchs.SetBaseObject`), спец. выноски (позиция/клеймо/маркер).
- ☐ **Обозначения баз** (`Bases`) — `ISymbols2DContainer.Bases`.
- ☐ **Допуски формы и расположения** (`Tolerances`) — `ISymbols2DContainer.Tolerances` (обычно в связке с базой).
- ☐ (опц.) **Доп. виды**: разрез/сечение (`CutLines`), выносной элемент (`RemoteElements`), местный вид.
- ☐ (опц.) **Осевые линии / обозначения центра** (`AxisLines`, `CentreMarkers`).
## Класс СБОРКИ
Готово: вставка компонента (`assembly_add_component`), сопряжения `coincidence`/`distance`.
- ☐ **Доп. типы сопряжений**: `parallel`, `perpendicular`, `concentric`, `angle`, `tangency`
(`MateConstraintType`; механика `assembly_add_mate` готова — расширить enum `MateType` + валидаторы).
- ☐ **Авто-позиционирование** компонентов (решатель сопряжений по нескольким связям).
- ☐ **Спецификация** (`ISpecification`) — таблица состава изделия.
## Класс ДЕТАЛЬ / параметрика / геометрия
Готово: насыщен (эскизы, формообразующие, массивы/зеркало, отверстия, уклон, переменные, STEP,
прямое редактирование B-rep, структурный осмотр) — см. `ARCHITECTURE.md` §10.
- ⛔ **Параметрические эскизы** (размер эскиза управляется переменной) — недостижимо через COM API
(`ksCDimWithVariable` не конструируется из внешней автоматизации). См.
`docs/superpowers/specs/2026-05-27-parametric-sketch-findings.md`. **Не повторять** без новой информации.
- ☐ **Пакет E (вспомогательная геометрия, продолжение)**: ось, точка, плоскость по 3 точкам / по углу
(`sketch_create_on_offset_plane` уже есть).
- ▣ **Рассечение/перемещение тела как инструменты** (`SplitSolids`/`BodyRepositions`) — механика есть
(`split_solid_by_plane`, `move_body` частично), полный workflow split-reposition не продуктизирован.
- ☐ **Свойства документа** (`IPropertyMng`/`IPropertyKeeper`) — чтение/запись пользовательских свойств.
## Сквозное / инфраструктура / технический долг
- ☐ **Утечки транзитных RCW** (долг v2-2): `EntityCollection`/`GetTopPart`/QI-контейнеры/`IRoughParams`
и пр. не освобождаются → накопление за длинную сессию. Нужна единая стратегия (scope/`ReleaseComObject`),
не точечно — поэтому отложено как самостоятельный этап.
- ☐ **Насос сообщений / отмена**: `CancellationToken` не прерывает идущий COM-вызов; модальный диалог
КОМПАС блокирует всю очередь STA-потока.
- ☐ **Транспорт HTTP/SSE** — опция для удалённых клиентов (сейчас только stdio).
## Навык / методика
- ☐ Промотировать проверенные приёмы 2D-чертежа из `usecases/` в навык `.claude/skills/kompas-3d/`
(playbook «оформление чертежа»: виды → размеры → обозначения → штамп → тех. требования).
---
*Обновлять по завершении каждого инкремента (отмечать ✓, добавлять вскрытые подэтапы). Источник
приоритетов для следующей сессии — верх раздела «2D-ЧЕРТЁЖ».*
+13 -8
View File
@@ -128,7 +128,7 @@
<!-- HERO -->
<header class="hero">
<div class="wrap">
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole+hole_counterbore+hole_countersink+hole_conic+draft+переменные+assembly_add_component+assembly_add_mate+drawing_create_standard_views+drawing_fill_title_block+drawing_add_linear_dimension+drawing_add_diametral_dimension (76 инструментов)</span>
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole+hole_counterbore+hole_countersink+hole_conic+draft+переменные+assembly_add_component+assembly_add_mate+drawing_create_standard_views+drawing_fill_title_block+drawing_add_linear_dimension+drawing_add_diametral_dimension+drawing_add_radial_dimension+drawing_add_angular_dimension+drawing_add_rough+drawing_add_text+drawing_set_technical_requirements+associate+drawing_set_sheet_format+drawing_add_leader (83 инструмента)</span>
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
@@ -261,7 +261,7 @@
впереди.</p>
<div class="prog">
<div class="progrow"><span>Общий прогресс</span><span>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 · assembly_add_mate · drawing_create_standard_views · drawing_fill_title_block · drawing_add_linear_dimension · drawing_add_diametral_dimension · навык kompas-3d · 76 инструментов · 218 тестов</span></div>
<div class="progrow"><span>Общий прогресс</span><span>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 · assembly_add_mate · drawing_create_standard_views · drawing_fill_title_block · drawing_add_linear_dimension · drawing_add_diametral_dimension · drawing_add_radial_dimension · drawing_add_angular_dimension · drawing_add_rough · drawing_add_text · drawing_set_technical_requirements · associate (ассоциативная привязка диам./радиального) · drawing_set_sheet_format (формат листа) · drawing_add_leader (выноска) · навык kompas-3d · 83 инструмента · 331 тест</span></div>
<div class="bar"><i style="width:97%"></i></div>
</div>
@@ -297,7 +297,7 @@
<!-- TOOLS -->
<div class="panel">
<h3>Инструменты v3+ <span class="tag done">ГОТОВО</span></h3>
<p class="meta">76 инструментов, отдаются по MCP-протоколу</p>
<p class="meta">83 инструмента, отдаются по MCP-протоколу</p>
<ul class="list">
<li class="muted"><span class="mark ok"></span><span class="t"><code>System</code>: connect · status · set_visible</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Documents</code>: create · open · save · save_as · close · active</span></li>
@@ -310,8 +310,8 @@
<li class="muted"><span class="mark ok"></span><span class="t"><code>Variables</code>: <b>create_variable</b> · <b>set_variable</b> · <b>delete_variable</b> (пакет D)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Conversion</code>: import_step · export_step</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Assembly</code>: <b>assembly_add_component</b> (вставка компонента .m3d/.a3d в сборку, API7) · <b>assembly_add_mate</b> (сопряжения coincidence/distance, API7)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Drawing</code>: <b>drawing_create_standard_views</b> (стандартные ассоциативные виды чертежа, API7) · <b>drawing_fill_title_block</b> (основная надпись/штамп: обозначение, наименование, материал, API7) · <b>drawing_add_linear_dimension</b> (линейный размер на виде, ISymbols2DContainer, API7) · <b>drawing_add_diametral_dimension</b> (диаметральный размер Ø, IDiametralDimension, API7)</span></li>
<li><span class="mark wip"></span><span>2D-чертёж: радиальные/угловые размеры, рамка/формат · пакет E (ось, угловая плоскость) · пакет D параметрика: параметрические эскизы (API2D) — <b>исследовано, недоступно через COM API</b> · прочие типы сопряжений (parallel/perpendicular/concentric/angle/tangency)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Drawing</code>: <b>drawing_create_standard_views</b> (стандартные ассоциативные виды чертежа, API7) · <b>drawing_fill_title_block</b> (основная надпись/штамп: обозначение, наименование, материал, API7) · <b>drawing_add_linear_dimension</b> (линейный размер на виде, ISymbols2DContainer, API7) · <b>drawing_add_diametral_dimension</b> (диаметральный размер Ø, IDiametralDimension, <b>associate=true</b> для ассоциативной привязки к окружности, API7) · <b>drawing_add_radial_dimension</b> (радиальный размер R, IRadialDimension, <b>associate=true</b> аналогично, API7) · <b>drawing_add_angular_dimension</b> (угловой размер, IAngleDimension, angleType=min/max/more, API7) · <b>drawing_add_rough</b> (знак шероховатости, IRough/IRoughParams, signType=delete/without/none, API7) · <b>drawing_add_text</b> (свободный текст на виде, IDrawingContainer/IDrawingText, API7) · <b>drawing_set_technical_requirements</b> (тех. требования документа, IDrawingDocument/TechnicalDemand, API7) · <b>drawing_set_sheet_format</b> (формат и ориентация листа, ILayoutSheet/ISheetFormat, PaperFormat A0A5/user, API7) · <b>drawing_add_leader</b> (линия-выноска с текстом, ISymbols2DContainer/IBaseLeader/IBranchs/ILeader, ShelfDirection enum, API7)</span></li>
<li><span class="mark wip"></span><span>2D-чертёж: рамка/основная надпись по ГОСТ-стилю (LayoutLibraryFileName/LayoutStyleNumber), несколько листов · привязка к дуге (Arcs), ассоциативный угловой размер, ассоциативная шероховатость (IRough.BaseObject) · обозначения баз (Bases), допуски формы (Tolerances), специальные выноски (позиция/клеймо/маркер) · пакет E (ось, угловая плоскость) · пакет D параметрика: параметрические эскизы (API2D) — <b>исследовано, недоступно через COM API</b> · прочие типы сопряжений (parallel/perpendicular/concentric/angle/tangency)</span></li>
</ul>
</div>
@@ -322,7 +322,7 @@
<ul class="list">
<li class="muted"><span class="mark ok"></span><span class="t">Unit-тесты — без COM: диспетчер, enum-маппинг, StepFormat, InspectionText</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Интеграционные тесты с КОМПАС: подключение, документы, снимок, цикл, STEP round-trip, сборка, move_face, инспекция</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Итого 218 тестов зелёных (141 unit + 77 integration)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Итого 331 тест зелёных (201 unit + 130 integration)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Сборка <code>kompas-mcp.exe</code>, README с конфигом клиента</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Навык <code>kompas-3d</code> (методика) + полигон <code>usecases/</code></span></li>
<li><span class="mark todo"></span><span>In-process тест MCP-клиента, релизный publish</span></li>
@@ -353,8 +353,13 @@
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">сборки инкремент 2 · готово ✓</div><h4>Сопряжения</h4><p><b>assembly_add_mate</b> (coincidence/distance, API7 IMateConstraints3D) · IntegrationTestBase · 72 инструмента · 156 тестов</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 2 ✓</div><h4>Стандартные виды + основная надпись</h4><p><b>drawing_create_standard_views</b> (AddStandartViews, API7) · <b>drawing_fill_title_block</b> (IStamp.Text[id].Str, API7) · 74 инструмента · 188 тестов</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 3 ✓</div><h4>Линейные размеры</h4><p><b>drawing_add_linear_dimension</b> (ISymbols2DContainer/LineDimensions.Add/ILineDimension, локальная СК вида, API7) · ViewNumbers в результате видов · 75 инструментов · 203 теста</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 4 ✓</div><h4>Диаметральный размер</h4><p><b>drawing_add_diametral_dimension</b> (ISymbols2DContainer.DiametralDimensions.Add/IDiametralDimension, Xc/Yc/Radius/Angle рад, API7) · RequireSymbols2DContainer общий хелпер · DimensionAngles.ToRadians · 76 инструментов · 218 тестов · далее: радиальные/угловые размеры, рамка/формат</p></div>
<div class="tl cur"><div class="ph">далее · в работе ▶</div><h4>Оформление чертежа</h4><p>Радиальные/угловые размеры · рамка/формат · пакет E (ось, угловая плоскость) · прочие типы сопряжений (parallel/perpendicular/concentric/angle/tangency) · пакет D параметрика — параметрические эскизы недоступны через COM API</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 4 ✓</div><h4>Диаметральный размер</h4><p><b>drawing_add_diametral_dimension</b> (ISymbols2DContainer.DiametralDimensions.Add/IDiametralDimension, Xc/Yc/Radius/Angle рад, API7) · RequireSymbols2DContainer общий хелпер · DimensionAngles.ToRadians · 76 инструментов · 218 тестов</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 5 ✓</div><h4>Радиальный и угловой размеры</h4><p><b>drawing_add_radial_dimension</b> (IRadialDimension, DimensionType=true, API7) · <b>drawing_add_angular_dimension</b> (IAngleDimension, angleType=min/max/more, AngleDimensionType enum, API7) · базовое семейство размеров завершено · 78 инструментов · 241 тест</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 6 ✓</div><h4>Текстовые обозначения</h4><p><b>drawing_add_rough</b> (IRough/IRoughParams, signType=delete/without/none, API7) · <b>drawing_add_text</b> (IDrawingContainer/IDrawingText/IText, API7) · <b>drawing_set_technical_requirements</b> (IDrawingDocument/TechnicalDemand, уровень документа, API7) · DrawingAnnotationResult · RoughSignType enum · 81 инструмент · 267 тестов</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 7 ✓</div><h4>Ассоциативная привязка к окружности</h4><p><b>associate=true</b> у drawing_add_diametral_dimension и drawing_add_radial_dimension · CircularObjectMatch (поиск по центру+радиусу, допуск 1 мм) · RequireViewContainers · dim.BaseObject=circle · значение из геометрии · 81 инструмент · 278 тестов</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 8 ✓</div><h4>Формат и ориентация листа</h4><p><b>drawing_set_sheet_format</b> (ILayoutSheet.Format / ISheetFormat, ksDocumentFormatEnum, A0A5/user, VerticalOrientation) · PaperFormat enum · SheetFormatResult · ValidateFormatDimensions · 82 инструмента · 308 тестов</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 9 ✓</div><h4>Линия-выноска</h4><p><b>drawing_add_leader</b> (ISymbols2DContainer.Leaders.Add / IBaseLeader / IBranchs.AddBranchByPoint / ILeader.TextOnShelf, ShelfDirection enum auto/right/left/up/down, API7) · DrawingAnnotationResult · 83 инструмента · 331 тест</p></div>
<div class="tl cur"><div class="ph">далее · в работе ▶</div><h4>Оформление чертежа</h4><p>Рамка/надпись по ГОСТ-стилю (LayoutLibraryFileName) · несколько листов · привязка к дуге (Arcs) · ассоциативный угловой размер · ассоциативная шероховатость (IRough.BaseObject) · обозначения баз (Bases) · допуски формы (Tolerances) · специальные выноски (позиция/клеймо/маркер) · пакет E (ось, угловая плоскость) · прочие типы сопряжений (parallel/perpendicular/concentric/angle/tangency) · пакет D параметрика — параметрические эскизы недоступны через COM API</p></div>
</div>
</div>
</section>
+20 -7
View File
@@ -8,6 +8,9 @@
пушь в `main`, каждую фичу (спек и реализацию) прогоняй через ревью Codex. Для интеграционных
тестов **КОМПАС-3D должен быть запущен**.
**Оставшиеся этапы — канонический бэклог: [`docs/TODO.md`](TODO.md)** (по классам + сквозное; верх раздела
«2D-ЧЕРТЁЖ» = следующий приоритет). Обновляй его по завершении каждого инкремента.
**Сначала сориентируйся** — этот промт задаёт направление, а не заменяет источники:
- `CLAUDE.md` — текущее состояние, проверенные факты реализации и конвенции (STA-поток, COM-QI
контейнеры, отложенный долг по RCW, команды сборки/тестов, инфраструктура интеграционных тестов).
@@ -16,17 +19,27 @@
`kompas-step-and-assembly-api`, `kompas-hole-api7`, `kompas-variables-api5`,
`kompas-parametric-sketch-findings`. Спеки фич — `docs/superpowers/specs/`.
**Состояние (main = origin):** 76 MCP-инструментов, 218 тестов зелёных, сборка Release чистая. Три класса:
**Ревью-гейт (важно):** спек И реализацию каждой фичи прогонять через **три ревьюера параллельно**
Codex (`codex:codex-rescue`) + pi/GLM-5.1 + pi/Kimi-K2.6 (субагент `pi-delegate`, `--model
ollama-cloud/glm-5.1` и `--model ollama-cloud/kimi-k2.6`, `--read-only --thinking high`). См. память
`review-gate-multi-reviewer`. Находки агрегировать и оценивать по существу (receiving-code-review).
**Состояние (main = origin):** 83 MCP-инструмента, 331 тест зелёный, сборка Release чистая. Три класса:
- **Деталь** — насыщена (эскизы, формообразующие, массивы/зеркало, отверстия, уклон, переменные,
STEP, прямое редактирование B-rep, структурный осмотр).
STEP, прямое редактирование B-rep, структурный осмотр). Есть навык `kompas-fdm-design` (DFM под FDM-печать).
- **Сборки** — базовое готово (вставка компонента, сопряжения coincidence/distance).
- **2D-чертёж** развивается: стандартные виды, основная надпись, размеры линейный и диаметральный.
- **2D-чертёж** — виды, основная надпись, размеры (инкр. 5), текстовые обозначения (инкр. 6),
ассоциативная привязка диам./радиального к окружности (инкр. 7), формат/ориентация листа (инкр. 8),
**выноска с текстом** (`drawing_add_leader`; инкр. 9).
**Фокус — веха 2D-ЧЕРТЁЖ** (самый ценный растущий класс; деталь и сборки близки к насыщению).
Осмотри, что уже реализовано в `DrawingService`/`DrawingTools`, найди пробелы и **предложи самый
ценный следующий инкремент**. Возможные направления (не исчерпывающе, не предрешено): радиальный и
угловой размеры, рамка/формат листа, текстовые обозначения (шероховатость, допуски, тех. требования).
Точки входа в API разведывай сам — рефлексия по `libs/kompas-interop/*.dll` + субагент `kompas-sdk-research`.
Осмотри `DrawingService`/`DrawingTools` и **бэклог `docs/TODO.md`** (верх раздела «2D-ЧЕРТЁЖ»), предложи
самый ценный следующий инкремент. Возможные направления (не предрешено): **базы** (`Bases`) + **допуски
формы** (`Tolerances`) — GD&T-пара; **расширить привязку** — к дуге (`Arcs`; свой спайк: дуги одного
родителя неотличимы по центру+радиусу), угловой (`BaseObject1/2`), шероховатость (`IRough.BaseObject`);
рамка/основная надпись по ГОСТ-стилю (`LayoutLibraryFileName`/`LayoutStyleNumber`), несколько листов;
спец. выноски (позиция/клеймо/маркер). Точки входа в API разведывай сам — рефлексия по
`libs/kompas-interop/*.dll` + субагент `kompas-sdk-research`.
**Метод (кратко):** при неопределённости (привязка к геометрии, координаты, единицы) — спайк до
спека; затем краткий спек → ревью Codex → TDD (валидаторы в чистый static для unit, интеграционный
@@ -0,0 +1,609 @@
# kompas-fdm-design Skill — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Also use `superpowers:writing-skills`** when authoring/editing SKILL.md frontmatter & body.
**Goal:** Создать отдельный навык `kompas-fdm-design` — методику проектирования деталей под FDM-печать в КОМПАС-3D через MCP (правила DFM + лёгкая самопроверка геометрии), без слайсера, с числами, масштабируемыми по `w`/`h`/`θ_max`.
**Architecture:** Навык — набор Markdown-файлов в `.claude/skills/kompas-fdm-design/`: лёгкий `SKILL.md` (диспетчер) + `references/fdm-rules.md` (полный свод DFM) + `references/geometry-audit.md` (самопроверка + границы). Progressive disclosure. **Полное финальное содержимое всех трёх файлов дано дословно в задачах ниже** (по итогам 3 ревью плана: транскрипция из spec признана риском тихой потери контента — поэтому план self-contained, исполнитель пишет готовый текст, не «переносит» из spec).
**Tech Stack:** Markdown + YAML frontmatter. Кода/сборки/тестов .NET нет — навык не трогает MCP-сервер (`src/`). Проверка — структурная (`Select-String -SimpleMatch`) + по инвариантам. Опционально: live-обкатка через MCP (требует запущенного КОМПАС).
**Source of truth (контекст, НЕ копировать вручную):** spec [`docs/superpowers/specs/2026-05-27-kompas-fdm-design-skill-design.md`](../specs/2026-05-27-kompas-fdm-design-skill-design.md) (учтены 3 ревью). Прецеденты: `.claude/skills/kompas-3d/SKILL.md`, `~/.claude/skills/orcaslicer/` (SKILL.md + references/).
**Критичные инварианты (проверяются в Task 4):**
1. `θ_max` — параметр (от вертикали; дефолт 45° PLA / 40° PETG-ABS; `≈arctan(w/2h)`); нависания/teardrop/фаски/зенковки из него.
2. Teardrop: вершина на **`r / sin θ_max`** над центром, угол **`2·θ_max`** (при 45° → `√2·r`, `0.414r` над верхом окружности).
3. 90°-полка НЕ печатается (≠ «≤6 мм»); мост ≠ полка.
4. Натяг = **отрицательный** зазор (вычесть из номинала); зазоры «на сторону», диаметральный = 2×.
5. Допуск ±0.2 мм — точность изготовления, **НЕ прибавлять** к зазору посадки.
6. Компенсация Ø вертикальных отверстий: +0.2 (<4), +0.20.3 (410), +0.10.2 (>10); к диаметру (радиус +X/2).
7. Z-прочность по материалам (PLA 4055%, PETG 3550%, ABS 2035%); Z-сжатие можно, Z-растяжение/срез — нет.
8. Бор инсёрта `Ø_bore = OD_инсёрта (0…0.1) мм` (≤ OD).
9. Гео-аудит эвристический, **НЕ доказывает печатнопригодность**; путь нагрузки — из задачи, не из габарита.
10. Экспорт — `export_step`; `export_stl` НЕ вводить.
11. **Имя продукта-слайсера** (OrcaSlicer/Cura/PrusaSlicer/Bambu/…) не упоминается. Понятия «слайсинг/слайсер/g-code» допустимы для описания границ.
---
## Task 1: Скелет навыка + `SKILL.md`
**Files:** Create `.claude/skills/kompas-fdm-design/SKILL.md`
- [ ] **Step 1: Создать каталог**
```powershell
New-Item -ItemType Directory -Force ".claude/skills/kompas-fdm-design/references" | Out-Null
```
- [ ] **Step 2: Записать `SKILL.md` дословно**
````markdown
---
name: kompas-fdm-design
description: >
Методика проектирования деталей под FDM/FFF 3D-печать в КОМПАС-3D через MCP-сервер этого
проекта: правила DFM (нависания и угол θ_max, толщины стенок n·w, отверстия и teardrop,
посадки/зазоры, ориентация под прочность, elephant foot, бобышки/инсёрты/защёлки) ПЛЮС
лёгкая самопроверка геометрии инструментами осмотра. Используй, когда задача — спроектировать
или ДОВЕСТИ деталь, чтобы она хорошо ПЕЧАТАЛАСЬ на FDM. Триггеры: «сделай деталь
печатнопригодной / под FDM», «спроектируй … под печать», «напечатается ли без поддержек?»,
«подбери зазоры для печатной посадки», «как ориентировать деталь под печать», «почему деталь
плохо печатается / где будут нависания», «доведи деталь под FDM». Строит через навык kompas-3d.
НЕ для: механики построения через MCP (это kompas-3d); слайсинга/нарезки/g-code (вне границ);
поиска по справке SDK (субагент kompas-sdk-research).
---
# kompas-fdm-design — проектирование деталей под FDM-печать
## Что это (и чем НЕ является)
Методический слой **поверх** навыка `kompas-3d`. Отвечает на вопрос **«как спроектировать, чтобы
напечаталось на FDM»**, а не «чем строить».
- **`kompas-3d`** = *чем и как строить* через MCP (эскиз→операция→осмотр→`validate_part`). Этот
навык **опирается** на него для механики.
- Этот навык = *какие правила геометрии* соблюдать, чтобы FDM-печать удалась.
- **Слайсинг — вне границ.** Навык не нарезает и не оценивает g-code.
## Когда применять / когда НЕ применять
**Применять:** «сделай печатнопригодным / под FDM», «спроектируй … под печать», «напечатается без
поддержек?», «подбери зазоры печатной посадки», «как ориентировать под печать», «почему плохо
печатается», «доведи деталь под печать».
**НЕ применять:** чистая механика построения (→ `kompas-3d`); слайсинг/нарезка/g-code (вне границ);
поиск сигнатур/констант в справке SDK (→ субагент `kompas-sdk-research`).
## Калибровка (выполни первым шагом)
Все правила масштабируются от трёх параметров (часть значений — абсолютные эмпирические мм,
помечены «калибровать тестом»):
- **`w` = ширина линии ≈ диаметр сопла.** Дефолт: сопло 0.4 → `w ≈ 0.40.45 мм`. → стенки `n·w`.
- **`h` = высота слоя.** Дефолт `h ≈ 0.5·сопло` (0.2 мм); структурная печать 0.2–0.25.
- **`θ_max` = предельный угол самонесущей поверхности ОТ ВЕРТИКАЛИ.** `θ_max ≈ arctan(w/2h)`
(≈45° при `w`=0.4, `h`=0.2). **Дефолт 45°** (PLA, хороший обдув); **40°** для PETG/ABS или
толстого слоя (`h≥0.3` → ~34°). Нависания, teardrop, фаски, зенковки берут угол из `θ_max`.
- **Материал** (PLA / PETG / ABS) — модификатор зазоров/мостов/коробления/`θ_max`. См.
`references/fdm-rules.md`.
Если сопло/слой/материал не заданы — прими дефолты (сопло 0.4, `h`=0.2, PLA) и скажи об этом.
## Два правила (соблюдай всегда)
**1. Ориентация печати — первое проектное решение.**
- **Спроси у пользователя** (если не задано): главное направление рабочей нагрузки и
косметические/критичные грани. **Путь нагрузки из геометрии не выводится** — его задаёт задача.
- Реши постановку на стол (ось Z = рост слоёв). От неё зависит: где нависания; куда смотрят
отверстия (вертикальные → компенсация Ø; горизонтальные → teardrop); путь нагрузки (**держи в
XY**; Z-сжатие можно, Z-растяжение/срез — нет); плоскости сопряжения (на XY-гранях); «лесенка» на
наклонных функциональных поверхностях; если поддержки неизбежны — чтобы опорные грани были
некритичными/скрытыми.
- Зафиксируй ориентацию и проектируй под неё.
**2. Чек-лист печатнопригодности перед выдачей** (ниже). Сначала **`validate_part`** (деталь
*валидна*), затем **FDM-чек-лист** (деталь *печатнопригодна*) — разные проверки. **Гео-аудит
эвристический и не доказывает печатнопригодность** (не ловит путь нагрузки/анизотропию).
## Рабочий цикл
1. **Калибровка**: сопло→`w`; слой→`h``θ_max`; материал→поправки.
2. **Ориентация** (правило 1): опрос (нагрузка/косметика) → постановка, ось слоёв, сопряжения,
«лесенка», поддержки.
3. **Правила эскиза/операции** (строй через `kompas-3d`): стенки `n·w`; нависания → скос под
`θ_max`; горизонтальные отверстия → teardrop; вертикальные → компенсация Ø; фаска у основания;
зазоры посадок (со знаком); заходные фаски; мин. элементы/текст; бобышки/инсёрты/защёлки.
Числа — в `references/fdm-rules.md`.
4. **Гео-аудит** (`references/geometry-audit.md`) — инструментами осмотра.
5. **Предусловия экспорта**: единое тело/манифолд (`boolean_union` при необходимости) →
`validate_part` чисто.
6. **Чек-лист** → экспорт **через `export_step`**.
## Чек-лист печатнопригодности
- [ ] Направление нагрузки и косметические грани **получены от пользователя**; ориентация
зафиксирована; нагрузка в XY (или Z только на сжатие); сопряжения на XY-гранях.
- [ ] Стенки кратны `w` (≥2·w; несущие ≥3·w = N периметров); нет «не кратных `w`» (кроме
функциональных).
- [ ] Нет 90°-полок; нависания ≤`θ_max` или заменены скосами; мосты в пределах пролёта по короткой
стороне; внутренним поддержкам — доступ.
- [ ] Горизонтальные отверстия — teardrop/D (геометрия из `θ_max`); вертикальные — компенсация Ø;
глухие — дно ≥2–3 мм.
- [ ] Фаска у основания (elephant foot); внутренние углы ≥R0.5; опорная площадка есть.
- [ ] Посадки по таблице со **знаком** (натяг — вычесть); допуск ±0.2 **НЕ** прибавлен к зазору;
заходные фаски на сопряжениях.
- [ ] Мин. элементы/текст ≥ порогов; аспект тонких выступов ≤4–5×.
- [ ] Бобышки/инсёрты (бор ≤ OD, ставить с Z-грани)/резьба/защёлки (изгиб в XY) по правилам.
- [ ] Полости — дренаж/вент; критичные поверхности не под «лесенкой»/поддержкой.
- [ ] Предусловия экспорта: единое тело/манифолд; `validate_part` чисто.
## Гео-аудит (кратко)
Лёгкая самопроверка построенной модели **существующими** инструментами осмотра MCP:
`describe_model` / `list_faces` / `describe_face` (нависания по нормалям нижних граней; цилиндры с
горизонтальной осью → нужен teardrop), `get_bounding_box` (как ось слоёв соотносится с габаритом),
`measure` (номиналы/зазоры), `list_bodies` + `validate_part` (единое тело). **Границы и методика —
`references/geometry-audit.md`.** Аудит эвристический; истинная мин. толщина стенки и полный детект
криволинейных нависаний не решаются — это **не приговор и не доказательство печатнопригодности**.
## Связанное
- Полный численный свод DFM: [`references/fdm-rules.md`](references/fdm-rules.md).
- Рецепты самопроверки и границы: [`references/geometry-audit.md`](references/geometry-audit.md).
- Механика построения через MCP: навык **`kompas-3d`**.
- Поиск по справке SDK: субагент **`kompas-sdk-research`**.
- Дизайн навыка: `docs/superpowers/specs/2026-05-27-kompas-fdm-design-skill-design.md`.
````
- [ ] **Step 3: Проверить структуру, имя, ссылки, объём**
```powershell
$skill = ".claude/skills/kompas-fdm-design/SKILL.md"
Test-Path $skill
(Select-String -SimpleMatch -Path $skill -Pattern "name: kompas-fdm-design").Count # 1
(Select-String -SimpleMatch -Path $skill -Pattern "## Два правила").Count # 1
(Select-String -SimpleMatch -Path $skill -Pattern "## Чек-лист печатнопригодности").Count # 1
(Select-String -SimpleMatch -Path $skill -Pattern "references/fdm-rules.md").Count # >=1
(Select-String -SimpleMatch -Path $skill -Pattern "references/geometry-audit.md").Count # >=1
(Get-Content $skill | Measure-Object -Line).Lines # <= ~200 (диспетчер лёгкий)
```
Expected: `True`; счётчики `1,1,1,≥1,≥1`; строк ≤ ~200.
- [ ] **Step 4: Commit**
```powershell
git add .claude/skills/kompas-fdm-design/SKILL.md
git commit -m "feat(skill): kompas-fdm-design — диспетчер SKILL.md (правила DFM + гео-аудит)"
```
---
## Task 2: `references/fdm-rules.md` (полный свод DFM)
**Files:** Create `.claude/skills/kompas-fdm-design/references/fdm-rules.md`
- [ ] **Step 1: Записать файл дословно**
````markdown
# Свод правил DFM для FDM-печати
> Выверено 3 ревью (pi/glm-5.1, pi/kimi-k2.6, Codex). Числа для `w≈0.40.45`, `h≈0.2` (сопло 0.4).
> **Зазоры — на сторону (радиальные)**; диаметральный = 2×. **Угол нависания — от вертикали**
> (вертикаль=0°, горизонталь=90°); самонесущие — ≤ `θ_max`. Параметры `w`/`h`/`θ_max` — см.
> SKILL.md → «Калибровка». Ссылки «§N» ниже — на разделы этого файла.
## 1. Стенки и оболочки
- Толщина стенки = **`n · w`**. Мин. конструктивная — **2·w (~0.8 мм)**; несущая — **≥3·w**.
- **Маппинг стенка→периметры:** нужно `N` периметров ⇒ стенка **`N·w`** (при `w`=0.45: 3 → 1.35,
4 → 1.8, 5 → 2.25 мм).
- **Не задавай толщину стенки, не кратную `w`** (напр. 0.6 при `w`=0.45): слайсер оставит зазор
или переэкструдирует. Прыгай на следующий кратный.
- **Caveat:** `n·w` — для конструктивных стенок; внешняя функциональная величина (флексура,
тепловой барьер, посадочный размер) важнее кратности.
- Одиночная стенка `1·w` — только декоративная. Узкий сквозной прорез — **≥2·w (~0.8 мм)**.
## 2. Нависания, полки, мосты (разделять!)
- **Самонесущие — поверхности ≤ `θ_max` от вертикали.** 45–60° (при дефолте) — печатается с
падением качества; **> `θ_max` существенно — поддержки** → избегать редизайном.
- **90°-полка (консоль, опора с одной стороны) НЕ печатается ни на какой длине** (миф «≤6 мм»
неверен — провисает с первого слоя). Любую горизонтальную полку: **скос под `θ_max`**, либо
**превратить в мост** (две опоры), либо поддержка.
- **Мост (bridge) — пролёт между двумя опорами на одной высоте.** При достаточном обдуве, `h≈0.2`,
консервативно (для ненастроенного слайсера): **PLA ~1525 мм, PETG ~1015 мм, ABS ~1218 мм**.
Длинные прямоугольные проёмы **ориентировать так, чтобы мост шёл по короткой стороне**; концы —
на сплошных опорах.
- Нижнюю функциональную поверхность моста — припуск **0.20.3 мм** на провис. *Граница:* величину
провиса из CAD не предсказать (обдув/скорость — настройки печати).
- **Внутренние/потолочные нависания хуже наружных** — потолок пазов аркой/шевроном, не плоским
пролётом > 2 мм.
- **Доступ к поддержкам:** окно во внутренней полости **≥810 мм**.
## 3. Отверстия
- **Вертикальные (ось ∥ Z)** печатаются уже номинала → **увеличить диаметр модели** (радиус на
половину): **+0.2 мм (Ø<4)**, **+0.20.3 мм (Ø 410)**, **+0.10.2 мм (Ø>10)**; калибровать,
критичные — рассверливать.
- **Горизонтальные (ось в XY)****teardrop** или **D-отверстие** (плоский верх). Мин. Ø **2 мм**.
Круглая часть тоже печатается уже → **+0.10.2 мм** к её Ø.
- **Геометрия teardrop:** боковины касательны окружности под углом `θ_max` к вертикали (с двух
сторон), сходятся в вершине на вертикальной оси. Высота вершины над центром = **`r / sin θ_max`**;
включённый угол при вершине = **`2·θ_max`**. При `θ_max`=45° → `r/sin45° = √2·r ≈ 1.414·r` над
центром (= **`0.414·r` над верхом окружности**), угол 90°. Низ — оставшаяся дуга окружности.
- **Глухое отверстие:** дно = внутренний мост → **толщина дна ≥2–3 мм** или купольное/
вентилируемое. Сквозные предпочтительнее.
- **Отступ от края** — через остаточную перемычку: стенка между отверстием и краем **≥23·w**
(лёгкая нагрузка) / больше под крепёж.
## 4. Посадки и зазоры (печатная деталь ↔ печатная деталь)
Зазор **на сторону** (радиальный); диаметральный = 2× значения:
| Посадка | Зазор/сторону | Примечание |
|---|---|---|
| **Натяг (press)** | **−0.05…0 мм** (вычесть из номинала!) | короткий, PLA; иначе snap-fit (§14) |
| Переходная/плотная | 0.05–0.15 мм | |
| Скользящая | 0.150.20 мм | PLA↔PLA; контакт ≥20 мм → 0.20; PETG +0.05 |
| Свободная | 0.250.35 мм | >0.35/сторону — уже очень слабо |
- **Знак:** «натяг» = **отрицательный** зазор → вычесть из номинала (вал +/отверстие −).
Положительное число в строке press — ошибка прочтения.
- **ABS↔ABS:** +0.05/сторону. **PETG:** прессовые со временем «расслабляются».
- **Допуск точности (НЕ прибавлять к посадкам):** общий разброс FDM — **XY ±0.2 мм** (±0.1
калибровано), **Z хуже**. Это точность изготовления, не добавка к зазору.
## 5. Первый слой / стол
- **Elephant foot** — от притирки первого слоя (низкий Z-offset/переэкструзия; НЕ от высоты слоя).
Фаска по нижним рёбрам: **0.3 × 45° (калибровано)** / **0.51.0 × 45° (слабая калибровка)**.
- Внутренние углы у основания — **скругление ≥R0.5**.
- **Опорная площадка:** без «лезвийных» оснований; контакт хотя бы ~3 периметра. Высокие тонкие
детали — **интегральный фланец 1–2 мм** (предпочтительнее brim).
## 6. Ориентация и прочность
- **Z (межслойная) прочность от XY:** PLA ~4055%, PETG ~3550%, **ABS ~2035% (выброс)**. Несущую
нагрузку — в **XY (вдоль слоёв)**.
- **Z-сжатие допустимо** (слои в сжатии не расслаиваются); избегать **Z-растяжения и Z-среза**.
- Изгиб: слои в растяжении/сжатии, не на срез по линии слоя.
- Z-нагрузка неизбежна → **увеличить несущее сечение** ~×2 относительно XY-расчёта.
- Плоскости сопряжения — на **XY-гранях**, не на Z-боковинах.
## 7. Минимальные элементы и текст
- Выступ/штифт/ребро — **≥1·w (≥0.5 мм)**; паз/щель — **≥2·w (~0.8 мм)**.
- **Аспект тонких выступов:** высота ≤ ~4–5× базовой ширины; выше — конусность/раскос/редизайн.
- **Выпуклый** текст: штрих **≥0.5 мм**, высота **≥2·h (~0.4 мм)**, sans-serif bold.
- **Гравированный** текст: штрих **≥2·w (~0.80.9 мм)** (нужно ≥2 периметра; 0.6 мм не влезает),
глубина **≥2·h (~0.4 мм)**. (pt не используем — геометрия в мм.)
## 8. «Лесенка» (staircase) — критерий ориентации
- Наклонные/криволинейные поверхности дают ступени: глубина ≈ **`h / tan(α)`** (α — угол от
горизонтали). Пример: α=30°, `h`=0.2 → ~0.35 мм.
- Критичные (скользящие/уплотняющие/оптические) поверхности **ориентировать вертикально или
горизонтально**. Это вход в правило ориентации (SKILL.md, правило 1).
- Большой плоский **верх** без опоры коробит («подушка») — внутренние рёбра каждые ~15–20 мм или
достаточная толщина верха.
## 9. Бобышки, инсёрты, резьба
- **Саморез/винтонарезной** пилот (M3): ~Ø2.5 PLA / Ø2.6 PETG / Ø2.7 ABS; заход ≥3 мм.
- **Термоинсёрт латунный** (M3): бор **по даташиту** (типично ~Ø4.0, ±0.05); **`Ø_bore = OD_инсёрта
(0…0.1) мм`** (≤ OD, лёгкий натяг под расплав — НЕ больше OD); стенка бобышки **≥2 мм**; глубина
= длина инсёрта + 0.5 мм; **ставить с верхней (Z) грани** (не в боковину).
- **Бобышка под винт:** OD ≥ 2–3× Ø винта; не делать массивный сплошной объём (карман/оболочка).
- **Сквозное под металлический болт:** радиальный зазор 0.2–0.3 → **+0.4–0.6 мм к номиналу** болта.
- **Резьба:** не моделировать <M6 → инсёрты/саморезы. Если моделировать: **≥M6, ось вертикальная**,
зазор +0.1–0.2 мм, профиль крупный/трапецеидальный (не мелкий ISO — вершины-нависания).
## 10. Фаски vs скругления
- **Нижние (у стола) рёбра — фаска** (скругление = нулевой контакт + EF).
- **Верхние рёбра — скругление** (R0.52.0); **но** радиус **> ~½ толщины стенки** сам даёт
нависание > `θ_max` → тогда фаска/ступень.
- **Внутренние углы — всегда скругление ≥R0.5**.
## 11. Зенковки / цековки
- **Цековка (counterbore)** — большим Ø/полостью **вверх** (дно по телу, не мостом); глубина +0.3 мм.
- **Зенковка (countersink):** конус **вверх**; включённый угол **≤90°** → стенки ≤45° от вертикали
печатается; **>90°** → стенки-нависание → поддержка или замена цилиндрической цековкой.
## 12. Заходные фаски (assembly relief)
- На штифтах, отверстиях, инсёртах, защёлках, «ласточкиных хвостах» — **заходная фаска** (≈0.51 мм
× 45° или ≈ половина зазора) против задиров при сборке.
## 13. Разбиение детали и сборка из печатных частей
- Конфликт «прочная ориентация vs бесподдержечность», или крупная/коробящаяся деталь → **разбить**
на части с самоустанавливающимися стыками (печатные штифты/шпонки/замки), склейка; стыки на
XY-гранях. Зазор стыка — по §4.
## 14. Защёлки (snap-fit) / живой шарнир
- Консольная защёлка: толщина балки **≥23·w (≈1.02.0 мм)**, зацеп/возврат **0.30.8 мм**,
длина/толщина **≥5:1** (до 10:1), **скругление в основании ≥R0.5**.
- **Направление слоёв:** балка гнётся **в плоскости XY** (слои перпендикулярны изгибу), **не
поперёк Z** (расслоится с первого нажатия).
- **Живой шарнир** — только PLA/PP-подобные, перемычка **0.30.5 мм**; PETG/ABS не годятся.
## 15. Коробление (геометрия)
- Большие плоскости (>80×80, особенно ABS): скругления углов R3–5 + рёбра/решётка снизу.
- Радиус внешних углов: R2 (ABS) / R1 (PLA/PETG).
- Длинные тонкие пролёты (>60 мм, <2 мм) — рёбра/косынки каждые 30–40 мм; высота ребра ≤5× базы.
- Избегать сплошных кубов/плит → карман/оболочка + рёбра. Усадка: PLA ~0.3%, PETG ~0.5%, ABS ~0.8%.
- Симметрия геометрии уравновешивает усадку.
- *«Мышиные уши» (Ø8–10 мм по углам)***крайняя мера адгезии** (по сути brim-геометрия);
предпочтительно интегральный фланец/скругления углов.
- *Граница:* стол/корпус/обдув для ABS — настройки печати, вне навыка; здесь только геометрия.
## 16. Полые детали и гигиена модели
- **Полости:** дренаж Ø35 мм у **низшей** точки + вент у **высшей**.
- Допуски/зазоры — **в геометрию** (слайсер читает модель буквально).
- Раздельные тела — зазор ≥0.2 мм (общая CAD-гигиена; перед выдачей объединять — рабочий цикл,
шаг 5 в SKILL.md).
````
- [ ] **Step 2: Структурная проверка (16 разделов + 4 строки таблицы посадок)**
```powershell
$rules = ".claude/skills/kompas-fdm-design/references/fdm-rules.md"
(Select-String -Path $rules -Pattern "^## \d+\.").Count # 16 (разделы)
(Select-String -SimpleMatch -Path $rules -Pattern "| Натяг (press)").Count # 1 (строка таблицы)
(Select-String -SimpleMatch -Path $rules -Pattern "| Свободная").Count # 1
```
Expected: `16, 1, 1`. Если разделов ≠16 — потерян/задвоен раздел.
- [ ] **Step 3: Проверка инвариантов содержимого (по литералам)**
```powershell
$rules = ".claude/skills/kompas-fdm-design/references/fdm-rules.md"
foreach ($p in @(
"r / sin θ_max", "2·θ_max", "0.414·r над верхом", # inv2 teardrop
"90°-полка", "НЕ печатается ни на какой длине", # inv3 полка
"вычесть из номинала", "0.05…0 мм", # inv4 натяг
"НЕ прибавлять к посадкам", # inv5 допуск
"+0.2 мм (Ø<4)", "+0.20.3 мм (Ø 410)", # inv6 компенсация
"PLA ~4055%", "ABS ~2035%", # inv7 Z-прочность
"Ø_bore = OD_инсёрта (0…0.1) мм" # inv8 инсёрт
)) { "{0,-40} {1}" -f $p, ((Select-String -SimpleMatch -Path $rules -Pattern $p).Count) }
```
Expected: каждая строка оканчивается `1` (или больше). Любой `0` = потерянный инвариант, исправить.
- [ ] **Step 4: Commit**
```powershell
git add .claude/skills/kompas-fdm-design/references/fdm-rules.md
git commit -m "feat(skill): kompas-fdm-design — references/fdm-rules.md (полный свод DFM)"
```
---
## Task 3: `references/geometry-audit.md` (самопроверка + границы)
**Files:** Create `.claude/skills/kompas-fdm-design/references/geometry-audit.md`
**Source:** spec §9 (таблица + границы). «Как применять в цикле» — авторский раздел (привязка к
рабочему циклу), содержимое дано ниже дословно (НЕ плейсхолдер).
- [ ] **Step 1: Записать файл дословно**
````markdown
# Гео-аудит модели под FDM — что проверяемо инструментами осмотра
Лёгкая самопроверка построенной модели **существующими** инструментами осмотра MCP. Запускать на
шаге 4 рабочего цикла (см. SKILL.md), перед чек-листом и экспортом.
## Что проверяемо
| Проверка | Как | Статус |
|---|---|---|
| Нависания (приближённо) | `list_faces`/`describe_face`: для **нижних** граней угол поверхности от вертикали; > `θ_max` → флаг | ✅ плоские; ⚠️ криволинейные грубо |
| Ориентация (геом. прокси) | `get_bounding_box`: как ось слоёв соотносится с габаритом | ⚠️ длинная ось ≠ путь нагрузки |
| Горизонтальные круглые отверстия | `describe_face`: цилиндр с горизонтальной осью → «нужен teardrop» | ✅ |
| Номиналы / зазоры / габариты | `measure` между гранями; `get_bounding_box` | ✅ |
| Тело / манифолд перед выдачей | `list_bodies` (одно тело?), `validate_part` | ✅ |
## Граница честности
- **Угол нависания** мерить в **той же конвенции, что fdm-rules.md** (от вертикали; нижняя грань с
поверхностью > `θ_max` от вертикали = нависание) — не путать с углом нормали от горизонтали.
- **Путь нагрузки агент НЕ выводит из габарита** — берёт из задачи/опроса (правило 1). Длинная ось
≠ несущая.
- **Истинная мин. толщина стенки и полный детект криволинейных нависаний — не решаются** (нет
thickness/overhang-солвера).
- **Аудит эвристический и НЕ доказывает печатнопригодность** (не ловит анизотропию/путь нагрузки).
Вывод — список флагов для решения, не «приговор». Слайсер навык не зовёт намеренно.
## Как применять в цикле
1. После построения и `validate_part` — пройти таблицу выше сверху вниз.
2. Каждый флаг — сверить с соответствующим правилом `fdm-rules.md` и решить: исправить геометрию
или принять осознанно.
3. Путь нагрузки и косметические грани взять из ответа пользователя (правило 1), не из габарита.
4. Затем — чек-лист печатнопригодности (SKILL.md) → экспорт через `export_step`.
````
- [ ] **Step 2: Проверка (5 строк таблицы + границы)**
```powershell
$audit = ".claude/skills/kompas-fdm-design/references/geometry-audit.md"
(Select-String -SimpleMatch -Path $audit -Pattern "| Нависания (приближённо)").Count # 1
(Select-String -SimpleMatch -Path $audit -Pattern "| Ориентация (геом. прокси)").Count # 1
(Select-String -SimpleMatch -Path $audit -Pattern "| Тело / манифолд перед выдачей").Count # 1
(Select-String -SimpleMatch -Path $audit -Pattern "НЕ доказывает печатнопригодность").Count # 1 (inv9)
(Select-String -SimpleMatch -Path $audit -Pattern "берёт из задачи/опроса").Count # 1 (inv9)
```
Expected: все `1`.
- [ ] **Step 3: Commit**
```powershell
git add .claude/skills/kompas-fdm-design/references/geometry-audit.md
git commit -m "feat(skill): kompas-fdm-design — references/geometry-audit.md (самопроверка + границы)"
```
---
## Task 4: Верификация навыка (инварианты + триггеры + границы scope)
**Files:** Read `.claude/skills/kompas-fdm-design/**`.
- [ ] **Step 1: Прогон по инвариантам 1–11 (одна команда, таблица результатов)**
```powershell
$skill = ".claude/skills/kompas-fdm-design/SKILL.md"
$rules = ".claude/skills/kompas-fdm-design/references/fdm-rules.md"
$audit = ".claude/skills/kompas-fdm-design/references/geometry-audit.md"
$all = @($skill,$rules,$audit)
# Должны ПРИСУТСТВОВАТЬ (count >=1):
$present = @(
@{n="1 θ_max param"; f=$skill; p="θ_max ≈ arctan(w/2h)"},
@{n="2 teardrop"; f=$rules; p="r / sin θ_max"},
@{n="3 полка"; f=$rules; p="НЕ печатается ни на какой длине"},
@{n="4 натяг знак"; f=$rules; p="вычесть из номинала"},
@{n="5 допуск"; f=$rules; p="НЕ прибавлять к посадкам"},
@{n="6 комп. Ø"; f=$rules; p="+0.2 мм (Ø<4)"},
@{n="7 Z PLA"; f=$rules; p="PLA ~4055%"},
@{n="8 инсёрт бор"; f=$rules; p="Ø_bore = OD_инсёрта (0…0.1) мм"},
@{n="9 аудит≠доказ"; f=$audit; p="НЕ доказывает печатнопригодность"},
@{n="10 export_step"; f=$skill; p="export_step"}
)
foreach ($c in $present) { "{0,-16} {1}" -f $c.n, ((Select-String -SimpleMatch -Path $c.f -Pattern $c.p).Count) }
# Должны ОТСУТСТВОВАТЬ (count = 0):
"10 export_stl {0}" -f ((Select-String -SimpleMatch -Path $all -Pattern "export_stl").Count)
"11 slicer-name {0}" -f ((Select-String -Path $all -Pattern "OrcaSlicer|PrusaSlicer|Cura|Bambu|SuperSlicer|Simplify3D|Slic3r|KISSlicer|ideaMaker").Count)
```
Expected: блок «present» — все ≥1; `export_stl` = **0**; `slicer-name` = **0**. Любое нарушение —
вернуться в Task 1/2/3, исправить файл, переидти Step.
- [ ] **Step 2: Тест триггеров `description` (пары + критерий)**
Критерий: для каждого промпта оценить, перекрывает ли его текст `description` (по ключевым словам/
смыслу). ✅ = должен сработать `kompas-fdm-design`; ❌ = должен уйти другому навыку/субагенту.
| Промпт | Ожидание |
|---|---|
| «сделай эту деталь печатнопригодной» | ✅ kompas-fdm-design |
| «как ориентировать кронштейн под FDM» | ✅ kompas-fdm-design |
| «подбери зазор посадки для печати» | ✅ kompas-fdm-design |
| «подбери отверстие под термоинсёрт M3» | ✅ kompas-fdm-design |
| «напечатается ли без поддержек?» | ✅ kompas-fdm-design |
| «построй коробку выдавливанием 20×20×10» | ❌ → kompas-3d |
| «нарежь модель / сколько будет печататься» | ❌ → вне границ (слайсинг) |
| «какая сигнатура у IHole3D» | ❌ → kompas-sdk-research |
Pass-критерий: все 5 ✅ покрываются триггерами `description`; все 3 ❌ покрываются блоком «НЕ для».
Если граница размыта (ложное ✅/❌) — уточнить формулировки триггеров/«НЕ для» в SKILL.md и
переидти Step 1+2.
- [ ] **Step 3: Граница scope — не тронут `src/` (инвариант §2 spec: без новых MCP-инструментов)**
```powershell
git diff --name-only main...HEAD | Where-Object { $_ -like "src/*" }
```
Expected: **пусто** (навык не меняет MCP-сервер). Если есть `src/*` — ошибка scope.
- [ ] **Step 4: Commit правок (если были)**
```powershell
git add .claude/skills/kompas-fdm-design
git commit -m "fix(skill): kompas-fdm-design — уточнения по верификации"
```
(Если правок не было — шаг пропустить, пустой коммит не делать.)
---
## Task 5: Синхронизация документации проекта
**Files (правит субагент `docs-maintainer`):** `README.md`, `CLAUDE.md`; при необходимости
`docs/ARCHITECTURE.md`.
- [ ] **Step 1: Делегировать `docs-maintainer`**
Через `Agent` (subagent_type `docs-maintainer`) передать сводку: «Добавлен навык
`.claude/skills/kompas-fdm-design/` — методика проектирования под FDM (правила DFM + лёгкий
гео-аудит), отдельный от `kompas-3d`, без слайсера, экспорт через `export_step`. Упомянуть рядом с
описанием навыка `kompas-3d` в `CLAUDE.md` (раздел Principle/skills) и в `README.md` (где
перечислены навыки). Счётчики инструментов/тестов НЕ меняются. presentation.html не трогать.»
- [ ] **Step 2: Фолбэк, если субагент недоступен (ручная правка)**
Если `docs-maintainer` недоступен — в `CLAUDE.md` найти строку-якорь:
`On top of MCP — skill **`.claude/skills/kompas-3d/`** with a methodology` и добавить сразу после
предложения:
> Additionally, the **`.claude/skills/kompas-fdm-design/`** skill layers FDM design-for-printing methodology (DFM rules + light geometry audit) on top of `kompas-3d`; standalone, no slicer, exports via `export_step`.
В `README.md` — в разделе про навыки/skills добавить аналогичную строку про `kompas-fdm-design`
(если такого раздела нет — пропустить README).
- [ ] **Step 3: Проверить и закоммитить**
```powershell
(Select-String -SimpleMatch -Path "CLAUDE.md","README.md" -Pattern "kompas-fdm-design").Count # >=1
git add CLAUDE.md README.md docs/ARCHITECTURE.md
git commit -m "docs: упомянуть навык kompas-fdm-design (проектирование под FDM)"
```
Expected: счётчик ≥1.
---
## Task 6 (опционально, рекомендуемый follow-up): live-обкатка в `usecases/`
> **Рационал:** навык — дистилляция знаний, уже выверенная экспертными консультациями + 6 ревью
> (spec×3, план×3). Поэтому live-UC — **рекомендуемая валидация на живой модели, а не гейт**
> существования навыка (в отличие от новых MCP-механик, которые проект требует доказывать в
> `usecases/`). Требует запущенного КОМПАС. `usecases/` в .gitignore — коммит не нужен.
**Files:** Create `usecases/0003-fdm-bracket-no-supports/case.md` (из `usecases/_TEMPLATE/`).
- [ ] **Step 1: Завести кейс**
```powershell
Copy-Item -Recurse "usecases/_TEMPLATE" "usecases/0003-fdm-bracket-no-supports"
```
- [ ] **Step 2: Заполнить `case.md`** — Цель: спроектировать простой кронштейн под FDM **без
поддержек**, применяя навык. Проверить на живой модели: ориентацию (правило 1), нависания (скос
под `θ_max`), горизонтальное отверстие (teardrop по `r/sin θ_max`), фаску у основания, гео-аудит,
чек-лист, экспорт `export_step`. Заполнить разделы Цель / Промт / Статус (см. соседние кейсы
`usecases/0001`, `usecases/0002` как образец оформления).
- [ ] **Step 3: Прогнать сценарий через MCP** (если КОМПАС запущен): построить деталь правилами
навыка через `kompas-3d`, прогнать гео-аудит и чек-лист, сохранить артефакты в `artifacts/`,
экспортировать `export_step`. Зафиксировать находки в «Выводы». Подтверждённые числовые уточнения
— поднять в `references/fdm-rules.md` (и переидти Task 4 Step 1).
---
## Task 7: Финальное ревью навыка и завершение ветки
- [ ] **Step 1:** `superpowers:requesting-code-review` по диффу ветки (навык + доки) — ясность
формулировок, отсутствие плейсхолдеров, корректность кросс-ссылок.
- [ ] **Step 2:** `superpowers:writing-skills`-верификация: frontmatter валиден; `description`
срабатывает на целевых триггерах и не перехватывает чужие (Task 4 Step 2); progressive disclosure
соблюдён (SKILL.md лёгкий ≤~200 строк, числа в references).
- [ ] **Step 3: Финальный коммит (условно)**
```powershell
if ((git status --porcelain).Length -gt 0) {
git add -A; git commit -m "chore(skill): kompas-fdm-design — финал ревью"
} else { "Нет изменений — коммит не нужен" }
```
- [ ] **Step 4:** `superpowers:finishing-a-development-branch` — merge/PR по выбору пользователя.
---
## Self-Review (выполнено автором плана)
**1. Покрытие spec:** §1–§3 (цель/locked/параметры) → Task 1 (SKILL.md «Калибровка», описание).
§4 (структура файлов) → Tasks 1–3. §5 (два правила) + §6 (цикл) + §10 (чек-лист) → Task 1 (в
SKILL.md дословно). §7 (триггеры) → Task 1 frontmatter + Task 4 Step 2. §8.1–§8.16 → Task 2 (полный
текст). §9 → Task 3 (полный текст). §11 (обкатка) → Task 6 (с рационалом про опциональность). §12
(non-goals: без слайсера/без MCP-инструментов/без export_stl) → Task 4 Step 1 (export_stl=0,
slicer-name=0) + Step 3 (нет `src/*`). §13 OQ-1/OQ-2 → Task 2 Step 3 (литералы компенсации/EF в
fdm-rules.md); OQ-3 → инв.10 (export_step есть, export_stl нет). Пробелов нет.
**2. Плейсхолдеры:** все три файла даны дословно (включая «Как применять в цикле» в Task 3 —
авторский, с готовым текстом). Делегирование docs-maintainer снабжено ручным фолбэком (Task 5
Step 2) с точным якорем и текстом. Task 4 Step 2 имеет явный pass-критерий. «TODO/TBD» нет.
**3. Согласованность имён/формул:** имя `kompas-fdm-design`, пути файлов, параметры `w`/`h`/`θ_max`,
формула teardrop `r / sin θ_max` и угол `2·θ_max`, строка инсёрта `Ø_bore = OD_инсёрта (0…0.1) мм`,
знак натяга `0.05…0` — записаны единообразно в содержимом файлов (Tasks 1–3) и в литералах
проверок (Tasks 2–4). Проверочные `Select-String -SimpleMatch` используют те же литералы, что
записаны в файлы (нет regex-эскейпинга/кодпоинт-хрупкости). Заголовок плана: инварианты проверяются
в **Task 4** (исправлено).
@@ -0,0 +1,130 @@
# Codex SDK Research Agent Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Turn the project-scoped `kompas-sdk-research` definition into a Codex-native, explicitly modeled, read-only documentation research agent.
**Architecture:** Keep the existing single custom-agent TOML and its KOMPAS SDK research methodology. Add Codex session overrides for model, reasoning effort, and sandboxing, and remove copied Claude-specific model terminology without changing `.claude/agents/kompas-sdk-research.md`.
**Tech Stack:** Codex custom-agent TOML, PowerShell, Python 3 `tomllib`, ripgrep, Git.
## Global Constraints
- Use `model = "gpt-5.6-terra"`.
- Use `model_reasoning_effort = "medium"`.
- Use `sandbox_mode = "read-only"`.
- Keep `docs/Kompas3D_SDK/` as the canonical COM API5/API7 documentation source.
- Do not add web research, workspace editing, KOMPAS geometry construction, or KsAPI research to the agent.
- Leave `.claude/agents/kompas-sdk-research.md` unchanged.
- The parent task remains responsible for interop reflection checks when documentation and generated .NET names may differ.
---
### Task 1: Adapt and verify the Codex custom agent
**Files:**
- Modify: `.codex/agents/kompas-sdk-research.toml`
- Verify unchanged: `.claude/agents/kompas-sdk-research.md`
**Interfaces:**
- Consumes: Codex standalone custom-agent fields `name`, `description`, `developer_instructions`, `model`, `model_reasoning_effort`, and `sandbox_mode`.
- Produces: project agent `kompas-sdk-research` running with `gpt-5.6-terra`, medium reasoning, and a read-only sandbox.
- [ ] **Step 1: Run the pre-change configuration check and confirm it fails**
Run:
```powershell
@'
import pathlib
import tomllib
path = pathlib.Path('.codex/agents/kompas-sdk-research.toml')
data = tomllib.loads(path.read_text(encoding='utf-8'))
text = path.read_text(encoding='utf-8')
assert data['model'] == 'gpt-5.6-terra'
assert data['model_reasoning_effort'] == 'medium'
assert data['sandbox_mode'] == 'read-only'
assert not {'Haiku', 'Opus', 'Claude'} & set(text.replace('(', ' ').replace(')', ' ').split())
'@ | python -
```
Expected: FAIL with `KeyError: 'model'` because the Codex-specific model fields have not been added yet.
- [ ] **Step 2: Add Codex-native model and isolation settings**
Apply this exact header change:
```diff
name = "kompas-sdk-research"
+model = "gpt-5.6-terra"
+model_reasoning_effort = "medium"
+sandbox_mode = "read-only"
description = "..."
```
Replace the `description` value with this exact single-line TOML string:
```toml
description = "ОБЯЗАТЕЛЬНО делегируй этому read-only субагенту ЛЮБОЙ поиск по справке COM API КОМПАС в MD-базе docs/Kompas3D_SDK/ — НЕ ищи по справке в основной задаче. Срабатывает ПРОАКТИВНО, без явной просьбы пользователя: как только при планировании или реализации фичи понадобилась сигнатура метода (Automation/COM), значения перечисления (Obj3dType, ksHoleTypeEnum, ST_MIX_*, стили линий…), нужный интерфейс под задачу, единицы измерения или цепочка вызовов — СНАЧАЛА делегируй исследование этому агенту, ПОТОМ пиши код. Агент возвращает сжатую выжимку, а основная задача при необходимости перепроверяет спорные детали рефлексией по libs/kompas-interop/*.dll. Триггеры: «нужна сигнатура …», «какие значения enum …», «каким интерфейсом сделать X через API КОМПАС», «что возвращает <метод>», «как через COM API построить …», «найди в справке КОМПАС». НЕ для построения геометрии в КОМПАС и НЕ для KsAPI (Qt/C++ — в MD-базе его нет)."
```
Replace the copied provider-specific sentence in `developer_instructions`:
```diff
-Тебя вызывают, чтобы основная модель (Opus) не тратила контекст на поиск:
+Тебя вызывают, чтобы основная задача не тратила контекст на поиск:
```
Keep the remaining SDK database structure, lookup rules, response contract, and reflection warning unchanged.
- [ ] **Step 3: Parse the TOML and verify the selected Codex settings**
Run:
```powershell
@'
import pathlib
import tomllib
path = pathlib.Path('.codex/agents/kompas-sdk-research.toml')
data = tomllib.loads(path.read_text(encoding='utf-8'))
assert data['name'] == 'kompas-sdk-research'
assert data['model'] == 'gpt-5.6-terra'
assert data['model_reasoning_effort'] == 'medium'
assert data['sandbox_mode'] == 'read-only'
assert data['description']
assert data['developer_instructions']
print('Codex agent configuration: OK')
'@ | python -
```
Expected: `Codex agent configuration: OK`.
- [ ] **Step 4: Check for stale Claude terminology and unintended changes**
Run:
```powershell
$matches = rg -n "Haiku|Opus|Claude" '.codex/agents/kompas-sdk-research.toml'
if ($LASTEXITCODE -eq 0) { $matches; throw 'Stale Claude terminology remains' }
if ($LASTEXITCODE -ne 1) { throw 'ripgrep failed' }
git diff --check
git diff --exit-code -- '.claude/agents/kompas-sdk-research.md'
git diff -- '.codex/agents/kompas-sdk-research.toml'
```
Expected: no stale-term output, `git diff --check` succeeds, the Claude-file diff is empty, and the final diff contains only the planned Codex-agent changes.
- [ ] **Step 5: Commit the Codex agent and implementation plan**
Run:
```powershell
git add -- '.codex/agents/kompas-sdk-research.toml' 'docs/superpowers/plans/2026-07-17-codex-sdk-research-agent.md'
git commit -m "chore: настроить Codex-агент поиска SDK"
```
Expected: one commit containing only the Codex agent definition and this implementation plan. Existing unrelated workspace changes remain unstaged.
@@ -0,0 +1,132 @@
# Дизайн: ассоциативная привязка диаметрального и радиального размеров к геометрии вида (API7)
**Дата:** 2026-05-27
**Статус:** дизайн согласован (вариант A — флаг `associate`), спайк проведён, ревью Codex спека учтено — к реализации
## Правки по ревью Codex (спек)
- **#3/#4** объём сужен до **окружностей** (`IDrawingContainer.Circles`): спайк подтвердил привязку
только к окружности. Дуги (`Arcs`) — следующий инкремент со своим спайком (привязка `IArc` как
`BaseObject` для диам./радиального не проверена; дуги одного родителя неотличимы по центру+радиусу).
- **#1** вид резолвится один раз и приводится к обоим контейнерам: `ISymbols2DContainer` (создание
размера) И `IDrawingContainer` (поиск окружности). Новый helper `RequireViewContainers(viewNumber)`
`(IView, ISymbols2DContainer, IDrawingContainer)`.
- **#2** весь блок после `…Dimensions.Add()` (присвоение `BaseObject`/`Angle`/`AutoNominalValue`,
`Update`, `Valid`, read-back) — внутри того же `try/catch` с `Delete` при любом сбое (как свободный путь).
- **#5** тест доказывает ассоциативность: ключ поиска `radius=10.3` (в допуске находит окружность R10)
→ associate вернёт Ø=20 (из геометрии), свободный дал бы 2·10.3=20.6; `Value≈20` (не 20.6) доказывает
чтение из геометрии. + диагностический read-back `BaseObject != null`.
- **#6** обе ветки (свободная/associate) конвертируют `angle` через `DimensionAngles.ToRadians(angleDeg)`
перед присвоением `dim.Angle`.
- **#7** пост-инвариант значения (`value > 0 && double.IsFinite`) сохраняется и в associate-ветке (иначе откат).
- **#8** регресс-тесты свободного пути (`associate=false`) для ОБОИХ — диаметрального и радиального.
## Цель
Веха 2D-ЧЕРТЁЖ, инкремент 7. Размеры пока «свободные» (значение из заданных координат). Ассоциативная
привязка делает диаметральный/радиальный размер **связанным с проекцией геометрии**: значение (Ø/R)
читается из спроецированной окружности/дуги вида через `BaseObject` и обновляется вместе с моделью.
Первый шаг к «умному» чертежу.
## Объём (обоснован рефлексией interop)
Только **диаметральный** и **радиальный**, привязка только к **окружности** (`IDrawingContainer.Circles`)
у `IDiametralDimension`/`IRadialDimension` есть свойство `BaseObject`; `_Circle` реализует
`IDrawingObject`, поэтому объект из коллекции присваивается `dim.BaseObject` (подтверждено спайком).
У `ILineDimension` `BaseObject` НЕТ; угловой требует `BaseObject1/2`; привязка к дуге (`Arcs`) не
проверена — всё это отдельные инкременты (вне объёма).
## Спайк: механизм подтверждён вживую (НЕ разучивать)
Спайк (`_SpikeAssocDim`, прогнан на реальном КОМПАС, затем удалён): цилиндр (окружность R10, выдавлен
на 20) → стандартные виды. Один из видов содержит спроецированную окружность.
```
IDrawingContainer dc = (IDrawingContainer)view; // QI от вида (как ISymbols2DContainer)
ICircles circles = dc.Circles; // Count=1; circles[0] as ICircle → Xc=0,Yc=0,R=10
IDiametralDimension dia = symbols.DiametralDimensions.Add();
dia.BaseObject = circles[0]; // IDrawingObject; Xc/Yc/Radius НЕ задаём
dia.Angle = π/4; // направление выноски (размещение)
dia.Update(); // True, Valid=True
double v = ((IDimensionText)dia).NominalValue; // == 20 — ДИАМЕТР, прочитан из геометрии!
```
**Проверено:** `BaseObject=circle` без задания координат → `Update=True`, `Valid=True`,
диаметральный `NominalValue=20` (=2·R геометрии), радиальный `NominalValue=10` (=R). `BaseObject`
остаётся непустым после. Геометрия вида: `IDrawingContainer.Circles` (`ICircle`: `Xc/Yc/Radius`),
`.Arcs` (`IArc`: `Xc/Yc/Radius/Angle1/Angle2`), `.LineSegments`. **Нет** «find by point» — выбор
перебором коллекции.
## MCP-инструменты (расширение существующих, вариант A)
К `drawing_add_diametral_dimension` и `drawing_add_radial_dimension` добавляется параметр
`associate=false`:
| `associate` | Поведение |
|---|---|
| `false` (по умолчанию) | Текущее «свободное»: `Xc/Yc/Radius` задаются напрямую (без изменений, обратная совместимость). |
| `true` | `(xc,yc,radius)`**ключ поиска**: найти в виде окружность/дугу с центром ≈ (xc,yc) и радиусом ≈ radius, привязать `BaseObject`; значение (Ø/R) читается из геометрии. |
Координаты ключа — view-local (= координаты модели при масштабе 1:1; при ином масштабе — масштабированные,
как у прочих размеров). `angle` — направление выноски (°), как сейчас. `viewNumber` — как сейчас.
## Выбор объекта
В `IDrawingContainer` нет «find by point». Перебираем `Circles`, собираем кандидатов `(Xc, Yc, Radius)`,
ищем подходящий по ключу `(xc, yc, radius)`:
- кандидат подходит, если `dist(центр, ключ) ≤ tol` И `|R radius| ≤ tol` (`tol = 1.0 мм`, константа);
- ровно 1 подходящий → привязываем; 0 → ошибка «окружность не найдена рядом с (xc,yc) R≈radius»;
>1 → ошибка «неоднозначно — уточните координаты/радиус».
Радиус в ключе снимает неоднозначность концентрических окружностей (цековка R5/R10 — ключ R10
выберет только внешнюю). Логика отбора — чистая (unit-тест), перечисление COM — в сервисе.
## Архитектура
- `DrawingService.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angleDeg, associate, ct)` и
`AddRadialDimensionAsync(..., associate, ct)` — добавлен `associate` (по умолчанию `false`). При `false`
текущая ветка без изменений. Обе ветки: `angleRad = DimensionAngles.ToRadians(angleDeg)`.
- При `associate=true`: `RequireViewContainers(viewNumber)` → собрать кандидатов из `Circles`
(`CollectCircles`) → `CircularObjectMatch.SelectMatchIndex(...)` → взять `IDrawingObject` по индексу →
`symbols.{Diametral|Radial}Dimensions.Add()`**внутри `try/catch`+`Delete`**: `dim.BaseObject = obj`,
`dim.Angle = angleRad`, `AutoNominalValue=true``Update()` (FALSE→откат) → `Valid` (false→откат) →
`value = NominalValue` (из геометрии); `value<=0 || !IsFinite` → откат → `DrawingDimensionResult`.
- Новый helper `RequireViewContainers(viewNumber)` → `(IView View, ISymbols2DContainer Symbols,
IDrawingContainer Drawing)` — резолвит вид один раз, оба QI с раздельной диагностикой (переиспользует
`FindView`).
- Новый файл `src/Kompas.Mcp.Core/Drawings/CircularObjectMatch.cs`: чистый статический
`record CircleCandidate(double Xc, double Yc, double Radius)` + `int SelectMatchIndex(IReadOnlyList<CircleCandidate>
candidates, double keyX, double keyY, double keyR, double tol)` — возвращает индекс единственного
совпадения; `InvalidOperationException` при 0 (нет) / >1 (неоднозначно).
- Сбор кандидатов из COM (Circles в список с сохранением ссылки на `IDrawingObject` + геометрии) — в
сервисе (private helper `CollectCircles(IDrawingContainer)``List<(IDrawingObject Obj, CircleCandidate Geom)>`).
- `DrawingDimensionResult` переиспользуется (значение из геометрии). Свободный путь не трогаем.
## Валидация
- Оба пути: `RequireFiniteCoords(xc, yc, angleDeg)` + `RequirePositiveRadius(radius)` (есть). В
associate-режиме radius — положительный ключ поиска.
- `CircularObjectMatch.SelectMatchIndex` — чистая логика отбора (unit).
## Тестирование
### Unit (`CircularObjectMatchTests`)
- Единственное совпадение по центру+радиусу → индекс.
- Нет кандидатов в допуске → `InvalidOperationException` (нет).
- Два совпадения в допуске → `InvalidOperationException` (неоднозначно).
- Концентрические (один центр, разный R): ключ по R выбирает нужный (не неоднозначно).
- Пустой список → `InvalidOperationException`.
### Integration (`DrawingAssociativeDimensionTests`; модель — цилиндр R10, как в спайке)
1. **Диаметральный associative + доказательство геометрии**: ключ `(0,0,10.3)` (radius в допуске
находит окружность R10) → `Value ≈ 20` (Ø из геометрии, НЕ 2·10.3=20.6 — доказывает чтение из
геометрии); попадание в вид (`DiametralDimensions.Count` +1).
2. **Радиальный associative**: `(0,0,10.3)``Value ≈ 10` (R из геометрии, не 10.3).
3. **Нет окружности рядом**: ключ далеко от геометрии → `InvalidOperationException`; счётчик не вырос (откат).
4. **Свободный путь не сломан** — диаметральный `associate=false` `(xc,yc,15)``Value ≈ 30`.
5. **Свободный путь не сломан** — радиальный `associate=false` `(xc,yc,12)``Value ≈ 12` (регресс обоих).
6. **Нет видов** (associate) → понятная ошибка.
## Дальнейшее (вне спека)
- Ассоциативная привязка углового (`BaseObject1/2` — пара отрезков) и шероховатости (`IRough.BaseObject`);
линейный — `BaseObject` отсутствует (нужен иной механизм). Рамка/формат листа; выноски/базы/допуски формы.
@@ -0,0 +1,182 @@
# Дизайн: радиальный (R) и угловой размеры на виде чертежа через API7
**Дата:** 2026-05-27
**Статус:** реализовано и проверено (5 радиальных + 8 угловых интеграционных + 9 unit; всего 240 тестов зелёных); ревью Codex спека учтено
## Уточнение по реализации (важно)
Спайк изначально решил, что измеряемый сектор выбирает положение дуги `(x3,y3)`. **При реализации
выяснилось иначе:** какой угол измерять, выбирает **`angleType`** (`ksADMinAngle`/`MaxAngle`/`MoreAngle`),
а `(x3,y3)` лишь позиционирует размерную дугу и задаёт её радиус. (270° в спайке возникал потому, что
`X3/Y3` не были заданы = совпадали с вершиной → вырождение.)
Три типа дают **три разных угла** (проверено на лучах 0° и 45°):
- `Min` (`ksADMinAngle`) → **45°** (острый);
- `Max` (`ksADMaxAngle`) → **135°** (тупой супплемент, 180−45);
- `More` (`ksADMoreAngle`) → **315°** (рефлексный, 360−45).
Тесты (#8 More→270 на 90°, Max→135 на 45°) и описания инструмента/сервиса исправлены под это.
## Правки по ревью Codex (реализация)
- **#1** описание параметра `x3,y3` в инструменте переформулировано (точка позиционирует дугу; угол
выбирает `angleType`, не она).
- **#2** сообщение пост-проверки углового больше не винит `(x3,y3)` — указывает на коллинеарность сторон.
- **#3** добавлен интеграционный тест `angleType=Max` (лучи 0°/45° → 135°) — покрыл маппинг `ksADMaxAngle`,
отличный от `Min`/`More`.
- **#4** (откат радиального не тестируется) — **отклонено с обоснованием:** как у диаметрального
(`radius>0` → всегда `Valid`, `value=radius>0`; детерминированного пост-Add сбоя нет), код отката
структурно идентичен протестированному угловому (коллинеарный путь, счётчик не меняется).
## Правки по ревью Codex (спек)
- **#1** угловой: добавлен `RequireDistinctPoints(x1,y1,x2,y2)` (совпавшие точки сторон → одинаковые
лучи → вырождение ещё до `Add`).
- **#3** добавлен тест отката после `Add`: коллинеарные сонаправленные лучи (вершина(0,0),(10,0),(20,0))
→ угол ≈0 → пост-проверка `value<=0` → откат `Delete` + ошибка; счётчик угловых не изменился.
- **#2** поведение `x3,y3` (выбор сектора) задокументировано в описании инструмента; добавлен
интеграционный тест выбора сектора (та же геометрия, `x3,y3` в дополнительном секторе → ~270°).
Большой/тупой угол НЕ отвергаем — это легитимный результат (рефлексный угол через `more`/положение дуги).
- **#4** добавлены тесты на несуществующий `viewNumber>0` (ветка `FindView` «вид не найден») для обоих.
- **#5** `DrawingDimensionResult.Value` — XML-док станет единице-нейтральным («значение размера»);
ответ `drawing_add_angular_dimension` форматируется в градусах (°), не «мм».
- **#6** радиальный использует `DimensionAngles.ToRadians(angleDeg)` (как диаметральный), не ручное `·π/180`.
## Цель
Веха 2D-ЧЕРТЁЖ, инкремент 5. После линейного и диаметрального размеров — **радиальный (R)** и
**угловой** размеры на виде. Завершают базовое семейство размеров чертежа. Оба переиспользуют
проверенный путь `ISymbols2DContainer` (QI от `IView`) и паттерн «Add → set → Update → проверка
`Valid` → чтение `NominalValue` → откат `Delete` при ошибке».
## Спайк: поведение подтверждено вживую (НЕ разучивать)
Спайк (`_SpikeDimensions`, прогнан на реальном КОМПАС, затем удалён) на коробке 40×30×20 с
тремя стандартными видами. Контейнер размеров — `(ISymbols2DContainer)view`.
### Радиальный
```
IRadialDimension rd = symbols.RadialDimensions.Add();
rd.Xc = 10; rd.Yc = 10; rd.Radius = 15; rd.Angle = π/4; // Angle — РАДИАНЫ (направление выноски)
rd.DimensionType = true; // BOOL: на значение НЕ влияет (стиль)
((IDimensionText)rd).AutoNominalValue = true;
rd.Update(); // True, rd.Valid == True
double value = ((IDimensionText)rd).NominalValue; // == 15 — РАДИУС (не диаметр!)
```
**Ключевой факт (расходится со справкой SDK):** у радиального `NominalValue` возвращает **радиус**
(`Radius=15 → NominalValue=15`), а НЕ диаметр. Проверено для `DimensionType` и `true`, и `false`
оба `Valid`, значение одинаковое (BOOL влияет лишь на стиль «от центра / нет», не на величину).
Координаты — локальная СК вида, мм. Размер «свободный» (по `Xc/Yc/Radius`, без `BaseObject`).
### Угловой
```
IAngleDimension ad = symbols.AngleDimensions.Add(DrawingObjectTypeEnum.ksDrADimension); // =10
ad.Xc = 0; ad.Yc = 0; // вершина угла
ad.X1 = 10; ad.Y1 = 0; // точка на стороне 1 → луч (Xc,Yc)→(X1,Y1)
ad.X2 = 0; ad.Y2 = 10; // точка на стороне 2 → луч (Xc,Yc)→(X2,Y2)
ad.X3 = 5; ad.Y3 = 5; // положение размерной дуги — ЗАДАЁТ ИЗМЕРЯЕМЫЙ СЕКТОР + радиус дуги
ad.DimensionType = ksADMinAngle; // 0=min / 1=max / 2=more180
((IDimensionText)ad).AutoNominalValue = true;
ad.Update(); // True, ad.Valid == True
double value = ((IDimensionText)ad).NominalValue; // == 90 — ГРАДУСЫ
```
**Проверено:**
- вершина(0,0), точки (10,0)&(0,10), `X3/Y3`=(5,5) → **90°** ✓;
- вершина(0,0), точки (10,0)&(10,10), `X3/Y3`=(8,4) → **45°** ✓.
- **`X3/Y3` критичен:** без него тот же угол измерился как **270°** (дополнительный сектор). Точка
положения дуги задаёт, какой из двух секторов меряется; заодно радиус дуги пересчитывается в
`dist(вершина, X3/Y3)` (заданный `Radius` перетирается → его не экспонируем).
- Подход через `Angle1/Angle2` (углы наклона сторон) — **вырожденный** (точки схлопываются к
вершине, `X1≈0.001`), не используем. Геометрию ведут только координаты точек.
- `NominalValue` — в **градусах**.
`Add(DrawingObjectTypeEnum)` требует код типа: `ksDrADimension = 10` (обычный угловой),
`ksDrABreakDimension = 39` (с обрывом — вне объёма).
## MCP-инструменты
| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_add_radial_dimension` | `xc,yc` (центр окружности), `radius`, `angle=0` (°), `viewNumber=0` | Радиальный размер (R) окружности/дуги с центром (xc,yc) и радиусом `radius` (ЛСК вида, мм). `angle` — направление выноски в ГРАДУСАХ (внутри → радианы). `viewNumber`: 0 = первый/главный. Значение (= радиус) измеряется автоматически. Возвращает радиус и номер вида. |
| `drawing_add_angular_dimension` | `xc,yc` (вершина), `x1,y1` (точка на стороне 1), `x2,y2` (точка на стороне 2), `x3,y3` (положение размерной дуги — задаёт сектор), `angleType="min"`, `viewNumber=0` | Угловой размер между лучами (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) (ЛСК вида, мм). `x3,y3` должны лежать в измеряемом секторе. `angleType`: min/max/more. Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида. |
Углы выноски (`angle`) экспонируются в **градусах** (LLM-дружелюбно), внутри `·π/180`.
## Архитектура
В существующем `DrawingService` (рядом с `AddDiametralDimensionAsync`); переиспользуем
`RequireSymbols2DContainer(viewNumber)`, `DrawingDimensionResult`, паттерн отката.
- `DrawingService.AddRadialDimensionAsync(viewNumber, xc, yc, radius, angleDeg, ct)`.
- `DrawingService.AddAngularDimensionAsync(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, angleType, ct)`.
- Инструменты `drawing_add_radial_dimension`, `drawing_add_angular_dimension` в `DrawingTools`.
- Helpers `GetViewRadialDimensionCountAsync`, `GetViewAngularDimensionCountAsync` (для тестов —
попадание в вид; размеры в `ISymbols2DContainer.RadialDimensions/AngleDimensions.Count`, не в
`IView.ObjectCount`).
- Новый файл `src/Kompas.Mcp.Core/Drawings/AngleDimensionType.cs`: `enum AngleDimensionType {Min,Max,More}`
+ static `AngleDimensionTypes.Parse(string)` / `ToKompas(...)` (→ `ksADMinAngle/ksADMaxAngle/ksADMoreAngle`),
по образцу `DimensionOrientations`.
`DrawingDimensionResult` (Value + ViewNumber) переиспользуется: радиальный `Value`=радиус, угловой
`Value`=градусы.
## Валидация (чистые static, unit-тест)
- Радиальный: `RequireFiniteCoords(xc, yc, angleDeg)` + `RequirePositiveRadius(radius)` — уже есть.
- Угловой: `RequireFiniteCoords(xc,yc,x1,y1,x2,y2,x3,y3)` (8 координат) + `RequireDistinctPoints`
четырежды: вершина≠(x1,y1), вершина≠(x2,y2), вершина≠(x3,y3) (иначе дуга нулевого радиуса),
(x1,y1)≠(x2,y2) (#1: совпавшие точки сторон → одинаковые лучи). Все уже есть; новых валидаторов не требуется.
## Реализация
**`AddRadialDimensionCore`** (зеркало диаметрального, но значение = радиус): валидация → `RequireSymbols2DContainer`
`RadialDimensions.Add()` (null-check) → `Xc/Yc/Radius`, `Angle=DimensionAngles.ToRadians(angleDeg)` (#6), `DimensionType=true`
(стиль R от центра, явно), `AutoNominalValue=true``Update()` (FALSE → откат `Delete`+ошибка) →
`Valid` (false → откат) → `value = NominalValue` (радиус); `value<=0 || !IsFinite` → откат+ошибка →
`DrawingDimensionResult{Value, ViewNumber}`.
**`AddAngularDimensionCore`**: валидация → `RequireSymbols2DContainer``AngleDimensions.Add(ksDrADimension)`
(null-check) → `Xc/Yc/X1/Y1/X2/Y2/X3/Y3`, `DimensionType=AngleDimensionTypes.ToKompas(angleType)`,
`AutoNominalValue=true``Update()` (FALSE → откат) → `Valid` (false → откат) → `value=NominalValue`
(°); `value<=0 || !IsFinite` → откат+ошибка с подсказкой (вырожденный угол: стороны коллинеарны или
`x3,y3` вне сектора) → `DrawingDimensionResult{Value, ViewNumber}`. Угол в (0,360); защиту по верхней
границе не форсируем (КОМПАС нормализует, `more` может дать >180).
RCW точечно не освобождаем (консистентно с линейным/диаметральным; долг v2-2).
## Тестирование
### Unit (`DrawingDimensionTypeTests` — новый, либо в `DrawingValidationTests`)
- `AngleDimensionTypes.Parse`: `min/max/more` (+ рус. синонимы) → enum; неизвестное → `ArgumentException`.
- `AngleDimensionTypes.ToKompas`: маппинг трёх значений → `ksADMinAngle/ksADMaxAngle/ksADMoreAngle`.
- (`ToRadians`, `RequirePositiveRadius`, координаты — уже покрыты.)
### Integration (`DrawingRadialTests`, `DrawingAngularTests`; наследуют `IntegrationTestBase`)
Радиальный:
1. **Значение = радиус**: `radius=12 → Value ≈ 12` (промотированный спайк).
2. **Попадание в вид**: `RadialDimensions.Count` целевого вида +1 (адресация по `viewNumber`).
3. **Радиус ≤ 0**`ArgumentOutOfRangeException` (до `Add`).
4. **Нет видов** → понятная ошибка.
Угловой:
5. **Измеряет 90°**: вершина(0,0), (10,0)&(0,10), x3/y3=(5,5) → `Value ≈ 90`.
6. **Измеряет 45°**: вершина(0,0), (10,0)&(10,10), x3/y3=(8,4) → `Value ≈ 45`.
7. **Попадание в вид**: `AngleDimensions.Count` целевого вида +1.
8. **Выбор угла типом** (#2): `angleType=More` на геометрии 90° → `Value ≈ 270`; `angleType=Max`
на лучах 0°/45° → `Value ≈ 135` (тупой супплемент). Какой угол измерять, выбирает `angleType`, не `x3,y3`.
9. **Вырожденный угол до Add** (вершина совпадает с точкой стороны) → `ArgumentException`.
10. **Откат после Add** (#3): коллинеарные сонаправленные лучи (0,0),(10,0),(20,0) → угол ≈0 →
`InvalidOperationException`; `AngleDimensions.Count` целевого вида НЕ изменился (откат `Delete`).
11. **Несуществующий `viewNumber>0`** (#4) → понятная ошибка (ветка `FindView`).
12. **Нет видов** → понятная ошибка.
(Радиальный — аналогично добавлен тест на несуществующий `viewNumber>0`, #4.)
## Дальнейшее (вне спека)
- Ассоциативная привязка размеров к геометрии вида (`BaseObject`/`BaseObject1/2`); рамка/формат листа;
текстовые обозначения (шероховатость `Roughs`, допуски формы `Tolerances`, выноски `Leaders`,
тех. требования); радиальный с изломом (`BreakRadialDimensions`), угловой с обрывом (`ksDrABreakDimension`).
@@ -0,0 +1,133 @@
# Дизайн: формат и ориентация листа чертежа (drawing_set_sheet_format) через API7
**Дата:** 2026-05-27
**Статус:** дизайн согласован, спайк проведён, ревью pi/glm-5.1 + pi/kimi-k2.6 учтено — к реализации
## Правки по ревью реализации (Codex + pi/glm-5.1 + pi/kimi-k2.6)
- **K1 (kimi, Major)** — опасение, что для `User` read-back ориентации устаревший (VerticalOrientation
не задаётся). **Снято спайком:** КОМПАС САМ выводит `VerticalOrientation` из соотношения W/H
(500×300→альбомная, 300×500→книжная), игнорирует заданный флаг и НЕ свопает W/H → read-back верный,
код корректен. Документировано комментарием; добавлены тесты на `Landscape` для User (обе ориентации).
- **K2**`ValidateFormatDimensions` теперь называет точный нарушивший параметр (`width`/`height`).
- **C1/G1/K3** — тесты User проверяют `Landscape`. **C2/G3** — тест `sheetNumber=0``ArgumentOutOfRangeException`.
**C3** — A1 в `Parse`-тесте. **G2** — тест A4 landscape (своп 297×210).
- Отклонено (нит): **G4** алиасы в описании (lenient parse — намеренно), **G5** сообщение при NaN
(для стандартного «размеры только для user» приемлемо), **K5** тип исключения для стандартного формата (ArgumentException
для «неверная комбинация» vs AOORE для «вне диапазона» — намеренное различие, на нём держатся тесты),
**G6** «нет unit для ValidateFormatDimensions» — ложно, покрыто в `DrawingValidationTests`.
## Правки по ревью спека (pi/glm-5.1 + pi/kimi-k2.6)
- **A** для `user` флаг `landscape` **игнорируется** — ориентация задаётся соотношением `width`/`height`
(КОМПАС сам выставит `VerticalOrientation`). `VerticalOrientation=!landscape` ставим ТОЛЬКО для
стандартных форматов (избегаем неоднозначного свопа W/H). Задокументировано в описании инструмента.
- **B/E** `width`/`height` для стандартного формата запрещены: валидатор `ValidateFormatDimensions`
(для `User` — оба конечны и `>0`; для стандартного — оба `==0`, иначе `ArgumentException` «размеры
только для user»).
- **C** read-back формата возвращает дружелюбное имя через новый `PaperFormats.FromKompas(ksEnum)`
`SheetFormatResult.Format` = `"A3"`/`"User"` (симметрия вход/выход), а не COM-имя `"ksFormatA3"`.
- **D** `RequireLayoutSheet(sheetNumber)`: `sheetNumber>0` (`ArgumentOutOfRangeException`) + null-check
после `ItemByNumber[sheetNumber]` (`InvalidOperationException` «нет листа N»); + интеграционный тест.
- **F** read-back `fmt2 = sheet.Format` с null-check.
- **G** `SheetFormatResult``public sealed record` с `public required … { get; init; }` (как
`DrawingDimensionResult`/`DrawingViewsResult`).
- **H** интеграционные сравнения размеров — через `Assert.InRange` (допуск на float).
- **I** члены `PaperFormat`с XML-`<summary>` (как `RoughSignType`).
- **J** `FormatMultiplicity` оставляем на COM-дефолте (1) — отмечено в реализации.
- **K** (верхняя граница `width`/`height`) — отклонено, в OPEN_QUESTIONS как будущее упрочнение
(консистентно с `RequirePositiveRadius` — верхней границы нет).
## Цель
Веха 2D-ЧЕРТЁЖ, инкремент 8. Задать **формат** (A0–A5 или пользовательский) и **ориентацию**
(книжная/альбомная) листа активного чертежа. Фундамент «правильного» по ГОСТ чертежа (сейчас лист
всегда дефолтный A4 книжный).
## Спайк: механизм подтверждён вживую (НЕ разучивать)
Спайк (`_SpikeSheetFormat`, прогнан на реальном КОМПАС, затем удалён): новый чертёж →
`doc.LayoutSheets.ItemByNumber[1]` (`ILayoutSheet`) → `sheet.Format` (`ISheetFormat`).
```
ISheetFormat fmt = sheet.Format;
fmt.Format = ksDocumentFormatEnum.ksFormatA3; // A0=0..A5=5, User=6
fmt.VerticalOrientation = false; // true=книжная (portrait), false=альбомная (landscape)
sheet.Update(); // True
// для стандартного формата FormatWidth/Height пересчитываются АВТО (A3 landscape → 420×297)
```
**Проверено:**
- Дефолт нового чертежа: `ksFormatA4`, `VerticalOrientation=true`, 210×297, `FormatMultiplicity=1`.
- A3 landscape: `Update=True`, read-back `Format=ksFormatA3`, `Vertical=False`, **W=420, H=297 (авто)**
для стандартных форматов размеры не задаём, их даёт enum+ориентация.
- A4 portrait: 210×297.
- Пользовательский: `Format=ksFormatUser` + `FormatWidth=500` + `FormatHeight=300` + `Update` → применён.
`ksDocumentFormatEnum`: `ksFormatA0=0`, `A1=1`, `A2=2`, `A3=3`, `A4=4`, `A5=5`, `ksFormatUser=6`.
`ISheetFormat`: `Format` (enum), `VerticalOrientation` (bool), `FormatWidth`/`FormatHeight` (double, мм),
`FormatMultiplicity` (int).
## MCP-инструмент
| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_set_sheet_format` | `format="A4"` (A0\|A1\|A2\|A3\|A4\|A5\|user), `landscape=false` (false=книжная), `width=0`,`height=0` (ТОЛЬКО для `user`, мм, >0), `sheetNumber=1` | Задать формат и ориентацию листа `sheetNumber` активного чертежа. Стандартный формат — размеры авто (по enum+ориентации), `width`/`height` для него запрещены (должны быть 0). `user` — задать `width`/`height` (>0); для `user` флаг `landscape` ИГНОРИРУЕТСЯ (ориентация — из соотношения сторон). Возвращает итоговый формат («A3»/«User»), ширину, высоту, ориентацию (read-back). |
## Архитектура
В `DrawingService` (namespace `Kompas.Mcp.Core.Drawings`).
- `SetSheetFormatAsync(PaperFormat format, bool landscape, double width, double height, int sheetNumber, ct)`
`SheetFormatResult`.
- Helper `RequireLayoutSheet(sheetNumber)``ILayoutSheet`: `sheetNumber>0`
(`ArgumentOutOfRangeException`) → нет активного чертежа / `LayoutSheets` null / `ItemByNumber[sheetNumber]`
null («нет листа N») — раздельная диагностика, рядом с `RequireActiveStamp`.
- Новый файл `src/Kompas.Mcp.Core/Drawings/PaperFormat.cs`: `enum PaperFormat {A0,A1,A2,A3,A4,A5,User}`
(каждый член с XML-`<summary>`) + `PaperFormats.Parse(string)` / `ToKompas(PaperFormat)→ksDocumentFormatEnum`
/ `FromKompas(ksDocumentFormatEnum)→PaperFormat` (для read-back дружелюбного имени), по образцу `RoughSignTypes`.
- Новый файл `src/Kompas.Mcp.Core/Drawings/SheetFormatResult.cs`:
`public sealed record SheetFormatResult { public required string Format; public required double Width;
public required double Height; public required bool Landscape; }` (стиль `DrawingDimensionResult`).
## Реализация
`SetSheetFormatCore`: `DrawingValidation.ValidateFormatDimensions(format, width, height)`
`PaperFormats.ToKompas(format)``RequireLayoutSheet(sheetNumber)``fmt = sheet.Format` (null-check) →
`fmt.Format = ksEnum`; if `format==User`: `fmt.FormatWidth=width`, `fmt.FormatHeight=height` (ориентацию
НЕ трогаем — задаётся W/H); else: `fmt.VerticalOrientation = !landscape` (размеры авто) → `sheet.Update()`
(FALSE → ошибка; объект уровня листа, перезапись идемпотентна — отката `Delete` нет) → read-back
`fmt2 = sheet.Format` (null-check) → `SheetFormatResult { Format = PaperFormats.FromKompas(fmt2.Format)
.ToString(), Width = fmt2.FormatWidth, Height = fmt2.FormatHeight, Landscape = !fmt2.VerticalOrientation }`.
`FormatMultiplicity` оставляем на COM-дефолте (1) — не читаем и не задаём. RCW точечно не освобождаем
(консистентно; долг v2-2).
## Валидация (чистые static, unit)
- `PaperFormats.Parse``A0..A5`/`user` (+ регистр/пробелы); неизвестное → `ArgumentException`; null → `ArgumentNullException`.
- Новый `DrawingValidation.ValidateFormatDimensions(PaperFormat format, double width, double height)`:
для `User``width` и `height` оба конечны и `> 0` (иначе `ArgumentOutOfRangeException`); для стандартного —
`width==0 && height==0` (иначе `ArgumentException` «размеры задаются только для format=user»).
- `PaperFormats.ToKompas`/`FromKompas` (↔ `ksDocumentFormatEnum`) НЕ покрываем unit — граница проекта
(тест не ссылается на `Kompas6Constants`), как у прочих enum-маппингов; покрываются интеграцией.
## Тестирование
### Unit (`PaperFormatsTests`, `DrawingValidationTests`)
- `PaperFormats.Parse`: `A4/a3/ A2 /a0/a5/user` → enum; неизвестное → `ArgumentException`; null → `ArgumentNullException`.
- `ValidateFormatDimensions`:
- `User`: бросает на (0,300)/(500,0)/(1,300)/(NaN,300)/(500,∞); пропускает (500,300).
- стандартный (`A4`): бросает на (500,0)/(0,300)/(500,300); пропускает (0,0).
### Integration (`DrawingSheetFormatTests`; сравнения размеров — `Assert.InRange`)
1. **A3 landscape**: `format=A3, landscape=true``Format=="A3"`, `Width≈420`, `Height≈297`, `Landscape==true`.
2. **A4 portrait** (`landscape=false`): → `Format=="A4"`, `Width≈210`, `Height≈297`, `Landscape==false`.
3. **User 500×300**: `format=user, width=500, height=300``Format=="User"`, `Width≈500`, `Height≈300`.
4. **User без размеров** (`width=0`) → `ArgumentOutOfRangeException` (до COM).
5. **Размеры для стандартного** (`format=A3, width=400`) → `ArgumentException` (до COM).
6. **Несуществующий лист** (`sheetNumber=99`) → `InvalidOperationException`.
7. **Активный документ не чертёж** (деталь) → понятная ошибка.
## Дальнейшее (вне спека)
- Рамка/основная надпись по конкретному ГОСТ-стилю оформления (`LayoutLibraryFileName`/`LayoutStyleNumber`),
несколько листов (`LayoutSheets.Add`); привязка к дуге, угловой/шероховатость; выноски/базы/допуски формы.
@@ -0,0 +1,167 @@
# Дизайн: текстовые обозначения чертежа — шероховатость, текст, тех. требования (API7)
**Дата:** 2026-05-27
**Статус:** дизайн согласован (все три в одном инкременте), спайк проведён, ревью Codex спека учтено — к реализации
## Правки по ревью Codex (спек)
- **#4** параметр `height` у `drawing_add_text` **убран:** `IDrawingText.Height` — высота блока
форматирования, НЕ размер шрифта (шрифт задаётся на уровне `ITextItem`, вне объёма). Текст ставится
стилем по умолчанию. (Снимает и #10 — валидация height не нужна.)
- **#3/#11** сервис возвращает значение, **прочитанное обратно из COM** после `Update` (rough —
`RoughParamText.Str`; text — `((IText)dt).Str`) — как `NominalValue` у размеров. Тест `Value==вход`
доказывает round-trip. Пост-Add откат `Delete` есть в коде; не форсируется тестом (для валидных
параметров COM всегда `Valid` — как решено у диаметрального; путь идентичен протестированному угловому).
- **#1** `value` шероховатости нормализуется (`null`/whitespace → `""`); возвращается нормализованное
(read-back), контракт `Value` непустой не нарушается.
- **#2/#5/#7** добавлены null/QI-guards: `Roughs`/`(IRoughParams)`/`RoughParamText`;
`(IDrawingContainer)`/`DrawingTexts`/`(IText)`; `(IDrawingDocument)`/`TechnicalDemand`/`Text`
каждый с раздельной диагностикой (как `RequireStampCell`/`RequireSymbols2DContainer`).
- **#6** тесты текста дополнены: многострочный round-trip (`\n`), несуществующий `viewNumber>0`.
- **#8** добавлен helper `ReadTechnicalRequirementsAsync` — тест читает `td.Text.Str` и проверяет
перезапись (как `DrawingStampTests` перечитывают графы).
- **#9** подсчёт строк тех. требований нормализован: `text.Replace("\r\n","\n")`, хвостовые пустые
строки отбрасываются (`"a\n"` → 1 строка).
## Цель
Веха 2D-ЧЕРТЁЖ, инкремент 6. После завершения базового семейства размеров — **текстовые обозначения**:
знак шероховатости, свободная текстовая надпись, технические требования. Три инструмента покрывают
самые частые «не-размерные» элементы конструкторского чертежа.
## Спайк: все три механизма подтверждены вживую (НЕ разучивать)
Спайк (`_SpikeAnnotations`, прогнан на реальном КОМПАС v24, затем удалён) на коробке 40×30×20 с
тремя стандартными видами.
### Шероховатость — на виде (`ISymbols2DContainer.Roughs`)
```
IRough rough = symbols.Roughs.Add(); // без параметров
rough.BranchX0 = 20; rough.BranchY0 = 25; // положение знака (ЛОКАЛЬНАЯ СК вида, мм)
rough.Angle = 0; // угол наклона оси знака (градусы)
IRoughParams rp = (IRoughParams)rough; // QI (как IDimensionText у размеров)
rp.SignType = ksRoughSignEnum.ksDeleteMaterial; // тип знака
rp.RoughParamText.Str = "Ra 1.6"; // значение (Ra/Rz) — текст
rough.Update(); // True, rough.Valid == True
```
**Проверено:** `Update=True`, `Valid=True`, `RoughParamText.Str` round-trip = `"Ra 1.6"`, `Roughs.Count`
→ 1. `ksRoughSignEnum`: `ksNoProcessingType=0` (без указания обработки), `ksDeleteMaterial=1` (с
удалением слоя материала), `ksWithoutDeleteMaterial=2` (без удаления). Положение — свободные координаты
(`BranchX0/Y0`); `BaseObject` (привязка к контуру) не задаём — будущее.
### Свободный текст — на виде (`IDrawingContainer.DrawingTexts`, НЕ Symbols!)
```
IDrawingContainer dc = (IDrawingContainer)view; // ВНИМАНИЕ: текст в контейнере геометрии,
IDrawingText dt = dc.DrawingTexts.Add(); // а НЕ в ISymbols2DContainer
dt.X = 30; dt.Y = 45; dt.Angle = 0; // точка привязки (ЛОКАЛЬНАЯ СК вида, мм)
((IText)dt).Str = "Образец надписи"; // содержимое (QI к IText), \n — многострочно
dt.Update(); // True, dt.Valid == True
```
**Проверено:** `Update=True`, `Valid=True`, `Str` round-trip, `DrawingTexts.Count`**2** (вид уже
содержал 1 текст — авто-подпись вида; проверять по ДЕЛЬТЕ before+1, не по абсолюту). `ObjectCount`
4→4 — текст **НЕ** входит в `IView.ObjectCount` (как и размеры) → проверять `DrawingTexts.Count`.
### Технические требования — на уровне ДОКУМЕНТА (`IDrawingDocument.TechnicalDemand`)
```
IDrawingDocument dd = (IDrawingDocument)doc; // QI от активного IKompasDocument2D
ITechnicalDemand td = dd.TechnicalDemand; // единый блок на документ
td.Text.Str = "1. Общие допуски по ГОСТ 30893.1.\n2. Острые кромки притупить."; // \n — строки
td.Update(); // True
```
**Проверено:** `IsCreated` False→True (первый `Text.Str`+`Update()` создаёт блок), `Update=True`, текст
с `\n` сохранён построчно. Объект уровня документа (не вида), единственный, над основной надписью.
`Str` замещает содержимое (как у штампа) → инструмент `set` (перезапись).
## MCP-инструменты
| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_add_rough` | `x,y` (положение знака), `value=""` (Ra/Rz, напр. "Ra 1.6"), `signType="delete"` (delete\|without\|none), `angle=0` (°), `viewNumber=0` | Поставить знак шероховатости на виде. Положение `x,y` в ЛСК вида (мм). `value` — текст параметра (пусто = знак без значения). Возвращает значение и номер вида. |
| `drawing_add_text` | `x,y` (точка привязки), `text`, `angle=0` (°), `viewNumber=0` | Поставить свободную текстовую надпись на виде стилем по умолчанию. `text` — содержимое (`\n` — многострочно). Возвращает текст (read-back) и номер вида. |
| `drawing_set_technical_requirements` | `text` | Задать технические требования активного чертежа (единый блок над штампом; перезаписывает прежние). `text` — строки через `\n`. Возвращает число строк. |
## Архитектура
В существующем `DrawingService` (namespace `Kompas.Mcp.Core.Drawings`). Шероховатость/текст —
per-view; тех. требования — per-document.
- `AddRoughAsync(viewNumber, x, y, value, signType, angleDeg, ct)``DrawingAnnotationResult`.
Введём `record DrawingAnnotationResult { string Value; int ViewNumber }` (универсальный для rough/text —
`Value`=строка, прочитанная обратно из COM; `ViewNumber`). Тех. требования возвращают число строк (int).
- `AddTextAsync(viewNumber, x, y, text, angleDeg, ct)``DrawingAnnotationResult`.
- `SetTechnicalRequirementsAsync(text, ct)``int` (число строк).
- `ReadTechnicalRequirementsAsync(ct)``string` (для теста — `td.Text.Str`).
- Helpers: `RequireSymbols2DContainer` (есть, для rough), новый `RequireDrawingContainer(viewNumber)`
= guarded `(IDrawingContainer)FindView(...)` (для text), новый `RequireDrawingDocument()` =
guarded `(IDrawingDocument)RequireActiveDrawing()` (для тех. требований). Все QI с раздельной диагностикой.
- Счётчики для тестов: `GetViewRoughCountAsync`, `GetViewTextCountAsync`.
- Новый enum-файл `RoughSignType.cs`: `enum RoughSignType {NoProcessing, DeleteMaterial, WithoutDeleteMaterial}`
+ `RoughSignTypes.Parse(string)`/`ToKompas(...)` (→ `ksRoughSignEnum`), по образцу `AngleDimensionTypes`.
- Новый файл `DrawingAnnotationResult.cs`.
- Инструменты в `DrawingTools.cs`.
## Валидация (чистые static, unit-тест)
- `RequireFiniteCoords` (есть) — координаты rough/text.
- Новый `RequireNonEmptyText(string, paramName)` — для `text` (drawing_add_text, тех. требования).
`value` шероховатости НЕ обязателен (знак без значения допустим; `null`/whitespace → `""`).
- `RoughSignTypes.Parse` — разбор строки (unit-тест; `ToKompas``ksRoughSignEnum` НЕ покрываем unit —
граница проекта, как у `AngleDimensionTypes`/`DimensionOrientations`).
## Реализация
**`AddRoughCore`**: нормализация `value` (`null`/whitespace → `""`) → `RequireFiniteCoords(x,y,angleDeg)`
`RequireSymbols2DContainer``symbols.Roughs` (null-check) → `.Add()` (null-check) → `BranchX0/Y0/Angle`
`(IRoughParams)rough` (null-check QI) → `SignType=ToKompas`, `rp.RoughParamText` (null-check) `.Str = value`
`Update()` (FALSE → откат `Delete`) → `Valid` (false → откат) → читаем `rp.RoughParamText.Str` обратно
`DrawingAnnotationResult{Value=read-back, ViewNumber}`.
**`AddTextCore`**: `RequireNonEmptyText(text)` + `RequireFiniteCoords(x,y,angleDeg)`
`RequireDrawingContainer``dc.DrawingTexts` (null-check) → `.Add()` (null-check) → `X/Y/Angle`
`(IText)dt` (null-check QI) `.Str = text``Update()` (FALSE → откат `Delete`) → `Valid` (false → откат)
→ читаем `((IText)dt).Str` обратно → `DrawingAnnotationResult{Value=read-back, ViewNumber}`.
**`SetTechnicalRequirementsCore`**: `RequireNonEmptyText(text)` → нормализация `text.Replace("\r\n","\n")`
`RequireDrawingDocument()``dd.TechnicalDemand` (null-check) → `td.Text` (null-check) `.Str = text`
`td.Update()` (FALSE → ошибка; объект уровня документа — отката `Delete` НЕ делаем, перезапись идемпотентна)
→ вернуть число непустых-после-trim хвоста строк (`TrimEnd('\n').Split('\n').Length`).
**`RequireDrawingContainer(viewNumber)`** = `FindView``(IDrawingContainer)view` (null → «вид не приводится
к IDrawingContainer»). **`RequireDrawingDocument()`** = `RequireActiveDrawing()``(IDrawingDocument)doc`
(null → «чертёж не приводится к IDrawingDocument»).
RCW точечно не освобождаем (консистентно с остальным `DrawingService`; долг v2-2).
## Тестирование
### Unit (`RoughSignTypesTests`, `DrawingValidationTests`)
- `RoughSignTypes.Parse`: `delete/without/none` (+ рус. синонимы) → enum; неизвестное → `ArgumentException`; null → `ArgumentNullException`.
- `RequireNonEmptyText`: бросает на null/пусто/пробелы; пропускает непустое.
### Integration (`DrawingRoughTests`, `DrawingTextTests`, `DrawingTechReqTests`; наследуют `IntegrationTestBase`)
Шероховатость:
1. **Ставится + round-trip**: `value="Ra 1.6"`, `signType=delete``Value=="Ra 1.6"` (read-back из COM),
попадание в целевой вид (`Roughs.Count` +1, адресация по `viewNumber`).
2. **Знак без значения**: `value=""` → ставится (Valid), `Roughs.Count` +1.
3. **Нет видов** → понятная ошибка. 4. **Несуществующий `viewNumber>0`** → ошибка.
Текст:
5. **Ставится + round-trip**: `text``Value==text` (read-back), `DrawingTexts.Count` +1 (по ДЕЛЬТЕ —
вид уже мог содержать подпись), адресация по целевому `viewNumber`.
6. **Многострочный** (`"строка1\nстрока2"`) → `Value` сохраняет обе строки (round-trip `\n`).
7. **Пустой текст**`ArgumentException` (до Add). 8. **Нет видов** → ошибка. 9. **Несуществующий `viewNumber>0`** → ошибка.
Тех. требования:
10. **Задаются + round-trip**: многострочный текст → `lines==2`; `ReadTechnicalRequirementsAsync` возвращает
тот же текст; повторный вызов с другим текстом перезаписывает (read-back = новый, без накопления).
11. **Пустой текст**`ArgumentException`. 12. **Активный документ не чертёж** → понятная ошибка.
## Дальнейшее (вне спека)
- Привязка шероховатости/выносок к геометрии (`BaseObject`); выноски (`Leaders`), обозначения баз
(`Bases`), допуски формы (`Tolerances`); рамка/формат листа; ассоциативная привязка размеров.
@@ -0,0 +1,342 @@
# Дизайн навыка `kompas-fdm-design` — проектирование деталей под FDM-печать
| Поле | Значение |
|------|----------|
| **Статус** | design (учтены 3 ревью: pi/glm-5.1, pi/kimi-k2.6, Codex CLI) |
| **Создан** | 2026-05-27 |
| **Тип** | новый навык (`.claude/skills/kompas-fdm-design/`) |
| **Источники** | консультации pi (`glm-5.1`, `kimi-k2.6`), ревью pi (`glm-5.1`, `kimi-k2.6`) + Codex CLI; прецеденты — навыки `kompas-3d`, `orcaslicer` |
## 1. Цель
Навык-**методика**: как проектировать (и доводить) детали так, чтобы они **хорошо печатались на
FDM/FFF** — без поддержек где возможно, с нужной прочностью на нагрузку, рабочими посадками и
компенсацией особенностей послойной печати. Модель строится через MCP-сервер КОМПАС-3D (этот
проект) с опорой на навык `kompas-3d`; новый навык отвечает на вопрос **«как спроектировать, чтобы
напечаталось»**, а не «чем строить».
## 2. Зафиксированные решения (locked)
1. **Отдельный навык**`kompas-fdm-design`, не сливается в `kompas-3d`.
2. **Без слайсера** — навык **не ссылается** на слайсер и не делает слайс-петлю. Слайсинг вне границ.
3. **Границы = методика DFM + лёгкая самопроверка геометрии** существующими инструментами осмотра
MCP. **Никаких новых MCP-инструментов**, никакого thickness/overhang-солвера.
4. **Универсальный FDM.** Правила **масштабируются** по `w` (ширина линии ≈ диаметр сопла) и `h`
(высота слоя); часть значений — **абсолютные эмпирические мм** (компенсация отверстий, зазоры
посадок, фаски, инсёрты, коробление), они помечены как **калибруемые тестом**. «Параметризация»
не означает, что все числа — формулы от `w`/`h`.
### Принцип проекта соблюдён
`MCP = возможности SDK (чем делать)`, `навык = методика (как делать)`. Правила DFM — методика,
в MCP не идут. Навык опирается на уже реализованные инструменты построения и осмотра.
## 3. Параметризация
- **`w` (ширина линии)** — для горизонтальных размеров/стенок. Дефолт: сопло 0.4 → `w ≈ 0.40.45 мм`.
- **`h` (высота слоя)** — для вертикали и поведения нависаний/мостов. Дефолт `h ≈ 0.5·сопло`
(0.2 мм); структурная печать `h = 0.20.25 мм`.
- **`θ_max` (предельный угол самонесущей поверхности от вертикали)** — **первоклассный параметр**.
Физически `θ_max ≈ arctan(w / 2h)` (≈45° при `w`=0.4, `h`=0.2). **Дефолт 45°** (PLA, хороший
обдув); **40°** для PETG/ABS или толстого слоя (`h≥0.3` → θ_max падает до ~34°). Все правила
нависаний/teardrop/фасок/зенковок берут угол **из `θ_max`**, не хардкодят 45°.
- **Материал** — модификатор: PLA (дефолт, стабилен), PETG (эластичнее, мосты хуже, посадки
«расслабляются», +0.05 мм/сторону к скользящим, θ_max ниже), ABS (усадка/коробление, +зазор,
скругления углов; корпус/обдув — настройки печати, см. границу §12).
- **Калибровочная памятка**: точные числа зависят от калибровки потока/притирки первого слоя/обдува;
навык даёт **разумные дефолты + «проверь печатным тестом»**, а не гарантии.
## 4. Структура файлов
```
.claude/skills/kompas-fdm-design/
├── SKILL.md ← диспетчер (progressive disclosure)
└── references/
├── fdm-rules.md ← полный численный свод DFM
└── geometry-audit.md ← рецепты самопроверки + честные границы
```
### 4.1 `SKILL.md` (диспетчер)
- **Frontmatter**: `name`, `description` с триггерами (§7).
- **Когда применять / связь с `kompas-3d`**: kompas-3d = чем строить; этот навык = как
спроектировать под печать. Слайсер не упоминается.
- **Калибровка**: сопло → `w`; высота слоя → `h``θ_max`; материал → поправки.
- **Два ключевых правила** (§5), включая **рецепт-опрос** (направление нагрузки, косметические/
критичные грани) перед фиксацией ориентации.
- **Рабочий цикл** (§6), **чек-лист печатнопригодности** (§10) — компактно.
- **Ссылки** на `references/`. Если SKILL.md перерастает ~180 строк — выносить детали в references
(держать диспетчер лёгким).
### 4.2 `references/fdm-rules.md` — §8. ### 4.3 `references/geometry-audit.md` — §9.
## 5. Два ключевых правила навыка
1. **Ориентация печати — первое проектное решение.** До эскизов:
- **Спроси у пользователя** (если не задано): главное направление рабочей нагрузки и
косметические/критичные грани. Агент **не выводит путь нагрузки из геометрии** — его задаёт
задача.
- Реши, как деталь стоит на столе: ось Z = направление роста слоёв. От ориентации зависит: где
нависания; куда смотрят отверстия (вертикальные по Z → компенсация диаметра; горизонтальные в
XY → teardrop); путь несущей нагрузки (**держи нагрузку в плоскости XY, вдоль слоёв** —
межслойная прочность по Z ниже, см. §8.6; **Z-сжатие допустимо, Z-растяжение/срез — нет**);
плоскости сопряжения (на XY-гранях верх/низ, не на Z-боковинах); «лесенка» на наклонных/
криволинейных функциональных поверхностях (§8.8); если поддержки неизбежны — чтобы опорные
грани были некритичными/скрытыми.
- Зафиксируй ориентацию и проектируй под неё.
2. **Чек-лист печатнопригодности перед выдачей.** DFM-чек-лист (§10) + лёгкий гео-аудит (§9), и
только потом экспорт. Связка с `kompas-3d`: сначала **`validate_part`** (деталь *валидна*), затем
**FDM-чек-лист** (деталь *печатнопригодна*) — разные проверки. **Гео-аудит эвристический и не
доказывает печатнопригодность** (не ловит путь нагрузки/анизотропию — §9).
## 6. Рабочий цикл
1. **Калибровка**: сопло → `w`; слой → `h``θ_max`; материал → поправки.
2. **Ориентация** (правило 1): опрос (нагрузка/косметика) → постановка на стол, ось слоёв,
плоскости сопряжения, учёт «лесенки» и поддержек.
3. **Правила эскиза/операции** (через `kompas-3d`): стенки `n·w`; нависания → фаски/скос под
`θ_max`; горизонтальные отверстия → teardrop; вертикальные → компенсация диаметра; фаска у
основания; зазоры посадок; заходные фаски; мин. элементы/текст; бобышки/инсёрты/защёлки.
4. **Гео-аудит** (§9).
5. **Предусловия экспорта**: единое тело/манифолд (`boolean_union` при необходимости) →
`validate_part` чисто.
6. **Чек-лист** (§10) → экспорт.
## 7. Триггеры (для `description`)
«сделай деталь печатнопригодной / под FDM», «спроектируй … под печать», «напечатается ли без
поддержек?», «подбери зазоры для печатной посадки», «как ориентировать деталь под печать», «почему
деталь плохо печатается / где будут нависания», «доведи деталь под FDM».
**НЕ для:** механики построения через MCP (это `kompas-3d`); **слайсинга/нарезки/g-code** (вне
границ, отдельный инструмент слайсинга — по имени не называем); поиска по справке SDK (субагент
`kompas-sdk-research`).
## 8. Свод правил DFM (`references/fdm-rules.md`)
> Сведено из двух консультаций (glm-5.1 + kimi-k2.6) и выверено тремя ревью. Где источники
> расходились — взят выверенный дефолт + «калибровать тестом». Числа для `w≈0.40.45`, `h≈0.2`
> (сопло 0.4). **Зазоры — на сторону (радиальные)**; диаметральный = 2× табличного. **Угол нависания
> — от вертикали** (вертикаль=0°, горизонталь=90°); самонесущие — ≤ `θ_max`.
### 8.1 Стенки и оболочки
- Толщина стенки = **`n · w`**. Мин. конструктивная — **2·w (~0.8 мм)**; несущая — **≥3·w**.
- **Маппинг стенка→периметры:** нужно `N` периметров ⇒ стенка **`N·w`** (при `w`=0.45: 3 пер. =
1.35, 4 = 1.8, 5 = 2.25 мм).
- **Не задавай толщину стенки, не кратную `w`** (напр. 0.6 при `w`=0.45): слайсер оставит зазор или
переэкструдирует → наплыв/размер «уехал». Прыгай на следующий кратный.
- **Caveat:** правило `n·w` — для **конструктивных** стенок; если толщина задана внешней
функциональной величиной (флексура, тепловой барьер, посадочный размер) — она важнее кратности.
- Одиночная стенка `1·w` — только декоративная. Узкий сквозной прорез — **≥2·w (~0.8 мм)**.
### 8.2 Нависания, полки, мосты (разделять!)
- **Самонесущие — поверхности ≤ `θ_max` от вертикали.** 45–60° (при дефолте) — печатается с
падением качества; **> `θ_max` существенно — поддержки** → избегать редизайном.
- **90°-полка (консоль, опора с одной стороны) НЕ печатается ни на какой длине** — это не «≤6 мм»,
а ~0 (один слой провисает). Любую горизонтальную полку: **скос под `θ_max`**, либо **превратить в
мост** (две опоры), либо поддержка.
- **Мост (bridge) — пролёт между двумя опорами на одной высоте.** При достаточном обдуве, `h≈0.2`,
консервативно (для ненастроенного слайсера): **PLA ~1525 мм, PETG ~1015 мм, ABS ~1218 мм**.
Длинные прямоугольные проёмы **ориентировать так, чтобы мост шёл по короткой стороне**; концы
моста — на сплошных опорах.
- Функциональную нижнюю поверхность моста закладывать с припуском **0.20.3 мм** на провис.
*Граница:* величину провиса (мм) из CAD не предсказать (зависит от обдува/скорости — настройки
печати); припуск — ориентир, не гарантия.
- **Внутренние/потолочные нависания хуже наружных** (нет обдува) — потолок пазов делать аркой/
шевроном, не плоским пролётом > 2 мм.
- **Доступ к поддержкам:** если поддержка во внутренней полости неизбежна — окно доступа **≥810 мм**.
### 8.3 Отверстия
- **Вертикальные (ось ∥ Z)** печатаются уже номинала → **увеличить диаметр модели** (радиус на
половину): **+0.2 мм (Ø<4)**, **+0.20.3 мм (Ø 410)**, **+0.10.2 мм (Ø>10)**; калибровать,
критичные — рассверливать.
- **Горизонтальные (ось в XY)****teardrop** или **D-отверстие** (плоский верх). Мин. Ø **2 мм**.
Круглая часть тоже печатается уже → **+0.10.2 мм** к её Ø.
- **Геометрия teardrop (выверено всеми ревью):** боковины касательны окружности под углом `θ_max`
к вертикали (с двух сторон), сходятся в вершине на вертикальной оси. Высота вершины над центром
= **`r / sin θ_max`**; включённый угол при вершине = **`2·θ_max`**. При `θ_max`=45° →
`r/sin45° = √2·r ≈ 1.414·r` над центром (= **`0.414·r` над верхом окружности**), включённый угол
90°. Низ — оставшаяся дуга окружности. (Проверка вывода: расстояние от центра до боковой прямой,
проходящей через вершину `(0, d)` с направлением `θ_max` от вертикали, равно `d·sin θ_max`;
приравнивая к `r`, получаем `d = r/sin θ_max`.)
- **Глухое отверстие:** дно — внутренний мост; делать **толщину дна ≥2–3 мм** или купольное/
вентилируемое. Сквозные предпочтительнее.
- **Отступ от края:** не «2·Ø», а через **остаточную перемычку** — стенка между отверстием и краем
**≥2–3·w** (лёгкая нагрузка) / больше под крепёж.
### 8.4 Посадки и зазоры (печатная деталь ↔ печатная деталь)
Зазор **на сторону** (радиальный); диаметральный = 2× значения:
| Посадка | Зазор/сторону | Примечание |
|---|---|---|
| **Натяг (press)** | **−0.05…0 мм** (вычесть из номинала!) | короткий, PLA; иначе snap-fit (§8.14) |
| Переходная/плотная | 0.05–0.15 мм | |
| Скользящая | 0.150.20 мм | PLA↔PLA; контакт ≥20 мм → 0.20; PETG +0.05 |
| Свободная | 0.250.35 мм | >0.35/сторону — уже очень слабо |
- **Знак:** «натяг» = **отрицательный** зазор → диаметр вала +/отверстия − относительно номинала.
Положительное число в строке press — ошибка прочтения.
- **ABS↔ABS:** +0.05/сторону. **PETG:** прессовые со временем «расслабляются».
- **Допуск точности (НЕ прибавлять к посадкам):** общий размерный разброс FDM — **XY ±0.2 мм**
(±0.1 на калиброванной), **Z хуже (±0.2…)**. Это точность изготовления, а не добавка к зазору;
критичные сопряжения проектировать на худшую ось.
### 8.5 Первый слой / контакт со столом
- **Elephant foot** — раздутие нижнего периметра от **притирки первого слоя** (низкий Z-offset/
переэкструзия; НЕ от высоты слоя). Снимать **фаской по нижним рёбрам**: **0.3 × 45° (калибровано)**
/ **0.51.0 × 45° (слабая калибровка / сильная притирка)**.
- Внутренние углы у основания — **скругление ≥R0.5**.
- **Опорная площадка:** без «лезвийных» оснований; контакт хотя бы в ~3 периметра. Высокие тонкие
детали — **интегральный фланец 1–2 мм** как геометрия (предпочтительнее всякого brim).
### 8.6 Ориентация и прочность
- **Z (межслойная) прочность от XY:** PLA ~4055%, PETG ~3550%, **ABS ~2035% (выброс)**.
Несущую нагрузку — в **XY (вдоль слоёв)**.
- **Z-сжатие допустимо** (слои в сжатии не расслаиваются); избегать **Z-растяжения и Z-среза**.
- Изгиб: слои в растяжении/сжатии, не на срез по линии слоя.
- Если Z-нагрузка неизбежна — **увеличить несущее сечение** (площадь, работающую на нагрузку)
ориентировочно ×2 относительно XY-расчёта.
- Плоскости сопряжения — на **XY-гранях** (глаже/точнее), не на Z-боковинах.
### 8.7 Минимальные элементы и текст
- Выступ/штифт/ребро — **≥1·w (≥0.5 мм)**; паз/щель — **≥2·w (~0.8 мм)**.
- **Аспект тонких выступов:** высота ≤ ~4–5× базовой ширины; выше — конусность/раскос/редизайн.
- **Выпуклый** текст: штрих **≥0.5 мм**, высота **≥2·h (~0.4 мм)**, sans-serif bold.
- **Гравированный** текст: штрих **≥2·w (~0.80.9 мм)** (нужно ≥2 периметра; 0.6 мм не влезает),
глубина **≥2·h (~0.4 мм)**. (pt не используем — моделируем геометрию в мм.)
### 8.8 «Лесенка» (staircase) — критерий ориентации
- Наклонные/криволинейные поверхности дают ступени слоёв: глубина ≈ **`h / tan(α)`** (α — угол от
горизонтали). Пример: α=30°, `h`=0.2 → ~0.35 мм.
- Критичные (скользящие/уплотняющие/оптические) поверхности **ориентировать вертикально или
горизонтально**, не под малым углом. Это вход в правило ориентации (§5).
- Большой плоский **верх** без опоры коробит («подушка») — внутренние рёбра каждые ~15–20 мм или
достаточная толщина верха.
### 8.9 Бобышки, инсёрты, резьба
- **Саморез/винтонарезной** пилот (M3): ~Ø2.5 PLA / Ø2.6 PETG / Ø2.7 ABS; заход ≥3 мм.
- **Термоинсёрт латунный** (M3): бор **по даташиту** (типично ~Ø4.0, ±0.05); **`Ø_bore = OD_инсёрта
(0…0.1) мм`** (≤ OD, лёгкий натяг под расплав — НЕ больше OD, иначе нет удержания); стенка бобышки
**≥2 мм**; глубина = длина инсёрта + 0.5 мм; **ставить с верхней (Z) грани** (не в боковину).
- **Бобышка под винт (с инсёртом или само-нарезом):** OD ≥ 2–3× Ø винта; не делать массивный
сплошной объём (карман/оболочка).
- **Сквозное под металлический болт:** радиальный зазор 0.2–0.3 → **+0.4–0.6 мм к номиналу** болта.
- **Резьба:** не моделировать <M6 → инсёрты/саморезы. Если моделировать: **≥M6, ось вертикальная**,
зазор +0.1–0.2 мм, профиль крупный/трапецеидальный (не мелкий ISO — вершины-нависания).
### 8.10 Фаски vs скругления
- **Нижние (у стола) рёбра — фаска** (скругление даёт нулевой контакт + EF).
- **Верхние рёбра — скругление** (R0.52.0); **но** радиус **> ~½ толщины стенки** сам создаёт зону
нависания > `θ_max` → тогда фаска/ступень.
- **Внутренние углы — всегда скругление ≥R0.5**.
### 8.11 Зенковки / цековки
- **Цековка (counterbore)** — большим Ø/полостью **вверх** (дно по телу, не мостом); глубина +0.3 мм.
- **Зенковка (countersink):** конус **вверх**; при включённом угле **≤90°** стенки ≤45° от вертикали
— печатается; **>90°** стенки становятся нависанием → поддержка или замена цилиндрической цековкой.
(Не путать с «узким» конусом — он печатается, но как посадка под головку бесполезен.)
### 8.12 Заходные фаски (assembly relief)
- На штифтах, отверстиях, инсёртах, защёлках, «ласточкиных хвостах» — **заходная фаска** (≈0.51 мм
× 45° или ≈ половина зазора) против задиров при первом контакте/сборке.
### 8.13 Разбиение детали и сборка из печатных частей
- Если оптимальная по прочности ориентация конфликтует с беспод­держечностью, или деталь крупная/
коробится — **разбить на части** с самоустанавливающимися стыками (печатные штифты/шпонки/замки),
склейка; стыки — на XY-гранях. Зазор стыка — по §8.4.
### 8.14 Защёлки (snap-fit) / живой шарнир
- Консольная защёлка: толщина балки **≥23·w (≈1.02.0 мм)**, зацеп/возврат **0.30.8 мм**,
отношение длина/толщина **≥5:1** (до 10:1), **скругление в основании ≥R0.5** (концентратор).
- **Направление слоёв:** балка должна гнуться **в плоскости XY** (слои перпендикулярны изгибу), **не
поперёк Z** (расслоится с первого нажатия).
- **Живой шарнир** — только PLA/PP-подобные, толщина перемычки **0.30.5 мм**; PETG/ABS не годятся.
### 8.15 Коробление (геометрия)
- Большие плоскости (>80×80, особенно ABS): скругления углов R3–5 + рёбра/решётка снизу.
- Радиус внешних углов: R2 (ABS) / R1 (PLA/PETG).
- Длинные тонкие пролёты (>60 мм, <2 мм) — рёбра/косынки каждые 30–40 мм; высота ребра ≤5× базы.
- Избегать сплошных кубов/плит → карман/оболочка + рёбра. Усадка: PLA ~0.3%, PETG ~0.5%, ABS ~0.8%.
- Симметрия геометрии уравновешивает усадку.
- *«Мышиные уши» (Ø8–10 мм пятна по углам)***крайняя мера адгезии** (по сути brim-геометрия,
ближе к настройке печати); предпочтительно интегральный фланец/скругления углов.
- *Граница:* стол/корпус/обдув для ABS — **настройки печати**, вне навыка; здесь только геометрия.
### 8.16 Полые детали и общая гигиена модели
- **Полости:** дренаж Ø35 мм у **низшей** точки + вент у **высшей**.
- Допуски/зазоры — **в геометрию** (слайсер читает модель буквально, «доводчика» нет).
- Раздельные тела — зазор ≥0.2 мм (это общая CAD-гигиена; перед выдачей объединять — §6 шаг 5).
## 9. Гео-аудит (`references/geometry-audit.md`)
Лёгкая самопроверка инструментами осмотра MCP. **Что проверяемо и границы:**
| Проверка | Как | Статус |
|---|---|---|
| Нависания (приближённо) | `list_faces`/`describe_face`: для **нижних** граней угол поверхности от вертикали; >`θ_max` → флаг | ✅ плоские; ⚠️ криволинейные грубо |
| Ориентация (геом. прокси) | `get_bounding_box`: как ось слоёв соотносится с габаритом | ⚠️ длинная ось ≠ путь нагрузки |
| Горизонтальные круглые отверстия | `describe_face`: цилиндр с горизонтальной осью → «нужен teardrop» | ✅ |
| Номиналы/зазоры/габариты | `measure` между гранями; `get_bounding_box` | ✅ |
| Тело/манифолд перед выдачей | `list_bodies` (одно тело?), `validate_part` | ✅ |
**Граница честности (явно в навыке):**
- **Угол нависания** мерить в **той же конвенции, что §8** (от вертикали; нижняя грань с поверхностью
> `θ_max` от вертикали = нависание) — не путать с углом нормали от горизонтали.
- **Путь нагрузки агент НЕ выводит из габарита** — берёт из задачи/опроса (§5). Длинная ось ≠ несущая.
- **Истинная мин. толщина стенки и полный детект криволинейных нависаний — не решаются** (нет
солвера).
- **Аудит эвристический и НЕ доказывает печатнопригодность** (не ловит анизотропию/путь нагрузки —
главный риск по ревью Codex). Вывод — список флагов для решения, не «приговор». Слайсер навык не
зовёт намеренно.
## 10. Чек-лист печатнопригодности (в `SKILL.md`)
- [ ] Направление нагрузки и косметические грани **получены от пользователя**; ориентация
зафиксирована; нагрузка в XY (или Z только на сжатие); сопряжения на XY-гранях.
- [ ] Стенки кратны `w` (≥2·w; несущие ≥3·w = `N` периметров); нет стенок «не кратных `w`» (кроме
функциональных).
- [ ] Нет 90°-полок; нависания ≤`θ_max` или заменены скосами; мосты в пределах пролёта по короткой
стороне; внутренним поддержкам — доступ.
- [ ] Горизонтальные отверстия — teardrop/D (геометрия из `θ_max`); вертикальные — компенсация Ø;
глухие — дно ≥2–3 мм.
- [ ] Фаска у основания (EF); внутренние углы ≥R0.5; опорная площадка есть.
- [ ] Посадки по таблице со **знаком** (натяг — вычесть); допуск ±0.2 НЕ прибавлен к зазору;
заходные фаски на сопряжениях.
- [ ] Мин. элементы/текст ≥ порогов; аспект тонких выступов ≤4–5×.
- [ ] Бобышки/инсёрты (бор ≤OD, с Z-грани)/резьба/защёлки (изгиб в XY) по правилам.
- [ ] Полости — дренаж/вент; критичные поверхности не под «лесенкой»/поддержкой.
- [ ] Предусловия экспорта: единое тело/манифолд; `validate_part` чисто.
## 11. Обкатка (конвенция проекта)
Приёмы доказываем в `usecases/` (полигон, в .gitignore), затем поднимаем в навык — как с `kompas-3d`.
Кандидаты: **UC «FDM-кронштейн без поддержек»** (с нуля; ориентация/нависания/teardrop/защёлка);
**UC «довести деталь под печать»** (реальный материал — Voron, ср. UC-0002).
## 12. Чего НЕ делаем (YAGNI / non-goals)
- Не зовём слайсер, не делаем слайс-петлю.
- Не добавляем DFM-инструменты в MCP (методика, не возможности SDK).
- Не строим thickness/overhang-солвер (аудит эвристический).
- Не привязываем числа к одному принтеру/материалу.
- Не дублируем механику построения из `kompas-3d`.
- **Не лезем в настройки печати/слайсера** (периметры, заполнение, температуры, **обдув/скорость**,
корпус, brim как настройка, высота первого слоя) — навык про **геометрию**; настройки только как
граница.
## 13. Решения по калибровке / открытые вопросы
- **OQ-1 (решено).** Компенсация Ø вертикальных отверстий: +0.2 (<4), +0.20.3 (410), +0.10.2
(>10); прибавлять **к диаметру** (радиус +X/2); калибровать.
- **OQ-2 (решено).** Elephant foot: 0.3×45° (калибровано) / 0.5–1.0×45° (слабая калибровка); причина
— притирка первого слоя, не высота слоя.
- **OQ-3 (решено).** Экспорт — **через `export_step`** (B-rep, манифолд гарантирован КОМПАС).
`export_stl` в MCP **не вводим** (вне границ навыка).
- **OQ-4 (закрыто).** Ревью прогнаны: pi/glm-5.1, pi/kimi-k2.6, Codex CLI — учтены.
## 14. Дальнейшие шаги
1. ~~Ревью pi + Codex~~ (3 ревью учтены).
2. Self-review спека.
3. **Ревью спека пользователем → отмашка.**
4. `writing-plans` → план реализации (SKILL.md + references/, опц. UC-полигон, синхронизация доков
через субагент `docs-maintainer`).
```
@@ -0,0 +1,119 @@
# Дизайн: линия-выноска с текстом (drawing_add_leader) через API7
**Дата:** 2026-05-28
**Статус:** дизайн согласован, спайк проведён, ревью Codex + pi/glm-5.1 + pi/kimi-k2.6 учтено — к реализации
## Правки по ревью реализации (Codex + pi/glm-5.1 + pi/kimi-k2.6)
- Проверка возврата `AddBranchByPoint`/`SetBranchTextPosition` (оба `bool`) — ранняя диагностика
вместо позднего `RPC_E_SERVERFAULT` (Codex, glm).
- RU-алиасы в сообщении `ShelfDirections.Parse` и в `[Description]` инструмента (glm, kimi).
- Интеграционный тест направления полки параметризован `Right/Left/Up/Down` — покрывает всю
поверхность `ToKompas` на реальном COM (glm, kimi).
- Отклонено (обоснование): unit-тест `ToKompas` (граница проекта — тест не ссылается на
`Kompas6Constants`); e2e-тест tool-обёртки (тонкая, `Parse`+сервис покрыты); `angleDeg`
(у `ILeader`/`IBaseLeader` нет свойства Angle — ориентация геометрическая); выравнивание default
`text` у `AddText` (вне объёма; required-контракт `AddLeader` корректнее).
## Правки по ревью спека (Codex + pi/glm-5.1 + pi/kimi-k2.6)
- **Сигнатура** `DrawingValidation.RequireNonEmptyText(text, nameof(text))` (двухаргументный — как есть).
- **QI с диагностикой:** `bl as IBranchs ?? throw`, `bl as ILeader ?? throw` (не прямой cast); read-back
`leader.TextOnShelf?.Str ?? string.Empty`; двухшаговый null-check `Leaders` (коллекция) и `Add()`
как `AddRoughCore`.
- **`ShelfDirections.ToKompas(Auto)` бросает `ArgumentOutOfRangeException`** (Auto обрабатывается в сервисе
ДО вызова — «не задавать ShelfDirection»; прямой вызов для Auto — ошибка). RU-алиасы `Parse`:
`авто/вправо/влево/вверх/вниз`.
- **Вырожденная выноска:** `RequireDistinctPoints(x, y, textX, textY)` (остриё ≠ полка; есть валидатор).
- **Критический порядок** (комментарий в коде): `AddBranchByPoint` ДО `SetBranchTextPosition`/`Update`
(иначе RPC_E_SERVERFAULT — выноска без ветвей).
- **Тесты:** пустой текст — `""` И `" "`; `viewNumber=0` резолвится в первый вид; путь `shelfDirection=auto`
(ShelfDirection не задаётся). XML-`<summary>` членов `ShelfDirection` — «в ЛОКАЛЬНОЙ СК вида».
- Нейминг COM-объекта — `baseLeader` (не `bl`). `GetViewLeaderCountAsync` считает ВСЕ подтипы выносок
(для v1 ок; фильтр — при добавлении ksDrPosLeader/… — в «Дальнейшее»).
## Цель
Веха 2D-ЧЕРТЁЖ, инкремент 9. Поставить на виде **линию-выноску** (leader) с текстовой надписью на полке —
поясняющее обозначение, указывающее стрелкой на элемент (примечание, материал детали, и т.п.).
## Спайк: последовательность подтверждена вживую (НЕ разучивать)
Спайк (`_SpikeLeader`, прогнан на реальном КОМПАС, затем удалён). **Ключ:** только что созданная
выноска имеет **0 ответвлений** — любые операции над ответвлением (`SetSignPosition`) и `Update` на
выноске без ветвей падают с `RPC_E_SERVERFAULT`; сначала нужно добавить ответвление через `IBranchs`.
```
IBaseLeader bl = symbols.Leaders.Add(DrawingObjectTypeEnum.ksDrLeader); // ksDrLeader=20
((IBranchs)bl).AddBranchByPoint(0, x, y); // QI; ответвление 0 — остриё (куда указывает стрелка)
bl.SetBranchTextPosition(textX, textY); // положение полки/текста
((ILeader)bl).TextOnShelf.Str = text; // надпись на полке (.Str — наш interop)
// опц.: ((ILeader)bl).ShelfDirection = ksShelfDirectionEnum.ksLSRight;
bl.Update(); // True, bl.Valid == True
```
**Проверено:** `AddBranchByPoint(0,x,y)``BranchCount=1`; `Update=True`, `Valid=True`,
`TextOnShelf.Str` round-trip; `Leaders.Count` +1. Работает с явной `ShelfDirection` и без неё.
`BaseLeader` реализует `IBaseLeader`/`IDrawingObject`; `IBranchs` и `ILeader` — COM-QI от него.
Интерфейсы: `ILeaders.Add(DrawingObjectTypeEnum)``BaseLeader` (для обычной выноски `ksDrLeader=20`);
`IBranchs.AddBranchByPoint(int index, double x, double y)`; `IBaseLeader.SetBranchTextPosition(x,y)` /
`Update` / `Valid` / `Delete` / `ArrowType` (`ksArrowEnum`); `ILeader.TextOnShelf` (`IText`),
`ShelfDirection` (`ksShelfDirectionEnum`: `ksLSNone=0`/`ksLSRight=1`/`ksLSUp=2`/`ksLSDown=3`/`ksLSLeft=-1`).
## MCP-инструмент
| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_add_leader` | `x,y` (остриё — куда указывает стрелка), `textX,textY` (полка/якорь текста), `text`, `shelfDirection="auto"` (auto\|right\|left\|up\|down), `viewNumber=0` | Поставить выноску на виде: стрелка из полки указывает в (x,y), текст `text` на полке у (textX,textY). Координаты — ЛОКАЛЬНАЯ СК вида (мм). `shelfDirection` — направление полки (auto = по умолчанию КОМПАС). Возвращает текст (read-back) и номер вида. |
## Архитектура
В `DrawingService` (namespace `Kompas.Mcp.Core.Drawings`).
- `AddLeaderAsync(viewNumber, x, y, textX, textY, text, shelfDirection, ct)``DrawingAnnotationResult`
(переиспользуем: `Value`=read-back текст, `ViewNumber`).
- Использует существующий `RequireSymbols2DContainer(viewNumber)` (выноски в `ISymbols2DContainer.Leaders`).
- Новый файл `src/Kompas.Mcp.Core/Drawings/ShelfDirection.cs`: `enum ShelfDirection {Auto,Right,Left,Up,Down}`
(XML-`<summary>` на членах) + `ShelfDirections.Parse(string)` / `ToKompas(ShelfDirection)→ksShelfDirectionEnum`
по образцу `AngleDimensionTypes`. `Auto` → не задавать `ShelfDirection` (оставить дефолт КОМПАС).
- Счётчик `GetViewLeaderCountAsync(viewNumber)` (для теста попадания в вид).
## Реализация
`AddLeaderCore`: `RequireNonEmptyText(text)` + `RequireFiniteCoords(x,y,textX,textY)`
`RequireSymbols2DContainer``symbols.Leaders` (null-check) → `.Add(ksDrLeader)` (null-check) →
**внутри `try/catch`+`Delete`**: `((IBranchs)bl).AddBranchByPoint(0, x, y)` (QI null-check),
`bl.SetBranchTextPosition(textX, textY)`, `((ILeader)bl).TextOnShelf` (null-check) `.Str = text`,
если `shelfDirection != Auto`: `((ILeader)bl).ShelfDirection = ShelfDirections.ToKompas(...)`
`bl.Update()` (FALSE → откат) → `bl.Valid` (false → откат) → read-back `((ILeader)bl).TextOnShelf.Str`
`DrawingAnnotationResult{Value=read-back, ViewNumber}`.
RCW точечно не освобождаем (консистентно; долг v2-2).
## Валидация (чистые static, unit)
- `RequireNonEmptyText(text, nameof(text))`, `RequireFiniteCoords(x,y,textX,textY)`,
`RequireDistinctPoints(x, y, textX, textY)` (остриё ≠ полка) — все уже есть.
- `ShelfDirections.Parse``auto/right/left/up/down` (+ рус. `авто/вправо/влево/вверх/вниз`, регистр/пробелы);
неизвестное → `ArgumentException`; null → `ArgumentNullException`. `ToKompas` (→ `ksShelfDirectionEnum`)
НЕ покрываем unit (граница проекта); бросает `ArgumentOutOfRangeException` на `Auto`.
## Тестирование
### Unit (`ShelfDirectionsTests`)
- `Parse`: `auto/right/left/up/down` (+ регистр, рус.) → enum; неизвестное → `ArgumentException`; null → `ArgumentNullException`.
### Integration (`DrawingLeaderTests`; модель — коробка + стандартные виды)
1. **Ставится + round-trip + viewNumber=0**: `text="Примечание 1"`, `shelfDirection=auto`, `viewNumber=0`
`Value=="Примечание 1"` (read-back), `viewNumber=0` резолвится в первый вид (`r.ViewNumber` совпал),
`Leaders.Count` целевого вида +1.
2. **shelfDirection=left** (не-auto путь) на целевом виде → ставится, `Leaders.Count` +1.
3. **Пустой текст** (`""` и `" "`) → `ArgumentException` (до Add).
4. **Вырожденная** (остриё совпадает с полкой) → `ArgumentException` (до Add).
5. **Нет видов** → понятная ошибка. 6. **Несуществующий `viewNumber>0`** → понятная ошибка.
## Дальнейшее (вне спека)
- Многострочный текст полки / текст под полкой (`TextUnderShelf`); несколько ответвлений
(`AddBranchByPoint` с разными индексами); привязка к геометрии (`IBranchs.SetBaseObject`); тип стрелки
(`ArrowType`); специальные выноски (позиция/клеймо/маркер: `ksDrPosLeader/ksDrBrandLeader/ksDrMarkerLeader`).
Базы (`Bases`), допуски формы (`Tolerances`).
@@ -0,0 +1,96 @@
# Каталог внешнего CAD-контракта для агента — дизайн
## Цель
Создать отдельный документ `docs/AGENT_CAD_TOOL_CATALOG.md`, который описывает желаемый внешний MCP-контракт управления CAD с точки зрения автономного агента, показывает все реализованные и необходимые нереализованные методы и позволяет оценить полноту контракта по сквозным CAD-сценариям.
## Границы
- Каталог описывает внешний контракт, а не внутреннюю реализацию.
- COM-интерфейсы, вызовы API5/API7 и другие детали SDK КОМПАС-3D в итоговую таблицу не включаются.
- SDK разрешено использовать только для внутренней оценки реалистичности предлагаемых методов и выявления платформенных ограничений.
- Методы формулируются как общие CAD-операции, а не как инструменты под отдельную пользовательскую задачу.
- В каталог входят все существующие MCP-инструменты и все выявленные методы, необходимые для полноты агентского контура.
## Структура итогового документа
1. Назначение документа и определение полноты внешнего контракта.
2. Легенда статусов и приоритетов.
3. Сводная матрица покрытия сквозных сценариев.
4. Таблицы методов по функциональным доменам.
5. Приоритизированный перечень пробелов.
6. Общий вывод о текущей полноте контракта.
Функциональные домены:
- сессия и документы;
- 2D-геометрия и эскизы;
- 3D-моделирование;
- прямое и историческое редактирование;
- инспекция, выбор объектов и измерения;
- сборки;
- чертежи и оформление;
- импорт и экспорт;
- управление состоянием, восстановление и надёжность агентской работы.
## Формат таблиц методов
Каждая строка описывает один метод внешнего контракта. Обязательные столбцы:
| Столбец | Содержание |
|---|---|
| Метод | Стабильное имя MCP-метода в `snake_case` |
| Назначение | Краткое описание результата метода с позиции вызывающего агента |
| Статус | `✅ реализован`, `🟡 частично`, `⬜ не реализован` или `⛔ ограничен платформой` |
| Необходимость | `Core`, `Advanced` или `Optional` |
| Пробел / ограничение | Что отсутствует в контракте или какая часть поведения не покрыта |
SDK-интерфейсы, классы сервисов и другие детали реализации в эти таблицы не добавляются.
## Критерии классификации
### Статус
- `✅ реализован` — метод существует в текущем MCP-каталоге и предоставляет заявленное внешнее поведение.
- `🟡 частично` — метод существует, но покрывает только часть необходимого внешнего поведения или поддерживает ограниченный набор вариантов.
- `⬜ не реализован` — метод нужен целевому контракту, но отсутствует как MCP-инструмент.
- `⛔ ограничен платформой` — желаемое поведение невозможно или ненадёжно в доступном Automation API; ограничение должно быть сформулировано на уровне внешнего результата без SDK-подробностей.
### Необходимость
- `Core` — без метода агент не может надёжно завершить базовый сквозной сценарий либо проверить результат мутации.
- `Advanced` — метод нужен для промышленно значимого расширенного сценария, но не блокирует минимальный цикл моделирования.
- `Optional` — повышает удобство, производительность или широту применения, сохраняя работоспособность основного контура без него.
### Приоритет пробела
- `P0` — разрыв базового сквозного сценария или отсутствие необходимой обратной связи/управления состоянием.
- `P1` — существенное ограничение распространённого профессионального сценария.
- `P2` — расширение охвата или удобства без разрыва основных сценариев.
## Оценка полноты
Полнота оценивается не числом методов, а способностью агента выполнить замкнутый цикл `обнаружить состояние → изменить модель → проверить результат → сохранить или безопасно откатить`.
Сводная матрица должна проверить минимум следующие сценарии:
1. Создать и сохранить параметрическую 3D-деталь.
2. Открыть или импортировать модель, локально изменить геометрию и проверить результат.
3. Создать сборку, разместить компоненты, наложить сопряжения и проверить структуру.
4. Создать комплект основных видов чертежа, оформить размеры и обозначения, сохранить результат.
5. Выполнить геометрическую и документную инспекцию без обязательного визуального анализа.
Для каждого сценария фиксируются покрытые этапы, блокирующие пробелы и итоговая оценка: `полный`, `частичный` или `неполный`.
## Проверка реалистичности
Перед включением нереализованного метода в контракт проверяется, что требуемое поведение в принципе доступно в установленной версии КОМПАС-3D либо может быть составлено из надёжных операций. Если реалистичность не подтверждена, метод получает статус `⛔ ограничен платформой` или явную пометку о необходимости технического исследования; детали исследования остаются вне итогового каталога.
## Критерии готовности
- Все текущие MCP-инструменты представлены ровно по одному разу.
- Для каждого домена перечислены необходимые отсутствующие методы.
- Частично реализованные методы не ошибочно помечены как полностью реализованные.
- Сквозные сценарии позволяют увидеть блокирующие пробелы независимо от общего количества методов.
- Итоговый список `P0P2` согласован с доменными таблицами и не содержит методов, отсутствующих в основном каталоге.
- Документ не содержит внутренних COM/SDK-деталей.
@@ -0,0 +1,75 @@
# Codex SDK Research Agent Design
## Goal
Adapt the project-scoped `kompas-sdk-research` custom agent for Codex while
leaving the Claude Code agent unchanged. The Codex agent must be optimized for
fast, read-heavy searches in the local KOMPAS SDK Markdown knowledge base and
must not modify the workspace.
## Configuration
The Codex agent remains in `.codex/agents/kompas-sdk-research.toml` and uses:
```toml
name = "kompas-sdk-research"
model = "gpt-5.6-terra"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
```
`gpt-5.6-terra` fits exploration, documentation scans, and distilled support
work. Medium reasoning is used because SDK questions can require correlating
several interfaces, overloads, constants, and COM call-chain details.
## Agent Responsibilities
The agent searches only local project and installed SDK reference material:
- `docs/Kompas3D_SDK/` is the canonical source for COM API5/API7 documentation.
- SDK headers under the installed KOMPAS SDK may be read when exact constant
values need confirmation.
- The agent must not browse the web, edit files, implement features, or build
geometry in KOMPAS-3D.
- KsAPI is outside the Markdown knowledge base and must be reported as outside
scope instead of guessed.
The parent agent delegates non-trivial SDK lookups before implementation. It
remains responsible for validating interop-specific discrepancies through
reflection over `libs/kompas-interop/*.dll` when needed.
## Prompt Adaptation
Remove Claude-specific references to Haiku and Opus from the Codex TOML. Keep
the existing domain guidance, but express it in provider-neutral Codex terms.
The description must make proactive delegation triggers clear without claiming
that the agent uses tools or model behavior specific to Claude Code.
The response contract remains concise and evidence-oriented:
1. Relevant interface, method, property, enum, or constant.
2. Automation syntax and COM syntax when parameter ordering matters.
3. Parameter and return-value meanings, including units and side effects.
4. Related overloads or interfaces when relevant.
5. Source article path and its `sources` frontmatter entry.
6. A warning when generated .NET interop names may differ from SDK docs.
## Isolation and Compatibility
The Claude definition in `.claude/agents/kompas-sdk-research.md` is not changed.
The two versions intentionally use different configuration formats and model
selection while sharing the same research methodology.
The Codex agent's read-only behavior is enforced by `sandbox_mode`, not merely
requested in prose. No exact Claude-style tool allowlist is copied into the
Codex configuration.
## Verification
After editing the TOML:
- parse it as TOML;
- confirm the required fields and selected model settings;
- confirm `sandbox_mode = "read-only"`;
- search the Codex file for stale `Haiku`, `Opus`, and Claude-specific wording;
- confirm the Claude agent file is unchanged.
@@ -0,0 +1,42 @@
using Kompas6Constants;
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Тип углового размера — какой из двух секторов измерять
/// (обёртка над <see cref="ksAngleDimTypeEnum"/>).</summary>
public enum AngleDimensionType
{
/// <summary>Минимальный (острый) угол.</summary>
Min,
/// <summary>Максимальный (тупой) угол.</summary>
Max,
/// <summary>Угол более 180° (рефлексный).</summary>
More,
}
/// <summary>Маппинг типа углового размера ↔ перечисление КОМПАС и разбор строки.</summary>
public static class AngleDimensionTypes
{
public static ksAngleDimTypeEnum ToKompas(AngleDimensionType type) => type switch
{
AngleDimensionType.Min => ksAngleDimTypeEnum.ksADMinAngle,
AngleDimensionType.Max => ksAngleDimTypeEnum.ksADMaxAngle,
AngleDimensionType.More => ksAngleDimTypeEnum.ksADMoreAngle,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "Неизвестный тип углового размера."),
};
/// <summary>Разобрать строку типа углового размера (англ./рус.) в <see cref="AngleDimensionType"/>.</summary>
public static AngleDimensionType Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
return value.Trim().ToLowerInvariant() switch
{
"min" or "minimal" or "минимальный" or "острый" => AngleDimensionType.Min,
"max" or "maximal" or "максимальный" or "тупой" => AngleDimensionType.Max,
"more" or "more180" or "больше180" or "рефлексный" => AngleDimensionType.More,
_ => throw new ArgumentException(
$"Неизвестный тип углового размера '{value}'. Допустимо: min, max, more.",
nameof(value)),
};
}
}
@@ -0,0 +1,44 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Геометрия окружности-кандидата вида (центр + радиус, локальная СК вида, мм).</summary>
public sealed record CircleCandidate(double Xc, double Yc, double Radius);
/// <summary>
/// Чистый отбор окружности проекционного вида по ключу (центр + радиус) для ассоциативной привязки
/// размера. Выделен из COM-перечисления ради unit-тестов.
/// </summary>
public static class CircularObjectMatch
{
/// <summary>
/// Индекс единственной окружности из <paramref name="candidates"/>, чей центр и радиус совпадают с
/// ключом (<paramref name="keyX"/>,<paramref name="keyY"/>,<paramref name="keyR"/>) в пределах
/// <paramref name="tol"/> (мм): <c>dist(центр,ключ) ≤ tol</c> И <c>|R keyR| ≤ tol</c>.
/// </summary>
/// <exception cref="InvalidOperationException">Нет подходящих (0) или неоднозначно (&gt;1).</exception>
public static int SelectMatchIndex(IReadOnlyList<CircleCandidate> candidates,
double keyX, double keyY, double keyR, double tol)
{
ArgumentNullException.ThrowIfNull(candidates);
var matches = new List<int>();
for (var i = 0; i < candidates.Count; i++)
{
var c = candidates[i];
var dx = c.Xc - keyX;
var dy = c.Yc - keyY;
if (Math.Sqrt(dx * dx + dy * dy) <= tol && Math.Abs(c.Radius - keyR) <= tol)
matches.Add(i);
}
return matches.Count switch
{
1 => matches[0],
0 => throw new InvalidOperationException(
$"Окружность не найдена рядом с ({keyX},{keyY}) R≈{keyR} (допуск {tol} мм). " +
"Проверьте координаты/радиус и наличие окружности на этом виде."),
_ => throw new InvalidOperationException(
$"Неоднозначный выбор окружности у ({keyX},{keyY}) R≈{keyR}: подходит {matches.Count}. " +
"Уточните координаты/радиус."),
};
}
}
@@ -0,0 +1,11 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Результат простановки текстового обозначения (шероховатость/надпись) на виде.</summary>
public sealed record DrawingAnnotationResult
{
/// <summary>Содержимое обозначения, прочитанное обратно из COM (значение Ra/Rz либо текст надписи).</summary>
public required string Value { get; init; }
/// <summary>Номер вида, на котором поставлено обозначение.</summary>
public required int ViewNumber { get; init; }
}
@@ -3,7 +3,7 @@ namespace Kompas.Mcp.Core.Drawings;
/// <summary>Результат простановки размера на чертеже.</summary>
public sealed record DrawingDimensionResult
{
/// <summary>Измеренное значение размера (мм).</summary>
/// <summary>Измеренное значение размера: линейный/диаметральный/радиальный — мм, угловой — градусы.</summary>
public required double Value { get; init; }
/// <summary>Номер вида, на котором поставлен размер.</summary>
+449 -8
View File
@@ -84,6 +84,65 @@ public sealed class DrawingService
?? throw new InvalidOperationException("У листа оформления нет основной надписи (Stamp).");
}
/// <summary>Лист оформления чертежа по номеру (1-based) с диагностикой (номер &gt; 0, лист существует).</summary>
private ILayoutSheet RequireLayoutSheet(int sheetNumber)
{
if (sheetNumber <= 0)
throw new ArgumentOutOfRangeException(nameof(sheetNumber), "Номер листа должен быть > 0.");
var doc2d = RequireActiveDrawing();
var sheets = doc2d.LayoutSheets
?? throw new InvalidOperationException("Не удалось получить листы оформления чертежа.");
return sheets.ItemByNumber[sheetNumber] as ILayoutSheet
?? throw new InvalidOperationException($"На чертеже нет листа оформления #{sheetNumber}.");
}
/// <summary>
/// Задать формат (<paramref name="format"/>: A0A5 или user) и ориентацию листа
/// <paramref name="sheetNumber"/> активного чертежа. Для стандартного формата размеры авто
/// (<paramref name="width"/>/<paramref name="height"/> запрещены); для user — заданные width/height (мм),
/// при этом <paramref name="landscape"/> игнорируется (ориентация из соотношения сторон). Возвращает
/// итоговый формат, размеры и ориентацию (read-back из COM).
/// </summary>
public Task<SheetFormatResult> SetSheetFormatAsync(PaperFormat format, bool landscape, double width,
double height, int sheetNumber = 1, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => SetSheetFormatCore(format, landscape, width, height, sheetNumber), ct);
private SheetFormatResult SetSheetFormatCore(PaperFormat format, bool landscape, double width,
double height, int sheetNumber)
{
DrawingValidation.ValidateFormatDimensions(format, width, height);
var sheet = RequireLayoutSheet(sheetNumber);
var fmt = sheet.Format
?? throw new InvalidOperationException("У листа недоступен формат (Format == null).");
fmt.Format = PaperFormats.ToKompas(format);
if (format == PaperFormat.User)
{
// Для пользовательского КОМПАС САМ выводит VerticalOrientation из соотношения width/height
// (W>H → альбомная, H>W → книжная) и игнорирует заданный флаг, W/H не свопает — проверено
// спайком. Поэтому VerticalOrientation не трогаем; read-back ниже даёт верную ориентацию.
fmt.FormatWidth = width;
fmt.FormatHeight = height;
}
else
{
fmt.VerticalOrientation = !landscape; // стандартный: размеры авто по формату+ориентации
}
// FormatMultiplicity оставляем на COM-дефолте (1).
if (!sheet.Update())
throw new InvalidOperationException("ILayoutSheet.Update вернул FALSE — формат листа не задан.");
var fmt2 = sheet.Format
?? throw new InvalidOperationException("Формат листа недоступен после Update (Format == null).");
return new SheetFormatResult
{
Format = PaperFormats.FromKompas(fmt2.Format).ToString(),
Width = fmt2.FormatWidth,
Height = fmt2.FormatHeight,
Landscape = !fmt2.VerticalOrientation,
};
}
/// <summary>
/// Поставить линейный размер на виде активного чертежа между точками (x1,y1)-(x2,y2) в ЛОКАЛЬНОЙ
/// СК вида (мм); размерная линия проходит через (x3,y3). <paramref name="viewNumber"/> — номер вида
@@ -137,27 +196,46 @@ public sealed class DrawingService
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.LineDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить диаметральный размер (Ø) окружности с центром (xc,yc) и радиусом <paramref name="radius"/>
/// (ЛОКАЛЬНАЯ СК вида, мм) на виде. <paramref name="angleDeg"/> — направление выноски (градусы).
/// Значение (диаметр = 2·radius) измеряется автоматически. Возвращает диаметр и номер вида.
/// Поставить диаметральный размер (Ø) окружности на виде. <paramref name="angleDeg"/> — направление
/// выноски (градусы). При <paramref name="associate"/>=false — свободный размер по (xc,yc,radius),
/// значение = 2·radius. При <paramref name="associate"/>=true — (xc,yc,radius) служат ключом поиска
/// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение Ø читается
/// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает диаметр и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddDiametralDimensionAsync(int viewNumber, double xc, double yc,
double radius, double angleDeg, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddDiametralDimensionCore(viewNumber, xc, yc, radius, angleDeg), ct);
double radius, double angleDeg, bool associate = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddDiametralDimensionCore(viewNumber, xc, yc, radius, angleDeg, associate), ct);
private DrawingDimensionResult AddDiametralDimensionCore(int viewNumber, double xc, double yc,
double radius, double angleDeg)
double radius, double angleDeg, bool associate)
{
DrawingValidation.RequireFiniteCoords(xc, yc);
DrawingValidation.RequirePositiveRadius(radius);
var angleRad = DimensionAngles.ToRadians(angleDeg);
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
// Свободный путь берёт только ISymbols2DContainer (как раньше); IDrawingContainer (геометрия)
// нужен лишь для ассоциативной привязки — не расширяем поверхность отказа свободного пути.
IView view; ISymbols2DContainer symbols; IDrawingObject? baseObject = null;
if (associate)
{
var c = RequireViewContainers(viewNumber);
view = c.View; symbols = c.Symbols;
baseObject = FindBaseCircle(c.Drawing, xc, yc, radius);
}
else
{
var c = RequireSymbols2DContainer(viewNumber);
view = c.View; symbols = c.Symbols;
}
var dim = symbols.DiametralDimensions?.Add()
?? throw new InvalidOperationException("DiametralDimensions.Add вернул null.");
try
{
dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; dim.Angle = angleRad;
if (baseObject != null)
dim.BaseObject = baseObject; // ассоциативно: значение Ø читается из геометрии
else
{ dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; }
dim.Angle = angleRad;
// DimensionType (стиль размерной линии) не задаём — оставляем COM-дефолт (полярность bool
// не подтверждена; дефолт даёт валидный размер с верным значением — проверено спайком).
((IDimensionText)dim).AutoNominalValue = true;
@@ -184,6 +262,369 @@ public sealed class DrawingService
public Task<int> GetViewDiametralDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.DiametralDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить радиальный размер (R) окружности на виде. <paramref name="angleDeg"/> — направление
/// выноски (градусы). При <paramref name="associate"/>=false — свободный размер по (xc,yc,radius),
/// значение = radius. При <paramref name="associate"/>=true — (xc,yc,radius) служат ключом поиска
/// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение R читается
/// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает радиус и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddRadialDimensionAsync(int viewNumber, double xc, double yc,
double radius, double angleDeg, bool associate = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddRadialDimensionCore(viewNumber, xc, yc, radius, angleDeg, associate), ct);
private DrawingDimensionResult AddRadialDimensionCore(int viewNumber, double xc, double yc,
double radius, double angleDeg, bool associate)
{
DrawingValidation.RequireFiniteCoords(xc, yc);
DrawingValidation.RequirePositiveRadius(radius);
var angleRad = DimensionAngles.ToRadians(angleDeg);
// Свободный путь берёт только ISymbols2DContainer (как раньше); IDrawingContainer (геометрия)
// нужен лишь для ассоциативной привязки — не расширяем поверхность отказа свободного пути.
IView view; ISymbols2DContainer symbols; IDrawingObject? baseObject = null;
if (associate)
{
var c = RequireViewContainers(viewNumber);
view = c.View; symbols = c.Symbols;
baseObject = FindBaseCircle(c.Drawing, xc, yc, radius);
}
else
{
var c = RequireSymbols2DContainer(viewNumber);
view = c.View; symbols = c.Symbols;
}
var dim = symbols.RadialDimensions?.Add()
?? throw new InvalidOperationException("RadialDimensions.Add вернул null.");
try
{
if (baseObject != null)
dim.BaseObject = baseObject; // ассоциативно: значение R читается из геометрии
else
{ dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; }
dim.Angle = angleRad;
dim.DimensionType = true; // стиль R «от центра» (на величину не влияет — спайк)
((IDimensionText)dim).AutoNominalValue = true;
if (!dim.Update())
throw new InvalidOperationException("IRadialDimension.Update вернул FALSE — размер не построен.");
if (!dim.Valid)
throw new InvalidOperationException("Радиальный размер построен невалидным.");
var value = ((IDimensionText)dim).NominalValue; // = радиус (не диаметр — спайк)
if (value <= 0 || !double.IsFinite(value)) // инвариант радиуса: строго положительный
throw new InvalidOperationException($"Радиальный размер измерил некорректное значение ({value}).");
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
}
catch
{
try { dim.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число радиальных размеров на виде (для проверки попадания в вид — они в
/// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
public Task<int> GetViewRadialDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.RadialDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить угловой размер между лучами (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) (ЛОКАЛЬНАЯ СК вида, мм)
/// на виде. (x3,y3) — положение размерной дуги (задаёт её радиус = расстояние от вершины).
/// <paramref name="angleType"/> выбирает, какой из двух углов измерять (min/max/more) — например
/// 90° или 270° для перпендикуляра. Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddAngularDimensionAsync(int viewNumber, double xc, double yc,
double x1, double y1, double x2, double y2, double x3, double y3,
AngleDimensionType angleType, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
AddAngularDimensionCore(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, angleType), ct);
private DrawingDimensionResult AddAngularDimensionCore(int viewNumber, double xc, double yc,
double x1, double y1, double x2, double y2, double x3, double y3, AngleDimensionType angleType)
{
DrawingValidation.RequireFiniteCoords(xc, yc, x1, y1, x2, y2, x3, y3);
DrawingValidation.RequireDistinctPoints(xc, yc, x1, y1); // вершина ≠ точка стороны 1
DrawingValidation.RequireDistinctPoints(xc, yc, x2, y2); // вершина ≠ точка стороны 2
DrawingValidation.RequireDistinctPoints(xc, yc, x3, y3); // вершина ≠ положение дуги (R дуги > 0)
DrawingValidation.RequireDistinctPoints(x1, y1, x2, y2); // стороны не совпадают (одинаковые лучи)
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
var dim = symbols.AngleDimensions?.Add(DrawingObjectTypeEnum.ksDrADimension)
?? throw new InvalidOperationException("AngleDimensions.Add вернул null.");
try
{
dim.Xc = xc; dim.Yc = yc;
dim.X1 = x1; dim.Y1 = y1; dim.X2 = x2; dim.Y2 = y2; dim.X3 = x3; dim.Y3 = y3;
dim.DimensionType = AngleDimensionTypes.ToKompas(angleType);
((IDimensionText)dim).AutoNominalValue = true;
if (!dim.Update())
throw new InvalidOperationException("IAngleDimension.Update вернул FALSE — размер не построен.");
if (!dim.Valid)
throw new InvalidOperationException("Угловой размер построен невалидным.");
var value = ((IDimensionText)dim).NominalValue; // градусы
// Нулевой/некорректный угол — стороны коллинеарны (лучи сонаправлены → угол вырожден).
if (value <= 0 || !double.IsFinite(value))
throw new InvalidOperationException(
$"Угловой размер измерил некорректное значение ({value}°) — стороны коллинеарны " +
"(лучи (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) сонаправлены).");
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
}
catch
{
try { dim.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число угловых размеров на виде (для проверки попадания в вид — они в
/// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
public Task<int> GetViewAngularDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.AngleDimensions?.Count ?? 0, ct);
// ───────────────────────── Текстовые обозначения (инкремент 6) ─────────────────────────
/// <summary>
/// Поставить знак шероховатости на виде в точке (<paramref name="x"/>,<paramref name="y"/>)
/// (ЛОКАЛЬНАЯ СК вида, мм). <paramref name="value"/> — текст параметра (Ra/Rz; пусто = знак без
/// значения). <paramref name="signType"/> — тип знака. <paramref name="angleDeg"/> — угол наклона
/// оси знака (°). Возвращает значение (read-back) и номер вида.
/// </summary>
public Task<DrawingAnnotationResult> AddRoughAsync(int viewNumber, double x, double y, string? value,
RoughSignType signType, double angleDeg, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddRoughCore(viewNumber, x, y, value, signType, angleDeg), ct);
private DrawingAnnotationResult AddRoughCore(int viewNumber, double x, double y, string? value,
RoughSignType signType, double angleDeg)
{
DrawingValidation.RequireFiniteCoords(x, y, angleDeg);
var text = string.IsNullOrWhiteSpace(value) ? string.Empty : value;
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
var roughs = symbols.Roughs
?? throw new InvalidOperationException("Коллекция Roughs недоступна на виде.");
var rough = roughs.Add()
?? throw new InvalidOperationException("Roughs.Add вернул null.");
try
{
rough.BranchX0 = x; rough.BranchY0 = y; rough.Angle = angleDeg;
var rp = rough as IRoughParams
?? throw new InvalidOperationException("Знак шероховатости не приводится к IRoughParams.");
rp.SignType = RoughSignTypes.ToKompas(signType);
var paramText = rp.RoughParamText
?? throw new InvalidOperationException("RoughParamText недоступен (текст значения).");
paramText.Str = text;
if (!rough.Update())
throw new InvalidOperationException("IRough.Update вернул FALSE — знак не построен.");
if (!rough.Valid)
throw new InvalidOperationException("Знак шероховатости построен невалидным.");
var readBack = rp.RoughParamText?.Str ?? string.Empty; // read-back из COM
return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number };
}
catch
{
try { rough.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число знаков шероховатости на виде (для проверки попадания в вид).</summary>
public Task<int> GetViewRoughCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.Roughs?.Count ?? 0, ct);
/// <summary>
/// Поставить свободную текстовую надпись на виде в точке (<paramref name="x"/>,<paramref name="y"/>)
/// (ЛОКАЛЬНАЯ СК вида, мм) стилем по умолчанию. <paramref name="text"/> — содержимое (\n — многострочно).
/// <paramref name="angleDeg"/> — угол наклона текста (°). Возвращает текст (read-back) и номер вида.
/// </summary>
public Task<DrawingAnnotationResult> AddTextAsync(int viewNumber, double x, double y, string text,
double angleDeg, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddTextCore(viewNumber, x, y, text, angleDeg), ct);
private DrawingAnnotationResult AddTextCore(int viewNumber, double x, double y, string text, double angleDeg)
{
DrawingValidation.RequireNonEmptyText(text, nameof(text));
DrawingValidation.RequireFiniteCoords(x, y, angleDeg);
var (view, container) = RequireDrawingContainer(viewNumber);
var texts = container.DrawingTexts
?? throw new InvalidOperationException("Коллекция DrawingTexts недоступна на виде.");
var dt = texts.Add()
?? throw new InvalidOperationException("DrawingTexts.Add вернул null.");
try
{
dt.X = x; dt.Y = y; dt.Angle = angleDeg;
var content = dt as IText
?? throw new InvalidOperationException("Надпись не приводится к IText.");
content.Str = text;
if (!dt.Update())
throw new InvalidOperationException("IDrawingText.Update вернул FALSE — надпись не построена.");
if (!dt.Valid)
throw new InvalidOperationException("Надпись построена невалидной.");
var readBack = ((IText)dt).Str ?? string.Empty; // read-back из COM
return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number };
}
catch
{
try { dt.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число текстовых надписей на виде (для проверки попадания в вид; текст в
/// IDrawingContainer, а не в IView.ObjectCount).</summary>
public Task<int> GetViewTextCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireDrawingContainer(viewNumber).Container.DrawingTexts?.Count ?? 0, ct);
/// <summary>
/// Поставить линию-выноску на виде: стрелка из полки указывает в (<paramref name="x"/>,<paramref name="y"/>),
/// текст <paramref name="text"/> на полке у (<paramref name="textX"/>,<paramref name="textY"/>) — всё в
/// ЛОКАЛЬНОЙ СК вида (мм). <paramref name="shelfDirection"/> — направление полки (Auto = по умолчанию КОМПАС).
/// Возвращает текст (read-back) и номер вида.
/// </summary>
public Task<DrawingAnnotationResult> AddLeaderAsync(int viewNumber, double x, double y, double textX,
double textY, string text, ShelfDirection shelfDirection, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddLeaderCore(viewNumber, x, y, textX, textY, text, shelfDirection), ct);
private DrawingAnnotationResult AddLeaderCore(int viewNumber, double x, double y, double textX,
double textY, string text, ShelfDirection shelfDirection)
{
DrawingValidation.RequireNonEmptyText(text, nameof(text));
DrawingValidation.RequireFiniteCoords(x, y, textX, textY);
DrawingValidation.RequireDistinctPoints(x, y, textX, textY); // остриё ≠ полка (иначе вырождение)
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
var leaders = symbols.Leaders
?? throw new InvalidOperationException("Коллекция Leaders недоступна на виде.");
var baseLeader = leaders.Add(DrawingObjectTypeEnum.ksDrLeader)
?? throw new InvalidOperationException("Leaders.Add вернул null.");
try
{
// КРИТИЧЕСКИЙ ПОРЯДОК: AddBranchByPoint ДОЛЖЕН предшествовать SetBranchTextPosition/Update —
// выноска без ответвлений валит КОМПАС (RPC_E_SERVERFAULT). Проверено спайком.
var branches = baseLeader as IBranchs
?? throw new InvalidOperationException("Выноска не приводится к IBranchs.");
if (!branches.AddBranchByPoint(0, x, y)) // остриё ответвления 0 (куда указывает стрелка)
throw new InvalidOperationException("AddBranchByPoint вернул FALSE — ответвление выноски не создано.");
if (!baseLeader.SetBranchTextPosition(textX, textY)) // положение полки/текста
throw new InvalidOperationException("SetBranchTextPosition вернул FALSE — полка выноски не задана.");
var leader = baseLeader as ILeader
?? throw new InvalidOperationException("Выноска не приводится к ILeader.");
var shelf = leader.TextOnShelf
?? throw new InvalidOperationException("Текст полки выноски недоступен (TextOnShelf == null).");
shelf.Str = text;
if (shelfDirection != ShelfDirection.Auto)
leader.ShelfDirection = ShelfDirections.ToKompas(shelfDirection);
if (!baseLeader.Update())
throw new InvalidOperationException("ILeader.Update вернул FALSE — выноска не построена.");
if (!baseLeader.Valid)
throw new InvalidOperationException("Выноска построена невалидной.");
var readBack = leader.TextOnShelf?.Str ?? string.Empty; // read-back из COM
return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number };
}
catch
{
try { baseLeader.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число выносок на виде (для проверки попадания в вид; считает все подтипы выносок).</summary>
public Task<int> GetViewLeaderCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.Leaders?.Count ?? 0, ct);
/// <summary>
/// Задать технические требования активного чертежа (единый блок над основной надписью; перезаписывает
/// прежние). <paramref name="text"/> — строки через \n. Возвращает число строк.
/// </summary>
public Task<int> SetTechnicalRequirementsAsync(string text, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => SetTechnicalRequirementsCore(text), ct);
private int SetTechnicalRequirementsCore(string text)
{
DrawingValidation.RequireNonEmptyText(text, nameof(text));
var normalized = text.Replace("\r\n", "\n");
var td = RequireTechnicalDemand();
var content = td.Text
?? throw new InvalidOperationException("Текст технических требований недоступен (Text == null).");
content.Str = normalized; // Str замещает содержимое (как у штампа)
if (!td.Update())
throw new InvalidOperationException("ITechnicalDemand.Update вернул FALSE — требования не заданы.");
// Число содержательных строк: хвостовые пустые строки отбрасываем ("a\n" → 1).
return normalized.TrimEnd('\n').Split('\n').Length;
}
/// <summary>Прочитать текст технических требований активного чертежа (для проверки/диагностики).</summary>
public Task<string> ReadTechnicalRequirementsAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireTechnicalDemand().Text?.Str ?? string.Empty, ct);
private ITechnicalDemand RequireTechnicalDemand()
{
var doc = RequireActiveDrawing() as IDrawingDocument
?? throw new InvalidOperationException("Активный чертёж не приводится к IDrawingDocument.");
return doc.TechnicalDemand
?? throw new InvalidOperationException("У чертежа недоступны технические требования (TechnicalDemand == null).");
}
/// <summary>Вид + его IDrawingContainer (QI от вида). Для текстовых надписей (DrawingTexts).</summary>
private (IView View, IDrawingContainer Container) RequireDrawingContainer(int viewNumber)
{
var view = FindView(viewNumber);
var container = view as IDrawingContainer
?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer.");
return (view, container);
}
/// <summary>Допуск совпадения окружности с ключом поиска при ассоциативной привязке (мм).</summary>
private const double CircleMatchToleranceMm = 1.0;
/// <summary>Вид + оба контейнера (QI от одного вида): ISymbols2DContainer (создание размера) и
/// IDrawingContainer (поиск геометрии для ассоциативной привязки).</summary>
private (IView View, ISymbols2DContainer Symbols, IDrawingContainer Drawing) RequireViewContainers(int viewNumber)
{
var view = FindView(viewNumber);
var symbols = view as ISymbols2DContainer
?? throw new InvalidOperationException("Вид не приводится к ISymbols2DContainer.");
var drawing = view as IDrawingContainer
?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer.");
return (view, symbols, drawing);
}
/// <summary>Найти окружность вида по ключу (центр + радиус) и вернуть её как IDrawingObject
/// (для <c>dim.BaseObject</c>). Бросает при отсутствии/неоднозначности.</summary>
private static IDrawingObject FindBaseCircle(IDrawingContainer drawing, double xc, double yc, double radius)
{
var items = CollectCircles(drawing);
var index = CircularObjectMatch.SelectMatchIndex(
items.Select(i => i.Geom).ToList(), xc, yc, radius, CircleMatchToleranceMm);
return items[index].Obj;
}
/// <summary>Собрать окружности вида (IDrawingContainer.Circles) с их геометрией и COM-ссылкой.</summary>
private static List<(IDrawingObject Obj, CircleCandidate Geom)> CollectCircles(IDrawingContainer drawing)
{
var result = new List<(IDrawingObject, CircleCandidate)>();
var circles = drawing.Circles;
if (circles == null) return result;
for (var i = 0; i < circles.Count; i++)
{
if (circles[i] is not ICircle c) continue;
result.Add(((IDrawingObject)c, new CircleCandidate(c.Xc, c.Yc, c.Radius)));
}
return result;
}
/// <summary>Вид + его ISymbols2DContainer (QI от вида). Общий для линейных/диаметральных/… размеров.</summary>
private (IView View, ISymbols2DContainer Symbols) RequireSymbols2DContainer(int viewNumber)
{
@@ -48,6 +48,38 @@ public static class DrawingValidation
throw new ArgumentOutOfRangeException(nameof(radius), "Радиус должен быть конечным и > 0.");
}
/// <summary>Проверить, что текст не пустой и не только из пробелов.</summary>
public static void RequireNonEmptyText(string? text, string paramName)
{
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException("Текст не должен быть пустым.", paramName);
}
/// <summary>
/// Проверить размеры формата листа: для <see cref="PaperFormat.User"/> — ширина и высота оба
/// конечны и &gt; 0; для стандартного формата — оба должны быть 0 (размеры авто, задавать нельзя).
/// </summary>
public static void ValidateFormatDimensions(PaperFormat format, double width, double height)
{
if (format == PaperFormat.User)
{
if (!double.IsFinite(width) || width <= 0)
throw new ArgumentOutOfRangeException(nameof(width),
"Для format=user ширина должна быть конечной и > 0.");
if (!double.IsFinite(height) || height <= 0)
throw new ArgumentOutOfRangeException(nameof(height),
"Для format=user высота должна быть конечной и > 0.");
}
else if (width != 0 || height != 0)
{
// Точное имя нарушившего параметра (а не всегда width).
var offender = width != 0 ? nameof(width) : nameof(height);
throw new ArgumentException(
"Ширина/высота задаются только для format=user; для стандартного формата размеры авто (передайте 0).",
offender);
}
}
/// <summary>Минимальное расстояние между выносными точками линейного размера (мм).</summary>
private const double MinPointDistance = 1e-7;
@@ -0,0 +1,70 @@
using Kompas6Constants;
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Формат листа чертежа (обёртка над <see cref="ksDocumentFormatEnum"/>).</summary>
public enum PaperFormat
{
/// <summary>A0 (841×1189 мм).</summary>
A0,
/// <summary>A1 (594×841 мм).</summary>
A1,
/// <summary>A2 (420×594 мм).</summary>
A2,
/// <summary>A3 (297×420 мм).</summary>
A3,
/// <summary>A4 (210×297 мм).</summary>
A4,
/// <summary>A5 (148×210 мм).</summary>
A5,
/// <summary>Пользовательский формат с явной шириной/высотой.</summary>
User,
}
/// <summary>Маппинг формата листа ↔ перечисление КОМПАС и разбор строки.</summary>
public static class PaperFormats
{
public static ksDocumentFormatEnum ToKompas(PaperFormat format) => format switch
{
PaperFormat.A0 => ksDocumentFormatEnum.ksFormatA0,
PaperFormat.A1 => ksDocumentFormatEnum.ksFormatA1,
PaperFormat.A2 => ksDocumentFormatEnum.ksFormatA2,
PaperFormat.A3 => ksDocumentFormatEnum.ksFormatA3,
PaperFormat.A4 => ksDocumentFormatEnum.ksFormatA4,
PaperFormat.A5 => ksDocumentFormatEnum.ksFormatA5,
PaperFormat.User => ksDocumentFormatEnum.ksFormatUser,
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Неизвестный формат листа."),
};
/// <summary>Обратный маппинг (для read-back дружелюбного имени).</summary>
public static PaperFormat FromKompas(ksDocumentFormatEnum format) => format switch
{
ksDocumentFormatEnum.ksFormatA0 => PaperFormat.A0,
ksDocumentFormatEnum.ksFormatA1 => PaperFormat.A1,
ksDocumentFormatEnum.ksFormatA2 => PaperFormat.A2,
ksDocumentFormatEnum.ksFormatA3 => PaperFormat.A3,
ksDocumentFormatEnum.ksFormatA4 => PaperFormat.A4,
ksDocumentFormatEnum.ksFormatA5 => PaperFormat.A5,
ksDocumentFormatEnum.ksFormatUser => PaperFormat.User,
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Неизвестный формат листа КОМПАС."),
};
/// <summary>Разобрать строку формата (A0..A5 / user, регистр/пробелы) в <see cref="PaperFormat"/>.</summary>
public static PaperFormat Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
return value.Trim().ToLowerInvariant() switch
{
"a0" => PaperFormat.A0,
"a1" => PaperFormat.A1,
"a2" => PaperFormat.A2,
"a3" => PaperFormat.A3,
"a4" => PaperFormat.A4,
"a5" => PaperFormat.A5,
"user" or "custom" or "пользовательский" => PaperFormat.User,
_ => throw new ArgumentException(
$"Неизвестный формат листа '{value}'. Допустимо: A0, A1, A2, A3, A4, A5, user.",
nameof(value)),
};
}
}
@@ -0,0 +1,41 @@
using Kompas6Constants;
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Тип знака шероховатости (обёртка над <see cref="ksRoughSignEnum"/>).</summary>
public enum RoughSignType
{
/// <summary>Без указания вида обработки.</summary>
NoProcessing,
/// <summary>С удалением слоя материала.</summary>
DeleteMaterial,
/// <summary>Без удаления слоя материала.</summary>
WithoutDeleteMaterial,
}
/// <summary>Маппинг типа знака шероховатости ↔ перечисление КОМПАС и разбор строки.</summary>
public static class RoughSignTypes
{
public static ksRoughSignEnum ToKompas(RoughSignType type) => type switch
{
RoughSignType.NoProcessing => ksRoughSignEnum.ksNoProcessingType,
RoughSignType.DeleteMaterial => ksRoughSignEnum.ksDeleteMaterial,
RoughSignType.WithoutDeleteMaterial => ksRoughSignEnum.ksWithoutDeleteMaterial,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "Неизвестный тип знака шероховатости."),
};
/// <summary>Разобрать строку типа знака (англ./рус.) в <see cref="RoughSignType"/>.</summary>
public static RoughSignType Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
return value.Trim().ToLowerInvariant() switch
{
"delete" or "deletematerial" or "с удалением" or "с удалением материала" => RoughSignType.DeleteMaterial,
"without" or "withoutdelete" or "без удаления" or "без удаления материала" => RoughSignType.WithoutDeleteMaterial,
"none" or "noprocessing" or "без указания" or "любая" => RoughSignType.NoProcessing,
_ => throw new ArgumentException(
$"Неизвестный тип знака шероховатости '{value}'. Допустимо: delete, without, none.",
nameof(value)),
};
}
}
@@ -0,0 +1,17 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Результат задания формата листа чертежа (read-back из COM).</summary>
public sealed record SheetFormatResult
{
/// <summary>Итоговый формат («A0»…«A5» или «User»).</summary>
public required string Format { get; init; }
/// <summary>Ширина листа, мм.</summary>
public required double Width { get; init; }
/// <summary>Высота листа, мм.</summary>
public required double Height { get; init; }
/// <summary>Альбомная ориентация (true) либо книжная (false).</summary>
public required bool Landscape { get; init; }
}
@@ -0,0 +1,52 @@
using Kompas6Constants;
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Направление полки выноски (обёртка над <see cref="ksShelfDirectionEnum"/>).</summary>
public enum ShelfDirection
{
/// <summary>Авто — направление полки оставляет КОМПАС (свойство не задаётся).</summary>
Auto,
/// <summary>Полка вправо (в ЛОКАЛЬНОЙ СК вида).</summary>
Right,
/// <summary>Полка влево (в ЛОКАЛЬНОЙ СК вида).</summary>
Left,
/// <summary>Полка вверх (в ЛОКАЛЬНОЙ СК вида).</summary>
Up,
/// <summary>Полка вниз (в ЛОКАЛЬНОЙ СК вида).</summary>
Down,
}
/// <summary>Маппинг направления полки ↔ перечисление КОМПАС и разбор строки.</summary>
public static class ShelfDirections
{
/// <summary>В <see cref="ksShelfDirectionEnum"/>. Бросает на <see cref="ShelfDirection.Auto"/> —
/// «авто» обрабатывается в сервисе (свойство не задаётся), сюда передавать нельзя.</summary>
public static ksShelfDirectionEnum ToKompas(ShelfDirection direction) => direction switch
{
ShelfDirection.Right => ksShelfDirectionEnum.ksLSRight,
ShelfDirection.Left => ksShelfDirectionEnum.ksLSLeft,
ShelfDirection.Up => ksShelfDirectionEnum.ksLSUp,
ShelfDirection.Down => ksShelfDirectionEnum.ksLSDown,
_ => throw new ArgumentOutOfRangeException(nameof(direction), direction,
"ShelfDirection.Auto не имеет COM-значения (направление не задаётся)."),
};
/// <summary>Разобрать строку направления (англ./рус., регистр/пробелы) в <see cref="ShelfDirection"/>.</summary>
public static ShelfDirection Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
return value.Trim().ToLowerInvariant() switch
{
"auto" or "авто" => ShelfDirection.Auto,
"right" or "вправо" => ShelfDirection.Right,
"left" or "влево" => ShelfDirection.Left,
"up" or "вверх" => ShelfDirection.Up,
"down" or "вниз" => ShelfDirection.Down,
_ => throw new ArgumentException(
$"Неизвестное направление полки '{value}'. Допустимо: auto/авто, right/вправо, " +
"left/влево, up/вверх, down/вниз.",
nameof(value)),
};
}
}
+149 -3
View File
@@ -71,16 +71,162 @@ public sealed class DrawingTools(KompasSession session, DrawingService drawing)
"задаётся центром (xc,yc) и радиусом radius в ЛОКАЛЬНОЙ СК вида (мм; при масштабе 1:1 = координаты " +
"модели на плоскости вида) — задайте параметры реальной окружности вида. angle — направление выноски " +
"в градусах. viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный). " +
"Значение (диаметр = 2·radius) измеряется автоматически. Возвращает диаметр и номер вида.")]
"associate=false: свободный размер (значение = 2·radius). associate=true: АССОЦИАТИВНО — (xc,yc,radius) " +
"используются как ключ для поиска спроецированной окружности на виде; размер привязывается к ней " +
"(BaseObject) и значение Ø читается из геометрии и обновляется с моделью. Значение измеряется " +
"автоматически. Возвращает диаметр и номер вида.")]
public async Task<string> AddDiametralDimension(
[Description("X центра окружности (СК вида, мм)")] double xc,
[Description("Y центра окружности")] double yc,
[Description("Радиус окружности, мм")] double radius,
[Description("Направление выноски, градусы")] double angle = 0,
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0,
[Description("true = привязать к окружности вида (значение из геометрии); false = свободный размер")] bool associate = false)
{
await session.ConnectAsync();
var r = await drawing.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angle, associate);
var mode = associate ? " (ассоциативно)" : "";
return $"Диаметральный размер поставлен на виде {r.ViewNumber}, Ø{r.Value:0.###} мм{mode}.";
}
[McpServerTool(Name = "drawing_add_radial_dimension")]
[Description("Поставить радиальный размер (R) окружности на виде активного чертежа. Окружность " +
"задаётся центром (xc,yc) и радиусом radius в ЛОКАЛЬНОЙ СК вида (мм; при масштабе 1:1 = координаты " +
"модели на плоскости вида) — задайте параметры реальной окружности вида. angle — направление выноски " +
"в градусах. viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный). " +
"associate=false: свободный размер (значение = radius). associate=true: АССОЦИАТИВНО — (xc,yc,radius) " +
"используются как ключ для поиска спроецированной окружности на виде; размер привязывается к ней " +
"(BaseObject) и значение R читается из геометрии и обновляется с моделью. Значение измеряется " +
"автоматически. Возвращает радиус и номер вида.")]
public async Task<string> AddRadialDimension(
[Description("X центра окружности (СК вида, мм)")] double xc,
[Description("Y центра окружности")] double yc,
[Description("Радиус окружности, мм")] double radius,
[Description("Направление выноски, градусы")] double angle = 0,
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0,
[Description("true = привязать к окружности вида (значение из геометрии); false = свободный размер")] bool associate = false)
{
await session.ConnectAsync();
var r = await drawing.AddRadialDimensionAsync(viewNumber, xc, yc, radius, angle, associate);
var mode = associate ? " (ассоциативно)" : "";
return $"Радиальный размер поставлен на виде {r.ViewNumber}, R{r.Value:0.###} мм{mode}.";
}
[McpServerTool(Name = "drawing_add_angular_dimension")]
[Description("Поставить угловой размер между двумя сторонами на виде активного чертежа. Угол задаётся " +
"лучами из вершины (xc,yc): сторона 1 проходит через точку (x1,y1), сторона 2 — через (x2,y2) " +
"(всё в ЛОКАЛЬНОЙ СК вида, мм). Точка (x3,y3) — положение размерной дуги (задаёт её радиус = " +
"расстояние от вершины); поместите её в стороне измеряемого угла. КАКОЙ из двух углов измерять " +
"(например 90° или 270° для перпендикуляра) — выбирает angleType: min (меньший/острый) | max " +
"(больший/тупой) | more (рефлексный, >180°). viewNumber — номер вида (0 = первый/главный). " +
"Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида.")]
public async Task<string> AddAngularDimension(
[Description("X вершины угла (СК вида, мм)")] double xc,
[Description("Y вершины угла")] double yc,
[Description("X точки на стороне 1")] double x1,
[Description("Y точки на стороне 1")] double y1,
[Description("X точки на стороне 2")] double x2,
[Description("Y точки на стороне 2")] double y2,
[Description("X положения размерной дуги (отстоит от вершины; угол выбирает angleType, не эта точка)")] double x3,
[Description("Y положения размерной дуги")] double y3,
[Description("Тип угла: min | max | more")] string angleType = "min",
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0)
{
await session.ConnectAsync();
var r = await drawing.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angle);
return $"Диаметральный размер поставлен на виде {r.ViewNumber}, Ø{r.Value:0.###} мм.";
var t = AngleDimensionTypes.Parse(angleType);
var r = await drawing.AddAngularDimensionAsync(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, t);
return $"Угловой размер поставлен на виде {r.ViewNumber}, {r.Value:0.###}°.";
}
[McpServerTool(Name = "drawing_add_rough")]
[Description("Поставить знак шероховатости на виде активного чертежа в точке (x,y) в ЛОКАЛЬНОЙ СК вида " +
"(мм). value — текст параметра (например \"Ra 1.6\" или \"Rz 40\"; пусто = знак без значения). " +
"signType: delete (с удалением слоя материала) | without (без удаления) | none (без указания вида " +
"обработки). angle — угол наклона оси знака в градусах. viewNumber — номер вида из " +
"drawing_create_standard_views (0 = первый/главный). Возвращает значение и номер вида.")]
public async Task<string> AddRough(
[Description("X положения знака (СК вида, мм)")] double x,
[Description("Y положения знака")] double y,
[Description("Текст параметра, напр. \"Ra 1.6\" (пусто = без значения)")] string value = "",
[Description("Тип знака: delete | without | none")] string signType = "delete",
[Description("Угол наклона оси знака, градусы")] double angle = 0,
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0)
{
await session.ConnectAsync();
var st = RoughSignTypes.Parse(signType);
var r = await drawing.AddRoughAsync(viewNumber, x, y, value, st, angle);
var shown = string.IsNullOrEmpty(r.Value) ? "(без значения)" : r.Value;
return $"Знак шероховатости поставлен на виде {r.ViewNumber}: {shown}.";
}
[McpServerTool(Name = "drawing_add_text")]
[Description("Поставить свободную текстовую надпись на виде активного чертежа в точке (x,y) в ЛОКАЛЬНОЙ " +
"СК вида (мм) стилем по умолчанию. text — содержимое надписи (\\n — несколько строк). angle — угол " +
"наклона текста в градусах. viewNumber — номер вида из drawing_create_standard_views (0 = первый/" +
"главный). Возвращает поставленный текст и номер вида.")]
public async Task<string> AddText(
[Description("X точки привязки (СК вида, мм)")] double x,
[Description("Y точки привязки")] double y,
[Description("Текст надписи (\\n — многострочно)")] string text = "",
[Description("Угол наклона текста, градусы")] double angle = 0,
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0)
{
await session.ConnectAsync();
var r = await drawing.AddTextAsync(viewNumber, x, y, text, angle);
return $"Надпись поставлена на виде {r.ViewNumber}.";
}
[McpServerTool(Name = "drawing_set_technical_requirements")]
[Description("Задать технические требования активного чертежа — единый текстовый блок над основной " +
"надписью (ГОСТ). text — содержимое (\\n разделяет пункты/строки, например \"1. ...\\n2. ...\"). " +
"Перезаписывает прежние технические требования (не добавляет). Возвращает число строк.")]
public async Task<string> SetTechnicalRequirements(
[Description("Текст требований (\\n — строки)")] string text)
{
await session.ConnectAsync();
var lines = await drawing.SetTechnicalRequirementsAsync(text);
return $"Технические требования заданы ({lines} стр.).";
}
[McpServerTool(Name = "drawing_set_sheet_format")]
[Description("Задать формат и ориентацию листа активного чертежа. format: A0|A1|A2|A3|A4|A5|user. " +
"landscape: false = книжная (portrait), true = альбомная (landscape). Для стандартного формата " +
"размеры вычисляются автоматически — width/height передавать НЕ нужно (должны быть 0). Для format=user " +
"задайте width и height в мм (>0); при user флаг landscape игнорируется (ориентация — из соотношения " +
"сторон). sheetNumber — номер листа (1 = первый). Возвращает итоговый формат, размеры и ориентацию.")]
public async Task<string> SetSheetFormat(
[Description("Формат: A0|A1|A2|A3|A4|A5|user")] string format = "A4",
[Description("Альбомная ориентация (true) или книжная (false)")] bool landscape = false,
[Description("Ширина листа, мм (только для format=user)")] double width = 0,
[Description("Высота листа, мм (только для format=user)")] double height = 0,
[Description("Номер листа (1 = первый)")] int sheetNumber = 1)
{
await session.ConnectAsync();
var f = PaperFormats.Parse(format);
var r = await drawing.SetSheetFormatAsync(f, landscape, width, height, sheetNumber);
var orient = r.Landscape ? "альбомная" : "книжная";
return $"Формат листа {sheetNumber}: {r.Format}, {r.Width:0.#}×{r.Height:0.#} мм, {orient}.";
}
[McpServerTool(Name = "drawing_add_leader")]
[Description("Поставить линию-выноску с текстом на виде активного чертежа. Стрелка выноски указывает " +
"в точку (x,y); текст text размещается на полке у точки (textX,textY) — всё в ЛОКАЛЬНОЙ СК вида (мм). " +
"shelfDirection: auto/авто (по умолчанию — КОМПАС сам) | right/вправо | left/влево | up/вверх | " +
"down/вниз — направление полки. " +
"viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный). Точка острия (x,y) " +
"должна отличаться от точки полки (textX,textY). Возвращает поставленный текст и номер вида.")]
public async Task<string> AddLeader(
[Description("X острия — куда указывает стрелка (СК вида, мм)")] double x,
[Description("Y острия")] double y,
[Description("X полки/якоря текста")] double textX,
[Description("Y полки/якоря текста")] double textY,
[Description("Текст надписи на полке")] string text,
[Description("Направление полки: auto | right | left | up | down")] string shelfDirection = "auto",
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0)
{
await session.ConnectAsync();
var dir = ShelfDirections.Parse(shelfDirection);
var r = await drawing.AddLeaderAsync(viewNumber, x, y, textX, textY, text, dir);
return $"Выноска поставлена на виде {r.ViewNumber}: «{r.Value}».";
}
}
@@ -0,0 +1,30 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class AngleDimensionTypesTests
{
[Theory]
[InlineData("min", AngleDimensionType.Min)]
[InlineData("max", AngleDimensionType.Max)]
[InlineData("more", AngleDimensionType.More)]
[InlineData(" MIN ", AngleDimensionType.Min)]
[InlineData("минимальный", AngleDimensionType.Min)]
[InlineData("максимальный", AngleDimensionType.Max)]
[InlineData("больше180", AngleDimensionType.More)]
public void Parse_maps_known(string value, AngleDimensionType expected)
=> Assert.Equal(expected, AngleDimensionTypes.Parse(value));
[Fact]
public void Parse_throws_on_unknown()
=> Assert.Throws<ArgumentException>(() => AngleDimensionTypes.Parse("acute"));
[Fact]
public void Parse_throws_on_null()
=> Assert.Throws<ArgumentNullException>(() => AngleDimensionTypes.Parse(null!));
// ToKompas (→ ksAngleDimTypeEnum) намеренно НЕ покрыт unit-тестом: тестовый проект не ссылается
// на Kompas6Constants (граница проекта, как у DimensionOrientations.ToKompas). Маппинг тривиален
// и проверяется интеграцией (min → острый угол 90°, выбор сектора).
}
@@ -0,0 +1,53 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class CircularObjectMatchTests
{
private const double Tol = 1.0;
private static IReadOnlyList<CircleCandidate> Candidates(params (double x, double y, double r)[] items)
=> items.Select(i => new CircleCandidate(i.x, i.y, i.r)).ToList();
[Fact]
public void SelectMatchIndex_returns_unique_match()
{
var c = Candidates((0, 0, 10), (50, 20, 5));
// ключ с допустимым отклонением центра и радиуса
Assert.Equal(0, CircularObjectMatch.SelectMatchIndex(c, 0.3, -0.2, 10.3, Tol));
Assert.Equal(1, CircularObjectMatch.SelectMatchIndex(c, 50, 20, 5, Tol));
}
[Fact]
public void SelectMatchIndex_concentric_disambiguated_by_radius()
{
// концентрические R5 и R10 (цековка): ключ R10 выбирает внешнюю, не неоднозначно
var c = Candidates((0, 0, 5), (0, 0, 10));
Assert.Equal(1, CircularObjectMatch.SelectMatchIndex(c, 0, 0, 10, Tol));
Assert.Equal(0, CircularObjectMatch.SelectMatchIndex(c, 0, 0, 5, Tol));
}
[Fact]
public void SelectMatchIndex_throws_when_none_in_tolerance()
{
var c = Candidates((0, 0, 10));
// центр далеко
Assert.Throws<InvalidOperationException>(() => CircularObjectMatch.SelectMatchIndex(c, 100, 100, 10, Tol));
// радиус далеко
Assert.Throws<InvalidOperationException>(() => CircularObjectMatch.SelectMatchIndex(c, 0, 0, 25, Tol));
}
[Fact]
public void SelectMatchIndex_throws_when_ambiguous()
{
// две почти совпадающие окружности в допуске
var c = Candidates((0, 0, 10), (0.2, 0.2, 10.2));
Assert.Throws<InvalidOperationException>(() => CircularObjectMatch.SelectMatchIndex(c, 0, 0, 10, Tol));
}
[Fact]
public void SelectMatchIndex_throws_on_empty()
=> Assert.Throws<InvalidOperationException>(
() => CircularObjectMatch.SelectMatchIndex(Candidates(), 0, 0, 10, Tol));
}
@@ -80,4 +80,43 @@ public sealed class DrawingValidationTests
[Fact]
public void RequirePositiveRadius_accepts_positive()
=> Assert.Null(Record.Exception(() => DrawingValidation.RequirePositiveRadius(15)));
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void RequireNonEmptyText_throws_on_empty(string? text)
=> Assert.Throws<ArgumentException>(() => DrawingValidation.RequireNonEmptyText(text, "text"));
[Fact]
public void RequireNonEmptyText_accepts_non_empty()
=> Assert.Null(Record.Exception(() => DrawingValidation.RequireNonEmptyText("Ra 1.6", "text")));
[Theory]
[InlineData(0, 300)]
[InlineData(500, 0)]
[InlineData(-1, 300)]
[InlineData(double.NaN, 300)]
[InlineData(500, double.PositiveInfinity)]
public void ValidateFormatDimensions_user_throws_on_non_positive_or_non_finite(double w, double h)
=> Assert.Throws<ArgumentOutOfRangeException>(
() => DrawingValidation.ValidateFormatDimensions(PaperFormat.User, w, h));
[Fact]
public void ValidateFormatDimensions_user_accepts_positive()
=> Assert.Null(Record.Exception(
() => DrawingValidation.ValidateFormatDimensions(PaperFormat.User, 500, 300)));
[Theory]
[InlineData(500, 0)]
[InlineData(0, 300)]
[InlineData(500, 300)]
public void ValidateFormatDimensions_standard_throws_when_dimensions_given(double w, double h)
=> Assert.Throws<ArgumentException>(
() => DrawingValidation.ValidateFormatDimensions(PaperFormat.A3, w, h));
[Fact]
public void ValidateFormatDimensions_standard_accepts_zero_dimensions()
=> Assert.Null(Record.Exception(
() => DrawingValidation.ValidateFormatDimensions(PaperFormat.A3, 0, 0)));
}
@@ -0,0 +1,194 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: угловой размер на виде (API7 ISymbols2DContainer.AngleDimensions).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingAngularTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingAngularTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
private async Task<string> BuildAndSaveBoxAsync()
{
var path = TestPaths.NewFile(".m3d");
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
await _modeler.RebuildAsync();
await _docs.SaveAsAsync(path);
}
finally { await _docs.CloseAsync(save: false); }
return path;
}
private async Task<string> NewDrawingWithViewsAsync()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
return partFile;
}
[Fact]
public async Task Angular_measures_right_angle()
{
await NewDrawingWithViewsAsync();
try
{
// Вершина (0,0); сторона1 → (10,0); сторона2 → (0,10); дуга в секторе (5,5) → 90°.
var r = await _drawing.AddAngularDimensionAsync(
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
angleType: AngleDimensionType.Min);
Assert.InRange(r.Value, 89.5, 90.5);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Angular_measures_45_degrees()
{
await NewDrawingWithViewsAsync();
try
{
// Вершина (0,0); сторона1 → (10,0); сторона2 → (10,10) (45°); дуга (8,4).
var r = await _drawing.AddAngularDimensionAsync(
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 10, y2: 10, x3: 8, y3: 4,
angleType: AngleDimensionType.Min);
Assert.InRange(r.Value, 44.5, 45.5);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Angle_type_max_measures_larger_angle()
{
await NewDrawingWithViewsAsync();
try
{
// Лучи 0° и 45°: три типа дают разные углы — min=45 (острый), max=135 (тупой
// супплемент 18045), more=315 (рефлексный). Покрывает маппинг ksADMaxAngle (≠ Min/More).
var r = await _drawing.AddAngularDimensionAsync(
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 10, y2: 10, x3: 4, y3: -8,
angleType: AngleDimensionType.Max);
Assert.InRange(r.Value, 134.0, 136.0);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Targets_specific_view_and_increments_angular_count()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var targetNumber = views.ViewNumbers[1];
var before = await _drawing.GetViewAngularDimensionCountAsync(targetNumber);
var r = await _drawing.AddAngularDimensionAsync(
targetNumber, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
angleType: AngleDimensionType.Min);
var after = await _drawing.GetViewAngularDimensionCountAsync(targetNumber);
Assert.Equal(targetNumber, r.ViewNumber);
Assert.Equal(before + 1, after);
Assert.InRange(r.Value, 89.5, 90.5);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Angle_type_more_measures_reflex_angle()
{
await NewDrawingWithViewsAsync();
try
{
// Та же геометрия 90°, но angleType=more → измеряется рефлексный угол ~270°.
// Тип угла (min/max/more) выбирает, какой из секторов измерять; (x3,y3) лишь
// позиционирует размерную дугу (её ставим в рефлексный сектор для согласованности).
var r = await _drawing.AddAngularDimensionAsync(
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: -5, y3: -5,
angleType: AngleDimensionType.More);
Assert.InRange(r.Value, 269.0, 271.0);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Coincident_vertex_and_side_throws()
{
await NewDrawingWithViewsAsync();
try
{
// Вершина совпадает с точкой стороны1 → вырождение до Add.
await Assert.ThrowsAsync<ArgumentException>(() =>
_drawing.AddAngularDimensionAsync(
viewNumber: 0, xc: 0, yc: 0, x1: 0, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
angleType: AngleDimensionType.Min));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Collinear_rays_rollback_leaves_count_unchanged()
{
await NewDrawingWithViewsAsync();
try
{
var before = await _drawing.GetViewAngularDimensionCountAsync(0);
// Сонаправленные лучи (0,0)→(10,0) и (0,0)→(20,0): угол ≈0 → пост-проверка → откат.
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddAngularDimensionAsync(
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 20, y2: 0, x3: 15, y3: 5,
angleType: AngleDimensionType.Min));
var after = await _drawing.GetViewAngularDimensionCountAsync(0);
Assert.Equal(before, after);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task No_views_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddAngularDimensionAsync(
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
angleType: AngleDimensionType.Min));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Unknown_view_number_throws()
{
await NewDrawingWithViewsAsync();
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddAngularDimensionAsync(
viewNumber: 999, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
angleType: AngleDimensionType.Min));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,143 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: ассоциативная привязка диаметрального/радиального размера к окружности вида
/// (BaseObject). Модель — цилиндр R10 (вид проецирует окружность).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingAssociativeDimensionTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingAssociativeDimensionTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
private async Task<string> BuildAndSaveCylinderAsync()
{
var path = TestPaths.NewFile(".m3d");
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s, 0, 0, 10);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
await _modeler.RebuildAsync();
await _docs.SaveAsAsync(path);
}
finally { await _docs.CloseAsync(save: false); }
return path;
}
[Fact]
public async Task Diametral_associative_reads_diameter_from_geometry()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0]; // вид с окружностью (спайк)
var before = await _drawing.GetViewDiametralDimensionCountAsync(target);
// Ключ radius=10.3 (в допуске 1.0 находит окружность R10). Ассоциативно → Ø=20 из геометрии,
// а НЕ 2·10.3=20.6 → доказывает чтение значения из геометрии, а не из входа.
var r = await _drawing.AddDiametralDimensionAsync(target, xc: 0, yc: 0, radius: 10.3,
angleDeg: 45, associate: true);
var after = await _drawing.GetViewDiametralDimensionCountAsync(target);
Assert.Equal(target, r.ViewNumber);
Assert.InRange(r.Value, 19.9, 20.1); // Ø=20 из геометрии (не 20.6)
Assert.Equal(before + 1, after);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Radial_associative_reads_radius_from_geometry()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0];
var r = await _drawing.AddRadialDimensionAsync(target, xc: 0, yc: 0, radius: 10.3,
angleDeg: -45, associate: true);
Assert.InRange(r.Value, 9.9, 10.1); // R=10 из геометрии (не 10.3)
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Associative_no_circle_near_throws_and_no_orphan()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0];
var before = await _drawing.GetViewDiametralDimensionCountAsync(target);
// Ключ далеко от любой окружности → отбор бросит до Add (размер не создаётся).
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddDiametralDimensionAsync(target, xc: 1000, yc: 1000, radius: 5,
angleDeg: 0, associate: true));
var after = await _drawing.GetViewDiametralDimensionCountAsync(target);
Assert.Equal(before, after); // нет «осиротевшего» размера
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Free_diametral_path_unchanged()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
// associate=false (по умолчанию) — свободный размер: значение = 2·radius.
var r = await _drawing.AddDiametralDimensionAsync(views.ViewNumbers[0], xc: 0, yc: 0,
radius: 15, angleDeg: 0, associate: false);
Assert.InRange(r.Value, 29.9, 30.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Free_radial_path_unchanged()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var r = await _drawing.AddRadialDimensionAsync(views.ViewNumbers[0], xc: 0, yc: 0,
radius: 12, angleDeg: 0, associate: false);
Assert.InRange(r.Value, 11.9, 12.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Associative_no_views_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddDiametralDimensionAsync(0, xc: 0, yc: 0, radius: 10, angleDeg: 0, associate: true));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,146 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: линия-выноска с текстом на виде (API7 ISymbols2DContainer.Leaders + IBranchs).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingLeaderTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingLeaderTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
private async Task<string> BuildAndSaveBoxAsync()
{
var path = TestPaths.NewFile(".m3d");
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
await _modeler.RebuildAsync();
await _docs.SaveAsAsync(path);
}
finally { await _docs.CloseAsync(save: false); }
return path;
}
[Fact]
public async Task Leader_is_placed_round_trip_and_resolves_default_view()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var first = views.ViewNumbers[0];
var before = await _drawing.GetViewLeaderCountAsync(first);
// viewNumber=0 → первый несистемный вид (= first)
var r = await _drawing.AddLeaderAsync(0, x: 5, y: 5, textX: 25, textY: 20,
text: "Примечание 1", shelfDirection: ShelfDirection.Auto);
var after = await _drawing.GetViewLeaderCountAsync(first);
Assert.Equal(first, r.ViewNumber);
Assert.Equal("Примечание 1", r.Value); // read-back из COM
Assert.Equal(before + 1, after);
}
finally { await _docs.CloseAsync(save: false); }
}
[Theory]
[InlineData(ShelfDirection.Right)]
[InlineData(ShelfDirection.Left)]
[InlineData(ShelfDirection.Up)]
[InlineData(ShelfDirection.Down)]
public async Task Leader_with_explicit_shelf_direction(ShelfDirection dir)
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0];
var before = await _drawing.GetViewLeaderCountAsync(target);
// Покрывает всю поверхность ShelfDirections.ToKompas (Right/Left/Up/Down) на реальном COM.
var r = await _drawing.AddLeaderAsync(target, x: 10, y: 8, textX: 30, textY: 25,
text: "Полка", shelfDirection: dir);
var after = await _drawing.GetViewLeaderCountAsync(target);
Assert.Equal(before + 1, after);
Assert.Equal("Полка", r.Value);
}
finally { await _docs.CloseAsync(save: false); }
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public async Task Empty_text_throws(string text)
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<ArgumentException>(() =>
_drawing.AddLeaderAsync(0, x: 5, y: 5, textX: 25, textY: 20, text: text,
shelfDirection: ShelfDirection.Auto));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Degenerate_tip_equals_shelf_throws()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<ArgumentException>(() =>
_drawing.AddLeaderAsync(0, x: 5, y: 5, textX: 5, textY: 5, text: "X",
shelfDirection: ShelfDirection.Auto));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task No_views_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddLeaderAsync(0, x: 5, y: 5, textX: 25, textY: 20, text: "X",
shelfDirection: ShelfDirection.Auto));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Unknown_view_number_throws()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddLeaderAsync(999, x: 5, y: 5, textX: 25, textY: 20, text: "X",
shelfDirection: ShelfDirection.Auto));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,115 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: радиальный размер (R) на виде (API7 ISymbols2DContainer.RadialDimensions).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingRadialTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingRadialTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
private async Task<string> BuildAndSaveBoxAsync()
{
var path = TestPaths.NewFile(".m3d");
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
await _modeler.RebuildAsync();
await _docs.SaveAsAsync(path);
}
finally { await _docs.CloseAsync(save: false); }
return path;
}
[Fact]
public async Task Radial_dimension_value_is_radius()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
// Радиус 12 → значение радиального = радиус (НЕ диаметр), подтверждено спайком.
var r = await _drawing.AddRadialDimensionAsync(viewNumber: 0, xc: 10, yc: 10, radius: 12, angleDeg: 45);
Assert.InRange(r.Value, 11.9, 12.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Targets_specific_view_and_increments_radial_count()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var targetNumber = views.ViewNumbers[1];
var before = await _drawing.GetViewRadialDimensionCountAsync(targetNumber);
var r = await _drawing.AddRadialDimensionAsync(targetNumber, xc: 5, yc: 5, radius: 8, angleDeg: 30);
var after = await _drawing.GetViewRadialDimensionCountAsync(targetNumber);
Assert.Equal(targetNumber, r.ViewNumber);
Assert.Equal(before + 1, after);
Assert.InRange(r.Value, 7.9, 8.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Non_positive_radius_throws()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
_drawing.AddRadialDimensionAsync(viewNumber: 0, xc: 0, yc: 0, radius: 0, angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task No_views_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddRadialDimensionAsync(viewNumber: 0, xc: 0, yc: 0, radius: 10, angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Unknown_view_number_throws()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddRadialDimensionAsync(viewNumber: 999, xc: 0, yc: 0, radius: 10, angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,107 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: знак шероховатости на виде (API7 ISymbols2DContainer.Roughs + IRoughParams).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingRoughTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingRoughTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
private async Task<string> BuildAndSaveBoxAsync()
{
var path = TestPaths.NewFile(".m3d");
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
await _modeler.RebuildAsync();
await _docs.SaveAsAsync(path);
}
finally { await _docs.CloseAsync(save: false); }
return path;
}
[Fact]
public async Task Rough_is_placed_with_value_and_targets_view()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0];
var before = await _drawing.GetViewRoughCountAsync(target);
var r = await _drawing.AddRoughAsync(target, x: 20, y: 25, value: "Ra 1.6",
signType: RoughSignType.DeleteMaterial, angleDeg: 0);
var after = await _drawing.GetViewRoughCountAsync(target);
Assert.Equal(target, r.ViewNumber);
Assert.Equal("Ra 1.6", r.Value); // read-back из COM
Assert.Equal(before + 1, after);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Rough_without_value_is_placed()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var before = await _drawing.GetViewRoughCountAsync(0);
var r = await _drawing.AddRoughAsync(0, x: 15, y: 15, value: "",
signType: RoughSignType.NoProcessing, angleDeg: 0);
var after = await _drawing.GetViewRoughCountAsync(0);
Assert.Equal(before + 1, after);
Assert.Equal(string.Empty, r.Value);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task No_views_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddRoughAsync(0, x: 0, y: 0, value: "Ra 1.6",
signType: RoughSignType.DeleteMaterial, angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Unknown_view_number_throws()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddRoughAsync(999, x: 0, y: 0, value: "Ra 1.6",
signType: RoughSignType.DeleteMaterial, angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,156 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: формат и ориентация листа чертежа (API7 ILayoutSheet.Format / ISheetFormat).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingSheetFormatTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly DrawingService _drawing;
public DrawingSheetFormatTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
[Fact]
public async Task A3_landscape()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var r = await _drawing.SetSheetFormatAsync(PaperFormat.A3, landscape: true, width: 0, height: 0);
Assert.Equal("A3", r.Format);
Assert.InRange(r.Width, 419.5, 420.5);
Assert.InRange(r.Height, 296.5, 297.5);
Assert.True(r.Landscape);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task A4_portrait()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var r = await _drawing.SetSheetFormatAsync(PaperFormat.A4, landscape: false, width: 0, height: 0);
Assert.Equal("A4", r.Format);
Assert.InRange(r.Width, 209.5, 210.5);
Assert.InRange(r.Height, 296.5, 297.5);
Assert.False(r.Landscape);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task A4_landscape_swaps_dimensions()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var r = await _drawing.SetSheetFormatAsync(PaperFormat.A4, landscape: true, width: 0, height: 0);
Assert.Equal("A4", r.Format);
Assert.InRange(r.Width, 296.5, 297.5); // своп: альбомная A4 = 297×210
Assert.InRange(r.Height, 209.5, 210.5);
Assert.True(r.Landscape);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task User_landscape_dimensions_derive_orientation()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
// 500×300 (W>H) → КОМПАС выводит альбомную ориентацию (Landscape=true), W/H литеральны.
var r = await _drawing.SetSheetFormatAsync(PaperFormat.User, landscape: false, width: 500, height: 300);
Assert.Equal("User", r.Format);
Assert.InRange(r.Width, 499.5, 500.5);
Assert.InRange(r.Height, 299.5, 300.5);
Assert.True(r.Landscape); // ориентация выведена из соотношения сторон (не из флага landscape=false)
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task User_portrait_dimensions_derive_orientation()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
// 300×500 (H>W) → книжная (Landscape=false).
var r = await _drawing.SetSheetFormatAsync(PaperFormat.User, landscape: true, width: 300, height: 500);
Assert.Equal("User", r.Format);
Assert.InRange(r.Width, 299.5, 300.5);
Assert.InRange(r.Height, 499.5, 500.5);
Assert.False(r.Landscape); // landscape=true проигнорирован — ориентация из сторон
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task User_without_dimensions_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
_drawing.SetSheetFormatAsync(PaperFormat.User, landscape: false, width: 0, height: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Standard_with_dimensions_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<ArgumentException>(() =>
_drawing.SetSheetFormatAsync(PaperFormat.A3, landscape: false, width: 400, height: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Nonexistent_sheet_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.SetSheetFormatAsync(PaperFormat.A4, landscape: false, width: 0, height: 0, sheetNumber: 99));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Nonpositive_sheet_number_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
_drawing.SetSheetFormatAsync(PaperFormat.A4, landscape: false, width: 0, height: 0, sheetNumber: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Active_document_not_drawing_throws()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.SetSheetFormatAsync(PaperFormat.A4, landscape: false, width: 0, height: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,76 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: технические требования чертежа (API7 IDrawingDocument.TechnicalDemand).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingTechReqTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly DrawingService _drawing;
public DrawingTechReqTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
[Fact]
public async Task Requirements_are_set_and_read_back()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var text = "1. Общие допуски по ГОСТ 30893.1.\n2. Острые кромки притупить.";
var lines = await _drawing.SetTechnicalRequirementsAsync(text);
Assert.Equal(2, lines);
var readBack = await _drawing.ReadTechnicalRequirementsAsync();
Assert.Contains("Общие допуски", readBack);
Assert.Contains("Острые кромки", readBack);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Overwrite_replaces_previous()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.SetTechnicalRequirementsAsync("1. Первая редакция.");
await _drawing.SetTechnicalRequirementsAsync("1. Вторая редакция.\n2. Ещё пункт.");
var readBack = await _drawing.ReadTechnicalRequirementsAsync();
Assert.Contains("Вторая редакция", readBack);
Assert.DoesNotContain("Первая редакция", readBack);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Empty_text_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<ArgumentException>(() =>
_drawing.SetTechnicalRequirementsAsync(" "));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Active_document_not_drawing_throws()
{
await _docs.CreateAsync(KompasDocumentType.Part); // деталь, не чертёж
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.SetTechnicalRequirementsAsync("1. Требование."));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,115 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: свободная текстовая надпись на виде (API7 IDrawingContainer.DrawingTexts + IText).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingTextTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingTextTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
private async Task<string> BuildAndSaveBoxAsync()
{
var path = TestPaths.NewFile(".m3d");
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
await _modeler.RebuildAsync();
await _docs.SaveAsAsync(path);
}
finally { await _docs.CloseAsync(save: false); }
return path;
}
[Fact]
public async Task Text_is_placed_and_targets_view()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0];
var before = await _drawing.GetViewTextCountAsync(target);
var r = await _drawing.AddTextAsync(target, x: 30, y: 45, text: "Образец надписи", angleDeg: 0);
var after = await _drawing.GetViewTextCountAsync(target);
Assert.Equal(target, r.ViewNumber);
Assert.Equal("Образец надписи", r.Value); // read-back из COM
Assert.Equal(before + 1, after);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Multiline_text_round_trips()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var r = await _drawing.AddTextAsync(0, x: 10, y: 10, text: "Строка один\nСтрока два", angleDeg: 0);
Assert.Contains("Строка один", r.Value);
Assert.Contains("Строка два", r.Value);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Empty_text_throws()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<ArgumentException>(() =>
_drawing.AddTextAsync(0, x: 10, y: 10, text: " ", angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task No_views_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddTextAsync(0, x: 0, y: 0, text: "Текст", angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Unknown_view_number_throws()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddTextAsync(999, x: 0, y: 0, text: "Текст", angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,29 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class PaperFormatsTests
{
[Theory]
[InlineData("A4", PaperFormat.A4)]
[InlineData("a3", PaperFormat.A3)]
[InlineData(" A2 ", PaperFormat.A2)]
[InlineData("a0", PaperFormat.A0)]
[InlineData("A1", PaperFormat.A1)]
[InlineData("A5", PaperFormat.A5)]
[InlineData("user", PaperFormat.User)]
[InlineData("USER", PaperFormat.User)]
public void Parse_maps_known(string value, PaperFormat expected)
=> Assert.Equal(expected, PaperFormats.Parse(value));
[Fact]
public void Parse_throws_on_unknown()
=> Assert.Throws<ArgumentException>(() => PaperFormats.Parse("A6"));
[Fact]
public void Parse_throws_on_null()
=> Assert.Throws<ArgumentNullException>(() => PaperFormats.Parse(null!));
// ToKompas/FromKompas (↔ ksDocumentFormatEnum) — граница проекта, проверяются интеграцией.
}
@@ -0,0 +1,29 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class RoughSignTypesTests
{
[Theory]
[InlineData("delete", RoughSignType.DeleteMaterial)]
[InlineData("without", RoughSignType.WithoutDeleteMaterial)]
[InlineData("none", RoughSignType.NoProcessing)]
[InlineData(" DELETE ", RoughSignType.DeleteMaterial)]
[InlineData("с удалением", RoughSignType.DeleteMaterial)]
[InlineData("без удаления", RoughSignType.WithoutDeleteMaterial)]
[InlineData("без указания", RoughSignType.NoProcessing)]
public void Parse_maps_known(string value, RoughSignType expected)
=> Assert.Equal(expected, RoughSignTypes.Parse(value));
[Fact]
public void Parse_throws_on_unknown()
=> Assert.Throws<ArgumentException>(() => RoughSignTypes.Parse("polished"));
[Fact]
public void Parse_throws_on_null()
=> Assert.Throws<ArgumentNullException>(() => RoughSignTypes.Parse(null!));
// ToKompas (→ ksRoughSignEnum) намеренно НЕ покрыт unit-тестом: тестовый проект не ссылается на
// Kompas6Constants (граница проекта, как у AngleDimensionTypes/DimensionOrientations).
}
@@ -0,0 +1,32 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class ShelfDirectionsTests
{
[Theory]
[InlineData("auto", ShelfDirection.Auto)]
[InlineData("right", ShelfDirection.Right)]
[InlineData("left", ShelfDirection.Left)]
[InlineData("up", ShelfDirection.Up)]
[InlineData("down", ShelfDirection.Down)]
[InlineData(" RIGHT ", ShelfDirection.Right)]
[InlineData("авто", ShelfDirection.Auto)]
[InlineData("вправо", ShelfDirection.Right)]
[InlineData("влево", ShelfDirection.Left)]
[InlineData("вверх", ShelfDirection.Up)]
[InlineData("вниз", ShelfDirection.Down)]
public void Parse_maps_known(string value, ShelfDirection expected)
=> Assert.Equal(expected, ShelfDirections.Parse(value));
[Fact]
public void Parse_throws_on_unknown()
=> Assert.Throws<ArgumentException>(() => ShelfDirections.Parse("diagonal"));
[Fact]
public void Parse_throws_on_null()
=> Assert.Throws<ArgumentNullException>(() => ShelfDirections.Parse(null!));
// ToKompas (→ ksShelfDirectionEnum) — граница проекта; для Auto бросает (проверяется косвенно).
}