# 3D-модели STEP/STL (твердотельное моделирование)

Ты строишь и правишь твердотельные 3D-модели через ядро **OCC** библиотекой **build123d** в песочнице. Геометрию пишешь Python-кодом, а измерение, рендер, серию исполнений и экспорт делают готовые скрипты.

Скрипты лежат в `$SKILLS_ROOT/cad_3d_ru/scripts/` и уже в `sys.path` — модули `solid_build`, `cad_check` импортируются напрямую по имени.

---

## S0. ТРИ ПРАВИЛА

**1. STEP пишет ядро.** Файл появляется двумя путями: `solid_build.save_part(part, "имя")` собирает его из твоего Python-исходника, либо `solid_build.load_part(path)` читает чужой. Текст STEP набранный вручную — это `ISO-10303-21;` с выдуманными номерами сущностей: он проходит взгляд и не открывается ни в одном ядре, а ошибка всплывает у человека, который его получил.

**2. После каждого изменения — `cad_check`, и только потом рендер.** Числа стоят доли секунды, картинка — секунды и токены на её разглядывание. Сходятся объём, масса и габариты — тогда смотри `cad_render` и сверяй форму с чертежом. Порядок «дёшево → дорого» экономит те попытки, которых потом не хватит.

**3. Три-пять попыток на одну и ту же ошибку — потолок.** Один и тот же `hint` четвёртый раз значит, что причина не там, где ты её ищешь. Выходи строкой:

```
HUMAN_REVIEW: <что не сходится и почему цикл не помог> — last failure: <hint из cad_check>
```

Человек читает эту строку и приносит недостающее (размер, которого нет на чертеже; допуск; вид сбоку). Пятая попытка вслепую тратит бюджет и приносит ту же ошибку.

---

## S1. WORKFLOW

| Задача | Шаги |
|--------|------|
| Новая деталь | геометрия в ячейке → `save_part` → `cad_check.py` (+ `--assert`) → `cad_render.py` → сверка с чертежом → `cad_export.py` |
| Деталь по чертежу с графой «кг» | то же, но `cad_check.py part.step --material сталь --assert "mass_kg=1.20..1.35"` — масса из штампа становится проверкой, а не надеждой |
| Серия исполнений (L, L1…L4) | модуль с `build(**params)` → `params_table.py --module … --table …` → файлы по исполнениям |
| Правка чужого STEP | `cad_inspect.py donor.step` → `load_part` → булевы/вырезы → `save_part` → `cad_check` → export |
| Сборка из нескольких тел | `cad_inspect.py --split /tmp/cad` → правь тело отдельно → собирай обратно булевыми |

Единицы — миллиметры, как в STEP AP214 и на чертеже. Промежуточные файлы живут в `/tmp/cad`, артефакты — в `$OUTPUT_ROOT/`, и кладёт их туда только `cad_export.py`: деталь, не прошедшая гейт, до пользователя не доезжает.

---

## S2. ГЕОМЕТРИЯ — build123d (Python в песочнице)

Алгебраический режим: примитивы складываются операторами, результат — обычный объект.

```python
from build123d import *

part = Box(80, 60, 20) - Cylinder(radius=8, height=30)
part = part + Pos(0, 0, 20) * Cylinder(radius=15, height=10)
```

Примитивы: `Box(l, w, h)`, `Cylinder(radius, height)`, `Cone(bottom_radius, top_radius, height)`, `Sphere(radius)`, `Torus(major_radius, minor_radius)`.
Позиционирование: `Pos(x, y, z) * shape` — сдвиг, `Rot(X, Y, Z) * shape` — поворот в градусах.
Булевы: `+` объединение, `-` вычитание, `&` пересечение.

Скругления и фаски берут рёбра запросом — сначала отбери, потом применяй:
```python
part = fillet(part.edges().filter_by(Axis.Z), radius=3)
part = chamfer(part.edges().group_by(Axis.Z)[-1], length=1.5)
```
`edges()`, `faces()`, `vertices()` возвращают списки с `.filter_by(Axis.Z)`, `.group_by(Axis.Z)[-1]` (верхняя группа), `.sort_by(SortBy.AREA)`.

Профиль → тело: контур точками, затем вращение или выдавливание (см. S3 — там это одной строкой).

Измерение прямо в ячейке, когда нужно принять решение по ходу:
```python
print(part.volume, part.bounding_box().size, len(part.faces()), part.is_valid, part.is_manifold)
```
`is_valid` и `is_manifold` — свойства, не методы: со скобками получишь `TypeError: 'bool' object is not callable`.

---

## S3. ГОТОВЫЕ ТЕЛА — `solid_build`

Формы, которые руками пишутся длинно и ломаются тихо.

```python
import solid_build as sb

вал = sb.revolve_profile([(0, 0), (20, 0), (20, 60), (15, 60), (15, 120), (0, 120)])
плита = sb.extrude_profile([(0, 0), (200, 0), (200, 80), (0, 80)], height=12)
плита = sb.hole_pattern(плита, [(20, 20), (180, 20), (20, 60), (180, 60)], diameter=11)
фланец = sb.flange(160, 14, bore_diameter=50, bolt_count=8,
                   bolt_circle_diameter=125, bolt_diameter=14,
                   hub_diameter=80, hub_height=20)
path = sb.save_part(фланец, "flange")
```

- `revolve_profile(points, angle=360, plane="XZ")` — тело вращения вокруг оси Z; точки это `(радиус, высота)`, то есть ровно та половина сечения, которой деталь размечена на чертеже. Контур замыкается сам; отрицательный радиус означает пересечение оси и отвергается сразу.
- `extrude_profile(points, height, plane="XY", taper=0)` — выдавливание замкнутого контура, `taper` в градусах даёт уклон.
- `hole_pattern(part, positions, diameter, depth=None)` — массив отверстий; `depth=None` сверлит насквозь по габариту.
- `bolt_circle(count, circle_diameter, start_angle=0)` — координаты болтовой окружности, отдельно от сверления.
- `flange(...)` — диск + опциональные бобышка, расточка и болтовая окружность; болтовая окружность, вылезающая за наружный диаметр, отвергается с указанием обоих чисел.
- `save_part(part, filename)` — STEP в `/tmp/cad`, не в артефакты. `load_part(path)` — чтение обратно.

---

## S4. ЯДРО ЦИКЛА — `cad_check`

```bash
python $SKILLS_ROOT/cad_3d_ru/scripts/cad_check.py /tmp/cad/flange.step \
    --material сталь --assert "mass_kg=2.2..2.5" --assert "bbox_z<=40"
```

Ответ — JSON, и первые два ключа `status` и `hint`. **`hint` читается первым**: это готовая подсказка, что делать дальше, остальные числа — её обоснование.

```json
{
  "status": "fail",
  "hint": "mass_kg: измерено 2.3394, по чертежу не меньше 0.5 и не больше 0.9. Правь размер, породивший эту величину, и прогони cad_check снова — рендер потом",
  "gate": {"passed": true, "failed": []},
  "volume_mm3": 298017.762, "mass_kg": 2.3394,
  "bbox": {"min": [-80, -80, 0], "max": [80, 80, 34], "size": [160, 160, 34]},
  "face_count": 14, "edge_count": 33, "solid_count": 1, "area_mm2": 56152.827,
  "assertions": [{"name": "mass_kg", "measured": 2.3394, "ge": 0.5, "le": 0.9, "ok": false}]
}
```

**Гейт**: `watertight` (в файле есть тело, а не поверхности) ∧ `manifold` ∧ `volume > 0` ∧ `valid` (ядро не нашло самопересечений). Гейт красный — экспорта нет: `cad_export` отказывается писать и печатает тот же отчёт. Так поверхностный «супчик», который вьюер ещё покажет, а станок и печать уже нет, не доезжает до человека.

**Проверки против чертежа** — величины `mass_kg`, `volume_mm3`, `bbox_x/y/z`, `face_count`, `edge_count`, `solid_count`, `area_mm2`. Формы: `имя=низ..верх`, `имя>=число`, `имя<=число`. Точного равенства нет намеренно: у чисел с плавающей точкой оно не выполняется никогда, а графа «кг» на чертеже всё равно округлена — бери коридор ±2%.

Материал задаётся именем (`сталь`, `чугун`, `алюминий`, `латунь`, `бронза`, `медь`, `титан`, `нержавейка`, `abs`, `pla`, `petg`) или плотностью числом в кг/м³; по умолчанию сталь 7850.

Из ячейки то же самое доступно как функции: `cad_check.check(path, material, [cad_check.parse_assertion("mass_kg>=1.2")])`.

---

## S5. РЕНДЕР — `cad_render`

```bash
python $SKILLS_ROOT/cad_3d_ru/scripts/cad_render.py /tmp/cad/flange.step --views iso,front,top,right
```
PNG с несколькими ракурсами одним листом в `$OUTPUT_ROOT/`. Ракурсы: `iso`, `front`, `back`, `left`, `right`, `top`, `bottom`. `--tolerance` — допуск тесселяции в мм (мельче = точнее и медленнее).

Смотри картинку тогда, когда числа уже сошлись: она отвечает на вопрос «та ли это форма», а не «те ли это размеры». Сверяй с чертежом по видам — фронт с главным видом, top с видом сверху.

---

## S6. СЕРИЯ ИСПОЛНЕНИЙ — `params_table`

Таблица исполнений на чертеже — это одна модель с параметрами, а не пять моделей. Напиши модуль с функцией `build(**params)`, где имена аргументов совпадают с колонками таблицы:

```python
import solid_build as sb


def build(D, L, d_bore):
    """Вал D×L с осевым отверстием."""
    заготовка = sb.revolve_profile([(0, 0), (D / 2, 0), (D / 2, L), (0, L)])
    отверстие = sb.revolve_profile([(0, -1), (d_bore / 2, -1), (d_bore / 2, L + 1), (0, L + 1)])
    return заготовка - отверстие
```

```bash
python $SKILLS_ROOT/cad_3d_ru/scripts/params_table.py \
    --module /tmp/cad/shaft.py --table /tmp/executions.csv \
    --name-col исполнение --out-dir $OUTPUT_ROOT --assert "mass_kg<=5"
```
Каждая строка проходит тот же гейт и те же `--assert`; файл получают только прошедшие, у остальных в отчёте стоит свой `hint`. Колонка с именем исполнения читается как текст, поэтому «01» остаётся «01».

---

## S7. ЧУЖОЙ STEP — `cad_inspect`

```bash
python $SKILLS_ROOT/cad_3d_ru/scripts/cad_inspect.py /home/user/input/donor.step
python $SKILLS_ROOT/cad_3d_ru/scripts/cad_inspect.py donor.step --split /tmp/cad
```
Печатает тела с объёмами и габаритами, заголовок файла и вердикт гейта — то есть говорит, был ли донор сломан ещё до твоих правок. `--split` раскладывает тела по отдельным STEP: сборку правят по одному телу, а не целиком.

`load_part` возвращает одно тело как тело, а несколько — как компаунд, и не сплавляет их: сборка из трёх деталей не должна молча стать одной.

---

## S8. ЭКСПОРТ — `cad_export`

```bash
python $SKILLS_ROOT/cad_3d_ru/scripts/cad_export.py /tmp/cad/flange.step \
    --name Фланец --material сталь --assert "mass_kg=2.2..2.5" --stl --dxf --dxf-plane XZ
```
- **STEP AP214** пишется всегда (`--no-step` отключает). Схему скрипт читает из записанного файла и печатает — это утверждение о самом артефакте.
- **STL** (`--stl`, `--tolerance`) — печать и меш.
- **DXF** (`--dxf`, `--dxf-plane XY|XZ|YZ`) — плоское сечение тела для чертежа. Полноценный чертёж с рамкой, штампом и размерами — это скилл `cad_drafting_ru`, сюда приходят за телом.

Гейт и `--assert` прогоняются здесь заново: артефакт — это то, что уйдёт человеку, и проверка на нём стоит дешевле разбирательства после.

---

## S9. ГРАНИЦЫ

- Поддержано: твердотельные примитивы и булевы, тела вращения, выдавливание с уклоном, скругления и фаски по запросу рёбер, массивы отверстий, фланцы, серии исполнений, импорт и правка STEP, экспорт STEP AP214 / STL / плоского DXF-сечения, измерение массы и габаритов, рендер PNG.
- Не поддержано: поверхностное моделирование класса A, листовой металл с развёрткой, кинематика и сборочные сопряжения, деревья параметрической истории (модель — это код, история правок — это код), чтение и запись нативных форматов КОМПАС/SolidWorks. Если задача требует этого — скажи прямо и предложи STEP как обменный формат.
- Оформленный чертёж, размеры по ГОСТ, рамка и основная надпись — `cad_drafting_ru`.

---

*Тело собирает ядро из твоего кода; `cad_check` решает, тело ли это; рендер отвечает на «та ли форма»; экспорт — последняя дверь, и она заперта, пока гейт красный.*
