71 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
mikhail 93a67d318e docs: CLAUDE.md — описать субагентов-делегатов (kompas-sdk-research, docs-maintainer)
Раздел «Delegation subagents» рядом с принципом проекта: модели (Haiku/Sonnet),
наборы инструментов, когда диспетчить, обоснование сплита моделей.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:15:17 +03:00
mikhail 35e06e8786 docs: NEXT-SESSION — снизить якорение, оптимизировать хэндофф-промт
Сокращён со 117 до ~40 строк. Убраны пред-решённый следующий инкремент,
пред-разведанные точки входа API (RadialDimensions/AngleDimensions, сниппет
рефлексии), развёрнутый процесс/ловушки/паттерн теста — заменены указателями
на CLAUDE.md / навык kompas-3d / авто-память. Текущее состояние свёрнуто до
инвентаря по классам. Модель сама разведывает API и выбирает инкремент —
эффект свежего взгляда.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:08:53 +03:00
mikhail 4e017fc1cf docs: README — убрать избыточную конкретику из секции «Архитектура (кратко)»
Свёрнуты method-level цепочки вызовов и имена внутренних сервисов до уровня
обзора; детали реализации — по ссылке в ARCHITECTURE.md/CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:01:12 +03:00
mikhail 92e869006f chore: kompas-sdk-research → Haiku (поиск-извлечение, ошибка ловится сверкой interop)
docs-maintainer остаётся на Sonnet (правит канонические доки, нужно суждение).
Обновлены подписи моделей в README.md и NEXT-SESSION.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:59:21 +03:00
mikhail 454a90100a refactor: навыки-делегаторы → нативные субагенты
docs-delegate и kompas-sdk-research были навыками-обёртками: их тело —
шаблон системного промпта, который Opus вручную копировал в вызов
general-purpose агента на Sonnet. Переоформлены в нативных субагентов
(.claude/agents/), где промпт живёт в теле определения, модель и набор
инструментов фиксируются во frontmatter.

- docs-delegate → docs-maintainer (имя по роли, а не по механизму);
  tools: Read/Edit/Write/Glob/Grep.
- kompas-sdk-research: read-only (tools: Glob/Grep/Read) — запрет правок
  теперь структурный, а не правило в промпте.
- Делегация теперь один вызов Agent(subagent_type, prompt) без копирования
  шаблона → экономия контекста Opus.
- Переписаны живые ссылки: CLAUDE.md, kompas-3d/SKILL.md, README.md,
  ARCHITECTURE.md, NEXT-SESSION.md. Старые каталоги навыков удалены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:55:13 +03:00
mikhail ced95b742b docs: переписан handoff набело под продолжение (веха 2D-чертёж, инкремент 5)
Убраны наслоения старой сессии (фокус «начни со сборок», дубли состояния, стартовая
инструкция про IParts7). Актуально: 76 инструментов / 218 тестов; сборки завершены,
чертёж 4 инкремента; следующий — радиальный/угловой размер (A) или рамка/формат (B).
Процесс, ловушки, паттерн теста, отрицательный результат сохранены и сжаты.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:49:05 +03:00
mikhail ecf7b15e7a docs: handoff — диаметральный размер готов (инкремент 4 чертежа); далее — радиус/угол
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:45:54 +03:00
mikhail 17efc79ffb Merge feat/drawing-diametral-dimension: диаметральный размер на виде (инкремент 4)
drawing_add_diametral_dimension — Ø окружности на виде через API7
(ISymbols2DContainer.DiametralDimensions; значение=диаметр 2R, авто). Извлечён
общий RequireSymbols2DContainer. 76 инструментов, 218 тестов (141 unit + 77
integration). Спек + ревью Codex (спека и реализации) учтены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:44:42 +03:00
mikhail 7359337a95 docs: drawing_add_diametral_dimension — диаметральные размеры (76 инструментов, 218 тестов)
README/CLAUDE/ARCHITECTURE/OPEN_QUESTIONS/presentation.html: счётчики 75→76,
203→218; drawing_add_diametral_dimension в группе Drawing; факты (ISymbols2DContainer.
DiametralDimensions, Xc/Yc/Radius/Angle радианы, NominalValue=2R, RequireSymbols2DContainer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:44:42 +03:00
mikhail b6ca2f7b21 feat: drawing_add_diametral_dimension — упрочнение по ревью Codex реализации
#2: инвариант диаметра value>0 (value<=0||!IsFinite, не Abs<1e-6). #3: сообщение
create_standard_views обобщено (номера видов для инструментов размеров). #4a:
ToRadians проверяет конечность результата (переполнение). #5d: тест viewNumber:999
(вид не найден). #6: комментарий DimensionType переформулирован (COM-дефолт).

Тесты: DrawingDiametralTests 4→5. Итого 141 unit + 77 integration = 218.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:38:33 +03:00
mikhail 910d120c8a feat: drawing_add_diametral_dimension — диаметральный размер на виде (API7)
Веха 2D-ЧЕРТЁЖ, инкремент 4. DrawingService.AddDiametralDimensionAsync:
(ISymbols2DContainer)view.DiametralDimensions.Add → Xc/Yc/Radius/Angle(рад) +
AutoNominalValue → Update → Valid → NominalValue (= диаметр 2R). angle в градусах
(DimensionAngles.ToRadians). Новый инструмент drawing_add_diametral_dimension.

Рефактор по ревью Codex: извлечён RequireSymbols2DContainer(viewNumber) (общий для
линейных/диаметральных размеров); AutoNominalValue выставляется явно (как у линейного);
DimensionType — COM-дефолт; ToRadians — чистый helper с unit-тестом.

Валидаторы: RequirePositiveRadius (новый). Тесты: +10 unit (ToRadians,
RequirePositiveRadius), +4 integration (DrawingDiametralTests: диаметр=2R, адресация
вида, radius<=0, нет видов). Итого 141 unit + 76 integration = 217.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:30:04 +03:00
mikhail b6cd92b95c docs: спек drawing_add_diametral_dimension — диаметральный размер (API7)
Веха 2D-ЧЕРТЁЖ, инкремент 4. Спайк: (ISymbols2DContainer)view.DiametralDimensions.
Add → Xc/Yc/Radius/Angle(рад) → Update; NominalValue == диаметр (2R, проверено 30).
Angle экспонируется в градусах. Та же модель, что линейный размер.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:17:15 +03:00
mikhail cd76abd652 docs: handoff — линейные размеры готовы (инкремент 3 чертежа); далее — диаметр/радиус
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:14:40 +03:00
mikhail 2c594bc890 Merge feat/drawing-linear-dimension: линейный размер на виде (инкремент 3)
drawing_add_linear_dimension — линейный размер (horizontal/vertical/parallel) на
ассоциативном виде через API7 (ISymbols2DContainer.LineDimensions, авто-значение).
drawing_create_standard_views теперь возвращает ViewNumbers (адресация видов).
75 инструментов, 203 теста (131 unit + 72 integration). Спек + ревью Codex
(спека и реализации) учтены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:13:46 +03:00
mikhail eda3242f62 docs: drawing_add_linear_dimension — линейные размеры (75 инструментов, 203 теста)
README/CLAUDE/ARCHITECTURE/OPEN_QUESTIONS/presentation.html: счётчики 74→75,
188→203; drawing_add_linear_dimension в группе Drawing; факты (ISymbols2DContainer
QI от вида, локальная СК вида, AutoNominalValue, размеры НЕ в IView.ObjectCount,
ViewNumbers в результате видов).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:13:46 +03:00
mikhail 71cafb2270 feat: drawing_add_linear_dimension — упрочнение по ревью Codex реализации
#4: отклонение нулевого NominalValue после Update (ориентация несовместима с
точками, напр. horizontal на вертикали |X2-X1|=0) — откат dim.Delete + ошибка.
#3: Parallel протестирован — работает БЕЗ явного Angle (КОМПАС авто-вычисляет угол
по точкам; (0,0)-(30,40)→50). #7: rollback-ветка покрыта тестом нулевой проекции.
#8: контракт «первый несистемный вид» — осознанный (задокументирован).

Тесты: DrawingDimensionTests 5→7 (+parallel, +нулевая проекция/откат). Итого
131 unit + 72 integration = 203.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:07:01 +03:00
mikhail 8e0ab374af feat: drawing_add_linear_dimension — линейный размер на виде (API7)
Веха 2D-ЧЕРТЁЖ, инкремент 3. DrawingService.AddLinearDimensionAsync: (ISymbols2D
Container)view (QI) → LineDimensions.Add → X1..Y3 (локальная СК вида) + Orientation
+ AutoNominalValue → Update → проверка Valid → ((IDimensionText)dim).NominalValue.
Свой enum DimensionOrientation (horizontal/vertical/parallel). Новый инструмент
drawing_add_linear_dimension. DrawingViewsResult += ViewNumbers (адресация видов).

Находка: размеры НЕ в IView.ObjectCount (он про геометрию IDrawingContainer) —
проверка попадания в вид через ISymbols2DContainer.LineDimensions.Count.

Ревью Codex спека учтено: RequireDistinctPoints (нулевая длина) + Valid после
Update; контракт «первый несистемный вид»; null-check Add; AutoNominalValue явно;
ViewNumbers required + текст ответа create_standard_views; тесты адресации/негатив.

Тесты: +8 unit (DimensionOrientations, DrawingValidation), +5 integration
(DrawingDimensionTests). Итого 131 unit + 70 integration = 201.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:01:02 +03:00
mikhail ca3d91ae68 docs: спек drawing_add_linear_dimension — линейный размер на виде (API7)
Веха 2D-ЧЕРТЁЖ, инкремент 3. Спайк подтвердил: (ISymbols2DContainer)view QI →
LineDimensions.Add → X1..Y3 (локальная СК вида) + Orientation + Update; авто-значение
((IDimensionText)dim).NominalValue == расстояние (проверено 40). Плюс: DrawingViewsResult
+= ViewNumbers (адресация видов, закрывает замечание Codex из ревью видов).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:49:18 +03:00
mikhail d6a129b0d1 docs: handoff — основная надпись готова (инкремент 2 чертежа); следующее — размеры на видах
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:32:22 +03:00
mikhail 56f6e0b45a Merge feat/drawing-title-block: основная надпись чертежа (оформление, инкремент 2)
drawing_fill_title_block — заполнение граф штампа (обозначение/наименование/
материал) активного чертежа через API7 (IStamp.Text[id].Str + Update).
74 инструмента, 188 тестов (123 unit + 65 integration). Спек + ревью Codex
(спека и реализации) учтены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:31:31 +03:00
mikhail 034e9aee24 docs: drawing_fill_title_block — оформление чертежа (74 инструмента, 188 тестов)
README/CLAUDE/ARCHITECTURE/OPEN_QUESTIONS/presentation.html: счётчики 73→74,
177→188; drawing_fill_title_block в группе Drawing; факты основной надписи
(LayoutSheets.ItemByNumber[1].Stamp, Text[id].Str индексировано, StampField→графы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:31:31 +03:00
mikhail eaa4a56708 feat: drawing_fill_title_block — упрочнение по ревью Codex реализации
#3/#6: доступ к графе через RequireStampCell(stamp,columnId) — null-check ячейки
+ единое место валидации (запись и чтение). #4: проверка columnId>0 в helper
(защищает и ReadStampCellAsync). #2/#5: нетранзакционность записи зафиксирована
комментарием (откат при Update()==FALSE спекулятивен, не делаем). RCW — долг v2-2.

Тесты без изменений в числе: 123 unit + 65 integration = 188 зелёных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:25:41 +03:00
mikhail 742a97177b feat: drawing_fill_title_block — основная надпись чертежа (API7)
Веха 2D-ЧЕРТЁЖ, инкремент 2 (оформление). DrawingService.FillTitleBlockAsync:
LayoutSheets.ItemByNumber[1].Stamp.Text[id].Str = text → Update(). Enum StampField
→ номер графы ГОСТ (designation=2, name=1, material=3). Заполнение нескольких
граф за вызов; перезапись (Str замещает, Clear не нужен — подтверждено спайком).
Новый инструмент drawing_fill_title_block; ReadStampCellAsync для проверки.

Ревью Codex спека учтено: Update() результат проверяется; null-checks
(LayoutSheets/sheet/Stamp); RequireActiveDrawing выделен (сохраняет cast-проверку);
ColumnId switch с дефолтным throw; счётчик = непустые поля; name=графа 1 однозначно.

Валидация — чистый StampFields (ColumnId + Collect). Тесты: +7 unit (StampFields),
+4 integration (DrawingStampTests: заполнение+reread 3 граф, перезапись, пустой
ввод, не-чертёж). Итого 123 unit + 65 integration = 188.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:20:16 +03:00
mikhail db23a1a748 docs: спек drawing_fill_title_block — основная надпись чертежа (API7)
Веха 2D-ЧЕРТЁЖ, инкремент 2. Спайк подтвердил round-trip графы: LayoutSheets.
ItemByNumber[1].Stamp.Text[id].Str + Update(). Enum StampField→номер графы ГОСТ
(designation=2, name=1, material=3). Заполнение нескольких граф за вызов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:07:35 +03:00
mikhail 23d245fd78 docs: handoff — веха 2D-чертёж начата (drawing_create_standard_views готов)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:45:48 +03:00
mikhail ddbb46196e Merge feat/drawing-standard-views: стандартные виды чертежа (веха 2D-чертёж)
drawing_create_standard_views (инкремент 1 вехи 2D-ЧЕРТЁЖ): стандартные
ассоциативные виды детали/сборки на чертеже (API7 IViews.AddStandartViews).
73 инструмента, 177 тестов (116 unit + 61 integration). Спек + ревью Codex
(спека и реализации) учтены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:44:28 +03:00
mikhail 6c6add72e2 docs: drawing_create_standard_views — веха 2D-чертёж начата (73 инструмента, 177 тестов)
README/CLAUDE/ARCHITECTURE/OPEN_QUESTIONS/presentation.html: счётчики 72→73,
156→177; новая группа инструментов Drawing; факты DrawingService (AddStandartViews,
ProjectionsTypes=object[]{1,3,5} SAFEARRAY VT_I4, created==3, непустота по новым
видам, откат IView.Delete).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:44:28 +03:00
mikhail 5aa4221a37 feat: drawing_create_standard_views — упрочнение по ревью Codex реализации
#1: непустота проверяется у ВНОВЬ созданных видов (разница IView.Number), не по
всей коллекции — прежняя геометрия чертежа не маскирует пустые новые виды.
#3: откат созданных видов (IView.Delete) при created!=3 или пустых видах.
#7: unit-тест нормализации кириллического пути. #4: повторный вызов (дубликаты)
отражён в описании инструмента. #5/#6/#2 — по обоснованию (release RCW отклонён v2-2).

Тесты: 116 unit + 61 integration = 177 зелёных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:37:35 +03:00
mikhail de063290f4 feat: drawing_create_standard_views — стандартные виды чертежа (API7)
Веха 2 (2D-ЧЕРТЁЖ), инкремент 1. Новый DrawingService (API7, namespace
Kompas.Mcp.Core.Drawings): doc2d.ViewsAndLayersManager.Views.AddStandartViews(
path, "#Спереди", {1,3,5}=front/top/left, x, y, scale, 20, 20). Новый DrawingTools
(инструмент drawing_create_standard_views), регистрация в DI.

Подтверждено спайком: object[]→SAFEARRAY VT_I4 ОК, 3 вида создаются (Count 1→4).
Ревью Codex спека учтено: File.Exists+Length>0; created==3 строго; гарантия
непустоты (сумма ObjectCount>0); опц. x,y (повторный вызов не стопкой); record
DrawingViewsResult{Created,Total}. Точечный release RCW отклонён (v2-2).

Валидация — чистый DrawingValidation (расширение .m3d/.a3d, scale>0).
Тесты: +16 unit (DrawingValidation), +3 integration (DrawingTests: создание 3 видов,
отклонение не-чертежа, несуществующий файл). Итого 114 unit + 61 integration = 175.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:30:25 +03:00
mikhail 46648f7635 docs: спек drawing_create_standard_views — стандартные виды чертежа (API7)
Веха 2 (2D-ЧЕРТЁЖ), инкремент 1. Механизм подтверждён спайком: doc2d.
ViewsAndLayersManager.Views.AddStandartViews(path, "#Спереди", {1,3,5}, ...);
object[]→SAFEARRAY VT_I4 ОК, 3 вида создаются (Count 1→4). Новый DrawingService.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:09:43 +03:00
65 changed files with 6838 additions and 346 deletions
+50
View File
@@ -0,0 +1,50 @@
---
name: docs-maintainer
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#).
Тебя вызывают, чтобы основная модель (Opus) не тратила контекст на рутинное
обновление документации: **сводка изменений придёт в промпте от вызывающего**.
Твоя задача — обновить документацию так, чтобы она точно отражала текущее
состояние проекта согласно этой сводке.
## Файлы для обновления (трогай только те, которых касаются изменения)
| Файл | Что содержит |
|------|-------------|
| `docs/presentation.html` | **Презентация проекта — синхронизируй ВСЕГДА.** Счётчики (число инструментов, тестов), бейдж статуса, прогресс-бар, панель инструментов, дорожная карта |
| `README.md` | Обзор проекта, список возможностей, команды сборки/запуска |
| `CLAUDE.md` | Гайд для агента: «Current state», ключевые факты реализации, что не сделано |
| `docs/ARCHITECTURE.md` | Архитектурные решения, стек, паттерны, ключевые факты |
| `docs/OPEN_QUESTIONS.md` | Нерешённые вопросы и ограничения |
## Правила
1. Прочитай каждый файл перед правкой — не угадывай содержимое.
2. Обновляй только секции, которые реально затронуты изменениями.
Не переписывай то, что остаётся актуальным.
3. Если изменение закрывает вопрос из `OPEN_QUESTIONS.md` — удали его
или перенеси в раздел «Решено» (если такой есть).
4. Сохраняй стиль, структуру и тон существующих документов.
5. Будь лаконичен: одно предложение вместо абзаца, если смысл не теряется.
6. Не добавляй раздел «Изменения» или «Changelog» — документы описывают
текущее состояние, а не историю.
7. Счётчики в `presentation.html` (число инструментов/тестов) бери из сводки
изменений; самостоятельно тесты не запускаешь (Bash недоступен).
После правок кратко сообщи, что именно было изменено в каждом файле.
## Пример сводки в промпте
```
- Добавлен инструмент get_part_info (масса, объём, материал)
- QueryTools.cs перенесён в отдельный файл
- Закрыт вопрос из OPEN_QUESTIONS про get_part_info
```
Или короче: «обнови доки — добавил get_part_info».
@@ -1,34 +1,28 @@
---
name: kompas-sdk-research
description: >
Делегирует поиск и чтение справки КОМПАС SDK субагенту на модели Sonnet.
Источник — структурированная MD-база знаний docs/Kompas3D_SDK/ (статьи по
интерфейсам с YAML-тегами, ищется обычным Grep/Read).
Используй ВСЕГДА, когда нужно найти интерфейс/метод/константу COM API КОМПАС,
его сигнатуру (Automation/COM), значения перечисления, единицы измерения или
паттерн использования — не рой справку сам, передавай Sonnet и получай
готовую выжимку. Триггеры: «найди в справке КОМПАС», «какая сигнатура у…»,
«какие значения enum…», «как через API КОМПАС сделать…», «что возвращает <метод>».
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/`: статьи по интерфейсам с
YAML-фронтматтером (`type`, `api`, `domain`, `tags`, `sources`); методы и
свойства — заголовки `## ` внутри статьи интерфейса.
# kompas-sdk-research — делегирование исследования справки КОМПАС SDK Sonnet
Тебя вызывают, чтобы основная модель (Opus) не тратила контекст на поиск:
**вопрос для исследования придёт в промпте от вызывающего**. Прогони Grep/Read
по базе, прочитай нужную статью и верни сжатый ответ (интерфейс, сигнатура,
параметры, значения констант, источник).
Справка КОМПАС SDK сконвертирована в структурированную MD-базу знаний
`docs/Kompas3D_SDK/` — статьи сгруппированы по COM-интерфейсам, с YAML-тегами для
поиска, чистым текстом, развёрнутыми блоками синтаксиса и реальными изображениями.
Этот навык позволяет Opus не тратить контекст на поиск: опиши, что нужно узнать, —
Sonnet прогонит Grep/Read по базе, прочитает нужную статью и вернёт сжатый ответ
(интерфейс, сигнатура, параметры, значения констант, источник).
## Когда применять
## Когда это про тебя
- Нужна сигнатура метода (`SetSideParam`, `GetGabarit`, …) — Automation/COM, параметры, возврат.
- Нужны значения перечисления/констант (`Obj3dType`, `ksMassUnitsEnum`, `ST_MIX_*`, стили линий).
- Нужно найти интерфейс под задачу («чем построить массив», «как задать переменную»).
- Нужен паттерн «как через API сделать X» с цепочкой вызовов.
Не применяй для правки документации проекта — это делает навык **docs-delegate**.
## Структура MD-базы `docs/Kompas3D_SDK/`
```
@@ -48,44 +42,7 @@ resources/ — изображения (ссылки в статьях в
> База **хранится в репозитории** (канонический источник справки) — отдельная
> генерация не нужна.
## Шаги выполнения
### 1. Сформулируй точный вопрос
Перед вызовом агента определи, что именно ищешь и в каком виде нужен ответ:
- объект поиска: интерфейс? метод? свойство? константа/enum? паттерн?
- желаемый результат: сигнатура? список значений? имя интерфейса? шаги вызова?
### 2. Запусти Agent с моделью Sonnet
Используй инструмент `Agent` со следующими параметрами:
```
subagent_type: "general-purpose"
model: "sonnet"
description: "Справка КОМПАС SDK: <одна строка что ищем>"
prompt: <см. шаблон ниже>
```
### 3. Шаблон промпта для Sonnet
Заполни `<ВОПРОС>` и передай Sonnet:
---
Ты — помощник по навигации в справке **КОМПАС SDK** в репозитории
**kompas3d-mcp** (MCP-сервер для КОМПАС-3D через COM API, .NET 8, C#).
Справка — это MD-база знаний `docs/Kompas3D_SDK/`: статьи по интерфейсам с
YAML-фронтматтером (`type`, `api`, `domain`, `tags`, `sources`); методы и
свойства — заголовки `## ` внутри статьи интерфейса.
**Вопрос для исследования:**
```
<ВОПРОС>
```
**Инструменты (используй именно их, не веб):**
## Инструменты (используй именно их, не веб)
- **Grep** по `docs/Kompas3D_SDK/` — ищи имя интерфейса/метода/свойства, русское
ключевое слово или тег. Имена методов — это заголовки `## SetSideParam - …`,
@@ -102,13 +59,14 @@ YAML-фронтматтером (`type`, `api`, `domain`, `tags`, `sources`); м
Точные числовые значения констант при необходимости сверяй в заголовках 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. Верни **сжатую выжимку**, а не дамп статьи:
@@ -121,15 +79,10 @@ YAML-фронтматтером (`type`, `api`, `domain`, `tags`, `sources`); м
3. Если у метода несколько перегрузок или связанных интерфейсов — перечисли релевантные.
4. Если в COM-interop имя отличается от справки (частый случай в этом проекте), отметь это
как вероятную тонкость и предложи свериться рефлексией по `libs/kompas-interop/*.dll`.
5. Не вноси изменений в код или документацию — только исследование и ответ.
5. Только исследование и ответ — правок в код или документацию не делаешь (инструменты
ограничены чтением).
---
## Пример задачи в промпте
## Пример вызова
```
Запусти kompas-sdk-research: нужна сигнатура и единицы измерения у метода расчёта
МЦХ детали через API5 — что возвращает, как задать мм/кг.
```
Или короче: «найди в справке КОМПАС, как построить массив по сетке (pattern)».
«Нужна сигнатура и единицы измерения у метода расчёта МЦХ детали через API5 — что
возвращает, как задать мм/кг.» Или короче: «как построить массив по сетке (pattern)».
-89
View File
@@ -1,89 +0,0 @@
---
name: docs-delegate
description: >
Делегирует обновление документации субагенту на модели Sonnet.
Используй ВСЕГДА, когда нужно синхронизировать README.md или docs/*.md
(ARCHITECTURE, OPEN_QUESTIONS) с изменениями в коде — не делай это сам,
передавай Sonnet. Триггер: «обнови доки», «синхронизируй документацию»,
«пусть Sonnet обновит README/ARCHITECTURE», «задокументируй изменения».
---
# docs-delegate — делегирование документации Sonnet
Этот навык позволяет Opus (основной модели) не тратить контекст на рутинное
обновление документации: нужно только описать, что изменилось, — Sonnet
сделает остальное.
## Шаги выполнения
### 1. Составь сводку изменений
Перед вызовом агента сформируй краткий список:
- Что добавлено (новые инструменты, классы, функции)
- Что изменилось (переименования, рефакторинг, изменение поведения)
- Что удалено или устарело
- Какие вопросы из OPEN_QUESTIONS теперь закрыты
### 2. Запусти Agent с моделью Sonnet
Используй инструмент `Agent` со следующими параметрами:
```
subagent_type: "general-purpose"
model: "sonnet"
description: "Обновление документации: <одна строка что изменилось>"
prompt: <см. шаблон ниже>
```
### 3. Шаблон промпта для Sonnet
Заполни `<СВОДКА_ИЗМЕНЕНИЙ>` и передай Sonnet:
---
Ты — помощник по документации проекта **kompas3d-mcp** (MCP-сервер для
автоматизации CAD-системы КОМПАС-3D через COM API, .NET 8, C#).
**Задача:** обнови документацию так, чтобы она точно отражала текущее состояние
проекта. Вот что изменилось:
```
<СВОДКА_ИЗМЕНЕНИЙ>
```
**Файлы для обновления** (трогай только те, которых касаются изменения):
| Файл | Что содержит |
|------|-------------|
| `docs/presentation.html` | **Презентация проекта — синхронизируй ВСЕГДА.** Счётчики (число инструментов, тестов), бейдж статуса, прогресс-бар, панель инструментов, дорожная карта |
| `README.md` | Обзор проекта, список возможностей, команды сборки/запуска |
| `CLAUDE.md` | Гайд для агента: «Current state», ключевые факты реализации, что не сделано |
| `docs/ARCHITECTURE.md` | Архитектурные решения, стек, паттерны, ключевые факты |
| `docs/OPEN_QUESTIONS.md` | Нерешённые вопросы и ограничения |
**Правила:**
1. Прочитай каждый файл перед правкой — не угадывай содержимое.
2. Обновляй только секции, которые реально затронуты изменениями.
Не переписывай то, что остаётся актуальным.
3. Если изменение закрывает вопрос из `OPEN_QUESTIONS.md` — удали его
или перенеси в раздел «Решено» (если такой есть).
4. Сохраняй стиль, структуру и тон существующих документов.
5. Будь лаконичен: одно предложение вместо абзаца, если смысл не теряется.
6. Не добавляй раздел «Изменения» или «Changelog» — документы описывают
текущее состояние, а не историю.
После правок кратко сообщи, что именно было изменено в каждом файле.
---
## Пример вызова
```
Запусти docs-delegate со следующей сводкой:
- Добавлен инструмент get_part_info (масса, объём, материал)
- QueryTools.cs перенесён в отдельный файл
- Закрыт вопрос из OPEN_QUESTIONS про get_part_info
```
Или короче: «обнови доки — добавил get_part_info».
+4 -4
View File
@@ -8,7 +8,7 @@ description: >
сборку, померить, отрендерить, экспортировать). Здесь — playbook'и и эвристики,
выработанные в usecases/. Триггеры: «построй деталь в КОМПАС», «импортируй STEP»,
«что в этой сборке», «нарасти/измени деталь», «экспортируй STEP», «сделай снимок модели».
НЕ для поиска по справке SDK — это навык kompas-sdk-research.
НЕ для поиска по справке SDK — это субагент kompas-sdk-research.
---
# kompas-3d — методика управления КОМПАС-3D через MCP
@@ -30,7 +30,7 @@ description: >
Любая задача «сделать что-то В КОМПАС» через MCP: создать/править деталь, эскизы и операции,
импорт/экспорт обменных форматов, разбор сборки, измерения, снимки. Для поиска сигнатур/констант
в справке — отдельный навык `kompas-sdk-research`. Для правки документации — `docs-delegate`.
в справке — отдельный субагент `kompas-sdk-research`. Для правки документации — субагент `docs-maintainer`.
## Два правила прежде всего
@@ -128,6 +128,6 @@ description: >
- Инструменты и их реализация: `src/Kompas.Mcp.Host/Tools/*`, `src/Kompas.Mcp.Core/*`.
- Кейсы (полигон подходов): `usecases/` (см. `usecases/README.md`).
- Поиск по справке SDK: навык **kompas-sdk-research**.
- Правка документации проекта: навык **docs-delegate**.
- Поиск по справке SDK: субагент **kompas-sdk-research**.
- Правка документации проекта: субагент **docs-maintainer**.
- Память: `kompas-step-and-assembly-api` (проверенные COM-приёмы импорта/экспорта/обхода).
+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)».'''
+55 -32
View File
File diff suppressed because one or more lines are too long
+11 -6
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
}
```
## Инструменты (72 инструмента)
## Инструменты (83 инструмента)
| Группа | Инструменты |
|---|---|
@@ -54,6 +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_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` |
### Типовой сценарий
@@ -72,11 +75,12 @@ 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/ 156 тестов: 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
.claude/skills/ навыки: docs-delegate, kompas-sdk-research, kompas-3d (методика)
.claude/skills/ навык kompas-3d (методика)
.claude/agents/ субагенты-делегаты: docs-maintainer (Sonnet), kompas-sdk-research (Haiku)
```
## Тесты
@@ -91,9 +95,10 @@ dotnet test --filter "Category=Integration" # только integration
- **STA-поток** (`KompasDispatcher`) владеет всеми COM-вызовами — КОМПАС однопоточный STA.
- Подключение через API5 `KompasObject` (`KOMPAS.Application.5`) → API7 `IApplication` (`ksGetApplication7`).
- 3D строится через API5 `ksPart` (эскизы, выдавливание, вращение, скругление/фаска); снимок — `ksDocument3D.SaveAsToRasterFormat`. Прямое редактирование импортированной B-rep (`move_face`) — через API7 `IPart7.FindObjectsByPoint` + `FaceMover`. Структурный осмотр (`describe_model` и др.) — через `ModelInspectionService` (дерево операций via `ksPart.GetFeature()`, тела via `BodyCollection()`, переменные via `GetFeature().VariableCollection`, измерения via `ksMeasurer`). Управление переменными (`create_variable`, `set_variable`, `delete_variable`) — через `VariableService` (API5). Вставка компонента в сборку (`assembly_add_component`) — через `AssemblyService` (API7, `IParts7.AddFromFile` + `IPlacement3D`).
- STEP импорт/экспорт — `IKompasDocument3D1.ConvertFromAdditionFormat`/`ConvertToAdditionFormat` через встроенный конвертер.
- 3D строится через API5 `ksPart`, снимок — через `ksDocument3D`; прямое редактирование импортированной B-rep — через API7. Структурный осмотр, переменные, сборки и чертежи инкапсулированы в сервисах COM-слоя.
- STEP импорт/экспорт — через встроенный конвертер КОМПАС.
- MCP — официальный C# SDK `ModelContextProtocol`, транспорт stdio, логи в stderr.
- Детали реализации, обоснования и проверенные COM-приёмы — в [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) и [CLAUDE.md](CLAUDE.md).
## Лицензирование
+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 для большинства семейств и безопасное восстановление. Все пять сквозных сценариев поэтому остаются частичными или неполными.
+21 -8
View File
File diff suppressed because one or more lines are too long
+47
View File
@@ -188,3 +188,50 @@ stateful-сессии? Пока полагаемся на последовате
### ✅ Сопряжения в сборках — реализованы (coincidence/distance)
**Решено (инкремент 2).** Новый инструмент `assembly_add_mate(mateType, x1,y1,z1, x2,y2,z2, value=0)`. Паттерн API7: `top.MateConstraints` (`IMateConstraints3D`) → `Add(MateConstraintType)``BaseObject1/2` (грани через `top.FindObjectsByPoint(x,y,z,FirstLevel=false)`) → `ParamValue``Update()``RebuildModel(true)`; проверка `mate.Valid`. Типы `mc_Coincidence=0`/`mc_Distance=5` проверены вживую. Enum `MateType` + static `Mates` в `src/Kompas.Mcp.Core/Assemblies/MateType.cs`. Итог: **72 инструмента, 156 тестов (98 unit + 58 integration).** Следующий шаг: прочие типы сопряжений (`parallel`, `perpendicular`, `concentric`, `angle`, `tangency`).
### ✅ Стандартные виды чертежа — реализованы (начало вехи «2D-чертёж»)
**Решено.** Новый инструмент `drawing_create_standard_views(partFilePath, scale, x, y)` — создаёт ассоциативные виды (спереди/сверху/слева) на активном чертеже. Реализовано через API7 `DrawingService` (namespace `Kompas.Mcp.Core.Drawings`), `DrawingTools`. Паттерн: `IKompasDocument2D.ViewsAndLayersManager.Views.AddStandartViews(path, "#Спереди", object[]{1,3,5}, x, y, scale, 20, 20)``object[]` как SAFEARRAY VT_I4. Проверено вживую: `Views.Count` 1→4, `ObjectCount>0` сразу (rebuild не нужен).
### ✅ Основная надпись (штамп) чертежа — реализована (инкремент 2 вехи «2D-чертёж»)
**Решено.** Новый инструмент `drawing_fill_title_block(designation?, name?, material?)` — заполняет графы основной надписи активного чертежа. Паттерн API7: `doc2d.LayoutSheets.ItemByNumber[1].Stamp` (`IStamp`) → `stamp.Text[columnId].Str = text``stamp.Update()`. `IText.Str` замещает содержимое напрямую (повторная запись перезаписывает — `Clear` не нужен, подтверждено round-trip тестом). Номера граф `ksStampEnum`: обозначение=2, наименование=1, материал=3. Свой enum `StampField` + `StampFields.ColumnId/Collect` в `src/Kompas.Mcp.Core/Drawings/StampField.cs`; инструмент в `DrawingTools.cs`. Итог: **74 инструмента, 188 тестов (123 unit + 65 integration).** Следующий шаг: размеры на видах.
### ✅ Линейные размеры на видах чертежа — реализованы (инкремент 3 вехи «2D-чертёж»)
**Решено.** Новый инструмент `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).**
### ✅ Радиальный и угловой размеры на виде чертежа — реализованы (инкремент 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-ЧЕРТЁЖ».*
+15 -6
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 (72 инструмента)</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 · навык kompas-3d · 72 инструмента · 156 тестов</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">72 инструмента, отдаются по 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,7 +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><span class="mark wip"></span><span>пакет 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>
@@ -321,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">Итого 156 тестов зелёных (98 unit + 58 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>
@@ -350,7 +351,15 @@
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">отверстия под крепёж · готово ✓</div><h4>Цековка, зенковка и конус</h4><p><b>hole_counterbore</b> (ksHTCounterbore) · <b>hole_countersink</b> (ksHTCountersinking) · <b>hole_conic</b> (ksHTConic, IConicHoleParameters) · HoleCore рефакторен (configure-колбэк) · 70 инструментов · 119 тестов</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">сборки start · готово ✓</div><h4>Вставка компонентов</h4><p><b>assembly_add_component</b> (API7 IParts7.AddFromFile + IPlacement3D)</p></div>
<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 cur"><div class="ph">далее · в работе</div><h4>пакет E продолжение</h4><p>вспомогательная геометрия (ось, угловая плоскость) · прочие типы сопряжений (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-чертёж · инкремент 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" 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>
+40 -131
View File
@@ -4,142 +4,51 @@
---
Продолжаем разработку MCP-сервера КОМПАС-3D (kompas3d-mcp). Работай автономно,
с коммитами и пушами в `main`; используй Codex для ревью каждой реализованной фичи.
КОМПАС-3D должен быть запущен (нужен для интеграционных тестов).
Продолжаем разработку MCP-сервера КОМПАС-3D (**kompas3d-mcp**). Работай автономно: коммить и
пушь в `main`, каждую фичу (спек и реализацию) прогоняй через ревью Codex. Для интеграционных
тестов **КОМПАС-3D должен быть запущен**.
**Фокус этой сессии — два новых крупных класса: СБОРКИ и 2D-ЧЕРТЁЖ.** До сих пор сделана
только геометрия ДЕТАЛИ (она близка к насыщению). Сборки и чертёж — самый ценный
нереализованный функционал. Начни со СБОРОК (тестируются объёмом/габаритом/числом компонентов),
затем ЧЕРТЁЖ.
**Оставшиеся этапы — канонический бэклог: [`docs/TODO.md`](TODO.md)** (по классам + сквозное; верх раздела
«2D-ЧЕРТЁЖ» = следующий приоритет). Обновляй его по завершении каждого инкремента.
## Текущее состояние (main, синхронизирован с origin)
- 72 MCP-инструмента, 156 тестов зелёных (98 unit + 58 integration).
- **СБОРКИ: инкременты 1 и 2 СДЕЛАНЫ** (API7, `AssemblyService`, namespace `Kompas.Mcp.Core.Assemblies`):
- `assembly_add_component(filePath, x, y, z)` — вставка детали/подсборки из `.m3d`/`.a3d` с позицией.
Workflow: `IParts7.AddFromFile(path,true,true)``part.Placement.SetOrigin(x,y,z)`
`UpdatePlacement(true)``top.RebuildModel(true)`. Эмпирика: `UpdatePlacement`=FALSE для вручную
позиционируемого компонента — НЕ ошибка; позицию применяет SetOrigin+RebuildModel; разфиксация
первого компонента НЕ нужна.
- `assembly_add_mate(mateType, x1,y1,z1, x2,y2,z2, value)` — сопряжение граней двух компонентов.
Workflow: `(IMateConstraints3D)top.MateConstraints``Add(MateConstraintType)``BaseObject1/2`
(грани через `top.FindObjectsByPoint(x,y,z,FirstLevel=false)`) → `ParamValue` (distance) →
`Update()``RebuildModel(true)` → проверка `mate.Valid` (false→ошибка+откат). Типы: `coincidence`,
`distance` (value>0). Решатель может двигать любой компонент → тест по ширине габарита.
- Спеки: `2026-05-27-assembly-add-component-design.md`, `2026-05-27-assembly-add-mate-design.md`.
- **Тест-инфра:** `IntegrationTestBase` (per-test `DocumentService.CloseAllAsync`) — все интеграционные
классы наследуют его.
- **СЛЕДУЮЩЕЕ — инкремент 3 (по желанию) или ВЕХА 2 (2D-ЧЕРТЁЖ).** См. ниже.
- Деталь: эскизы (пакет A), формообразующие (extrude/revolve/fillet/chamfer/shell/rib/sweep/loft),
массивы и зеркало (пакет C), уклон (`draft`), все типы отверстий (`hole`/`_counterbore`/
`_countersink`/`_conic`, API7), переменные (`create/set/delete_variable`), STEP import/export,
прямое редактирование B-rep (`move_face`/`split_solid_by_plane`/`move_body`/`boolean_union`),
структурный осмотр (`describe_model`/`list_*`/`measure`).
- **`list_components` УЖЕ ЕСТЬ** (читает сборку: `IKompasDocument3D.TopPart → IPart7.Parts (IParts7)`).
- Документы: `DocumentService` уже умеет создавать `Assembly` и `Drawing`
(`KompasDocumentType.Assembly/Drawing``DocumentTypeEnum.ksDocumentAssembly/ksDocumentDrawing`).
Есть `document_save_as` (сохранить деталь в файл — нужно, чтобы вставить её в сборку/на чертёж).
- API7-сервисы существуют как образец: `HoleService`, `FaceEditService` (паттерн: `(IXxx)part`
через COM-QI, операции на STA-потоке `KompasDispatcher`).
- Прочитай память: `kompas-step-and-assembly-api` (обход сборки, извлечение компонента, STEP),
`kompas-hole-api7` (паттерн API7-сервиса), `kompas-parametric-sketch-findings` (тупик — не повторять).
**Сначала сориентируйся** — этот промт задаёт направление, а не заменяет источники:
- `CLAUDE.md` — текущее состояние, проверенные факты реализации и конвенции (STA-поток, COM-QI
контейнеры, отложенный долг по RCW, команды сборки/тестов, инфраструктура интеграционных тестов).
- Навык `kompas-3d` — методика управления КОМПАС через MCP (базовый цикл, `validate_part`, осмотр).
- Авто-память (читай по теме перед работой): `kompas-drawing-api7`, `kompas-assembly-insert-api7`,
`kompas-step-and-assembly-api`, `kompas-hole-api7`, `kompas-variables-api5`,
`kompas-parametric-sketch-findings`. Спеки фич — `docs/superpowers/specs/`.
## ОТРИЦАТЕЛЬНЫЙ результат (не повторять)
- **Параметрические эскизы (переменная двигает геометрию) — НЕДОСТИЖИМЫ через COM-API.**
`ksCDimWithVariable` (`IDrawingObject1.NewConstraint`) `Create()`=FALSE из внешней автоматизации;
`ksCFixedLenght` игнорирует `Expression`-ссылку на переменную. См.
`docs/superpowers/specs/2026-05-27-parametric-sketch-findings.md`. НЕ трогать повторно.
**Ревью-гейт (важно):** спек И реализацию каждой фичи прогонять через **три ревьюера параллельно**
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).
## Веха 1 — СБОРКИ (начать отсюда; новый `AssemblyService` на API7)
**Состояние (main = origin):** 83 MCP-инструмента, 331 тест зелёный, сборка Release чистая. Три класса:
- **Деталь** — насыщена (эскизы, формообразующие, массивы/зеркало, отверстия, уклон, переменные,
STEP, прямое редактирование B-rep, структурный осмотр). Есть навык `kompas-fdm-design` (DFM под FDM-печать).
- **Сборки** — базовое готово (вставка компонента, сопряжения coincidence/distance).
- **2D-чертёж** — виды, основная надпись, размеры (инкр. 5), текстовые обозначения (инкр. 6),
ассоциативная привязка диам./радиального к окружности (инкр. 7), формат/ориентация листа (инкр. 8),
**выноска с текстом** (`drawing_add_leader`; инкр. 9).
Точки входа (проверено рефлексией interop, всё есть):
- Создать сборку: `DocumentService.CreateAsync(KompasDocumentType.Assembly)`.
- `IKompasDocument3D.TopPart → IPart7``IPart7.Parts` (это `IParts7`).
- **Вставка компонента из файла:** `IParts7.AddFromFile(string fileName, bool, bool)` или
`AddFromFileWithParam(string fileName, IPlacement3D placement, IInsertPartParameters prm, bool)`.
Позиция/ориентация — `IPlacement3D` (есть `IPlacement3DManipulator`).
- **Сопряжения (mates):** `IMateConstraints3D`/`IMateConstraint3D` + типы `IMate3DByAngle`,
`IMate3DByTangent`, `IMate3DSymmetry`, и др. (совпадение/параллельность/расстояние/угол).
**Фокус — веха 2D-ЧЕРТЁЖ** (самый ценный растущий класс; деталь и сборки близки к насыщению).
Осмотри `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`.
Рекомендуемые инкременты (по одному, до зелёного → merge):
1.**СДЕЛАНО:** `assembly_add_component(filePath, x, y, z)` — вставка детали из файла в сборку.
2.**СДЕЛАНО:** `assembly_add_mate` — сопряжения coincidence/distance (см. «Текущее состояние»).
Расширения (будущее): прочие типы (parallel/perpendicular/concentric/angle/tangency — добавить с
геометрической проверкой, concentric требует цилиндрических граней), параметр `Alignment`
(`ksMCAlignmentCooriented`/`Opposite`/`Closest`). Для concentric/angle проверять тип грани.
3. **(по желанию)** Перемещение/массив/фиксация компонентов. ИЛИ сразу переходить к ВЕХЕ 2 (чертёж) —
она самый ценный нереализованный класс после сборок.
**Метод (кратко):** при неопределённости (привязка к геометрии, координаты, единицы) — спайк до
спека; затем краткий спек → ревью Codex → TDD (валидаторы в чистый static для unit, интеграционный
тест наследует `IntegrationTestBase`) → ревью Codex реализации → документация субагентом
`docs-maintainer` → merge/push в `main` → обнови этот хэндофф и память. Число тестов задавай по
раннеру `dotnet test`, не по числу `[Fact]`/`[Theory]`.
Проверенный API сопряжений (для расширений): `(IMateConstraints3D)top.MateConstraints`
`Add(MateConstraintType)``IMateConstraint3D` (`BaseObject1`/`BaseObject2` = IModelObject граней;
`ParamValue` для distance/angle; `Alignment`) → `Update()``RebuildModel(true)`. Типы:
`mc_Coincidence=0`, `mc_Parallel=1`, `mc_Perpendicular=2`, `mc_Tangency=3`, `mc_Concentric=4`,
`mc_Distance=5`, `mc_Angle=6`, `mc_Symmetric=11`. Грани компонентов — `top.FindObjectsByPoint(x,y,z,
FirstLevel=false)` (мировые координаты с учётом placement). Память: `kompas-assembly-insert-api7`.
**Не повторять:** параметрические эскизы (переменная двигает геометрию) недостижимы через COM-API —
см. память `kompas-parametric-sketch-findings` и спек `2026-05-27-parametric-sketch-findings.md`.
Тонкости: компоненту нужен сохранённый файл-источник (сначала сохрани деталь). Уточни через
`kompas-sdk-research` точные сигнатуры `AddFromFile`/`AddFromFileWithParam`, что такое
`IInsertPartParameters` и как задать `IPlacement3D` (СК вставки).
## Веха 2 — 2D-ЧЕРТЁЖ (после сборок; новый `DrawingService` на API7)
Точки входа (проверено):
- Создать чертёж: `DocumentService.CreateAsync(KompasDocumentType.Drawing)`.
- `IKompasDocument2D.ViewsAndLayersManager.Views` (это `IViews`).
- **Стандартные ассоциативные виды детали из файла:**
`IViews.AddStandartViews(string FileName, string ProjectionName, object ProjectionsTypes,
double X, double Y, double Scale, double DX, double DY)` — создаёт виды (спереди/сверху/слева)
по сохранённой модели.
- Отдельный вид: `IViews.Add(LtViewType)`; геометрия/размеры вида — через `IDrawingContainer`
вида (`LineSegments`/`Circles`/… — как в разведке параметрики).
Рекомендуемые инкременты:
1. `drawing_create_standard_views(partFilePath, scale)` — чертёж со стандартными видами детали.
Тест: построить деталь → сохранить → создать чертёж → `AddStandartViews` → проверить
`Views.Count > 0` (виды созданы). Объёмом не проверить — считать виды/объекты.
2. Размеры/обозначения на видах (линейные/диаметральные), рамка/основная надпись — по мере надобности.
Тонкости: чертёж проверяется не объёмом, а числом видов/объектов; ассоциативные виды требуют
сохранённого `.m3d`-файла детали. Уточни workflow через `kompas-sdk-research`.
## Процесс на каждую операцию (отлажен в прошлых сессиях)
Сборки/чертёж — это НЕ `PartModeler` (он API5-деталь). Заводи НОВЫЕ сервисы (`AssemblyService`,
`DrawingService`) и НОВЫЕ классы инструментов (`AssemblyTools`, `DrawingTools`); регистрируй
сервисы в DI (`Program.cs`, `AddSingleton`). Образец API7-сервиса — `HoleService`.
1. Рефлексия interop — подтверди сигнатуры (`AddFromFile`, `AddStandartViews`, mate-типы):
```powershell
$a7=[Reflection.Assembly]::LoadFrom((Resolve-Path 'libs/kompas-interop/KompasAPI7.dll'))
# члены IParts7 / IMateConstraints3D / IViews — имена методов, типы параметров
```
Если высокая неопределённость (как было с параметрикой) — сделай spike ДО спека.
2. Семантику API уточняй через навык `kompas-sdk-research` (субагент Sonnet), не рой справку сам.
3. Краткий спек в `docs/superpowers/specs/` → коммит → ОТДАЙ НА РЕВЬЮ Codex (`codex:rescue`,
`--fresh` для новой темы) → внеси обоснованные правки (оценивай технически, не слепо).
4. Реализация TDD: метод в новом сервисе (паттерн API7 как в `HoleService`), инструмент в новом
классе `*Tools.cs`, интеграционный тест в новом `*Tests.cs` (Collection `KompasCollection`).
5. Codex-ревью реализации (`--resume` того же треда) → правки.
6. Документация через навык `docs-delegate` (Sonnet обновит README/CLAUDE/ARCHITECTURE/
presentation.html). **ВАЖНО:** число тестов задавай субагенту ТОЧНО (по `dotnet test` раннеру,
НЕ по числу `[Fact]`/`[Theory]` — теории разворачиваются в несколько кейсов).
7. `merge --no-ff` в main, удали ветку, `push origin main`. Обнови этот handoff.
## Паттерн интеграционного теста
`CreateAsync(...)` → построить/вставить → операция → `RebuildAsync`/`Update` →
проверить результат; `finally CloseAsync`. Для СБОРКИ — объём/габарит сборки или `list_components`
(число компонентов). Для ЧЕРТЕЖА — число видов (`Views.Count`)/объектов (объёмом не проверить).
Артефакты (сохранённые `.m3d`) — в gitignored `.scratch/`.
## Ключевые ловушки (проверено)
- Все COM-вызовы — на STA-потоке (`KompasDispatcher.InvokeAsync`). Контейнеры API7 берём
COM-QI: `(IModelContainer)part`, `(IParts7)part.Parts` и т.п.
- Выбор грани/ребра — по стабильному индексу из `list_faces`/`list_edges`; для сопряжений
объекты компонентов искать через их `IPart7`/`FindObjectsByPoint`.
- Углы — в градусах. Валидацию — в чистые static (unit-тестируемо), проверяй `double.IsFinite`.
- НЕ освобождай транзитные RCW точечно — общий отложенный долг (OPEN_QUESTIONS v2-2).
- Команды: `dotnet build -c Release`; `dotnet test --filter "Category=Unit"`;
`dotnet test --filter "Category=Integration"` (нужен запущенный КОМПАС).
Начни с рефлексии interop `IParts7` (вставка компонента) + `kompas-sdk-research` по
`AddFromFile`/`AddFromFileWithParam`/`IPlacement3D`, затем первый инкремент —
`assembly_add_component` (вставка детали из файла в сборку).
Начни с обзора текущих возможностей 2D-чертежа и пробелов, предложи следующий инкремент, согласуй
направление — и приступай.
@@ -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.2–0.3 мм** на провис. *Граница:* величину
провиса из CAD не предсказать (обдув/скорость — настройки печати).
- **Внутренние/потолочные нависания хуже наружных** — потолок пазов аркой/шевроном, не плоским
пролётом > 2 мм.
- **Доступ к поддержкам:** окно во внутренней полости **≥8–10 мм**.
## 3. Отверстия
- **Вертикальные (ось ∥ Z)** печатаются уже номинала → **увеличить диаметр модели** (радиус на
половину): **+0.2 мм (Ø<4)**, **+0.20.3 мм (Ø 410)**, **+0.10.2 мм (Ø>10)**; калибровать,
критичные — рассверливать.
- **Горизонтальные (ось в XY)** → **teardrop** или **D-отверстие** (плоский верх). Мин. Ø **2 мм**.
Круглая часть тоже печатается уже → **+0.1–0.2 мм** к её Ø.
- **Геометрия teardrop:** боковины касательны окружности под углом `θ_max` к вертикали (с двух
сторон), сходятся в вершине на вертикальной оси. Высота вершины над центром = **`r / sin θ_max`**;
включённый угол при вершине = **`2·θ_max`**. При `θ_max`=45° → `r/sin45° = √2·r ≈ 1.414·r` над
центром (= **`0.414·r` над верхом окружности**), угол 90°. Низ — оставшаяся дуга окружности.
- **Глухое отверстие:** дно = внутренний мост → **толщина дна ≥2–3 мм** или купольное/
вентилируемое. Сквозные предпочтительнее.
- **Отступ от края** — через остаточную перемычку: стенка между отверстием и краем **≥2–3·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.5–1.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.5–2.0); **но** радиус **> ~½ толщины стенки** сам даёт
нависание > `θ_max` → тогда фаска/ступень.
- **Внутренние углы — всегда скругление ≥R0.5**.
## 11. Зенковки / цековки
- **Цековка (counterbore)** — большим Ø/полостью **вверх** (дно по телу, не мостом); глубина +0.3 мм.
- **Зенковка (countersink):** конус **вверх**; включённый угол **≤90°** → стенки ≤45° от вертикали
печатается; **>90°** → стенки-нависание → поддержка или замена цилиндрической цековкой.
## 12. Заходные фаски (assembly relief)
- На штифтах, отверстиях, инсёртах, защёлках, «ласточкиных хвостах» — **заходная фаска** (≈0.5–1 мм
× 45° или ≈ половина зазора) против задиров при сборке.
## 13. Разбиение детали и сборка из печатных частей
- Конфликт «прочная ориентация vs бесподдержечность», или крупная/коробящаяся деталь → **разбить**
на части с самоустанавливающимися стыками (печатные штифты/шпонки/замки), склейка; стыки на
XY-гранях. Зазор стыка — по §4.
## 14. Защёлки (snap-fit) / живой шарнир
- Консольная защёлка: толщина балки **≥2–3·w (≈1.0–2.0 мм)**, зацеп/возврат **0.3–0.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).
````
- [ ] **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,108 @@
# Дизайн: диаметральный размер на виде чертежа (drawing_add_diametral_dimension) через API7
**Дата:** 2026-05-27
**Статус:** реализовано и проверено (5 интеграционных + unit, всего 218); ревью Codex спека и реализации учтено
## Правки по ревью реализации Codex
- **#2** инвариант диаметра: пост-проверка `value <= 0 || !double.IsFinite(value)` (а не `Abs<1e-6`).
- **#3** сообщение `drawing_create_standard_views` обобщено: «номера видов для инструментов размеров».
- **#4a** `DimensionAngles.ToRadians` — проверка конечности результата (защита от переполнения).
- **#5d** тест `viewNumber: 999` (виды есть, номер не найден) — покрывает ветку FindView.
- **#6** комментарий `DimensionType` переформулирован (просто COM-дефолт, без претензии на «стиль»).
- **#4b** минимальный радиус — отклонён (`>0` честный контракт). **#5a/#5b/#5c** (реальная окружность,
read-back угла, пост-Add откат) — отклонены (инструмент geometry-agnostic; ToRadians покрыт unit;
пост-Add сбой для radius>0 не форсировать). Транзитный сбой пакета (нестабильность КОМПАС после
большого прогона) не воспроизводится — повторный прогон 5/5 зелёный.
## Правки по ревью Codex
- **#5** `((IDimensionText)dim).AutoNominalValue = true` выставляем явно (консистентно с линейным).
- **#7** извлечь `RequireSymbols2DContainer(viewNumber)` (= `(ISymbols2DContainer)FindView(...)`) —
переиспользуют диаметральный путь и счётчики (радиальный — следующий, тот же паттерн).
- **#4** конверсия °→рад вынесена в чистый `DimensionAngles.ToRadians(deg)` + unit-тест (значение от
угла не зависит → математику проверяем отдельно).
- **#2** `DimensionType` НЕ задаём — принимаем COM-дефолт (стандартный стиль; полярность bool не
подтверждена, спайк с дефолтом дал `Valid` + верное значение). Зафиксировано комментарием.
- **#1** размер «свободный» (по Xc/Yc/Radius, без `BaseObject`) — как у линейного; LLM задаёт верные
параметры окружности. Ассоциативная привязка — будущее. Отражено в описании инструмента.
- **#3** `angle` — любой конечный (направление выноски, КОМПАС нормализует); валидируем лишь
конечность. **#6** откат после `Add()` для диаметрального не форсируется (radius>0 → всегда Valid,
value=2R>0); код отката идентичен протестированному линейному (zero-projection тест).
## Цель
Веха 2D-ЧЕРТЁЖ, инкремент 4. После линейного размера — **диаметральный** размер окружности на виде
(Ø для отверстий/цилиндров — частый случай конструкторского чертежа). Радиальный (с ветвями) и
угловой — следующие инкременты.
## Спайк: механизм подтверждён (НЕ разучивать)
Та же модель, что у линейного размера (`ISymbols2DContainer` QI от вида). Спайк
(`DrawingDiametralSpikeTests`, зелёный):
```
ISymbols2DContainer symbols = (ISymbols2DContainer)view; // QI от конкретного IView
IDiametralDimension dim = symbols.DiametralDimensions.Add();
dim.Xc = xc; dim.Yc = yc; // центр окружности (ЛОКАЛЬНАЯ СК вида, мм)
dim.Radius = radius; // радиус окружности (мм)
dim.Angle = angleRad; // направление выноски (РАДИАНЫ)
bool ok = dim.Update();
double value = ((IDimensionText)dim).NominalValue; // = ДИАМЕТР (2·Radius)
```
**Проверено:** `Xc=10,Yc=10,Radius=15,Angle=0.785``Update=True`, `Valid=True`,
`NominalValue == 30` (= диаметр = 2·Radius). `Angle` — в **радианах** (конвенция 2D-API КОМПАС;
для размера задаёт лишь направление выноски, на значение не влияет).
**Координаты** — локальная СК вида (как у линейного). Размер «свободный» (по Xc/Yc/Radius), без
ассоциативной привязки к геометрии (`BaseObject` не задаём — будущее).
## MCP-инструмент
| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_add_diametral_dimension` | `xc,yc` (центр), `radius`, `angle=0` (°), `viewNumber=0` | Поставить диаметральный размер (Ø) окружности с центром (xc,yc) и радиусом `radius` (ЛОКАЛЬНАЯ СК вида, мм) на виде. `angle` — направление выноски в ГРАДУСАХ (внутри → радианы). `viewNumber`: 0 = первый/главный вид. Значение (диаметр = 2·radius) измеряется автоматически. Возвращает диаметр и номер вида. |
`angle` экспонируется в **градусах** (LLM-дружелюбно), внутри умножается на π/180.
## Архитектура
В существующем `DrawingService` (рядом с `AddLinearDimensionAsync`); переиспользуем `FindView`,
`DrawingDimensionResult`, паттерн отката.
- `DrawingService.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angleDeg, ct)`.
- Инструмент `drawing_add_diametral_dimension` в `DrawingTools`.
- Helper `GetViewDiametralDimensionCountAsync` (для теста — попадание в вид; размеры в
`ISymbols2DContainer`, не в `ObjectCount` — как выяснено для линейных).
## Валидация (чистые static, unit-тест)
- `DrawingValidation.RequireFiniteCoords(xc, yc, angleDeg)` (уже есть).
- `DrawingValidation.RequirePositiveRadius(double radius)` (новый) — конечный и `> 0`.
## Реализация
`AddDiametralDimensionCore`: валидация (coords + radius>0) → `FindView``(ISymbols2DContainer)view`
`DiametralDimensions.Add()` (null-check) → `Xc/Yc/Radius`, `Angle = angleDeg·π/180``Update()`
(FALSE → откат `dim.Delete` + ошибка) → `Valid` (false → откат) → `value = ((IDimensionText)dim).
NominalValue` (диаметр); если `≤ 0` → откат + ошибка → вернуть `DrawingDimensionResult { Value, ViewNumber }`.
RCW точечно не освобождаем (v2-2).
## Тестирование
### Unit (`DrawingValidationTests`)
- `RequirePositiveRadius` — бросает на 0/отрицательном/NaN/Infinity; пропускает положительный.
### Integration (`DrawingDiametralTests`)
1. **Значение = диаметр**: Radius=15 → `Value ≈ 30`.
2. **Попадание в вид**: `*DiametralDimensions.Count` целевого вида +1 (адресация по `viewNumber` из
`ViewNumbers`).
3. **Радиус ≤ 0**`ArgumentOutOfRangeException` (откат не нужен — до `Add`).
4. **Нет видов** → понятная ошибка.
(Сценарий 1 = промотированный спайк; спайк-файл удаляется.)
## Дальнейшее (вне спека)
- Радиальный размер (`RadialDimensions`, ветви), угловой; ассоциативная привязка к окружности вида
(`BaseObject`); рамка/формат листа.
@@ -0,0 +1,138 @@
# Дизайн: линейный размер на виде чертежа (drawing_add_linear_dimension) через API7
**Дата:** 2026-05-27
**Статус:** реализовано и проверено (7 интеграционных + unit, всего 203); ревью Codex спека и реализации учтено
## Правки по ревью реализации Codex
- **#4 нулевое значение** (важно): после `Update`+`Valid` проверяем `|NominalValue| > 1e-6` — ловит
несовместимость ориентации с точками (horizontal для точек на одной вертикали: `|X2-X1|=0`), которую
`RequireDistinctPoints` не видит. При нуле — откат (`dim.Delete`) + ошибка. Тест добавлен.
- **#3 Parallel** (важно): добавлен интеграционный тест — параллельный (0,0)-(30,40) → 50 (3-4-5).
**Работает БЕЗ явного `Angle`** (КОМПАС авто-вычисляет угол по точкам) — Parallel оставлен.
- **#7 rollback** (важно): тест нулевой проекции проходит через ветку `dim.Delete()` (откат) —
проверяет, что число размеров вида не изменилось. (Update==false/Valid==false без fault injection.)
- **#8 ассоциативность вида** (минор): осознанный контракт «первый НЕсистемный вид» (задокументирован
в описании инструмента и ошибке FindView).
## Находка реализации: размеры НЕ в IView.ObjectCount
`IView.ObjectCount` считает **геометрию** (`IDrawingContainer`: линии/дуги), а размеры живут в
`ISymbols2DContainer` отдельно → после простановки размера `ObjectCount` НЕ меняется (проверено: 4→4).
Поэтому «размер попал в целевой вид» проверяем счётом `ISymbols2DContainer.LineDimensions.Count`
(helper `GetViewLineDimensionCountAsync`), а не `ObjectCount`. (Гарантия непустоты у стандартных видов
по `ObjectCount` остаётся верной — там именно геометрия.)
## Правки по ревью Codex
- **Нулевая длина** (#1): `DrawingValidation.RequireDistinctPoints(x1,y1,x2,y2)` — расстояние между
точками > 1e-7 (иначе вырожденный размер); + проверка `dim.Valid` после `Update()` (откат);
+ негативный тест.
- **Контракт вида** (#4): честно — «первый НЕСИСТЕМНЫЙ вид (Number != 0)» при `viewNumber<=0`, без
претензии на ассоциативность и без QI к `IAssociationView` (в наших чертежах иных видов нет).
- **null-check + порядок** (#9): `LineDimensions.Add()` проверять на null; `AutoNominalValue=true`
выставлять явно; `NominalValue` читать ТОЛЬКО после `Update()==true && Valid==true`.
- **ViewNumbers** (#7): поле `required` в `DrawingViewsResult`; инициализатор и текст MCP-ответа
`drawing_create_standard_views` обновить (вернуть номера видов).
- **Тесты** (#6): ObjectCount ЦЕЛЕВОГО вида до/после; адресация конкретного `viewNumber`; негатив на
совпадающие точки. **viewNumber не найден** (#2) → понятная ошибка. 3D-конвертер точек (#8) — будущее.
## Цель
Веха 2D-ЧЕРТЁЖ, инкремент 3 (размеры). После видов и основной надписи — **линейный размер** на
ассоциативном виде: между двумя точками (в локальной СК вида) с авто-измерением длины. Размеры —
самый ценный элемент оформления (несут размерную информацию). Начинаем с линейного
(горизонтальный/вертикальный/параллельный); диаметральные/радиальные/угловые — далее.
## Спайк: механизм подтверждён вживую (НЕ разучивать)
Спайк (`DrawingDimensionSpikeTests`, зелёный) подтвердил путь API7:
```
IView view = первый вид с Number != 0; // системный вид — #0, пропускаем
ISymbols2DContainer symbols = (ISymbols2DContainer)view; // COM-QI ОТ ВИДА (не от документа!)
ILineDimension dim = symbols.LineDimensions.Add();
dim.X1=x1; dim.Y1=y1; dim.X2=x2; dim.Y2=y2; // выносные точки (ЛОКАЛЬНАЯ СК вида, мм)
dim.X3=x3; dim.Y3=y3; // положение размерной линии
dim.Orientation = ksLinDHorizontal; // 0=Parallel / 1=Horizontal / 2=Vertical
bool ok = dim.Update();
double value = ((IDimensionText)dim).NominalValue; // авто-измеренное значение (мм)
```
**Проверено (вид спереди коробки 40×30×20):** размер (0,0)-(40,0) горизонтальный →
`Update()=True`, `Valid=True`, `NominalValue == 40` (авто `|X2-X1|`), `view.ObjectCount` вырос.
**Подтверждённые факты:**
- **`ISymbols2DContainer` берётся COM-QI ОТ КОНКРЕТНОГО `IView`** — размер попадает именно в этот
вид (не нужен `Current=true`). (Аналогично 3D: `part``IModelContainer`.)
- **Координаты X1..Y3 — ЛОКАЛЬНАЯ СК вида** (= координаты модели на плоскости вида, при масштабе
1:1 совпадают с мм модели). НЕ координаты листа.
- **`AutoNominalValue=true` по умолчанию** → `NominalValue` = расстояние между точками
(горизонтальный: `|X2-X1|`, вертикальный: `|Y2-Y1|`, параллельный: реальная длина). Значение
читается `((IDimensionText)dim).NominalValue` (QI).
- `Orientation` (`ksLineDimensionOrientationEnum`): `ksLinDParallel=0`, `ksLinDHorizontal=1`,
`ksLinDVertical=2``Kompas6Constants`).
- `Update()` обязателен; перестроение документа не нужно.
## Адресация вида (часть инкремента)
Чтобы указать, НА КАКОМ виде ставить размер, нужен его `Number`. Сейчас
`drawing_create_standard_views` возвращает только `{Created, Total}`. **Расширяем `DrawingViewsResult`
полем `ViewNumbers` (номера созданных видов)** — уже собираются как `newViews` в
`CreateStandardViewsCore` (тривиально: `newViews.Select(v => v.Number)`). Это закрывает замечание
Codex из ревью видов (#10: «нет ссылок на созданные виды») и делает размеры адресуемыми.
## MCP-инструмент
| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_add_linear_dimension` | `x1,y1,x2,y2` (выносные точки), `x3,y3` (положение размерной линии), `orientation="horizontal"`, `viewNumber=0` | Поставить линейный размер на виде активного чертежа между точками (x1,y1)-(x2,y2) в ЛОКАЛЬНОЙ СК вида (мм). `orientation`: horizontal/vertical/parallel. `viewNumber`: номер вида (0 = первый ассоциативный/главный). Значение измеряется автоматически. Возвращает измеренное значение и номер вида. |
Контракт: активный документ — чертёж; на чертеже есть хотя бы один ассоциативный вид. Координаты —
локальные вида (см. выше). `drawing_create_standard_views` возвращает `ViewNumbers` для адресации.
## Архитектура
В существующем `DrawingService`. Свой enum `DimensionOrientation` (паттерн `MateType`/`StampField`).
- `src/Kompas.Mcp.Core/Drawings/DimensionOrientation.cs` — enum + static `DimensionOrientations`
(`Parse(string)`, `ToKompas``ksLineDimensionOrientationEnum`).
- `DrawingService.AddLinearDimensionAsync(viewNumber, x1,y1,x2,y2,x3,y3, DimensionOrientation, ct)`.
- `DrawingViewsResult` += `IReadOnlyList<int> ViewNumbers`.
- Инструмент `drawing_add_linear_dimension` в `DrawingTools` (orientation строкой → Parse).
## Валидация (чистые static, unit-тест)
- `DimensionOrientations.Parse` — horizontal/vertical/parallel (+рус.) → enum; иначе `ArgumentException`.
- `DimensionOrientations.ToKompas` — enum → `ksLineDimensionOrientationEnum` (0/1/2).
- `DrawingValidation.RequireFiniteCoords(params)` — координаты конечны (NaN/Infinity → ошибка).
## Реализация
`AddLinearDimensionCore`: валидация координат + orientation → `RequireActiveDrawing` → найти вид
(`viewNumber<=0` → первый с `Number!=0`; иначе по `Number`, ошибка если нет) → `(ISymbols2DContainer)view`
`LineDimensions.Add()` → задать X1..Y3 + Orientation → `Update()` (FALSE → откат `dim.Delete()` +
ошибка) → проверить `dim.Valid` (false → откат + ошибка) → вернуть `AddedDimension { Value =
((IDimensionText)dim).NominalValue, ViewNumber = view.Number }`. RCW точечно не освобождаем (v2-2).
## Тестирование
### Unit (`DimensionOrientationsTests`)
- `Parse` — horizontal/vertical/parallel и рус.-синонимы → enum; бросает на мусоре.
- `ToKompas` — каждый → 1/2/0.
- `RequireFiniteCoords` — бросает на NaN/Infinity; пропускает конечные.
### Integration (`DrawingDimensionTests`)
Паттерн: построить деталь → сохранить → создать чертёж → стандартные виды → размер → проверить →
`finally CloseAsync` (база `IntegrationTestBase`).
1. **Горизонтальный** (0,0)-(40,0) → `Value ≈ 40`; `view.ObjectCount` вырос.
2. **Вертикальный** (0,0)-(0,20) → `Value ≈ 20`.
3. **Адресация вида**: `CreateStandardViewsAsync` вернул `ViewNumbers` (3 шт); размер с конкретным
`viewNumber` ставится в этот вид.
4. **Нет видов**: чертёж без видов → понятная ошибка.
(Сценарий 1 = промотированный спайк; спайк-файл удаляется.)
## Дальнейшее (вне спека)
- Диаметральные/радиальные/угловые размеры; привязка к геометрии вида (ассоциативно, `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,128 @@
# Дизайн: стандартные виды чертежа (drawing_create_standard_views) через API7
**Дата:** 2026-05-27
**Статус:** реализовано и проверено (3 интеграционных + 18 unit, всего 177); ревью Codex спека (10) и реализации (7) учтено
## Правки по ревью реализации Codex (7 замечаний)
- **#1** непустоту проверяем у ВНОВЬ созданных видов (по разнице `IView.Number` до/после), а не по
всей коллекции — на непустом чертеже прежняя геометрия не маскирует пустые новые виды.
- **#3** откат: при `created != 3` или пустых видах удаляем созданные виды (`IView.Delete`) → чертёж
не остаётся в полу-состоянии.
- **#2** ленивость `ObjectCount` опровергнута эмпирически (happy-тест зелёный = геометрия есть сразу,
rebuild не нужен). **#7** добавлен unit-тест нормализации кириллического пути.
- **#4** повторный вызов намеренно добавляет ещё набор (дубликаты в разных `x,y` легитимны) —
отражено в описании инструмента. **#5** обход `IViews` уточнён (по `Number`); точечный release RCW
отклонён (v2-2). **#6** доп. негативы (.a3d, пустой валидный .m3d) — маргинальны, отложены.
## Решения по ревью Codex
- **#1** файл модели: `File.Exists` И `FileInfo.Length > 0` (несохранённый/битый файл).
- **#3/#5/#6** непустота видов — гарантия В СЕРВИСЕ: после создания суммируем `IView.ObjectCount`
по коллекции; если 0 → ошибка «виды пусты (модель не спроецировалась)». Это и проверяет, что
rebuild не нужен (геометрия уже есть). Координатные проверки (`GetProjectionPoint`) — избыточны.
- **#8** строгий контракт: `created == 3` (иначе ошибка с `before/after/ok`), а не `> 0`.
- **#9** позиция главного вида — опциональные `x`,`y` (дефолт 100/150), чтобы повторный вызов не
клал виды стопкой; `dx`/`dy`=20 фиксированы (YAGNI).
- **#10** возврат — record `DrawingViewsResult { Created, Total }` (не голый int) для отчёта и
будущей адресации видов.
- **#2** негативный unit-тест на чужое расширение (`.step`) — в плане.
- **#4** точечный release RCW отклонён (конвенция v2-2, как `AssemblyService`). **#7** неверный
`ProjectionName` не тестируем — `"#Спереди"` захардкожен, не управляется пользователем.
## Цель
Веха 2 — **2D-ЧЕРТЁЖ** (новый класс функционала после деталей и сборок). Инкремент 1: создать на
активном чертеже **стандартные ассоциативные виды** (спереди/сверху/слева) по сохранённой 3D-модели
`.m3d`/`.a3d`. Это фундамент чертежа — без видов лист пуст. Чертёж проверяется не объёмом, а
**числом созданных видов**.
## Спайк: механизм подтверждён вживую (НЕ разучивать)
Спайк (`DrawingSpikeTests`, зелёный) подтвердил путь API7:
```
IKompasDocument2D doc2d = (IKompasDocument2D)app.ActiveDocument; // активный чертёж
IViews views = doc2d.ViewsAndLayersManager.Views; // COM-свойства (не QI)
object[] projTypes = { 1, 3, 5 }; // ksPtFront=1, ksPtUp=3, ksPtLeft=5 (SAFEARRAY VT_I4)
bool ok = views.AddStandartViews(path, "#Спереди", projTypes, X:100, Y:150, Scale:1.0, DX:20, DY:20);
int count = views.Count;
```
**Проверенные факты:**
- `object[] { 1, 3, 5 }` **маршалится в SAFEARRAY VT_I4 без проблем** (риск из ревью SDK снят).
- Новый чертёж стартует с `Views.Count == 1` (системный вид #0). После `AddStandartViews` с тремя
кодами проекций `Count == 4`**создано ровно 3 вида** (спереди/сверху/слева). `ok == True`.
- `ProjectionName = "#Спереди"` — ориентация главного вида (стандартное имя проекции 3D-модели).
- `IKompasDocument2D` напрямую (без QI к `IKompasDocument2D1`) даёт `ViewsAndLayersManager.Views`
(подтверждено рефлексией interop — отличие от ревью SDK).
- Явный rebuild/save после `AddStandartViews` НЕ нужен — виды уже в документе (`Count` сразу 4).
**Параметры `AddStandartViews(FileName, ProjectionName, ProjectionsTypes, X, Y, Scale, DX, DY)`:**
- `FileName` — путь к `.m3d`/`.a3d`.
- `ProjectionName` — ориентация главного вида (`"#Спереди"`).
- `ProjectionsTypes``SAFEARRAY VT_I4` кодов `ksRelativeProjectionTypeEnum`: `ksPtFront=1`,
`ksPtRear=2`, `ksPtUp=3`, `ksPtDown=4`, `ksPtLeft=5`, `ksPtRight=6`, `ksPtIsoXYZ=7`.
- `X, Y` — точка привязки главного вида на листе, мм.
- `Scale` — масштаб (1.0 = 1:1).
- `DX, DY` — зазоры между видами, мм. Возврат: `bool` (успех).
## Архитектура
Новый класс (как сборки — отдельный от `PartModeler`). Сервис `DrawingService` (API7), инструменты
`DrawingTools`, namespace `Kompas.Mcp.Core.Drawings`.
- `src/Kompas.Mcp.Core/Drawings/DrawingService.cs` — сервис.
- `src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs` — чистые static-валидаторы (unit).
- `src/Kompas.Mcp.Host/Tools/DrawingTools.cs` — MCP-инструмент.
- DI: `AddSingleton<DrawingService>()` в `Program.cs`.
## MCP-инструмент
| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_create_standard_views` | `partFilePath: string`, `scale=1.0` | На активном чертеже создать стандартные ассоциативные виды (спереди/сверху/слева) сохранённой детали/сборки. Возвращает число созданных видов. |
Контракт: активный документ — чертёж (`ksDocumentDrawing`); файл модели существует. Набор видов
фиксирован (спереди+сверху+слева, 3 вида); позиция/зазоры — дефолты (X=100, Y=150, DX=20, DY=20 мм).
Выбор набора видов / позиции / изометрии — будущие инкременты (YAGNI).
## Валидация (чистые static, unit-тест)
`DrawingValidation`:
- `NormalizeModelPath(string? path)``Trim` → непустота → расширение `.m3d`/`.a3d``Path.GetFullPath`.
- `RequireScale(double scale)` — конечный и `> 0`.
`File.Exists` — в сервисе (`FileNotFoundException` с абсолютным путём).
## Проверка активного документа
`RequireActiveDrawing()` — раздельные ошибки: нет документа / тип != `ksDocumentDrawing` / не
приводится к `IKompasDocument2D`.
## Реализация
`CreateStandardViewsAsync(partFilePath, scale, ct)` — на STA-потоке: валидация (`NormalizeModelPath`,
`RequireScale`) → `File.Exists``RequireActiveDrawing``views.Count` (before) →
`AddStandartViews(path, "#Спереди", {1,3,5}, 100, 150, scale, 20, 20)` (результат проверяем: FALSE →
ошибка) → `created = views.Count - before` (ожидаем 3; если 0 — ошибка «виды не созданы»). Возврат —
число созданных видов. Транзитные RCW не освобождаем точечно (долг v2-2).
## Тестирование
### Unit (`DrawingValidationTests`)
- `NormalizeModelPath` — бросает на пустом/чужом расширении (`.step`); принимает `.m3d`/`.a3d`
(регистр, пробелы), возвращает абсолютный путь.
- `RequireScale` — бросает на 0/отрицательном/NaN/Infinity; принимает положительный.
### Integration (`DrawingTests`)
Паттерн: построить деталь 40×30×20 → `SaveAsAsync(.m3d)` → проверить файл → `CloseAsync` → создать
чертёж → `CreateStandardViewsAsync` → проверить → `finally CloseAsync` (база `IntegrationTestBase`).
1. **Создание видов**: `AddStandartViews` → возврат `== 3`; `Views.Count` вырос на 3 (с 1 до 4).
2. **Не-чертёж**: активный документ — деталь → `CreateStandardViewsAsync` бросает (понятная ошибка).
(Сценарий 1 = промотированный спайк; спайк-файл удаляется.)
## Дальнейшее (вне спека)
- Выбор набора видов (+изометрия, +сечения), позиция/масштаб листа, размеры/обозначения на видах,
рамка/основная надпись.
@@ -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,129 @@
# Дизайн: основная надпись чертежа (drawing_fill_title_block) через API7
**Дата:** 2026-05-27
**Статус:** реализовано и проверено (4 интеграционных + 7 unit, всего 188); ревью Codex спека и реализации учтено
## Правки по ревью реализации Codex
- **null-check ячейки** (#3): доступ к графе через `RequireStampCell(stamp, columnId)` — проверяет
`columnId > 0` и `Text[columnId] != null` с диагностикой; используется в записи и чтении (единое
место валидации, #6).
- **`columnId <= 0`** (#4): валидируется в `RequireStampCell` (защищает и `ReadStampCellAsync`).
- **Нетранзакционность** (#2/#5): записываем все графы, затем один `Update()`; при `Update()==FALSE`
откат старых значений не делаем — зафиксировано комментарием как осознанное поведение (откат
спекулятивен, сценарий невоспроизводим).
- **RCW** — осознанный долг (v2-2), точечный release не вводим.
## Находка спайка: `Str` ПЕРЕЗАПИСЫВАЕТ (Clear не нужен)
Двойная запись в графу 2 («ДЕТАЛЬ.001» → «ИЗДЕЛИЕ.002») → чтение даёт «ИЗДЕЛИЕ.002» (не
дописано). Значит `stamp.Text[id].Str = value` **замещает** содержимое — `IText.Clear()` перед
записью НЕ нужен.
## Правки по ревью Codex
- **Update() результат проверять**: `if (!stamp.Update()) throw`.
- **null-checks**: `LayoutSheets`, `sheet`, `sheet.Stamp` (ранняя диагностируемая ошибка).
- **`RequireActiveDrawing()`** сохраняет проверку `doc is not IKompasDocument2D` (не только
`DocumentType`); `RequireActiveDrawingViews` становится надстройкой над ним.
- **`ColumnId` switch** — дефолт `ArgumentOutOfRangeException`; unit-тест на `(StampField)999`.
- **Тест**: писать и перечитывать ВСЕ три графы (вкл. `name`=графа 1); явный reread `Text[id].Str`;
тест overwrite (запись дважды → читается второе) — уже подтверждён спайком, перенесём в регрессию.
- **`name` = «Наименование изделия» (графа 1, `ksStPartNumber`)** — однозначно в описании (не путать
с `ksStDocumentName=51`).
- **Счётчик результата** = число непустых (trimmed) полей, записанных до успешного `Update()`.
- **Сигнатура**: `FillTitleBlockAsync(string? designation = null, string? name = null,
string? material = null, ct)`.
- **Scale (графа 6) и прочие — ЯВНО вне scope инкремента 2.** Spike-метод и spike-тест удаляются.
- RCW точечно не освобождаем (v2-2).
## Цель
Веха 2D-ЧЕРТЁЖ, инкремент 2 (оформление). После стандартных видов (`drawing_create_standard_views`)
— заполнить **основную надпись** (штамп, title block) чертежа: обозначение, наименование, материал.
Чертёж с видами + заполненным штампом = презентабельный конструкторский документ. Размеры на видах
— отдельный инкремент 3 (нужен спайк по размерным интерфейсам).
## Спайк: механизм подтверждён вживую (НЕ разучивать)
Спайк (`DrawingStampSpikeTests`, зелёный) подтвердил путь API7 — round-trip графы:
```
IKompasDocument2D doc2d = (IKompasDocument2D)app.ActiveDocument; // активный чертёж
ILayoutSheet sheet = (ILayoutSheet)doc2d.LayoutSheets.ItemByNumber[1]; // лист 1 (1-based)
IStamp stamp = (IStamp)sheet.Stamp;
stamp.Text[columnId].Str = text; // Text[Int32 Id] — индексированное свойство; Id = номер графы
stamp.Update(); // фиксирует
string readBack = stamp.Text[columnId].Str; // чтение для проверки
```
**Проверено:** запись в графу 2 (обозначение) «ДЕТАЛЬ.001» → чтение возвращает «ДЕТАЛЬ.001».
`IText.Str` — read/write; `IStamp.Text[Id]` — индексированное свойство (рефлексия: `get_Text(Int32 Id)`).
`Update()` фиксирует; перестроение документа не нужно.
**Номера граф ГОСТ (форма 1), `ksStampEnum`:** `ksStPartNumber=1` (наименование изделия),
`ksStDescription=2` (обозначение документа), `ksStMaterial=3` (материал), `ksStScale=6` (масштаб),
`ksStAuthor=110` (разработал) и т.д.
## Архитектура
Метод в существующем `DrawingService`. Свой enum `StampField` (паттерн `BasePlane`/`MateType`)
маппится на номера граф — COM-числа не текут в Host/тесты.
- `src/Kompas.Mcp.Core/Drawings/StampField.cs` — enum `StampField` + static `StampFields`
(`ColumnId(field)` → номер графы).
- `DrawingService.FillTitleBlockAsync(designation?, name?, material?, ct)` — реализация.
- Инструмент `drawing_fill_title_block` в `DrawingTools`.
## MCP-инструмент
| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_fill_title_block` | `designation?`, `name?`, `material?` (все опц.) | Заполнить графы основной надписи активного чертежа: обозначение (графа 2), наименование (1), материал (3). Передаются только нужные поля (null/пусто — пропускается). Возвращает число заполненных граф. |
Контракт: активный документ — чертёж; хотя бы одно поле задано (иначе ошибка). Набор полей —
3 ключевые графы ГОСТ (расширяемо). За один вызов заполняются все переданные (меньше round-trip'ов
для LLM-агента, чем у «одна графа за вызов»).
## Валидация (чистые static, unit-тест)
- `StampFields.ColumnId(StampField)` — `Designation→2`, `Name→1`, `Material→3` (unit-тест маппинга).
- В сервисе: собрать непустые (после `Trim`) поля; если ни одного — `ArgumentException`
«укажите хотя бы одно поле». (Чистый помощник `StampFields.Collect(...)` → список (columnId,text)
— unit-тестируемо без COM.)
## Проверка активного документа
`RequireActiveDrawing()` → `IKompasDocument2D` (тип `ksDocumentDrawing`, иначе раздельная ошибка) —
выделить из существующего `RequireActiveDrawingViews` общий шаг (тот станет надстройкой: вернуть
`.ViewsAndLayersManager.Views`).
## Реализация
`FillTitleBlockCore`: собрать поля (`StampFields.Collect`) → если пусто, ошибка → `RequireActiveDrawing`
→ `sheet = LayoutSheets.ItemByNumber[1]` → `stamp = (IStamp)sheet.Stamp` → для каждого поля
`stamp.Text[columnId].Str = text` → `stamp.Update()` (результат проверить) → вернуть число граф.
Транзитные RCW не освобождаем точечно (долг v2-2, как везде).
## Тестирование
### Unit (`StampFieldsTests`)
- `StampFields.ColumnId` — каждое поле → ожидаемый номер графы (2/1/3).
- `StampFields.Collect` — пропускает null/пусто/пробелы; при всех пустых даёт пустой список;
собирает заданные с обрезкой пробелов.
### Integration (`DrawingStampTests`)
Паттерн: создать чертёж → `FillTitleBlockAsync(...)` → перечитать графы напрямую (helper в сервисе
`ReadStampCellAsync(columnId)` для теста ИЛИ проверка через возврат) → `finally CloseAsync` (база
`IntegrationTestBase`).
1. **Заполнение и чтение**: `designation="ДЕТАЛЬ.001", material="Сталь 45"` → 2 графы; чтение графы
2 == «ДЕТАЛЬ.001», графы 3 == «Сталь 45».
2. **Пустой ввод**: все поля null → `ArgumentException` (ни одной графы не тронуто).
3. **Не-чертёж**: активный документ — деталь → ошибка.
(Для чтения в тесте — добавить `DrawingService.ReadStampCellAsync(columnId)` — он же подтверждает
round-trip; спайк-файл удаляется.)
## Дальнейшее (вне спека)
- Прочие графы (масштаб, разработал, даты, литера), размеры/обозначения на видах (инкремент 3),
рамка/формат листа.
@@ -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.5–2.0); **но** радиус **> ~½ толщины стенки** сам создаёт зону
нависания > `θ_max` → тогда фаска/ступень.
- **Внутренние углы — всегда скругление ≥R0.5**.
### 8.11 Зенковки / цековки
- **Цековка (counterbore)** — большим Ø/полостью **вверх** (дно по телу, не мостом); глубина +0.3 мм.
- **Зенковка (countersink):** конус **вверх**; при включённом угле **≤90°** стенки ≤45° от вертикали
— печатается; **>90°** стенки становятся нависанием → поддержка или замена цилиндрической цековкой.
(Не путать с «узким» конусом — он печатается, но как посадка под головку бесполезен.)
### 8.12 Заходные фаски (assembly relief)
- На штифтах, отверстиях, инсёртах, защёлках, «ласточкиных хвостах» — **заходная фаска** (≈0.5–1 мм
× 45° или ≈ половина зазора) против задиров при первом контакте/сборке.
### 8.13 Разбиение детали и сборка из печатных частей
- Если оптимальная по прочности ориентация конфликтует с беспод­держечностью, или деталь крупная/
коробится — **разбить на части** с самоустанавливающимися стыками (печатные штифты/шпонки/замки),
склейка; стыки — на XY-гранях. Зазор стыка — по §8.4.
### 8.14 Защёлки (snap-fit) / живой шарнир
- Консольная защёлка: толщина балки **≥2–3·w (≈1.0–2.0 мм)**, зацеп/возврат **0.3–0.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 Полые детали и общая гигиена модели
- **Полости:** дренаж Ø3–5 мм у **низшей** точки + вент у **высшей**.
- Допуски/зазоры — **в геометрию** (слайсер читает модель буквально, «доводчика» нет).
- Раздельные тела — зазор ≥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,56 @@
using Kompas6Constants;
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Ориентация линейного размера (обёртка над <see cref="ksLineDimensionOrientationEnum"/>).</summary>
public enum DimensionOrientation
{
/// <summary>Горизонтальный (измеряет |X2X1|).</summary>
Horizontal,
/// <summary>Вертикальный (измеряет |Y2Y1|).</summary>
Vertical,
/// <summary>Параллельный отрезку P1→P2 (реальная длина).</summary>
Parallel,
}
/// <summary>Конверсии углов размеров (2D-API КОМПАС использует радианы).</summary>
public static class DimensionAngles
{
/// <summary>Градусы → радианы (угол выноски в API задаётся в радианах).</summary>
public static double ToRadians(double degrees)
{
if (!double.IsFinite(degrees))
throw new ArgumentException("Угол должен быть конечным числом.", nameof(degrees));
var result = degrees * (Math.PI / 180.0);
if (!double.IsFinite(result)) // защита от переполнения на экстремальных degrees
throw new ArgumentOutOfRangeException(nameof(degrees), "Угол слишком велик (переполнение при переводе в радианы).");
return result;
}
}
/// <summary>Маппинг ориентации размера ↔ перечисление КОМПАС и разбор строки.</summary>
public static class DimensionOrientations
{
public static ksLineDimensionOrientationEnum ToKompas(DimensionOrientation orientation) => orientation switch
{
DimensionOrientation.Horizontal => ksLineDimensionOrientationEnum.ksLinDHorizontal,
DimensionOrientation.Vertical => ksLineDimensionOrientationEnum.ksLinDVertical,
DimensionOrientation.Parallel => ksLineDimensionOrientationEnum.ksLinDParallel,
_ => throw new ArgumentOutOfRangeException(nameof(orientation), orientation, "Неизвестная ориентация размера."),
};
/// <summary>Разобрать строку ориентации (англ./рус.) в <see cref="DimensionOrientation"/>.</summary>
public static DimensionOrientation Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
return value.Trim().ToLowerInvariant() switch
{
"horizontal" or "h" or "горизонтальный" or "горизонтальная" => DimensionOrientation.Horizontal,
"vertical" or "v" or "вертикальный" or "вертикальная" => DimensionOrientation.Vertical,
"parallel" or "p" or "параллельный" or "параллельная" => DimensionOrientation.Parallel,
_ => throw new ArgumentException(
$"Неизвестная ориентация размера '{value}'. Допустимо: horizontal, vertical, parallel.",
nameof(value)),
};
}
}
@@ -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; }
}
@@ -0,0 +1,11 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Результат простановки размера на чертеже.</summary>
public sealed record DrawingDimensionResult
{
/// <summary>Измеренное значение размера: линейный/диаметральный/радиальный — мм, угловой — градусы.</summary>
public required double Value { get; init; }
/// <summary>Номер вида, на котором поставлен размер.</summary>
public required int ViewNumber { get; init; }
}
@@ -0,0 +1,748 @@
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;
namespace Kompas.Mcp.Core.Drawings;
/// <summary>
/// Операции с 2D-чертежами через API7 (<c>IViews</c>). Все вызовы — на STA-потоке
/// <see cref="KompasDispatcher"/>.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class DrawingService
{
private readonly KompasSession _session;
private readonly KompasDispatcher _dispatcher;
public DrawingService(KompasSession session, KompasDispatcher dispatcher)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>
/// Создать на активном чертеже стандартные ассоциативные виды (спереди/сверху/слева) сохранённой
/// 3D-модели <paramref name="partFilePath"/> (.m3d/.a3d). Главный вид привязывается к точке
/// (<paramref name="x"/>,<paramref name="y"/>) листа, мм; масштаб — <paramref name="scale"/>.
/// </summary>
public Task<DrawingViewsResult> CreateStandardViewsAsync(string partFilePath, double scale,
double x, double y, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => CreateStandardViewsCore(partFilePath, scale, x, y), ct);
/// <summary>
/// Заполнить графы основной надписи активного чертежа: обозначение (графа 2), наименование
/// изделия (графа 1), материал (графа 3). Передаются только нужные поля (null/пусто —
/// пропускается). Возвращает число заполненных граф.
/// </summary>
public Task<int> FillTitleBlockAsync(string? designation = null, string? name = null,
string? material = null, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => FillTitleBlockCore(designation, name, material), ct);
private int FillTitleBlockCore(string? designation, string? name, string? material)
{
var fields = StampFields.Collect(designation, name, material);
if (fields.Count == 0)
throw new ArgumentException("Укажите хотя бы одно поле основной надписи (designation/name/material).");
var stamp = RequireActiveStamp();
// Нетранзакционно: все графы записываются, затем один Update() фиксирует. При Update()==FALSE
// (на практике не воспроизводится) откат старых значений не делаем — он спекулятивен (Str уже
// на in-memory объектах графы, повторный Update мог бы тоже упасть); сообщаем об ошибке.
foreach (var (columnId, text) in fields)
RequireStampCell(stamp, columnId).Str = text; // Str замещает содержимое (Clear не нужен)
if (!stamp.Update())
throw new InvalidOperationException("IStamp.Update вернул FALSE — основная надпись не обновлена.");
return fields.Count;
}
/// <summary>Прочитать текст графы основной надписи (для проверки/диагностики).</summary>
public Task<string> ReadStampCellAsync(int columnId, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var stamp = RequireActiveStamp();
return RequireStampCell(stamp, columnId).Str ?? string.Empty;
}, ct);
/// <summary>Получить графу основной надписи по номеру с диагностикой (номер &gt; 0, ячейка != null).</summary>
private static IText RequireStampCell(IStamp stamp, int columnId)
{
if (columnId <= 0)
throw new ArgumentOutOfRangeException(nameof(columnId), "Номер графы основной надписи должен быть > 0.");
return stamp.Text[columnId]
?? throw new InvalidOperationException($"Графа {columnId} основной надписи недоступна (Text[{columnId}] == null).");
}
private IStamp RequireActiveStamp()
{
var doc2d = RequireActiveDrawing();
var sheets = doc2d.LayoutSheets
?? throw new InvalidOperationException("Не удалось получить листы оформления чертежа.");
var sheet = sheets.ItemByNumber[1] as ILayoutSheet
?? throw new InvalidOperationException("На чертеже нет листа оформления #1.");
return sheet.Stamp as IStamp
?? 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"/> — номер вида
/// (≤0 = первый несистемный/главный). Значение измеряется автоматически. Возвращает значение и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddLinearDimensionAsync(int viewNumber, double x1, double y1,
double x2, double y2, double x3, double y3, DimensionOrientation orientation, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
AddLinearDimensionCore(viewNumber, x1, y1, x2, y2, x3, y3, orientation), ct);
private DrawingDimensionResult AddLinearDimensionCore(int viewNumber, double x1, double y1,
double x2, double y2, double x3, double y3, DimensionOrientation orientation)
{
DrawingValidation.RequireFiniteCoords(x1, y1, x2, y2, x3, y3);
DrawingValidation.RequireDistinctPoints(x1, y1, x2, y2);
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
var dim = symbols.LineDimensions?.Add()
?? throw new InvalidOperationException("LineDimensions.Add вернул null.");
try
{
dim.X1 = x1; dim.Y1 = y1; dim.X2 = x2; dim.Y2 = y2; dim.X3 = x3; dim.Y3 = y3;
dim.Orientation = DimensionOrientations.ToKompas(orientation);
((IDimensionText)dim).AutoNominalValue = true; // авто-измерение значения (по умолчанию, явно)
if (!dim.Update())
throw new InvalidOperationException("ILineDimension.Update вернул FALSE — размер не построен.");
if (!dim.Valid)
throw new InvalidOperationException("Размер построен невалидным (проверьте точки/ориентацию).");
// NominalValue читаем ТОЛЬКО после успешного Update и Valid.
var value = ((IDimensionText)dim).NominalValue;
// Нулевое значение — ориентация несовместима с точками (напр. horizontal для точек на одной
// вертикали: |X2-X1|=0). RequireDistinctPoints это не ловит — точки различны по Y.
if (Math.Abs(value) < 1e-6)
throw new InvalidOperationException(
"Размер измерил нулевое значение — выбранная ориентация несовместима с точками " +
"(например, horizontal для точек на одной вертикали; используйте vertical/parallel).");
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
}
catch
{
try { dim.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число линейных размеров на виде по номеру (для проверки/диагностики). Размеры живут
/// в ISymbols2DContainer вида, а НЕ в IView.ObjectCount (тот считает геометрию).</summary>
public Task<int> GetViewLineDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.LineDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить диаметральный размер (Ø) окружности на виде. <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, 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, 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.DiametralDimensions?.Add()
?? throw new InvalidOperationException("DiametralDimensions.Add вернул null.");
try
{
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;
if (!dim.Update())
throw new InvalidOperationException("IDiametralDimension.Update вернул FALSE — размер не построен.");
if (!dim.Valid)
throw new InvalidOperationException("Диаметральный размер построен невалидным.");
var value = ((IDimensionText)dim).NominalValue; // диаметр = 2·radius
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> 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)
{
var view = FindView(viewNumber);
var symbols = view as ISymbols2DContainer
?? throw new InvalidOperationException("Вид не приводится к ISymbols2DContainer.");
return (view, symbols);
}
/// <summary>Найти вид: при viewNumber ≤ 0 — первый несистемный (Number != 0); иначе по номеру.</summary>
private IView FindView(int viewNumber)
{
var views = RequireActiveDrawingViews();
foreach (var o in views)
{
if (o is not IView v) continue;
if (viewNumber <= 0)
{
if (v.Number != 0) return v; // первый несистемный
}
else if (v.Number == viewNumber)
{
return v;
}
}
throw new InvalidOperationException(viewNumber <= 0
? "На чертеже нет несистемного вида. Сначала создайте виды: drawing_create_standard_views."
: $"Вид с номером {viewNumber} не найден на чертеже.");
}
// Стандартная тройка проекций: ksPtFront=1, ksPtUp=3, ksPtLeft=5 (ksRelativeProjectionTypeEnum).
private static readonly object[] StandardProjections = { 1, 3, 5 };
private const int ExpectedCreated = 3;
private const double GapMm = 20; // зазоры между видами (DX=DY)
private DrawingViewsResult CreateStandardViewsCore(string partFilePath, double scale, double x, double y)
{
var path = DrawingValidation.NormalizeModelPath(partFilePath);
DrawingValidation.RequireScale(scale);
if (!double.IsFinite(x) || !double.IsFinite(y))
throw new ArgumentException("Координаты привязки вида должны быть конечными числами.");
if (!File.Exists(path))
throw new FileNotFoundException($"Файл модели не найден: {path}", path);
if (new FileInfo(path).Length == 0)
throw new InvalidOperationException($"Файл модели пуст (не сохранён?): {path}");
var views = RequireActiveDrawingViews();
// Снимок номеров существующих видов — чтобы потом отличить созданные ИМЕННО этим вызовом
// (на чертеже уже есть системный вид и могут быть прежние виды).
var beforeNumbers = CollectViewNumbers(views);
if (!views.AddStandartViews(path, "#Спереди", StandardProjections, x, y, scale, GapMm, GapMm))
throw new InvalidOperationException(
$"AddStandartViews вернул FALSE — виды не созданы (файл модели или проекция недоступны): {path}");
var newViews = CollectNewViews(views, beforeNumbers);
try
{
if (newViews.Count != ExpectedCreated)
throw new InvalidOperationException(
$"Ожидалось {ExpectedCreated} вида, создано {newViews.Count}.");
// Гарантия непустоты: у созданных видов должна быть геометрия (иначе модель не
// спроецировалась). Проверяем ИМЕННО новые виды, не всю коллекцию.
if (newViews.Sum(v => v.ObjectCount) <= 0)
throw new InvalidOperationException(
"Стандартные виды созданы, но пусты (модель не спроецировалась). Проверьте файл модели.");
return new DrawingViewsResult
{
Created = newViews.Count,
Total = views.Count,
ViewNumbers = newViews.Select(v => v.Number).ToArray(),
};
}
catch
{
// Откат: удаляем виды, созданные этим вызовом, чтобы чертёж не остался в полу-состоянии.
foreach (var v in newViews)
try { v.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>
/// Получить активный чертёж как <see cref="IKompasDocument2D"/>. Раздельные ошибки: нет документа /
/// тип != ksDocumentDrawing / не приводится к IKompasDocument2D.
/// </summary>
private IKompasDocument2D RequireActiveDrawing()
{
var doc = _session.Application.ActiveDocument
?? throw new InvalidOperationException("Нет активного документа. Создайте чертёж: document_create drawing.");
if (doc.DocumentType != DocumentTypeEnum.ksDocumentDrawing)
throw new InvalidOperationException(
$"Активный документ не является чертежом (тип: {doc.DocumentType}). Создайте чертёж: document_create drawing.");
return doc as IKompasDocument2D
?? throw new InvalidOperationException("Активный документ не приводится к IKompasDocument2D.");
}
private IViews RequireActiveDrawingViews()
=> RequireActiveDrawing().ViewsAndLayersManager?.Views
?? throw new InvalidOperationException("Не удалось получить коллекцию видов чертежа.");
private static HashSet<int> CollectViewNumbers(IViews views)
{
var set = new HashSet<int>();
foreach (var o in views)
if (o is IView v)
set.Add(v.Number);
return set;
}
private static List<IView> CollectNewViews(IViews views, HashSet<int> beforeNumbers)
{
var list = new List<IView>();
foreach (var o in views)
if (o is IView v && !beforeNumbers.Contains(v.Number))
list.Add(v);
return list;
}
}
@@ -0,0 +1,96 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Чистые валидаторы операций чертежа (без COM) — выделены для unit-тестов.</summary>
public static class DrawingValidation
{
/// <summary>Допустимые расширения файла-источника модели: деталь (.m3d) и сборка (.a3d).</summary>
private static readonly HashSet<string> AllowedExtensions =
new(StringComparer.OrdinalIgnoreCase) { ".m3d", ".a3d" };
/// <summary>
/// Нормализовать путь к файлу 3D-модели: обрезать пробелы, проверить непустоту и расширение
/// (.m3d/.a3d), вернуть абсолютный путь. Существование файла НЕ проверяет (это делает сервис).
/// </summary>
public static string NormalizeModelPath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Путь к файлу модели не задан.", nameof(path));
var trimmed = path.Trim();
var ext = Path.GetExtension(trimmed);
if (!AllowedExtensions.Contains(ext))
throw new ArgumentException(
$"Недопустимое расширение файла модели '{ext}'. Ожидается .m3d (деталь) или .a3d (сборка).",
nameof(path));
return Path.GetFullPath(trimmed);
}
/// <summary>Проверить масштаб чертежа: конечный и строго положительный.</summary>
public static void RequireScale(double scale)
{
if (!double.IsFinite(scale) || scale <= 0)
throw new ArgumentOutOfRangeException(nameof(scale), "Масштаб должен быть конечным и > 0.");
}
/// <summary>Проверить, что все координаты конечны (отсекает NaN/Infinity).</summary>
public static void RequireFiniteCoords(params double[] coords)
{
foreach (var c in coords)
if (!double.IsFinite(c))
throw new ArgumentException("Координаты должны быть конечными числами (не NaN/Infinity).");
}
/// <summary>Проверить радиус: конечный и строго положительный.</summary>
public static void RequirePositiveRadius(double radius)
{
if (!double.IsFinite(radius) || radius <= 0)
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;
/// <summary>
/// Проверить, что выносные точки размера различны (иначе размер вырожден — нулевая длина).
/// </summary>
public static void RequireDistinctPoints(double x1, double y1, double x2, double y2)
{
var dx = x2 - x1;
var dy = y2 - y1;
if (Math.Sqrt(dx * dx + dy * dy) < MinPointDistance)
throw new ArgumentException("Выносные точки размера совпадают — нельзя поставить размер нулевой длины.");
}
}
@@ -0,0 +1,14 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Результат создания стандартных видов на чертеже.</summary>
public sealed record DrawingViewsResult
{
/// <summary>Сколько видов создано этим вызовом.</summary>
public required int Created { get; init; }
/// <summary>Общее число видов на чертеже после операции (включая системный вид).</summary>
public required int Total { get; init; }
/// <summary>Номера созданных видов (для адресации при простановке размеров).</summary>
public required IReadOnlyList<int> ViewNumbers { get; init; }
}
@@ -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)),
};
}
}
@@ -0,0 +1,47 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>
/// Поле основной надписи (графа штампа ГОСТ). Маппится на номер графы <c>ksStampEnum</c>.
/// Пока поддержаны три ключевые графы; остальные (масштаб, разработал, даты…) — позже.
/// </summary>
public enum StampField
{
/// <summary>Обозначение документа (графа 2, ksStDescription).</summary>
Designation,
/// <summary>Наименование изделия (графа 1, ksStPartNumber).</summary>
Name,
/// <summary>Материал (графа 3, ksStMaterial).</summary>
Material,
}
/// <summary>Маппинг поля основной надписи → номер графы и сбор непустых полей.</summary>
public static class StampFields
{
/// <summary>Номер графы (<c>ksStampEnum</c>) для поля основной надписи.</summary>
public static int ColumnId(StampField field) => field switch
{
StampField.Designation => 2, // ksStDescription
StampField.Name => 1, // ksStPartNumber (наименование изделия)
StampField.Material => 3, // ksStMaterial
_ => throw new ArgumentOutOfRangeException(nameof(field), field, "Неизвестное поле основной надписи."),
};
/// <summary>
/// Собрать непустые (после Trim) поля в список (номер графы, текст). null/пусто/пробелы —
/// пропускаются. Чистая (без COM) — unit-тестируемо.
/// </summary>
public static IReadOnlyList<(int ColumnId, string Text)> Collect(string? designation, string? name, string? material)
{
var list = new List<(int, string)>(3);
Add(list, StampField.Designation, designation);
Add(list, StampField.Name, name);
Add(list, StampField.Material, material);
return list;
}
private static void Add(List<(int, string)> list, StampField field, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
list.Add((ColumnId(field), value.Trim()));
}
}
+2
View File
@@ -2,6 +2,7 @@ using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Assemblies;
using Kompas.Mcp.Core.Conversion;
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Editing;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;
@@ -25,6 +26,7 @@ builder.Services.AddSingleton<DocumentService>();
builder.Services.AddSingleton<PartModeler>();
builder.Services.AddSingleton<HoleService>();
builder.Services.AddSingleton<AssemblyService>();
builder.Services.AddSingleton<DrawingService>();
builder.Services.AddSingleton<VariableService>();
builder.Services.AddSingleton<SnapshotService>();
builder.Services.AddSingleton<QueryService>();
+232
View File
@@ -0,0 +1,232 @@
using System.ComponentModel;
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Drawings;
using ModelContextProtocol.Server;
namespace Kompas.Mcp.Host.Tools;
/// <summary>Инструменты 2D-чертежа (виды, оформление).</summary>
[McpServerToolType]
public sealed class DrawingTools(KompasSession session, DrawingService drawing)
{
[McpServerTool(Name = "drawing_create_standard_views")]
[Description("На активном чертеже создать стандартные ассоциативные виды (спереди, сверху, слева) " +
"сохранённой 3D-модели (.m3d деталь / .a3d сборка). Активный документ должен быть чертежом " +
"(document_create drawing); модель должна быть сохранена в файл (document_save_as). " +
"scale — масштаб (1.0 = 1:1); x,y — точка привязки главного вида на листе (мм). " +
"Повторный вызов добавит ещё один набор видов — задайте другие x,y, чтобы не накладывались. " +
"Возвращает число созданных видов.")]
public async Task<string> CreateStandardViews(
[Description("Путь к файлу 3D-модели (.m3d/.a3d)")] string partFilePath,
[Description("Масштаб видов (1.0 = 1:1)")] double scale = 1.0,
[Description("X привязки главного вида на листе, мм")] double x = 100,
[Description("Y привязки главного вида на листе, мм")] double y = 150)
{
await session.ConnectAsync();
var r = await drawing.CreateStandardViewsAsync(partFilePath, scale, x, y);
var numbers = string.Join(", ", r.ViewNumbers);
return $"Создано {r.Created} стандартных вида на чертеже (всего видов: {r.Total}, масштаб {scale}). " +
$"Номера созданных видов: {numbers} (для инструментов простановки размеров — параметр viewNumber).";
}
[McpServerTool(Name = "drawing_fill_title_block")]
[Description("Заполнить графы основной надписи (штампа) активного чертежа. Передавайте только нужные поля: " +
"designation — обозначение документа (графа 2); name — наименование изделия (графа 1); " +
"material — материал (графа 3). Незаданные/пустые поля пропускаются (нужно хотя бы одно). " +
"Существующий текст графы перезаписывается. Возвращает число заполненных граф.")]
public async Task<string> FillTitleBlock(
[Description("Обозначение документа (графа 2)")] string? designation = null,
[Description("Наименование изделия (графа 1)")] string? name = null,
[Description("Материал (графа 3)")] string? material = null)
{
await session.ConnectAsync();
var filled = await drawing.FillTitleBlockAsync(designation, name, material);
return $"Заполнено граф основной надписи: {filled}.";
}
[McpServerTool(Name = "drawing_add_linear_dimension")]
[Description("Поставить линейный размер на виде активного чертежа между точками (x1,y1)-(x2,y2) " +
"в ЛОКАЛЬНОЙ СК вида (мм; при масштабе 1:1 = координаты модели на плоскости вида). Размерная " +
"линия проходит через (x3,y3). orientation: horizontal (|X2-X1|) | vertical (|Y2-Y1|) | parallel " +
"(длина отрезка). viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный вид). " +
"Значение измеряется автоматически. Возвращает измеренное значение и номер вида.")]
public async Task<string> AddLinearDimension(
[Description("X первой выносной точки (СК вида, мм)")] double x1,
[Description("Y первой выносной точки")] double y1,
[Description("X второй выносной точки")] double x2,
[Description("Y второй выносной точки")] double y2,
[Description("X положения размерной линии")] double x3,
[Description("Y положения размерной линии")] double y3,
[Description("Ориентация: horizontal | vertical | parallel")] string orientation = "horizontal",
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0)
{
await session.ConnectAsync();
var o = DimensionOrientations.Parse(orientation);
var r = await drawing.AddLinearDimensionAsync(viewNumber, x1, y1, x2, y2, x3, y3, o);
return $"Линейный размер ({orientation}) поставлен на виде {r.ViewNumber}, значение {r.Value:0.###} мм.";
}
[McpServerTool(Name = "drawing_add_diametral_dimension")]
[Description("Поставить диаметральный размер (Ø) окружности на виде активного чертежа. Окружность " +
"задаётся центром (xc,yc) и радиусом radius в ЛОКАЛЬНОЙ СК вида (мм; при масштабе 1:1 = координаты " +
"модели на плоскости вида) — задайте параметры реальной окружности вида. angle — направление выноски " +
"в градусах. viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный). " +
"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 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));
}
@@ -0,0 +1,36 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class DimensionOrientationsTests
{
[Theory]
[InlineData("horizontal", DimensionOrientation.Horizontal)]
[InlineData("vertical", DimensionOrientation.Vertical)]
[InlineData("parallel", DimensionOrientation.Parallel)]
[InlineData("ГОРИЗОНТАЛЬНЫЙ", DimensionOrientation.Horizontal)]
[InlineData(" vertical ", DimensionOrientation.Vertical)]
public void Parse_maps_known(string value, DimensionOrientation expected)
=> Assert.Equal(expected, DimensionOrientations.Parse(value));
[Fact]
public void Parse_throws_on_unknown()
=> Assert.Throws<ArgumentException>(() => DimensionOrientations.Parse("diagonal"));
// ToKompas (→ ksLineDimensionOrientationEnum) намеренно НЕ покрыт unit-тестом: тестовый проект
// не ссылается на Kompas6Constants (граница проекта). Маппинг тривиален и проверяется интеграцией
// (horizontal → |X2-X1|, vertical → |Y2-Y1|).
[Theory]
[InlineData(0, 0)]
[InlineData(90, Math.PI / 2)]
[InlineData(180, Math.PI)]
[InlineData(-45, -Math.PI / 4)]
public void ToRadians_converts_degrees(double degrees, double expected)
=> Assert.Equal(expected, DimensionAngles.ToRadians(degrees), 9);
[Fact]
public void ToRadians_throws_on_non_finite()
=> Assert.Throws<ArgumentException>(() => DimensionAngles.ToRadians(double.NaN));
}
@@ -0,0 +1,122 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class DrawingValidationTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void NormalizeModelPath_throws_on_empty(string? path)
=> Assert.Throws<ArgumentException>(() => DrawingValidation.NormalizeModelPath(path));
[Theory]
[InlineData(@"C:\m\part.step")]
[InlineData(@"C:\m\part.txt")]
[InlineData(@"C:\m\part")]
public void NormalizeModelPath_throws_on_wrong_extension(string path)
=> Assert.Throws<ArgumentException>(() => DrawingValidation.NormalizeModelPath(path));
[Theory]
[InlineData(@"C:\m\part.m3d", ".m3d")]
[InlineData(@"C:\m\frame.a3d", ".a3d")]
[InlineData(@" C:\m\part.M3D ", ".m3d")]
[InlineData(@"C:\модели\деталь.m3d", ".m3d")] // кириллица в пути
public void NormalizeModelPath_accepts_model_files(string path, string expectedExt)
{
var result = DrawingValidation.NormalizeModelPath(path);
Assert.True(Path.IsPathFullyQualified(result));
Assert.EndsWith(expectedExt, result, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void NormalizeModelPath_preserves_cyrillic()
{
var result = DrawingValidation.NormalizeModelPath(@"C:\модели\деталь.m3d");
Assert.Contains("деталь", result);
Assert.Contains("модели", result);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public void RequireScale_throws_on_non_positive_or_non_finite(double scale)
=> Assert.Throws<ArgumentOutOfRangeException>(() => DrawingValidation.RequireScale(scale));
[Theory]
[InlineData(1.0)]
[InlineData(0.5)]
[InlineData(2.0)]
public void RequireScale_accepts_positive(double scale)
=> Assert.Null(Record.Exception(() => DrawingValidation.RequireScale(scale)));
[Fact]
public void RequireFiniteCoords_throws_on_non_finite()
{
Assert.Throws<ArgumentException>(() => DrawingValidation.RequireFiniteCoords(0, double.NaN));
Assert.Throws<ArgumentException>(() => DrawingValidation.RequireFiniteCoords(double.PositiveInfinity));
Assert.Null(Record.Exception(() => DrawingValidation.RequireFiniteCoords(0, 1, -5, 40)));
}
[Fact]
public void RequireDistinctPoints_throws_on_coincident()
{
Assert.Throws<ArgumentException>(() => DrawingValidation.RequireDistinctPoints(10, 20, 10, 20));
Assert.Null(Record.Exception(() => DrawingValidation.RequireDistinctPoints(0, 0, 40, 0)));
}
[Theory]
[InlineData(0)]
[InlineData(-5)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
public void RequirePositiveRadius_throws_on_non_positive_or_non_finite(double radius)
=> Assert.Throws<ArgumentOutOfRangeException>(() => DrawingValidation.RequirePositiveRadius(radius));
[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,116 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: диаметральный размер на виде (API7 ISymbols2DContainer.DiametralDimensions).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingDiametralTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingDiametralTests(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 Diametral_dimension_value_is_diameter()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
// Радиус 15 → диаметр 30.
var r = await _drawing.AddDiametralDimensionAsync(viewNumber: 0, xc: 10, yc: 10, radius: 15, angleDeg: 45);
Assert.InRange(r.Value, 29.9, 30.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Targets_specific_view_and_increments_diametral_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.GetViewDiametralDimensionCountAsync(targetNumber);
var r = await _drawing.AddDiametralDimensionAsync(targetNumber, xc: 5, yc: 5, radius: 8, angleDeg: 30);
var after = await _drawing.GetViewDiametralDimensionCountAsync(targetNumber);
Assert.Equal(targetNumber, r.ViewNumber);
Assert.Equal(before + 1, after);
Assert.InRange(r.Value, 15.9, 16.1); // Ø = 2·8
}
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.AddDiametralDimensionAsync(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.AddDiametralDimensionAsync(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);
// Виды есть, но номер 999 не существует → ветка FindView "вид не найден".
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddDiametralDimensionAsync(viewNumber: 999, xc: 0, yc: 0, radius: 10, angleDeg: 0));
}
finally { await _docs.CloseAsync(save: false); }
}
}
@@ -0,0 +1,159 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: линейный размер на виде чертежа (API7 ISymbols2DContainer.LineDimensions).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingDimensionTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingDimensionTests(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 Horizontal_dimension_auto_measures_distance()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
Assert.Equal(3, views.ViewNumbers.Count);
var r = await _drawing.AddLinearDimensionAsync(
viewNumber: 0, x1: 0, y1: 0, x2: 40, y2: 0, x3: 20, y3: -10, DimensionOrientation.Horizontal);
Assert.InRange(r.Value, 39.9, 40.1); // авто-значение = |X2-X1|
Assert.Contains(r.ViewNumber, views.ViewNumbers); // размер попал в один из видов
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Vertical_dimension_measures_y_span()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var r = await _drawing.AddLinearDimensionAsync(
viewNumber: 0, x1: 0, y1: 0, x2: 0, y2: 20, x3: -10, y3: 10, DimensionOrientation.Vertical);
Assert.InRange(r.Value, 19.9, 20.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Targets_specific_view_by_number_and_increments_its_object_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]; // второй созданный вид (напр. сверху)
// Размеры живут в ISymbols2DContainer вида — считаем именно их у ЦЕЛЕВОГО вида.
var before = await _drawing.GetViewLineDimensionCountAsync(targetNumber);
var r = await _drawing.AddLinearDimensionAsync(
viewNumber: targetNumber, x1: 0, y1: 0, x2: 40, y2: 0, x3: 20, y3: -10, DimensionOrientation.Horizontal);
var after = await _drawing.GetViewLineDimensionCountAsync(targetNumber);
Assert.Equal(targetNumber, r.ViewNumber);
Assert.Equal(before + 1, after); // размер попал именно в целевой вид
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Parallel_dimension_measures_true_length()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
// Параллельный размер (0,0)-(30,40): реальная длина = 50 (3-4-5).
var r = await _drawing.AddLinearDimensionAsync(
viewNumber: 0, x1: 0, y1: 0, x2: 30, y2: 40, x3: 25, y3: 5, DimensionOrientation.Parallel);
Assert.InRange(r.Value, 49.5, 50.5);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Zero_projection_orientation_throws_and_rolls_back()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var before = await _drawing.GetViewLineDimensionCountAsync(0);
// Горизонтальный размер для точек на одной вертикали → |X2-X1|=0 → ошибка + откат.
await Assert.ThrowsAsync<InvalidOperationException>(() => _drawing.AddLinearDimensionAsync(
viewNumber: 0, x1: 10, y1: 0, x2: 10, y2: 30, x3: 0, y3: 15, DimensionOrientation.Horizontal));
// Откат: число размеров не изменилось (вырожденный размер удалён).
var after = await _drawing.GetViewLineDimensionCountAsync(0);
Assert.Equal(before, after);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Coincident_points_throw()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
await Assert.ThrowsAsync<ArgumentException>(() => _drawing.AddLinearDimensionAsync(
viewNumber: 0, x1: 10, y1: 10, x2: 10, y2: 10, x3: 0, y3: 0, DimensionOrientation.Horizontal));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task No_views_throws()
{
// Чертёж без ассоциативных видов: только системный вид → понятная ошибка.
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() => _drawing.AddLinearDimensionAsync(
viewNumber: 0, x1: 0, y1: 0, x2: 40, y2: 0, x3: 20, y3: -10, DimensionOrientation.Horizontal));
}
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,85 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: заполнение основной надписи чертежа (API7 IStamp.Text).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingStampTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly DrawingService _drawing;
private readonly PartModeler _modeler;
public DrawingStampTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
}
[Fact]
public async Task Fills_and_reads_back_title_block_cells()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var filled = await _drawing.FillTitleBlockAsync(
designation: "ДЕТАЛЬ.001", name: "Кронштейн", material: "Сталь 45");
Assert.Equal(3, filled);
// Явный reread каждой графы (графа 2/1/3).
Assert.Equal("ДЕТАЛЬ.001", await _drawing.ReadStampCellAsync(2));
Assert.Equal("Кронштейн", await _drawing.ReadStampCellAsync(1));
Assert.Equal("Сталь 45", await _drawing.ReadStampCellAsync(3));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Overwrites_existing_cell_text()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await _drawing.FillTitleBlockAsync(designation: "СТАРОЕ.000");
await _drawing.FillTitleBlockAsync(designation: "НОВОЕ.111");
// Перезапись, не дописывание.
Assert.Equal("НОВОЕ.111", await _drawing.ReadStampCellAsync(2));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Empty_input_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<ArgumentException>(
() => _drawing.FillTitleBlockAsync(null, " ", null));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Rejects_non_drawing_document()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Деталь — не чертёж: FillTitleBlock должен бросить.
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, 0, 0, 10, 10);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 5);
await Assert.ThrowsAsync<InvalidOperationException>(
() => _drawing.FillTitleBlockAsync(designation: "X"));
}
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,88 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: стандартные ассоциативные виды чертежа (API7 IViews.AddStandartViews).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingTests(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); }
Assert.True(File.Exists(path), $"Файл детали не сохранён: {path}");
Assert.True(new FileInfo(path).Length > 0, "Файл детали пуст.");
return path;
}
[Fact]
public async Task Creates_three_standard_views_on_drawing()
{
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var r = await _drawing.CreateStandardViewsAsync(partFile, scale: 1.0, x: 100, y: 150);
// Создано ровно 3 вида (спереди/сверху/слева); всего на чертеже +3 к системному виду.
Assert.Equal(3, r.Created);
Assert.True(r.Total >= 4, $"Ожидалось ≥4 видов всего (системный + 3), получено {r.Total}.");
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Rejects_non_drawing_document()
{
// Активный документ — деталь (не чертёж): понятная ошибка.
var partFile = await BuildAndSaveBoxAsync();
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(
() => _drawing.CreateStandardViewsAsync(partFile, scale: 1.0, x: 100, y: 150));
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Missing_model_file_throws()
{
var missing = Path.Combine(TestPaths.Scratch, $"no_such_{Guid.NewGuid():N}.m3d");
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<FileNotFoundException>(
() => _drawing.CreateStandardViewsAsync(missing, scale: 1.0, x: 100, y: 150));
}
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 бросает (проверяется косвенно).
}
@@ -0,0 +1,41 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class StampFieldsTests
{
[Theory]
[InlineData(StampField.Designation, 2)]
[InlineData(StampField.Name, 1)]
[InlineData(StampField.Material, 3)]
public void ColumnId_maps_known_fields(StampField field, int expected)
=> Assert.Equal(expected, StampFields.ColumnId(field));
[Fact]
public void ColumnId_throws_on_unknown()
=> Assert.Throws<ArgumentOutOfRangeException>(() => StampFields.ColumnId((StampField)999));
[Fact]
public void Collect_skips_null_empty_and_whitespace()
=> Assert.Empty(StampFields.Collect(null, "", " "));
[Fact]
public void Collect_trims_and_maps_provided_fields()
{
var result = StampFields.Collect(" ДЕТАЛЬ.001 ", null, "Сталь 45");
Assert.Equal(2, result.Count);
Assert.Contains(result, t => t.ColumnId == 2 && t.Text == "ДЕТАЛЬ.001"); // designation
Assert.Contains(result, t => t.ColumnId == 3 && t.Text == "Сталь 45"); // material
Assert.DoesNotContain(result, t => t.ColumnId == 1); // name не задан
}
[Fact]
public void Collect_includes_name_as_column_1()
{
var result = StampFields.Collect(null, "Кронштейн", null);
Assert.Single(result);
Assert.Equal((1, "Кронштейн"), result[0]);
}
}