Навыки и README: два пути формообразования, надписи, проверенные факты

kompas-3d: раздел «Два пути формообразования — выбирай по форме, а не по
привычке» (призматика и карманы примитивами, текст и кривые эскизами) и
раздел про надписи с числами, снятыми на практике: height задаёт высоту
ПРОПИСНОЙ, а не габарит строки; разрыв между advance-длиной и габаритом
глифов зависит от шрифта (1-2 % у наборных, 7-9 % у скриптовых) — прежние
формулировки были выведены из одного замера и оказались неверны.

Там же — почему тонкая стенка по многоконтурному эскизу не строится поверх
пересекаемого тела (разложено экспериментом: число тел, происхождение тела
и chooseType ни при чём) и как скруглять рельефный текст: по одному ребру
строится, пакетом по всем 203 — нет.

CLAUDE.md и kompas-mcp-dev: спайк-проект под кейс допустим для проверки
теории, но итоговая деталь всегда строится инструментами MCP — раннер
обходит тот самый слой, который мы проверяем.
This commit is contained in:
2026-07-31 14:51:29 +03:00
parent 4f4dd082d9
commit d0c9d8ca3f
5 changed files with 175 additions and 6 deletions
+16
View File
@@ -46,6 +46,13 @@ description: Внутренняя методика доработки самог
- **Схема не запрещает неверную комбинацию полей — это делает валидация.** Сообщение называет
`type=…` и недостающий параметр (`SketchEntityInputs`, `FeatureTools.Required`), иначе агент
тратит лишний ход на догадки.
- **Текст исключения доходит до клиента только через фильтр.** MCP-SDK показывает сообщение лишь у
`McpException`, остальное подменяет на «An error occurred invoking '<tool>'» — то есть без
фильтра ВСЕ наши подсказки в исключениях пропадают. В `Program.cs` стоит
`WithRequestFilters(... AddCallToolFilter ...)`, который отдаёт `CallToolResult{IsError=true}` с
текстом от `ToolErrorText.Describe` (Core/Startup: разворачивает `AggregateException`, склеивает
до трёх вложенных причин через «←»). Пишешь новое сообщение об ошибке — помни, что до агента
дойдёт именно оно.
- **Мутирующая операция сама дописывает итог проверки** через `AutoValidation.AnnotateAsync`
(`Core/Validation`). Переключатель — `set_auto_validate` и `KOMPAS_MCP_AUTOVALIDATE`; проверка
никогда не превращает удачную операцию в ошибку (сбой самой проверки уходит в примечание).
@@ -56,6 +63,15 @@ description: Внутренняя методика доработки самог
> докажи механику (спайк или минимальный прогон), затем продуктизируй как **общий** инструмент MCP
> (с тестами, TDD) и опиши приём в навыке `kompas-3d`.
**Спайк — только чтобы проверить теорию; результат выдаётся инструментами MCP.** Отдельный
dotnet-проект в `usecases/<кейс>/` (ссылка на `Kompas.Mcp.Core`, `KompasInteropLoader.Install()`
первой строкой) — законный способ измерить метрику, проверить COM-цепочку или опровергнуть
утверждение навыка. Но **итоговая деталь/чертёж всегда строится вызовами MCP-инструментов**:
раннер обходит ровно тот слой, который мы и проверяем (схемы инструментов, разбор входа, форму
ответа, авто-валидацию), поэтому «построено раннером» не считается за проверку сервера. Если
инструментов не хватает — это и есть находка кейса: доработать сервер, а не достроить в обход.
Проверочный проект после кейса удаляй, полученные числа переноси в `plan.md` кейса и в навык.
Порядок для нового инструмента:
1. **Сигнатуры и константы** — через субагента `kompas-sdk-research` (см. ниже), затем перепроверка
@@ -205,3 +205,58 @@
`.Features.cs` (extrude/revolve/fillet/chamfer). Static `SketchGeometry` (`Core/Modeling`) holds the enum maps
+ validators (`RequirePositive`, `RequireVertexCount`, `RequirePoints`, `RequireMin`). Point lists use
`record SketchPoint(X, Y)` (JSON names `x`/`y`).
- **Text in a sketch → curves (`type=text`, verified live)**: a text object is *annotation* — a formative
operation never sees it. The chain that works: build `ksTextParam` (`ko_TextParam=28`) → `Init()` →
`GetParagraphParam()` (`ksParagraphParam`: `x`, `y`, `ang`, `height`, `width`) → `SetParagraphParam`;
one `ksTextItemParam` (`ko_TextItemParam=31`, `type=ksTItString=0`, `s=<string>`) whose `GetItemFont()`
(`ksTextItemFont`) carries `fontName`/`height` (mm)/`ksu` (width factor)/`bitVector`
(`ITALIC_ON=0x40`, `BOLD_ON=0x100` — from `Include/ldefin2d.h`); wrap it in
`KompasObject.GetDynamicArray(TEXT_ITEM_ARR=4)` → `ksTextLineParam` (`ko_TextLineParam=29`,
`SetTextItemArr`) → `GetDynamicArray(TEXT_LINE_ARR=3)` → `SetTextLineArr`; then
`ksDocument2D.ksTextEx(param, align)` → **`ksConvertTextToCurve(ref)`** — this is what turns glyphs into
NURBS/lines that extrude. Traps: the `align` argument of `ksTextEx` does **not** move the string (a
centred value still lays it out right-and-up from the insertion point) — instead read
`ksGetTextLengthFromReference(ref)` (advance width in mm, ≈ 10 % wider than the glyph bbox) and place the
text yourself; the dynamic arrays live in КОМПАС, so `ksDeleteArray()` them in a `finally`; an unknown
font name is silently substituted rather than failing; `height` sizes the cap+ascender, not the bbox.
- **Elementary bodies / primitives (`primitive`, verified live)**: `(IPart7 as IModelContainer)
.ElementaryBodies.Add(ksObj3dTypeEnum)` → cast to `IBlockBySizes` / `ICylinder` / `ISphere` /
`IConeByHeight` → set sizes → `Update()`. Types: `o3d_BlockBySizes=668`, `o3d_Cylinder=663`,
`o3d_Sphere=662`, `o3d_ConeByHeight=670` — and `Add` takes **`ksObj3dTypeEnum`, not `Obj3dType`**
(same numbers, different type; the compiler catches it). Placement is the primitive's own
`Position` (a `LocalCoordinateSystem` with writable X/Y/Z); for a block the anchor is a corner
(`BlockPosition = ksVertex`), for bodies of revolution the base centre. `OperationResult`
(`ksOperationUnion=0 / NewBody=1 / Cut=2 / Intersect=3`) makes the primitive a boolean in itself —
a pocket needs no sketch and no cut extrusion. Traps: `Height`, `HeightType`, `HeightOffset` and
cone's `Diameter` are **indexed properties** (`Height[true] = …`, index = the direction side), plain
assignment does not compile; and `Update()` returns TRUE for a subtraction that misses the body
entirely, so `PrimitiveService` compares the volume before/after and rolls the feature back
(`Owner.Delete()`) when nothing changed.
- **Part name/marking (`set_part_info`)**: write through API7 `IPart7.Name` / `IPart7.Marking` on
`IKompasDocument3D.TopPart`, then **`top.Update()`** — without it the tree keeps showing «Деталь».
Read-back through the same properties (API5 `ksPart.name` / `ksPart.marking` also expose them, which is
what `ModelInspectionService` uses, since its `GetTopPart` is already an API5 `ksPart`). The document's
file name is a different thing entirely: saving as `Badge.m3d` does not rename the part.
- **`IKompasDocument.SaveAs(path)` returns `void` and fails silently** when a file of that name is already
open in КОМПАС as another document: nothing is written, no error, the document simply stays unsaved.
`DocumentService.SaveAsAsync` therefore verifies afterwards — `doc.PathName` must equal the requested
path AND `File.Exists(path)` — and `SaveAsync` checks `doc.Changed == false`. Covered by
`DocumentTests.SaveAs_over_a_name_held_by_another_open_document_fails_loudly`. To free the name,
`document_close(all: true)` (→ `DocumentService.CloseAllAsync`) closes every open document.
- **Thin-wall extrusion (`extrude(thinThickness, thinSide)`)**: `ksBoss/CutExtrusionDefinition.SetThinParam(
thin, thinType, normalThickness, reverseThickness)` with `thinType` = `dtNormal` (outward), `dtReverse`
(inward), `dtBoth` (half each side). **Trap: the thickness must sit in the field matching the direction**
— with `dtReverse` the *reverseThickness* argument is the one read; passing it as `normalThickness`
leaves the wall at zero and КОМПАС silently extrudes a SOLID section instead of failing (caught only by
the volume: a 82×10.5 plate came out full instead of a 1.2 mm frame). `outward` over a closed contour is the cheap equivalent of a 2D
equidistant: it grows a rim around it (verified — a 20×20 square with 2 mm outward wall gives a 24×24
frame). Building the same sketch twice (solid + outward wall) yields a silhouette with an offset border,
which is how a lettered badge gets its backing plate without `ksEquidistant`.
**Intersection trap (isolated by experiment, not guessed):** a thin-wall extrusion over a
*multi-contour* sketch (a text line is ~10 closed contours, several with inner loops) returns
`Create()==FALSE` when the wall **intersects an already existing body**. Bisected: empty part → OK;
body present but not touched by the wall → OK; wall crosses the body → FAIL. Body count is
irrelevant (2+ bodies with a plain rectangular contour build fine), so is the body's origin
(sketch or `ElementaryBodies` primitive), and `chooseType` (tried `-1/0/1/2/3`) changes nothing —
the earlier "multi-body" explanation was wrong. Practical rule: build the thin wall before the body
it would cross; `primitive(result=union|subtract)` has no such restriction.
+6
View File
@@ -85,6 +85,12 @@ is a bug report, not a reason to work around it by hand. Three feedback channels
Reading the sources, proposing patches, editing and rebuilding are all explicitly sanctioned — including
asking for a session restart when a rebuilt server has to be picked up.
- **A spike proves a theory; the deliverable is built with MCP tools.** A throwaway dotnet project under
`usecases/<case>/` (referencing `Kompas.Mcp.Core`) is a legitimate way to measure something or falsify a
claim — but the final part/drawing must be produced by MCP tool calls. A runner bypasses the very layer
under test (tool schemas, input parsing, answer shape, auto-validation), so "built by the runner" proves
nothing about the server. Missing tools are the finding, not a reason to finish by hand; delete the
probe project afterwards and carry its numbers into the case's `plan.md` and the skill.
- **Skills are debugged like code.** `kompas-3d` and `kompas-fdm-design` are objects of revision, not fixed
inputs. Edit them **only** in `plugin/skills/<name>/` (the source of truth — the other trees are junctions);
a skill edit needs no rebuild but is only picked up by a *new* session.
+15 -3
View File
@@ -61,14 +61,14 @@ kompas-mcp.exe --version # версия бинаря, КОМПАС для эт
}
```
## Инструменты (56)
## Инструменты (58)
| Группа | Инструменты |
|---|---|
| System | `kompas_connect`, `kompas_status`, `kompas_set_visible`, `set_auto_validate` |
| Documents | `document_create`, `document_open`, `document_save`, `document_close`, `document_active` |
| Documents | `document_create`, `document_open`, `document_save`, `document_close`, `document_active`, `set_part_info` |
| Sketch | `sketch_create`, `sketch_add`, `sketch_close` |
| Features | `extrude`, `revolve`, `fillet_edge`, `chamfer_edge`, `shell`, `rib`, `sweep`, `loft`, `pattern`, `mirror`, `hole`, `draft`, `rebuild` |
| Features | `extrude`, `revolve`, `primitive`, `fillet_edge`, `chamfer_edge`, `shell`, `rib`, `sweep`, `loft`, `pattern`, `mirror`, `hole`, `draft`, `rebuild` |
| Edit | `move_face`, `split_solid_by_plane`, `move_body`, `boolean_union` |
| Inspection | `describe_model`, `list_features`, `list_bodies`, `list_variables`, `measure` |
| Vision | `model_snapshot` (PNG-снимок; сначала — `describe_model`) |
@@ -85,6 +85,18 @@ kompas-mcp.exe --version # версия бинаря, КОМПАС для эт
либо точкой в мировых координатах. Эскиз строится списком примитивов за один вызов:
`sketch_create(plane|faceIndex, entities[])`.
Формообразование идёт двумя путями, и их сочетают: `primitive(kind=block|cylinder|sphere|cone,
result=new|union|subtract|intersect)` строит тело по размерам без эскиза (в том числе вычитает
карман или паз), а `sketch_create` + `extrude` нужен там, где контур сложный — текст, кривые,
произвольный профиль.
Примитивы эскиза: `line`, `circle`, `rectangle`, `arc`, `arc3points`, `ellipse`, `polyline`,
`polygon`, `spline`, `point`, `axis`, `text`. Надпись (`type=text` с `text`, `height`, `fontName`)
сразу переводится в кривые, поэтому её можно выдавливать — так делаются логотипы и гравировка;
в ответе возвращается фактическая длина строки, по ней надпись выравнивают. Параметры
`extrude(thinThickness, thinSide)` превращают контур в стенку заданной толщины: `outward`
наращивает её наружу и даёт ободок-эквидистанту вокруг контура (например, подложку под надпись).
### Типовой сценарий
`kompas_connect``document_create part`
+83 -3
View File
@@ -47,9 +47,9 @@ MCP-сервер даёт **общие** операции КОМПАС (эски
**Реализованы (56 инструментов).** Инструмент назван по СМЫСЛУ операции, а вид задаётся параметром —
не ищи отдельный инструмент под каждый вариант:
- *Система/документы:* `kompas_connect`, `kompas_set_visible`, `kompas_status`, `set_auto_validate`; `document_create|open|save|close|active` (`document_save(path)` — сохранить новый документ или копию, без `path` — на прежнее место).
- *Эскиз (три инструмента вместо шестнадцати):* `sketch_create` — основание задаётся `plane` (+`offset`) ЛИБО `faceIndex` ЛИБО точкой `x,y,z`, а геометрия сразу списком `entities[{type: line|circle|rectangle|arc|arc3points|ellipse|polyline|polygon|spline|point|axis, …}]`; `sketch_add` — дополнить открытый эскиз; `sketch_close`.
- *Формообразующие:* `extrude(mode=boss|cut)`, `revolve(mode=boss|cut)`, `fillet_edge`/`chamfer_edge` (список `edgeIndices` — одной операцией на все рёбра — либо точка), `shell`, `rib`, `sweep`, `loft`, `draft`, `rebuild`.
- *Система/документы:* `kompas_connect`, `kompas_set_visible`, `kompas_status`, `set_auto_validate`; `document_create|open|save|close|active` (`document_save(path)` — сохранить новый документ или копию, без `path` — на прежнее место); `set_part_info(name, marking)` — наименование и обозначение детали.
- *Эскиз (три инструмента вместо шестнадцати):* `sketch_create` — основание задаётся `plane` (+`offset`) ЛИБО `faceIndex` ЛИБО точкой `x,y,z`, а геометрия сразу списком `entities[{type: line|circle|rectangle|arc|arc3points|ellipse|polyline|polygon|spline|point|axis|text, …}]`; `sketch_add` — дополнить открытый эскиз; `sketch_close`.
- *Формообразующие:* `primitive(kind=block|cylinder|sphere|cone, result=new|union|subtract|intersect)` — тело по размерам БЕЗ эскиза, `extrude(mode=boss|cut)`, `revolve(mode=boss|cut)`, `fillet_edge`/`chamfer_edge` (список `edgeIndices` — одной операцией на все рёбра — либо точка), `shell`, `rib`, `sweep`, `loft`, `draft`, `rebuild`.
- *Отверстия:* `hole(type=simple|counterbore|countersink|conic)` — грань по `faceIndex` (центр грани) либо точкой.
- *Массивы и зеркало:* `pattern(kind=linear|circular)`, `mirror` (без `featureIds` — всё тело, с ними — только эти операции).
- *Прямое редактирование (без дерева, в т.ч. импортированная B-rep):* `move_face` (сдвинуть грань на N мм по нормали; грань — `faceIndex` или точка), `split_solid_by_plane` (рассечь тело плоскостью), `move_body` (сдвинуть тело на вектор), `boolean_union` (объединить тела).
@@ -63,6 +63,43 @@ MCP-сервер даёт **общие** операции КОМПАС (эски
Набор растёт от версии к версии — актуальный перечень с описаниями параметров отдаёт сам
MCP-сервер; если инструмента под задачу нет, собирай результат из имеющихся общих операций.
## Два пути формообразования — выбирай по форме, а не по привычке
«Эскиз → выдавливание» — не единственный способ. `primitive` строит тело сразу по размерам, и это
короче и надёжнее там, где форма призматическая:
| Форма | Чем строить |
|---|---|
| Плита, брусок, бобышка, штифт, цилиндрическая стойка | `primitive(kind=block\|cylinder)` — один вызов вместо «эскиз + выдавливание» |
| Прямоугольный карман, паз, срез угла | `primitive(..., result=subtract)` — вычитание тела вместо `extrude(mode=cut)` по эскизу |
| Скруглённые углы призмы | `primitive` + `fillet_edge` по рёбрам (скругление после, а не дуги в эскизе) |
| Текст, кривые, произвольный контур, переменное сечение | ТОЛЬКО эскиз: `sketch_create(entities=[…])` + `extrude`/`revolve`/`loft`/`sweep` |
| Тонкая стенка, ободок по контуру | эскиз + `extrude(thinThickness, thinSide)` |
Их **сочетают в одной детали**: корпусные объёмы — примитивами, сложные контуры — эскизами.
Точка привязки у `block`**угол** (не центр), у тел вращения — центр основания; размеры идут по
осям X/Y/Z текущей системы координат.
- **Тонкую стенку по сложному контуру строй ДО того, как появится тело, которое она пересечёт.**
`extrude(thinThickness=…)` по многоконтурному эскизу (надпись — это десяток замкнутых контуров с
внутренними «дырками») ОТКАЗЫВАЕТ, если стенка пересекает уже построенное тело. Разложено
экспериментом: в пустой детали — строится; тело есть, но стенка его не задевает — строится;
стенка пересекает тело — отказ. Ни число тел, ни то, чем тело создано (эскизом или примитивом),
роли не играют, а простой прямоугольный контур проходит и с пересечением. Поэтому на шильдике
«плашка → буквы → кайма» падает, а «буквы → кайма → плашка `primitive(union)` → карман
`primitive(subtract)`» проходит: примитивам пересечение безразлично.
- **Скругления делай, пока рёбер мало** — сразу после примитивов их видно в `list_edges` наперечёт.
Если момент упущен и рёбер сотни, бери рёбра ПО ТОЧКЕ: `fillet_edge(radius, x, y, z)` на углу
известных координат — по одному ребру за вызов, зато без перебора индексов.
- **Скругление не построится там, где ребро «съедено» соседним элементом.** У шильдика угол плашки,
накрытый каймой букв, не скругляется ни R0.5, ни R0.3 — там уже нет ребра. Это нормальный ответ
геометрии, а не ошибка вызова: проверь снимком, что угол вообще виден снаружи.
- **Вычитание проверяется объёмом.** `primitive(result=subtract)` мимо тела КОМПАС считает удачей;
инструмент это ловит и сообщает, но привычку сверять `describe_model(sections="mass")` не отменяет.
- **`result=new` даёт ОТДЕЛЬНОЕ тело.** Оно объединится с остальными, только если последующая
операция их пересечёт; иначе в детали останется несколько тел — проверяй `list_bodies` и при
необходимости зови `boolean_union`.
## Базовый цикл (эскиз → операция → осмотр)
Опорный сценарий построения, проверен end-to-end:
@@ -73,11 +110,50 @@ MCP-сервер; если инструмента под задачу нет, с
4. `extrude(mode="boss", depth=…)` / `revolve(mode="boss", …)` → читаешь итог проверки прямо в ответе (правило 2).
5. **Осмотр — структурно:** `describe_model` (паспорт: габарит, МЦХ, топология, дерево); нужен кусок — `describe_model(sections="box,mass")`. Снимок — только если нужен визуальный контроль.
6. Итерация «на грани»: `list_faces` (при нужде `list_faces(index=N)` за подробностями) → `sketch_create(faceIndex=N, entities=[…])` → операция.
7. **Назови деталь**`set_part_info(name=…)``marking`, если известно обозначение). Без этого в дереве
стоит безликая «Деталь», и она же уедет в штамп чертежа и в спецификацию; `describe_model` специально
отмечает деталь без наименования. Имя файла при сохранении этого НЕ заменяет — это разные свойства.
**Не разбивай построение на лишние вызовы.** Прямоугольник с четырьмя отверстиями — это ОДИН
`sketch_create` со списком из пяти примитивов, а не шесть вызовов. Скругление восьми рёбер — один
`fillet_edge(radius, edgeIndices=[…])`, а не восемь. Каждый лишний вызов — лишний шанс сбиться.
## Надписи, логотипы и рельеф
Надпись в эскизе — примитив `type=text` (`points:[{x,y}]` — левый край базовой линии, `text`,
`height` в мм, `fontName` — любой установленный в системе шрифт, плюс `bold`/`italic`,
`widthFactor` — сужение, `angle` — наклон строки). Она **сразу переводится в кривые**, поэтому
выдавливается как обычный контур: гравировка — `extrude(mode="cut")`, выпуклые буквы — `mode="boss"`.
- **Позиционирование — в два прохода, длина строки лишь для прикидки.** Ответ содержит длину
надписи в мм («надпись «OldMan» — длина 79.05 мм»), но это ширина С боковыми просветами, и
насколько она больше самих глифов — зависит от шрифта: замерено 1–2 % у наборных (Zilla Slab,
Bevan) и 7–9 % у скриптовых (Lobster, Pacifico). Поэтому: вставь надпись, выдави, прочитай
**фактический** габарит `describe_model(sections="box")` и уже по нему пересчитай точку вставки.
- **`height` — это высота ПРОПИСНОЙ (cap height), а не габарит строки.** Замер на Zilla Slab при
`height=10`: «HH» → 9.97 мм, «hd» → 10.71 (восходящие выше прописной), «Hy» → 13.24 мм
(нижний выносной уходит на −3.27). Считаешь компоновку — закладывай выносные отдельно.
- **Ширина линейна** и по `height`, и по `widthFactor`: вписать строку в заданную ширину можно
одним пересчётом, без подбора вслепую.
- **Подложка под надпись — тонкой стенкой, а не вторым эскизом.** `extrude(..., thinThickness=1.2,
thinSide="outward")` по контуру букв даёт ободок-эквидистанту вокруг них; выдавь ТОТ ЖЕ эскиз
дважды — сплошным на высоту подложки и стенкой наружу — и получишь силуэт надписи с равномерной
каймой. `inward` съедает контур внутрь (рамка по периметру плашки), `both` делает контур средней
линией стенки. **Проверяй стенку по объёму:** если толщина не применилась, КОМПАС молча строит
сплошное сечение — габарит при этом тот же, и по нему подмену не увидеть.
- **Рельеф = разница глубин.** Основание 2 мм + буквы 3 мм от той же плоскости: буквы выступают
на 1 мм. Не строй буквы «на грани основания» — из одного эскиза на базовой плоскости получается
и то и другое, и рельеф не зависит от порядка операций.
- **Проверяй объёмом, а не габаритом.** Буквы дают сотни NURBS-рёбер, и ни снимок, ни габарит не
покажут, что операция построила не то: сравнивай ПРИРОСТ объёма (`describe_model(sections="mass")`)
с прикидкой «площадь контура × высота». Именно так ловится молча не применившаяся тонкая стенка.
- **Скруглять рельефный текст можно, но по одному-двум рёбрам, а не пакетом.** Проверено на
Zilla Slab «AB» (203 ребра): одиночное ребро скругляется R0.1 и R0.2, а `fillet_edge` сразу по
всем рёбрам отказывает при обоих радиусах (соседние скругления конфликтуют). Адресно выбрать
«только верхние» рёбра пока нечем — `list_edges` не отдаёт ни координат, ни принадлежности грани.
- **Шрифт обязан быть установлен в системе**, где работает КОМПАС: неизвестное имя молча
подменяется другим шрифтом, и надпись «поедет» по ширине без единого сообщения об ошибке.
## Работа с импортом / сборками
Конвейер «импорт → разбор → извлечение детали → осмотр → модификация → экспорт»:
@@ -118,6 +194,10 @@ MCP-сервер; если инструмента под задачу нет, с
сверяй результат числами (`describe_model(sections="box,mass")`), снимок лишь при необходимости.
- **МЦХ.** Объём/масса меняющейся геометрии — `describe_model(sections="mass")`; перед чтением после правок — `rebuild`.
- **id эскизов/операций** недействительны после смены активного документа (create/open/close).
- **Имя файла занимает открытый документ.** Если та же деталь уже открыта в КОМПАС (например, прошлая
версия из этой же папки), `document_save(path)` по этому пути не пройдёт — КОМПАС не перезаписывает
занятый файл. Освободить имя: `document_close(all=true)`. Проверяй, что файл на диске реально
обновился (время изменения), особенно когда перестраиваешь деталь поверх прежней.
- **Единицы — мм** (геометрия) и кг (масса). Локальные координаты эскиза ≠ мировые координаты модели.
## Открытые вопросы / границы