Compare commits
71 Commits
cb1984f227
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e119afdba | |||
| b188e1904f | |||
| c0d855c171 | |||
| 8e56af61b3 | |||
| 69d037c875 | |||
| 6aa05b1adb | |||
| 7cb44a073e | |||
| 16406a34be | |||
| 5d51b9e1af | |||
| beaa220510 | |||
| f89ad951ae | |||
| 08191db25c | |||
| e60c8d48e4 | |||
| 4937b9e677 | |||
| 24f3531a3c | |||
| 56ed2cb0cf | |||
| adc9da37fa | |||
| f490c146c7 | |||
| a87957582c | |||
| 178ca48faf | |||
| 38f2687c8d | |||
| 2fb0f8347d | |||
| a5de3566ba | |||
| df7bf92884 | |||
| 7b50ac1a5d | |||
| 8f4567732e | |||
| ff5fb27dd5 | |||
| ecf2e22265 | |||
| d3b3a7d589 | |||
| 775128484e | |||
| ecabaa1828 | |||
| 23341092cb | |||
| 679171c5a7 | |||
| d8edb18f06 | |||
| 5272066bef | |||
| 1f153e43ab | |||
| 328e89a6e7 | |||
| 7fa434afae | |||
| db9d76a7bd | |||
| 86a5a52347 | |||
| 246233727c | |||
| 93a67d318e | |||
| 35e06e8786 | |||
| 4e017fc1cf | |||
| 92e869006f | |||
| 454a90100a | |||
| ced95b742b | |||
| ecf7b15e7a | |||
| 17efc79ffb | |||
| 7359337a95 | |||
| b6ca2f7b21 | |||
| 910d120c8a | |||
| b6cd92b95c | |||
| cd76abd652 | |||
| 2c594bc890 | |||
| eda3242f62 | |||
| 71cafb2270 | |||
| 8e0ab374af | |||
| ca3d91ae68 | |||
| d6a129b0d1 | |||
| 56f6e0b45a | |||
| 034e9aee24 | |||
| eaa4a56708 | |||
| 742a97177b | |||
| db23a1a748 | |||
| 23d245fd78 | |||
| ddbb46196e | |||
| 6c6add72e2 | |||
| 5aa4221a37 | |||
| de063290f4 | |||
| 46648f7635 |
@@ -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».
|
||||
+23
-70
@@ -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)».
|
||||
@@ -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».
|
||||
@@ -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-приёмы импорта/экспорта/обхода).
|
||||
|
||||
@@ -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.4–0.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.4–0.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 ~15–25 мм, PETG ~10–15 мм, ABS ~12–18 мм**.
|
||||
Длинные прямоугольные проёмы **ориентировать так, чтобы мост шёл по короткой стороне**; концы —
|
||||
на сплошных опорах.
|
||||
- Нижнюю функциональную поверхность моста — припуск **0.2–0.3 мм** на провис. *Граница:* величину
|
||||
провиса из CAD не предсказать (обдув/скорость — настройки печати).
|
||||
- **Внутренние/потолочные нависания хуже наружных** — потолок пазов аркой/шевроном, не плоским
|
||||
пролётом > 2 мм.
|
||||
- **Доступ к поддержкам:** окно во внутренней полости **≥8–10 мм**.
|
||||
|
||||
## 3. Отверстия
|
||||
- **Вертикальные (ось ∥ Z)** печатаются уже номинала → **увеличить диаметр модели** (радиус на
|
||||
половину): **+0.2 мм (Ø<4)**, **+0.2–0.3 мм (Ø 4–10)**, **+0.1–0.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.15–0.20 мм | PLA↔PLA; контакт ≥20 мм → 0.20; PETG +0.05 |
|
||||
| Свободная | 0.25–0.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 ~40–55%, PETG ~35–50%, **ABS ~20–35% (выброс)**. Несущую
|
||||
нагрузку — в **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.8–0.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.5–1 мм
|
||||
× 45° или ≈ половина зазора) против задиров при сборке.
|
||||
|
||||
## 13. Разбиение детали и сборка из печатных частей
|
||||
- Конфликт «прочная ориентация vs бесподдержечность», или крупная/коробящаяся деталь → **разбить**
|
||||
на части с самоустанавливающимися стыками (печатные штифты/шпонки/замки), склейка; стыки на
|
||||
XY-гранях. Зазор стыка — по §4.
|
||||
|
||||
## 14. Защёлки (snap-fit) / живой шарнир
|
||||
- Консольная защёлка: толщина балки **≈1.0–2.0 мм** (функциональный размер, не привязан к кратности
|
||||
`w`; тоньше ~1 мм — хрупко), зацеп/возврат **0.3–0.8 мм**, длина/толщина **≥5:1** (до 10:1),
|
||||
**скругление в основании ≥R0.5**.
|
||||
- **Направление слоёв:** балка гнётся **в плоскости XY** (слои перпендикулярны изгибу), **не
|
||||
поперёк Z** (расслоится с первого нажатия).
|
||||
- **Живой шарнир** — только PLA/PP-подобные, перемычка **0.3–0.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`.
|
||||
@@ -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)».'''
|
||||
@@ -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).
|
||||
|
||||
## Лицензирование
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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`); рамка по ГОСТ-стилю; специальные выноски (позиция, клеймо, маркер).
|
||||
|
||||
@@ -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`; нужен иной механизм (отложено).
|
||||
- ✓ **Формат/ориентация листа** (A0–A5/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
@@ -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 A0–A5/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, A0–A5/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>
|
||||
|
||||
@@ -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.2–0.3 (4–10), +0.1–0.2 (>10); к диаметру (радиус +X/2).
|
||||
7. Z-прочность по материалам (PLA 40–55%, PETG 35–50%, ABS 20–35%); 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.4–0.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.4–0.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 ~15–25 мм, PETG ~10–15 мм, ABS ~12–18 мм**.
|
||||
Длинные прямоугольные проёмы **ориентировать так, чтобы мост шёл по короткой стороне**; концы —
|
||||
на сплошных опорах.
|
||||
- Нижнюю функциональную поверхность моста — припуск **0.2–0.3 мм** на провис. *Граница:* величину
|
||||
провиса из CAD не предсказать (обдув/скорость — настройки печати).
|
||||
- **Внутренние/потолочные нависания хуже наружных** — потолок пазов аркой/шевроном, не плоским
|
||||
пролётом > 2 мм.
|
||||
- **Доступ к поддержкам:** окно во внутренней полости **≥8–10 мм**.
|
||||
|
||||
## 3. Отверстия
|
||||
- **Вертикальные (ось ∥ Z)** печатаются уже номинала → **увеличить диаметр модели** (радиус на
|
||||
половину): **+0.2 мм (Ø<4)**, **+0.2–0.3 мм (Ø 4–10)**, **+0.1–0.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.15–0.20 мм | PLA↔PLA; контакт ≥20 мм → 0.20; PETG +0.05 |
|
||||
| Свободная | 0.25–0.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 ~40–55%, PETG ~35–50%, **ABS ~20–35% (выброс)**. Несущую
|
||||
нагрузку — в **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.8–0.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.3–0.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.2–0.3 мм (Ø 4–10)", # inv6 компенсация
|
||||
"PLA ~40–55%", "ABS ~20–35%", # 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 ~40–55%"},
|
||||
@{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.4–0.45 мм`.
|
||||
- **`h` (высота слоя)** — для вертикали и поведения нависаний/мостов. Дефолт `h ≈ 0.5·сопло`
|
||||
(0.2 мм); структурная печать `h = 0.2–0.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.4–0.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 ~15–25 мм, PETG ~10–15 мм, ABS ~12–18 мм**.
|
||||
Длинные прямоугольные проёмы **ориентировать так, чтобы мост шёл по короткой стороне**; концы
|
||||
моста — на сплошных опорах.
|
||||
- Функциональную нижнюю поверхность моста закладывать с припуском **0.2–0.3 мм** на провис.
|
||||
*Граница:* величину провиса (мм) из CAD не предсказать (зависит от обдува/скорости — настройки
|
||||
печати); припуск — ориентир, не гарантия.
|
||||
- **Внутренние/потолочные нависания хуже наружных** (нет обдува) — потолок пазов делать аркой/
|
||||
шевроном, не плоским пролётом > 2 мм.
|
||||
- **Доступ к поддержкам:** если поддержка во внутренней полости неизбежна — окно доступа **≥8–10 мм**.
|
||||
|
||||
### 8.3 Отверстия
|
||||
- **Вертикальные (ось ∥ Z)** печатаются уже номинала → **увеличить диаметр модели** (радиус на
|
||||
половину): **+0.2 мм (Ø<4)**, **+0.2–0.3 мм (Ø 4–10)**, **+0.1–0.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°. Низ — оставшаяся дуга окружности. (Проверка вывода: расстояние от центра до боковой прямой,
|
||||
проходящей через вершину `(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.15–0.20 мм | PLA↔PLA; контакт ≥20 мм → 0.20; PETG +0.05 |
|
||||
| Свободная | 0.25–0.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.5–1.0 × 45° (слабая калибровка / сильная притирка)**.
|
||||
- Внутренние углы у основания — **скругление ≥R0.5**.
|
||||
- **Опорная площадка:** без «лезвийных» оснований; контакт хотя бы в ~3 периметра. Высокие тонкие
|
||||
детали — **интегральный фланец 1–2 мм** как геометрия (предпочтительнее всякого brim).
|
||||
|
||||
### 8.6 Ориентация и прочность
|
||||
- **Z (межслойная) прочность от XY:** PLA ~40–55%, PETG ~35–50%, **ABS ~20–35% (выброс)**.
|
||||
Несущую нагрузку — в **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.8–0.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.3–0.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.2–0.3 (4–10), +0.1–0.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-инструменты представлены ровно по одному разу.
|
||||
- Для каждого домена перечислены необходимые отсутствующие методы.
|
||||
- Частично реализованные методы не ошибочно помечены как полностью реализованные.
|
||||
- Сквозные сценарии позволяют увидеть блокирующие пробелы независимо от общего количества методов.
|
||||
- Итоговый список `P0–P2` согласован с доменными таблицами и не содержит методов, отсутствующих в основном каталоге.
|
||||
- Документ не содержит внутренних 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) или неоднозначно (>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>Горизонтальный (измеряет |X2−X1|).</summary>
|
||||
Horizontal,
|
||||
/// <summary>Вертикальный (измеряет |Y2−Y1|).</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>Получить графу основной надписи по номеру с диагностикой (номер > 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) с диагностикой (номер > 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"/>: A0–A5 или 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"/> — ширина и высота оба
|
||||
/// конечны и > 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,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>();
|
||||
|
||||
@@ -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 (тупой
|
||||
// супплемент 180−45), 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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user