commit 1971daf8c74c71f8b5c7c08ccd1a99013b9c0b43 Author: Denis Khristolyubov Date: Sun Sep 13 00:15:15 2026 +0200 первый коммит: инструмент, документ, правила, шаблоны diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..85d7499 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +*.step filter=lfs diff=lfs merge=lfs -text +*.stp filter=lfs diff=lfs merge=lfs -text +*.stl filter=lfs diff=lfs merge=lfs -text +*.glb filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.FCStd filter=zippey diff=zippey diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b91f3f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +.venv/ +tool/static/ +*.FCBak +*.tmp +tmp*/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8dbee39 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,28 @@ +# Контекст для ассистента + +Продукт: автобагажники Airholder (RECADA, Аугсбург). Платформа — алюминиевые +профили, ноги — гнутая нержавейка 1.4301 t=2–3 мм. Резка и гибка — Blechcon.de. + +## Стандарты +- Материал ног: 1.4301 (прибрежная версия 1.4404), t = 3.0 по умолчанию +- Радиус гиба: R = 1×t (R3 при t=3) +- Крепёж: DIN, класс A2; A4 для прибрежной версии +- Между нержавейкой и алюминием всегда изолятор (EPDM) +- Покрытие снаружи: полиэфирный порошок (не эпоксид) + +## Архитектура +- Ядро: OpenCASCADE напрямую через OCP 7.9.3, headless +- FreeCAD — рабочее место инженера, не звено автоматики +- Истина: params.json + генератор; геометрия — артефакт +- На границах — STEP с именами (XCAF), внутри — BREP + +## Что делать при работе с деталью +1. Обмер (measure) → паспорт с толщиной, гибами, отверстиями, разгрузками +2. Реверс (fold2) → тело + метрология + целостность +3. Правки — операциями инструмента, не руками в CAD +4. Экспорт — пакет производства (STEP, DXF, паспорт, BOM) +5. Проверка на портале Blechcon перед заказом + +## Незакрытое +- Разгрузки в зоне гиба: генерировать по правилу, а не копировать — + блокирует параметрический поворот углов через портал diff --git a/README.md b/README.md new file mode 100644 index 0000000..f1ee57a --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# RECADA / Airholder + +Параметрическая система проектирования автобагажников: истина — параметры и код, +геометрия — артефакт. Реверс листовых деталей, приёмка по трём критериям, +пакет производства для поставщика. Всё headless, на своём сервере в Германии. + +**Рабочая папка:** `C:\CAD COWORK` + +## Структура + +``` +docs/ документ проекта (RU/DE), резюме, архив ревизий +tool/ recada.py — инструмент реверса, документация, код для FreeCAD +parts/ детали: STEP, паспорта обмера, генераторы, spec.yaml +vehicles/ паспорта машин: точки крепления, зоны, ссылки на Aufbaurichtlinien +accessories/ каталог аксессуаров и интерфейсы крепления +library/ библиотека для регрессии: детали + ответы портала +rules/ dfm_blechcon.json, materials.json, тарифы, правила валидатора +ci/ пайплайны сборки и регрессии +``` + +## Первая установка (один раз) + +**cmd:** +``` +cd /d "C:\CAD COWORK\tool" +python -m venv .venv +.venv\Scripts\pip install cadquery-ocp build123d networkx numpy scipy streamlit ezdxf pandas +``` + +**PowerShell:** +```powershell +cd "C:\CAD COWORK\tool" +python -m venv .venv +.\.venv\Scripts\pip install cadquery-ocp build123d networkx numpy scipy streamlit ezdxf pandas +``` + +## Запуск + +**cmd:** `"C:\CAD COWORK\go.bat"` +**PowerShell:** `& "C:\CAD COWORK\go.ps1"` + +Скрипты переносят свежий `recada.py` из Downloads (если он там есть) и запускают +интерфейс на `http://localhost:8501`. + +Подробности — `tool/УСТАНОВКА.md` и `tool/ЗАПУСК.md`. + +## Три критерия приёмки + +| Критерий | Чем проверяется | Что ловит | +|---|---|---| +| Метрология | point-sampling двусторонний, до поверхности, порог 99.9 % в 0.05 мм | точность геометрии | +| Целостность | свободных рёбер = 0, солидов ≥ 1 | дыры, незамкнутость | +| Производство | загрузка на портал поставщика | распознавание как листовой детали | + +Далее: посадка по скану, входной контроль поставки, поле. + +## Правила проекта + +1. Правится только истина (params.json, генераторы, паспорта, rules, спеки). + Всё остальное пересобирается — артефакт руками не трогают. +2. Файл без SKU и ревизии в имени не покидает сервер. +3. При расхождении версий права та, что выше в трубе истины. +4. Версия ядра OCCT фиксирована (7.9.3) и записана в project.json; + переход — только после прогона регрессии по библиотеке. +5. BREP внутри процесса, STEP на любой границе; STEP пишется с именами через XCAF. +6. Инструмент входит в стек, только если open source или бесплатен без подписки. + +## Документ + +- `docs/RECADA_проект_полное_описание_v59.pdf` — полная версия, 85 стр., сквозная нумерация 1–44 +- `docs/RECADA_проект_полное_описание_v59.md` — то же в Markdown: истина документа, для правок и диффов +- `docs/RECADA_Projektbeschreibung_v50_DE_komplett.pdf` — немецкая версия, 55 стр. +- `docs/RECADA_Резюме_RU_v50.pdf`, `docs/RECADA_Zusammenfassung_DE_v50.pdf` — резюме для внешних + +## Git (когда поднимется сервер) + +``` +cd /d "C:\CAD COWORK" +git init +git lfs install +git add . +git commit -m "первый коммит: инструмент, документ, правила, шаблоны" +git remote add origin git@<сервер>:recada/project.git +git push -u origin main +``` diff --git a/accessories/.gitkeep b/accessories/.gitkeep new file mode 100644 index 0000000..198ecfb --- /dev/null +++ b/accessories/.gitkeep @@ -0,0 +1 @@ +Пока пусто. См. README.md — назначение папки. diff --git a/ci/.gitkeep b/ci/.gitkeep new file mode 100644 index 0000000..198ecfb --- /dev/null +++ b/ci/.gitkeep @@ -0,0 +1 @@ +Пока пусто. См. README.md — назначение папки. diff --git a/docs/RECADA_Projektbeschreibung_v50_DE_komplett.pdf b/docs/RECADA_Projektbeschreibung_v50_DE_komplett.pdf new file mode 100644 index 0000000..cd492c1 --- /dev/null +++ b/docs/RECADA_Projektbeschreibung_v50_DE_komplett.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1952ec339374c9e295de4b856d2183e19478002fa027037c3fe3e573d93e8e4 +size 407725 diff --git a/docs/RECADA_Zusammenfassung_DE_v50.pdf b/docs/RECADA_Zusammenfassung_DE_v50.pdf new file mode 100644 index 0000000..396f3de --- /dev/null +++ b/docs/RECADA_Zusammenfassung_DE_v50.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c193c6b1916e4e6862111b05a753a8557f9206705a4d7abb0f3849cef6a92d55 +size 56278 diff --git a/docs/RECADA_Резюме_RU_v50.pdf b/docs/RECADA_Резюме_RU_v50.pdf new file mode 100644 index 0000000..3b62e91 --- /dev/null +++ b/docs/RECADA_Резюме_RU_v50.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e276642a20f389a7bc00051d375fd4af89c698a4a185a47ccdfbb91069ba497 +size 65831 diff --git a/docs/RECADA_проект_полное_описание_v59.md b/docs/RECADA_проект_полное_описание_v59.md new file mode 100644 index 0000000..559b042 --- /dev/null +++ b/docs/RECADA_проект_полное_описание_v59.md @@ -0,0 +1,1700 @@ +# RECADA / Airholder — полное описание проекта + +**Облачная параметрическая CAD-система** · ревизия 59 · 11 сентября 2026 + +## Содержание + +- Легенда: термины и сокращения +- История ревизий +- 1. Идея и исходная ситуация +- 2. Архитектурный принцип и слои +- 3. Файлы и форматы: как устроена истина +- 4. Как проектирует человек +- 5. Конфигуратор (Unity + Shopify) и инструкции +- 6. Прототипирование: четыре ступени +- 7. Аксессуары: вторая ось +- 8. Кадровая лестница: рост по результатам в логах +- 9. Наём в разных странах: рынки и ставки +- 10. Жизненный цикл: две оси +- 11. Изменение деталей: где дорого +- 12. ISO 9001 +- 13. Физические испытания и ISO 11154 +- 14. Открытая платформа сообщества +- 15. Роль FreeCAD и инженера: кто что делает +- 16. Инструмент реверса recada.py — построен и проверен +- 17. Редактор изделия: багажник как параметр, а не как геометрия +- 18. Библиотека деталей и сервер: система с обратной связью +- 19. Инструкция по монтажу как артефакт изделия +- 20. Два мира: B-Rep и сетка +- 21. Данные: источники, каталоги, единая структура +- 22. Конкуренты как источник данных +- 23. Проверки уровня изделия: размещение, доступность, высота, настройка +- 24. Дизайн продукта: что даёт система +- 25. Обратная связь из поля: рекламации, опыт покупателей, соцсети +- 26. Модель версий: уроки Onshape +- 27. Инфраструктура: решения по железу +- 28. Граф зависимостей конструкции и автоматизация +- 29. Финансовая модель — каркас +- 30. Управленческий слой: роли подробно +- 31. Материалы и технологии +- 32. Внешние ресурсы: что берём и чего не берём +- 33. Скорость: что ещё ускоряет вывод багажника +- 34. Удалённые столы: способы, выбор, география +- 35. Найм: три профиля и тесты самой системой +- 36. Права на результаты и коммерческая тайна +- 37. Монетизация: модели А, Б, В +- 38. Программа «Airholder Ready» +- 39. Поток вместо кодинга: измерить и уменьшить итерации +- 40. Управление проектом: доска по воротам вместо диаграммы Ганта +- 41. Датасеты CAD и CAD-агенты: что есть в мире и что из этого наше +- 42. Гибка листового металла в датасетах: BenDFM и KUL-bend +- 43. Риски и узкие места +- 44. Дорожная карта + +--- + +## Легенда: термины и сокращения + +Короткий словарь для чтения документа. Каждый термин объяснён одной строкой — подробности в соответствующих разделах. + +| Термин | Что это простыми словами | +|---|---| +| JSON | Текстовый формат данных «поле: значение» — язык параметров системы (params.json, паспорта); правится блокнотом | +| YAML | «JSON для людей» — тот же смысл, но читается как список с отступами; у нас — язык инструкций по монтажу | +| spec.yaml / SDD | Спека задачи до геометрии: зачем деталь, с чем стыкуется, проверяемые условия приёмки, чего не делаем; CI строит из неё приёмочный прогон; основа сдельной оплаты (Spec-Driven Development) | +| Python / build123d | Язык программирования / библиотека, на которой написаны генераторы — код, строящий геометрию из параметров | +| Генератор (generator.py) | Скрипт, читающий params.json и паспорт и строящий деталь; читается как техпроцесс | +| Паспорт (vehicle/accessory passport) | JSON с точками крепления, углами и поверхностями машины или аксессуара — всё, что нужно знать о чужом объекте | +| Скелет / MasterParams | Каркас из точек и осей с главными параметрами (высота, углы) — мост между машиной и изделием | +| Git | Система контроля версий: полная история «кто, что, когда и зачем изменил»; основа всей работы | +| Gitea | Наш собственный веб-сервер для Git (аналог GitHub, но на нашем железе): репозитории, права, ревью, канбан | +| Репозиторий (repo) | Папка проекта под управлением Git со всей историей | +| Коммит (commit) | Одно сохранённое изменение с обязательной причиной («зачем») в сообщении | +| Дифф (diff) | Наглядная разница между двумя версиями файла: что добавлено, что убрано, строка за строкой; «читаемый дифф» = изменение видно глазами (height: 60 → 80), поэтому истина хранится текстом | +| Ветка (branch) | Параллельная линия работы для экспериментов; main — главная, защищённая | +| PR (Pull Request) | Запрос «принять мои изменения в main» — точка ревью и подписи; в main нельзя писать напрямую | +| Тег (git-tag) / Релиз | Именованная метка версии; релиз детали = подписанный тег, только из него идёт производство | +| LFS (Large File Storage) | Расширение Git для тяжёлых файлов: в истории — лёгкий указатель, тело — в хранилище рядом; для сканов, STEP, отчётов | +| zippey | Фильтр, позволяющий Git читать содержимое .FCStd (FreeCAD) и показывать осмысленные диффы | +| CI (Continuous Integration) | Серверный конвейер: по коммиту/тегу сам собирает геометрию, гоняет проверки и выкладывает артефакты | +| Артефакт | Файл, построенный CI из истины (STEP, GLB, PDF, BOM): не редактируется — пересобирается | +| Валидатор / DFM | Автопроверка «производимо ли»: правила гибки Blechcon, зазоры, упаковка, крепёж; красный = стоп | +| B-Rep / STEP-топология | Способ хранения точной геометрии: тело → оболочки → грани → рёбра → вершины; из него и извлекаются размеры при обмере | +| AAG (граф смежности граней) | Граф «грань → соседняя грань»: отсечение торцевых граней расщепляет деталь на верхнюю и нижнюю оболочки — основа автоматического распознавания гибов | +| Скелет детали / Medial Axis | Срединное представление без толщины: пластины + линии гиба + толщина одним параметром; результат обмера и основа параметризации | +| FTG (граф переходов гибки) | Свёртка AAG: гиб = ребро графа, фланцы = узлы; объединяет соосные гибы, разорванные разгрузками, в один технологический переход | +| BA / BD (припуск и вычет) | Формулы длины развёртки через K-фактор и нейтральную ось (ANSI/DIN) — считаются аналитически, без CAD | +| Kerf (ширина реза) | Металл, съедаемый лазером/плазмой; закладывается в раскрой, иначе деталь выходит меньше номинала | +| Разгрузка (Entlastungsschnitt / relief) | Вырез у края гиба, чтобы металл не рвался при гибке; в системе вычисляется правилом по t/R, а не копируется из оригинала | +| Пружинение (springback) | Металл после гибки частично распрямляется; у нержавейки заметно — угол на прессе задают с запасом | +| FEM / CalculiX / Gmsh | Расчёт прочности и собственных частот (модальный); Gmsh строит сетку, CalculiX считает — бесплатные | +| CFD / OpenFOAM | Расчёт обтекания воздухом (аэродинамика); тяжёлый, применяется разово вне конвейера | +| STEP / DXF | Обменные форматы точной геометрии: STEP — 3D для Blechcon и от OEM, DXF — плоские развёртки и сечения | +| STL / GLB | Сетки из треугольников без точных размеров: STL — печать прототипов, GLB — показ в конфигураторе | +| .FCStd | Файл FreeCAD; у нас двойной статус: скелеты руками = истина, просмотровые сборки от CI = артефакт | +| BOM (Bill of Materials) | Спецификация: из чего состоит изделие, массы, цены — генерируется CI | +| catalog.json | Контракт между CI и конфигуратором: список деталей со статусами, ссылками на GLB и правилами | +| SKU / Rev (ревизия) | Артикул детали / версия конструкции внутри артикула (Rev A, B…) | +| FFF (Form-Fit-Function) | Правило: посадка и функция не изменились → новая ревизия того же SKU; изменились → новый SKU | +| VDI / Kasm | FreeCAD в браузере: программа работает на нашем сервере, человеку летят только пиксели; файлы не покидают сервер | +| Контейнер / Docker | Изолированная «коробка» с одним приложением; VDI-сессия = одноразовый контейнер | +| WireGuard / VPN | Шифрованный туннель в нашу сеть; ключ = допуск, отзыв ключа = мгновенное отключение человека | +| 2FA / TOTP | Второй фактор входа: код из приложения на телефоне | +| Webhook | Автоматический сигнал одной системы другой («остаток = 0») — триггер для скриптов CI | +| LLM / Claude / MCP | Языковая модель как помощник; MCP — протокол, через который Claude управляет FreeCAD и инструментами | +| Промпт | Задание модели обычным языком («поднять платформу на 40 мм»); меняет параметры, не геометрию напрямую | +| GDPR / GeschGehG / StVZO / ISO 11154 | Регуляторика: защита данных ЕС / закон ФРГ о коммерческой тайне / правила допуска ТС / стандарт испытаний багажников | +| CM1 / CM2 | Маржа изделия / маржа с учётом стоимости разработки — уровни финансовой модели | +| CPQ | Configure-Price-Quote: инструмент дилера «собрал конфигурацию → цена → КП → заказ в производство» | +| OCCT / OpenCASCADE | открытое геометрическое ядро (B-Rep, булевы, скругления, STEP); то же ядро внутри FreeCAD; у нас вызывается напрямую | +| OCP | Python-обёртка OCCT из пакета cadquery-ocp; на ней написан recada.py; версия ядра 7.9.3 | +| recada.py | единый файл инструмента реверса: обмер, оболочки, кинематика, растяжение, операции, приёмка; CLI и браузерный интерфейс | +| headless | работа без графического экрана — на сервере, в CI, пакетно | +| Streamlit | библиотека браузерного интерфейса на Python; рабочее окно инструмента | +| three.js | библиотека 3D-графики в браузере; наш просмотрщик сборок | +| HLR (Hidden Line Removal) | модуль OCCT: проекции тела с видимыми и скрытыми линиями — штриховые виды для чертежей и инструкций, вектор | +| XCAF | слой OCCT для структуры сборки: имена деталей, дерево, положения, цвета в STEP | +| BREP (формат) | внутренний формат ядра; версия меняется с релизом; внутри процесса — да, на границах — только STEP | +| Три критерия приёмки | метрология (точность), целостность (свободные рёбра = 0, солид ≥ 1), распознавание порталом поставщика; далее: посадка по скану, входной контроль, полевой | +| Метрология (point-sampling) | расстояние от точек эталона до поверхности кандидата, двусторонняя выборка, порог 99.9 % в 0.05 мм | +| Свободное ребро | ребро с одной гранью — дыра в оболочке; их ноль = деталь замкнута | +| Виртуальный щуп | точка − нормаль × t попадает на поверхность → грань оболочечная; метод расщепления листа на две стороны | +| Оригами-кинемати ка | жёсткие пластины на шарнирах по осям гибов; поворот угла — поворот поддерева | +| K-фактор | положение нейтрального слоя в гибе (0.33–0.5); задаёт припуск BA; калибруется по ответам портала | +| Портал Blechcon | веб-портал поставщика резки и гибки: принимает STEP, считает развёртку; третий критерий приёмки | +| product.json / project.json | истина изделия и проекта: параметры и журнал операций; геометрия — артефакт из них | +| Aufbaurichtlinien | руководства производителей для строителей надстроек (VW, Ford BEMM, Stellantis): нагрузки, зоны, запреты, крепёж — первый источник ограничений | +| OEM-CAD / JT / CATIA | заводская геометрия кузова; форматы JT и CATIA конвертируются в STEP один раз (Inventor открывает JT) | +| FBX | формат визуальных моделей (Blender, Unity); машина как сетка с условным масштабом | +| ICP / Open3D / trimesh | регистрация скана в систему координат (ICP), библиотеки работы с сетками | +| WebRTC / стриминг вьюпорта | передача кадров в браузер; «наружу только пиксели» — геометрия не покидает сервер | +| WASM / OCP.wasm | ядро в браузере: OCCT и OCP, скомпилированные в WebAssembly; инструмент у клиента без сервера | +| Zoo / KCL | zoo.dev — CAD с кодом как истиной (язык KCL), облачный движок в США; ближайший по философии проект | +| Nutenstein / T-паз | сухарь в паз алюминиевого профиля; интерфейс крепления аксессуаров и кронштейнов | +| CAPA | Corrective and Preventive Action — цикл корректирующих действий ISO 9001; у нас: рекламация → правило валидатора | +| Струхаль (St) | безразмерное число частоты срыва вихрей; расчёт гула на поперечинах формулой без CFD | +| DIN 33402 / ISO 2768 | антропометрия (доступность) / общие допуски на чертежах | +| EX44 / Storage Box / restic | сервер Hetzner (i5-13500, 64 ГБ, FSN1) / хранилище для бэкапов (Хельсинки) / программа резервного копирования | +| Микроверсия | неизменяемая запись изменения со ссылкой на родителя (Onshape); у нас — операция журнала с хешем родителя | +| Определение / результат перестроения | что хранится (список операций) / что кэшируется и всегда перестраивается (B-Rep, сетка); истина — определение | +| Устойчивое изменение | операция адресует элемент по постоянному признаку, а не по позиции — применима к другой версии исходника | +| Переигрывание журнала | применение всех операций к новой ревизии исходной детали; Ranger-кронштейн v2 получает те же правки | +| Геометрический отпечаток | адресация грани или отверстия по типу, нормали, центру, площади — замена постоянных ID, которых в OCCT нет | +| Проблема топологического именования | номера граней меняются при перестроении; известная беда FreeCAD; у нас решается отпечатком для листовых деталей | +| Ветка / слияние журнала | варианты изделия как ветки списка операций; слияние без конфликтов — «источник побеждает» (правило Onshape) | +| Геометрический diff | сравнение двух версий: грани добавлены, удалены, сдвинуты — подсветкой в просмотрщике | +| FeatureScript | язык собственных операций Onshape; подтверждение, что «операция как код с параметрами» — верная абстракция | + + +## История ревизий + +| Рев. | Дата | Что добавлено | +|---|---|---| +| v25 | 31.08.2026 | цеховая практика, сборочные проверки, закрыты классы «физических» и «геймдев» идей | +| v26 | 03.09.2026 | 15а инструмент реверса recada.py; 15б редактор изделия; три критерия приёмки; три исправленные ошибки | +| v27 | 03.09.2026 | 15 переписан: роль FreeCAD и инженера, идея workbench; 15в библиотека и сервер; риски и дорожная карта по практике; Zoo | +| v28 | 03.09.2026 | как Zoo стримит; свой движок: ядро нет, обвязка да | +| v29 | 03.09.2026 | ядро напрямую без FreeCAD; создано с ИИ, проверено реальностью; просмотрщики на OCCT | +| v30 | 03.09.2026 | решение: каскадный вьювер на сервере, наружу только пиксели; достаточность EX44; защита данных | +| v31 | 03.09.2026 | версии ядра 7.8/7.9/8.0; STEP на границах; STEP с именами как требование | +| v32 | 03.09.2026 | 15г инструкция по монтажу как артефакт: разбор Defender 110, инструменты, четыре слоя | +| v33 | 03.09.2026 | 15д два мира: B-Rep и сетка; FBX, GLB из OCCT, Blender | +| v34 | 03.09.2026 | 15е данные: шесть источников, два каталога, единая структура, заводская система координат, четвёртый критерий | +| v35 | 03.09.2026 | опция: реверс из STL; входной контроль как пятый критерий | +| v36 | 04.09.2026 | 15ж конкуренты как источник данных; история ревизий | +| v37 | 04.09.2026 | 15з проверки уровня изделия: размещение, доступность, высота, настройка | +| v38 | 04.09.2026 | 15и дизайн продукта: что даёт система — покупатель, конструктор, эксплуатация, бренд | +| v39 | 04.09.2026 | 15к обратная связь из поля: рекламации, опыт покупателей, соцсети; шестой критерий | +| v40 | 04.09.2026 | Aufbaurichtlinien как первый источник ограничений; ссылки в правилах валидатора | +| v41 | 04.09.2026 | легенда дополнена терминами v26–v40 (26 понятий) | +| v42 | 04.09.2026 | 15л модель версий: уроки Onshape — микроверсии, устойчивые изменения, переигрывание, ветки без конфликтов; легенда | +| v43 | 06.09.2026 | материалы и технологии; учёт времени по журналу | +| v44 | 06.09.2026 | внешние ресурсы: OCCT 8.0.1, YouCanNotUnfold, китайские и российские ядра | +| v45 | 07.09.2026 | скорость: девять идей; удалённые столы; найм: три профиля; права и коммерческая тайна | +| v46 | 07.09.2026 | монетизация: экосистема аксессуаров; проектирование в системе на данных о машинах | +| v47 | 07.09.2026 | модель В: производители аксессуаров и самоделкины; классы ответственности | +| v48 | 07.09.2026 | модель В: дилеры и установщики — B2B у прилавка | +| v49 | 07.09.2026 | программа «Airholder Ready» | +| v50 | 09.09.2026 | поток вместо кодинга: методика на Ranger; четыре механизма против итераций | +| v51 | 10.09.2026 | управление проектом: доска по воротам вместо Ганта | +| v52 | 10.09.2026 | датасеты CAD и агенты: обзор, лицензии, MM-CAD; свой датасет | +| v53 | 10.09.2026 | гибка в датасетах: BenDFM, KUL-bend; исправление | +| v54 | 10.09.2026 | пересборка в единый документ со сквозной нумерацией 1–44; ссылки обновлены | +| v55 | 10.09.2026 | раздел 42 подробно: BenDFM по файлам, CADFS по ключевым словам, ABC щупом, пружинение, TRUMPF, ESICUP; сводный план | +| v56 | 10.09.2026 | раздел 42: инструмент прессов и таблицы гибки — WILA/Rolleri в оригами-кинематику, базы Bendguide/LVD через Blechcon, экспорт из CAM, шесть шагов | +| v57 | 10.09.2026 | раздел 42: NUMISHEET 2025 с картой 1.4301, бенчмарки пружинения, FEM-корпуса (не наше — зафиксировано), first-part-right, полный реестр внешних источников | +| v58 | 10.09.2026 | раздел 42: SMCAD, HeteroMF, DOI для скачивания, ESAFORM, трубы (не наше), четыре кластера; приложение «Источники» — 63 ссылки | +| v59 | 11.09.2026 | раздел 18: как учится система — три механизма, правило и случай, внешние ресурсы как полигон, портал Blechcon как оракул цены, что знать об оборудовании | + + +--- + +## 1. Идея и исходная ситуация + +RECADA (Аугсбург) производит автомобильные багажники Airholder: платформа из алюминиевых экструдированных профилей (профили на складе, сечения нарисованы один раз) + гнутые ноги из нержавеющей стали 1.4301 (2–3 мм); резка и гибка — аутсорс (Blechcon.de). Узкое место — ноги под каждую машину и аксессуары. Цель: система, где на входе крыша (STL-скан с точками крепления или STEP от OEM), на выходе — производственные файлы, обновлённый конфигуратор и инструкции; дизайн управляется по промпту; работа командная с версионированием; знания накапливаются; исполнители — удалённые инженеры из разных стран, отбираемые по результатам тестовых работ. + + +--- + +## 2. Архитектурный принцип и слои + +Источник истины — параметры и код, а не геометрия. Файлы FreeCAD/STEP — артефакты сборки. Это даёт промпт-управление (LLM меняет числа в JSON, не двигает грани), читаемые Git-диффы, командную работу без конфликтов бинарников, автоматическую пересборку производных. + +| Слой | Содержание | +|---|---| +| 1. Паспорт автомобиля | JSON из STL/STEP (один раз): точки крепления с нормалями, углы, межосевые; для деталей, повторяющих крышу (windshield), — опорные поверхности как сплайн-аппроксимация (не сырая сетка); поле точности geometry_source. Сырой скан — в архив, роль: контрольный эталон для проверки зазоров. | +| 2. Скелет + MasterParams | Точки, оси, плоскости, главные параметры (H, alpha). Всё «твёрдое» читает размеры отсюда. Скелет — мост между паспортом машины и паспортом изделия. Железное правило скелетного метода: детали никогда не ссылаются друг на друга — только на скелет (иначе связи рвутся непредсказуемо); направление зависимостей строго сверху вниз: MasterParams → Skeleton → детали. | +| 3. Каталог профилей | Сечения один раз (импорт DXF); параметр — длина резки; «ширина от длины» — правило. | +| 4. Генераторы (build123d) | Код строит детали от скелета; материал, R3, покрытие зафиксированы; библиотека «кубиков» (bent_flange, hole_array, bolt_joint) прячет сложность. | +| 5. Валидатор DFM+ | Правила Blechcon (отгибы, радиусы, развёртка, коллизии) + зазор до крыши ≥20 мм (статический запас вместо расчёта деформации кузова) + упаковка по лимитам канала доставки (текущий канал GLS: ≤40 кг/коробка — главный лимит, масса считается по коробкам комплекта с раскладкой в BOM; длина до ~2 м практически, пороги надбавок — из тарифа GLS в rules/) + крепёж (таблица болт/гайка, проверка зацепления). Красный — артефакты не собираются. Физическая калибровка правил гибки (FEA-in-the-loop): CalculiX симулирует сам процесс гиба и показывает, при каком радиусе напряжение на внешнем волокне грозит микротрещинами у 1.4301; расчёт делается один раз для сетки «толщина × радиус × марка» → таблица допустимых R в dfm_blechcon.json → валидатор смотрит в неё мгновенно. Так справочное «R ≥ 1×t» заменяется на подтверждённое расчётом и практикой поставщика. Проверка против реальной оснастки цеха (CAM-in-the-Loop в практичной форме): у Blechcon запрашивается список доступных пуансонов и матриц (радиусы, ширина V-паза, максимальная длина гиба) и заводится таблицей в dfm_blechcon.json — валидатор отвечает «R2.5 нет, ближайший R3, пересчитываю K-фактор» и проверяет минимальную длину фланца, чтобы деталь не проваливалась в матрицу, ещё до отправки заказа. Генерация G-кода для их пресса не входит в систему — станок программирует поставщик. | +| 6. CI-сборка | STEP/DXF → Blechcon (перед заказом автоматический раскрой: SheetNest / nest2d / CAD-N, для больших партий u-nesting на Rust с Python-интерфейсом — отходы, проверка счёта, учёт ширины реза kerf из dfm_blechcon.json); GLB → конфигуратор; студийные рендеры (Blender headless) → Shopify; PDF/HTML → инструкции; BOM → цены и склад. | + +recada-cad/ ├── vehicles/ паспорта машин ├── products/ конфигурации (params.json) ├── catalog/ профили, крепёж ├── accessories/ аксессуары и адаптеры ├── rules/ DFM, правила └── ci/ пайплайны сборки ├── generators/ build123d + кубики Версионирование: Gitea (self-hosted) + Git LFS; .FCStd — через zippey (читаемые диффы). Релиз = git-тег с персональной подписью ревьюера. Новая машина = паспорт + params.json — механизм масштабирования. + + +--- + +## 3. Файлы и форматы: как устроена истина + +Никакого собственного формата: истина детали = два обычных файла. params.json — числа и слова (SKU, машина, материал, высота, отверстия), правится блокнотом/промптом. generator.py — Python/build123d, читается как техпроцесс: «лист 120×340×3 → скруглить углы → отверстия по точкам паспорта → фланец 90° R3 → нижний гиб под угол крыши из паспорта». Числа приходят из трёх источников (params / паспорт / каталог материалов), в коде их почти нет. FCStd — не источник, а результат (как .exe при исходниках): CI собирает его для просмотра. Исключение: скелеты и эталоны реверса, построенные руками, — полноценные FCStd-исходники в Git. + +Дерево построения не исчезло — оно переехало в текст: порядок строк = порядок операций, но вместо хрупких ссылок на номера граней (Topological Naming Problem) — устойчивые правила (edge="top", точки по именам) и порождающие конструкции (циклы по точкам паспорта, условия). Плюс история с «почему»: каждый коммит несёт причину/промпт («поднять платформу 40 мм — бокс задевает антенну»), чего нет ни в одном GUI-дереве; это же — QMS-след и обучающие данные системы. + + +### Сборки: тот же принцип уровнем выше + +Сборочный JSON описывает состав и привязки, но не координаты: детали публикуют именованные интерфейсы («sole», «head» с плоскостью, отверстиями, нормалью), сборка говорит «посадить sole на mount_front_L». Вместо решателя констрейнов (Inventor) — детерминированное вычисление позиций из интерфейсов: ничего не «желтеет» при изменениях. Крепёж — свойство соединения: bolt_joint() сам ставит болт по оси, считает длину из пакета, кормит BOM. Промпт «поднять платформу на 40 мм» меняет один параметр скелета — всё пересобирается. Иерархия: деталь → сборка изделия → конфигурация клиента, один формат и конвейер. Цена подхода: нет свободной кинематики (шарниры — позой-параметром или в песочнице). + + +### Роли форматов и «труба» + +JSON+Python (истина) → STEP/FCStd (точная геометрия) → DXF (развёртка) → STL/GLB (сетки) Вниз — всегда, автоматически. Вверх — только реверс-инжиниринг (дорого, один раз, на входе). + +STEP — «PDF мира CAD»: точный, но мёртвый; выход к Blechcon и вход от OEM. STL/GLB — сетки без размеров: печать и показ, никогда для производства и измерений. У каждого производного файла есть родитель-истина (SKU+ревизия в имени); файл-сирота — нарушение процесса. При конфликте прав всегда верхний по трубе, нижние пересобираются. Разнородные исходники (скан машины + чужой STEP багажника) никогда не стыкуются напрямую — каждый переводится в паспорт-JSON, стыковка на языке паспортов; ноги — материализованный мост между двумя паспортами. + + +### FFF-правило (Form-Fit-Function) + +Посадка и функция не изменились → новая ревизия того же артикула (взаимозаменяемо, склад смешивает). Изменились → новый SKU с политикой замены. Защищает от зоопарка артикулов и от тихой несовместимости. Исполняется машиной: валидатор сравнивает именованные интерфейсы между ревизиями; «функция сохранена?» — единственный пункт, остающийся подписи человека. + + +### Справочник типов файлов: что лежит в Git и как с этим работать + +| Тип файла | Что это | В Git? | Кто и как правит | +|---|---|---|---| +| params.json | Параметры детали/изделия: SKU, машина, материал, размеры, статусы; рядом — spec.yaml: намерение, интерфейсы, ограничения приёмки, non-goals (контракт задачи) | Да, обычный текст — главный источник истины | Инженер (редактор/веб-форма), LLM по промпту; каждое изменение — коммит с причиной; в main только через PR | +| generator.py и recada_lib | Python/build123d: код построения геометрии; библиотека кубиков (bent_flange, hole_array, bolt_joint) | Да, обычный текст — источник истины | Senior/владелец (+LLM с ревью); пробы в ветке convert/*, squash-merge при приёмке | +| Паспорта: vehicle_*.json, accessory_*.jso n | Точки крепления с нормалями, углы, опорные поверхности (сплайны), интерфейсы, масса/ЦТ, geometry_source с точностью | Да — источник истины | Создаются один раз при оцифровке (человек или ИИ-агент, подпись человека обязательна); уточняются при пилотных установках | +| Сборочные JSON | Состав и привязки по именованным интерфейсам (без координат) | Да — источник истины | Инженер/LLM; те же PR-правила | +| rules/: DFM, материалы, тарифы | Правила Blechcon, materials.json (1.4301, R3), лимиты GLS, таблица крепежа, пороги FEM | Да — источник истины | Владелец/lead; пополняются из рекламаций и ревью технолога Blechcon (цикл CAPA) | +| YAML инструкций | Шаги монтажа: деталь, крепёж, момент затяжки | Да | Инженер; CI рендерит PDF/HTML/Unity-сцены сам | +| .FCStd (FreeCAD) | Два разных случая! (а) Скелеты и эталоны реверса — построены руками; (б) просмотровые сборки — построены CI | (а) Да, через zippey (читаемые диффы) + LFS; (б) НЕТ — артефакт | (а) Владелец/senior в FreeCAD; (б) не редактируется никогда — пересобирается | +| STEP / DXF | Точная «мёртвая» геометрия: обмен с Blechcon и приём от OEM/исполнителей | Входящие — архив (LFS) как сырьё; исходящие — НЕТ, артефакты CI с SKU+ревизией в имени | Не редактируются; исходящие уходят Blechcon только из CI по тегу; входящие — через извлечение в паспорт | +| STL / GLB | Сетки без размеров: печать прототипов (STL с ID получателя), конфигуратор (GLB огрублённый) | НЕТ — артефакты CI | Не редактируются; для производства и измерений непригодны принципиально | +| Сканы крыш (STL), STEP от OEM | Сырьё оцифровки | Архив в LFS (один раз) | Не редактируются; роль — контрольный эталон зазоров и источник паспорта | +| BOM, catalog.json, рендеры, PDF- инструкции | Производные для склада, конфигуратора, клиентов | НЕТ — артефакты CI (хранилище артефактов; релизные — вечно) | Не редактируются; изменения — только через правку истины и пересборку | +| Отчёты испытаний и FEM | Протоколы физических испытаний, эталонные расчёты (статика, модальный) | Да (LFS) — прикрепляются к релизному тегу | Только добавляются; связь «ревизия ↔ протокол» = QMS-след | + +Три железных правила для всей команды: (1) редактируется только истина (JSON, .py, паспорта, rules, YAML, скелеты) — всё остальное пересобирается, файл-артефакт руками не трогается никогда, даже «быстро поправить»; (2) у файла без SKU и ревизии в имени нет права покидать сервер; (3) при расхождении версий прав всегда тот файл, что выше по трубе истины — нижние пересобираются, а не чинятся. Нарушение правила 1 (ручной STEP в производство мимо params) — главный риск смерти системы, поэтому отправка на Blechcon технически возможна только из CI-артефактов релизного тега. + + +--- + +## 4. Как проектирует человек + +Принцип «шаблон, не копия»: дерево/генератор строится из конструктивного замысла (база → полки → гибы → крепёж, каждый размер осмыслен и привязан к параметру), а не как копия всех артефактов конкретной штампованной детали — копия хрупка и ломается при больших изменениях, шаблон держит. Путь 1 — новый тип детали: творческий поиск руками в песочнице FreeCAD (мышкой, как привык) → готовая конструкция становится измеримым эталоном (объём, масса, точки поверхности) → перевод в код по шагам (контур → сверка → гибы → сверка → отверстия → сверка), каждая попытка автоматически сверяется с эталоном; пробы живут в отдельной ветке и сплющиваются в один коммит при приёмке; 3 неудачи LLM подряд → эскалация человеку. Путь 2 — повседневный: правка чисел в params (редактор/форма/промпт) → пересборка → просмотр; CAD превращается в место просмотра. Путь 3 — LLM-переводчик: человек описывает словами/эскизом, Claude пишет build123d по образцам библиотеки, CI собирает, метрология возвращает расхождение, LLM чинит. + + +### Классы деталей по стоимости перевода + +| Класс | Перевод в систему | Трудозатраты | +|---|---|---| +| Профили (алюминий, прокат) | Импорт DXF-сечения + extrude; параметр — длина | час на каталог | +| Гнутый лист (ноги, адаптеры) | Реверс техпроцесса в код; быстрее с библиотекой кубиков | часы–дни | +| Литые/скульптурны е | НЕ переводить: паспорт интерфейсов (20 мин) + STEP как мёртвый референс; офсеты от опорных поверхностей при необходимости; полный реверс — только если планируется семейство размеров | минуты | +| Покупное (крепёж) | Библиотеки стандартных деталей build123d | 0 | + +Правило границы внутри детали: форма — руками (один раз), интерфейсы и адаптация — кодом. Люди в системе остаются для четырёх ролей, где машина слаба: постановка задачи (знание реального мира), топология («как вообще решить»), суждение («валидно, но глупо» — подпись ревьюера), обучение системы (опыт → правила). Час «обвести контур» система обнуляет; час «понять, как обойти рейлинг» становится вечным активом. + +Вход открыт для любого CAD: исполнитель рисует в SolidWorks/Inventor/Catia/FreeCAD — сдаёт STEP + чертёж с размерами; перевод в код не зависит от исходного CAD. Требовать build123d от конкурсантов = сузить пул вдесятеро. Лицензии: проверить чужую лицензию невозможно — декларация в договоре переносит ответственность, а VDI-сессия снимает проблему полностью (легальное рабочее место без единой лицензии у исполнителя) и очищает цепочку поставок для ISO/GeschGehG. Субподряд безопасен по построению: оплата за деталь, прошедшую валидатор; аккаунт персонален (сессии из двух стран видны в логах); на лестнице доверия субподрядчик не растёт. + + +### Инженерный задел: что уже проверено практикой + +Реверс-инжиниринг с метрологической приёмкой (point-sampling по контрольным точкам): Bracket_L — 100% совпадение; Bracket_R — зеркало, 99.12%; wind_side_v2 — 100.000% по 88 точкам; Part55 → новый Bracket_88_W30 с полным DFM-контролем; узел Defender 110 (LRDE-2BR1) — 99.9994%, SheetMetal-дерево от скелета. Материал везде 1.4301 2B, t=3.0, K-фактор из таблицы. Критический урок сессий: установка networkx включила SheetMetal Unfolder V2, который показал истинные углы гиба 99.389° и 135° вместо ошибочно измеренных V1 80.6° и 45° (дополнительные углы) — ошибка с прямыми производственными последствиями; правило: развёртки только через V2. Словарь конструкции зафиксирован: полка (Flansch) — боковые грани с отверстиями, стенка (Steg) — центральная грань с логотипом. + +Выученные технические правила (в skills): SMBendWall падает на прерванных гибах → Revolution или реконструкция из face-призм; extend задавать до навешивания дочерних стенок (иначе TNP рвёт привязки); упавший конструктор оставляет null-объект — удалять немедленно; boolean cut ненадёжен для верификации — только point-sampling через distToShape; перед поиском кромок — полный touch()+recompute (кэш SheetMetal не сбрасывается сам); привязки только через SubShapeBinder (защита от TNP); для деталей с формовками — двухконфигурационное дерево (плоская модель для развёртки + финальная с формовкой). Модули к установке: SheetMetal 0.8.22 (Solid-to-SheetMetal, folding marks для пресса Blechcon, индикация битых ссылок), Defeaturing WB (срезать фаски в зоне гиба → развернуть → вернуть вырезы), Curves WB (чистка B-Spline-граней из STEP Inventor), networkx. Материалы — Material Definition Sheet (таблица R/T → K-фактор вместо жёсткого K=0.42): одна таблица питает и Claude, и параметрические модели. Unattended Unfold — headless-развёртка для CI. Точка входа репозитория — CLAUDE.md: описание изделия, материалы по умолчанию, какие skills когда применять. + + +### Валидатор с автопочинкой: следующая ступень DFM + +Правила оцифровываются в три слоя: числа → dfm_blechcon.json (R=1×t, мин. полка 15 мм для нержавейки, отступ отверстия от гиба, Entlastungsnut, зазоры ≥ t, лимиты листа, GLS-габариты); исправления → features.py (генераторы разгрузок: овал, Entlastungsnut, угловая разгрузка); методики → skills (markdown: «как думать»). Валидатор не только говорит «ошибка: отверстие близко к гибу», но предлагает исправление: «конфликт → предлагаю relief_obround, вот превью» — в RECADA-workbench это красная подсветка + кнопка «исправить», в CI тот же код headless блокирует выпуск неисправленного. Калибровка библиотеки — по реальному производству: принятые Blechcon детали с их правками разгрузок обмеряются (простой реверс), формулы «размер разгрузки от t/R/d» зашиваются в features.py; каждое письмо-исправление от Blechcon — строка в dfm_blechcon.json, и ошибка не повторяется ни у кого. Дизайн по правилам перестаёт зависеть от того, помнит ли инженер PDF поставщика. + + +### Реверс без GUI: STEP → Python-код (метод и стек) + +Дерево построения как таковое не нужно — нужен Python-код: он хранит ту же последовательность операций, но без хрупких ссылок на грани (TNP), с циклами и условиями, диффами и историей «зачем». Просмотровое дерево CI может собрать для глаз, но истина — код. Вся труба реверса работает headless (build123d/OCP — то же ядро OpenCASCADE, что у FreeCAD): без GUI, без MCP-сессий, повторяемо и пригодно для CI (десять STEP за ночь). + +Алгоритм автоматического обмера (step_probe.py): обход B-Rep через TopExp_Explorer → классификация граней (плоские = полки/стенки, цилиндрические = гибы, узкие торцевые = толщина) → построение AAG (атрибутированный граф смежности граней, networkx): узлы — грани, рёбра — сопряжения → отсечение торцевых граней расщепляет граф на две изолированные эквидистантные оболочки (верх/низ), их сопоставление даёт нейтральную поверхность и толщину t → извлечение параметров гибов (радиус, угол, ось; numpy: нормали, PCA осей), отверстий (цилиндры с осями и D), внешнего контура. Выход — measurements.json. + + +### Скелет детали — главный результат обмера и основа параметризации + +Ключевое решение: обмерщик выдаёт не «список размеров», а СКЕЛЕТ детали — срединное представление без толщины: пластины (плоскости фланцев с контурами) + линии гиба (оси, углы, радиусы) + толщина как ОДИН параметр (t = 2× расстояние от срединной поверхности до границы). Реверс превращается из «восстановления формы» в «восстановление скелета», и это даёт три эффекта. (1) Толщина перестаёт быть геометрией: смена 3 мм → 2 мм — правка одного числа, а не перестроение всех поверхностей (актуально: применяются обе толщины). (2) Скелет детали и общий Skeleton сборки говорят на одном языке — плоскости и линии, поэтому связка «изменил H платформы → пересчитались углы фланцев ноги» становится геометрически прямой: линия гиба лежит на пересечении плоскостей, заданных скелетом сборки. (3) Параметризация осмысляется почти сама: длины пластин, углы между ними и положения линий гиба — ровно те величины, что меняются от машины к машине, а артефакты производства (керн-метки, случайные скругления) в скелет не попадают вовсе — схлопываются вместе с толщиной. Medial Axis фильтрует шум за инженера. + +Реализация — без «настоящего» Medial Axis Transform (в OCC штатной функции нет, устойчивое построение срединных поверхностей на реальных B-Rep нетривиально): скелет строится из уже существующего AAG — пары эквидистантных граней дают срединные плоскости, цилиндры гибов дают срединные оси и радиусы. Результат идентичен, а код — тот, что и так спроектирован. Скелет детали сохраняется в measurements.json как основная структура; дальше на него навешиваются параметры (spec.yaml) и правила (rules/). + + +### Кривизна как устойчивый детектор гибов и формовок + +Второй, независимый от описания поверхности критерий: для каждой грани считаются главные кривизны и производные величины — плоскость даёт гауссову K=0 и среднюю H=0; цилиндрический гиб — K=0 при H≠0, причём направление нулевой главной кривизны и есть ось гиба, а вторая кривизна даёт 1/R; формовки и выштамповки — K≠0 (неразвёртываемые участки). Практическая ценность прямая: критерий работает независимо от того, как STEP описал грань — при экспорте из Inventor плоскости и цилиндры часто приходят B-spline-аппроксимациями, где формального «цилиндра» нет и поиск по типу поверхности проваливается, а кривизна есть у любой поверхности. Бонус: K≠0 сразу помечает деталь как содержащую формовку, то есть требующую двухконфигурационного подхода, — ещё до попытки развернуть. Порядок в обмерщике: сначала опрос типа поверхности через OCC, при неудаче — замер кривизны. + +Автопроверка корректности развёртки (Гаусс-Бонне): для правильной плоской развёртки сумма угловых дефектов по контурам даёт 2π; отклонение означает топологически неверную развёртку — складки или самонакладки. Одна формула в приёмке развёртки: дешёвая страховка, ловящая грубые провалы анфолдера автоматически, без человеческого взгляда на DXF (метрологию не заменяет). + +Что автоматика НЕ делает: выбор, какое число — параметр (высота, меняется от машины), какое — правило (R3 всегда), какое — формула (ширина от длины), а какое — артефакт производства, который не воспроизводится. Это конструктивный замысел; его расставляет человек или Claude со skills, читая measurements.json как текст. Далее — spec.yaml + generator.py + params.json. + + +### Детектор разгрузочных вырезов (Entlastungsschnitte / bend reliefs) + +Разгрузки предотвращают разрыв и смятие металла при гибке, но усложняют топологию — именно они вызывают «прерванные гибы», на которых падал SMBendWall и путался Unfolder V1. Распознавание в три шага, встраивается в тот же обмерщик: (1) дешёвая эвристика по контурам и рёбрам грани гиба — канонический гиб имеет 1 контур и 4 ребра; больше одного контура = сквозной вырез внутри гиба, больше четырёх рёбер = вырезы по краям; (2) паттерн в AAG — идеальный гиб смежен ровно с двумя плоскими фланцами, разгрузка добавляет ему в соседи мелкие торцевые грани или разрывает цилиндр на несколько соосных (поиск подграфа в уже построенном графе, ничего нового не требуется); (3) UV-проекция границ гиба на цилиндр — отклонение от прямоугольника даёт точные размеры выреза (глубина, ширина, отступ); применяется только к найденным случаям. + +Ключевое отличие от «просто копирования»: в системе разгрузка — это ПРАВИЛО, а не форма из оригинала. Генератор вычисляет её по толщине, радиусу и типу узла (features.py), а не переносит контур. Поэтому у детектора две роли: сигнал «этот узел технологически требует разгрузки» (иначе валидатор выдаст конфликт и предложит relief_obround) и источник калибровки — обмер разгрузок на деталях, принятых Blechcon, даёт реальную зависимость «размер от t/R/d», которая зашивается в features.py навсегда. Третий выигрыш практический: знание «на этом гибе прерывание из-за разгрузки» позволяет автоматически выбрать проверенный обход развёртки (Revolution вместо SMBendWall либо Defeaturing: срезать вырезы → развернуть чистый гиб → вернуть вырезы плоскими) — то, что сегодня решается руками в каждой сессии, становится ветвью в коде. Инструменты те же: OCP (TopExp_Explorer, BRepAdaptor_Surface) + networkx; отдельный pythonocc-core не нужен — то же ядро уже доступно через OCP. + + +### Гибрид «точные числа + зрение» и агентная петля коррекции + +Ключевое дополнение к обмерщику: геометрия и семантика извлекаются разными средствами и соединяются. OCC/AAG/FTG дают точные числа (толщина, радиусы, углы, координаты отверстий) — в этом машина безупречна. Рендер детали в 4 ракурсах (проекции + изометрия из build123d) плюс мультимодальная модель дают семантику ролей: «это разгрузка у основания гиба», «это фальц для жёсткости края», «это П-образный кронштейн», «это артефакт штампа, воспроизводить не нужно». Именно семантики не хватало: AAG измеряет, но не знает, что параметр, что правило, а что артефакт. Работает сегодня без обучения моделей и без датасетов — рендер, взгляд, разметка measurements.json, генерация. + +Агентная петля автокоррекции (штатный режим реверса): экстрактор → LLM пишет generator.py → CI собирает STEP → сверка с исходным → расхождения возвращаются модели числами («вырез смещён на 2 мм, строка 45») → правка → повтор до сходимости. Стоп-правило прежнее: 3 неудачные итерации подряд → эскалация человеку с дневником попыток (иначе модель крутится вечно). Судья — только числовая метрология point-sampling ≥ 99.9% и точные интерфейсы; визуальный дифф (наложение рендеров оригинала и сборки с подсветкой расхождений) используется как навигация по ошибке и объяснение человеку, но НЕ как критерий приёмки: деталь может «выглядеть одинаково» при расхождении в полмиллиметра на посадочном месте. + +Эволюционный подбор параметров — для тех 10–20% случаев, где распознавание сдаётся (свободные радиусы, скульптурные переходы): шаблон пишет человек, числа подбирает оптимизатор (scipy / CMA-ES, без ML), минимизируя ту же метрологическую невязку point-sampling. Дёшево, детерминированно, встраивается в CI как ночной прогон. + + +### Оригами-кинематика и модальная сигнатура + +Кинематическая развёртка («жёсткое оригами»): деталь представляется как жёсткие плоские пластины (фланцы) и одномерные шарниры по осям гибов; последовательные повороты вокруг осей до нулевых углов дают плоскую заготовку. Один алгоритм даёт два выхода сразу: траектория решателя (последовательность поворотов) — это и есть порядок гибки, а конечное состояние — заготовка. Метод закрывает сразу две задачи: развёртку без анфолдера (углы шарниров = углы гибов, конечное состояние = контур заготовки) и автоматическую проверку самопересечений при складывании — ту самую Bending Feasibility, которую иначе считает регрессивная развёртка. Реализация — чистая геометрия на numpy (последовательные повороты + тест пересечений); физические движки (PyBullet/MuJoCo) для жёсткого оригами избыточны. + +Модальная сигнатура как финальный валидатор реверса: для оригинала и для реверснутой модели считаются первые ~5 собственных частот (CalculiX). Совпадение спектров доказывает механическую эквивалентность, а не только геометрическую: частоты чувствительны к толщине, радиусам в зонах гиба и распределению массы — то, что точечная метрология может проглядеть. Применяется к ответственным деталям (ноги, несущие кронштейны), необязательно для мелочи; инфраструктура FEM всё равно строится под валидатор, так что чек почти бесплатный. Красивое замыкание: тот же модальный анализ, что уже выполнен компанией, служит и приёмкой реверса, и правилом «f1 ≥ порога по Струхалю» против гула на скорости. + + +### Цеховая практика в коде: базы, прокат, допуски, следы инструмента + +Базирование от технологических баз (datums), а не от нуля CAD: деталь описывается от тех плоскостей, от которых её меряет ОТК (базы на поверочной плите, щуп КИМ) и от которых её позиционирует оператор у заднего упора листогиба. Тогда переменные генератора называются STOP_DISTANCE_X1, а не offset_x, размеры в коде совпадают с контролируемыми на производстве, и цепочка размеров идёт от базы — меньше накопленной ошибки. В spec.yaml появляется секция datums (базовые плоскости и точки установки). + +Направление проката: гиб вдоль волокна даёт микротрещины на внешнем радиусе у нержавейки — при радиусах, близких к минимальным, риск реальный. Правило связывает конструкцию и раскрой: если ось гиба параллельна направлению проката (оно задаётся ориентацией детали на листе при нестинге), валидатор либо требует увеличить минимальный радиус, либо переориентировать деталь в раскройной карте — и записывает предупреждение в комментарий генератора. + +Толщина как распределение, а не число: реальный лист «3 мм» бывает 2.93. Обмерщик замеряет толщину в N точках и выдаёт номинал плюс разброс; в генератор идёт номинал, в спеку — допуск с assert-проверкой, K-фактор считается от фактической толщины. Обмерщик перестаёт спотыкаться на «плавающей» толщине. + +Следы матрицы как источник технологии: при воздушной гибке на внешнем радиусе остаются две параллельные линии сопряжения цилиндра и плоскости — расстояние между ними выдаёт ширину V-канала, на котором деталь гнули. По чужой детали восстанавливается не только геометрия, но и технология, а радиус сразу привязывается к стандартному ряду матриц (V = 6, 8, 10, 12, 16 мм) — прямое дополнение к таблице оснастки поставщика. Проверка коллизий уточняется: не «согнётся ли вообще», а «согнётся ли на этом прессе в этом порядке» — деталь не должна задевать верхнюю траверсу на 2-м и 3-м гибе (габаритные модели инструмента запрашиваются у Blechcon; фирменные каталоги Amada/Trumpf не нужны). В задел без приоритета: распознавание замков «шип-паз» и зазоров под сварку — актуально, если появятся сварные узлы (сейчас соединения болтовые). + +Рассмотрено и отклонено (чтобы не возвращаться по кругу): GNN-распознавание граней (BRepNet/CADNet/Sheet-metalNet) — нет открытых моделей под листовой металл, нужен размеченный корпус в тысячи деталей, точность вероятностная; ваш класс деталей распознаётся правилами детерминированно. ML-предиктор пружинения на FEA-датасетах (ANSYS/LS-DYNA — платные) — решает задачу, которую уже решает гибщик на своём прессе; наш путь — строка в dfm_blechcon.json из практики Blechcon. Специализированная VLM «картинка → код» (CAD-Coder) — research-модели слабее связки «точные числа OCC + рассуждение сильной модели». EvoCAD (популяция вариантов с VLM-жюри) — дорого по токенам и оптимизирует визуальное сходство вместо размерной точности. Gaussian Splatting с телефона — точность на бликующем металле далека от требований к интерфейсам; практичная версия (фотограмметрия оболочки + штангенциркуль по крепежу) уже принята; вернуться при оцифровке чужих аксессуаров «в поле». Analysis Situs, pythonocc-core отдельно, CadQuery — дублируют то, что уже в стеке. Инверсный CSG («воздушный слепок»: вычесть деталь из габаритного параллелепипеда и разбирать отрицательный объём на примитивы) — опирается на булевы операции, которые по нашему же выученному уроку на сложных телах возвращают бессмыслицу; радиусы и отверстия и так берутся прямо с цилиндрических граней. Синтетический лазерный скан STEP + нейросети облака точек (CAD-Recode / Point2CAD) — осознанная потеря точности ради устойчивости к дефектам STEP, которые лечатся healing-ом в две строки OCC. Дифференцируемый CAD (SDF + градиентный спуск) — технически рабочий, но решает ту же задачу, что принятый оптимизатор CMA-ES, требуя дифференцируемого рендера и подбора гиперпараметров; вернуться, если CMA-ES упрётся по скорости. Синтез программ через SMT-решатели (SyGuS/Z3) — «доказанная корректность» недостижима: решатели работают с логикой и арифметикой, а не с B-Rep; наша гарантия — измеренная (метрология + spec-прогон), а не доказанная. Латентная диффузия CAD-деревьев (GenCAD-3D) и спектр Лапласа-Бельтрами — обучение на миллионах программ / математика для поиска похожих форм; плоскости и цилиндры распознаются напрямую. PINN для динамического K-фактора — уравнения пластичности ради числа, которое берётся из Material Definition Sheet и калибруется по принятым Blechcon деталям. RL-агент для развёртки (обучение подбору последовательности разгибания) — задача уже решена детерминированно дважды: регрессивная развёртка и оригами-кинематика дают порядок гибов и проверку самопересечений за секунды и воспроизводимо. Нейро-символический DSL с AST-валидацией через Z3 — тревога «LLM ошибается синтаксически» снята дешевле: код исполняется в CI (синтаксис падает мгновенно и возвращается модели), семантику ловят валидатор и метрология. INR/SIREN (нейросетевое поле заполненности) — потеря точности ради градиентов там, где точные плоскости лежат прямо в B-Rep. Квантово-вдохновлённый генетический алгоритм — обёртка над обычным эволюционным поиском; принятый CMA-ES делает ту же работу с проверяемой сходимостью. Персистентные гомологии (TDA) — из идеи взята только суть (инвариант целостности) в виде чек-листа сохранности элементов, без алгебраического аппарата. Оптические каустики (трассировка лучей для замера радиуса), голономия/параллельный перенос (двугранный угол получается скалярным произведением нормалей в одну строку), тепловой поток по уравнению Фурье для поиска вырезов (перечисление внутренних контуров дешевле на два порядка) — экзотика ради экзотики. Карта мартенситного превращения (γ→α′ при холодной деформации) — физика верная, но для расчёта нужна симуляция деформации, для которой уже надо знать, где гибы: круговая логика; реальная ценность темы у нас другая — наклёп в зонах гиба влияет на коррозионную стойкость и магнитность готовой детали, это вопрос качества продукта, а не реверса. Класс «фундаментально-физических» методов рассмотрен целиком и отклонён (фаза Берри, поток Риччи/Ямабе, калибровочная теория дефектов, полиномы узлов, обратная задача рассеяния, голографический принцип, симплектическая механика, топологические краевые состояния, функционал Онсагера, сложность Колмогорова): все они решают три задачи — углы и радиусы гибов, развёртка, поиск вырезов — которые уже закрыты кривизной, оригами-кинематикой и счётом контуров с машинной точностью за секунды. Точность системы сегодня упирается не в математику распознавания, а в физику и данные: K-фактор реального листа, компенсация пружинения на конкретном прессе, допуски исходного STEP, точность скана крыши — их эти методы не уменьшают. Читаемость кода (циклы вместо повторов) достигается инструкцией в промпте и ревью, а не вариационной задачей. Класс «геймдев-математических» подходов также рассмотрен и отклонён: IK-риг совпадает с уже принятой оригами-кинематикой (приятное подтверждение естественности решения); SDF-реймаршинг, поток Риччи, shape grammars и Wave Function Collapse, MCTS-поиск кода, воксельные октодеревья, клеточные автоматы — это ранее отклонённые классы в новой упаковке (дифференцируемый CAD, нейро-символический DSL, потеря точности ради устойчивости); краудсорсинговый Foldit требует аудитории игроков, которой нет. Из теории Морса взято только следствие, и так вытекающее из скелетного подхода: строить деталь в коде от базовой плоскости к периферии, а не в произвольном порядке. + +Критерий «100% копия с параметрическими изменениями» (в rules/): реверс завершён, когда выполнены все четыре условия — (1) интерфейсы (отверстия, посадочные плоскости, углы) совпадают точно; (2) метрология point-sampling на номинальных параметрах ≥ 99.9% против исходного STEP; (3) прогон на ИЗМЕНЁННЫХ параметрах зелёный у валидатора и spec-теста (доказательство, что параметрика живая, а не декорация); (4) чек-лист сохранности элементов: отверстий было N — стало N, вырезов и разгрузок было M — стало M (считается напрямую: сквозные отверстия — парные цилиндрические грани, вырезы — внутренние контуры; ловит самую обидную ошибку «забыл дырку»); (5) список осознанных отличий от оригинала записан в спеку (керн-метки и случайные скругления не воспроизводятся; технологический язычок воспроизводится, но как правило, а не как копия). Оставшиеся доли процента — обычно K-фактор, точность сплайнов и допуски самого исходного STEP; гнаться за ними — воспроизводить чужие ошибки экспорта. + +Стек (только то, что уже в системе): build123d/OCP — чтение STEP и B-Rep; networkx — AAG; numpy — векторная математика; метрология — distToShape point-sampling; развёртка — SheetMetalUnfolder V2 headless (FreeCAD здесь импортируемая библиотека, не среда), с миграцией на YouCanNotUnfold (быстрый анфолдер на networkx, drop-in замена) или встроенную развёртку build123d по мере зрелости; нестинг — SheetNest / nest2d / CAD-N / u-nesting (NFP-алгоритмы с учётом kerf). Отвергнуто как избыточное: pythonocc-core отдельно (то же ядро уже есть через OCP), CadQuery (дублирует build123d), Analysis Situs (тяжёлая C++-платформа ради того, что закрывают ~100 строк на networkx). Пружинение (springback): нержавейка 1.4301 сильно наклёпывается — измеренный на готовой детали угол уже включает отпружинивание; компенсация — забота гибщика, но её реальные значения от t/R стоит завести строкой в dfm_blechcon.json из практики Blechcon. + + +### Spec-Driven Development: спека задачи как контракт + +Система уже устроена по принципам SDD (истина — спецификация, геометрия — артефакт, валидатор — исполняемая спека ограничений, история «зачем» в коммитах). Дополняем тремя приёмами. (1) Спека до геометрии: для каждой новой детали до первой линии в песочнице пишется spec.yaml — намерение (intent), интерфейсы стыковки (из паспортов), проверяемые ограничения приёмки (масса, нагрузка, зазор, целевая себестоимость по BOM) и явные non-goals (чего НЕ делаем). Спека — файл истины в products/ рядом с params.json. (2) Спека → тест → реализация: CI генерирует приёмочный прогон из constraints автоматически ещё до начала работы; красный прогон на пустой детали — норма, работа исполнителя = сделать его зелёным; приёмка превращается из «проверить сданное» в «позеленить спеку», субъективность ревью сжимается до вопроса «валидно и не глупо?». Спека = формальное основание сдельной оплаты — контракт чище любого текстового ТЗ; спор «деталь не та» превращается в «спека была неполна», и чинится спека, обогащая шаблон. (3) LLM работает от спеки, не от чата: промпт сначала материализуется в spec.yaml (Claude сам оформляет), человек подтверждает за секунды, потом генерация — намерение зафиксировано в Git до реализации, «LLM понял не так» ловится на спеке, а не на геометрии; правило зашивается в rules/ и в системный промпт RECADA-ассистента. Внедрение: этап 2, рядом с валидатором (генератор приёмочного прогона — малый скрипт CI). + + +### Сборка: проверки уровня изделия + +Детали собираются в багажник, поэтому у сборки свой слой проверок — отдельно от геометрии деталей. Позиционирование уже описано (именованные интерфейсы вместо констрейнов, всё от скелета); добавляются четыре проверки, все — геометрия и арифметика, без физических движков и симуляции монтажника. + +| Проверка | Что делает и зачем | +|---|---| +| Доступ инструмента (assembly feasibility) | Для каждого болтового соединения проверяется свободный цилиндр доступа: прямой канал от оси болта наружу под головку ключа (диаметр, длина хода) и вылет для установки стержня. Ловит классическую ошибку «спроектировано, но не собирается»; критично, потому что монтаж идёт на крыше в неудобной позе. Заодно проверяется, что деталь заводится на место при уже смонтированных соседях. | +| Размерные цепи (tolerance stack-up) | Платформа стоит на четырёх ногах: допуск гибки (±0.5–1°), допуск отверстий лазера, разброс крыши — сумма даёт перекос или несведение отверстий на последней ноге. Система проходит цепочку «крыша → нога → профиль → следующая нога» по худшему случаю и статистически (RSS) и выдаёт минимальный необходимый овал отверстий. Снимает целый класс рекламаций «не встало». | +| Автопорядок монтажа | Последовательность выводится из графа связей (нога к крыше → профиль к ногам → аксессуар к профилю) с учётом проверки доступа, а не пишется руками. Инструкция генерируется автоматически и всегда актуальна; Unity-конфигуратор получает готовую последовательность для интерактивной 3D-инструкции. YAML остаётся для человеческих подсказок («не затягивать до установки задних ног»). | +| spec.yaml сборки | У изделия тоже есть контракт приёмки: суммарная масса, допустимая нагрузка, число уникальных SKU (метрика унификации — чем меньше разных деталей, тем дешевле склад), оценка времени монтажа по числу крепежа, совместимость аксессуаров. Приёмка сборки — такой же зелёный прогон, как у детали. | + + +--- + +## 5. Конфигуратор (Unity + Shopify) и инструкции + +Конфигуратор разрабатывается на Unity (WebGL для сайта; из того же проекта — нативные мобильные приложения с AR-примеркой через AR Foundation и десктоп для дилерского киоска/выставки). Контракт CI ↔ фронтенд не зависит от движка — catalog.json: SKU, ревизия, статус, GLB, точки стыковки, варианты, правило замены; Unity грузит GLB в рантайме через glTFast, attach-точки читаются как named nodes. Возможность «техпроцесс в конфигураторе»: Unity-сцена визуализирует сборочные шаги из тех же YAML-инструкций — интерактивная 3D-инструкция вместо PDF. Риски Unity-WebGL: тяжёлый билд (15–50 МБ против 2–5 у three.js) — следить за временем первой загрузки на мобильном (~5 с порог); при провале — гибрид: лёгкая витрина-превью + полный Unity-конфигуратор. GLB сжимается (gltfpack+Draco: десятки КБ), ревизия в имени файла против кэша CDN. Клиенты видят только протегированные релизы. «Светофор нагрузки»: суммирование масс аксессуаров против допустимой нагрузки крыши — рост конверсии, все данные уже в каталоге. Инструкции из YAML (шаг, детали, крепёж, момент) рендерятся CI с актуальными изображениями; конфигурация клиента известна — инструкция генерируется под его заказ. Конфигуратор на базе FreeCAD для розницы отвергнут (секунды на пересборку, ферма контейнеров, мобильные): розница = pre-built GLB, мгновенно; VDI-FreeCAD с панелью RECADA = CPQ-инструмент для дилеров/нестандарта, выход — params.json сразу в производство. + + +--- + +## 6. Прототипирование: четыре ступени + +| Ступень | Что проверяет | Срок/цена ошибки | +|---|---|---| +| 1. Цифровой | Коллизии, FEM (CalculiX+Gmsh в валидаторе), AR-примерка на машине (model-viewer, тот же GLB) | минуты / 0 € | +| 2. Печатный | --target=proto → STL, FDM 1:1 (PETG): посадка, отверстия, помехи; ID получателя выдавлен на нелицевой стороне; принтер 300–500 € окупается первой итерацией | часы / ~2 € | +| 3. Металл (DC01) | Blechcon как прототипная мастерская; material_override: DC01 вместо 1.4301 (2–3× дешевле); маркировка PROTO not for sale | дни / десятки € | +| 4. Пилотный комплект | Боевой материал, установка, фотофиксация → правки в паспорт и DFM-правила: цикл обучения замкнут | недели / сотни € | + + +--- + +## 7. Аксессуары: вторая ось + +Боксы, маркизы, Starlink, LED-бары, солнечные панели. Ключ — стандартизованный интерфейс платформы (Т-паз, шаг): аксессуар привязан к интерфейсу, не к машине — спроектирован один раз, подходит всем. Паспорт аксессуара: крепление, объём + зоны исключения, масса и ЦТ (валидатор считает суммарную нагрузку конфигурации), совместимость. Для покупных изделий проектируется только адаптер — тот же конвейер. Оцифровка: STEP от производителя (дают дилерам) или скан — фотограмметрия для оболочки (±1–2 мм, глянец матировать) + штангенциркуль для крепежа (±0.05); поле точности в паспорте, неточность компенсируется конструктивно (овальные отверстия). Печатная примерка ловит ошибки оцифровки за 2 €. Библиотека паспортов популярных аксессуаров — накапливаемый актив. + + +--- + +## 8. Кадровая лестница: рост по результатам в логах + +| Ступень | Доступ | Переход | +|---|---|---| +| Новичок | Тестовые и простые задачи, только VDI, сдельно | вход свободный | +| Проверенн ый | Прямые заказы, ставка выше | 3+ принятых работы | +| Senior | Сложные узлы, Git-доступ, проектная оплата | устойчивое качество работ + коммуникация | +| Lead/review er | Первичная приёмка, наставничество, ТЗ; решает bus factor | отдельный отбор по качеству мышления в комментариях (принцип Питера: лучший чертёжник ≠ лучший ревьюер) | + +Одна удачная работа — шум, паттерн на 4–6 задачах — сигнал. Система логирует: качество вопросов к ТЗ, итерации до зелёного, поведение при проигрыше, экономическое чутьё. Повышение вычисляется, а не назначается — прозрачность правил сильнее премий на рынках СНГ/Азии. Финальное доверие (доступ к ядру) — личное решение. + + +--- + +## 9. Наём в разных странах: рынки и ставки + +| Рынок | €/час | Роль | +|---|---|---| +| Индия | 4–10 | Основной пул: глубина, английский, бюро sheet metal (Пуна/Ченнай/Бангалор), UTC+5:30. Пинг 100–160 мс — фильтровать пробной сессией. | +| Узбекистан/Кы ргызстан | 5–12 | Русский трек: ГОСТ-школа, лояльность; отсюда вырастут постоянные и lead. Пинг 60–90 мс — VDI комфортен. | +| Пакистан | 3–7 | Добавочные руки строго сдельно (свет/интернет — риск исполнителя; PayPal нет). | +| Вьетнам | 5–12 | Отложить (язык, UTC+7, пинг 180–250); стратегичен при азиатском векторе производства. | +| Россия | — | Напрямую нет (платежи, комплаенс); легально — релоканты (Армения/Грузия/Казахстан) или ИП в третьей стране; Kasm по HTTPS работает при любых блокировках. | + +Модель найма: удалённые инженеры-конструкторы на сдельной основе (оплата за деталь, прошедшую валидатор) с ростом до постоянного сотрудничества. Воронка: 100 приглашений → 30 откликов → 15 тестов → 8 сдали → 3–4 прошли → 1–2 на скамейку; цена постоянного ~300–500 €. Каналы: Upwork (активные приглашения на платный тест), hh.uz, GrabCAD, кафедры вузов, рефералы. Оффер-магниты: платный тест; не нужен софт и мощный ПК (браузерная сессия — заодно легальность и доступ пула без железа); автоматическая приёмка по опубликованным правилам; прозрачная лестница; постоянный поток. Ступенчатый отсев: фильтр-вопрос → платный тест с автоприёмкой (эталон Bracket_L) → лично смотрите 3–4 из ста. Тест включает 10-мин пробную VDI-сессию (меряет реальный канал кандидата). + + +--- + +## 10. Жизненный цикл: две оси + +| Конструкция (status) | Склад/продажа (sales_status) | +|---|---| +| Draft / Черновик — в работе, снаружи не видна In Review / На проверке — валидатор зелёный, PR открыт Released / Выпущена — подпись + тег + Rev; единственный производимый статус Superseded / Заменена — вышла новая ревизия (автоматически); хранится вечно Obsolete / Снята — негодна, преемника нет; производство блокирует CI On Hold / Заморожена — остановлена без решения | Not for Sale — прототипы (PROTO), пилоты Active — производится и продаётся Run-out — superseded, остаток >0: продаём, не дозаказываем; webhook Shopify при нуле включает преемника Discontinued — снята; карточка живёт вечно (гарантия, QR-инструкции) Service Only — по обращению как запчасть к проданному | + +Политика замены A→B выбирается при релизе: run-out / немедленная (дефект) / параллельно (FFF: та же посадка и функция = ревизия, не новый SKU). Ось 1 движет CI по git-событиям; ось 2 — webhook Shopify + два ручных решения. + + +--- + +## 11. Изменение деталей: где дорого + +Создание дёшево; изменение дорого пропорционально зависимостям. Три тяжести: в существующих параметрах (бесплатно) / новый параметр (правка генератора — узкое место квалификации) / слом топологии (= новая деталь; правило «больше N строк — новый шаблон» против костылей). Каскад (развёртка, масса и нагрузка конфигураций, GLB, инструкция, BOM) CI пересобирает сам; управленческими остаются: неприкосновенность интерфейса (изменение = «версия 2 платформы»), явная политика замены на складе, запрет обходов мимо params (STEP на Blechcon — только из CI, технически). Плюс: геометрический дифф в PR (подсветка изменений старый/новый STEP), версионирование библиотечных блоков с явной миграцией, шлюз на изменение released (issue с обоснованием + go владельца). + + +--- + +## 12. ISO 9001 + +Система = исполняемый QMS: 8.3 (входы=паспорт+ТЗ, выходы=артефакты CI, верификация=валидатор, валидация=прототипы, изменения=PR+ревизии); 8.5.2 прослеживаемость (деталь → тег → автор → ревьюер → заказ); 7.5 документация = Git; 10.2 CAPA (рекламация → issue → правило валидатора навсегда). Фрилансеры = внешние поставщики (8.4): страница критериев допуска/оценки/отстранения (данные собирает Gitea), объём контроля привязан к лестнице (риск-ориентированность), компетентность (7.2) = конкурсная история, ревьюеру — чек-лист и персональная подпись. GDPR: обработка данных фрилансеров (аккаунты, логи) — пункт в договоре, запись в Verzeichnis, срок хранения логов. Сертифицироваться — когда спросит рынок (B2B/OEM): фундамент ISO-ready, формализация 2–3 мес, ~3–6 т.€. Правило: документируется исполняемое или используемое — иначе театр качества. + + +--- + +## 13. Физические испытания и ISO 11154 + +Слепая зона, закрытая внешним аудитом: система проектирования не заменяет испытаний готового изделия. Даже если съёмный багажник юридически классифицируется как груз (Ladung, §22a StVZO — проверить у юриста, не принимать на веру) и не требует ABE/Teilegutachten, отсутствие документированных динамических испытаний по ISO 11154 (включая City Crash, ускорения до ~16g) при аварии превращает Produkthaftung в персональный риск руководства и закрывает вход в B2B-дилерские сети. Действия: (1) выяснить текущий статус испытаний продукта; (2) при отсутствии — отдельный испытательный трек: эталонные испытания базовой конструкции в аккредитованной лаборатории (бюджет — тысячи €, это цена входа в B2B); (3) расширить FFF-правило пунктом «изменение, влияющее на несущую способность, требует переиспытания» — валидатор помечает такие изменения автоматически; (4) прослеживаемость «какая ревизия испытана» система уже даёт бесплатно (git-тег + протокол испытаний как артефакт релиза). + + +### Текущий статус и расчётная база + +Компанией уже выполнены прочностной (статический) и модальный анализы конструкции; планируется аэродинамический. Разграничение: расчёт (FEM) и физическое испытание по ISO 11154 юридически не взаимозаменяемы — расчёт говорит «должна выдержать», протокол испытания документирует «выдержала»; для Produkthaftung и B2B нужен физический протокол, а расчёты — сильное дополнение, удешевляющее сертификацию (меньше итераций в лаборатории). Модальный анализ: собственные частоты против частот вихревого срыва на 100–130 км/ч (гул, резонанс, усталость) — превращается в правило валидатора «первая собственная частота ноги ≥ порога» (порог по формуле Струхаля из скорости и размера профиля; CalculiX умеет модальный анализ штатно). Аэродинамика прагматично: ветровые нагрузки как входные силы прочностного расчёта — нормативные формулы без CFD; частоты срыва — Струхаль; полный CFD (OpenFOAM) — разово для формы windshield и гула, вне конвейера. Существующие отчёты — эталоны калибровки CalculiX-пайплайна: прогнать ту же деталь, сверить частоты и напряжения — валидатор рождается проверенным (логика метрологического эталона Bracket_L, но для физики). + + +--- + +## 14. Открытая платформа сообщества + +Печатаемые аксессуары сообщества (держатели, клипсы) делают платформу «липкой» и служат разведкой спроса (популярная модель → задача на металлическую версию). Публикуется открытая спецификация интерфейса (измерима штангенциркулем; опубликовать первым = стать стандартом; управление — у вас, модель USB); публичный репозиторий + STL; автопроверка посадки тем же конвейером; категория Community в конфигураторе. Защита (Produkthaftung): CC BY-NC-SA + disclaimer (вы хостинг, не производитель), автомодерация категорий (несущее на скорости — на свой риск/не публикуется), витринная граница «металл протестирован / community на свой страх». В правилах — право Airholder на металлическую версию по мотивам (с упоминанием автора). Сообщество = нулевая ступень кадровой лестницы. Веб-визуализация не раскрывает ИС: в браузер уходит огрублённый GLB («фотография часов, не чертёж»); децимация — параметр CI; точная геометрия покидает сервер единственным маршрутом CI → Blechcon. От обмера купленного изделия не защищает ничто — реальная защита: скорость конвейера, паспорта, библиотека решений, валидатор, скамейка, стандарт. + + +--- + +## 15. Роль FreeCAD и инженера: кто что делает + +Позиция уточнена по итогам практики. Инженер из системы не уходит — он остаётся автором формы. Полноценный CAD мы не пишем: это не наш продукт и не наша экспертиза. Но новые детали делать надо, и делать их надо руками — в FreeCAD. Инструмент recada.py не заменяет конструктора, он снимает с него рутину: обмер, реверс, проверки, развёртки, пакет производства. + + +### Разделение труда + +| Кто | Что делает | Чем | +|---|---|---| +| Инженер | придумывает и рисует новую деталь, принимает решения по форме, радиусам, разгрузкам | FreeCAD — руками, привычно | +| Генератор кода | превращает нарисованное или реверснутое в код с параметрами; код открывается во FreeCAD как обычная деталь | recada.py → Python для FreeCAD | +| Инструмент | обмер, реверс, растяжение, операции, три критерия приёмки, DXF, BOM, пакет для Blechcon | headless-ядро на OCCT, веб-интерфейс | +| Редактор изделия | багажник как параметры: высота, положение, длина; ноги перегенерируются | рабочее окно | + + +### Почему FreeCAD остаётся, а не уходит + +Опыт показал: в цепочке автоматики FreeCAD не нужен и даже мешает — его OCCT 7.8 не выполняет операции, которые проходят на 7.9 в нашем ядре. Но там, где нужна рука конструктора — новая форма, нестандартный узел, эскиз под конкретную машину — FreeCAD незаменим и бесплатен. Отсюда гибрид: FreeCAD рисует, инструмент считает и проверяет, генератор кода связывает их в обе стороны. + + +### Идея workbench: как связать + +Два пути, проверены обсуждением. Первый — полноценный верстак FreeCAD на его Part-API: требует переноса кода с OCP на другой API и упирается в отличия версий OCCT (offset у нас падал именно там). Второй — гибридный верстак: кнопки в FreeCAD сохраняют выделенное тело во временный STEP, вызывают внешний инструмент из его окружения и импортируют результат обратно. Ни переноса, ни конфликта версий; тяжёлая геометрия считается там, где она работает. Рекомендован второй путь. Делать его стоит после того, как устаканятся операции ядра — оборачивать движущуюся цель в интерфейс рано. + +VDI (Kasm/Guacamole) как способ дать внешним исполнителям FreeCAD в браузере остаётся в резерве — для сценария с недоверенными подрядчиками из раздела о найме. Для доверенной команды достаточно WireGuard и локального FreeCAD с генератором кода. + + +--- + +## 16. Инструмент реверса recada.py — построен и проверен + +За два дня сентября собран рабочий инструмент, который снимает деталь с чужой геометрии и превращает её в проверенный производственный файл. Всё headless, без FreeCAD в цепочке: ядро OpenCASCADE подключено как библиотека (OCP), FreeCAD остаётся только смотрелкой. Один файл recada.py: командная строка и браузерный интерфейс. + + +### Что умеет + +| Функция | Что делает | Проверено на | +|---|---|---| +| Обмер | толщина, гибы с двумя углами (поворот и двугранный), радиусы, отверстия с осями, разгрузки | Defender LRDE-2BR1: t=3.0, 7 гибов, 6 отверстий — повторяется до сотых | +| Расщепление на оболочки | виртуальный щуп: точка − нормаль × t попадает на поверхность → грань оболочечная; парование + двухцветная раскраска графа | 41 грань покрывает 93 % площади; стороны 21/20 | +| Реконструкци я | кинематика на всех гранях тела, перестроение зоны гиба, заделка дыр | объём 78031.6 против эталонных 78031.6 | +| Растяжение | разрез полупространством вдоль плоскости стенки, сдвиг, призма сечения; автоподбор места разреза по одиночному сечению | стенка +10 мм, +20 мм — замкнуто, годно | +| Операции | зеркало, сверление, заглушка, обрезка, скругления — с проверкой после каждой | все дают замкнутый солид | +| Развёртка | DXF с линиями гиба на отдельном слое | 299 × 168 мм — совпало с порталом | +| Сборка | разбор STEP на детали с именами, группировка одинаковых, пакетный реверс | 84.5 МБ, 628 объектов | + + +### Три критерия приёмки — главный вывод + +Критериев оказалось не один и не два, а три, и третий не заменяется первыми. Это не теория: каждый найден через отказ. + +| Критерий | Чем проверяется | Что ловит | Как найден | +|---|---|---|---| +| Метрология | point-sampling двусторонний, расстояние до ПОВЕРХНОСТИ | точность геометрии | свой | +| Целостность | свободных рёбер = 0, солидов ≥ 1 | дыры, незамкнутость | отказ портала Blechcon | +| Производство | загрузка на портал поставщика | распознаётся ли как листовая деталь | опыт | + + +### Три ошибки, найденные и исправленные + +1. Метрология мерила расстояние до тела, а не до поверхности. distToShape до солида возвращает ноль для точек внутри материала — «толстая» деталь проходила приёмку. Проверка: сдвиг детали на 0.3 мм давал 74 % точек в допуске, после исправления 29.8 %. + +2. Односторонняя выборка не видела лишний металл: кандидат длиннее эталона давал RMS 0 и 100 %. Невязка сделана двусторонней. + +3. Проверки замкнутости не было вовсе. Деталь может быть геометрически точной и дырявой одновременно — метрология этого не ловит. Обнаружено отказом портала, теперь считается одной строкой и предсказывает отказ до отправки. + + +### Что не получилось и почему + +Наращивание оболочки целиком (offset всей сшитой оболочки) оставляет дыры и зависит от версии OCCT: на 7.9 работает, на 7.8 внутри FreeCAD падает. Заменено поэлементным наращиванием с булевым объединением — для одной грани offset корректен всегда. Заплатки BRepFill в зоне гиба дают сплайновую грань — распознаватель листового металла на такой ломается. Отсюда правило: в зоне гиба строить только канонические поверхности (цилиндры и плоские секторы), профиль разгрузки генерировать по правилу, а не латать. + +Параметрический поворот угла гиба даёт замкнутое тело, но пока не проходит распознавание у поставщика: торцевой профиль гиба с разгрузкой надо генерировать заново, а не копировать. Это единственная незакрытая задача цепочки — она понятна до деталей. + + +--- + +## 17. Редактор изделия: багажник как параметр, а не как геометрия + +Ключевая мысль, к которой пришли в работе: у багажника нет произвольных степеней свободы. У него есть продуктовые параметры, а положение и форма деталей — их следствия. Редактор поэтому не двигает детали, а перегенерирует их. + + +### Сценарий, ради которого всё делалось + +Багажник спроектирован, между крышей и низом поперечины 20 мм — руку не просунуть. Решение: приподнять. Если ноги уже реверснуты в код, достаточно одного числа: точки крепления на крыше зафиксированы, вертикальная стенка каждой ноги растягивается на 20 мм, развёртки пересчитываются, DXF обновляются, три критерия приёмки прогоняются заново. Полный реверс всей системы не нужен. + +Тот же цикл обслуживает аэродинамику: CFD говорит «сдвинуть платформу вперёд на 30, поднять на 10» — это два числа в product.json, а не неделя ручной работы. Именно поэтому гнутые детали обязаны жить как код с параметрами. + + +### Единое рабочее окно + +| Зона | Содержимое | +|---|---| +| Дерево слева | таблица как BOM: видимость, выбор, имя, количество экземпляров, статус (исходник / реверснута / параметрическая), роль (нога, кронштейн крыши, платформа, поперечина, крепёж, кузов). Поиск по имени, автоопределение ролей по геометрии | +| Сцена | свой просмотрщик на three.js: разлёт, прозрачность, сечение, измерение, виды, изоляция. Выбранные подсвечены, реверснутые зелёные | +| Действия | для исходника — «Полный реверс» (одинаковые детали считаются один раз); для реверснутой — растяжение, отверстия, зеркало, DXF | +| Изделие | точки крепления к крыше как якорь, «поднять платформу на N мм» — перегенерация всех ног | +| Журнал | project.json: каждая операция записана параметрами. Истина — журнал, геометрия — артефакт | + + +### Пакет производства + +Одна кнопка собирает архив: step/ — уникальные детали, dxf/ — развёртки с линиями гиба, passport/ — обмер каждой в JSON, BOM.csv — спецификация с количествами, project.json — журнал операций. Это и есть то, что уходит в Blechcon. + + +### Что это меняет в экономике проекта + +Реверс кронштейна Defender занял минуты вместо дней. Четыре одинаковые ноги считаются один раз. Подъём платформы — одно число вместо перепроектирования. Инфраструктура: EX44 за 59 евро нетто плюс Storage Box — около 63 евро в месяц за систему, которой нет в коробочных CAD: автоматический реверс листа с проверкой производственной пригодности. + + +--- + +## 18. Библиотека деталей и сервер: система с обратной связью + +Сервер EX44 куплен, библиотека собственных STEP-файлов собирается. Вместе они превращают инструмент из демонстрации в систему, где каждая проверенная деталь делает следующую точнее. + + +### Что даёт сервер + +Разделение ядра и интерфейса: ядро — библиотека функций с командной строкой, интерфейс — тонкий слой над очередью задач. Реверс ставится в очередь, страница не блокируется, результаты сохраняются в постоянное хранилище по хешу файла. Без этого не заработают ни второй пользователь, ни библиотека. Непрерывная регрессия: каждый коммит в инструмент прогоняется по библиотеке ночью на CI, утром — одна цифра: сколько деталей проходят три критерия. Ломать работающее незаметно больше нельзя. + +Общий доступ: удалённые инженеры из раздела о найме работают через WireGuard с одним инструментом и одними правилами. + + +### Что даёт библиотека + +| Применение | Как работает | Что закрывает | +|---|---|---| +| Калибровка K-фактора | наша аналитическая развёртка против ответа портала Blechcon по каждой детали (299 × 168 подтверждено) | третий критерий из ручной проверки становится источником данных | +| Каталог разгрузок | типы, глубины, привязка к кромке — снятые с реальных деталей | незакрытую задачу зоны гиба: генерировать по правилу, не латать | +| DFM-правила | статистика радиусов, углов, толщин, диаметров, отступов от кромки | dfm_blechcon.json из практики, а не из головы | +| Поиск похожего | подпись детали: t, число гибов, набор углов, рисунок отверстий | взять проверенную и растянуть вместо проектирования с нуля | +| Паспорта авто | точки крепления и профили крыш как данные библиотеки | редактор изделия без ручного ввода координат | +| Стандарт школы | новый конструктор видит принятые радиусы и решения | передачу опыта без устных преданий | + +Граница честности: библиотека советует и предупреждает, решает человек. Она не скажет, выдержит ли деталь нагрузку, и не заменит инженерное решение там, где новая машина требует того, чего в практике ещё не было. + + +### Рынок: Zoo как ориентир + +Zoo (zoo.dev, бывший KittyCAD) — ближайший по философии проект: их язык KCL — читаемый текст как источник истины, версионирование в Git, агент Zookeeper переписывает STEP в код. Два отличия делают нашу нишу свободной: геометрический движок Zoo облачный и американский — 3D-вид это видеопоток с их серверов, чертежи уходят к ним; и у них нет производственной приёмки листового металла — метрологии, целостности, распознавания порталом поставщика. Zoo подтверждает направление и не закрывает нашу задачу. + + +### Как Zoo стримит и что из этого следует + +Движок Zoo работает на их серверах. Клиент ничего не считает: команды моделирования уходят по WebSocket, обратно приходят видеокадры по WebRTC — как видеозвонок. Стримится только 3D-вьюпорт; интерфейс, дерево и редактор кода живут в клиенте. Это не Kasm: Kasm стримит целый рабочий стол или приложение через захват экрана, тяжело и с задержкой; Zoo встроили рендер и кодек прямо в собственный движок на Rust, поэтому клиент лёгкий. Три причины их выбора: не портировать движок под платформы, открывать любые модели на слабых машинах, и главное — геометрия никогда не покидает их инфраструктуру, клиент видит только пиксели. + +Для нас это независимое подтверждение принципа из раздела о VDI — «пиксели, не геометрия». Разница в реализации: они встроили стриминг в свой движок, мы стримили бы готовое приложение. Обратная сторона нашей текущей схемы: просмотрщик на three.js отдаёт в браузер триангулированную сетку — для доверенной команды через WireGuard это нормально, для внешних подрядчиков это утечка, сетку можно сохранить. Поэтому для недоверенных остаётся стриминг пикселей. + + +### Свой движок: ядро — нет, обвязка — да + +«Движок» Zoo — три слоя: геометрическое ядро (B-Rep, булевы, скругления, развёртки), рендер и стриминг с API. Ядро они пишут годами командой специалистов по геометрическим ядрам с десятками миллионов инвестиций — и в марте 2026 всё ещё писали, что скругления рёбер остаются их болью. Это цена ядра. Нам его писать не нужно: OpenCASCADE — тридцать лет работы, бесплатно и открыто, и на нём вчера реверснута деталь, которую принял портал. Ядро — решённая задача чужими руками. + +Что собрать реально можно — два верхних слоя вокруг OCCT. Рендер на сервере: наш three.js-просмотрщик в headless-браузере, либо встроенная визуализация OCCT. Стриминг: картинка по WebRTC в клиент, команды камеры обратно — готовые библиотеки, не исследование, порядок работы недели. Это «стриминг вьюпорта вокруг OCCT», не «свой движок». + +Когда это нужно: только для внешних исполнителей, которым нельзя отдавать геометрию. Для своей команды нынешняя схема проще и быстрее. Стриминг вьюпорта — в резерве рядом с VDI, делается при появлении первого недоверенного подрядчика. Zoo пришлось писать ядро, потому что они продают платформу. Мы продаём багажники — разная экономика, разные решения. + + +### Ядро напрямую: OpenCASCADE без FreeCAD + +Точная формулировка архитектуры: OpenCASCADE и есть ядро внутри FreeCAD. Мы вызываем его напрямую через обёртку OCP, минуя FreeCAD — двигатель на стенде вместо автомобиля. Это даёт headless-работу на сервере и в CI, пакетную обработку библиотеки и свою версию ядра (7.9, где наращивание работает; в FreeCAD 7.8 оно падает). FreeCAD остаётся кабиной для инженера — там, где нужна рука. Одна геометрия, два способа обращения: программный для автоматики, ручной для человека. Практика: держать FreeCAD той сборки, где версия OCCT совпадает с ядром. + +Ядро создано с ИИ, проверено реальностью, работает без ИИ. ИИ писал код ядра, но не работает внутри него: каждая функция — обычный детерминированный Python, один вход даёт один выход. Три ошибки ядра нашёл не ИИ — их нашли перепроверка, негативный тест и отказ портала. Отсюда правило разработки: тест раньше кода (Spec-Driven Development, применённый к самому инструменту), библиотека как регрессия после. Обучать свою модель не нужно и не выйдет — десятки деталей не выборка; библиотека работает как справочник. Готовый ИИ подключается как переводчик намерений к операциям ядра, не как источник геометрии. + + +### Просмотрщики на том же ядре (состояние на сентябрь 2026) + +| Инструмент | Что это | Где применить | +|---|---|---| +| OCP.wasm + Yet Another CAD Viewer | OpenCascade и обёртка OCP скомпилированы в WebAssembly через Pyodide; build123d работает целиком в браузере. Та же OCP, что в recada.py | инструмент у клиента в браузере без сервера — ничего не покидает машину; ответ на утечку сетки для внешних подрядчиков | +| OpenCascade.js (ocjs.org) | порт OCCT в JavaScript/WASM через Emscripten, почти нативная скорость, многопоточность, урезанные сборки; на нём CascadeStudio | браузерный просмотр и лёгкие операции без утечки B-Rep | +| OCCT WebGL viewer | собственная визуализация OCCT в WASM — открывает BREP в браузере, нужен WebGL 2.0 | показать «ровно как в ядре» без FreeCAD | +| Mayo | настольный просмотрщик и конвертер на Qt + OCCT: STEP, IGES, BREP, экспорт glTF; обновляется | бесплатный десктопный STEP-вьювер на нашем ядре — проверить, что видит ядро | +| OCP CAD Viewer (VS Code) | показывает объекты OCP/build123d прямо из Python, без STEP | отладка ядра при разработке | +| Наш three.js-просмо трщик | сетка с сервера, выбор, разлёт, сечение, измерение | быстрый интерфейс для доверенной команды | + +Стратегически интереснее всего OCP.wasm: наша обёртка та же, значит recada.py может работать у клиента в браузере без сервера вообще — обратный подход Zoo, где всё улетает в облако. Для внешних исполнителей это третий путь наряду с VDI и стримингом вьюпорта: считают у себя, отправляют только результат приёмки. + + +### Версии ядра и формат на границах + +OCCT 7.8 в FreeCAD 1.1, 7.9.3 в нашем ядре, 8.0 вышла в мае 2026 (крупнейший шаг за годы: чтение STEP до 75 % быстрее, потокобезопасное чтение и запись STEP, графовая топология BRepGraph, ускоренные экстремумы — всё под сервер и библиотеку; новых операций для листового металла нет). FreeCAD на восьмёрке — реалистично 2027 год. Правило: ядро инструмента фиксируется на одной версии, записывается в project.json, и переезжает только целиком — после прогона библиотеки-регрессии до и после. Ядро и FreeCAD никогда не обязаны совпадать; архитектура это переживает, потому что генератор кода вшивает готовые тела, а не пересчитывает их во FreeCAD. + +Формат на границах — всегда STEP. BREP — внутренний формат ядра, его версия меняется с мажорным релизом, и 7.9 может не прочитать то, что записала 8. STEP — ISO 10303, читается любой версией в обе стороны, Inventor, SolidWorks, порталом поставщика. Правило в одну строку: BREP — внутри процесса, STEP — на любой границе: между инструментом и FreeCAD, между сервером и подрядчиком, между сегодняшним ядром и завтрашним. Цена — объём (800 КБ против 120 у BREP) — терпима. + + +### STEP с именами: обязательное требование к экспорту + +STEP хранит имена деталей и узлов, дерево сборки с положениями, количество экземпляров, цвета и слои — если писать через XCAF (STEPCAFControl_Writer с документом). На входе инструмент так и читает: имена Bracket_F_v2.1, wind_side_v2.1 приходят из сборки с положениями. На выходе сейчас пишет упрощённо (STEPControl_Writer) и имена теряет — дерево сплющивается в безымянный компаунд. Это ошибка инструмента, не формата; исправляется заменой писателя: имя, положение и цвет для каждой детали. Цветом кодируется статус: зелёный — реверснута. + +| STEP переносит | STEP не переносит — едет в project.json | +|---|---| +| имена деталей и узлов | обмер: толщина, гибы, отверстия | +| дерево сборки с положениями | статус реверса, роль | +| количество экземпляров | журнал операций | +| цвета и слои | точки крепления | + +Пара «STEP + project.json» — не избыточность, а разделение: STEP несёт то, что понимают все, JSON — то, что понимаем только мы; связь по имени детали. Безымянные экземпляры вида =>[0:1:1:5] в сборке — не потеря при чтении, а неназванные компоненты в исходном CAD; лечится именованием перед экспортом из Inventor. + + +### Решение: каскадный вьювер на сервере, наружу только пиксели + +Принято после появления EX44. Ядро и рендер OCCT работают на сервере; визуализация OCCT (V3d, AIS) доступна из Python через ту же OCP — без C++ и без WASM. Браузер получает кадры и отправляет клики; сервер сам определяет, на какую грань, ребро или вершину пришёлся клик — выбор точный по B-Rep, с привязкой к центрам отверстий и серединам рёбер. Геометрия не покидает сервер: ни сетка, ни STEP. Это модель Zoo на OCCT и раздел о VDI без Kasm. + +Отвергнутая альтернатива — гибрид с локальным вращением по сетке: быстрее и дешевле для сервера, но сетка уходит в браузер и может быть сохранена. Для защиты данных неприемлемо. + + +### Достаточно ли EX44 + +Видеокарты нет — рендер программный, через Mesa на процессоре. Сцена CAD проста: сотни тысяч треугольников, десятки кадров в секунду на ядро. Плюс кодирование кадра — ещё ядро. Итого два потока на активную сессию; активная — только пока человек вращает, при просмотре нагрузка нулевая. За вычетом обмера и инфраструктуры — пять-шесть одновременно вращающих. Для команды с запасом, для внешних подрядчиков — несколько человек одновременно. + +Что расширяет запас без железа: во время вращения кадр пониженного разрешения, после остановки — один чёткий; лимит частоты кадров на сессию. Когда понадобится другое железо: при десяти и более одновременно активных — GEX-серверы Hetzner с GPU, цена в три-четыре раза выше; решение на потом. + + +### Защита данных — не только стриминг + +| Мера | Что закрывает | +|---|---| +| Все библиотеки (three.js, pako и др.) на своём сервере | сейчас просмотрщик грузит их с внешних CDN — факт открытия детали виден третьей стороне по запросам | +| Доступ только через WireGuard, свой ключ у каждого | в логах видно, кто что открывал и когда | +| Без точек скачивания для внешних ролей | экспорт STEP и DXF — только доверенным | +| Водяной знак с именем сессии на кадре | снимок экрана запретить нельзя, атрибутировать — можно | +| Рендер по запросу, не поток | нагрузка на сервер и объём трафика | + +Граница, признанная заранее: от фотографии экрана не защищает ничто. Пиксели закрывают утечку системы — моделей целиком, параметров, библиотеки. Одну деталь перерисовать с экрана можно всегда: «перенабрать деталь можно, систему нельзя». Этапность: сначала статичный рендер по запросу — кадр с заданной камерой и номер грани по клику; непрерывный поток — позже, если понадобится. Место в очереди: после разделения ядра и интерфейса и очереди задач; строить точный вьювер к инструменту, который меняется по три раза в день, рано. + +Ответ на вопрос «как учить наш алгоритм» — на пальцах и как идея использования внешних ресурсов. Алгоритм не запоминает примеры и не угадывает: он работает по правилам и числам. Учить его — значит добавлять правила из отказов, уточнять числа по фактам и не давать забыть выученное. Внешние ресурсы — датасеты, портал поставщика — участвуют в этом как полигон и как оракул, не как учитель. + + +### Три механизма обучения + +| Механизм | Как работает — на реальном примере | Куда пишется | +|---|---|---| +| 1. Каждый отказ становится правилом | кронштейн с повёрнутым гибом отклонён порталом → причина: сплайновая поверхность в зоне гиба → правило «в зоне гиба только плоскости и цилиндры» → валидатор ловит за секунду. То же с дырами: отказ → «свободных рёбер ноль». Одна ситуация — одна строка; ошибка случается один раз | rules/dfm_blechcon.json, валидатор | +| 2. Числа уточняются по фактам | K-фактор из справочника 0,42; портал считает развёртку 299 × 168, наша 298,4 → коэффициент для этой пары R/t сдвигается; двадцать деталей — таблица под реальный цех без единого вопроса поставщику. То же с пружинением, глубиной разгрузок, минимальной полкой | таблицы в rules/ | +| 3. Библиотека не даёт забыть | правка под новую деталь ломает старую незаметно (просмотрщик переделан шесть раз, каждый раз что-то отваливалось). После каждого изменения прогон по всем проверенным деталям: «было 47 из 50, стало 46» — что-то сломали, смотрим что | library/, регрессия в CI | + +> Что не участвует: нейросеть. Ей нужны тысячи размеченных примеров и она ошибается вероятностно; у нас десятки деталей и требование «либо годно, либо нет». Одной фразой: алгоритм учится не на примерах, а на ошибках, разобранных до причины. + + +### Итог — не «больше правил» + +Экспертные системы восьмидесятых умерли ровно от этого: тысячи правил, которые никто не мог ни проверить, ни отменить. Правил становится больше — но их держат в порядке три вещи. Правила обобщаются: три частных «глубина разгрузки при R3/t3 — 4, при R6/t3 — 5, при R3/t2 — 3» становятся одной формулой «1,3 × t + 0,2 × R», которая короче, покрывает небывшие случаи и проверяема; настоящее обучение — когда список сжимается, а покрытие растёт. У каждого правила есть тест и счётчик: правило без детали в библиотеке, на которой оно срабатывает, — мёртвое; не сработавшее за год — кандидат на удаление; срабатывающее на каждой второй детали — сигнал, что оно слишком строгое или конструкторы систематически делают что-то не так. Правила лежат слоями: физика («нержавейка не гнётся с R меньше t» — вечное), профиль поставщика («матрицы 8, 10, 12, 16» — пока жив Blechcon), стандарт школы («у нас всегда R3» — меняется решением); разные владельцы, разная скорость изменений, не смешивать. + +Реалистичный масштаб — десятки правил, не тысячи: листовой металл узок, BenDFM оценивает производимость десятком критериев; больше сотни — значит, что-то не обобщили. И главное: копятся не правила, а данные. Библиотека с ответами портала растёт всегда; правила — её сжатая форма, пересчитываемая заново при смене поставщика или метода. Правила устаревают, данные — нет. + + +### Правило и случай — две вещи, страхующие друг друга + +Правило — обобщение: «отверстие не ближе 2,5 толщин к линии гиба»; применимо к любой детали, но слепо к контексту. Случай — конкретная деталь с исходом: «LRDE-2BR1, отверстие 8,5 в 6 мм от гиба, портал принял, в серии, рекламаций ноль»; ничего не обобщает, но это правда. На новой детали сначала правила — секунда, красное или зелёное; потом случаи — библиотека находит ближайшие по отпечатку и показывает прецедент: «три похожих прошли с первого раза, одна со второго, вот что правили». Где они спорят — самое ценное: правило говорит «слишком близко», а принятая деталь с тем же расстоянием лежит в библиотеке → либо правило смягчается с условием, либо случай помечается исключением. Без случаев правила черствеют, без правил случаи — архив. Цикл: новая деталь → правила → похожие случаи → решение человека → деталь становится новым случаем. Это рассуждение по прецедентам с производственным исходом вместо оценки «похоже». + + +### Внешние ресурсы: тренировочная площадка, не учитель + +BenDFM с двадцатью тысячами деталей не научит алгоритм ничему новому, но покажет, где он ошибается на формах, которых у нас нет. Нашли ошибку → поняли причину → правило. Учитель всё равно один: отказ, разобранный человеком. Роли внешних ресурсов по трём механизмам: для правил — синтетика и чужие формы (BenDFM, SMCAD, HeteroMF, ABC щупом) как источник ситуаций, в которых правило ещё не проверялось; для чисел — литературные таблицы (пружинение, BendGuide) и материальные карты (NUMISHEET 2025 для 1.4301) как стартовые значения до калибровки; для библиотеки — внешние наборы как регрессия на два порядка больше своей, чтобы ломать старое было видно на тысячах, а не на десятках. Раздел 42 содержит полный реестр с приоритетами. + + +### Онлайн-калькулятор Blechcon как оракул + +Портал возвращает на каждую деталь пять сигналов, а мы читали один: распознана ли как листовая, габарит развёртки, толщина и материал, цена по количеству, срок изготовления, иногда замечания технолога. + +| Применение | Как | Что даёт | +|---|---|---| +| 1. Приёмка | прошло / не прошло, развёртка совпала / нет | третий критерий — уже есть | +| 2. Калибровка чисел | их развёртка против нашей по каждой паре R/t | K-фактор под их цех | +| 3. Модель цены — самое ценное | спрашивать портал о вариантах, которых не собираемся заказывать: 2,5 против 3,0 мм; 1.4301 против 1.4404; партии 1, 10, 50; четыре гиба против пяти — каждый ответ точка на кривой; 20–30 загрузок дают формулу цена = f(длина реза, гибы, материал, партия) | «цена в контуре» (раздел 33) без писем — бесплатный оракул стоимости | +| 4. Прецеденты | каждый ответ сохраняется к детали: принята, цена, срок, замечания | через год — история: как менялись цены, что стали и перестали принимать | + +> Автоматизация: официального API у портала, насколько известно, нет — спросить. Через браузер технически несложно, но это чужой сервис: сотни автоматических загрузок за ночь могут нарушить условия и испортить отношения с поставщиком, которому вы нужны как клиент, а не как нагрузка. Порядок: сначала письмо — «хотим отправлять пачки вариантов для оценки; есть ли API или допустимо ли регулярно?». Клиент с готовыми файлами — лучший клиент цеха, обычно отвечают да. Чего не делать: гонять через портал чужие датасеты (BenDFM, ABC) — тысячи деталей, которые никогда не закажете; внешние наборы проверяются нашим инструментом, портал — только для своих деталей и их вариантов. + +Надо ли знать оборудование Blechcon: обязательно — только то, что они публикуют (материалы, толщины, лист, радиусы, допуски): это огибающая возможностей, и портал уже проверяет против неё. Спросить один раз — стандарт инструмента, ширины матриц, усилие и длину стола: без этого три проверки (полка, тоннаж, коллизия) работают на «типичном» инструменте, с этим — на их; портал распознаёт лист и считает развёртку, но не симулирует гибку, коллизия на третьем шаге обнаружится у них на прессе. Не спрашивать — модели станков, внутренние таблицы, выбор последовательности оператором: их ремесло; всё это придёт косвенно через ответы. И зависимость: чем точнее валидатор под Blechcon, тем сильнее привязка — поэтому правила строятся как консервативное ядро под «любой нормальный цех» плюс профиль текущего поставщика (раздел 33, идея 9). Тогда знание о поставщике — преимущество, а не зависимость. + + +--- + +## 19. Инструкция по монтажу как артефакт изделия + +Разобрана действующая инструкция Airholder для Defender 110 (Montageanleitung, 15 страниц). Вывод: около двух третей содержимого — прямые следствия product.json и BOM, треть — постоянные правила и юридика. Инструкция становится седьмым артефактом пакета производства рядом с STEP, DXF, паспортами, BOM, чертежами и project.json — с адресатом покупатель, а не Blechcon. + + +### Что в инструкции откуда берётся + +| Страница / блок | Источник | Режим | +|---|---|---| +| Спецификация Komplettset: 16 позиций с количествами (S13A ×6, B13A ×14, M8x14 DIN 912 ×56, Halterung 1/2 ×8+8…) | BOM; количество винтов — из числа соединений | авто | +| Пиктограммы деталей, виды сверху и изометрия, разнесённый вид | HLR-проекции с сервера в штриховом стиле | авто | +| Позиции сухарей в пазах: 14, 54, 44, 344, 15, 815 мм по профилям A/B/C/D | вычисляются из положений поперечин и кронштейнов вдоль профиля | авто | +| Масса багажника 48 кг в предупреждении о нагрузке | BOM с плотностями | авто | +| Шаги 1–13: соединение половин профиля D, рама, уголки, кронштейны, установка на крышу; выноски крепежа | порядок из графа связей; текст из шаблонов по типу соединения | шаблон + числа | +| Юридика, предупреждения, правило ориентации B-профиля, сборка на ровной поверхности, «правильно / неправильно» 10.1 и 10.2, два способа вставки сухаря | постоянные блоки — конструкторские знания, хранятся как правила изделия | ручной | + +Находка для модели изделия: «Completed connector» = Halterung 1 + Halterung 2. Нога состоит из двух гнутых деталей — кронштейн крыши и стойка — плюс резиновая подкладка и винт M6x22 в водосток. В редакторе изделия нога — узел из двух деталей с общим интерфейсом; подъём платформы растягивает только стойку, кронштейн крыши не трогает. Это подтверждает механику точек фиксации, реализованную в инструменте. + + +### Инструменты + +| Задача | Инструмент | Статус | +|---|---|---| +| Штриховые виды как в инструкции | HLR из OCCT через OCP — проекции с видимыми и скрытыми линиями, вектор | ядро есть, модуль не задействован | +| Растр с тенями при необходимости | визуализация OCCT headless / three.js в безголовом браузере | резерв | +| Вектор в PDF | reportlab (собирает этот документ) + svgwrite | есть | +| Граф связей, порядок шагов | networkx + контакты деталей через BRepExtrema | есть | +| Шаблоны текста, DE/EN | Jinja2 + словари | добавить | +| Штрих-код EAN | python-barcode | добавить | +| Каталог крепежа DIN | своя таблица | добавить | + +Ключевой выбор — HLR вместо растра: инструкция сделана в штриховом стиле технической иллюстрации, HLR даёт ровно это в векторе для печати и считается на процессоре без графики. + + +### Что добавить в систему — четыре слоя + +1. Граф связей: какая деталь касается какой — из геометрии; из него порядок монтажа снизу вверх. 2. Модель соединений в product.json: пара деталей → крепёж, количество, позиция вдоль профиля, момент, фиксатор резьбы; из неё считаются 56 винтов, позиции сухарей и выноски — самый содержательный кусок, он же нужен для проверок зазоров и коллизий. 3. Серверный рендер по шаблонам видов: сверху, изометрия, разнесённый, шаг с подсветкой. 4. Генератор документа: шаблон страницы с рамкой, статичные блоки, подстановка данных, два языка. Сервер: HLR-проекция сборки — секунды на ядро; полная инструкция около полусотни картинок — минута-две; память — сотни мегабайт; GPU не нужен. Инструкция пересобирается на CI при каждом изменении изделия, как DXF. Что даёт сверх ручной работы: изменили число поперечин — пересчитались позиции сухарей, количества винтов, масса; Ranger получает инструкцию в день готовности геометрии; несоответствий между спецификацией и схемами не бывает по построению. + + +--- + +## 20. Два мира: B-Rep и сетка + +Уточнение по практике: автомобиль в работе — не STEP, а FBX от сторонних моделеров, часто с неверным масштабом и пропорциями. Сейчас багажник ставится на машину в Blender: STEP багажника конвертируется в GLB, масштаб машины подгоняется вручную. Система делится на два мира с разной точностью, и смешивать их нельзя. + +| | Мир B-Rep | Мир сетки | +|---|---|---| +| Объект | багажник, детали, крепёж | автомобиль, окружение | +| Формат | STEP (на границах), BREP (внутри) | FBX на входе, GLB на границе | +| Точность | производственная: 0.05 мм, три критерия приёмки | визуальная: сантиметры, калибруется | +| Назначение | производство, реверс, параметрика | картинка, примерка, грубые зазоры, конфигуратор | +| Инструмент | recada.py на OCCT | Blender (скриптуемый, MCP уже есть) | + + +### Следствия + +Точки крепления нельзя брать с модели машины — она неточная. Источник — собственные кронштейны, проверенные на реальной крыше: у Defender геометрия Halterung 1 и есть паспорт точек, снятый с металла. Логика разворачивается: не «машина задаёт багажник», а «багажник, который уже стоит, задаёт координаты машины». FBX подкладывается под паспорт для визуальной проверки. + +Масштаб FBX калибруется по заводским размерам — колёсная база, ширина, высота крыши. Один коэффициент записывается в паспорт автомобиля вместе с моделью и переиспользуется вместо подгонки на глаз при каждой примерке. + +Конвертация STEP → GLB без Blender: у OCCT есть собственный писатель glTF (RWGltf_CafWriter, доступен из OCP), сохраняет имена деталей через XCAF. GLB багажника получается из инструмента headless на сервере, со структурой и именами. + +Blender остаётся для того, что OCCT не умеет: чтение FBX, фотореалистичные рендеры для сайта, картинка машины в инструкции. Он скриптуем — примерка «взять GLB багажника, подставить откалиброванный FBX, отрендерить» автоматизируется через уже имеющийся Blender MCP. + +Граница форматов: STEP — внутри B-Rep-мира и на границе с производством; GLB — на границе с визуализацией и конфигуратором Unity. Защита данных: сторонние FBX-модели машин часто лицензированы на одного пользователя и запрещают распространение — на сервере они лежат в той же защищённой зоне, что чертежи, наружу в конфигуратор уходят только рендеры или упрощённые сетки. + + +--- + +## 21. Данные: источники, каталоги, единая структура + +По практике источников оказалось шесть, и им нужно одно место. Модель данных: два каталога — автомобили и аксессуары — с единой структурой «объект → представления с классом точности». Собственные детали в той же структуре: STEP, чертёж, ответ портала, паспорт обмера. Одна библиотека на всё. + + +### Источники по убыванию точности и доверия + +| Источник | Что даёт | Ограничение | +|---|---|---| +| Aufbaurichtlinien производителя (VW Aufbaurichtlinien, Ford Body and Equipment Mounting Manual, Stellantis Conversion Guidelines) | что с кузовом разрешено делать: нагрузка на крышу статическая и динамическая, несущая структура (B-стойки, дуги крыши), зоны крепления с координатами, запретные зоны (подушки, датчики, проводка), центр тяжести, моменты и классы крепежа, антикоррозионные требования, точки электрики | доступ через порталы для Aufbauhersteller после регистрации; версионируются по модельным годам | +| CAD от производителя (VW, Ford, Stellantis) | точная геометрия кузова, штатные точки, заводская система координат | NDA, лицензия; формат JT/CATIA требует конвертации (Inventor открывает JT) — один раз в STEP | +| 2D-чертежи производителя | номинальные размеры и допуски штатных точек — что гарантируется для всех машин; единственная официальная геометрия для старых (Defender) | DXF читается ezdxf; PDF — вручную, чисел десяток | +| STL-скан | реальная машина или аксессуар как есть: допуски производства, накладки, модификации | регистрация в общую систему координат, чистка шума и дыр | +| Кронштейны в металле | подтверждённые точки крепления | только там, где уже стоим | +| CAD аксессуаров от поставщика | STEP, масса, интерфейс крепления | не все дают | +| FBX | картинка, конфигуратор | масштаб условный, калибруется по заводским размерам | + +Aufbaurichtlinien выше OEM-CAD по одной причине: CAD показывает геометрию, а Richtlinien — что с ней разрешено делать. Половина правил валидатора уровня изделия получает официальный источник с номером страницы вместо нашей оценки. Ссылка на Richtlinien в документации изделия — позиция в споре о гарантии и в сертификации: «кронштейн стоит в зоне, разрешённой производителем, нагрузка в пределах указанной». Для покупателя — сохранение заводской гарантии на кузов; ISO 11154 плюс соответствие Richtlinien — комплект, который дилер примет. Извлечение: готовый ИИ вытаскивает разделы о крыше, стойках, нагрузках и зонах в JSON с указанием страницы, инженер проверяет числа; каждое правило паспорта хранит документ, версию, страницу. + + +### Единая структура: объект → представления + +| Представлен ие | Автомобиль | Аксессуар | Своя деталь | Роль | +|---|---|---|---|---| +| CAD | OEM | STEP поставщика | STEP из реверса или FreeCAD | номинал | +| 2D-чертёж | заводской | схема отверстий | чертёж из HLR | номинал с допусками | +| Скан | реальная крыша | реальный тент, канистра | — | реальность | +| Визуальная модель | FBX | GLB | GLB из OCCT | картинка | +| Подтвержден ие | кронштейны в металле | — | ответ портала, паспорт обмера | факт | + +Не у каждого объекта есть все представления — у старого Defender нет OEM-CAD, у чужого тента нет STEP. Система работает с тем, что есть, и знает класс точности каждого источника. + + +### Общая система координат — заводская + +OEM-файлы приходят в стандартной системе автомобиля: X вдоль машины, ноль по производителю. Это и есть общая система, которой не хватало: скан выравнивается по ней (ICP, Open3D), FBX масштабируется по ней, паспорт записывается в ней. Там, где OEM-CAD нет, систему задаёт скан. Один раз выровняли — все проверки в одном пространстве. + + +### Что открывают сканы — четвёртый критерий + +Проверка посадки: грань кронштейна, которая ложится на крышу, сравнивается со сканом той же метрологией — точки грани → расстояние до сетки. Карта прилегания: где лежит, где висит, где упирается. Это критерий монтажный, не производственный; ни Blechcon, ни портал его не дают. Зазоры в реальности: поперечина к крыше, нога к рейлингу, платформа к антенне — от B-Rep до скана, без FBX. Сценарий «двадцать миллиметров, руку не просунуть» проверяется на настоящей крыше. + +Скан против OEM-CAD — сама по себе информация: разбег у машин одной серии показывает, сколько регулировки должен иметь кронштейн. Продольные пазы Halterung 2 — ровно об этом. + +Граница: реверс из скана в B-Rep — задача другого порядка, чем из STEP; для простого кронштейна реально, для сложной отливки нет. Скан аксессуара — источник размеров и проверок, не исходник для производства; адаптер под чужой аксессуар проектируется в FreeCAD по числам со скана. + + +### Опция: реверс из STL + +Статус — опция, не план. Отличается от реверса из STEP принципиально: в STEP плоскости и цилиндры уже заданы, в STL только треугольники — плоскость надо найти и подобрать, цилиндр гиба распознать по кривизне, отверстие восстановить по границе, каждый шаг с погрешностью скана. + +Для листового металла реально: две поверхности — плоскости и цилиндры — лучший случай для распознавания (сегментация по нормалям, RANSAC, подбор цилиндра). Получаются те же числа, что даёт обмер: толщина по расстоянию между параллельными плоскостями, радиусы, углы, отверстия, контур. Ключевое решение: реверсить не в B-Rep, а в параметры, а геометрию строить заново генератором — сшивать подобранные поверхности в тело не нужно, тело строит генератор, который уже проходит три критерия. Совпадает с принципом «истина — параметры». + +Ограничения: качество скана (структурированный свет 0.05–0.1 мм, бытовые хуже; кромки скругляются, отверстия обрастают); лист снимать с двух сторон, иначе толщина — предположение; литьё, вытяжка, свободные поверхности — вне метода, там NURBS уровня Geomagic. Инструменты: Open3D, pyransac3d, scipy, далее наш генератор — всё открытое. + +Где нужно: чужие детали без CAD; детали клиента под его крепления; и главное — входной контроль: скан детали от Blechcon против нашего STEP той же метрологией. Реверс наоборот — не восстанавливаем, а проверяем. Пятый критерий: приёмка поставки. + + +### Аксессуары: вторая ось с данными + +Интерфейс крепления описывается один раз для платформы (шаг пазов, сечение профиля) и один раз для аксессуара (в T-паз, на airline-рейку, на кронштейн, крепёж). Дальше автоматически: аксессуар в конфигураторе показывается только там, где физически встанет; масса добавляется к нагрузке на крышу (нагрузка производителя минус багажник минус аксессуары); габарит проверяется на коллизии с кронштейнами и дефлектором; попадает в инструкцию отдельным шагом; тент на переднем краю — отдельный прогон CFD. Инструменты для сеток: OCCT читает STL (RWStl); для тяжёлых сканов — trimesh для расстояний точка–сетка, Open3D для регистрации и чистки. Всё на процессоре; память под скан в 300 МБ — несколько гигабайт на время обработки, EX44 достаточно. + +Защита: OEM-файлы под NDA — самое чувствительное на сервере; их утечка — нарушение договора с производителем. Пиксели наружу, личные ключи, запрет скачивания для внешних — именно для них. OEM-геометрия никогда не уходит в конфигуратор и рендеры — там только багажники и FBX. + + +--- + +## 22. Конкуренты как источник данных + +Конкуренты публикуют фото, PDF инструкций, размеры и массу. Всё это можно использовать — легально, пока анализируется, а не копируется геометрия. Конкурент — тот же объект библиотеки с представлениями низкой точности: инструкция как документ, извлечённая спецификация, габариты, масса, фото. + +| Что | Как | Что даёт | +|---|---|---| +| Структурный бенчмарк | из PDF инструкций: число деталей, крепёжных позиций, шагов, время монтажа, тип ног (одна или две детали), решение углов, задействованные точки крыши | таблица по конкурентам на одну машину: где проще, где сложнее | +| Подсказки для паспорта | инструкция показывает, куда конкурент крепится: водосток, рейлинг, штатные резьбы | подтверждение точек там, где OEM-данных нет | +| Метрики эффективности | масса на м² платформы, высота над крышей, масса на точку крепления — в тех же единицах, что считает наша система из BOM | аргумент в карточку товара и цель для конструктора (55 мм против 70 у Front Runner) | +| Фото | фотограмметрия — грубая сетка | только габариты, пропорции, обтекаемость; не для реверса | +| Извлечение из PDF | готовый ИИ: PDF на входе, структурированный JSON на выходе, проверка глазами | спецификации конкурентов в базе, сравнения автоматически | +| Уроки DFM | как решают разгрузки, углы, стыки профилей | смотреть и учиться, не повторяя | +| Патентный поиск | по механизмам крепления на крышу до проектирования | недорогая страховка от претензии | + +Граница: публичные документы анализировать можно; воспроизводить геометрию, отличительные формы и узлы под патентом или зарегистрированным промышленным образцом — нет. Ценность — не повторить их багажник, а вывести свой быстрее и точнее. + + +--- + +## 23. Проверки уровня изделия: размещение, доступность, высота, настройка + +Уровень изделия — не геометрия детали, а багажник на конкретной машине. Каждый вопрос «куда можно ставить», «достанет ли человек», «влезет ли в паркинг» превращается в правило валидатора: число, источник данных, порог. Каждая настройка — параметр product.json, каждый параметр тянет проверки заново. + + +### Куда можно ставить + +В паспорте машины — зоны. Разрешённые: водосток, рейлинги, штатные резьбы, с допустимой нагрузкой на точку от производителя. Запрещённые: люк, антенна, плавник, датчики систем помощи водителю, камера, стоп-сигнал, зона качания задней двери. Положение вдоль крыши проверяется против зон автоматически: сдвинули вперёд — валидатор сообщает, что передняя нога зашла в зону датчика дождя. Источники: OEM-CAD и чертежи для зон, скан для реальной машины, инструкции конкурентов как подтверждение точек. + + +### Правила валидатора уровня изделия + +| Проверка | Что считается | Порог / источник | +|---|---|---| +| Зоны крепления | ноги и кронштейны внутри разрешённых зон, вне запрещённых | паспорт машины (OEM, чертежи, скан) | +| Нагрузка на крышу | производитель − масса багажника − аксессуары; −50 % на бездорожье | заводская допустимая нагрузка; из инструкции Airholder | +| Нагрузка на точку | распределение массы по точкам крепления | допустимое на точку от производителя | +| Двери и задняя дверь | дуга открывания против ног и заднего свеса платформы | кинематика двери из OEM или замер; полное открывание обязательно | +| Высота для паркинга | высота машины + багажник + самый высокий аксессуар | 1.85 / 1.90 / 2.00 / 2.10 м; показывать в конфигураторе для каждой конфигурации | +| Загрузка с земли | высота нижней кромки платформы от земли | антропометрия DIN 33402; при превышении — ступенька из аксессуаров | +| Дотягиваемость | до центра платформы сбоку по ширине и высоте | DIN 33402 | +| Зазор до крыши | низ поперечины ↔ крыша, нога ↔ рейлинг, платформа ↔ антенна | B-Rep ↔ скан или OEM; сценарий «20 мм, руку не просунуть» | +| Видимость | обзор через заднее стекло, камера заднего вида, антенна | луч обзора из OEM-положения зеркала | +| Аэродинамика | дефлектор, шумовые кромки, частоты срыва по Струхалю на поперечинах | формулы в валидаторе; CFD — разово | +| Центр тяжести | высота ЦТ машины с нагрузкой на крыше и аксессуарами | предел из Aufbaurichtlinien; для высокого фургона с палаткой — ограничивающий фактор | +| Динамика ISO 11154 | город, экстренное торможение, боковой удар — расчётно | до испытаний в лаборатории | +| Совместимость аксессуаров | интерфейс, положение, масса, коллизии с кронштейнами и дефлектором | каталог аксессуаров | + +Каждое правило хранит источник со ссылкой — документ, версия, страница. Для зон крепления, нагрузки на крышу, нагрузки на точку, центра тяжести и крепежа источник — Aufbaurichtlinien производителя; для зазоров и посадки — скан и OEM-CAD; для доступности — DIN 33402; для динамики — ISO 11154. В конфигураторе покупателю показывается: «соответствует требованиям производителя к надстройкам». + + +### Что регулируется без перепроектирования + +Положение багажника вдоль крыши в пределах разрешённых зон; высота над крышей; шаг и число поперечин под груз; положение аксессуаров с проверкой совместимости; наличие и ориентация дефлектора. Всё это параметры product.json — меняются числом, ноги и кронштейны перегенерируются, проверки прогоняются заново, инструкция и DXF пересобираются. + +Для покупателя в конфигураторе: полная высота с выбранными аксессуарами и вердикт по паркингу — до покупки, а не в паркинге. Это продолжение того, что уже есть в карточке T6 (2045 мм), только для каждой конфигурации автоматически. + + +--- + +## 24. Дизайн продукта: что даёт система + +Всё предыдущее — про то, как деталь попадает в производство. Этот раздел — про то, каким должен быть сам багажник, и где система даёт конструктору не удобство, а новые решения. Три вещи опираются на данные, которые уже собираются: общность деталей между машинами, диапазон регулировки из разброса сканов, время монтажа из графа связей. Остальное — конструкторские правила, каждое кладётся в валидатор одной строкой. + + +### Для покупателя + +Время монтажа как проектный показатель. Сейчас 2–3 часа. Каждый шаг инструкции — минуты; сумма считается по графу связей. Система показывает, как предсобранные узлы её сокращают: нога в сборе с завода (Halterung 1 + 2 + подкладка, четыре винта) вместо двух деталей у клиента. Цель — час. Каждое изменение конструкции сразу видно во времени монтажа, а не после первой жалобы. + +Один инструмент на всю сборку. Сейчас M8 и M6 — два ключа. Весь крепёж под один размер: инструкция короче, ошибок меньше, в коробку кладётся один ключ. Правило валидатора: число различных размеров крепежа в изделии — целевое значение 1, допустимое 2. + +Сценарии груза. Палатка, велосипеды, каяк, канистры, ящики — у каждого своё требование к шагу поперечин, точкам крепления и распределению массы. Покупатель выбирает «что вожу», конфигуратор предлагает число поперечин, их позиции и аксессуары; валидатор проверяет нагрузку и высоту для паркинга с этим грузом. Сценарий груза — часть product.json. + +Высота и паркинг — уже в разделе 23. Добавляется: показывать покупателю в конфигураторе полную высоту с выбранным грузом и аксессуарами, не только с пустым багажником. + + +### Для конструктора + +Семейство ног вместо ног под каждую машину — самое сильное, что даёт библиотека. Анализ общности деталей между машинами: если стойка Halterung 2 с регулировкой пазом покрывает и Defender, и Ranger, а различается только кронштейн крыши Halterung 1 — это в разы меньше артикулов, оснастки у Blechcon и складских остатков. Система считает общность автоматически, когда есть паспорта нескольких машин: для каждой детали — на скольких машинах она применима без изменений, с растяжением, с другим кронштейном. Целевой показатель: доля общих деталей в новом изделии. + +Диапазон регулировки из разброса. Скан против OEM-CAD даёт, на сколько реальные машины отличаются от чертежа: допуски кузова, накладки, просадка. Длина паза в стойке и запас по высоте кронштейна должны покрывать этот разброс с коэффициентом — число, а не интуиция. Чем больше сканов одной модели, тем точнее диапазон. Правило: регулировка ≥ 2 × измеренный разброс. + +Прочность и масса вместе. Планируемый CalculiX закрывает перебор толщины и формы ноги под нагрузку: где можно 2.5 вместо 3.0 мм, где нужны рёбра или отбортовка, где отверстия ослабляют. Масса на квадратный метр платформы — целевой показатель против конкурентов; модальный анализ — против дребезга на поперечинах. + +Общая высота профиля как конкурентное поле: 55 мм у Airholder на T6 против 70 у Front Runner. Держать ниже конкурента — правило валидатора для каждой новой машины. + + +### Для эксплуатации + +Гальваническая пара. Нержавейка 1.4301 на алюминии в мокрой среде с солью — коррозия на стыке через сезон. Резиновые подкладки Gummiunterlage у вас есть; правило валидатора: между разнородными металлами всегда изолятор, и он обязан присутствовать в BOM на каждом контакте. Проверяется по графу связей: контакт сталь–алюминий без изолятора — ошибка. + +Расходники и запчасти. Подкладки, заглушки, крепёж изнашиваются и теряются. BOM с именами и номерами позиций из инструкции делает возможным заказ отдельной детали по номеру — сервисная выручка, которой сейчас нет, и удержание клиента. Каждая позиция BOM — потенциальный артикул в Shopify. + +Защита от кражи — гайки с секретом на кронштейнах крыши как аксессуар: спрашивают, стоит копейки, повышает средний чек. + +Обслуживание после бездорожья: инструкция содержит проверку затяжки; система может выдавать карту контрольных точек — какие винты проверить и с каким моментом — из той же модели соединений. + + +### Для бренда и прослеживаемости + +Серийный номер и QR-код на изделии: ведёт на инструкцию именно этой конфигурации, регистрацию гарантии, заказ запчастей. Прослеживаемость по ISO 9001 из коробки: серийник → product.json → партия у Blechcon → ответ портала. Отзыв партии при дефекте — по базе, а не по памяти. + +Фирменные элементы как параметрика: логотип уже вырезается лазером — деталь U5-Logo в сборке. Логотип, форма углов, рисунок дефлектора ведутся в той же системе и перегенерируются под размер изделия, сохраняя узнаваемость. + +Разборность и переработка: алюминий и нержавейка разделяются без инструмента — для немецкого рынка заявляемое преимущество; система может считать долю материалов по BOM и подтверждать разделяемость по графу связей (нет клеёных и неразъёмных соединений разнородных металлов). + + +### Что из этого системное + +| Решение | На чём основано | Показатель | +|---|---|---| +| Семейство ног | паспорта нескольких машин, библиотека | доля общих деталей в новом изделии | +| Диапазон регулировки | скан против OEM, разброс по машинам | регулировка ≥ 2 × разброс | +| Время монтажа | граф связей, минуты на шаг | цель 1 час; предсборка узлов | +| Один инструмент | BOM | размеров крепежа: 1, допустимо 2 | +| Гальваническая изоляция | граф связей + материалы | контактов сталь–алюминий без изолятора: 0 | +| Масса и прочность | BOM + CalculiX | кг/м² ниже конкурента; запас по ISO 11154 | +| Высота профиля | геометрия | ниже конкурента на той же машине | +| Прослеживаемость | серийник → product.json → партия → портал | отзыв по базе | + +Общий принцип: каждое дизайнерское решение, которое можно выразить числом, становится правилом валидатора и целевым показателем. Тогда следующая машина проектируется не «как получится», а против измеримых целей, и отклонение видно до производства. + + +--- + +## 25. Обратная связь из поля: рекламации, опыт покупателей, соцсети + +До сих пор обратная связь шла от портала и от испытаний. Этот раздел — про сторону после продажи: от дороги и людей. Три источника, у каждого своя механика; все они — ещё два представления в библиотеке у изделия и у машины: «полевые данные». Цикл тот же, что с порталом: поле → библиотека → правило → валидатор → следующая ревизия product.json. + + +### Рекламации — как ответы портала, только про эксплуатацию + +Каждая жалоба — структурированная запись: машина, конфигурация по серийному номеру, деталь, вид отказа (ослаб винт, коррозия на стыке, гул на скорости, не встало на крышу), фото. Серийник ведёт к product.json, к партии у Blechcon, к ответу портала. Отсюда: частота отказов по детали и по машине; корневая причина по цепочке — конструкция, партия или монтаж клиентом; правило в валидатор: три жалобы на ослабление одного винта — в инструкцию момент затяжки и фиксатор, в модель соединений — требование. Ровно как отказ портала стал критерием приёмки. Это CAPA-цикл ISO 9001 с данными вместо папки; стоимость отказа по видам определяет, что чинить первым. + + +### Опыт покупателей — прямо из инструкции + +QR-код на каждом шаге: «шаг непонятен», «заняло дольше», «деталь не подошла». Время монтажа, ставшее проектным показателем в 24, измеряется реальными людьми, а не оценкой по графу. Шаг, на котором спотыкаются все, переделывается: предсборка узла или другая картинка. + + +### Соцсети — то, о чём не пишут в жалобах + +| Что показывают | Что это значит | Куда идёт | +|---|---|---| +| Самодельные крепления канистр, фонарей, лопат | спрос на аксессуары, которых нет | каталог аксессуаров, приоритет разработки | +| Где сверлят платформу | штатного интерфейса не хватило; каждое чужое отверстие — недостающая позиция паза или аксессуар | интерфейс платформы, каталог | +| Как грузят | реальные сценарии груза и распределение массы | сценарии груза в product.json (15и) | +| На каких машинах | багажник на машине, для которой не делали — спрос на программу | очередь вывода машин | +| Что ломается и как чинят | рекламации, которые не дошли | библиотека отказов, валидатор | + +Механика сбора: посты по хэштегам и упоминаниям; готовый ИИ читает и раскладывает по меткам — аксессуар, машина, модификация, отказ, сценарий. Не аналитика больших данных, а еженедельная сводка: «12 постов с самодельным креплением лопаты за месяц». Плюс материал для маркетинга с разрешения автора. + + +### Шестой критерий — полевой + +Критерии приёмки выстраиваются в цепочку: метрология, целостность, распознавание порталом — производство; посадка по скану — монтаж; входной контроль поставки; и полевой — изделие приняли не Blechcon и не лаборатория, а дорога и люди. По серийникам видно, какая ревизия стоит у кого; отзыв и улучшение — адресные, а не всей серии. + + +--- + +## 26. Модель версий: уроки Onshape + +Источник — статья «Under the Hood: How Collaboration Works» Ильи Барана, VP по архитектуре Onshape (единственный CAD с Git-подобным ветвлением). В ней описан принцип, к которому мы пришли сами — «истина — определение, геометрия — артефакт», — доведённый до конца за десять лет. Onshape как продукт нам не подходит (облако PTC в США), но модель данных берём. + + +### Как устроено у Onshape + +Три типа данных разделены жёстко: состояние интерфейса не хранится; определение — список операций, имена, импорт — единственное, что лежит в базе; результаты перестроения — B-Rep, треугольники, ошибки — кэшируются и всегда перестраиваются из определения. + +Микроверсия: определение — вечный неизменяемый объект; любое изменение создаёт новую микроверсию со ссылкой на родителя и самим изменением («глубина Extrude 1 → 4 дюйма»). Вся эволюция хранится, любое состояние восстанавливается. + +Устойчивые изменения — ключевая находка: изменение адресует операцию по внутреннему постоянному идентификатору, не по позиции; пока исходная операция существует, изменение применимо к другой микроверсии. Поэтому правки нескольких людей ложатся на последнюю версию, не мешая друг другу. + +Ветвление: рабочее пространство — указатель на последнюю микроверсию; микроверсии образуют дерево. Слияние — найти изменения из «откуда», которых нет в «куда», создать микроверсию с ними. Сознательное отличие от Git: конфликтов не бывает — при разном изменении одного размера побеждает «откуда», пользователь никогда не блокируется. Отмена — не откат, а применение обратного изменения поверх последней версии: моё отверстие исчезает, чужое скругление остаётся. Compare — сравнение версий на тех же неизменяемых микроверсиях. + + +### Что берём + +| У Onshape | У нас | Статус | +|---|---|---| +| определение / результат | project.json / STEP-артефакты | есть; формализовать: STEP — кэш, не истина; в Git только для релизов | +| микроверсия с родителем и дельтой | плоский журнал | сделать: каждая операция — объект с хешем родителя | +| адресация по постоянному ID | отверстия по центру, стенка по нормали и площади | частично; довести до правила для всех операций | +| переигрывание на новом родителе | нет | главное: журнал применяется к новой ревизии исходника — Ranger-кронштейн v2 получает те же правки | +| ветки и слияние без конфликтов | нет | на уровне журнала, правило «источник побеждает» | +| сравнение версий | нет | геометрический diff: грани добавлены, удалены, сдвинуты — подсветкой | +| тесселяция по требованию, кэш | кэш Streamlit | формализовать: кэш по хешу микроверсии | +| FeatureScript | генератор на Python/OCCT | подтверждение абстракции; ничего менять | + + +### Где у нас сложнее + +У Onshape своё ядро, и оно ведёт постоянные идентификаторы граней через перестроения. У OCCT их нет — это известная проблема топологического именования, на которой FreeCAD бился годами. Мы адресуем элементы геометрическим отпечатком: тип, нормаль, центр, площадь. Для листовых деталей это работает — так уже находятся отверстия и стенки. Универсально — нет; это признано. Ещё две UX-идеи Onshape для нашего просмотрщика: синхронный показ развёртки и согнутой детали (клик по грани в одном окне подсвечивает её в другом) и визуальное сравнение ревизий детали. + + +--- + +## 27. Инфраструктура: решения по железу + +Почему Германия — самые дешёвые серверы мира: промышленные энергоконтракты + free cooling, DE-CIX (почти бесплатный трафик → unlimited), ЦОДы в дешёвой провинции при толстой оптике, культура ценовой конкуренции (Hetzner/netcup — семейные компании без инвесторов), вертикальная интеграция с десктопным железом. AWS отвергнут: 6–8× цена инстанса, egress ~0,09 $/ГБ убивает VDI-потоки (у Hetzner unlimited), CLOUD Act против data sovereignty, сложность без пользы. Совместимость бесплатна (Docker переезжает за день), зависимость не нужна. + +| Роль | Решение | Статус/цена | +|---|---|---| +| Целевой сервер (ядро+VDI 8–10 сессий) | Hetzner EX44: i5-13500 (быстрый однопоток — ключ к отзывчивости), 64 ГБ, 2×512 NVMe RAID 1, unlimited, FSN1 | €70,21, setup 0, почасово; Limited-волны — подписка на availability, правило 15 секунд на заказ | +| Параллельный канал | Server Auction (Robot → Server Market): запросы «i5-13», «i5-12», «i7-12»; NVMe, не HDD; ничего старше 12-го поколения | критерий: ≤ €75–80 | +| План Б (через ~3 недели) | OVH RISE-S: Ryzen 9700X (лучший однопоток), 64 ГБ, Frankfurt/Strasbourg, месяц-к-месяцу | €78 + €78 setup | +| Отвергнуто | AX41 (Zen 2 2019 за те же €70), AX42 (€118 за 35-Вт чип), EX63 (€177 — переплата за ядра), i9-13900 аукцион (€165 — рано), Contabo (CPU steal) | — | +| Масштаб (этап 5, 10+ сессий) | OVH RISE-L (9950X, 128 ГБ, €179) — вытеснил AX102 (€308); i9 с аукциона | по факту роста | +| Уже есть | ThinkPad i7-8850H/32ГБ/Quadro P1000 — рабочее место + локальный старт этапа 1 (Git, build123d, FreeCAD; диск 54 ГБ свободно — Docker позже); Strato VPS 2/2/90 — Gitea+WireGuard и/или вторая копия бэкапа; Hetzner CPX — CI | 0 € новых | +| Второй провайдер | netcup: RS G12 (выделенные EPYC-ядра, ECC, дёшево) — бэкап сейчас (~5 €), в будущем возможное отдельное ядро (Gitea+CI) при VDI на десктопном CPU; EPYC-однопоток слаб для VDI | ~5 €/мес | + +Три входа Hetzner: accounts (паспорт) → Cloud Console (облачные CPX) и Robot (dedicated: заказ, rescue, Server Market). EX44 после поставки: rescue → installimage → Ubuntu 24.04 LTS + RAID 1 → SSH-ключ, ufw (наружу только SSH+WireGuard), unattended-upgrades, бэкап в Storage Box (~4 €) + вторая копия у netcup. Домен: поддомены git./work./cdn. существующего домена (или отдельный технический ~15 €/год) — Let's Encrypt, стабильность при переездах, разделение контуров; рабочие поддомены могут резолвиться только внутри WireGuard. Авторизация — три непересекающихся контура: рабочий (локальные аккаунты Gitea+Kasm + TOTP 2FA + WireGuard-периметр; SSO/Keycloak после 20 человек), клиентский (Shopify Customer Accounts / Google — конфигуратор наследует), сообщество (OAuth GitHub/Google в публичной Gitea). Бюджет полной системы при 100 зарегистрированных (пик 20–25 сессий): ~150–200 €/мес. + + +### Стоимость владения: только open source + +| Категория | Инструменты (все бесплатны, без подписок) | +|---|---| +| CAD и генерация | FreeCAD + SheetMetal + аддоны (GPL/LGPL), build123d/CadQuery (Apache) | +| Версии и сервер | Git, Gitea, Git LFS, zippey; Ubuntu, Docker (self-hosted), WireGuard, ufw; Let's Encrypt; Cloudflare free (CDN) | +| Расчёты | CalculiX + Gmsh (статика, модальный), OpenFOAM (CFD разово) | +| Рендер и артефакты | Blender headless, SheetNest (раскрой), gltfpack/Draco (GLB) | +| VDI | Kasm Community Edition (до 5 одновременных сессий); без лимитов — Apache Guacamole / webtop-образы linuxserver.io | +| Визуализация | Mermaid/Graphviz (граф зависимостей), React Flow/Rete.js (composер, этап 5), Sverchok (нодовое моделирование) | +| Конфигуратор | Unity Personal — бесплатен до $200k годовой выручки с продукта; единственный инструмент с порогом; бесплатная альтернатива при необходимости — three.js/Babylon.js | + +Платные строки проекта — только три, все приняты осознанно: серверы (~70–200 €/мес по росту), токены Claude/API (~20–50 €/мес на старте), физический мир (FDM-принтер разово 300–500 €; испытания ISO 11154 — разовые тысячи €). Правило проекта: инструмент входит в стек, только если он open source или бесплатен для нашего масштаба без подписки; платными могут быть только вычислительные ресурсы и физические вещи; кандидат на платный инструмент требует явного решения владельца с бесплатной альтернативой в кармане. + +Claude как копилот — четыре слоя: (1) Claude Code на сервере — админ-диалог вместо ручного Linux; (2) MCP-FreeCAD headless — двигатель промпт-слоя; (3) в CI неинтерактивно (claude -p): упал валидатор → фикс коммитом в ветку → ревью человеком; перевод сданных STEP в черновики генераторов; (4) в VDI-образе подрядчика — отдельный API-ключ с лимитом и системный промпт RECADA (ядро невидимо). Правило: Claude ходит через те же двери, что люди (ветки, PR, валидатор), автор коммита «LLM via MCP» с промптом в сообщении. Бюджет 20–50 €/мес на старте. + + +--- + +## 28. Граф зависимостей конструкции и автоматизация + +Граф зависимостей — визуальная карта конструкции: узлы — файлы истины (паспорта, скелеты, генераторы, детали, сборки, конфигурации), стрелки — «читает из». Зависимости в системе объявлены явно, поэтому карта строится автоматически скриптом CI (Mermaid/Graphviz, бесплатно) — рисовать руками ничего не надо, схема всегда актуальна. Главный режим — анализ изменения: перед merge CI отвечает «что пересоберётся, если принять этот PR» — список и подсветка на схеме (красное = затронуто, отдельно помечаются released-детали). Каскад «что за что тянет» становится видимым ДО нажатия кнопки; это же — инструмент онбординга (новый человек понимает систему за час по карте) и диагностики (где в цепочке красный валидатор). Делается на этапе 2 рядом с валидатором. + +Автоматизация бизнес-событий — простыми скриптами и webhook в CI, без отдельного оркестратора: складской webhook Shopify (остаток А=0 → политика run-out → активировать Б → уведомление), онбординг (тест пройден → карточка исполнителя → письмо), мониторинг (релизный тег → сводка в канал; CI красный сутки → алерт), маркетинг (релиз → рендер → черновик карточки через LLM на утверждение), продажи как источник ТЗ (форма «нет крепления?» → issue + счётчик спроса → 3 запроса = приоритет в разработку). Граница неизменна: автоматизация не трогает геометрию и не пишет в main — всё, что меняет истину, идёт через PR и валидатор. Скрипт создаётся под событие, случившееся руками трижды. Нодовый composер поверх библиотеки кубиков (React Flow/Rete.js — открытые) — этап 5: интерфейс для не-программистов (дилер-CPQ, клиент с шаблоном); свой Grasshopper не делать (есть Sverchok/FreeCAD-ноды — бесплатные). + + +--- + +## 29. Финансовая модель — каркас + +Поставщики и данные для честной экономики: структурированный архив счетов Blechcon и тарифа GLS с первого заказа + pre-flight nesting (знать справедливую цену резки до запроса) + периодический ручной бенчмарк 1–2 европейских альтернатив + квалификация второго поставщика как страховки от монополии. Полный API-аукцион нескольких сервисов отвергнут: правила валидатора = правила конкретного производителя, несколько поставщиков = кратная поддержка DFM-правил. + +CM1 (маржа комплекта) = Цена − металл − Blechcon − профили − крепёж/покрытие − упаковка/доставка − комиссия канала CM2 (с учётом дизайна) = CM1 × ожидаемые продажи модели − (турниры + часы владельца × ставка + прототипы + паспорт) Операционная = Σ CM2 − постоянные (инфраструктура, время, маркетинг, склад) Ориентир отрасли (4x4-аксессуары): валовая 45–60% в рознице, 30–40% через дилеров; ниже ~35–40% от розницы — чинить до масштабирования. Ключевой вопрос системы: за сколько проданных комплектов окупается вывод новой машины. Для расчёта нужны 8 цифр: розничная цена комплекта и канал; счёт Blechcon за комплект ног (или масса); €/метр профиля и метраж; крепёж/уплотнители/покрытие; упаковка+доставка; текущие и плановые объёмы; внутренняя ставка часа владельца; текущая стоимость вывода машины по-старому. Метрика №2 включает доставку металла — юнит-экономика честная. + + +--- + +## 30. Управленческий слой: роли подробно + +Роли — это не должности, а зоны ответственности с чёткими правами решений; в малой команде один человек совмещает несколько ролей, но границы решений остаются письменными. Принцип: у каждого решения — один владелец; «решили вместе» = не решил никто. + +| Роль | За что отвечает | Права решений | Доступы | Время | +|---|---|---|---|---| +| Owner (вл аделец) | Стратегия и деньги; приоритеты трубы (что делаем следующим); критерии приёмки; доверие людям; отношения с Blechcon и ключевыми партнёрами; развитие системы (ворота этапов) | Единолично: релизная кнопка (тег), бюджеты трёх конвертов, повышения по лестнице, изменение интерфейса платформы, изменение released-деталей (go на issue), допуск инструментов в стек (FOSS-правило) | Полный: Git-admin, сервер root, Robot/Cloud Hetzner, Shopify, банк | Цель — сжатие до управления трубой; всё, что затягивает внутрь (рисовать, чинить CI), — сигнал незакрытой роли | +| Lead Reviewer | Первичная приёмка всех PR (геометрия, посадка, собираемость, экономика по BOM, эстетика); ведение исполнителей (ответы на вопросы к ТЗ, разбор отказов валидатора); формулировка ТЗ вместе с Owner; наставничество новичков; предложение правил в rules/ из повторяющихся ошибок | Merge в main для деталей (кроме изменений интерфейса и released — эскалация Owner); отклонение работ; распределение задач по скамейке; эскалация повышений | Git: merge-права на products/access ories; чтение generators и rules; Gitea-канбан; без root сервера | Первая делегируемая роль — решает bus factor: минимум раз в месяц проводит приёмку полностью без Owner. Отбор — по качеству мышления в комментариях, не по скорости черчения (принцип Питера) | +| Senior Engineer | Сложные узлы и новые типы деталей (топология); перевод «песочница → код» (реверс в генераторы); развитие библиотеки кубиков; калибровка FEM по эталонным отчётам; сложные паспорта (windshield-поверхности) | Коммиты в generators/ через PR; создание веток convert/*; предложение новых кубиков; техрешения внутри ТЗ без согласования | Git-доступ (режим Б: локальная работа + push), выбранные репозитории; VDI по желанию; без прав merge в main | Вырастает из проверенных по логам (устойчивое качество + коммуникация); финальное доверие — личное решение Owner | +| Engineer ( проверенн ый) | Прямые задачи по готовым шаблонам и ТЗ: адаптеры, вариации ног, паспорта аксессуаров по чек-листу; печатные примерки своих деталей | Работа в своей ветке; сдача через PR; вопросы к ТЗ приветствуются (логируются как сигнал роста) | VDI-сессия с расширенным набором репозиториев; либо локально + Git по решению Owner | 3+ принятых работы; сдельная оплата выше новичка; очередь на задачи | +| Engineer (новичок) | Тестовые и простые задачи строго по ТЗ; работа только в изолированной VDI-сессии | Только своя ветка своей задачи; ничего кроме | VDI-сессия: один репозиторий задачи, egress только Gitea, водяной знак, без скачивания | Вход свободный, платный тест с автоприёмкой; риск компании — сотни евро | +| Ops (инфр аструктур а) | Сервер, Docker, Gitea, Kasm, WireGuard, бэкапы (+ квартальный тест восстановления!), обновления с фиксацией версий, мониторинг, онбординг/оффбординг аккаунтов (одна процедура: ключи, сессии, права) | Плановые обновления и правки конфигурации; экстренные действия при инцидентах с пост-отчётом Owner; НЕ решает про доступы людей (это Owner) | Root сервера, admin Gitea/Kasm; без прав в конструкторские репозитории по содержанию | 10–20% времени одного человека; на старте совмещает Owner (+Claude Code как копилот), позже — первый кандидат на ау тсорс/частичну ю занятость | +| Технолог Blechcon (внешняя) | Производственное ревью выпускаемых деталей («если гнуть иначе — на 15% дешевле»); консультации по граничным случаям DFM | Рекомендательные; замечания конвертируются в правила rules/ через PR (цикл CAPA) | Получает только конкретные DXF/STEP по заказу; доступа в систему не имеет | По договорённо сти, возможно платно; дешёвый канал чужого произво дственного опыта | + + +### Правила совмещения и роста + +На старте: Owner совмещает Lead Reviewer и Ops (с Claude как копилотом обеих ролей), Senior — по мере появления. Порядок делегирования: сначала Lead Reviewer (снимает приёмку — главный пожиратель времени Owner и единственное лекарство от bus factor), затем Ops, затем постановка ТЗ. Несовместимость: автор детали не может быть её единственным ревьюером — подпись на released всегда чужая; Ops не решает про доверие и доступы людей. Каждое повышение = расширение доступов по гибридной модели (VDI-only → +репозитории → Git локально → merge-права) и фиксируется в карточке исполнителя в Gitea. Оффбординг любой роли — одна процедура: отзыв WireGuard-ключа, закрытие сессий, ревизия прав, чек передачи незавершённого. + + +### Ритм и бюджет (без изменений) + +Релизный такт 2 недели; еженедельный 30-мин обзор трубы (канбан); месячный бизнес-срез (юнит-экономика, метрики); квартальные ворота со стоп-лоссами. Бюджет — три конверта: Infra (~50–150 €/мес фикс), Design (N деталей × целевая стоимость), System-dev (только под ворота); часы Owner — всегда отдельной строкой по внутренней ставке. + +| № | Метрика | Смысл | +|---|---|---| +| 1 | Время «задача → released» | скорость трубы | +| 2 | Полная стоимость released-детали (исполнители + часы владельца + прототипы + доставка металла) | честная цена дизайна | +| 3 | Доля прохода валидатора с 1-го раза | качество пула и ТЗ | +| 4 | Время добавления новой машины | главная метрика системы | +| 5 | Доля выручки от деталей без участия владельца в рисовании | освобождённость владельца | + + +--- + +## 31. Материалы и технологии + + +--- + +## 32. Внешние ресурсы: что берём и чего не берём + + +--- + +## 33. Скорость: что ещё ускоряет вывод багажника + + +--- + +## 34. Удалённые столы: способы, выбор, география + +Сервер EX44 есть; удалённый инженер работает на нём, данные не покидают сервер. Выбор способа зависит от того, что отдаётся: весь стол, одно приложение или только 3D-окно. + +| Способ | Что это | Плюс | Минус | +|---|---|---|---| +| xrdp | Linux-стол по RDP, клиент встроен в Windows | самый экономный, терпим к задержке, нулевая настройка у клиента | 3D идёт картинками, вращение вязкое | +| Guacamole (Apache) | шлюз RDP/VNC/SSH через браузер | открытый, бесплатный без ограничений, запись сессий, 2FA, запрет буфера и файлов | сам ничего не запускает — нужен стол | +| Kasm Workspaces | стол в одноразовом контейнере на пользователя, в браузере | полная изоляция, водяные знаки, запреты встроены | тяжёлый; бесплатен до 5, дальше лицензия | +| Selkies | WebRTC-стол в браузере, открытый | видеопоток, плавное 3D | настройка, кодирование ест CPU | +| Xpra | одно приложение как отдельное окно у клиента | экономнее полного стола | — | +| NoMachine | протокол NX | очень эффективный | закрытый, лицензия для компании | +| Sunshine + Moonlight | игровой стриминг | минимальная задержка | под GPU; без него грузит CPU | +| Стриминг вьюпорта OCCT | только 3D-окно (15в) | легче и защищённее всех | только просмотр и выбор | + +Guacamole против Kasm: Guacamole — дверь к столу, Kasm — стол в контейнере. Для доверенной команды до пяти человек — Guacamole с xrdp: проще, легче, бесплатно. Для недоверенных подрядчиков — Kasm: изоляция по контейнеру. Не исключают друг друга; Kasm в резерве до первого внешнего исполнителя. + +Рекомендуемая схема: WireGuard (личный ключ) → Guacamole (2FA, запись, буфер выключен) → xrdp-стол с FreeCAD, инструментом и Blender. Всё открытое, всё на EX44, стоимость ноль. Для интерактивного Blender с тяжёлыми сценами без GPU тесно — рендеры пакетно ночью. + + +### География и задержка + +Скорость определяет физика, не софт: Восточная Европа и Турция 30–50 мс, Северная Африка и Ближний Восток 60–90, Индия 130–180, Юго-Восточная Азия 180–220, Латинская Америка 200–250. До 80 мс — как местная работа; 100–150 терпимо; выше 150 — каждый клик с запозданием, 3D мучительно. Софт вносит единицы миллисекунд; протокол — заметно: RDP и Xpra передают команды интерфейса и терпят задержку, Kasm и Selkies стримят видео и на далёком канале мутнеют. Для дальних стран — Guacamole с RDP. Важнее обоих — канал подрядчика: проводной от 10 Мбит с ровной задержкой, проверять тестом до найма. Второй сервер ближе к подрядчику не поможет: данные в Германии, переносить их — та утечка, которую исключили. + +> Комфортная зона по задержке — Восточная Европа, Балканы, Турция, Кавказ, Северная Африка: любой протокол, полноценная работа с 3D. Индия и Юго-Восточная Азия — для задач без активного вращения: параметры, проверки, библиотека, инструкции. Совпадает с раскладом ставок раздела 9. + + +--- + +## 35. Найм: три профиля и тесты самой системой + +Система меняет профиль того, кого искать: форму задаёт владелец, инструмент считает, проверяет и упаковывает. Три уровня — три разных найма. + +| Профиль | Что делает | Уровень | Где и как | +|---|---|---|---| +| Инженер-оператор системы | гоняет детали через реверс и портал, ведёт библиотеку и паспорта, переносит Aufbaurichtlinien в JSON, собирает пакеты для Blechcon, правит инструкции | младший или средний по листовому металлу, знающий гибку руками | постоянный, география до 80 мс; недорог — правила в валидаторе, не в голове | +| Конструктор аксессуаров | создаёт то, чего нет: интерфейс между чужим объектом (бокс, тент) и платформой; варианты концепции с компромиссами; нагрузки, вибрация, ветер, доступ руками | средний или старший с выпущенными в серию изделиями (aftermarket, outdoor, крепёжные системы); листовой металл и пластик | постоянный при потоке аксессуаров или проектный подряд на изделие; Польша, Чехия, Турция, Сербия — бюро aftermarket | +| Разработчик Python/OCCT | расширяет инструмент: правила, каталог разгрузок, стриминг, интеграции | средний; может вырасти из оператора | один, не сразу | + +Система сужает, что должен знать даже опытный конструктор: интерфейс платформы — данные, DFM — в валидаторе, цена — в контуре, библиотека даёт проверенные заготовки. Опыт тратится на концепцию и компоновку; опытный становится в разы быстрее, средний работает как опытный. + + +### Что предлагать + +Рабочее место без затрат с их стороны — стол через Guacamole, никаких лицензий и железа кроме ноутбука и проводного интернета (для страны, где SolidWorks стоит годовую зарплату — аргумент). Понятная система с правилами: продуктивен с первой недели. Рост по цифрам журнала, не по симпатии (раздел 8). Современный стек: OCCT, Python, Git, параметрика. Стабильная оплата в евро, долгосрочно, владение своим участком. + + +### Как тестировать — самой системой, экзамен и рабочее место совпадают + +| Тест | Что проверяет | +|---|---| +| Реверс с ловушкой | STEP → реверс → объяснить вердикты; одна деталь — реально отклонённая порталом: найдёт ли причину и предложит ли правку, а не нажмёт кнопки | +| Грамотность по листу | чертёж с намеренными ошибками: отверстие у гиба, нет разгрузки, нержавейка на алюминии без изолятора — сколько найдёт за 20 минут | +| Чтение Aufbaurichtlinien | кусок руководства → зоны и нагрузки в паспорт JSON с указанием страниц: аккуратность, языки, первоисточник | +| Честность | одна задача нерешаемая по условию: скажет «не сделать, потому что…» или нарисует что-то, чтобы сдать | +| Git-дисциплина | изменить параметр, закоммитить с внятным сообщением, объяснить дифф | +| Канал и пояс | замер задержки до сервера до разговора; выше 100 мс для роли с 3D — нет | +| Испытательный срок | месяц на реальных деталях; оценка по цифрам журнала — тем же, что определяют рост | +| Конструктор аксессуаров | оплаченная задача на неделю-две с вашим боксом: два-три варианта с компромиссами (масса, цена, время установки); выбранный проходит валидатор; посчитана цена; написан шаг инструкции; использованы библиотечные детали | +| Разработчик | одна задача: добавить правило валидатора с тестом (например, отступ отверстия от кромки) как pull request | + + +--- + +## 36. Права на результаты и коммерческая тайна + +Оговорка: не юридическая консультация; шаблон договора — у адвоката по немецкому и трансграничному праву. Каркас правильный вот такой. Половина этой системы уже построена — мы называли её защитой данных. + + +### Юридический слой — один раз, при подключении + +В Германии авторское право неотчуждаемо, но передаются исключительные права использования; изобретения — по отдельному режиму. Для внешнего подрядчика — рамочный договор, подписанный один раз: все результаты, созданные в системе, — с исключительными правами у RECADA; изобретения передаются и сообщаются; конфиденциальность; немецкое право, подсудность Аугсбург. Подпись квалифицированная электронная по eIDAS (Skribble или подобный) — работает через границу. Клик при входе как единственный договор — слабая позиция; клик как подтверждение уже подписанного — сильная. + + +### Технический слой — подтверждение при каждом входе + +Guacamole показывает условия перед сессией («работа в системе подпадает под рамочный договор от …») и пишет в лог: кто, когда, с какого ключа принял. Доказательство, что человек знал условия в момент создания. + + +### Происхождение результата — сильнее договора + +Журнал проекта пишет автора и время каждой операции; Git — автора каждого коммита; запись сессий Guacamole — что происходило на экране. Три независимых лога: через год не нужно доказывать, кто спроектировал кронштейн. Работа физически не покидала сервер — создана в системе; спор «сделал дома, принёс потом» невозможен. + + +### Коммерческая тайна — неожиданное следствие защиты данных + +> Немецкий закон о защите коммерческой тайны (GeschGehG) защищает секрет только при «надлежащих мерах по его охране». Пиксели наружу, личные ключи, запрет скачивания, запись сессий, договор при входе — это ровно те меры. Защита данных, построенная для другого, одновременно даёт правовую защиту библиотеки, паспортов и правил как коммерческой тайны. Без этих мер суд может сказать, что тайны не было. + + +### Про «идею» + +Идея сама по себе не охраняется нигде. Охраняется воплощение: чертёж и код — авторским правом, форма — промышленным образцом, механизм — патентом, остальное — как коммерческая тайна при тех самых мерах. «Подписывать идею» не получится; «всё, созданное в системе, принадлежит нам» — получится. + +Порядок: рамочный договор с электронной подписью → подтверждение при каждом входе с логом → работа только в системе → результаты только в Git компании → оплата привязана к приёмке. Для подрядчика в Турции или Польше адвокат добавит одну-две страницы про местное право. Доступ конструктора аксессуаров — стол с данными только его аксессуара и интерфейса, не вся библиотека; здесь Kasm уместнее Guacamole. + + +--- + +## 37. Монетизация: модели А, Б, В + + +### Продолжение: Модель В + + +### Продолжение: Модель В, четвёртый + + +--- + +## 38. Программа «Airholder Ready» + + +--- + +## 39. Поток вместо кодинга: измерить и уменьшить итерации + + +--- + +## 40. Управление проектом: доска по воротам вместо диаграммы Ганта + + +--- + +## 41. Датасеты CAD и CAD-агенты: что есть в мире и что из этого наше + + +--- + +## 42. Гибка листового металла в датасетах: BenDFM и KUL-bend + +Второй обзор, посвящённый именно листовому металлу, добавил четыре источника, которых в разделе не было, и уточнил BenDFM. Ниже — каждый источник подробно: что внутри, как получить, что с ним делать у нас, какие ограничения. Это важная часть: она превращает «свой датасет» из идеи в план с конкретными входами. + + +### BenDFM — уточнения по составу файлов + +| Файл | Содержимое | Что делаем | +|---|---|---| +| .stp | 3D-модель согнутой детали | вход для probe / measure / fold2 | +| _unfolded.stp | развёртка той же детали как STEP | эталон для нашего unfold: метрология «наша развёртка против их» по 20 000 деталям | +| _sequence.json | параметры исходной пластины и упорядоченная последовательность гибов с параметрами каждого | эталон для нашего FTG и порядка гибки из оригами-кинематики | +| _labels.json | коллизии с пуансоном и матрицей по каждому гибу, усилия гибки, коэффициенты сложности | проверка валидатора коллизий; калибровка «сложности» как компонента цены | +| предобработанные представления | графы для UV-Net (DGL) и облака точек для PointNeXt | сравнить наш обмер с тем, как деталь видят графовые сети — без обучения | + +Разбивка: BenDFM — 14 000 деталей с 2–8 гибами, сбалансирован 50/50 по коллизиям инструмента; BenDFM-U — 6 000 деталей с 7–10 гибами, сбалансирован по самопересечениям при развёртке. Инструмент в симуляции один: пуансон 10 мм с углом 90°, матрица 40 мм. Это значит, что метки коллизий верны для этой пары; для пресса Blechcon нужна своя таблица инструментов (уже предусмотрена в dfm_blechcon.json). + + +### CADFS: человеческие последовательности листового металла по ключевым словам + +Самая ценная мысль второго обзора. В 450 тысячах программ FeatureScript из Onshape есть класс, написанный специализированными инструментами Sheet Metal: sheetMetalStart, bend, flange, rip, unfold. Отфильтровать по тексту программ — и получаются тысячи человеческих, не синтетических, последовательностей построения листовых деталей. Разница с BenDFM принципиальна: BenDFM показывает, как гнут случайно выбранные параметры; CADFS показывает, как гнут люди — какие радиусы выбирают, где ставят разгрузки и какого типа, в каком порядке идут фланцы, как решают углы. Лицензия открытая (Hugging Face, VladPyatov/CADFS). + +| Шаг | Действие | Результат | +|---|---|---| +| 1 | скачать CADFS, отфильтровать программы по наличию sheetMetalStart | подмножество листовых деталей — ожидаемо тысячи | +| 2 | разобрать вызовы bend / flange: радиус, угол, длина фланца, тип разгрузки | статистика реальной практики: распределение радиусов к толщине, углов, типов разгрузок | +| 3 | собрать геометрию из программ (FeatureScript исполняется в Onshape; альтернатива — взять готовые STEP/облака точек из датасета) и прогнать наш обмер | проверка распознавания на человеческих деталях; каталог разгрузок, снятый с практики | +| 4 | сопоставить со своей библиотекой | где наша школа совпадает с мировой практикой, где отличается — и почему | + +> Ограничение: FeatureScript — язык Onshape, исполняется только там; для геометрии брать рендеры и облака точек из самого CADFS либо экспортировать STEP через Onshape для выборки. Разгрузки в Onshape Sheet Metal ставятся по встроенным правилам инструмента — это практика одного CAD, не всех; но именно её и стоит знать как ориентир. + + +### ABC: фильтрация нашим же щупом + +Обзор предлагает выделять листовые детали из миллиона моделей ABC по топологическому признаку: две параллельные эквидистантные поверхности (внутренняя и внешняя), соединённые торцевыми гранями фиксированной ширины, равной толщине, плюс цилиндрические переходы гибов между плоскими гранями. Это дословно наш виртуальный щуп из раздела 16: точка − нормаль × t попадает на поверхность → грань оболочечная; парование; двухцветная раскраска графа. То есть recada.py уже является тем фильтром, о котором говорит обзор — без единой новой строки кода. + +Что это даёт: прогон probe по подмножеству ABC (например, 50 тысяч моделей за ночь на EX44) выделяет реальные листовые детали из практики Onshape — не синтетику и не наши, а чужие, с полным разнообразием форм. Два применения: тест распознавания (сколько листовых деталей щуп находит, сколько пропускает, сколько ложных) и расширение каталога разгрузок и DFM-статистики за пределы нашей школы. Ограничение: STEP-файлы ABC под условиями Onshape Public Terms of Service — для внутреннего тестирования инструмента допустимо, для продукта и обучения нет. + + +### Данные о пружинении: стартовые значения для rules/ + +Работы по прогнозированию пружинения при свободной гибке на прессе (V-die air bending): Mulidrán и соавторы, Li и соавторы — массивы конечно-элементных расчётов и натурных испытаний. В данных: геометрия заготовки, толщина, марка материала (высокопрочные стали HCT600X, алюминиевые сплавы, нержавейка), радиусы пуансона и матрицы, рабочий ход, конечный угол пружинения. Используются для обучения физико-информированных сетей (PINN, Theory-Guided DNN). + +Что берём — не сети (PINN для одной цифры мы отклонили в разделе 4), а стартовые значения: таблица «толщина × радиус × материал → угол пружинения» для 1.4301 и 6063 в rules/dfm_blechcon.json. Записанное ранее правило «строка из практики Blechcon» остаётся, но теперь у неё есть литературный ноль: сначала цифры из измерений, потом калибровка по своим деталям. Ограничение честное: пружинение зависит от конкретной прессы, инструмента и партии металла; литература даёт порядок величины (для 1.4301 t = 3 при R = 3 — единицы градусов), не число для вашего цеха. + + +### TRUMPF: извлечение детали из листа — новый класс правила валидатора + +Промышленный датасет от TRUMPF, опубликованный в рамках исследовательского челленджа TU Dortmund: 15 293 технологические детали, 84 признака — геометрия внешнего и внутренних контуров (периметр, площадь, число вершин, моменты инерции), толщина, свойства сплава, параметры лазерной резки. Метка бинарная: удалось ли манипулятору автоматически вынуть деталь из листа после резки без заклинивания, микроперемычек и брака при выгрузке. Данные от производителя станков — редкость. + +Для нас это класс проверки, о котором мы не думали: узкие контуры, мелкие детали, тонкие перемычки и неудачное соотношение площади к периметру влияют на выгрузку у поставщика — а значит на брак, время и цену. Blechcon режет на таких же станках. Действие: из 84 признаков и меток вывести простые пороги (минимальная площадь, минимальная ширина перемычки, отношение площади к периметру) и добавить в валидатор строку «риск заклинивания при выгрузке». Не обучать модель — извлечь пороги статистикой и проверить у Blechcon. + + +### ESICUP и коллекции DXF: проверка оценки раскроя + +Стандартизированные бенчмарки рабочей группы EURO по раскрою и упаковке: наборы Jakobs, Dighe, Albano, Dagli, Fu, Mao, Shapes — 2D-контуры произвольной формы для нерегулярного нестинга с учётом зазора под рез. Плюс открытые коллекции DXF/SVG для лазера и плазмы (Deepnest, Sheet2Nest), очищенные от сплайнов. У нас предполётный нестинг уже предусмотрен (SheetNest, nest2d, CAD-N, u-nesting — раздел 2), а в разделе 33 записана эвристика «площадь заготовки к выпуклой оболочке». ESICUP — способ проверить эту эвристику на задачах с известным оптимумом: если наша оценка выхода материала коррелирует с эталонным раскроем, ей можно верить в валидаторе. Полезно, не срочно. + + +### Где обзор преувеличивает + +> «Analysis Situs / AAG Sheet Metal Benchmarks» как датасет — такого набора не существует. Analysis Situs — инструмент с функциями распознавания листовых элементов и построения дерева развёртки; мы отклонили его в разделе 4 как тяжёлую C++-платформу для того, что делают сто строк networkx. Функциональность есть, датасета нет. Запись здесь — чтобы не искать. + + +### Сводный план действий по внешним источникам + +| Приоритет | Источник | Действие | Что закрывает | +|---|---|---|---| +| 1 | BenDFM (Zenodo) | регрессия unfold на 20 000 парах; обмер ~4 000 деталей с разгрузками | регрессия; каталог разгрузок; коллизии | +| 2 | CADFS, фильтр sheetMetalStart | статистика человеческой практики: радиусы, углы, типы разгрузок | DFM-правила из практики; каталог разгрузок с другой стороны | +| 3 | ABC, прогон щупом | 50 тыс. моделей за ночь → реальные листовые детали | тест распознавания; разнообразие форм | +| 4 | пружинение (Mulidrán, Li) | стартовая таблица t × R × материал → угол в rules/ | ноль для калибровки по Blechcon | +| 5 | TRUMPF extraction | пороги по площади, ширине перемычки, площадь/периметр | новое правило валидатора: выгрузка | +| 6 | ESICUP, DXF-коллекции | проверка эвристики раскроя на задачах с известным оптимумом | доверие к оценке выхода материала | +| 7 | CC3D, AutoMate | из раздела 41: скан против STEP; сопряжения | входной контроль; граф связей | + +> Все источники — полигон для тестов и статистика, не обучающие данные; каждый входит в library/external после проверки лицензии. Что остаётся уникальным в нашей библиотеке после всех семи: ответ портала, правки Blechcon, партия, поле. Внешние наборы делают инструмент проверенным на тысячах деталей; наши — делают его правильным для нашего производства. + +Третий обзор — про производителей листогибочного оборудования и инструмента: TRUMPF, AMADA, Bystronic, LVD, Salvagnini, WILA, Rolleri. Они почти не публикуют размеченных датасетов — коммерческая тайна. Но одна вещь доступна и закрывает конкретную дыру в нашем валидаторе: библиотеки геометрии инструмента. Ниже — все четыре группы источников подробно, с честной оценкой доступности и действием для нас. + + +### Что у нас записано и чего не хватало + +В разделе 2, в описании валидатора, стоит: «оболочечные модели инструментов запросить у Blechcon; каталоги Amada/Trumpf не нужны». В разделе 4 — «спуры матрицы как источник технологии: расстояние между линиями перехода выдаёт ширину V-паза». В разделе 42 — BenDFM с одним синтетическим инструментом: пуансон 10 мм, угол 90°, матрица 40 мм. Не хватало реальной геометрии инструмента поставщика, чтобы проверка коллизий из «оценки по синтетике» стала «проверкой на прессе Blechcon». Она есть в открытом доступе — у производителей инструмента. + + +### Группа 1. Библиотеки инструмента WILA и Rolleri — главное + +WILA (Нидерланды, инструмент под прессы TRUMPF и не только) выкладывает через официальный портал и приложение WILA Smart Tooling: 2D-контуры в DXF и 3D-модели в STEP пуансонов, матриц и адаптеров систем New Standard и American Standard; для каждого — геометрический профиль, рабочая высота, радиусы кромок, допустимый тоннаж на метр и минимальная длина полки гиба. Rolleri, ToolsPress, Precitools публикуют полные параметрические каталоги в STEP и DXF под кинематику четырёх стандартов: Trumpf-WILA, Amada-Promecam (европейский стандарт), Bystronic, LVD. + +| Что скачивается | Как используем | Что закрывает | +|---|---|---| +| STEP пуансона и матрицы | вставить в оригами-кинематику (раздел 4): деталь складывается пошагово, на каждом шаге пуансон и матрица ставятся по оси гиба, коллизия — булева операция в OCCT | проверка «согнётся ли на этом прессе в этой последовательности» с реальным инструментом, а не с синтетикой BenDFM | +| Таблица «минимальная полка от ширины матрицы» | перенести в dfm_blechcon.json как правило с источником WILA | замена нашей оценки «15 мм при нержавейке» на цифру производителя инструмента | +| Тоннаж на метр | правило: длина гиба × тоннаж на метр ≤ усилие пресса Blechcon | деталь, которую пресс физически не согнёт по длине, ловится до заказа | +| Радиусы кромок пуансона | связать наш R с реальной кромкой: R ≥ радиус пуансона при свободной гибке | радиусы из стандартной реихи инструмента, а не произвольные | +| Ширины V-пазов | таблица V = 6, 8, 10, 12, 16 … мм; из раздела 4: спуры матрицы на чужой детали выдают V, теперь V сверяется с каталогом | реконструкция технологии чужих деталей проверяется по стандартной реихе | + +> Единственный вопрос к Blechcon сводится к одному письму: «Какой стандарт инструмента у вас — Trumpf-WILA или Promecam — и какие ширины матриц и типы пуансонов в наличии?» После ответа таблица инструментов в dfm_blechcon.json заполняется из каталога WILA или Rolleri по названиям, без обмера и без запроса моделей у самого поставщика. Реалистично ожидать Trumpf-WILA: Blechcon в Золингене, TRUMPF в Дитцингене — типичная пара для немецкого цеха. + + +### Группа 2. Базы таблиц гибки — недоступны напрямую, доступны через Blechcon + +| База | Что внутри | Доступ | Наш путь | +|---|---|---|---| +| TRUMPF Bendguide | более 5 000 верифицированных сочетаний марки стали, предела текучести, толщины и инструмента для расчёта пружинения и коррекции угла | внутри TruTops у владельцев станков; есть бесплатное приложение BendGuide для тоннажа и минимальных размеров | стартовые значения в rules/ из приложения; полная таблица — неявно в каждом ответе портала | +| LVD Easy-Form Laser / CADMAN-B | реальные замеры лазерными датчиками при адаптивной гибке: ход пуансона против фактического угла после снятия нагрузки, с учётом разнотолщинности проката и направления волокна | внутри прессов LVD | только если у поставщика LVD; тогда — просить экспорт по нашим деталям | +| Salvagnini MAC 3.0 | динамические замеры усилий и пружинения на панелегибах P4/P2 | внутри панелегибов | не наш класс оборудования | + +Вывод из группы 2: самые точные таблицы в мире живут внутри станков. У Blechcon они есть. Это ещё один аргумент за то, чтобы третий критерий работал как источник данных: каждая деталь, прошедшая через их портал и пресс, неявно содержит их таблицу коррекции. Калибровка K-фактора и пружинения по ответам портала (раздел 18) — единственный способ получить эти данные, не покупая станок. + + +### Группа 3. CAM-пакеты с API — не покупать, просить экспорт + +LVD CADMAN-B и CADMAN-FLOW (пакетный импорт STEP, автоматическое дерево развёртки, последовательность гибов, лог коллизий), Delem Profile-T3D (ЧПУ Delem: генерация последовательностей, экспорт развёрток, разметка), TRUMPF TruTops Bend (симуляция траекторий задних упоров и захватов роботизированных ячеек). Обзор называет это «стандартом генерации обучающих выборок в индустрии» — верно для тех, у кого есть лицензии, которые стоят как станок. + +Наш путь: у Blechcon один из этих пакетов есть. Попросить экспортировать для наших деталей то, что он выдаёт — последовательность гибки, развёртку, лог коллизий. Это ровно те данные, что мы генерируем сами оригами-кинематикой и unfold. Сравнение двух источников — их CAM против нашего ядра — и есть калибровка: где совпало, ядро подтверждено; где разошлось, найдена либо наша ошибка, либо особенность их станка, которую надо записать в правило. Формулировка запроса: «Для каждой нашей детали — экспорт последовательности гибки и развёртки из вашего CAM в любом формате; мы сверяем со своими расчётами, чтобы присылать вам файлы без итераций». Поставщику это выгодно. + + +### Группа 4. Промышленные датасеты TRUMPF + +TRUMPF Sheet Metal Extraction Prediction Dataset с TU Dortmund — уже в разделе 42: 15 293 детали, 84 признака, метка успешности выгрузки после резки. Доступен. «Исследовательские датасеты TRUMPF по гибке с SCCH и TÜV для PINN и валидации пружинения» — консорциумные данные европейских проектов по промышленному ИИ; публичных ссылок найти не удаётся. Не рассчитывать; если появятся — та же роль, что у литературных данных о пружинении: стартовые значения, не сети. + + +### Как это встраивается в инструмент — конкретно + +| Шаг | Действие | Где в системе | Срок | +|---|---|---|---| +| 1 | письмо Blechcon: стандарт инструмента, ширины матриц, типы пуансонов, усилие пресса, длина стола | — | один день, ответ — неделя | +| 2 | скачать из WILA/Rolleri STEP и DXF по списку из ответа; сохранить в rules/tooling/ | rules/tooling/*.step, tooling.json (имя, V, R кромки, высота, тоннаж/м) | вечер | +| 3 | перенести таблицы WILA в правила: минимальная полка от V, тоннаж на метр, стандартные R | dfm_blechcon.json | вечер | +| 4 | встроить в оригами-кинематику постановку пуансона и матрицы по оси каждого гиба и булеву проверку пересечения с уже согнутой частью | recada.py: fold2 + check_tooling | несколько дней | +| 5 | прогнать на своей библиотеке и на BenDFM с реальным инструментом вместо синтетического; сравнить метки | CI, регрессия | ночь | +| 6 | попросить у Blechcon экспорт из CAM по нашим деталям; сверить последовательность и коллизии | library/, паспорт детали: поле cam_export | по мере заказов | + +> Результат: проверка коллизий из «оценки по синтетике» становится «проверкой на инструменте вашего поставщика». Валидатор получает три новых правила из первоисточника — минимальная полка, тоннаж, стандартные радиусы — и одну новую проверку — пересечение с пуансоном и матрицей по шагам гибки. Всё это до отправки на портал, то есть ещё один сдвиг проверок влево (раздел 39): доля деталей, проходящих с первого раза, растёт. + + +### Что остаётся уникальным после всех трёх обзоров + +Внешние источники теперь закрывают: геометрию инструмента (WILA, Rolleri), синтетическую разметку коллизий (BenDFM), человеческую практику гибки (CADFS), реальные листовые формы (ABC щупом), стартовые значения пружинения (литература, BendGuide), риск выгрузки (TRUMPF), проверку раскроя (ESICUP). Ни один из них не содержит: ответа портала конкретного поставщика на конкретную деталь, его правок, партии, полевого поведения. Библиотека RECADA остаётся единственной с этой меткой — и чем больше внешних полигонов проверят инструмент, тем ценнее становится каждая такая метка, потому что она ложится на уже проверенное ядро. + +Четвёртый обзор — самый широкий: от B-Rep-корпусов до конечно-элементных выборок и датчиков адаптивного ЧПУ. Здесь записаны все упомянутые в нём датасеты, включая те, что не наш процесс — чтобы не искать повторно и чтобы граница «наше / не наше» была зафиксирована. Одна настоящая находка — под наш материал: NUMISHEET 2025 с конститутивной моделью 1.4301. + + +### Главная находка: NUMISHEET 2025 Industrial Benchmark — узел Mercedes-Benz T-Node из 1.4301 + +NUMISHEET — международная конференция по численному моделированию процессов листовой штамповки; её бенчмарки — эталон физической достоверности в мировой практике: строго синхронизированные экспериментальные данные оптических измерений, физико-механические паспорта металлов и CAD-модели штампов. Промышленный бенчмарк 2025 года посвящён силовому кузовному узлу Mercedes-Benz из аустенитной нержавеющей стали 1.4301 — нашей стали ног. Технологический цикл: глубокая вытяжка заготовки, трёхмерная лазерная обрезка контура, свободная упругая разгрузка. Открытый архив на Zenodo (DOI 10.5281/zenodo.17662411). + +| В архиве | Что это для нас | +|---|---| +| CAD-модели детали и инструмента: STEP, IGES, STL | референсная геометрия для проверки чтения и метрологии на сложной вытянутой форме | +| Параметры конститутивной модели 1.4301: кривая упрочнения, анизотропия, предел текучести | готовый паспорт материала для CalculiX — раздел 13 записывал калибровку валидатора по прочности как план; параметры 1.4301 брать больше не из справочника, а из верифицированного бенчмарка | +| Сканы реальной изготовленной детали после пружинения (оптические КИМ) | эталон для проверки, что наш расчёт пружинения в CalculiX совпадает с металлом — до того как верить ему на своих деталях | + +> Технология там — вытяжка, не гибка на прессе. Но материальная карта от процесса не зависит: те же кривые идут в расчёт гиба, модальный анализ и прочность ног. Это единственный источник в четырёх обзорах, который даёт верифицированные данные именно по нашему материалу. + + +### Бенчмарки пружинения NUMISHEET — эталоны для проверки алгоритмов + +| Бенчмарк | Содержание | Материалы | Нам | +|---|---|---|---| +| NUMISHEET 1993 U-Draw Bending | классический тест свободной U-образной гибки: геометрия штампов, режимы, профили после разгрузки; эталон для калибровки компенсации угла методами Displacement Adjustment и Spring Forward | низкоуглеродистая и высокопрочная сталь, алюминий | ДА: тест для расчёта пружинения в CalculiX, если будем считать, а не брать таблицей | +| NUMISHEET 2005 Benchmark 2 | геометрия штампов и замеры пружинения поперечины днища | DP590, DP980, AA5182-O | косвенно: методика замеров | +| NUMISHEET 2008 Benchmark 2 (S-Rail) | скручивание и коробление S-образного лонжерона при усилии прижима 650–850 кН | автомобильные стали | нет: глубокая вытяжка сложной формы | +| NUMISHEET 2025 Scientific (DP800) | многомасштабные данные: EBSD, наноиндентирование, растяжение, сдвиг, послойное сжатие — для калибровки поверхностей текучести | двухфазная сталь DP800 | нет: не наш материал; доступ по регистрации участников | + + +### Конечно-элементные корпуса — не наш процесс, зафиксировано + +| Датасет | Содержание | Почему не наше | +|---|---|---| +| ISMF TINY Dataset (Zenodo, 10,3 ГБ) | 1 975 конечно-элементных расчётов инкрементального формообразования листа пуансоном по траектории с подвижной матрицей: поля перемещений, градиенты пластических деформаций, тензоры остаточных напряжений, профили пружинения; для обучения свёрточных и диффузионных операторов | инкрементальное формообразование — другая технология; поля напряжений с него на гибку на прессе не переносятся. Возможная роль: проверка настроек CalculiX на задаче с известным ответом | +| Automotive Body-in-White Springback Dataset (NSF) | тысячи многостадийных нелинейных симуляций: формовка на геометрии NUMISHEET, позиционирование в кондукторах, прижим, контактная точечная сварка; параметризация по толщине, модулям, контактным силам | сварные кузовные подсборки; у нас болты и никакой сварки — осознанное решение раздела 31 | +| RWTH Aachen MUC-Test | диаграммы цифровой корреляции изображений (DIC), лазерная экстензометрия, структуры RVE для стали DP800HHE; параметры анизотропного пластического течения | не наш материал; методика DIC может пригодиться при собственных испытаниях по ISO 11154 | + + +### Уточнения к уже записанным источникам + +WILA: каталог New Standard Premium, New Standard Pro и American Style; инструмент с углами гиба от остроугольных 28° до прямых 90°; паспорта нагрузки на метр; координаты стыковки для роботизированных захватов с системой Smart Tool Locator; доступ по регистрации на портале. ToolsPress и Rolleri — открытые порталы без регистрации, тысячи единиц; Amada-Promecam — европейский стандарт хвостовика с пазом 12 мм, Trumpf-WILA — быстродействующие гидравлические зажимы. TRUMPF TruBend: таблицы Bendguide калибруются датчиками ACB (Automatically Controlled Bending) с точностью ±0,1° на первом цикле; TRUMPF Extraction: цель — вероятность успешного захвата ≥ 0,8 с оптимизацией полноты, потому что пропущенная заклинившая деталь останавливает всю гибкую ячейку. ESICUP: обучение с подкреплением и геометрические диффузионные сети с учётом kerf, термических поводок и направления проката. Про «бенчмарки Analysis Situs с пограневой разметкой полок, гибов, подгибки кромок и рёбер жёсткости» — обзор снова описывает их как датасет; позиция прежняя: это функции инструмента, публичного набора нет. + + +### Вывод обзора, который берём дословно: «first-part-right» + +> Индустрия отказывается от чисто геометрического машинного обучения в пользу гибридных физико-информированных подходов и идёт к слиянию трёх потоков: топологических B-Rep-графов деталей (BenDFM), конечно-элементных выборок (ISMF, NUMISHEET) и данных датчиков адаптивного ЧПУ (Easy-Form Laser, ACB, MAC 3.0) — ради концепции «first-part-right»: первое изделие без брака, без итераций на наладке. Это дословно наша метрика из раздела 39 — «доля деталей, проходящих портал с первого раза, цель девять из десяти». Мы измеряем то, что индустрия объявила целью, только на своём уровне: не станок, а поставщик. И наш гибрид тот же: детерминированное ядро, правила из практики, статистика поверх — раздел 15 и подтверждение из Лёвена в этом же разделе. + + +### Полный реестр внешних источников после четырёх обзоров + +| Источник | Тип | Наш статус | Куда | +|---|---|---|---| +| BenDFM / BenDFM-U | B-Rep, синтетика, гибка | приоритет 1 | регрессия unfold, каталог разгрузок, коллизии | +| CADFS (фильтр sheetMetalStart) | программы, человеческие | приоритет 2 | DFM-статистика практики | +| ABC (прогон щупом) | B-Rep, реальные | приоритет 3 | тест распознавания | +| WILA / Rolleri / ToolsPress | геометрия инструмента | приоритет 1 (после письма Blechcon) | коллизии на реальном инструменте, правила полки и тоннажа | +| NUMISHEET 2025 Industrial (1.4301) | материальная карта + сканы | приоритет 2 | CalculiX: паспорт материала, проверка расчёта | +| NUMISHEET 1993 U-Bend | эталон пружинения | при расчёте пружинения | тест алгоритма компенсации | +| Пружинение (Mulidrán, Li), BendGuide | таблицы | стартовые значения | rules/dfm_blechcon.json | +| TRUMPF Extraction | табличный, промышленный | приоритет 3 | правило «риск выгрузки» | +| ESICUP, Deepnest/Sheet2Nest | контуры раскроя | при проверке нестинга | доверие к оценке выхода | +| CC3D / CC3D-Ops | STEP + сканы | приоритет 2 | входной контроль по скану | +| AutoMate | сопряжения | при модели соединений | граф связей | +| KUL-bend | реальный, закрытый | идея | время гибки как компонент цены | +| MM-CAD, CADCap-1M, SketchGraphs | мультимодальные | идеи | текст из паспорта, эскиз как запрос | +| ISMF TINY, BIW Springback, NUMISHEET DP800, RWTH MUC | FEM, другие процессы и материалы | не наше — зафиксировано | — | +| Fusion 360 (3), Text2CAD, DeepCAD и др. (раздел 41) | общий CAD | нет / лицензии | — | +| CAM-экспорт Blechcon (CADMAN, TruTops, Delem) | данные поставщика | просить с каждым заказом | калибровка ядра против их CAM | + +Итог четырёх обзоров в одной фразе: внешний мир даёт геометрию инструмента, синтетику коллизий, человеческую практику, материальную карту нашей стали и эталоны пружинения; не даёт — ответа конкретного поставщика на конкретную деталь. Всё первое проверяет ядро; только второе делает его правильным для RECADA. + +Пятый обзор — «тотальный аудит» Zenodo, Mendeley, Figshare, GitHub, Hugging Face и архивов NUMISHEET, ESAFORM, IDDRG. Добавляет то, чего не хватало: точные DOI для скачивания, два китайских датасета распознавания элементов листового металла, бенчмарк ESAFORM и кластер по гибке труб. Ниже — только новое относительно предыдущих частей раздела, плюс исправление ссылок. + + +### Новое по нашей теме — распознавание элементов листа + +| Датасет | Что содержит | Нам | Где | +|---|---|---|---| +| SMCAD (Northwestern Polytechnical University, Ma и Yang, 2024) | листовые CAD-модели в STEP с графами смежности граней; пограневая разметка: цилиндрические зоны гиба, вырезы, отбортовки, шарниры, рёбра жёсткости, проушины; для сегментации графовыми сетями в режиме Class-Incremental Learning | ДА: тест нашего распознавания на размеченных гранях — совпадает ли наш «гиб / фланец / разгрузка» с их метками; вход для каталога типов элементов | GitHub MaFangchang/SMCAD-dataset | +| HeteroMF (та же группа) | 63 520 синтетических моделей с разнородными элементами листового формообразования: высечки, жалюзи, рёбра жёсткости, сложные гибы; стресс-тест классификаторов | ДА как стресс-тест: элементы, которых у нас нет (жалюзи, рёбра), проверяют, что щуп и AAG не ломаются на чужих формах | GitHub MaFangchang | + +> Оговорка из самого BenDFM (записана в его статье): SMCAD строит разнообразие «добавлением элементов к небольшому фиксированному набору базовых геометрий», а не разнообразием фланцев и их взаимодействием — поэтому для производимости гибки он слабее BenDFM. Для распознавания элементов — наоборот, полезнее: у BenDFM только гибы и разгрузки, у SMCAD весь зоопарк. + + +### DOI и точки скачивания — исправление и дополнение + +| Датасет | Точный адрес | Примечание | +|---|---|---| +| BenDFM / BenDFM-U | Zenodo DOI 10.5281/zenodo.18622958; репозиторий github.com/UGent-CVAMO/bendfm | в предыдущей части DOI не был записан — теперь есть | +| NUMISHEET 2025 Industrial (T-Node, 1.4301) | Zenodo DOI 10.5281/zenodo.17662411 | подтверждено | +| ISMF TINY | Zenodo DOI 10.5281/zenodo.17917675; IT4Innovations, Чехия | не наш процесс, DOI для полноты | +| ESAFORM EXACT Benchmark | Zenodo DOI 10.5281/zenodo.6874577 | см. ниже | +| Rotary Tube Bending (Университет Зигена) | Zenodo DOI 10.5281/zenodo.16614082; github.com/zeyneddinoz/tubebend | трубы, не наше | +| Pressurized Tube Bending (Colorado Boulder) | Zenodo DOI 10.5281/zenodo.7136215 | трубы, не наше | +| TRUMPF Extraction | портал челленджа TU Dortmund | прямая ссылка в обзорах не приведена; искать по названию челленджа | +| ESICUP | репозиторий рабочей группы EURO по раскрою и упаковке; серии Shapes, Albano, Blaz, Dighe, Fu, Jakobs, Swim, Dagli | добавлены серии Blaz и Swim | +| RL-Kirigami | github.com/milad1378yz/RL-Kirigami | лазерная микропрорезка; не наше | + + +### ESAFORM EXACT — материалы не наши, методика близкая + +Экспериментальные данные и численное моделирование цилиндрической вытяжки алюминия AA6016-T4 и стали DC06: кривые упрочнения, профили фестонообразования, эволюция усилий. Материалы не наши (у нас 1.4301 и 6063), процесс — вытяжка. Ценность одна: это второй после NUMISHEET пример того, как выглядит открытый пакет «эксперимент + расчёт» для калибровки FEM — образец структуры для наших собственных испытаний по ISO 11154, когда они начнутся: что измерять, как публиковать паспорт материала, как сопоставлять расчёт с замером. + + +### Гибка труб — зафиксировано как не наше + +Rotary Tube Bending Dataset (Зиген): 318 натурных экспериментов ротационной вытяжной гибки труб на станке с ЧПУ, включая случаи брака; датчики усилий на дорне и прижимных колодках, траектории осей, 3D-геометрия сечения (овальность, утонение). Pressurized Tube Bending (Колорадо): 10 ГБ видео и диаграмм «момент — угол» для тонкостенных труб под внутренним давлением. У Airholder труб нет — платформа из профилей, ноги из листа. Если когда-либо появится трубчатый аксессуар (дуги, поручни), это стартовая точка; пока не наше. + + +### Уточнения к инструменту + +AMADA SheetWorks (Япония): библиотеки деформаций, выколоток, полок и матриц AMADA с контролем интерференции пуансона — интегрировано в среду Amada AP100/SheetWorks, отдельно не скачивается; актуально только если у Blechcon прессы Amada. WILA: обзор называет каталог открытым; предыдущий говорил о регистрации — при скачивании выяснится. Про «Analysis Situs Sheet Metal Feature Repository» — пятый обзор называет это «коллекцией тестовых B-Rep моделей для верификации алгоритмов распознавания»; тестовые модели в репозитории самого инструмента возможны, но публичного размеченного датасета под этим именем не находится. Позиция: проверить репозиторий Analysis Situs на наличие тестовых STEP, не рассчитывать на разметку. + + +### Четыре кластера мировых данных — карта, в которой мы себя находим + +| Кластер | Что в нём | Наше место | +|---|---|---| +| 1. Синтетические CAD/B-Rep корпуса | BenDFM, SMCAD, HeteroMF, CADFS-фильтр, ABC-щуп | полигон для распознавания, развёртки, коллизий; наша библиотека — тот же кластер, но с производственной меткой | +| 2. Физические и FEM-бенчмарки | NUMISHEET, ESAFORM, ISMF TINY, BIW, RWTH | берём только карту 1.4301 и U-Bend как тест; остальное не наш процесс | +| 3. Сенсорные данные реального оборудования | TRUMPF Extraction, KUL-bend, ACB, Easy-Form, MAC 3.0, трубогибы | доступно через Blechcon как экспорт CAM и через ответы портала; сами станков не имеем | +| 4. Векторные библиотеки раскроя | ESICUP, Deepnest, Sheet2Nest | проверка оценки выхода материала | + +> Ключевое наблюдение обзора, которое объясняет, почему наша библиотека имеет цену: «чертежи реальных серийных изделий защищены строгими NDA автоконцернов, оборонных предприятий и контрактных производств» — поэтому весь открытый мир состоит из синтетики, академических бенчмарков и обезличенных табличных данных. Реальная деталь с реальным ответом реального поставщика в открытом виде не существует нигде. У нас она будет с первого заказа. + +Все ссылки, собранные из пяти обзоров и собственных поисков (сентябрь 2026). Формат: название — тип — адрес. Адреса приведены как в источниках; при скачивании проверять лицензию (правило раздела 41). + +| Название | Тип | Адрес | +|---|---|---| +| BenDFM | статья | arxiv.org/abs/2603.13102; Springer JIM doi 10.1007/s10845-026-02857-9 | +| BenDFM | данные | doi 10.5281/zenodo.18622958; github.com/UGent-CVAMO/bendfm | +| KUL-bend / гибридный метод | статья | arxiv.org/abs/2605.12266 | +| SMCAD | данные | github.com/MaFangchang/SMCAD-dataset | +| HeteroMF | данные | github.com/MaFangchang | +| NUMISHEET 2025 Industrial (T-Node) | данные | doi 10.5281/zenodo.17662411 | +| NUMISHEET benchmarks (архив) | данные | архивы конференции NUMISHEET; 1993 U-Draw Bending, 2005 BM2, 2008 BM2 S-Rail | +| ESAFORM EXACT | данные | doi 10.5281/zenodo.6874577 | +| ISMF TINY | данные | doi 10.5281/zenodo.17917675 | +| Rotary Tube Bending (Зиген) | данные | doi 10.5281/zenodo.16614082; github.com/zeyneddinoz/tubebend | +| Pressurized Tube Bending (Colorado) | данные | doi 10.5281/zenodo.7136215 | +| TRUMPF Extraction Prediction | данные | челлендж TU Dortmund (ссылку искать по названию) | +| RL-Kirigami | данные | github.com/milad1378yz/RL-Kirigami | +| ESICUP nesting benchmarks | данные | репозиторий EURO Special Interest Group on Cutting and Packing | +| Deepnest | инструмент/данные | deepnest.io; github | +| WILA Tooling / Smart Tooling | каталог инструмента | портал WILA | +| Rolleri, ToolsPress, Precitools | каталог инструмента | порталы производителей | +| MM-CAD | статья | Computer Graphics Forum, doi 10.1111/cgf.70523 | +| CADSketchNet | данные | github.com/bharadwaj-manda/CADSketchNet | +| CAD-RAG | статья | wscg.zcu.cz/WSCG2025/papers/D11.pdf | +| ABC Dataset | данные | archive.nyu.edu/handle/2451/43778 | +| A2Z-10M+ | статья | arxiv.org/abs/2603.12605 | +| Better STEP | статья | openreview.net/forum?id=RlKUK83N1L | +| DeepCAD | данные | github.com/ChrisWu1997/DeepCAD; kaggle vitalygladyshev/deepcad | +| CADFS | данные | arxiv.org/abs/2605.01925; huggingface.co/datasets/VladPyatov/CADFS; voyleg.github.io/cadfs | +| CADParser | данные | github.com/spicywagyu04/CADParser; ijcai.org/proceedings/2023/200 | +| Fusion 360 Gallery (Reconstruction, Segmentation, Assembly) | данные | github.com/AutodeskAILab/Fusion360GalleryDataset | +| Text2CAD | данные | arxiv.org/abs/2409.17106 | +| Text2CAD-Bench | статья | arxiv.org/abs/2605.18430 | +| CADCap-1M / DreamCAD | данные | huggingface.co/datasets/SadilKhan/CADCap-1M; sadilkhan.github.io/dreamcad2026 | +| Text-to-CadQuery | данные | arxiv.org/abs/2505.06507; github.com/Text-to-CadQuery | +| MFCAD / MFCAD++ | данные | github.com/hducg/MFCAD | +| UV-Net | статья | arxiv.org/abs/2006.10211 | +| AutoMate | данные | github.com/degravity/automate; Zenodo | +| SketchGraphs / Vitruvion | данные | github SketchGraphs; courses.cs.washington.edu (Vitruvion) | +| JoinABLe | статья | karlddwillis.com/joinable | +| ArchCAD-400K | данные | archiai-lab.github.io/ArchCAD.github.io; arxiv.org/abs/2503.22346 | +| CC3D / CC3D-Ops / CADOps-Net | данные | Uni Luxembourg CVI2; skazizali.com CADOps-Net | +| MCB / CADNet40v2 | данные | см. Awesome-CAD | +| Awesome-CAD (список работ) | реестр | github.com/bertjiazheng/Awesome-CAD | +| BRep-GD | статья | computer.org/csdl/journal/tg/2026/07/11450536 | +| GraphBRep | статья | academic.oup.com/jcde/article/13/1/259/8381229 | +| Zero-to-CAD | статья | arxiv.org/abs/2604.24479 | +| BenchCAD | статья | arxiv.org/abs/2605.10865 | +| CadBench | статья | arxiv.org/abs/2605.10873 | +| UniCAD | статья | arxiv.org/abs/2606.05058 | +| CADENA (пошаговый реверс) | статья | arxiv.org/abs/2608.00799 | +| Agent-Aided Design | статья | arxiv.org/abs/2604.15184 | +| FllumaOne | статья | arxiv.org/abs/2606.17696 | +| Graph-CAD | статья | arxiv.org/abs/2604.10075 | +| cadrille | статья | arxiv.org/abs/2505.22914 | +| mrCAD | статья | arxiv.org/abs/2504.20294 | +| Обзоры: Geometric DL for CAD; AI-driven generation of 3D CAD; DL in 3D CAD Reconstruction | обзоры | researchgate; sciopen.com CVM.2025.9450521; mdpi.com/2076-3417/15/12/6681 | +| The AI CAD Frontier | отраслевой обзор | spectrallabs.ai/research/The-AI-CAD-Frontier | +| OCCT 8.0.1 | ядро | github.com/Open-Cascade-SAS/OCCT/releases | +| YouCanNotUnfold | библиотека | github.com/alexneufeld/YouCanNotUnfold | +| Onshape: Under the Hood | статья | onshape.com/en/blog/under-the-hood-how-collaboration-works | +| Zoo / KCL | продукт | zoo.dev | +| OCP.wasm / Yet Another CAD Viewer; OpenCascade.js; Mayo; OCP CAD Viewer | просмотрщики | github (yeicor, ocjs.org, fougue/mayo, bernhard-42/vscode-ocp-cad-viewer) | +| Autodesk Platform Services | продукт | aps.autodesk.com | +| Aufbaurichtlinien VW / Ford BEMM / Stellantis | документы производителей | порталы для Aufbauhersteller (регистрация) | +| Front Runner Slimline 3 (Ranger P703) | конкурент | dometic.com | +| DX: исследование ИИ и производительности | исследование | DX, сентябрь 2026, 500+ команд | + +> Правило работы со списком: каждый источник перед использованием проверяется на лицензию и актуальность; внешние наборы — полигон и статистика, не обучающие данные; ссылка в документе не означает, что источник уже скачан — статус скачивания ведётся в library/external/README.md. + + +--- + +## 43. Риски и узкие места + +| Риск | Суть и защита | +|---|---| +| Обратный поток правок (№1) | Ручная правка в GUI не возвращается в params → два источника истины → смерть системы. Ручное — только песочница; в систему — через параметры/генератор; STEP на Blechcon — только из CI технически. | +| FreeCAD/SheetM etal | Community-аддон с пределами (SMBendWall, boolean OCC, 7-й фланец — предвестник: реальные крыши грязные, паспорт может расщепиться по комплектациям). Правило 80/20: экзотика руками вне конвейера — это нормально. | +| LLM-слой | Валидатор не ловит «корректную глупость» — человеческое ревью остаётся; время владельца — узкое место. | +| Bus factor = 1 | Lead reviewer реально проводит приёмку без владельца минимум раз в месяц. | +| Scope creep | Система строится только вслед за реальным заказом; ворота со стоп-лоссами; чужие ревью концепта фильтровать вопросом «что убрать?» — оба присланных списка только добавляли. | +| Вы — IT-отдел | Gitea/CI/Kasm/VPN — скрытые 10–20% времени, учитывать в бюджете. | +| Заморозка интерфейса | Максимум внимания при минимуме информации; после продаж менять нельзя (legacy или слом совместимости). | +| Внешние зависимости | Shopify API, three.js, аддоны — версии фиксируются; восстановление из бэкапа тестируется ежеквартально. | +| Дешёвый аутсорс | Не бесплатен: ТЗ и ревью — часы владельца; смягчение — валидатор и сдельность. | + + +### Риски, добавленные по итогам практики (сентябрь 2026) + +Инструмент — один файл без автоматических тестов; библиотека как регрессия — первое лекарство. Зависимость от версии OCCT: 7.8 в FreeCAD и 7.9 в ядре ведут себя по-разному — стоило полдня; фиксировать версию ядра и не смешивать. Третий критерий проверяется руками через чужой портал, который может измениться без предупреждения — вести журнал ответов портала как данные, чтобы изменения были видны. + + +--- + +## 44. Дорожная карта + + +### Ближайший порядок работ — конкретно + +| Шаг | Что | Зачем | +|---|---|---| +| 1 | установка сервера: ОС, ключи, ufw, WireGuard, restic на Storage Box (Хельсинки) | база и бэкап в другом ДЦ | +| 2 | разделить ядро и интерфейс, очередь задач, постоянное хранилище | многопользовательность | +| 3 | сканер библиотеки: пакетный обмер, таблица, статистика | данные вместо догадок | +| 4 | регрессия на CI по библиотеке: процент прохождения трёх критериев | не ломать работающее | +| 5 | калибровка K-фактора по ответам портала | развёртки точнее | +| 6 | каталог разгрузок → генерация зоны гиба по правилу | параметрика углов проходит производство | +| 7 | гибридный workbench FreeCAD (вызов внешнего инструмента) | инженер рисует руками, считает ядро | +| 8 | паспорта автомобилей в библиотеке → редактор изделия на данных | багажник как параметры | + +| Этап | Содержание | Статус | +|---|---|---| +| 0. Фундамент | Реверс Bracket_L/R (метрология 100%), wind_side_v2, Skeleton_Ranger + MasterParams, паспорт Ranger, DFM-правила как skills; решения по железу зафиксированы (EX44-критерий) | Сделано | +| 1. Хребет | Локальный старт на ThinkPad: git init + структура + build123d + первый генератор L-кронштейна с автосверкой (вечер №1); Gitea+WireGuard (Strato или сразу EX44 по прибытии); первый CI «params → STEP+GLB»; step_probe.py (обмерщик STEP на AAG) как первый модуль библиотеки; zippey; 7-й фланец (BSpline) | Первый шаг | +| 2. Валидатор и каталог | Валидатор DFM+ (cad-khana + зазор 20/упаковка GLS (40 кг/коробка)/крепёж) + геометрический дифф в PR; spec.yaml + автогенерация приёмочного прогона из constraints; catalog.json; статусы двух осей + webhook Shopify; SheetNest; рендер-конвейер; автосхема конвейера (Mermaid); архив счетов Blechcon и тарифа GLS (пороги надбавок — в rules/, честная доставка в CM1); параллельный трек испытаний: собрать существующие отчёты (статика, модальный) как эталоны калибровки; запланировать физические испытания ISO 11154 с расчётным обоснованием | | +| 3. Рабочие места | Kasm CE на EX44 (пентест лично, вкл. Python-консоль); egress-файрвол; RECADA-workbench; Claude в VDI-образе с лимитированным ключом | | +| 4. Люди | Onboarding-пакет EN; воронка Upwork+hh.uz с пробой канала; автотест с автоприёмкой; GDPR-оформление; такт 2 недели; YAML-инструкции; светофор нагрузки; скрипты CI: складской webhook и онбординг | | +| 5. Масштаб | Вторая машина по конвейеру; lead reviewer; библиотека узлов → нодовый composер (CPQ с PDF-КП для дилеров; быстрый внутренний прототип панели параметров — Streamlit, выход строго в params.json/Git, чтобы не плодить второй источник истины); FEM в валидаторе (CalculiX+Gmsh: статика с нормативной аэронагрузкой, Мизес с запасом 1.5, прогиб подтверждает зазор 20 мм, модальная проверка f1 ≥ порога по Струхалю, автоотклонение PR с картой напряжений; калибровка по существующим отчётам); ИИ-агент паспортов (STL → распознавание отверстий/нормалей → черновик vehicle_passport + PR; подпись человека обязательна); второй поставщик металла; публикация спецификации интерфейса; сообщество; узел Сингапур/Мумбаи при 40+ ч/мес; RISE-L/i9 при 10+ сессиях; контент-маркетинг; ISO-формализация по запросу рынка | | + +Формула проекта: мозг и знания — в Германии (структурно самой дешёвой серверной стране), руки — где угодно (пиксели вместо файлов, любой CAD на входе, STEP на выходе), приёмка — автоматическая, рост людей — по логам, а не по резюме. Деталь описывается один раз параметрами; производство, конфигуратор, инструкции и склад — производные одного конвейера. Владелец управляет трубой: приоритеты на входе, критерии приёмки, кнопка релиза. + +RECADA / Airholder · Полное описание проекта v25 · Сводка обсуждений, август 2026 · Ставки, цены, пинги — ориентировочные на дату; цены Hetzner/OVH проверять при заказе diff --git a/docs/RECADA_проект_полное_описание_v59.pdf b/docs/RECADA_проект_полное_описание_v59.pdf new file mode 100644 index 0000000..f01f366 --- /dev/null +++ b/docs/RECADA_проект_полное_описание_v59.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:481d2063591e1014bbc122c667a43f2f3a92a0fcfe13e7e847de666e71e62472 +size 345704 diff --git a/docs/archiv/RECADA_проект_полное_описание_v25.pdf b/docs/archiv/RECADA_проект_полное_описание_v25.pdf new file mode 100644 index 0000000..ce90b54 --- /dev/null +++ b/docs/archiv/RECADA_проект_полное_описание_v25.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c445ce33e566e025a26cc66dfc6d235e7e89e6ecd42a25a818493038e2c85917 +size 218632 diff --git a/docs/archiv/RECADA_проект_полное_описание_v30.pdf b/docs/archiv/RECADA_проект_полное_описание_v30.pdf new file mode 100644 index 0000000..96a285b --- /dev/null +++ b/docs/archiv/RECADA_проект_полное_описание_v30.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:611b69fb26ff3f087a27e6945f4f91c376d90a262797d0a74f56c772a7e88095 +size 238702 diff --git a/docs/archiv/RECADA_проект_полное_описание_v35.pdf b/docs/archiv/RECADA_проект_полное_описание_v35.pdf new file mode 100644 index 0000000..8b3fc07 --- /dev/null +++ b/docs/archiv/RECADA_проект_полное_описание_v35.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:842befe162f14c45af5afc4070a0a7dc96f45ff48f426a118d601e67107c40fd +size 261017 diff --git a/docs/archiv/RECADA_проект_полное_описание_v40.pdf b/docs/archiv/RECADA_проект_полное_описание_v40.pdf new file mode 100644 index 0000000..6e8a688 --- /dev/null +++ b/docs/archiv/RECADA_проект_полное_описание_v40.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e76ade5b66d8183a338a037c2df7c59928cbdf58e69d727273ea905a9e2eed5 +size 287978 diff --git a/docs/archiv/RECADA_проект_полное_описание_v45.pdf b/docs/archiv/RECADA_проект_полное_описание_v45.pdf new file mode 100644 index 0000000..99037e5 --- /dev/null +++ b/docs/archiv/RECADA_проект_полное_описание_v45.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:025d7e5002c2f98237475e7c0d15116fe46c6f37d106d903d01f61b7ac21a165 +size 479234 diff --git a/docs/archiv/RECADA_проект_полное_описание_v50.pdf b/docs/archiv/RECADA_проект_полное_описание_v50.pdf new file mode 100644 index 0000000..b052bdb --- /dev/null +++ b/docs/archiv/RECADA_проект_полное_описание_v50.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57de2e2f7a5d6e6ea755d3f72f4a0f10b40c68871337942fc96345e9f3da56c4 +size 756306 diff --git a/go.bat b/go.bat new file mode 100644 index 0000000..04b858c --- /dev/null +++ b/go.bat @@ -0,0 +1,13 @@ +@echo off +rem Start RECADA: move fresh recada.py from Downloads + launch UI +setlocal +set ROOT=C:\CAD COWORK +set DL=%USERPROFILE%\Downloads\recada.py + +if exist "%DL%" ( + move /Y "%DL%" "%ROOT%\tool\recada.py" + echo recada.py updated from Downloads +) + +cd /d "%ROOT%\tool" +".venv\Scripts\python.exe" -m streamlit run recada.py --server.maxUploadSize 2000 diff --git a/go.ps1 b/go.ps1 new file mode 100644 index 0000000..34ef0a7 --- /dev/null +++ b/go.ps1 @@ -0,0 +1,11 @@ +# Запуск RECADA: перенос свежего recada.py из Downloads + старт интерфейса +$root = "C:\CAD COWORK" +$dl = Join-Path $env:USERPROFILE "Downloads\recada.py" + +if (Test-Path $dl) { + Move-Item -Force $dl (Join-Path $root "tool\recada.py") + Write-Host "Обновлён recada.py из Downloads" -ForegroundColor Green +} + +Set-Location (Join-Path $root "tool") +& ".\.venv\Scripts\python.exe" -m streamlit run recada.py --server.maxUploadSize 2000 diff --git a/library/.gitkeep b/library/.gitkeep new file mode 100644 index 0000000..198ecfb --- /dev/null +++ b/library/.gitkeep @@ -0,0 +1 @@ +Пока пусто. См. README.md — назначение папки. diff --git a/library/external/README.md b/library/external/README.md new file mode 100644 index 0000000..2deffc7 --- /dev/null +++ b/library/external/README.md @@ -0,0 +1,40 @@ +# Внешние датасеты — полигон для тестов, не обучающие данные + +Правило: любой внешний набор входит сюда только после проверки лицензии на коммерческое использование +и используется как полигон для регрессии и калибровки нашего инструмента, не как обучающая выборка. + +## BenDFM (UGent-CVAMO, 2026) — первый приоритет +- 20 000 гнутых деталей STEP + развёртки STEP + последовательность гибов + метки коллизий (JSON) +- BenDFM-U: 6 000 деталей с 7–10 гибами, 50/50 по перекрытиям при развёртке +- Репозиторий: github.com/UGent-CVAMO/bendfm — примеры и описание форматов +- Полный датасет: Zenodo (ссылка в репозитории) +- Задачи: регрессия unfold; каталог разгрузок; проверка валидатора коллизий + +## CC3D / CC3D-Ops (Uni Luxembourg) — второй приоритет +- пары STEP + симулированный скан с шумом +- Задача: входной контроль по скану, метрология «скан против STEP» + +## AutoMate (Zenodo, открытый) +- 100 000 пар сопряжений в сборках +- Задача: проверка эвристик графа связей + +## NUMISHEET 2025 Industrial Benchmark (Zenodo, DOI 10.5281/zenodo.17662411) — второй приоритет +- узел Mercedes-Benz T-Node из 1.4301: CAD инструмента и детали (STEP/IGES/STL), + конститутивная модель 1.4301, сканы после пружинения +- Задача: паспорт материала для CalculiX; проверка расчёта пружинения против сканов + +## NUMISHEET 1993 U-Draw Bending — при расчёте пружинения +- эталон свободной U-гибки с замерами; тест алгоритма компенсации + +## Инструмент: WILA (регистрация), Rolleri / ToolsPress (открыто) — см. rules/tooling/ + +## Не наше (зафиксировано, чтобы не искать): ISMF TINY, Automotive BIW Springback, +## NUMISHEET DP800, RWTH MUC-Test — другие процессы и материалы + +## DOI (из пятого обзора) +- BenDFM: 10.5281/zenodo.18622958 +- NUMISHEET 2025 T-Node: 10.5281/zenodo.17662411 +- ESAFORM EXACT: 10.5281/zenodo.6874577 (не наши материалы; образец структуры «эксперимент + расчёт») +- SMCAD: github.com/MaFangchang/SMCAD-dataset — тест распознавания элементов (гиб / фланец / вырез / отбортовка) +- HeteroMF: github.com/MaFangchang — стресс-тест щупа на чужих формах (жалюзи, рёбра) +Полный список ссылок — приложение к разделу 42 документа. diff --git a/parts/.gitkeep b/parts/.gitkeep new file mode 100644 index 0000000..198ecfb --- /dev/null +++ b/parts/.gitkeep @@ -0,0 +1 @@ +Пока пусто. См. README.md — назначение папки. diff --git a/parts/LRDE-2BR1/LRDE-2BR1_bend94_plus10_v2.step b/parts/LRDE-2BR1/LRDE-2BR1_bend94_plus10_v2.step new file mode 100644 index 0000000..1892f98 --- /dev/null +++ b/parts/LRDE-2BR1/LRDE-2BR1_bend94_plus10_v2.step @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6b195c194713c44dbc5372cb53f1adef43bd3cd09c21d5ad9dfe5b3978de458 +size 857771 diff --git a/parts/LRDE-2BR1/LRDE-2BR1_nominal_v2.step b/parts/LRDE-2BR1/LRDE-2BR1_nominal_v2.step new file mode 100644 index 0000000..bcbd1c5 --- /dev/null +++ b/parts/LRDE-2BR1/LRDE-2BR1_nominal_v2.step @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa572bba523d79fc608a93bccf7b779d6ef95c9fdaabde4b1d42184c2151e290 +size 813281 diff --git a/parts/LRDE-2BR1/LRDE-2BR1_wall_plus10_v2.step b/parts/LRDE-2BR1/LRDE-2BR1_wall_plus10_v2.step new file mode 100644 index 0000000..ed85cc3 --- /dev/null +++ b/parts/LRDE-2BR1/LRDE-2BR1_wall_plus10_v2.step @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e9c433653ea7d78dac4c622446923bfe93f742c80dd8a642923a6563b3667b9 +size 897429 diff --git a/parts/_vorlage_spec.yaml b/parts/_vorlage_spec.yaml new file mode 100644 index 0000000..9d42d47 --- /dev/null +++ b/parts/_vorlage_spec.yaml @@ -0,0 +1,22 @@ +sku: "" +rev: "A" +intent: "зачем деталь" +vehicle: "" +role: "нога | кронштейн крыши | поперечина | адаптер" +material: "1.4301" +thickness_mm: 3.0 +interfaces: + - name: "sole" + mates_with: "vehicle.mount_points.L1" + - name: "head" + mates_with: "platform.tnut" +constraints: + metrology_min_percent: 99.9 + watertight: true + portal_accepted: null + mass_max_kg: null + clearance_to_roof_min_mm: 20 +non_goals: + - "не воспроизводим следы кернения оригинала" +datums: + - "базовая плоскость: опорная грань на крыше" diff --git a/rules/.gitkeep b/rules/.gitkeep new file mode 100644 index 0000000..198ecfb --- /dev/null +++ b/rules/.gitkeep @@ -0,0 +1 @@ +Пока пусто. См. README.md — назначение папки. diff --git a/rules/dfm_blechcon.json b/rules/dfm_blechcon.json new file mode 100644 index 0000000..262a57e --- /dev/null +++ b/rules/dfm_blechcon.json @@ -0,0 +1,13 @@ +{ + "_comment": "Правила поставщика. Пополняется из ответов портала и писем Blechcon.", + "bend_radius_min_factor": 1.0, + "flange_min_mm": 15, + "hole_to_bend_min_factor": 2.5, + "hole_to_edge_min_factor": 2.0, + "slot_min_width_factor": 1.0, + "sheet_max_mm": [3000, 1500], + "kerf_mm": 0.2, + "k_factor_table": {"_comment": "R/t -> K; калибруется по ответам портала", + "1.0": 0.42, "1.5": 0.44, "2.0": 0.45}, + "gls": {"max_weight_kg": 40, "max_length_mm": 2000} +} diff --git a/rules/materials.json b/rules/materials.json new file mode 100644 index 0000000..5495d20 --- /dev/null +++ b/rules/materials.json @@ -0,0 +1,12 @@ +{ + "1.4301": {"name": "Edelstahl 1.4301 (AISI 304)", "rho_kg_mm3": 7.9e-6, + "Rp02_MPa": 230, "E_MPa": 200000, "korrosion": "mittel", "uv": "bestaendig"}, + "1.4404": {"name": "Edelstahl 1.4404 (316L)", "rho_kg_mm3": 8.0e-6, + "Rp02_MPa": 220, "E_MPa": 200000, "korrosion": "hoch (Chloride)", "uv": "bestaendig"}, + "6063T6": {"name": "Aluminium 6063 T6", "rho_kg_mm3": 2.7e-6, + "Rp02_MPa": 214, "E_MPa": 69000, "korrosion": "mittel", "uv": "bestaendig"}, + "6082T6": {"name": "Aluminium 6082 T6", "rho_kg_mm3": 2.7e-6, + "Rp02_MPa": 260, "E_MPa": 70000, "korrosion": "mittel", "uv": "bestaendig"}, + "EPDM": {"name": "EPDM", "rho_kg_mm3": 1.2e-6, "uv": "bestaendig", "rolle": "Isolator"}, + "ASA": {"name": "ASA", "rho_kg_mm3": 1.07e-6, "uv": "bestaendig"} +} diff --git a/rules/tooling/README.md b/rules/tooling/README.md new file mode 100644 index 0000000..c36cf8f --- /dev/null +++ b/rules/tooling/README.md @@ -0,0 +1,11 @@ +# Инструмент прессов + +Сюда — STEP/DXF пуансонов и матриц из библиотек WILA / Rolleri после ответа Blechcon +о стандарте инструмента (Trumpf-WILA или Promecam) и ширинах матриц в наличии. + +tooling.json — по каждому инструменту: имя, стандарт, V (ширина паза), R кромки, +рабочая высота, тоннаж на метр, минимальная полка. + +Источники: портал WILA (Smart Tooling), каталоги Rolleri / ToolsPress / Precitools. +Используется: fold2 + check_tooling — булева проверка пересечения инструмента +с уже согнутой частью на каждом шаге оригами-кинематики. diff --git a/tool/freecad_LRDE-2BR1_PARAMETRIC.py b/tool/freecad_LRDE-2BR1_PARAMETRIC.py new file mode 100644 index 0000000..453ec84 --- /dev/null +++ b/tool/freecad_LRDE-2BR1_PARAMETRIC.py @@ -0,0 +1,467 @@ +# -*- coding: utf-8 -*- +# RECADA — ПАРАМЕТРИЧЕСКАЯ модель детали LRDE-2BR1-00-2B (оригами-кинематика). +# +# Модель: жёсткие пластины оболочки + шарниры на линиях гиба. +# Меняете DELTA у любого гиба — поддерево поворачивается, форма перестраивается. +# Проверено: при нулевых DELTA совпадение с оригиналом 0.00000 мм (100 % точек). +# +# Обмер: толщина 3.0 мм · гибов 7 · отверстий 6 +# объём эталона 78031.59 мм3 + +import FreeCAD as App, Part, zlib, base64, math +from FreeCAD import Vector, Placement, Rotation + +# ================= ПАРАМЕТРЫ ГИБОВ ================= +# angle — измеренный поворот; DELTA — ваша добавка в градусах +JOINTS = [ + dict(name="bend_94", angle=90.000, axis=(0.16314, -0.98660, -0.00000), point=(17.150, 6.509, 35.200), R=3.00, subtree=[13, 16, 17]), + dict(name="bend_91", angle=90.000, axis=(-0.16314, 0.98660, -0.00000), point=(17.150, 6.509, -35.200), R=3.00, subtree=[10, 14]), + dict(name="bend_25", angle=45.000, axis=(0.00000, 0.00000, 1.00000), point=(-40.276, 86.000, 37.700), R=3.00, subtree=[0, 1, 2, 3, 4, 5]), + dict(name="bend_11", angle=45.000, axis=(-0.00000, 0.00000, 1.00000), point=(-57.404, 90.400, 0.000), R=3.00, subtree=[0, 1]), + dict(name="bend_54", angle=80.611, axis=(0.00000, 0.00000, 1.00000), point=(5.494, 77.000, 0.000), R=3.00, subtree=[8]), + dict(name="bend_93", angle=90.000, axis=(0.00000, -0.00000, 1.00000), point=(30.438, -73.848, 0.000), R=3.00, subtree=[12]), + dict(name="bend_176", angle=80.611, axis=(0.00000, -0.00000, -1.00000), point=(20.648, -84.589, 0.000), R=3.00, subtree=[19]), +] + +DELTA = { # <<< МЕНЯЙТЕ ЗДЕСЬ + "bend_94": 0.0, + "bend_91": 0.0, + "bend_25": 0.0, + "bend_11": 0.0, + "bend_54": 0.0, + "bend_93": 0.0, + "bend_176": 0.0, +} + +THICKNESS = 3.0 # толщина листа + +# ================= СКЕЛЕТ (пластины оболочки) ================= +SHELL = ( + "eNrsvUvPLElyprc/vyKXumXC75fliBxpI2AEkRgthRJZ4hAgugn2UML8e72PuXtkZGbEdzLrVGmarSqSzer4PCP8Ym53e+2v/8f/7ed//j/++l9++n9++j//" + "6ee/+U8//fPP37791b/7m7/6d3/97y9/+8d//uM//fEf/svlP8b/4fLf/N1/e/kP//zzHy5/9dOf/u6nv//52//yx7/76T//4x//8KeL+/ZX//ov//fP4e//" + "dPHeffOXems59uZLTKn60Fq/xMvVX9xFfyy30KLXAFd97bnWdIk3t/vHh9oZfY234lstKcWaawn615+vPvEOd6vrC6Ek70sL+mjKemUuekWKJYSs0e5yDTf9" + "Xf+9JFe6ry5VPS8Xz3uu6ab5RU2wuah/69nzg777p/kcL4+TSbHpQ9tk/C1XVztTqT21Xppmf8sXW2/Vf7iLlh/yJcSLZq6fhpxD19hcSqxFI/PDJ3OpY6Dz" + "NQUNyq0VvbtffFk7VbXOFlywgaVp03yNPbueii9pDfQx9BZb8n28saSqPXAh9BR9qTGugTXElHMqY4bF51B8zC2zpTrFOUy7V5x2QOfJsNx0kjFoP3xyWYua" + "w6JrvDDXFvwYmF3SE+2lj67nWOfA1FLVJuQa54qzKKJpGVVf776UteLqNKdefBXJ2MDUctbeNReyz5pHmQO7HreWmiu1jIFZk9Ono9c6eg1zjj6krv1PyYU0" + "3xhcjdmFqP/vNc85LmuFer1eU7ONE7Gw5F6q/uLWDL0+3Lv3Ojw/ziTqQcgh6mnz+rf5wqClanW+OC1nDNROibarCC2EGtNcSmi+FT3T5tax3UHvj02knF3W" + "vBc5aBatJhd91nGPgbnVnGPkkF1Mbh5fbLH2oCV7/X4MDNrDnryDKnIU8dtADSlB2w19jUX7rl/5qHPqolud7RjIF4t32skex6e9qE+HmLUx2nS9eg4supWi" + "hpi0xDFQR6KJiLaa7p+ba9F/zSK+pu0NY9Happ5FEyIezbP4EMbIyu3osWn4vAQibvGIrCVqUZrAPGmOSZdTdNXbGKcNd5CmPpN0pd0cyM5qr2yH5xvZGF27" + "IGrTxLX/Y2TXdUq6SkHchnfOp6LmJNrTqlO/fBOf85p60mlBRlFHeUk8rGKJvmsaWk+AfuxhFwVErUH/1BjGw6ZrE0T6moEYZhkPuwuueW28fsNFt2eiTtGu" + "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" + "nurU2yTHrHMyVdxxbNp3ufDDLiH92uMuGUZhepRR1ZLvRxHA7kCA5JRZVW90zs2AKVbWXUeN2OHY6+Fb94w4kVrXlwsBiHeg8msA03nWQ4h/ebL1KCREXyt9" + "1Un4twjrYx78cBfDALV8uI4LXfXxNprLqzwJJImqKNXRkv4lRGQ7BGKh9J+aaa+S0ZSHFPrR9gHZbGmvZsd22vYUok3z8XP+9Hz70TTE4MmeBhQeLHkaKU8O" + "kN9nU498+M0ffcUe32TlB5z8o1N8W9I8suFH8VvHtdtL4Flk/3T0pQ4/98PRU7ICUjs1LJ3+W55F4mxf5R6vYw9fCv+h6xfOV6pokl05XKt+mMn0CTdX+cau" + "Ik4wOnRkvDRpWflDBL+nu3woOgP9g5MpvZMqgBgCo33BpH6HV70v5dPNL3V+3MX04VRllZJWI+qw5s8VhQWvuO8zYPXFVD+Z6cFEPyTivZlZDs3MH7Ayrw/L" + "JKt6WTZv2vKnXswHA5ucjR6ezZJIXOLiLodPD39udvfm48hupAmUQx/Hj7g4Tj0c3/UkHZqrYhw43Wi27TeAp3yzrHu4dy7uzI0kkZ3qkx/Jjw7ZBP1oGBcA" + "fyF+mQ78SGPs0Stl19Pr2xAvca63vtxIb7sBz13Y+w9KE6Ts4XFi2VrojNN/fXr4+1ff56rU3/s+82ke1S9xfb56dM/XPNovWQo6ELZp57S9nvxl4Dc9+60f" + "3dYTH/IDrzW4eqyQbJRZWP6+1/50fSQ2FpIUvHVUGh+buIDX0z8fRCX8Q1QiX36FeMRbEZcvXPG7gEKle5cfYaat4sfaioXqJyjV6/OjF8yl34NP2U/Zsg8+" + "ldNNsCBTsvWS3bEINpj06xI7VvS3AxB7O6h36H91hqBYMehGFxALySWKqvLgW6uRwRVoWW+QDdLGNzK4j6d6lYSMzV979IPtg1ShuG79ZUa8bhcB7eE0AvrD" + "gdaHC3/ggcyXM9+j5AzFq8PBfu57nExk73tMX542+Mp4pUqp6yIv6TzO2YUj92k8cALPcrIzH7BxDe1n/9oHPNFg9j7gPJXzY+eprcGlkbV276HwxToeNmzo" + "UfsNm0GM5D/x1e6UmonFd+h1njB99jlADOsXIIbl+Fi/+pmV/OH8kpVcw7yNV/E+It2BBJ82tZeGMLZQECg3ee5bHl1sZIgg4Cl+ninjdqeMv5z8ypI5dGdR" + "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" +) + +_brep = zlib.decompress(base64.b64decode(SHELL)).decode("utf-8", "ignore") +_c = Part.Shape(); _c.importBrepFromString(_brep) +plates = list(_c.Faces) +print("пластин в оболочке:", len(plates)) + +# ---- складывание: поворот поддерева вокруг оси гиба ---- +for J in JOINTS: + d = DELTA.get(J["name"], 0.0) + if abs(d) < 1e-9: + continue + ax_p = Vector(*J["point"]); ax_d = Vector(*J["axis"]) + rot = Rotation(ax_d, d) + pl = Placement(ax_p, rot, Vector(0, 0, 0)) + pl.Base = ax_p - rot.multVec(ax_p) + for k in J["subtree"]: + plates[k] = plates[k].transformGeometry(pl.toMatrix()) + print(" %s: поворот на %+.2f° (пластин %d)" % (J["name"], d, len(J["subtree"]))) + +shell = Part.Shell(plates) +if App.ActiveDocument is None: + App.newDocument("RECADA") +Part.show(shell, "RECADA_shell") + +# ---- попытка нарастить на толщину (может не пройти на сложной геометрии) ---- +solid = None +for _join in (2, 1, 0): + for _sgn in (-1, 1): + try: + _s = shell.makeOffsetShape(_sgn * THICKNESS, 1.0e-4, False, False, 0, _join, True) + if _s.Volume > 0: + solid = _s; print("тело построено: join=%d знак=%+d" % (_join, _sgn)); break + except Exception: + pass + if solid: break +if solid is not None: + Part.show(solid, "RECADA_solid") + print("объём:", round(solid.Volume, 1)) +else: + print("Тело не построилось — показана оболочка. Толщину можно задать") + print("в SheetMetal/Thickness вручную или экспортировать оболочку в STEP.") +App.ActiveDocument.recompute() diff --git a/tool/freecad_LRDE-2BR1_solid.py b/tool/freecad_LRDE-2BR1_solid.py new file mode 100644 index 0000000..55fb563 --- /dev/null +++ b/tool/freecad_LRDE-2BR1_solid.py @@ -0,0 +1,1001 @@ +# -*- coding: utf-8 -*- +# RECADA — деталь LRDE-2BR1-00-2B, воссоздание во FreeCAD. +# Тело построено и проверено метрологией: 100.000 % точек в допуске 0.05 мм. +# Внешние файлы НЕ нужны — геометрия вшита в этот текст. +# +# ОБМЕР: толщина 3.0 мм · гибов 7 · отверстий 6 +# объём эталона 78031.59 мм3 · граней 187 (112 плоскостей, 71 цилиндр, 4 сплайна) +# +# ГИБЫ: +# поворот 90.00° двугранный 90.00° R3.0 длина 144.9 разгрузка +# поворот 90.00° двугранный 90.00° R3.0 длина 144.9 разгрузка +# поворот 45.00° двугранный 135.00° R3.0 длина 75.4 разгрузка +# поворот 45.00° двугранный 135.00° R3.0 длина 32.0 разгрузка +# поворот 80.61° двугранный 99.39° R3.0 длина 27.4 разгрузка +# поворот 90.00° двугранный 90.00° R3.0 длина 27.2 разгрузка +# поворот 80.61° двугранный 99.39° R3.0 длина 27.2 +# +# ОТВЕРСТИЯ: +# D3.0 центр (-76.16, 0.00, 0.00) +# D8.5 центр (-66.16, 0.00, 0.00) +# D8.5 центр (-9.76, 0.00, 0.00) +# D20.0 центр (0.85, -5.25, -0.00) +# D20.0 центр (-3.22, 19.41, 0.00) +# D20.0 центр (-7.30, 44.08, 0.00) + +import FreeCAD as App, Part, zlib, base64 + +BODY = ( + "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" + "2GmmF9LRYAP3Ts80YK45debcsO+76Pf4AEx9Fo0=" +) + +_brep = zlib.decompress(base64.b64decode(BODY)).decode("utf-8", "ignore") +solid = Part.Shape() +solid.importBrepFromString(_brep) + +if App.ActiveDocument is None: + App.newDocument("RECADA") +Part.show(solid, "LRDE_2BR1_00_2B") +App.ActiveDocument.recompute() +print("граней:", len(solid.Faces), " объём:", round(solid.Volume, 1), + "мм3 эталон:", 78031.59) diff --git a/tool/recada.py b/tool/recada.py new file mode 100644 index 0000000..8bc2cb6 --- /dev/null +++ b/tool/recada.py @@ -0,0 +1,4008 @@ +""" +recada.py — инструмент реверса листовых деталей RECADA (единый файл, без FreeCAD) +CLI: python recada.py probe|metro|unfold|dxf|rebuild|fold|fold2|stretch|check|fit файл.step +Браузер: streamlit run recada.py +""" +import sys, os, json, math, tempfile +from collections import defaultdict +import numpy as np +import networkx as nx +from scipy.optimize import minimize +from build123d import export_step +import json, math, sys +from collections import defaultdict + +import numpy as np +import networkx as nx + +from OCP.STEPControl import STEPControl_Reader +from OCP.IFSelect import IFSelect_RetDone +from OCP.TopExp import TopExp_Explorer, TopExp +from OCP.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_WIRE, TopAbs_VERTEX +from OCP.TopoDS import TopoDS +from OCP.BRepAdaptor import BRepAdaptor_Surface +from OCP.GeomAbs import GeomAbs_Plane, GeomAbs_Cylinder, GeomAbs_Cone, GeomAbs_BSplineSurface, GeomAbs_Torus +from OCP.GProp import GProp_GProps +from OCP.BRepGProp import BRepGProp +from OCP.TopTools import (TopTools_IndexedDataMapOfShapeListOfShape, + TopTools_IndexedMapOfShape) +from OCP.BRepBndLib import BRepBndLib +from OCP.Bnd import Bnd_Box +from OCP.BRepTools import BRepTools + + +# ---------- утилиты ---------- + +def read_step(path): + reader = STEPControl_Reader() + if reader.ReadFile(path) != IFSelect_RetDone: + raise RuntimeError("STEP не прочитан") + reader.TransferRoots() + return reader.OneShape() + + +def face_area(f): + props = GProp_GProps() + BRepGProp.SurfaceProperties_s(f, props) + return props.Mass() + + +def solid_volume(shape): + props = GProp_GProps() + BRepGProp.VolumeProperties_s(shape, props) + return props.Mass() + + +def count_sub(shape, t): + ex, n = TopExp_Explorer(shape, t), 0 + seen = [] + while ex.More(): + n += 1 + ex.Next() + return n + + +def surf_info(f): + """тип поверхности + параметры""" + a = BRepAdaptor_Surface(f) + t = a.GetType() + if t == GeomAbs_Plane: + pl = a.Plane() + n = pl.Axis().Direction() + p = pl.Location() + return dict(kind="plane", + normal=[n.X(), n.Y(), n.Z()], + point=[p.X(), p.Y(), p.Z()]) + if t == GeomAbs_Cylinder: + cy = a.Cylinder() + ax = cy.Axis().Direction() + loc = cy.Axis().Location() + return dict(kind="cylinder", radius=cy.Radius(), + axis=[ax.X(), ax.Y(), ax.Z()], + axis_point=[loc.X(), loc.Y(), loc.Z()]) + if t == GeomAbs_Cone: + return dict(kind="cone") + if t == GeomAbs_Torus: + return dict(kind="torus") + if t == GeomAbs_BSplineSurface: + return dict(kind="bspline") + return dict(kind="other(%d)" % t) + + +def uv_extent(f): + umin, umax, vmin, vmax = BRepTools.UVBounds_s(f) + return umin, umax, vmin, vmax + + +# ---------- основной анализ ---------- + +def probe(path, thickness_hint=None): + shape = read_step(path) + vol = solid_volume(shape) + + # 1. собрать грани (индексированная карта = стабильные id) + fmap = TopTools_IndexedMapOfShape() + TopExp.MapShapes_s(shape, TopAbs_FACE, fmap) + faces = [TopoDS.Face_s(fmap.FindKey(i)) for i in range(1, fmap.Extent() + 1)] + + info = [] + for i, f in enumerate(faces): + si = surf_info(f) + si["id"] = i + si["area"] = face_area(f) + si["n_wires"] = count_sub(f, TopAbs_WIRE) + si["n_edges"] = count_sub(f, TopAbs_EDGE) + si["uv"] = list(uv_extent(f)) + info.append(si) + + # 2. AAG: грани-соседи через общие рёбра + m = TopTools_IndexedDataMapOfShapeListOfShape() + TopExp.MapShapesAndAncestors_s(shape, TopAbs_EDGE, TopAbs_FACE, m) + G = nx.Graph() + for i in range(len(faces)): + G.add_node(i, **{k: v for k, v in info[i].items() if k != "uv"}) + + for k in range(1, m.Extent() + 1): + lst = m.FindFromIndex(k) + ids = [] + for sh in lst: + idx = fmap.FindIndex(sh) + if idx > 0: + ids.append(idx - 1) + for a in range(len(ids)): + for b in range(a + 1, len(ids)): + if ids[a] != ids[b]: + G.add_edge(ids[a], ids[b], edge_key=k) + + return shape, faces, info, G, vol + + + +"""Детекция отверстий по круглым рёбрам (устойчивее, чем по цилиндрам)""" +import math +from collections import defaultdict +import numpy as np +from OCP.TopExp import TopExp_Explorer +from OCP.TopAbs import TopAbs_EDGE +from OCP.TopoDS import TopoDS +from OCP.BRepAdaptor import BRepAdaptor_Curve +from OCP.GeomAbs import GeomAbs_Circle + + +def circular_edges(shape): + out = [] + ex = TopExp_Explorer(shape, TopAbs_EDGE) + while ex.More(): + e = TopoDS.Edge_s(ex.Current()) + try: + c = BRepAdaptor_Curve(e) + if c.GetType() == GeomAbs_Circle: + circ = c.Circle() + sweep = c.LastParameter() - c.FirstParameter() + loc, ax = circ.Location(), circ.Axis().Direction() + out.append(dict(r=circ.Radius(), + sweep=math.degrees(sweep), + c=(round(loc.X(),3), round(loc.Y(),3), round(loc.Z(),3)), + ax=(round(ax.X(),3), round(ax.Y(),3), round(ax.Z(),3)))) + except Exception: + pass + ex.Next() + return out + + +import json, math, sys +from collections import defaultdict +import numpy as np + + + +TOL_DIR = 1e-4 # совпадение направлений +TOL_POS = 1e-3 # совпадение положения оси + + +def v(a): + return np.array(a, dtype=float) + + +def unit(a): + a = v(a) + n = np.linalg.norm(a) + return a / n if n > 1e-12 else a + + +def axis_key(info): + """ключ оси цилиндра: направление (с точностью до знака) + положение линии""" + d = unit(info["axis"]) + if (d[0], d[1], d[2]) < (-d[0], -d[1], -d[2]): + d = -d + p = v(info["axis_point"]) + # точка, ближайшая к началу координат на этой оси + p_perp = p - np.dot(p, d) * d + return (tuple(np.round(d, 4)), tuple(np.round(p_perp, 3))) + + +def measure(path): + shape, faces, info, G, vol = probe(path) + + cyl = [i for i in info if i["kind"] == "cylinder"] + pln = [i for i in info if i["kind"] == "plane"] + + # ---------- группировка цилиндров по осям ---------- + groups = defaultdict(list) + for c in cyl: + groups[axis_key(c)].append(c) + + # ---------- толщина: соосные пары с разностью радиусов ---------- + dr_hist = defaultdict(int) + coax_pairs = [] + for key, g in groups.items(): + radii = sorted(set(round(x["radius"], 3) for x in g)) + for a in range(len(radii)): + for b in range(a + 1, len(radii)): + d = round(radii[b] - radii[a], 3) + if 0.3 < d < 10: + dr_hist[d] += 1 + coax_pairs.append((key, radii[a], radii[b], d)) + thickness = max(dr_hist.items(), key=lambda kv: kv[1])[0] if dr_hist else None + + # ---------- толщина по парам параллельных плоскостей (контроль) ---------- + par_d = defaultdict(int) + for i in range(len(pln)): + for j in range(i + 1, len(pln)): + n1, n2 = unit(pln[i]["normal"]), unit(pln[j]["normal"]) + if abs(abs(np.dot(n1, n2)) - 1) < 1e-3: + d = abs(np.dot(v(pln[j]["point"]) - v(pln[i]["point"]), n1)) + if 0.3 < d < 10: + par_d[round(d, 2)] += 1 + t_planes = max(par_d.items(), key=lambda kv: kv[1])[0] if par_d else None + + # ---------- классификация цилиндров ---------- + bends, holes, others = [], [], [] + for key, g in groups.items(): + radii = {round(x["radius"], 3): x for x in g} + rs = sorted(radii) + # гиб: две соосные грани с разностью радиусов = толщина + matched = set() + for a in range(len(rs)): + for b in range(a + 1, len(rs)): + if thickness and abs((rs[b] - rs[a]) - thickness) < 0.05: + inner, outer = radii[rs[a]], radii[rs[b]] + sweep_i = inner["uv"][1] - inner["uv"][0] + sweep_o = outer["uv"][1] - outer["uv"][0] + length = abs(inner["uv"][3] - inner["uv"][2]) + bends.append(dict( + inner_face=inner["id"], outer_face=outer["id"], + r_inner=rs[a], r_outer=rs[b], + angle_deg=round(math.degrees(max(sweep_i, sweep_o)), 3), + dihedral_deg=round(180 - math.degrees(max(sweep_i, sweep_o)), 3), + length=round(length, 2), + axis=[float(q) for q in np.round(unit(inner["axis"]), 5)], + axis_point=[round(x, 3) for x in inner["axis_point"]], + n_wires=inner["n_wires"], n_edges=inner["n_edges"], + relief=bool(inner["n_wires"] > 1 or inner["n_edges"] > 4 + or outer["n_wires"] > 1 or outer["n_edges"] > 4), + )) + matched.add(rs[a]); matched.add(rs[b]) + # суммируем развороты частичных цилиндров одного радиуса на одной оси + by_r = defaultdict(list) + for x in g: + rr = round(x["radius"], 3) + if rr not in matched: + by_r[rr].append(x) + for r, lst in by_r.items(): + sweep_tot = sum(x["uv"][1] - x["uv"][0] for x in lst) + f = lst[0] + depth = max(abs(x["uv"][3] - x["uv"][2]) for x in lst) + rec = dict(faces=[x["id"] for x in lst], radius=r, + sweep_deg=round(math.degrees(sweep_tot), 1), + depth=round(depth, 2), pieces=len(lst), + axis=[float(q) for q in np.round(unit(f["axis"]), 5)], + axis_point=[round(x, 3) for x in f["axis_point"]]) + if abs(sweep_tot - 2 * math.pi) < 1e-2: + holes.append(rec) # полный оборот = отверстие + else: + others.append(rec) + + # ---------- отверстия по круглым рёбрам (надёжнее цилиндров) ---------- + ce = circular_edges(shape) + arc_sum = defaultdict(float); + for x in ce: + d = np.array(x["ax"], float); n = np.linalg.norm(d) + if n < 1e-9: continue + d = d / n + if tuple(d) < tuple(-d): d = -d + arc_sum[(round(x["r"], 2), tuple(np.round(d, 3)), tuple(np.round(x["c"], 2)))] += x["sweep"] + circles = [k for k, s in arc_sum.items() if s > 359] + hole_axes = defaultdict(list) + keys = [] + for (r, ax, c) in circles: + p = np.array(c, float); dd = np.array(ax, float) + perp = p - np.dot(p, dd) * dd + # склейка с допуском 0.05 мм: ищем уже существующую близкую ось + found = None + for (kr, kax, kperp) in keys: + if abs(kr - r) < 0.02 and abs(abs(float(np.dot(np.array(kax), dd))) - 1) < 1e-3 \ + and float(np.linalg.norm(np.array(kperp) - perp)) < 0.05: + found = (kr, kax, kperp); break + if found is None: + found = (r, ax, tuple(np.round(perp, 3))); keys.append(found) + hole_axes[found].append(list(c)) + hole_by_d = defaultdict(list) + for (r, ax, perp), cs in hole_axes.items(): + hole_by_d[round(r * 2, 2)].append(dict(axis=list(ax), center=list(perp), circles=len(cs))) + + # ---------- разгрузки: гибы с нарушенной каноникой ---------- + reliefs = [b for b in bends if b["relief"]] + + # ---------- габарит ---------- + from OCP.Bnd import Bnd_Box + from OCP.BRepBndLib import BRepBndLib + bb = Bnd_Box() + BRepBndLib.Add_s(shape, bb) + xmin, ymin, zmin, xmax, ymax, zmax = bb.Get() + + out = dict( + source=path.split("/")[-1], + volume_mm3=round(vol, 2), + bbox=dict(dx=round(xmax - xmin, 2), dy=round(ymax - ymin, 2), dz=round(zmax - zmin, 2)), + faces_total=len(info), + faces_by_kind={k: sum(1 for i in info if i["kind"] == k) for k in set(i["kind"] for i in info)}, + thickness_from_bends=thickness, + thickness_from_planes=t_planes, + bends=sorted(bends, key=lambda b: -b["length"]), + holes_total=sum(len(v_) for v_ in hole_by_d.values()), + holes_by_diameter={str(d): dict(count=len(v_), items=v_) for d, v_ in sorted(hole_by_d.items())}, + arcs_other=len(others), + reliefs_detected=len(reliefs), + ) + return out + + + +import sys, math +import numpy as np + +from OCP.TopExp import TopExp +from OCP.TopAbs import TopAbs_FACE +from OCP.TopoDS import TopoDS, TopoDS_Vertex +from OCP.TopTools import TopTools_IndexedMapOfShape +from OCP.BRepAdaptor import BRepAdaptor_Surface +from OCP.BRepTools import BRepTools +from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeVertex +from OCP.BRepExtrema import BRepExtrema_DistShapeShape +from OCP.gp import gp_Pnt +from OCP.BRepTopAdaptor import BRepTopAdaptor_FClass2d +from OCP.gp import gp_Pnt2d +from OCP.TopAbs import TopAbs_OUT, TopAbs_ON +from OCP.GProp import GProp_GProps +from OCP.BRepGProp import BRepGProp + + + + + +def _as_solid(shape): + """Замкнутую оболочку превратить в SOLID — иначе порталы поставщиков + (Blechcon и др.) не распознают деталь: видят поверхность без тела.""" + from OCP.TopAbs import TopAbs_SOLID, TopAbs_SHELL + from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid + from OCP.BRepClass3d import BRepClass3d_SolidClassifier + ex = TopExp_Explorer(shape, TopAbs_SOLID) + if ex.More(): + return shape # уже тело + ex = TopExp_Explorer(shape, TopAbs_SHELL) + if not ex.More(): + return shape + from OCP.TopoDS import TopoDS + from OCP.BRepCheck import BRepCheck_Shell, BRepCheck_NoError + mk = BRepBuilderAPI_MakeSolid() + while ex.More(): + sh = TopoDS.Shell_s(ex.Current()) + try: + chk = BRepCheck_Shell(sh) + if chk.Closed() == BRepCheck_NoError: + sh.Closed(True) # без флага STEP запишет поверхность, не тело + except Exception: + sh.Closed(True) + mk.Add(sh) + ex.Next() + if not mk.IsDone(): + return shape + sol = mk.Solid() + # проверить ориентацию: объём должен быть положительным + p = GProp_GProps(); BRepGProp.VolumeProperties_s(sol, p) + if p.Mass() < 0: + from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeShape + sol.Reverse() + return sol + + +def surface_of(shape): + """Оболочка (только грани) — цель для замера расстояний. + ВАЖНО: distToShape до СОЛИДА возвращает 0 для точек внутри материала, + поэтому мерить всегда до поверхности, а не до тела.""" + from OCP.TopAbs import TopAbs_SHELL + from OCP.TopoDS import TopoDS_Compound + from OCP.BRep import BRep_Builder + ex = TopExp_Explorer(shape, TopAbs_SHELL) + comp = TopoDS_Compound(); b = BRep_Builder(); b.MakeCompound(comp) + n = 0 + while ex.More(): + b.Add(comp, ex.Current()); n += 1; ex.Next() + if n == 0: + fm = TopTools_IndexedMapOfShape() + TopExp.MapShapes_s(shape, TopAbs_FACE, fm) + for i in range(1, fm.Extent() + 1): + b.Add(comp, fm.FindKey(i)) + return comp + + +def sample_points(shape, per_face=6, seed=7): + """равномерная выборка точек по UV каждой грани""" + rng = np.random.default_rng(seed) + fmap = TopTools_IndexedMapOfShape() + TopExp.MapShapes_s(shape, TopAbs_FACE, fmap) + pts = [] + for i in range(1, fmap.Extent() + 1): + f = TopoDS.Face_s(fmap.FindKey(i)) + umin, umax, vmin, vmax = BRepTools.UVBounds_s(f) + surf = BRepAdaptor_Surface(f) + clf = BRepTopAdaptor_FClass2d(f, 1e-6) + got, tries = 0, 0 + while got < per_face and tries < per_face * 40: + tries += 1 + u = umin + (umax - umin) * rng.random() + v = vmin + (vmax - vmin) * rng.random() + if clf.Perform(gp_Pnt2d(u, v)) in (TopAbs_OUT,): + continue # точка вне обрезанной грани — пропускаем + try: + p = surf.Value(u, v) + pts.append((p.X(), p.Y(), p.Z())) + got += 1 + except Exception: + pass + return pts + + +def deviations(ref_shape, cand_shape, per_face=6): + pts = sample_points(ref_shape, per_face) + target = surface_of(cand_shape) # до ПОВЕРХНОСТИ, не до тела + dev = [] + for (x, y, z) in pts: + vtx = BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z)).Vertex() + d = BRepExtrema_DistShapeShape(vtx, target) + d.Perform() + if d.IsDone(): + dev.append(d.Value()) + return np.array(dev) + + +def volume(shape): + p = GProp_GProps() + BRepGProp.VolumeProperties_s(shape, p) + return p.Mass() + + +def report(ref_path, cand_path, per_face=6, tol=0.05): + ref = read_step(ref_path) + cand = read_step(cand_path) if isinstance(cand_path, str) else cand_path + dev = deviations(ref, cand, per_face) + vr, vc = volume(ref), volume(cand) + inside = float((dev <= tol).sum()) / len(dev) * 100 + print("=" * 58) + print("МЕТРОЛОГИЯ") + print(" контрольных точек : %d" % len(dev)) + print(" среднее отклонение : %.5f мм" % dev.mean()) + print(" максимальное отклонение : %.5f мм" % dev.max()) + print(" 95-й процентиль : %.5f мм" % np.percentile(dev, 95)) + print(" точек в допуске %.2f мм : %.3f %%" % (tol, inside)) + print(" объём эталон / кандидат : %.2f / %.2f мм3 (Δ %.4f %%)" + % (vr, vc, (vc - vr) / vr * 100 if vr else 0)) + verdict = "ПРИНЯТО" if (inside >= 99.9 and abs(vc - vr) / vr * 100 < 1) else "НЕ ПРИНЯТО" + print(" вердикт (порог 99.9%%) : %s" % verdict) + print("=" * 58) + return dict(points=len(dev), mean=float(dev.mean()), max=float(dev.max()), + inside_pct=inside, vol_ref=vr, vol_cand=vc) + + + +""" +recada_lib.py — кубики листового металла для build123d (RECADA) +Своя реализация: в build123d 0.11 операций Bend/Flange/Hem НЕТ. + +Модель детали: цепочка сегментов вдоль сечения + ("flat", L) — прямой участок длиной L + ("bend", angle_deg, r_in) — гиб: поворот на angle_deg, внутренний радиус r_in +Сечение "протягивается" на ширину W. Гибы с параллельными осями. +""" +import math +from build123d import (BuildPart, BuildSketch, BuildLine, Line, JernArc, Polyline, + trace, extrude, Plane, Axis, Location, Locations, Hole, + Mode, Vector, export_step) + + +# ---------- математика развёртки ---------- + +def k_factor(r_inner, t, table=None): + """K-фактор по таблице R/t (Material Definition Sheet). Дефолт — практика для нержавейки.""" + if table: + ratio = r_inner / t + best = min(table, key=lambda x: abs(x - ratio)) + return table[best] + ratio = r_inner / t + if ratio < 1.0: return 0.38 + if ratio < 2.0: return 0.42 + if ratio < 4.0: return 0.44 + return 0.46 + + +def bend_allowance(angle_deg, r_inner, t, k=None): + """BA — длина нейтральной оси в зоне гиба (припуск на изгиб).""" + k = k_factor(r_inner, t) if k is None else k + return math.radians(angle_deg) * (r_inner + k * t) + + +def bend_deduction(angle_deg, r_inner, t, k=None): + """BD — вычет: OSSB*2 - BA.""" + a = math.radians(angle_deg) + ossb = (r_inner + t) * math.tan(min(a, math.radians(179.9)) / 2) + return 2 * ossb - bend_allowance(angle_deg, r_inner, t, k) + + +def flat_length(segments, t, k=None): + """Длина развёртки цепочки сегментов.""" + L = 0.0 + for s in segments: + if s[0] == "flat": + L += s[1] + elif s[0] == "bend": + L += bend_allowance(s[1], s[2], t, k) + return L + + +# ---------- построение тела ---------- + +def sheet_profile_line(segments, t): + """Линия по нейтральной оси сечения (радиус дуги = r_in + t/2).""" + with BuildLine() as bl: + pos = Vector(0, 0) + tangent = Vector(1, 0) + for s in segments: + if s[0] == "flat": + nxt = pos + tangent * s[1] + Line(pos, nxt) + pos = nxt + elif s[0] == "bend": + ang, r_in = s[1], s[2] + arc = JernArc(start=pos, tangent=tangent, + radius=r_in + t / 2, arc_size=ang) + pos = arc @ 1 + tangent = arc % 1 + return bl.line + + +def make_bent_part(segments, width, t, holes=None): + """Тело детали: сечение по нейтральной оси, утолщённое на t, протянутое на ширину.""" + line = sheet_profile_line(segments, t) + with BuildPart() as part: + with BuildSketch(Plane.XZ) as sk: + trace(line, line_width=t) + extrude(amount=width) + if holes: + for h in holes: + with Locations(Location(Vector(*h["at"]))): + Hole(radius=h["d"] / 2) + return part.part + + +# ---------- самопроверка ---------- + + +import json, math +from collections import defaultdict +import numpy as np + + +def _axis_groups(bends, tol=0.02): + """группируем гибы по направлению оси (параллельные оси = одна цепочка)""" + groups = [] + for b in bends: + d = np.array(b["axis"], float) + d = d / np.linalg.norm(d) + placed = False + for g in groups: + if abs(abs(float(np.dot(d, g["dir"]))) - 1) < tol: + g["items"].append(b); placed = True; break + if not placed: + groups.append(dict(dir=d, items=[b])) + return groups + + +def make_params(res): + t = res["thickness_from_bends"] + p = { + "sku": "TODO-SKU", + "vehicle": "TODO-vehicle", + "material": {"grade": "1.4301 2B", "thickness": t, "k_factor": "from_table"}, + "bend_radius_inner": res["bends"][0]["r_inner"] if res["bends"] else None, + "bends": [{"rotation_deg": b["angle_deg"], "dihedral_deg": b["dihedral_deg"], + "length": b["length"], "axis": b["axis"], "relief": b["relief"]} + for b in res["bends"]], + "holes": {d: {"count": g["count"], "centers": [i["center"] for i in g["items"]]} + for d, g in res["holes_by_diameter"].items()}, + "bbox": res["bbox"], + "reference_volume_mm3": res["volume_mm3"], + } + return p + + +def make_generator(res, module_name="recada"): + t = res["thickness_from_bends"] + bends = res["bends"] + groups = _axis_groups(bends) + single_chain = len(groups) == 1 + r_in = bends[0]["r_inner"] if bends else 3.0 + + L = [] + A = L.append + A('"""') + A("Черновик параметрического генератора (создан автоматически из обмера STEP).") + A("ПРОВЕРИТЬ ИНЖЕНЕРУ: что параметр, что правило, что артефакт производства.") + A('"""') + A("import json") + A(f"from {module_name} import make_bent_part, flat_length, bend_allowance") + A("from build123d import export_step") + A("") + A("p = json.load(open('params.json'))") + A("t = p['material']['thickness'] # ПАРАМЕТР материала") + A(f"R = p['bend_radius_inner'] # ПРАВИЛО: R = {round(r_in / t, 2)} x t" + if t else "R = p['bend_radius_inner']") + A("") + A("# ---- гибы (углы поворота; двугранный = 180 - поворот) ----") + for i, b in enumerate(bends, 1): + note = " # разгрузка: генерируется правилом features.py" if b["relief"] else "" + A(f"ANG{i} = {b['angle_deg']:.3f} # двугранный {b['dihedral_deg']:.2f}°, " + f"длина гиба {b['length']:.1f} мм{note}") + A("") + A("# ---- прямые участки (между осями гибов вычислены из обмера) ----") + P = [np.array(b["axis_point"], float) for b in bends] if bends and "axis_point" in bends[0] else [] + A("FLAT0 = 0.0 # TODO: от кромки до первого гиба (измерить)") + for i in range(1, len(bends)): + if len(P) > i: + d = float(np.linalg.norm(P[i] - P[i - 1])) + A(f"FLAT{i} = {d:.3f} # расстояние между осями гибов {i} и {i+1}") + else: + A(f"FLAT{i} = 0.0 # TODO") + A(f"FLAT{len(bends)} = 0.0 # TODO: от последнего гиба до кромки (измерить)") + A("") + + if single_chain: + A("# все оси гибов параллельны -> деталь строится сечением + протяжкой") + segs = [] + for i, b in enumerate(bends, 1): + segs.append(f'("flat", FLAT{i-1})') + segs.append(f'("bend", ANG{i}, R)') + segs.append(f'("flat", FLAT{len(bends)})') + A("segments = [\n " + ",\n ".join(segs) + ",\n]") + A("WIDTH = %.1f # длина гибов из обмера" % max(b["length"] for b in bends)) + A("") + A("part = make_bent_part(segments, width=WIDTH, t=t)") + else: + A("# ВНИМАНИЕ: оси гибов НЕ параллельны (%d направлений) —" % len(groups)) + A("# деталь пространственная, одной протяжкой не строится.") + A("# Порядок: базовая пластина -> цепочки гибов по группам осей.") + for gi, g in enumerate(groups, 1): + ax = ", ".join(f"{x:.3f}" for x in g["dir"]) + A(f"# группа {gi}: ось [{ax}], гибов {len(g['items'])}: " + + ", ".join("%.2f°" % b["angle_deg"] for b in g["items"])) + A("") + A("# порядок гибов по удалению от базовой грани (черновая цепочка):") + try: + order = sorted(range(len(bends)), key=lambda i: -bends[i]["length"]) + for k, i in enumerate(order, 1): + b = bends[i] + A("# %d) поворот %.2f° вокруг оси [%s] через точку (%s), длина %.1f%s" + % (k, b["angle_deg"], ", ".join("%.3f" % x for x in b["axis"]), + ", ".join("%.1f" % x for x in b["axis_point"]), b["length"], + ", разгрузка" if b["relief"] else "")) + except Exception: + pass + A("part = None # TODO: собрать цепочку поворотов вокруг осей выше") + + A("") + A("# ---- отверстия ----") + for d, g in res["holes_by_diameter"].items(): + A(f"# D{d}: {g['count']} шт") + for it in g["items"]: + c = ", ".join("%.2f" % x for x in it["center"]) + A(f"# центр ({c}) ось ({', '.join('%.2f' % x for x in it['axis'])})") + A("# TODO: перенести в hole_array() с привязкой к базам (datums), а не к нулю CAD") + A("") + A("# ---- контроль ----") + A(f"REFERENCE_VOLUME = {res['volume_mm3']} # мм3, эталон из STEP") + A("if part is not None:") + A(" print('объём:', round(part.volume, 1), 'эталон:', REFERENCE_VOLUME)") + A(" print('развёртка:', round(flat_length(segments, t), 2), 'мм')" + if single_chain else " pass") + A(" export_step(part, 'out.step')") + return "\n".join(L) + + + +import json, math, sys, tempfile, os +import numpy as np +from scipy.optimize import minimize + + + + +def build_from_vector(x, angles, r_in, t): + """x = [flat_0, flat_1, ..., flat_n, width]""" + flats, width = x[:-1], x[-1] + segs = [] + for i, a in enumerate(angles): + segs.append(("flat", max(float(flats[i]), 0.01))) + segs.append(("bend", float(a), r_in)) + segs.append(("flat", max(float(flats[-1]), 0.01))) + return make_bent_part(segs, width=max(float(width), 1.0), t=t), segs + + +def objective(x, ref_shape, angles, r_in, t, ref_pts, tmp): + try: + part, _ = build_from_vector(x, angles, r_in, t) + except Exception: + return 1e6 + p = os.path.join(tmp, "cand.step") + export_step(part, p) + cand = read_step(p) + # отклонение точек эталона до кандидата + from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeVertex + from OCP.BRepExtrema import BRepExtrema_DistShapeShape + from OCP.gp import gp_Pnt + def rms(points, target): + s = 0.0 + for (px, py, pz) in points: + v = BRepBuilderAPI_MakeVertex(gp_Pnt(px, py, pz)).Vertex() + d = BRepExtrema_DistShapeShape(v, target) + d.Perform() + s += d.Value() ** 2 if d.IsDone() else 100.0 + return math.sqrt(s / max(len(points), 1)) + # ДВУСТОРОННЯЯ невязка: эталон->кандидат И кандидат->эталон + # (односторонняя не видит лишнего металла на кандидате) + cand_pts = sample_points(cand, 2) + return rms(ref_pts, surface_of(cand)) + rms(cand_pts, surface_of(ref_shape)) + + +def fit(step_path, x0=None, maxiter=60, sample_per_face=3, verbose=True): + res = measure(step_path) + t = res["thickness_from_bends"] + bends = res["bends"] + r_in = bends[0]["r_inner"] + angles = [b["angle_deg"] for b in bends] + + # известные из обмера: расстояния между осями гибов + P = [np.array(b["axis_point"], float) for b in bends] + mid = [float(np.linalg.norm(P[i] - P[i - 1])) for i in range(1, len(P))] + width0 = max(b["length"] for b in bends) + bb = res["bbox"] + guess_end = max(bb["dx"], bb["dy"], bb["dz"]) * 0.25 # грубая догадка + + x0 = x0 or ([guess_end] + mid + [guess_end] + [width0]) + ref = read_step(step_path) + ref_pts = sample_points(ref, sample_per_face) + tmp = tempfile.mkdtemp() + + if verbose: + print("подбираем %d параметров, контрольных точек %d" % (len(x0), len(ref_pts))) + print("старт:", [round(v, 2) for v in x0]) + + hist = [] + + def cb(xk): + val = objective(xk, ref, angles, r_in, t, ref_pts, tmp) + hist.append(val) + if verbose: + print(" итерация %2d: RMS %.4f мм x=%s" % + (len(hist), val, [round(float(v), 2) for v in xk])) + + r = minimize(objective, x0, args=(ref, angles, r_in, t, ref_pts, tmp), + method="Powell", callback=cb, + options=dict(maxiter=maxiter, xtol=1e-3, ftol=1e-4)) + + part, segs = build_from_vector(r.x, angles, r_in, t) + out = os.path.join(tmp, "fitted.step") + export_step(part, out) + return dict(x=[float(v) for v in r.x], rms=float(r.fun), segments=segs, + thickness=t, r_inner=r_in, angles=angles, + fitted_step=out, ref_step=step_path, measurements=res) + + + + + + +def export_unfold_dxf(res, path="unfold.dxf", width=None): + """Развёртка в DXF: контур заготовки + линии гиба (для деталей с параллельными осями).""" + import ezdxf + t = res["thickness_from_bends"] + bends = res["bends"] + groups = _axis_groups(bends) + if len(groups) != 1: + raise ValueError("оси гибов не параллельны — развёртка одной картой невозможна") + W = width or max(b["length"] for b in bends) + P = [np.array(b["axis_point"], float) for b in bends] + flats = [float(np.linalg.norm(P[i] - P[i-1])) for i in range(1, len(P))] + L = sum(flats) + sum(bend_allowance(b["angle_deg"], b["r_inner"], t) for b in bends) + doc = ezdxf.new(); msp = doc.modelspace() + msp.add_lwpolyline([(0,0),(L,0),(L,W),(0,W),(0,0)], dxfattribs={"layer":"CONTOUR"}) + x = 0.0 + for i, b in enumerate(bends): + ba = bend_allowance(b["angle_deg"], b["r_inner"], t) + if i > 0: x += flats[i-1] + msp.add_line((x,0),(x,W), dxfattribs={"layer":"BEND"}) + msp.add_line((x+ba,0),(x+ba,W), dxfattribs={"layer":"BEND"}) + msp.add_text("R%.1f %.2f deg" % (b["r_inner"], b["angle_deg"]), + dxfattribs={"layer":"BEND","height":2.0}).set_placement((x, W+2)) + x += ba + doc.saveas(path) + return dict(path=path, flat_length=round(L,2), width=round(W,2), bends=len(bends)) + + + +# ================= ПРОСТРАНСТВЕННАЯ РЕКОНСТРУКЦИЯ ================= +# Расщепление детали на две оболочки щупом + парование + двухцветная раскраска, +# затем сшивка одной стороны и наращивание на толщину. Работает на деталях +# с непараллельными осями гибов (там, где протяжка сечением невозможна). + +def _face_probe_points(f, n=3, rng=None): + from OCP.TopAbs import TopAbs_OUT + from OCP.BRepTopAdaptor import BRepTopAdaptor_FClass2d + from OCP.gp import gp_Pnt2d + from OCP.BRepLProp import BRepLProp_SLProps + rng = rng or np.random.default_rng(5) + umin, umax, vmin, vmax = BRepTools.UVBounds_s(f) + clf = BRepTopAdaptor_FClass2d(f, 1e-6); surf = BRepAdaptor_Surface(f) + out, tries = [], 0 + while len(out) < n and tries < 200: + tries += 1 + u = umin + (umax - umin) * rng.random(); v = vmin + (vmax - vmin) * rng.random() + if clf.Perform(gp_Pnt2d(u, v)) == TopAbs_OUT: continue + pr = BRepLProp_SLProps(surf, u, v, 1, 1e-6) + if not pr.IsNormalDefined(): continue + p = pr.Value(); nv = pr.Normal() + nn = np.array([nv.X(), nv.Y(), nv.Z()]); nn /= np.linalg.norm(nn) + out.append((np.array([p.X(), p.Y(), p.Z()]), nn)) + return out + + +def split_shells(shape, faces, G, t, tol=0.05): + """-> (side_A_ids, side_B_ids, thin_ids). Щуп на глубину t + двухцветная раскраска.""" + from OCP.TopAbs import TopAbs_FACE + SURF = surface_of(shape) + fmap = TopTools_IndexedMapOfShape(); TopExp.MapShapes_s(shape, TopAbs_FACE, fmap) + rng = np.random.default_rng(5) + + def hit(pt): + v = BRepBuilderAPI_MakeVertex(gp_Pnt(*pt)).Vertex() + d = BRepExtrema_DistShapeShape(v, SURF); d.Perform() + if not d.IsDone() or d.Value() > tol: return None + sup = d.SupportOnShape2(1) + return fmap.FindIndex(sup) - 1 if sup.ShapeType() == TopAbs_FACE else -1 + + shell, thin, pairs = [], [], nx.Graph() + for i, f in enumerate(faces): + pts = _face_probe_points(f, 3, rng) + if not pts: thin.append(i); continue + hits, votes = 0, {} + for (p, nn) in pts: + for sgn in (-1, 1): + j = hit(p + sgn * nn * t) + if j is not None: + hits += 1 + if j >= 0 and j != i: votes[j] = votes.get(j, 0) + 1 + break + if hits >= max(1, len(pts) - 1): + shell.append(i) + if votes: pairs.add_edge(i, max(votes, key=votes.get)) + else: + thin.append(i) + pairs.add_nodes_from(shell) + sub = pairs.subgraph(shell) + if not nx.is_bipartite(sub): + raise ValueError("оболочки не разделяются (нечётные циклы в паровании)") + color = nx.bipartite.color(sub) + A = [k for k, v in color.items() if v == 0] + B = [k for k, v in color.items() if v == 1] + return A, B, thin + + +def rebuild_spatial(step_path, thickness=None, out_path=None): + """Реконструкция пространственной детали: оболочка + наращивание на толщину. + Толщина — настоящий параметр: t=2 даёт ту же форму в 2 мм.""" + from OCP.BRepBuilderAPI import BRepBuilderAPI_Sewing + from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin + from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_Arc + from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + shape, faces, info, G, vol = probe(step_path) + res = measure(step_path) + t = thickness or res["thickness_from_bends"] + A, B, thin = split_shells(shape, faces, G, res["thickness_from_bends"]) + best = None + for name, ids in (("B", B), ("A", A)): + sol, npc = thicken_by_faces([faces[i] for i in ids], t) + if sol is None: continue + p = GProp_GProps(); BRepGProp.VolumeProperties_s(sol, p) + v = p.Mass() + if v <= 0: continue + err = abs(v - vol) / vol if thickness is None else 0.0 + if best is None or err < best[0]: + best = (err, sol, name, v) + if best is None: + raise ValueError("наращивание оболочки не удалось") + err, sol, side, v = best + out_path = out_path or os.path.join(tempfile.mkdtemp(), "rebuilt.step") + sol = _as_solid(sol) + w = STEPControl_Writer(); w.Transfer(sol, STEPControl_AsIs); w.Write(out_path) + return dict(path=out_path, side=side, volume=v, ref_volume=vol, + thickness=t, faces_A=len(A), faces_B=len(B), faces_thin=len(thin)) + + + +def make_freecad_code(res, step_path=None, sides=None): + """Генерирует Python-код для вставки в консоль FreeCAD (только модуль Part).""" + t = res["thickness_from_bends"] + bends = res["bends"] + groups = _axis_groups(bends) + L = []; A = L.append + A("# -*- coding: utf-8 -*-") + A("# Код сгенерирован RECADA из обмера STEP. Вставить в консоль FreeCAD (View > Panels > Python console)") + A("import FreeCAD as App, Part, math") + A("from FreeCAD import Vector") + A("") + A("# ================= ПАРАМЕТРЫ =================") + A("T = %.3f # толщина листа (ПАРАМЕТР: можно 2.0 / 2.5 / 3.0)" % t) + A("R = %.3f # внутренний радиус гиба (ПРАВИЛО: R = %.2f x T)" + % (bends[0]["r_inner"] if bends else 3.0, (bends[0]["r_inner"]/t) if bends and t else 1)) + A("") + if len(groups) == 1 and bends: + P = [np.array(b["axis_point"], float) for b in bends] + mid = [float(np.linalg.norm(P[i]-P[i-1])) for i in range(1, len(P))] + W = max(b["length"] for b in bends) + A("W = %.3f # ширина детали (длина линий гиба)" % W) + A("FLAT = [%s] # прямые участки: первый и последний ПРОВЕРИТЬ" % + ", ".join(["30.0"] + ["%.3f" % m for m in mid] + ["30.0"])) + A("ANG = [%s] # углы поворота, град (двугранный = 180 - угол)" % + ", ".join("%.3f" % b["angle_deg"] for b in bends)) + A("") + A("# ---- профиль по нейтральной оси ----") + A("pts, tang, pos = [], Vector(1,0,0), Vector(0,0,0)") + A("edges = []") + A("for i, a in enumerate(ANG + [None]):") + A(" nxt = pos + tang * FLAT[i]") + A(" edges.append(Part.LineSegment(pos, nxt).toShape()); pos = nxt") + A(" if a is None: break") + A(" rad = math.radians(a); rm = R + T/2.0") + A(" nrm = Vector(-tang.y, tang.x, 0) # влево от направления") + A(" ctr = pos + nrm * rm") + A(" v1 = pos - ctr") + A(" v2 = Vector(v1.x*math.cos(-rad) - v1.y*math.sin(-rad),") + A(" v1.x*math.sin(-rad) + v1.y*math.cos(-rad), 0)") + A(" end = ctr + v2") + A(" mid_ang = -rad/2.0") + A(" vm = Vector(v1.x*math.cos(mid_ang) - v1.y*math.sin(mid_ang),") + A(" v1.x*math.sin(mid_ang) + v1.y*math.cos(mid_ang), 0)") + A(" edges.append(Part.Arc(pos, ctr + vm, end).toShape())") + A(" pos = end") + A(" tang = Vector(tang.x*math.cos(-rad) - tang.y*math.sin(-rad),") + A(" tang.x*math.sin(-rad) + tang.y*math.cos(-rad), 0)") + A("") + A("wire = Part.Wire(edges)") + A("face = wire.makeOffset2D(T/2.0, join=2, fill=True) # полоса шириной T вдоль оси") + A("solid = face.extrude(Vector(0, 0, W))") + A("Part.show(solid, 'RECADA_part')") + A("App.ActiveDocument.recompute()") + A("print('объём:', round(solid.Volume, 1), ' эталон:', %.2f)" % res["volume_mm3"]) + else: + A("# Деталь ПРОСТРАНСТВЕННАЯ (%d направлений осей гибов) —" % len(groups)) + A("# строится реконструкцией: одна оболочка + наращивание на толщину T.") + A("STEP = r'%s' # путь к исходному STEP" % (step_path or "C:\\путь\\деталь.step")) + if sides: + A("SHELL_FACES = %s # номера граней одной стороны (из обмера RECADA)" % sides) + else: + A("SHELL_FACES = [] # заполнится при обмере") + A("") + A("src = Part.Shape(); src.read(STEP)") + A("faces = [src.Faces[i] for i in SHELL_FACES]") + A("shell = Part.Shell(faces)") + A("solid = shell.makeOffsetShape(-T, 1e-4, fill=True) # знак поменять, если вывернуло") + A("Part.show(solid, 'RECADA_rebuilt_T%g')") + A("App.ActiveDocument.recompute()") + A("print('объём:', round(solid.Volume, 1), ' эталон:', %.2f)" % res["volume_mm3"]) + A("") + A("# ---- отверстия (координаты из обмера; перенести на базы) ----") + for d, g in res["holes_by_diameter"].items(): + for it in g["items"]: + A("# D%s центр (%s) ось (%s)" % (d, + ", ".join("%.2f" % x for x in it["center"]), + ", ".join("%.2f" % x for x in it["axis"]))) + return "\n".join(L) + + + +def make_freecad_code_selfcontained(step_path, side=None): + """Самодостаточный код для FreeCAD: скелет-оболочка вшита в текст (внешний файл не нужен). + Толщина остаётся параметром — та же форма пересобирается в любом металле.""" + import zlib, base64 + from OCP.BRepBuilderAPI import BRepBuilderAPI_Sewing + shape, faces, info, G, vol = probe(step_path) + res = measure(step_path) + t = res["thickness_from_bends"] + A, B, thin = split_shells(shape, faces, G, t) + ids = sorted(A if (side == "A" or (side is None and len(A) >= len(B))) else B) + sew = BRepBuilderAPI_Sewing(0.05) + for i in ids: sew.Add(faces[i]) + sew.Perform(); shell = sew.SewedShape() + p = os.path.join(tempfile.mkdtemp(), "shell.brep") + BRepTools.Write_s(shell, p) + blob = base64.b64encode(zlib.compress(open(p, "rb").read(), 9)).decode() + chunks = [blob[i:i+120] for i in range(0, len(blob), 120)] + body = "\n".join(' "%s"' % c for c in chunks) + hole_lines = [] + for d, g in res["holes_by_diameter"].items(): + for it in g["items"]: + hole_lines.append("# D%s центр (%s) ось (%s)" % (d, + ", ".join("%.2f" % x for x in it["center"]), + ", ".join("%.2f" % x for x in it["axis"]))) + bend_lines = ["# поворот %6.2f° двугранный %6.2f° R%.1f длина %6.1f ось (%s)%s" % + (b["angle_deg"], b["dihedral_deg"], b["r_inner"], b["length"], + ", ".join("%.3f" % x for x in b["axis"]), + " разгрузка" if b["relief"] else "") + for b in res["bends"]] + return f"""# -*- coding: utf-8 -*- +# RECADA — самодостаточный код воссоздания детали. Внешние файлы НЕ нужны. +# Вставить в консоль FreeCAD: View -> Panels -> Python console +# +# Обмер исходной детали: +# толщина {t} мм, гибов {len(res['bends'])}, отверстий {res.get('holes_total', 0)}, +# объём эталона {res['volume_mm3']} мм3, оболочка из {len(ids)} граней +# +# ГИБЫ: +{chr(10).join(bend_lines)} +# +# ОТВЕРСТИЯ: +{chr(10).join(hole_lines) if hole_lines else "# нет"} + +import FreeCAD as App, Part, zlib, base64 + +# ================= ПАРАМЕТРЫ ================= +T = {t} # толщина листа — ПАРАМЕТР (поставьте 2.0 или 2.5 и пересоберите) +SIGN = -1 # сторона наращивания; если деталь вывернуло — поменяйте на +1 + +# ================= СКЕЛЕТ (срединная оболочка детали) ================= +SKELETON = ( +{body} +) + +_brep = zlib.decompress(base64.b64decode(SKELETON)).decode("utf-8", "ignore") +shell = Part.Shape() +shell.importBrepFromString(_brep) +print("оболочка: граней", len(shell.Faces)) + +# makeOffsetShape(offset, tol, inter, self_inter, offsetMode, join, fill) +# join: 0=Arc (падает на этой геометрии), 1=Tangent, 2=Intersection <- рабочий +solid = None +for _join in (2, 1, 0): + for _sgn in (SIGN, -SIGN): + try: + _s = shell.makeOffsetShape(_sgn * T, 1.0e-4, False, False, 0, _join, True) + if _s.Volume > 0: + solid = _s + print("построено: join=%d, знак=%+d" % (_join, _sgn)) + break + except Exception as _e: + pass + if solid is not None: + break + +if solid is None: + print("Не удалось нарастить оболочку. Попробуйте makeThickness или другой допуск.") +else: + Part.show(solid, "RECADA_T%g" % T) + App.ActiveDocument.recompute() + print("объём:", round(solid.Volume, 1), "мм3 эталон:", {res['volume_mm3']}) +""" + + + +# ================= ПАРАМЕТРИЧЕСКОЕ СКЛАДЫВАНИЕ (оригами-кинематика) ================= + + +# ============ ПЕРЕСТРОЕНИЕ ЗОНЫ ГИБА И ЗАДЕЛКА ДЫР ============ + +import math, numpy as np +from OCP.gp import gp_Ax1, gp_Ax3, gp_Pnt, gp_Dir, gp_Cylinder, gp_Circ, gp_Ax2, gp_Vec +from OCP.BRepAdaptor import BRepAdaptor_Surface +from OCP.BRepTools import BRepTools +from OCP.BRepBuilderAPI import (BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeWire) +from OCP.GC import GC_MakeArcOfCircle +from OCP.TopoDS import TopoDS + + +def bend_zone_faces(inner_face, t, extra_deg=0.0): + """По исходной внутренней цилиндрической грани строит зону гиба нового угла.""" + a = BRepAdaptor_Surface(inner_face) + cyl = a.Cylinder() + ax3 = cyl.Position() + r_in = cyl.Radius() + r_out = r_in + t + u0, u1, v0, v1 = BRepTools.UVBounds_s(inner_face) + du = (u1 - u0) + math.radians(extra_deg) + faces = [] + # два цилиндра + for R in (r_in, r_out): + c = gp_Cylinder(ax3, R) + mf = BRepBuilderAPI_MakeFace(c, u0, u0 + du, v0, v1) + if not mf.IsDone(): return None + faces.append(mf.Face()) + # два торцевых сектора (плоскости, перпендикулярные оси) + loc = ax3.Location(); dirz = ax3.Direction(); dirx = ax3.XDirection() + for v in (v0, v1): + center = gp_Pnt(loc.X() + dirz.X()*v, loc.Y() + dirz.Y()*v, loc.Z() + dirz.Z()*v) + ax2 = gp_Ax2(center, dirz, dirx) + try: + arc_i = GC_MakeArcOfCircle(gp_Circ(ax2, r_in), u0, u0 + du, True).Value() + arc_o = GC_MakeArcOfCircle(gp_Circ(ax2, r_out), u0, u0 + du, True).Value() + except Exception: + return None + e_i = BRepBuilderAPI_MakeEdge(arc_i).Edge() + e_o = BRepBuilderAPI_MakeEdge(arc_o).Edge() + p_i0 = arc_i.Value(arc_i.FirstParameter()); p_o0 = arc_o.Value(arc_o.FirstParameter()) + p_i1 = arc_i.Value(arc_i.LastParameter()); p_o1 = arc_o.Value(arc_o.LastParameter()) + e_s = BRepBuilderAPI_MakeEdge(p_i0, p_o0).Edge() + e_e = BRepBuilderAPI_MakeEdge(p_i1, p_o1).Edge() + mw = BRepBuilderAPI_MakeWire() + for e in (e_i, e_e, e_o, e_s): mw.Add(e) + if not mw.IsDone(): return None + mf = BRepBuilderAPI_MakeFace(mw.Wire(), True) + if not mf.IsDone(): return None + faces.append(mf.Face()) + return faces + + +from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds +from OCP.TopExp import TopExp, TopExp_Explorer +from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_WIRE, TopAbs_SHELL +from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_HSequenceOfShape +from OCP.TopoDS import TopoDS +from OCP.BRepFill import BRepFill_Filling +from OCP.BRepBuilderAPI import BRepBuilderAPI_Sewing, BRepBuilderAPI_MakeSolid +from OCP.GeomAbs import GeomAbs_C0 + + +def free_edges(shape): + m = TopTools_IndexedDataMapOfShapeListOfShape() + TopExp.MapShapesAndAncestors_s(shape, TopAbs_EDGE, TopAbs_FACE, m) + return [TopoDS.Edge_s(m.FindKey(k)) for k in range(1, m.Extent() + 1) + if m.FindFromIndex(k).Size() < 2] + + +def cap_holes(shape, tol=0.05): + edges = free_edges(shape) + if not edges: + return shape, 0 + seq = TopTools_HSequenceOfShape() + for e in edges: seq.Append(e) + wires = TopTools_HSequenceOfShape() + ShapeAnalysis_FreeBounds.ConnectEdgesToWires_s(seq, tol, False, wires) + patches = [] + for i in range(1, wires.Length() + 1): + w = TopoDS.Wire_s(wires.Value(i)) + try: + f = BRepFill_Filling() + ex = TopExp_Explorer(w, TopAbs_EDGE) + while ex.More(): + f.Add(TopoDS.Edge_s(ex.Current()), GeomAbs_C0, True) + ex.Next() + f.Build() + if f.IsDone(): + patches.append(f.Face()) + except Exception: + pass + if not patches: + return shape, 0 + sew = BRepBuilderAPI_Sewing(tol) + ex = TopExp_Explorer(shape, TopAbs_FACE) + while ex.More(): sew.Add(ex.Current()); ex.Next() + for p in patches: sew.Add(p) + sew.Perform(); sh = sew.SewedShape() + ex = TopExp_Explorer(sh, TopAbs_SHELL); mk = BRepBuilderAPI_MakeSolid(); n = 0 + while ex.More(): + s = TopoDS.Shell_s(ex.Current()); s.Closed(True); mk.Add(s); n += 1; ex.Next() + return (mk.Solid() if n else sh), len(patches) + + + + +def ov_viewer_html(model_url, height=640): + return """ + +
+
+ + + + + + + + + + + + + + + +
+
+ Разлёт + + Прозрачность + + Сечение + + + + +
+
+
+
детали появятся после загрузки…
+
+
загрузка модели…
+
+ + + +""".replace("MODELURL", model_url).replace("HEIGHT", str(height)) + + +# ================= ИЗДЕЛИЕ AIRHOLDER ================= + +import numpy as np +from OCP.gp import gp_Pnt, gp_Vec, gp_Trsf, gp_Ax1, gp_Dir +from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox +from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform +from OCP.TopoDS import TopoDS_Compound +from OCP.BRep import BRep_Builder +from OCP.GProp import GProp_GProps +from OCP.BRepGProp import BRepGProp + +PROFILE_H = 33.0 # высота алюминиевого профиля платформы +PROFILE_W = 40.0 +RHO_ALU = 2.70e-6 # кг/мм3 +RHO_STEEL = 7.90e-6 + + +def _box(x, y, z, dx, dy, dz): + return BRepPrimAPI_MakeBox(gp_Pnt(x, y, z), gp_Pnt(x + dx, y + dy, z + dz)).Shape() + + +def _move(sh, dx, dy, dz): + t = gp_Trsf(); t.SetTranslation(gp_Vec(dx, dy, dz)) + return BRepBuilderAPI_Transform(sh, t, True).Shape() + + +def _rotz(sh, deg, cx=0, cy=0): + t = gp_Trsf() + t.SetRotation(gp_Ax1(gp_Pnt(cx, cy, 0), gp_Dir(0, 0, 1)), np.radians(deg)) + return BRepBuilderAPI_Transform(sh, t, True).Shape() + + +def build_product(P): + """P: dict(length, width, height, crossbars, leg_t, mounts_per_side) + -> (список (имя, форма), метрики)""" + from recada import make_bent_part + L, W, H = P["length"], P["width"], P["height"] + n_cb = int(P["crossbars"]) + t = P.get("leg_t", 3.0) + n_m = int(P.get("mounts_per_side", 3)) + + parts = [] + # платформа: два продольных профиля + поперечины + for name, y in (("профиль левый", 0.0), ("профиль правый", W - PROFILE_W)): + parts.append((name, _box(0, y, 0, L, PROFILE_W, PROFILE_H))) + if n_cb > 1: + step = (L - PROFILE_W) / (n_cb - 1) + else: + step = 0 + for i in range(n_cb): + x = i * step if n_cb > 1 else (L - PROFILE_W) / 2 + parts.append(("поперечина %d" % (i + 1), + _box(x, PROFILE_W, 0, PROFILE_W, W - 2 * PROFILE_W, PROFILE_H))) + + # ноги: Г-образные гнутые, вертикаль = H, горизонтальная полка под профиль + FOOT = 60.0 + leg = make_bent_part([("flat", H + PROFILE_H), ("bend", 90, 2 * t), ("flat", FOOT)], 50.0, t) + # make_bent_part строит в своей СК: разворачиваем стоймя + from OCP.gp import gp_Ax1 as _A1 + tr = gp_Trsf() + tr.SetRotation(_A1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), np.radians(90)) + leg0 = BRepBuilderAPI_Transform(leg.wrapped if hasattr(leg, "wrapped") else leg, + tr, True).Shape() + xs = [L * (i + 1) / (n_m + 1) for i in range(n_m)] + k = 0 + for side, y in (("Л", 8.0), ("П", W - 8.0)): + for x in xs: + k += 1 + lg = leg0 + if side == "П": + lg = _rotz(lg, 180) + lg = _move(lg, 0, 0, 0) + lg = _move(lg, x - 25.0, y, -(H)) + parts.append(("нога %s%d" % (side, (k - 1) % n_m + 1), lg)) + + # метрики + mass = 0.0 + for nm, sh in parts: + p = GProp_GProps(); BRepGProp.VolumeProperties_s(sh, p) + rho = RHO_STEEL if nm.startswith("нога") else RHO_ALU + mass += abs(p.Mass()) * rho + total_h = H + PROFILE_H + checks = { + "полная высота над крышей, мм": round(total_h, 1), + "цель < 70 мм (конкурент)": "да" if total_h < 70 else "НЕТ", + "длина ≤ 2000 (GLS)": "да" if L <= 2000 else "НЕТ", + "масса, кг": round(mass, 1), + "вес коробки ≤ 40 кг": "да" if mass <= 40 else "НЕТ", + "деталей": len(parts), + } + return parts, checks + + +# ============ СВОЙ ПРОСМОТРЩИК НА THREE.JS ============ + +import base64, json, zlib +import numpy as np + + +def assembly_payload(parts, deflection=0.8, colors=None, highlight=None): + """parts: [(имя, shape)] -> компактный payload. colors: hex по деталям, highlight: индексы.""" + out = [] + for nm, sh in parts: + try: + v, t, f = mesh_by_face(sh, deflection, 0.6) + except Exception: + continue + if len(v) == 0 or len(t) == 0: + continue + out.append({ + "n": nm, + "c": (colors[len(out)] if colors and len(out) < len(colors) else 0x8fa8bf), + "hl": bool(highlight and len(out) in highlight), + "v": base64.b64encode(np.asarray(v, np.float32).tobytes()).decode(), + "i": base64.b64encode(np.asarray(t, np.uint32).tobytes()).decode(), + "nv": len(v), "nt": len(t), + }) + return out + + +def viewer3_html(payload, height=560, show_list=True): + data = base64.b64encode(zlib.compress(json.dumps(payload).encode(), 6)).decode() + return r""" + +
+
+ + + + + + + + + + +
+
+ + + + + Разлёт + Прозр. + Сечение + + + + + + + + + +
+
+ +
+
+
загрузка…
+
+ + + +""".replace("PAYLOAD", data).replace("HEIGHT", str(height)).replace("LISTDISP", "block" if show_list else "none") + + +# ================= РАЗБОР СБОРКИ ================= + +import numpy as np +from OCP.TopExp import TopExp_Explorer +from OCP.TopAbs import TopAbs_SOLID, TopAbs_FACE, TopAbs_SHELL +from OCP.TopoDS import TopoDS +from OCP.GProp import GProp_GProps +from OCP.BRepGProp import BRepGProp +from OCP.Bnd import Bnd_Box +from OCP.BRepBndLib import BRepBndLib +from OCP.STEPCAFControl import STEPCAFControl_Reader +from OCP.TDocStd import TDocStd_Document +from OCP.XCAFDoc import XCAFDoc_DocumentTool +from OCP.TCollection import TCollection_ExtendedString +from OCP.TDF import TDF_LabelSequence +from OCP.TDataStd import TDataStd_Name +from OCP.IFSelect import IFSelect_RetDone + + +def read_assembly(path): + """Читает STEP-сборку через XCAF: имя + геометрия с накоплением положений + по дереву (иначе детали 'разлетаются'). -> [(имя, TopoDS_Shape), ...]""" + from OCP.TopLoc import TopLoc_Location + doc = TDocStd_Document(TCollection_ExtendedString("d")) + rd = STEPCAFControl_Reader() + rd.SetNameMode(True) + if rd.ReadFile(path) != IFSelect_RetDone: + return None + rd.Transfer(doc) + st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) + tops = TDF_LabelSequence() + st.GetFreeShapes(tops) + out = [] + + def name_of(label): + nm = TDataStd_Name() + if label.FindAttribute(TDataStd_Name.GetID_s(), nm): + s = nm.Get().ToExtString() + if s and s.strip(): + return s.strip() + return None + + def walk(label, loc, prefix=""): + # ссылка -> идём в исходную деталь, положение накапливаем + target = label + if st.IsReference_s(label): + from OCP.TDF import TDF_Label + ref = TDF_Label() + if st.GetReferredShape_s(label, ref): + target = ref + loc = loc.Multiplied(st.GetLocation_s(label)) + nm = name_of(label) or name_of(target) or prefix or "деталь" + if st.IsAssembly_s(target): + sub = TDF_LabelSequence() + st.GetComponents_s(target, sub) + for i in range(1, sub.Length() + 1): + walk(sub.Value(i), loc, nm) + return + sh = st.GetShape_s(target) + if sh is None or sh.IsNull(): + return + out.append((nm, sh.Moved(loc) if not loc.IsIdentity() else sh)) + + for i in range(1, tops.Length() + 1): + walk(tops.Value(i), TopLoc_Location()) + return out + + +def split_solids(shape): + """Разбивает форму на отдельные тела.""" + res = [] + ex = TopExp_Explorer(shape, TopAbs_SOLID) + while ex.More(): + res.append(TopoDS.Solid_s(ex.Current())); ex.Next() + if not res: + ex = TopExp_Explorer(shape, TopAbs_SHELL) + while ex.More(): + res.append(ex.Current()); ex.Next() + return res + + +def part_stats(sh): + p = GProp_GProps(); BRepGProp.VolumeProperties_s(sh, p) + b = Bnd_Box(); BRepBndLib.Add_s(sh, b) + x0, y0, z0, x1, y1, z1 = b.Get() + ex = TopExp_Explorer(sh, TopAbs_FACE); n = 0 + while ex.More(): n += 1; ex.Next() + return dict(volume=p.Mass(), faces=n, + bbox=(round(x1-x0, 1), round(y1-y0, 1), round(z1-z0, 1))) + + +# ================= ОПЕРАЦИИ ИЗМЕНЕНИЯ ================= + +import numpy as np +from OCP.gp import gp_Pnt, gp_Dir, gp_Vec, gp_Ax1, gp_Ax2, gp_Trsf, gp_Pln +from OCP.BRepPrimAPI import (BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakeHalfSpace, + BRepPrimAPI_MakeBox) +from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse, BRepAlgoAPI_Common +from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform, BRepBuilderAPI_MakeFace +from OCP.BRepFilletAPI import BRepFilletAPI_MakeFillet, BRepFilletAPI_MakeChamfer +from OCP.TopExp import TopExp_Explorer +from OCP.TopAbs import TopAbs_EDGE +from OCP.TopoDS import TopoDS +from OCP.Bnd import Bnd_Box +from OCP.BRepBndLib import BRepBndLib +from OCP.BRepAdaptor import BRepAdaptor_Curve +from OCP.GCPnts import GCPnts_AbscissaPoint + + +def _bbox(shape): + b = Bnd_Box(); BRepBndLib.Add_s(shape, b) + x0, y0, z0, x1, y1, z1 = b.Get() + return np.array([x0, y0, z0]), np.array([x1, y1, z1]) + + +def mirror(shape, plane="yz", origin=None): + """Зеркальная копия — для левой/правой ноги.""" + lo, hi = _bbox(shape) + o = np.array(origin) if origin is not None else (lo + hi) / 2 + nrm = {"yz": (1, 0, 0), "xz": (0, 1, 0), "xy": (0, 0, 1)}[plane] + t = gp_Trsf() + t.SetMirror(gp_Ax2(gp_Pnt(*o), gp_Dir(*nrm))) + return BRepBuilderAPI_Transform(shape, t, True).Shape() + + +def drill(shape, center, axis, diameter, depth=None): + """Просверлить отверстие (насквозь, если depth не задан).""" + lo, hi = _bbox(shape) + L = depth or (float(np.linalg.norm(hi - lo)) * 2) + d = np.array(axis, float); d /= np.linalg.norm(d) + start = np.array(center, float) - d * L / 2 + ax2 = gp_Ax2(gp_Pnt(*start), gp_Dir(*d)) + cyl = BRepPrimAPI_MakeCylinder(ax2, diameter / 2.0, L).Shape() + return BRepAlgoAPI_Cut(shape, cyl).Shape() + + +def plug(shape, center, axis, diameter, length=None): + """Заглушить отверстие (заполнить материалом).""" + lo, hi = _bbox(shape) + L = length or (float(np.linalg.norm(hi - lo)) * 2) + d = np.array(axis, float); d /= np.linalg.norm(d) + start = np.array(center, float) - d * L / 2 + cyl = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(*start), gp_Dir(*d)), + diameter / 2.0, L).Shape() + add = BRepAlgoAPI_Common(cyl, _solid_box(shape)).Shape() + return BRepAlgoAPI_Fuse(shape, add).Shape() + + +def _solid_box(shape, pad=0.0): + lo, hi = _bbox(shape) + return BRepPrimAPI_MakeBox(gp_Pnt(*(lo - pad)), gp_Pnt(*(hi + pad))).Shape() + + +def enlarge_holes(shape, holes, new_diameter): + """Рассверлить перечисленные отверстия: holes = [(center, axis), ...]""" + res = shape + for c, a in holes: + res = drill(res, c, a, new_diameter) + return res + + +def trim(shape, point, normal, keep="negative"): + """Обрезать деталь плоскостью.""" + lo, hi = _bbox(shape) + size = float(np.linalg.norm(hi - lo)) * 2 + n = np.array(normal, float); n /= np.linalg.norm(n) + if keep == "positive": n = -n + pl = gp_Pln(gp_Pnt(*point), gp_Dir(*n)) + f = BRepBuilderAPI_MakeFace(pl, -size, size, -size, size).Face() + ref = gp_Pnt(*(np.array(point, float) - n * size * 0.5)) + hs = BRepPrimAPI_MakeHalfSpace(f, ref).Solid() + return BRepAlgoAPI_Cut(shape, hs).Shape() + + +def round_edges(shape, radius, min_len=1.0, max_len=1e9, mode="fillet"): + """Скругление или фаска на рёбрах в заданном диапазоне длин.""" + mk = BRepFilletAPI_MakeFillet(shape) if mode == "fillet" else BRepFilletAPI_MakeChamfer(shape) + ex = TopExp_Explorer(shape, TopAbs_EDGE); n = 0 + while ex.More(): + e = TopoDS.Edge_s(ex.Current()) + try: + L = GCPnts_AbscissaPoint.Length_s(BRepAdaptor_Curve(e)) + if min_len <= L <= max_len: + mk.Add(radius, e); n += 1 + except Exception: + pass + ex.Next() + if n == 0: + return shape, 0 + try: + mk.Build() + return (mk.Shape(), n) if mk.IsDone() else (shape, 0) + except Exception: + return shape, 0 + + +# ================= РАСТЯЖЕНИЕ ДЕТАЛИ ================= +# Разрез плоскостью, сдвиг части, вставка призмы сечения. Только булевы операции +# над телом — результат остаётся замкнутым солидом, годным для производства. + +import numpy as np +from OCP.gp import gp_Pnt, gp_Vec, gp_Dir, gp_Trsf, gp_Pln, gp_Ax3 +from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakePrism +from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse +from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform +from OCP.Bnd import Bnd_Box +from OCP.BRepBndLib import BRepBndLib +from OCP.TopExp import TopExp_Explorer +from OCP.TopAbs import TopAbs_FACE +from OCP.TopoDS import TopoDS +from OCP.BRepAdaptor import BRepAdaptor_Surface +from OCP.GeomAbs import GeomAbs_Plane +from OCP.GProp import GProp_GProps +from OCP.BRepGProp import BRepGProp +from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + +import numpy as np +from OCP.gp import gp_Pnt, gp_Vec, gp_Dir, gp_Trsf, gp_Pln, gp_Ax3 +from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakePrism +from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse +from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform +from OCP.Bnd import Bnd_Box +from OCP.BRepBndLib import BRepBndLib +from OCP.TopExp import TopExp_Explorer +from OCP.TopAbs import TopAbs_FACE +from OCP.TopoDS import TopoDS +from OCP.BRepAdaptor import BRepAdaptor_Surface +from OCP.GeomAbs import GeomAbs_Plane +from OCP.GProp import GProp_GProps +from OCP.BRepGProp import BRepGProp +from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + +AXES = {"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)} + + +def _vol(s): + p = GProp_GProps(); BRepGProp.VolumeProperties_s(s, p); return p.Mass() + + +def stretch(shape, axis="z", cut=None, amount=10.0, out=None): + """axis: 'x'/'y'/'z' или вектор (dx,dy,dz) — направление растяжения. + Для наклонных стенок направление должно лежать В плоскости стенки, + иначе в контуре появляется ступенька.""" + d = np.array(AXES[axis] if isinstance(axis, str) else axis, float) + d = d / np.linalg.norm(d) + bb = Bnd_Box(); BRepBndLib.Add_s(shape, bb) + xmin, ymin, zmin, xmax, ymax, zmax = bb.Get() + lo = np.array([xmin, ymin, zmin]); hi = np.array([xmax, ymax, zmax]) + pad = 10.0 + c = float(np.dot(d, lo) + np.dot(d, hi)) / 2 if cut is None else cut + + def halfspace(below): + from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace + from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace + n = gp_Dir(*(d if not below else -d)) + origin = gp_Pnt(*(d * c)) + pl = gp_Pln(origin, n) + size = float(np.linalg.norm(hi - lo)) + 2 * pad + f = BRepBuilderAPI_MakeFace(pl, -size, size, -size, size).Face() + ref = gp_Pnt(*(d * (c + (-size if below else size) * 0.5))) + return BRepPrimAPI_MakeHalfSpace(f, ref).Solid() + + low = BRepAlgoAPI_Common(shape, halfspace(True)).Shape() + up = BRepAlgoAPI_Common(shape, halfspace(False)).Shape() + tr = gp_Trsf(); tr.SetTranslation(gp_Vec(*(d * amount))) + up_m = BRepBuilderAPI_Transform(up, tr, True).Shape() + + # грани сечения: плоские, нормаль вдоль оси, лежат на уровне c + mids = [] + ex = TopExp_Explorer(low, TopAbs_FACE) + while ex.More(): + f = TopoDS.Face_s(ex.Current()) + a = BRepAdaptor_Surface(f) + if a.GetType() == GeomAbs_Plane: + pl = a.Plane(); n = pl.Axis().Direction(); p = pl.Location() + nn = np.array([n.X(), n.Y(), n.Z()]) + if abs(abs(float(np.dot(nn, d))) - 1) < 1e-3 and \ + abs(float(np.dot(np.array([p.X(), p.Y(), p.Z()]), d)) - c) < 1e-3: + mids.append(f) + ex.Next() + res = low + for f in mids: + pr = BRepPrimAPI_MakePrism(f, gp_Vec(*(d * amount))).Shape() + res = BRepAlgoAPI_Fuse(res, pr).Shape() + res = BRepAlgoAPI_Fuse(res, up_m).Shape() + if out: + w = STEPControl_Writer(); w.Transfer(res, STEPControl_AsIs); w.Write(out) + return res, len(mids), _vol(low), _vol(up), _vol(res) + + + + +def guess_role(step_path): + """Грубая классификация детали по геометрии: что это в багажнике.""" + try: + shape = read_step(step_path) + st_ = part_stats(shape) + dx, dy, dz = st_["bbox"] + dims = sorted([dx, dy, dz], reverse=True) + vol = abs(st_["volume"]) + nf = st_["faces"] + if dims[0] > 1500: + return "кузов", "габарит %.0f мм" % dims[0] + if vol < 4000 and nf < 60: + return "крепёж", "объём %.0f мм3" % vol + try: + m = measure(step_path) + t = m["thickness_from_bends"] + nb = len(m["bends"]) + except Exception: + t, nb = None, 0 + if nb >= 1 and t and 1.0 <= t <= 5.0: + return ("нога" if dims[0] > 150 else "кронштейн крыши", + "лист t=%.1f, гибов %d" % (t, nb)) + # профиль: длинный, сечение постоянно + if dims[0] > 300 and dims[0] / max(dims[1], 1e-6) > 4: + return "поперечина", "длина %.0f, сечение %.0fx%.0f" % (dims[0], dims[1], dims[2]) + if t and 1.0 <= t <= 5.0: + return "кронштейн крыши", "лист t=%.1f, без гибов" % t + return "—", "%d граней, %.0f мм3" % (nf, vol) + except Exception as e: + return "—", str(e)[:40] + + +def best_cut(step_path, axis, samples=25, tol_area=0.5): + """Ищет место разреза, где плоскость режет ТОЛЬКО одно сечение и площадь + локально постоянна — там растяжение не даёт ступеньки в контуре.""" + import numpy as _np + from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace + from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace + from OCP.BRepAlgoAPI import BRepAlgoAPI_Common + from OCP.gp import gp_Pln, gp_Pnt, gp_Dir + from OCP.Bnd import Bnd_Box + from OCP.BRepBndLib import BRepBndLib + shape = read_step(step_path) + d = _np.array(AXES[axis] if isinstance(axis, str) else axis, float) + d /= _np.linalg.norm(d) + b = Bnd_Box(); BRepBndLib.Add_s(shape, b) + x0, y0, z0, x1, y1, z1 = b.Get() + lo = _np.array([x0, y0, z0]); hi = _np.array([x1, y1, z1]) + c0, c1 = float(_np.dot(d, lo)), float(_np.dot(d, hi)) + size = float(_np.linalg.norm(hi - lo)) * 2 + out = [] + for c in _np.linspace(c0 + (c1-c0)*0.15, c1 - (c1-c0)*0.15, samples): + try: + pl = gp_Pln(gp_Pnt(*(d*c)), gp_Dir(*d)) + f = BRepBuilderAPI_MakeFace(pl, -size, size, -size, size).Face() + ref = gp_Pnt(*(d*(c - size*0.5))) + hs = BRepPrimAPI_MakeHalfSpace(f, ref).Solid() + low = BRepAlgoAPI_Common(shape, hs).Shape() + n = 0; area = 0.0 + ex = TopExp_Explorer(low, TopAbs_FACE) + while ex.More(): + fc = TopoDS.Face_s(ex.Current()) + a = BRepAdaptor_Surface(fc) + if a.GetType() == GeomAbs_Plane: + pln = a.Plane(); nn = pln.Axis().Direction(); pp = pln.Location() + v = _np.array([nn.X(), nn.Y(), nn.Z()]) + if abs(abs(float(_np.dot(v, d))) - 1) < 1e-3 and \ + abs(float(_np.dot(_np.array([pp.X(), pp.Y(), pp.Z()]), d)) - c) < 1e-3: + g = GProp_GProps(); BRepGProp.SurfaceProperties_s(fc, g) + n += 1; area += g.Mass() + ex.Next() + out.append((float(c), n, area)) + except Exception: + continue + single = [(c, n, a) for c, n, a in out if n == 1] + if not single: + return None, out + # среди одиночных берём тот, где площадь наиболее стабильна к соседям + best, score = None, 1e18 + for i, (c, n, a) in enumerate(single): + nb = [x[2] for x in single[max(0, i-1):i+2]] + var = max(nb) - min(nb) + if var < score: + score, best = var, c + return best, out + + +def stretch_part(step_path, axis="z", cut=None, amount=10.0, out_path=None, auto=True): + shape = read_step(step_path) + if cut is None and auto: + try: + c, _scan = best_cut(step_path, axis) + if c is not None: + cut = c + except Exception: + pass + out_path = out_path or os.path.join(tempfile.mkdtemp(), "stretched.step") + res, nmid, vlow, vup, vres = stretch(shape, axis, cut, amount, out_path) + chk = check_watertight(out_path) + return dict(path=out_path, sections=nmid, volume=vres, check=chk) + + + +# ================= 3D-ПРОСМОТРЩИК ================= + +import json +import numpy as np +from OCP.BRepMesh import BRepMesh_IncrementalMesh +from OCP.TopExp import TopExp +from OCP.TopAbs import TopAbs_FACE, TopAbs_REVERSED +from OCP.TopoDS import TopoDS +from OCP.TopTools import TopTools_IndexedMapOfShape +from OCP.BRep import BRep_Tool +from OCP.TopLoc import TopLoc_Location + + +def mesh_by_face(shape, deflection=0.4, angle=0.4): + """-> verts Nx3, tris Mx3, face_id длины M (номер грани для каждого треугольника)""" + BRepMesh_IncrementalMesh(shape, deflection, False, angle, True) + fmap = TopTools_IndexedMapOfShape() + TopExp.MapShapes_s(shape, TopAbs_FACE, fmap) + verts, tris, fid = [], [], [] + for k in range(1, fmap.Extent() + 1): + f = TopoDS.Face_s(fmap.FindKey(k)) + loc = TopLoc_Location() + tri = BRep_Tool.Triangulation_s(f, loc) + if tri is None: + continue + base = len(verts) + trsf = loc.Transformation() + for i in range(1, tri.NbNodes() + 1): + p = tri.Node(i).Transformed(trsf) + verts.append((p.X(), p.Y(), p.Z())) + rev = f.Orientation() == TopAbs_REVERSED + for i in range(1, tri.NbTriangles() + 1): + a, b, c = tri.Triangle(i).Get() + if rev: a, c = c, a + tris.append((base + a - 1, base + b - 1, base + c - 1)) + fid.append(k - 1) + return np.array(verts, float), np.array(tris, int), np.array(fid, int) + + +def viewer_select_html(verts, tris, fids, labels=None, highlight=None, + height=520, bg="#14181d"): + """Просмотрщик с подсветкой выбранной грани и кликом для определения номера.""" + v = np.asarray(verts, float) + c = v.mean(axis=0) if len(v) else np.zeros(3) + r = float(np.linalg.norm(v - c, axis=1).max()) if len(v) else 1.0 + payload = json.dumps({ + "v": (v - c).round(3).flatten().tolist(), + "f": np.asarray(tris, int).flatten().tolist(), + "id": np.asarray(fids, int).tolist(), + "hl": int(highlight) if highlight is not None else -1, + "lab": labels or {}, + }) + return f""" +
+
+
+ кликните по грани
+
+ + +""" + + + +import json +import numpy as np +from OCP.BRepMesh import BRepMesh_IncrementalMesh +from OCP.TopExp import TopExp_Explorer +from OCP.TopAbs import TopAbs_FACE, TopAbs_REVERSED +from OCP.TopoDS import TopoDS +from OCP.BRep import BRep_Tool +from OCP.TopLoc import TopLoc_Location + + +def mesh_arrays(shape, deflection=0.4, angle=0.4): + """-> (vertices Nx3, triangles Mx3) в мировых координатах""" + BRepMesh_IncrementalMesh(shape, deflection, False, angle, True) + verts, tris = [], [] + ex = TopExp_Explorer(shape, TopAbs_FACE) + while ex.More(): + f = TopoDS.Face_s(ex.Current()) + loc = TopLoc_Location() + tri = BRep_Tool.Triangulation_s(f, loc) + if tri is not None: + base = len(verts) + trsf = loc.Transformation() + for i in range(1, tri.NbNodes() + 1): + p = tri.Node(i).Transformed(trsf) + verts.append((p.X(), p.Y(), p.Z())) + rev = f.Orientation() == TopAbs_REVERSED + for i in range(1, tri.NbTriangles() + 1): + t = tri.Triangle(i) + a, b, c = t.Get() + if rev: a, c = c, a + tris.append((base + a - 1, base + b - 1, base + c - 1)) + ex.Next() + return np.array(verts, dtype=float), np.array(tris, dtype=int) + + +def viewer_html(verts, tris, height=520, color="#8fa8bf", bg="#14181d"): + v = np.asarray(verts, float) + c = v.mean(axis=0) if len(v) else np.zeros(3) + r = float(np.linalg.norm(v - c, axis=1).max()) if len(v) else 1.0 + data = json.dumps({"v": (v - c).round(3).flatten().tolist(), + "f": np.asarray(tris, int).flatten().tolist()}) + return f""" +
+ + +""" + + + +def fold_prepare2(path): + """Кинематика на ВСЕХ гранях тела (не только оболочки) — номинал выходит + канонически идентичным оригиналу, значит проходит распознавание поставщика.""" + shape, faces, info, G, vol = probe(path) + res = measure(path) + root = max(range(len(faces)), key=lambda i: info[i]["area"]) + joints = [] + for b in res["bends"]: + i_, o_ = b["inner_face"], b["outer_face"] + both = set(G.neighbors(i_)) & set(G.neighbors(o_)) + cut = {i_, o_} | both + H = G.copy(); H.remove_nodes_from(cut) + sub = set() + for c in nx.connected_components(H): + if root not in c: sub |= c + if sub: + joints.append(dict(name="bend_%d" % i_, bend=b, cut=cut, sub=sub, + angle=b["angle_deg"], dihedral=b["dihedral_deg"], + length=b["length"], relief=b["relief"])) + return dict(faces=faces, res=res, vol=vol, joints=joints, n=len(faces), path=path) + + +def fold_apply2(ctx, deltas=None, out_path=None): + """Поворот поддеревьев + перестроение зоны гиба под новый угол + заделка дыр.""" + import math + from OCP.gp import gp_Trsf, gp_Ax1, gp_Pnt, gp_Dir + from OCP.BRepBuilderAPI import (BRepBuilderAPI_Transform, BRepBuilderAPI_Sewing, + BRepBuilderAPI_MakeSolid) + from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + from OCP.TopAbs import TopAbs_SHELL + from OCP.TopoDS import TopoDS + deltas = deltas or {} + faces, res = ctx["faces"], ctx["res"] + t = res["thickness_from_bends"] + moved = {i: faces[i] for i in range(ctx["n"])} + drop, add, applied = set(), [], [] + for j in ctx["joints"]: + d = float(deltas.get(j["name"], 0.0)) + if abs(d) < 1e-9: continue + b = j["bend"] + ax = gp_Ax1(gp_Pnt(*b["axis_point"]), gp_Dir(*b["axis"])) + tr = gp_Trsf(); tr.SetRotation(ax, math.radians(d)) + for fid in j["sub"]: + moved[fid] = BRepBuilderAPI_Transform(moved[fid], tr, True).Shape() + drop |= j["cut"] + nf = bend_zone_faces(faces[b["inner_face"]], t, d) + if nf: add.extend(nf) + applied.append((j["name"], d, len(j["sub"]))) + sew = BRepBuilderAPI_Sewing(0.05) + for i, f in moved.items(): + if i not in drop: sew.Add(f) + for f in add: sew.Add(f) + sew.Perform(); sh = sew.SewedShape() + from OCP.TopExp import TopExp_Explorer as _TE + ex = _TE(sh, TopAbs_SHELL); mk = BRepBuilderAPI_MakeSolid(); n = 0 + while ex.More(): + s = TopoDS.Shell_s(ex.Current()); s.Closed(True); mk.Add(s); n += 1; ex.Next() + outshape = mk.Solid() if n else sh + patches = 0 + try: + fixed, patches = cap_holes(outshape) + if patches: outshape = fixed + except Exception: + pass + out_path = out_path or os.path.join(tempfile.mkdtemp(), "folded.step") + w = STEPControl_Writer(); w.Transfer(outshape, STEPControl_AsIs); w.Write(out_path) + chk = check_watertight(out_path) + return dict(path=out_path, applied=applied, patches=patches, + joints=[j["name"] for j in ctx["joints"]], + is_solid=chk["solids"] > 0, volume=chk["volume"], + ref_volume=ctx["vol"], thickness=t, plates=ctx["n"], check=chk) + + +def fold_prepare(step_path): + """Тяжёлая часть: обмер, расщепление, шарниры. Кэшируется в интерфейсе.""" + shape, faces, info, G, vol = probe(step_path) + res = measure(step_path) + A, B, thin = split_shells(shape, faces, G, res["thickness_from_bends"]) + side = set(A if len(A) >= len(B) else B) + root = max(side, key=lambda i: info[i]["area"]) + H = G.subgraph(side).copy() + joints = [] + for b in res["bends"]: + fid = b["inner_face"] if b["inner_face"] in side else ( + b["outer_face"] if b["outer_face"] in side else None) + if fid is None: continue + H2 = H.copy(); H2.remove_node(fid) + sub = set() + for c in nx.connected_components(H2): + if root not in c: sub |= c + joints.append(dict(name="bend_%d" % fid, face=fid, axis=b["axis"], + point=b["axis_point"], angle=b["angle_deg"], + dihedral=b["dihedral_deg"], length=b["length"], + relief=b["relief"], r=b["r_inner"], subtree=sub)) + return dict(shape=shape, faces=faces, info=info, vol=vol, res=res, + side=side, root=root, joints=joints) + + +def fold_apply(ctx, deltas=None, thickness=None, out_path=None): + """Быстрая часть: применить углы, сшить, нарастить.""" + import math + from OCP.gp import gp_Trsf, gp_Ax1, gp_Pnt, gp_Dir + from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform, BRepBuilderAPI_Sewing + from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin + from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_Arc + from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + deltas = deltas or {} + faces, side, joints = ctx["faces"], ctx["side"], ctx["joints"] + vol, res = ctx["vol"], ctx["res"] + t = thickness or res["thickness_from_bends"] + moved = {i: faces[i] for i in side} + applied = [] + for j in joints: + d = float(deltas.get(j["name"], 0.0)) + if abs(d) < 1e-9: continue + ax = gp_Ax1(gp_Pnt(*j["point"]), gp_Dir(*j["axis"])) + tr = gp_Trsf(); tr.SetRotation(ax, math.radians(d)) + for fid in j["subtree"] | {j["face"]}: + moved[fid] = BRepBuilderAPI_Transform(moved[fid], tr, True).Shape() + applied.append((j["name"], d, len(j["subtree"]))) + + sew = BRepBuilderAPI_Sewing(0.05) + for f in moved.values(): sew.Add(f) + sew.Perform(); shell = sew.SewedShape() + + # наращивание по граням + объединение (даёт замкнутое тело) + solid, vnew = None, None + s, npieces = thicken_by_faces([moved[i] for i in side], t) + if s is not None: + p = GProp_GProps(); BRepGProp.VolumeProperties_s(s, p) + if 0 < p.Mass() < vol * 5: + solid, vnew = s, p.Mass() + + out_path = out_path or os.path.join(tempfile.mkdtemp(), "folded.step") + _out = _as_solid(solid) if solid is not None else shell + w = STEPControl_Writer(); w.Transfer(_out, STEPControl_AsIs) + w.Write(out_path) + return dict(path=out_path, joints=[j["name"] for j in joints], applied=applied, + is_solid=solid is not None, volume=vnew, ref_volume=vol, + thickness=t, plates=len(side)) + + +def fold_model(step_path, deltas=None, thickness=None, out_path=None): + """CLI-обёртка: подготовка + применение.""" + return fold_apply(fold_prepare(step_path), deltas, thickness, out_path) + + + + +def thicken_by_faces(faces_list, t, unify=True): + """Наращивание по граням + объединение. Надёжнее, чем offset всей оболочки: + offset одной грани корректен всегда, а стыки достраивает булева операция, + поэтому результат получается замкнутым (свободных рёбер 0) и точным.""" + from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin + from OCP.GeomAbs import GeomAbs_Intersection, GeomAbs_Arc + from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse + from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain + pieces = [] + for f in faces_list: + for join in (GeomAbs_Intersection, GeomAbs_Arc): + done = False + for sgn in (-1, 1): + try: + mk = BRepOffset_MakeOffset() + mk.Initialize(f, sgn * t, 1e-4, BRepOffset_Skin, False, False, join, True) + mk.MakeOffsetShape() + if mk.IsDone(): + s = mk.Shape() + p = GProp_GProps(); BRepGProp.VolumeProperties_s(s, p) + if p.Mass() > 1e-6: + pieces.append(s); done = True; break + except Exception: + pass + if done: break + if not pieces: + return None, 0 + res = pieces[0] + for s in pieces[1:]: + try: + fu = BRepAlgoAPI_Fuse(res, s); fu.Build() + if fu.IsDone(): res = fu.Shape() + except Exception: + pass + if unify: + try: + u = ShapeUpgrade_UnifySameDomain(res, True, True, True); u.Build(); res = u.Shape() + except Exception: + pass + return res, len(pieces) + + +def check_watertight(shape_or_path): + """Проверка топологической целостности: свободные рёбра = дыры в оболочке. + Критично для производства: порталы поставщиков (Blechcon) принимают только + замкнутые тела. Метрология этого НЕ ловит — точки могут лежать на поверхности + идеально, а деталь при этом быть дырявой.""" + from OCP.TopAbs import TopAbs_SOLID, TopAbs_SHELL + from OCP.TopoDS import TopoDS + from OCP.BRepAdaptor import BRepAdaptor_Curve + from OCP.GCPnts import GCPnts_AbscissaPoint + s = read_step(shape_or_path) if isinstance(shape_or_path, str) else shape_or_path + m = TopTools_IndexedDataMapOfShapeListOfShape() + TopExp.MapShapesAndAncestors_s(s, TopAbs_EDGE, TopAbs_FACE, m) + free = [] + for k in range(1, m.Extent() + 1): + if m.FindFromIndex(k).Size() < 2: + e = TopoDS.Edge_s(m.FindKey(k)) + try: + L = GCPnts_AbscissaPoint.Length_s(BRepAdaptor_Curve(e)) + except Exception: + L = 0.0 + free.append(round(L, 4)) + def count(t): + ex = TopExp_Explorer(s, t); n = 0 + while ex.More(): n += 1; ex.Next() + return n + p = GProp_GProps(); BRepGProp.VolumeProperties_s(s, p) + micro = [x for x in free if x < 0.05] + real = [x for x in free if x >= 0.05] + return dict(free_edges=len(free), free_micro=len(micro), free_real=len(real), + lengths=sorted(free, reverse=True)[:10], + solids=count(TopAbs_SOLID), shells=count(TopAbs_SHELL), + volume=p.Mass(), + ok=(len(free) == 0 and count(TopAbs_SOLID) > 0)) + + +def fill_code(code, fitted): + """подставить подобранные значения в черновик кода""" + vals = fitted["x"]; out = []; fi = 0 + for ln in code.split("\n"): + if ln.startswith("FLAT") and fi < len(vals) - 1: + name = ln.split("=")[0].strip() + out.append("%s = %.3f # подобрано петлёй (RMS %.5f мм)" % (name, vals[fi], fitted["rms"])) + fi += 1 + elif ln.startswith("WIDTH"): + out.append("WIDTH = %.3f # подобрано петлёй" % vals[-1]) + else: + out.append(ln) + return "\n".join(out) + + +def _cli(): + if len(sys.argv) < 2: print(__doc__); return + c = sys.argv[1] + if c == "probe": + res = measure(sys.argv[2]); json.dump(res, open("measurements.json","w"), ensure_ascii=False, indent=2) + print("толщина %s мм | гибов %d | отверстий %d | объём %.1f" % + (res["thickness_from_bends"], len(res["bends"]), res.get("holes_total",0), res["volume_mm3"])) + for b in res["bends"]: + print(" R%.1f/%.1f поворот %.2f° двугранный %.2f° длина %.1f разгрузка:%s" % + (b["r_inner"],b["r_outer"],b["angle_deg"],b["dihedral_deg"],b["length"], + "да" if b["relief"] else "нет")) + elif c == "metro": report(sys.argv[2], sys.argv[3]) + elif c == "unfold": + res = measure(sys.argv[2]); t = res["thickness_from_bends"]; tot=0 + for b in res["bends"]: + ba = bend_allowance(b["angle_deg"], b["r_inner"], t); tot += ba + print(" поворот %6.2f° R%.1f K=%.2f BA=%6.3f" % (b["angle_deg"],b["r_inner"],k_factor(b["r_inner"],t),ba)) + print("суммарный припуск: %.2f мм" % tot) + elif c == "dxf": + res = measure(sys.argv[2]) + r = export_unfold_dxf(res, sys.argv[3] if len(sys.argv) > 3 else "unfold.dxf") + print("развёртка %.2f x %.2f мм, гибов %d -> %s" % (r["flat_length"], r["width"], r["bends"], r["path"])) + elif c == "rebuild": + th = float(sys.argv[3]) if len(sys.argv) > 3 else None + r = rebuild_spatial(sys.argv[2], th) + print("оболочка %s (A=%d, B=%d, торцов %d) | t=%.2f | объём %.1f (эталон %.1f, Δ %.3f %%)" + % (r["side"], r["faces_A"], r["faces_B"], r["faces_thin"], r["thickness"], + r["volume"], r["ref_volume"], (r["volume"]-r["ref_volume"])/r["ref_volume"]*100)) + print("->", r["path"]) + if th is None: report(sys.argv[2], r["path"]) + elif c == "fold": + d = {} + for a in sys.argv[3:]: + if "=" in a: + k, v = a.split("="); d[k.strip()] = float(v) + r = fold_model(sys.argv[2], d) + print("пластин %d | шарниры: %s" % (r["plates"], ", ".join(r["joints"]))) + for n, dd, ns in r["applied"]: + print(" %s: %+.2f° (поддерево %d пластин)" % (n, dd, ns)) + if r["is_solid"]: + print("тело: объём %.1f (эталон %.1f, Δ %.2f %%)" % + (r["volume"], r["ref_volume"], (r["volume"]-r["ref_volume"])/r["ref_volume"]*100)) + else: + print("тело не построилось — сохранена оболочка") + print("->", r["path"]) + w = check_watertight(r["path"]) + print("целостность: свободных рёбер %d (дыр %d), солидов %d — %s" + % (w["free_edges"], w["free_real"], w["solids"], + "годен для производства" if w["ok"] else "НЕ ГОДЕН для отправки поставщику")) + if not d: report(sys.argv[2], r["path"]) + elif c == "fold2": + d = {} + for a in sys.argv[3:]: + if "=" in a: + k, v = a.split("="); d[k.strip()] = float(v) + ctx = fold_prepare2(sys.argv[2]) + r = fold_apply2(ctx, d) + print("граней %d | шарниры: %s" % (r["plates"], ", ".join(r["joints"]))) + for n_, dd, ns in r["applied"]: + print(" %s: %+.2f° (поддерево %d граней)" % (n_, dd, ns)) + c2 = r["check"] + print("объём %.1f (эталон %.1f, Δ %.3f %%) | заплаток %d" % + (r["volume"], r["ref_volume"], (r["volume"]-r["ref_volume"])/r["ref_volume"]*100, r["patches"])) + print("целостность: свободных рёбер %d, солидов %d -> %s" + % (c2["free_edges"], c2["solids"], "ГОДЕН ДЛЯ ПРОИЗВОДСТВА" if c2["ok"] else "не годен")) + print("->", r["path"]) + if not d: report(sys.argv[2], r["path"]) + elif c == "stretch": + ax = sys.argv[3] if len(sys.argv) > 3 else "z" + amt = float(sys.argv[4]) if len(sys.argv) > 4 else 10.0 + cutpos = float(sys.argv[5]) if len(sys.argv) > 5 else None + r = stretch_part(sys.argv[2], ax, cutpos, amt) + c2 = r["check"] + print("растянуто по оси %s на %+.1f мм | сечений %d | объём %.1f" % (ax, amt, r["sections"], r["volume"])) + print("целостность: своб.рёбер %d, солидов %d -> %s" + % (c2["free_edges"], c2["solids"], "ГОДЕН ДЛЯ ПРОИЗВОДСТВА" if c2["ok"] else "не годен")) + print("->", r["path"]) + elif c == "check": + r = check_watertight(sys.argv[2]) + print("солидов %d | оболочек %d | объём %.1f" % (r["solids"], r["shells"], r["volume"])) + print("свободных рёбер: %d (микро <0.05мм: %d, настоящих дыр: %d)" + % (r["free_edges"], r["free_micro"], r["free_real"])) + if r["lengths"]: print(" длины: %s" % ", ".join("%.3f" % x for x in r["lengths"])) + print("ГОДЕН ДЛЯ ПРОИЗВОДСТВА" if r["ok"] else + "НЕ ГОДЕН: деталь не замкнута — портал поставщика её отклонит") + elif c == "fit": + r = fit(sys.argv[2]); print("подобрано:", [round(v,3) for v in r["x"]], "RMS %.5f" % r["rms"]) + report(r["ref_step"], r["fitted_step"]) + print("\n" + fill_code(make_generator(r["measurements"]), r)) + else: print(__doc__) + +def _is_streamlit(): + try: + from streamlit.runtime.scriptrunner import get_script_run_ctx + return get_script_run_ctx() is not None + except Exception: return False + +if _is_streamlit(): + """ + app.py — браузерный интерфейс инструмента реверса RECADA + Запуск: streamlit run app.py -> откроется http://localhost:8501 + """ + import os, json, tempfile + import streamlit as st + + st.set_page_config(page_title="RECADA", layout="wide", initial_sidebar_state="expanded") + + st.title("RECADA · реверс листовых деталей") + st.caption("STEP → обмер → развёртка → приёмка. Без FreeCAD, без GUI, локально на вашем компьютере.") + + tab_work, tab_prod, tab_ov, tab_asm, tab_view, tab_ops, tab_probe, tab_param, tab_code, tab_metro, tab_help = st.tabs( + ["Рабочее окно", "Изделие", "Просмотр 3D", "Сборка", "3D просмотр и изменение", "Операции", "Обмер детали", "Параметрическая модель", "Генерация кода", "Приёмка", "Справка"]) + + + def save_upload(up): + d = tempfile.mkdtemp() + p = os.path.join(d, up.name) + with open(p, "wb") as f: + f.write(up.getbuffer()) + return p + + + # ================= РАБОЧЕЕ ОКНО ================= + with tab_work: + import streamlit.components.v1 as components + from OCP.STEPControl import STEPControl_Writer as _SW, STEPControl_AsIs as _SA + + def _save_shape(sh, name): + p = os.path.join(tempfile.mkdtemp(), name) + w = _SW(); w.Transfer(sh, _SA); w.Write(p); return p + + S = st.session_state + if "wk_parts" not in S: + S.wk_parts = [] # [{name, path, status, role, log:[...], meas}] + S.wk_journal = [] # журнал операций проекта + S.wk_sel = 0 + + upw = st.file_uploader("Загрузите деталь или сборку (STEP)", type=["step", "stp"], key="wk") + if upw and S.get("wk_loaded") != "%s:%d" % (upw.name, upw.size): + pw = save_upload(upw) + raw = [] + try: + named = read_assembly(pw) + if named: + for nm, sh in named: + for j, s in enumerate(split_solids(sh), 1): + raw.append((nm if j == 1 else "%s [%d]" % (nm, j), s)) + except Exception: + raw = [] + if len(raw) <= 1: + raw = [(upw.name.rsplit(".", 1)[0] if len(split_solids(read_step(pw))) == 1 + else "деталь %d" % (i + 1), s) + for i, s in enumerate(split_solids(read_step(pw)))] + parts = [] + for nm, s in raw: + try: + stt = part_stats(s) + except Exception: + continue + if abs(stt["volume"]) < 50 or stt["faces"] < 4: + continue + parts.append(dict(name=nm, path=_save_shape(s, "p.step"), status="исходник", + role="—", log=[], meas=None, vol=abs(stt["volume"]))) + parts.sort(key=lambda p: -p["vol"]) + S.wk_parts = parts; S.wk_journal = []; S.wk_sel = 0 + S.wk_loaded = "%s:%d" % (upw.name, upw.size) + + if not S.wk_parts: + st.info("Загрузите STEP: одна деталь или сборка багажника. Дальше всё здесь — " + "реверс, редактирование, подъём платформы, экспорт.") + else: + P = S.wk_parts + + # группировка одинаковых имён + def _base(nm): + import re as _re + return _re.sub(r"\s*\[\d+\]\s*$", "", nm) + groups = {} + for i, p in enumerate(P): + groups.setdefault(_base(p["name"]), []).append(i) + gkeys = list(groups.keys()) + + ROLES = ["—", "нога", "кронштейн крыши", "платформа", "поперечина", "крепёж", "кузов"] + icons = {"исходник": "○", "реверснута": "◐", "параметрическая": "●"} + + with st.sidebar: + st.markdown("### Модель") + st.caption("%d деталей · %d уникальных" % (len(P), len(gkeys))) + q = st.text_input("Поиск", "", placeholder="фильтр по имени", + label_visibility="collapsed") + show = [g for g in gkeys if q.lower() in g.lower()] if q else gkeys + + # таблица-дерево: видимость, выбор, имя, кол-во, статус, роль + if "wk_vis" not in S: S.wk_vis = {} + if "wk_pick" not in S: S.wk_pick = {} + rows = [] + for g in show: + ids = groups[g]; p0 = P[ids[0]] + rows.append({"вид": S.wk_vis.get(g, True), "выбор": S.wk_pick.get(g, False), + "деталь": g, "×": len(ids), "ст": icons[p0["status"]], + "роль": p0["role"]}) + import pandas as _pd + df = _pd.DataFrame(rows) + ed = st.data_editor( + df, hide_index=True, use_container_width=True, height=min(520, 40 + 35 * len(rows)), + column_config={ + "вид": st.column_config.CheckboxColumn("👁", width="small"), + "выбор": st.column_config.CheckboxColumn("✓", width="small"), + "деталь": st.column_config.TextColumn("деталь", disabled=True), + "×": st.column_config.NumberColumn("×", width="small", disabled=True), + "ст": st.column_config.TextColumn("", width="small", disabled=True), + "роль": st.column_config.SelectboxColumn("роль", options=ROLES, width="medium"), + }, key="wk_tree") + for _, r in ed.iterrows(): + g = r["деталь"] + S.wk_vis[g] = bool(r["вид"]); S.wk_pick[g] = bool(r["выбор"]) + for i in groups[g]: + P[i]["role"] = r["роль"] + + picked = [g for g in gkeys if S.wk_pick.get(g)] + b1, b2 = st.columns(2) + if b1.button("Все", use_container_width=True): + for g in gkeys: S.wk_vis[g] = True + st.rerun() + if b2.button("Только выбранные", use_container_width=True, disabled=not picked): + for g in gkeys: S.wk_vis[g] = g in picked + st.rerun() + if st.button("Определить роли автоматически", use_container_width=True): + with st.spinner("Классифицирую…"): + for g, ids in groups.items(): + if P[ids[0]]["role"] == "—": + r_, why = guess_role(P[ids[0]]["path"]) + for i in ids: P[i]["role"] = r_; P[i]["why"] = why + st.rerun() + + # ---------- ДЕТАЛЬ / ВЫБРАННЫЕ ---------- + st.divider() + if not picked: + st.info("Отметьте детали галочкой ✓ в таблице") + gsel = None; gids = []; cur = None + else: + gsel = picked[0]; gids = [i for g in picked for i in groups[g]]; cur = P[groups[gsel][0]] + st.markdown("### %s" % ("Выбрано: %d деталей" % len(gids) if len(picked) > 1 + else "%s%s" % (gsel[:26], " ×%d" % len(gids) if len(gids) > 1 else ""))) + if len(picked) == 1: + st.caption(cur["status"] + (" · " + cur["why"] if cur.get("why") else "")) + + todo = [g for g in picked if P[groups[g][0]]["status"] == "исходник"] + if todo: + n_todo = sum(len(groups[g]) for g in todo) + if st.button("Полный реверс (%d)" % n_todo, type="primary", + use_container_width=True): + bar = st.progress(0.0); okn = 0 + for j, g in enumerate(todo): + p = P[groups[g][0]] + try: + mres = measure(p["path"]) + rb = fold_apply2(fold_prepare2(p["path"]), {}) + ckb = rb["check"] + dvb = deviations(read_step(p["path"]), read_step(rb["path"]), 4) + insb = float((dvb <= 0.05).sum()) / len(dvb) * 100 + if ckb["ok"] and insb >= 99.9: + upd = dict(meas=mres, status="реверснута", recon=rb["path"], + metro=insb, check=ckb) + if p["role"] == "—": + upd["role"], upd["why"] = guess_role(p["path"]) + for i in groups[g]: P[i].update(upd) + okn += len(groups[g]) + S.wk_journal.append({"op": "reverse", "part": g, + "instances": len(groups[g]), + "metro": round(insb, 3)}) + else: + for i in groups[g]: + P[i]["why"] = "приёмка %.1f%%, рёбер %d" % (insb, ckb["free_edges"]) + except Exception as e: + for i in groups[g]: P[i]["why"] = "ошибка: %s" % str(e)[:40] + bar.progress((j + 1) / len(todo)) + st.success("Реверснуто %d из %d" % (okn, n_todo)) + st.rerun() + st.caption("Одинаковые детали считаются один раз") + + done = [g for g in picked if P[groups[g][0]]["status"] != "исходник"] + if done and len(picked) == 1: + m = cur["meas"] + st.write("t = %.1f · гибов %d · отверстий %s · метрология %.2f %%" + % (m["thickness_from_bends"], len(m["bends"]), + m.get("holes_total", 0), cur.get("metro", 0))) + op = st.selectbox("Операция", ["— выбрать —", "Растянуть / поднять", + "Рассверлить отверстие", "Заглушить отверстие", + "Зеркало", "Экспорт DXF развёртки"]) + if op == "Растянуть / поднять": + dz = st.number_input("На сколько, мм", -100.0, 200.0, 20.0, 1.0) + cutp = st.number_input("Плоскость разреза (0 = авто)", value=0.0, step=5.0) + if st.button("Применить (×%d)" % len(gids), type="primary", use_container_width=True): + with st.spinner("Растягиваю…"): + try: + _sh, _f, _i, _G, _v = probe(cur["path"]) + big = max((x for x in _i if x["kind"] == "plane"), key=lambda x: x["area"]) + n = unit(big["normal"]); z = np.array([0, 0, 1.0]) + if abs(float(np.dot(n, z))) > 0.9: z = np.array([1.0, 0, 0]) + rs = stretch_part(cur["path"], tuple(unit(np.cross(n, z))), + cutp if abs(cutp) > 1e-9 else None, dz) + if rs["check"]["ok"]: + for i in gids: + P[i]["path"] = rs["path"]; P[i]["status"] = "параметрическая" + S.wk_journal.append({"op": "stretch", "part": gsel, "mm": dz, + "sections": rs["sections"]}) + st.rerun() + else: + st.error("Результат не замкнут — отклонено") + except Exception as e: + st.error("Не получилось: %s" % e) + elif op in ("Рассверлить отверстие", "Заглушить отверстие"): + items = [(d, i) for d, g_ in m["holes_by_diameter"].items() for i in g_["items"]] + if items: + k = st.selectbox("Отверстие", range(len(items)), + format_func=lambda i: "D%s @ (%s)" % (items[i][0], + ", ".join("%.0f" % x for x in items[i][1]["center"]))) + d0, it = items[k] + nd = st.number_input("Новый диаметр", 1.0, 60.0, float(d0) + 2, 0.5) \ + if op.startswith("Рассв") else None + if st.button("Применить", type="primary", use_container_width=True): + shp = read_step(cur["path"]) + res = drill(shp, it["center"], it["axis"], nd) if nd else \ + plug(shp, it["center"], it["axis"], float(d0) + 0.2) + pth = _save_shape(res, "op.step"); ck = check_watertight(pth) + if ck["ok"]: + mm2 = measure(pth) + for i in gids: P[i].update(path=pth, status="параметрическая", meas=mm2) + S.wk_journal.append({"op": op, "part": gsel, "hole": d0, "new": nd}) + st.rerun() + else: + st.error("Не замкнуто — отклонено") + else: + st.info("Отверстий нет") + elif op == "Зеркало": + if st.button("Создать зеркальную", type="primary", use_container_width=True): + res = mirror(read_step(cur["path"]), "xz") + pth = _save_shape(res, "mir.step") + P.append(dict(name=gsel + " (зерк.)", path=pth, status=cur["status"], + role=cur["role"], log=[], meas=cur["meas"], vol=cur["vol"])) + S.wk_journal.append({"op": "mirror", "part": gsel}); st.rerun() + elif op == "Экспорт DXF развёртки": + try: + dd = export_unfold_dxf(m, os.path.join(tempfile.mkdtemp(), "u.dxf")) + st.success("Заготовка %.1f × %.1f мм" % (dd["flat_length"], dd["width"])) + st.download_button("Скачать DXF", open(dd["path"], "rb").read(), + file_name="%s.dxf" % gsel, use_container_width=True) + except Exception: + st.warning("Одним листом не раскладывается") + st.download_button("Скачать STEP", open(cur["path"], "rb").read(), + file_name="%s.step" % gsel, use_container_width=True) + + # ---------- ИЗДЕЛИЕ ---------- + legs = [p for p in P if p["role"] == "нога" and p["status"] != "исходник"] + st.divider() + st.markdown("### Изделие") + pin_txt = st.text_input("Точки крепления (из 3D: «Копировать точки»)", "", + placeholder="x,y,z;x,y,z", key="wk_pins") + if pin_txt.strip(): + pts = [] + for grp in pin_txt.split(";"): + c = [t for t in grp.replace(" ", "").split(",") if t] + if len(c) == 3: + try: pts.append(tuple(float(t) for t in c)) + except ValueError: pass + if pts: S.wk_pins = pts; st.caption("зафиксировано точек: %d" % len(pts)) + if legs: + st.caption("%d ног готовы к перегенерации" % len(legs)) + dzp = st.number_input("Поднять платформу, мм", -50.0, 100.0, 20.0, 1.0) + if st.button("Поднять все ноги", type="primary", use_container_width=True): + done_n = 0; cache = {} + with st.spinner("Перегенерирую ноги…"): + for lg in legs: + kk = _base(lg["name"]) + try: + if kk in cache: + lg["path"] = cache[kk]; lg["status"] = "параметрическая"; done_n += 1; continue + _sh, _f, _i, _G, _v = probe(lg["path"]) + big = max((x for x in _i if x["kind"] == "plane"), key=lambda x: x["area"]) + n = unit(big["normal"]); z = np.array([0, 0, 1.0]) + if abs(float(np.dot(n, z))) > 0.9: z = np.array([1.0, 0, 0]) + dvec = tuple(unit(np.cross(n, z))); cutv = None + if S.get("wk_pins"): + cutv = max(float(np.dot(np.array(p_), np.array(dvec))) for p_ in S.wk_pins) + 5.0 + rs = stretch_part(lg["path"], dvec, cutv, dzp) + if rs["check"]["ok"]: + lg["path"] = rs["path"]; lg["status"] = "параметрическая" + cache[kk] = rs["path"]; done_n += 1 + except Exception: + pass + S.wk_journal.append({"op": "raise_platform", "mm": dzp, "legs": done_n}); st.rerun() + else: + st.caption("Назначьте роль «нога» и реверсните — появится подъём платформы") + + # ---------- СЦЕНА НА ВСЮ ШИРИНУ ---------- + COL = {"исходник": 0x8fa8bf, "реверснута": 0x35d07f, "параметрическая": 0x35d07f} + vis_idx = [i for g in gkeys if S.wk_vis.get(g, True) for i in groups[g]] + hl_set = set() + for g in gkeys: + if S.wk_pick.get(g): + for i in groups[g]: hl_set.add(i) + hl_local = {k for k, i in enumerate(vis_idx) if i in hl_set} + + @st.cache_resource(show_spinner=False) + def _wk_pay(paths_key, paths, names, cols, hls): + return assembly_payload([(n, read_step(p)) for n, p in zip(names, paths)], + 1.0, list(cols), set(hls)) + key = "|".join("%s#%s" % (P[i]["path"], P[i]["status"]) for i in vis_idx) + "|hl:" + ",".join(map(str, sorted(hl_local))) + pay = _wk_pay(key, [P[i]["path"] for i in vis_idx], [P[i]["name"] for i in vis_idx], + tuple(COL[P[i]["status"]] for i in vis_idx), tuple(sorted(hl_local))) + st.caption("○ серые — исходники · ● зелёные — реверснуты · оранжевые — выбраны в дереве · " + "двойной клик по детали показывает её имя") + components.html(viewer3_html(pay, height=740, show_list=False), height=900) + + # ---------- ЖУРНАЛ ---------- + st.divider() + j1, j2 = st.columns([3, 1]) + with j1: + st.markdown("**Журнал проекта** (истина — параметры, не геометрия)") + st.code(json.dumps(S.wk_journal, ensure_ascii=False, indent=1) or "[]", language="json") + with j2: + st.download_button("project.json", json.dumps( + {"parts": [{k: v for k, v in p.items() if k in ("name", "status", "role", "log")} + for p in P], "journal": S.wk_journal}, ensure_ascii=False, indent=2), + file_name="project.json", use_container_width=True) + if st.button("Пакет производства (ZIP)", type="primary", use_container_width=True): + import zipfile, io, csv + buf = io.BytesIO() + bom_rows = [] + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + done_g = set() + for i, p in enumerate(P): + g = _base(p["name"]) + if g in done_g: continue + done_g.add(g) + qty = sum(1 for q in P if _base(q["name"]) == g) + safe = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in g)[:60] + # STEP каждой уникальной детали + zf.write(p["path"], "step/%s.step" % safe) + row = {"деталь": g, "кол-во": qty, "роль": p["role"], "статус": p["status"], + "толщина": "", "гибов": "", "отверстий": "", "развёртка": ""} + m = p.get("meas") + if m: + row.update({"толщина": m["thickness_from_bends"], + "гибов": len(m["bends"]), "отверстий": m.get("holes_total", 0)}) + # DXF развёртки, если раскладывается одним листом + try: + dd = export_unfold_dxf(m, os.path.join(tempfile.mkdtemp(), "u.dxf")) + zf.write(dd["path"], "dxf/%s.dxf" % safe) + row["развёртка"] = "%.1f x %.1f" % (dd["flat_length"], dd["width"]) + except Exception: + row["развёртка"] = "через портал (пространственная)" + zf.writestr("passport/%s.json" % safe, + json.dumps(m, ensure_ascii=False, indent=1)) + bom_rows.append(row) + # BOM + sio = io.StringIO() + wcsv = csv.DictWriter(sio, fieldnames=list(bom_rows[0].keys()) if bom_rows else ["деталь"], + delimiter=";") + wcsv.writeheader(); [wcsv.writerow(r) for r in bom_rows] + zf.writestr("BOM.csv", "\ufeff" + sio.getvalue()) + zf.writestr("project.json", json.dumps( + {"parts": [{k: v for k, v in p.items() if k in ("name", "status", "role")} + for p in P], "journal": S.wk_journal, + "pins": S.get("wk_pins", [])}, ensure_ascii=False, indent=2)) + zf.writestr("README.txt", + "RECADA — пакет производства\n\n" + "step/ уникальные детали (STEP)\n" + "dxf/ развёртки листовых деталей с линиями гиба\n" + "passport/ обмер: толщина, гибы, отверстия (JSON)\n" + "BOM.csv спецификация: деталь; кол-во; роль; статус; t; гибы; отверстия\n" + "project.json журнал операций — истина проекта\n") + st.download_button("Скачать пакет.zip", buf.getvalue(), file_name="airholder_package.zip", + use_container_width=True) + st.caption("%d уникальных деталей · BOM · DXF · паспорта" % len(bom_rows)) + if st.button("Экспорт всех STEP", use_container_width=True): + from OCP.TopoDS import TopoDS_Compound as _C + from OCP.BRep import BRep_Builder as _B + comp = _C(); bb = _B(); bb.MakeCompound(comp) + for p in P: bb.Add(comp, read_step(p["path"])) + po = _save_shape(comp, "project.step") + st.download_button("Скачать project.step", open(po, "rb").read(), + file_name="project.step", use_container_width=True) + if st.button("Очистить", use_container_width=True): + S.wk_parts = []; S.wk_journal = []; S.wk_loaded = None; st.rerun() + + # ---------------- ИЗДЕЛИЕ ---------------- + with tab_prod: + import streamlit.components.v1 as components + st.write("Редактор изделия: продуктовые параметры порождают геометрию. " + "Истина — product.json, детали и проверки пересчитываются из него.") + + pc1, pc2, pc3 = st.columns(3) + with pc1: + veh = st.selectbox("Автомобиль", ["ford_ranger_p703_dc", "defender_110", "vw_t6_swb"]) + pl_len = st.number_input("Длина платформы, мм", 800, 3000, 1330, 10) + pl_wid = st.number_input("Ширина платформы, мм", 800, 1800, 1240, 10) + with pc2: + h_roof = st.slider("Высота ног над крышей, мм", 10, 120, 30, 1) + n_cb = st.slider("Поперечин", 2, 10, 6, 1) + with pc3: + leg_t = st.select_slider("Толщина ноги, мм", options=[2.0, 2.5, 3.0], value=3.0) + n_mnt = st.slider("Точек крепления на сторону", 2, 5, 3, 1) + + prm = dict(vehicle=veh, length=pl_len, width=pl_wid, height=h_roof, + crossbars=n_cb, leg_t=leg_t, mounts_per_side=n_mnt) + + with st.spinner("Пересобираю изделие из параметров…"): + try: + parts_p, checks_p = build_product(prm) + except Exception as e: + st.error("Сборка не удалась: %s" % e); parts_p = None + + if parts_p: + ok_all = all(str(v) != "НЕТ" for v in checks_p.values()) + cA, cB = st.columns([2, 3]) + with cA: + st.subheader("Проверки") + for k, v in checks_p.items(): + (st.success if str(v) != "НЕТ" else st.error)("%s: %s" % (k, v)) + st.download_button("product.json", + json.dumps(prm, ensure_ascii=False, indent=2), + file_name="product.json") + if st.button("Выгрузить сборку STEP"): + from OCP.TopoDS import TopoDS_Compound as _C + from OCP.BRep import BRep_Builder as _B + from OCP.STEPControl import STEPControl_Writer as _W, STEPControl_AsIs as _AI + comp = _C(); bb = _B(); bb.MakeCompound(comp) + for _, sh in parts_p: bb.Add(comp, sh) + po = os.path.join(tempfile.mkdtemp(), "airholder.step") + w = _W(); w.Transfer(comp, _AI); w.Write(po) + st.download_button("Скачать airholder.step", open(po, "rb").read(), + file_name="airholder_%s.step" % veh) + st.caption("Нога — гнутая деталь: вертикаль = высота, полка 60 мм. " + "Меняете высоту — ноги перегенерируются, а не сдвигаются.") + with cB: + with st.spinner("Триангуляция…"): + pay = assembly_payload(parts_p, 1.0) + components.html(viewer3_html(pay, height=470), height=670) + # ---------------- ПРОСМОТР 3D ---------------- + with tab_ov: + import streamlit.components.v1 as components + st.write("Свой просмотрщик на three.js: детали кликабельны, изоляция, разлёт, " + "прозрачность, сечение, измерение расстояний. Всё управление наше.") + + @st.cache_resource(show_spinner=False) + def _v3_prep(path, key, defl): + raw = [] + try: + named = read_assembly(path) + if named: + for nm, sh in named: + for j, s in enumerate(split_solids(sh), 1): + raw.append((nm if j == 1 else "%s [%d]" % (nm, j), s)) + except Exception: + raw = [] + if len(raw) <= 1: + raw = [("деталь %d" % (i + 1), s) + for i, s in enumerate(split_solids(read_step(path)))] + parts, junk = [], 0 + for nm, s in raw: + try: + stt = part_stats(s) + except Exception: + junk += 1; continue + if abs(stt["volume"]) < 50.0 or stt["faces"] < 4: + junk += 1; continue + parts.append((nm, s, abs(stt["volume"]))) + parts.sort(key=lambda p: -p[2]) + parts = [(nm, s) for nm, s, _ in parts] + return assembly_payload(parts, defl), parts, junk + + up3 = st.file_uploader("STEP / STP", type=["step", "stp"], key="v3") + defl = st.select_slider("Точность сетки", options=[0.3, 0.5, 0.8, 1.5, 3.0], value=0.8, + help="меньше — точнее и тяжелее; для больших сборок берите 1.5–3.0") + if up3: + p3 = save_upload(up3) + with st.spinner("Читаю и триангулирую… (первый раз дольше, потом из кэша)"): + payload, parts3, junk = _v3_prep(p3, "%s:%d" % (up3.name, up3.size), defl) + st.success("Деталей: %d · отсеяно мусора: %d · треугольников: %s" + % (len(parts3), junk, "{:,}".format(sum(p["nt"] for p in payload)).replace(",", " "))) + components.html(viewer3_html(payload, height=560), height=760) + + st.divider() + st.subheader("Реверс выбранной детали") + st.caption("Номер детали виден при клике в окне и в списке справа") + names3 = [p["n"] for p in payload] + k3 = st.selectbox("Деталь", range(len(parts3)), + format_func=lambda i: "%d · %s" % (i, names3[i] if i < len(names3) else "")) + shape_k = parts3[k3][1] + from OCP.STEPControl import STEPControl_Writer as _W, STEPControl_AsIs as _AI + pk = os.path.join(tempfile.mkdtemp(), "part_%d.step" % k3) + _w = _W(); _w.Transfer(shape_k, _AI); _w.Write(pk) + + b1, b2, b3 = st.columns(3) + do_probe = b1.button("Обмерить", type="primary") + do_rev = b2.button("Реконструировать") + b3.download_button("Скачать STEP", open(pk, "rb").read(), + file_name="part_%d.step" % k3) + + if do_probe: + with st.spinner("Обмеряю: граф граней, гибы, отверстия…"): + try: + rr = measure(pk) + except Exception as e: + st.error("Обмер не удался: %s" % e); rr = None + if rr: + c1, c2, c3, c4 = st.columns(4) + c1.metric("Толщина, мм", rr["thickness_from_bends"]) + c2.metric("Гибов", len(rr["bends"])) + c3.metric("Отверстий", rr.get("holes_total", 0)) + c4.metric("Объём, мм3", "%.0f" % rr["volume_mm3"]) + if rr["bends"]: + st.dataframe([{"R внутр": b["r_inner"], "R внеш": b["r_outer"], + "поворот°": b["angle_deg"], "двугранный°": b["dihedral_deg"], + "длина, мм": b["length"], + "разгрузка": "да" if b["relief"] else "—"} + for b in rr["bends"]], use_container_width=True) + else: + st.info("Гибов не найдено — деталь не листовая " + "(профиль, литьё, крепёж) либо иной толщины") + rows3 = [{"D, мм": d, "центр": ", ".join("%.1f" % x for x in it["center"])} + for d, g in rr["holes_by_diameter"].items() for it in g["items"]] + if rows3: + st.dataframe(rows3, use_container_width=True) + st.download_button("measurements.json", + json.dumps(rr, ensure_ascii=False, indent=2), + file_name="measurements_%d.json" % k3) + try: + _d = export_unfold_dxf(rr, os.path.join(tempfile.mkdtemp(), "u.dxf")) + st.success("Развёртка: %.1f × %.1f мм" % (_d["flat_length"], _d["width"])) + st.download_button("Развёртка DXF", open(_d["path"], "rb").read(), + file_name="unfold_%d.dxf" % k3) + except Exception as e: + st.caption("Карта развёртки одним листом недоступна: %s" % e) + + if do_rev: + with st.spinner("Расщепление оболочек, кинематика, сборка тела…"): + try: + ctx3 = fold_prepare2(pk) + rf = fold_apply2(ctx3, {}) + except Exception as e: + st.error("Реконструкция не удалась: %s" % e); rf = None + if rf: + ck = rf["check"] + m1, m2, m3 = st.columns(3) + m1.metric("Объём, мм3", "%.1f" % ck["volume"]) + m2.metric("Свободных рёбер", ck["free_edges"]) + m3.metric("Шарниров", len(rf["joints"])) + (st.success if ck["ok"] else st.error)( + "ГОДЕН ДЛЯ ПРОИЗВОДСТВА" if ck["ok"] else "Не замкнуто") + try: + dv = deviations(read_step(pk), read_step(rf["path"]), 6) + ins = float((dv <= 0.05).sum()) / len(dv) * 100 + st.write("Метрология: %.3f %% точек в допуске 0.05 мм" % ins) + except Exception: + pass + st.download_button("Скачать реконструкцию STEP", + open(rf["path"], "rb").read(), + file_name="recon_%d.step" % k3) + + # ---------------- СБОРКА ---------------- + with tab_asm: + import streamlit.components.v1 as components + st.write("Загрузите сборку (STEP с несколькими деталями). Инструмент разберёт её " + "на детали, покажет в 3D и даст выбрать нужную для реверса.") + + @st.cache_resource(show_spinner=False) + def _asm_prep(path, key): + raw = [] + try: + named = read_assembly(path) # имя + геометрия из одного источника + if named: + for nm, sh in named: + sols = split_solids(sh) + if len(sols) == 1: + raw.append((nm, sols[0])) + else: + for j, s in enumerate(sols, 1): + raw.append(("%s [%d]" % (nm, j), s)) + except Exception: + raw = [] + if len(raw) <= 1: # запасной путь без имён + shp = read_step(path) + raw = [("деталь %d" % (i + 1), s) for i, s in enumerate(split_solids(shp))] + + parts, junk = [], 0 + for nm, s in raw: + try: + stt = part_stats(s) + except Exception: + junk += 1; continue + if abs(stt["volume"]) < 50.0 or stt["faces"] < 4: + junk += 1; continue + parts.append((nm, s, abs(stt["volume"]))) + parts.sort(key=lambda p: -p[2]) + parts = [(nm, s) for nm, s, _ in parts] + + V, T, ID = [], [], [] + for k, (nm, s) in enumerate(parts): + try: + v, t, f = mesh_by_face(s) + except Exception: + continue + if len(v) == 0: + continue + base = len(V) + V.extend(v.tolist()); T.extend((t + base).tolist()); ID.extend([k] * len(t)) + return parts, np.array(V), np.array(T, int), np.array(ID, int), junk + + upa = st.file_uploader("STEP-сборка", type=["step", "stp"], key="asm") + if upa: + pa = save_upload(upa) + with st.spinner("Разбираю сборку…"): + parts, V, T, ID, junk = _asm_prep(pa, "%s:%d" % (upa.name, upa.size)) + st.success("Деталей в сборке: %d · отсеяно мусора (обрезки, вспомогательные поверхности): %d" % (len(parts), junk)) + + rows, labels = [], {} + for k, (nm, s) in enumerate(parts): + st_ = part_stats(s) + rows.append({"№": k, "деталь": nm[:32], "объём, мм3": round(abs(st_["volume"]), 1), + "граней": st_["faces"], + "габарит": "%.0f x %.0f x %.0f" % st_["bbox"]}) + labels[k] = "%s · %.0f мм3" % (nm[:20], abs(st_["volume"])) + + c1, c2 = st.columns([3, 2]) + with c2: + st.subheader("Детали") + st.dataframe(rows, use_container_width=True, height=260) + sel = st.selectbox("Выбрать для работы", range(len(parts)), + format_func=lambda i: "%d · %s" % (i, parts[i][0][:28])) + if st.button("Выделить деталь в STEP", type="primary"): + from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + outp = os.path.join(tempfile.mkdtemp(), "part_%d.step" % sel) + w = STEPControl_Writer(); w.Transfer(parts[sel][1], STEPControl_AsIs) + w.Write(outp) + ch = check_watertight(outp) + st.write("объём %.1f · своб.рёбер %d · солидов %d" + % (ch["volume"], ch["free_edges"], ch["solids"])) + st.download_button("Скачать деталь STEP", open(outp, "rb").read(), + file_name="part_%d.step" % sel) + st.caption("Дальше: загрузите этот файл на вкладку «Обмер детали» " + "или «Параметрическая модель»") + with c1: + st.caption("Оранжевым — выбранная деталь · клик по детали показывает её номер") + components.html(viewer_select_html(V, T, ID, labels, highlight=sel, height=520), + height=550) + + # ---------------- 3D ПРОСМОТР И ИЗМЕНЕНИЕ ---------------- + with tab_view: + import streamlit.components.v1 as components + st.write("Кликните по грани в 3D — она подсветится и покажет номер. " + "Выберите опорную грань в списке справа: её плоскость задаёт направление растяжения.") + + @st.cache_resource(show_spinner=False) + def _prep_view(path, key): + shape, faces, info, G, vol = probe(path) + v, t, f = mesh_by_face(shape) + return dict(shape=shape, info=info, v=v, t=t, f=f, vol=vol) + + upv = st.file_uploader("STEP-файл", type=["step", "stp"], key="view") + if upv: + pv = save_upload(upv) + with st.spinner("Чтение и триангуляция…"): + D = _prep_view(pv, "%s:%d" % (upv.name, upv.size)) + info = D["info"] + labels = {i["id"]: "%s, %.0f мм2" % (i["kind"], i["area"]) for i in info} + + planes = sorted([i for i in info if i["kind"] == "plane"], + key=lambda x: -x["area"])[:15] + opts = ["#%d · плоскость · %.0f мм2 · нормаль [%s]" + % (p["id"], p["area"], ", ".join("%.2f" % q for q in unit(p["normal"]))) + for p in planes] + + c1, c2 = st.columns([3, 2]) + with c2: + st.subheader("Опорная грань") + sel = st.selectbox("Крупнейшие плоскости", opts, index=0, + label_visibility="collapsed") + fid = int(sel.split("·")[0].strip().lstrip("#")) + sel_face = next(p for p in planes if p["id"] == fid) + n = unit(sel_face["normal"]) + st.caption("Нормаль [%s] — растяжение пойдёт В плоскости этой грани" + % ", ".join("%.3f" % q for q in n)) + + st.subheader("Изменение") + inplane = st.radio("Направление", ["в плоскости грани (вдоль)", + "в плоскости грани (поперёк)", + "по нормали к грани"], index=0) + amount = st.slider("На сколько, мм", -30.0, 30.0, 10.0, 0.5) + cutpos = st.number_input("Плоскость разреза (координата вдоль направления)", + value=0.0, step=5.0, help="0 = центр габарита") + go = st.button("Построить", type="primary") + + with c1: + st.caption("Оранжевым — выбранная грань") + components.html(viewer_select_html(D["v"], D["t"], D["f"], labels, + highlight=fid, height=470), height=500) + + if go: + z = np.array([0.0, 0.0, 1.0]) + if abs(float(np.dot(n, z))) > 0.9: + z = np.array([1.0, 0.0, 0.0]) + d_along = unit(np.cross(n, z)) + d_cross = unit(np.cross(n, d_along)) + axis = {"в плоскости грани (вдоль)": tuple(d_along), + "в плоскости грани (поперёк)": tuple(d_cross), + "по нормали к грани": tuple(n)}[inplane] + st.caption("Вектор растяжения [%s]" % ", ".join("%.3f" % q for q in axis)) + with st.spinner("Режу, сдвигаю, вставляю призму сечения…"): + try: + r = stretch_part(pv, axis, (cutpos if abs(cutpos) > 1e-9 else None), amount) + except Exception as e: + st.error("Не получилось: %s" % e); r = None + if r: + ch = r["check"] + k1, k2, k3 = st.columns(3) + k1.metric("Объём, мм3", "%.1f" % ch["volume"]) + k2.metric("Сечений", r["sections"]) + k3.metric("Свободных рёбер", ch["free_edges"]) + (st.success if ch["ok"] else st.error)( + "ГОДЕН ДЛЯ ПРОИЗВОДСТВА" if ch["ok"] else "Не замкнуто — поставщик отклонит") + try: + sh2 = read_step(r["path"]) + v1, t1, f1 = mesh_by_face(sh2) + st.caption("Результат") + components.html(viewer_select_html(v1, t1, f1, {}, height=470, + bg="#111a14"), height=500) + except Exception as e: + st.caption("Просмотр результата недоступен: %s" % e) + st.download_button("Скачать результат STEP", open(r["path"], "rb").read(), + file_name="recada_modified.step") + + # ---------------- ОПЕРАЦИИ ---------------- + with tab_ops: + import streamlit.components.v1 as components + st.write("Операции над деталью: зеркало, сверление, заглушка, обрезка, скругление. " + "Каждый результат проверяется на замкнутость.") + + @st.cache_resource(show_spinner=False) + def _ops_prep(path, key): + return read_step(path), measure(path) + + upo = st.file_uploader("STEP-файл", type=["step", "stp"], key="ops") + if upo: + po = save_upload(upo) + with st.spinner("Чтение и обмер…"): + shp, mres = _ops_prep(po, "%s:%d" % (upo.name, upo.size)) + + op = st.selectbox("Операция", [ + "Зеркало (левая/правая деталь)", + "Рассверлить отверстие", + "Просверлить новое отверстие", + "Заглушить отверстие", + "Обрезать плоскостью", + "Скругление рёбер", + ]) + res_shape, note = None, "" + + if op == "Зеркало (левая/правая деталь)": + pl = st.selectbox("Плоскость зеркала", ["yz", "xz", "xy"], index=1) + if st.button("Выполнить", type="primary"): + res_shape = mirror(shp, pl); note = "зеркало по %s" % pl + + elif op in ("Рассверлить отверстие", "Заглушить отверстие"): + items = [(d, i) for d, g in mres["holes_by_diameter"].items() for i in g["items"]] + lbl = ["D%s @ (%s)" % (d, ", ".join("%.1f" % x for x in i["center"])) for d, i in items] + if lbl: + k = st.selectbox("Отверстие", range(len(lbl)), format_func=lambda i: lbl[i]) + d0, it = items[k] + if op == "Рассверлить отверстие": + nd = st.number_input("Новый диаметр, мм", 1.0, 60.0, float(d0) + 2.0, 0.5) + if st.button("Выполнить", type="primary"): + res_shape = drill(shp, it["center"], it["axis"], nd) + note = "D%s -> D%.1f" % (d0, nd) + else: + if st.button("Выполнить", type="primary"): + res_shape = plug(shp, it["center"], it["axis"], float(d0) + 0.2) + note = "заглушено D%s" % d0 + else: + st.info("Отверстий не найдено") + + elif op == "Просверлить новое отверстие": + c1, c2, c3 = st.columns(3) + cx = c1.number_input("X", value=0.0, step=1.0) + cy = c2.number_input("Y", value=0.0, step=1.0) + cz = c3.number_input("Z", value=0.0, step=1.0) + a1, a2, a3 = st.columns(3) + ax = a1.number_input("ось X", value=0.0, step=0.1) + ay = a2.number_input("ось Y", value=1.0, step=0.1) + az = a3.number_input("ось Z", value=0.0, step=0.1) + dia = st.number_input("Диаметр, мм", 1.0, 60.0, 8.5, 0.5) + if st.button("Выполнить", type="primary"): + res_shape = drill(shp, (cx, cy, cz), (ax, ay, az), dia) + note = "D%.1f в (%.1f, %.1f, %.1f)" % (dia, cx, cy, cz) + + elif op == "Обрезать плоскостью": + c1, c2 = st.columns(2) + axn = c1.selectbox("Нормаль плоскости", ["X", "Y", "Z"], index=1) + pos = c2.number_input("Координата", value=-60.0, step=5.0) + keep = st.radio("Оставить", ["меньшую сторону", "большую сторону"], horizontal=True) + if st.button("Выполнить", type="primary"): + n = {"X": (1, 0, 0), "Y": (0, 1, 0), "Z": (0, 0, 1)}[axn] + p = tuple(np.array(n) * pos) + res_shape = trim(shp, p, n, + "negative" if keep == "меньшую сторону" else "positive") + note = "обрезано по %s=%.1f" % (axn, pos) + + elif op == "Скругление рёбер": + r = st.number_input("Радиус, мм", 0.2, 20.0, 1.0, 0.2) + lmin = st.number_input("Только рёбра длиннее, мм", 0.0, 200.0, 5.0, 1.0) + mode = st.radio("Тип", ["скругление", "фаска"], horizontal=True) + if st.button("Выполнить", type="primary"): + res_shape, cnt = round_edges(shp, r, lmin, 1e9, + "fillet" if mode == "скругление" else "chamfer") + note = "%s R%.1f на %d рёбрах" % (mode, r, cnt) + + if res_shape is not None: + from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs + outp = os.path.join(tempfile.mkdtemp(), "op_result.step") + w = STEPControl_Writer(); w.Transfer(res_shape, STEPControl_AsIs); w.Write(outp) + ch = check_watertight(outp) + k1, k2, k3 = st.columns(3) + k1.metric("Объём, мм3", "%.1f" % ch["volume"]) + k2.metric("Свободных рёбер", ch["free_edges"]) + k3.metric("Солидов", ch["solids"]) + (st.success if ch["ok"] else st.error)( + ("ГОДЕН ДЛЯ ПРОИЗВОДСТВА · " if ch["ok"] else "Не замкнуто · ") + note) + try: + v1, t1, f1 = mesh_by_face(read_step(outp)) + components.html(viewer_select_html(v1, t1, f1, {}, height=460, bg="#111a14"), + height=490) + except Exception as e: + st.caption("Просмотр недоступен: %s" % e) + st.download_button("Скачать STEP", open(outp, "rb").read(), + file_name="recada_op.step") + + # ---------------- ОБМЕР ---------------- + with tab_probe: + up = st.file_uploader("STEP-файл детали", type=["step", "stp", "STEP", "STP"], key="probe") + if up: + path = save_upload(up) + with st.spinner("Обмеряю: чтение B-Rep, граф смежности граней, гибы, отверстия…"): + + res = measure(path) + + c1, c2, c3, c4 = st.columns(4) + c1.metric("Толщина, мм", res["thickness_from_bends"]) + c2.metric("Гибов", len(res["bends"])) + c3.metric("Отверстий", res.get("holes_total", 0)) + c4.metric("Объём, мм³", f'{res["volume_mm3"]:.0f}') + + st.write(f'**Габарит:** {res["bbox"]["dx"]} × {res["bbox"]["dy"]} × {res["bbox"]["dz"]} мм ' + f'· **граней:** {res["faces_total"]} {res["faces_by_kind"]}') + + st.subheader("Гибы") + st.dataframe([{ + "R внутр": b["r_inner"], "R внеш": b["r_outer"], + "поворот°": b["angle_deg"], "двугранный°": b["dihedral_deg"], + "длина, мм": b["length"], "разгрузка": "да" if b["relief"] else "—", + "ось": ", ".join(f"{x:.3f}" for x in b["axis"]), + } for b in res["bends"]], width='stretch') + st.info("Поворот — на сколько отогнулась полка. Двугранный = 180° − поворот — " + "угол между полками. Путать их нельзя: старый Unfolder V1 показывал 45° и 80.6° " + "там, где истина 135° и 99.39°.") + + st.subheader("Отверстия") + rows = [] + for d, g in res["holes_by_diameter"].items(): + for it in g["items"]: + rows.append({"D, мм": d, + "центр": ", ".join(f"{x:.1f}" for x in it["center"]), + "ось": ", ".join(f"{x:.2f}" for x in it["axis"])}) + st.dataframe(rows or [{"—": "не найдено"}], width='stretch') + + st.subheader("Развёртка (припуски на гибы)") + + t = res["thickness_from_bends"] + tot, ur = 0, [] + for b in res["bends"]: + ba = bend_allowance(b["angle_deg"], b["r_inner"], t) + tot += ba + ur.append({"поворот°": b["angle_deg"], "R": b["r_inner"], + "K-фактор": k_factor(b["r_inner"], t), "BA, мм": round(ba, 3)}) + st.dataframe(ur, width='stretch') + st.write(f"**Суммарный припуск на гибы: {tot:.2f} мм** " + "(прямые участки берутся из скелета детали)") + + try: + _d = export_unfold_dxf(res, os.path.join(tempfile.mkdtemp(), "unfold.dxf")) + st.success("Развёртка: %.2f × %.2f мм" % (_d["flat_length"], _d["width"])) + st.download_button("Скачать развёртку DXF", open(_d["path"], "rb").read(), + file_name="unfold.dxf", mime="application/dxf") + except Exception as _e: + st.caption("Карта развёртки одним листом недоступна: %s" % _e) + + st.download_button("Скачать measurements.json", + json.dumps(res, ensure_ascii=False, indent=2), + file_name="measurements.json", mime="application/json") + + + # ---------------- ПАРАМЕТРИЧЕСКАЯ МОДЕЛЬ ---------------- + with tab_param: + st.write("Оригами-кинематика: пластины оболочки на шарнирах по линиям гиба. " + "Меняете углы — поддеревья поворачиваются, деталь перестраивается.") + + @st.cache_resource(show_spinner=False) + def _prep(path, key): + return fold_prepare(path) + + upp = st.file_uploader("STEP-файл детали", type=["step", "stp"], key="param") + if upp: + pth = save_upload(upp) + with st.spinner("Обмер, расщепление, поиск шарниров… (1–3 мин, дальше мгновенно)"): + ctx = _prep(pth, "%s:%d" % (upp.name, upp.size)) + js = ctx["joints"] + st.success("Пластин %d · шарниров %d · толщина %.2f мм · эталонный объём %.1f мм3" + % (len(ctx["side"]), len(js), ctx["res"]["thickness_from_bends"], ctx["vol"])) + + st.subheader("Углы гибов") + deltas = {} + cols = st.columns(2) + for k, j in enumerate(js): + with cols[k % 2]: + st.markdown("**%s** — поворот %.2f° (двугранный %.2f°), длина %.1f мм%s" + % (j["name"], j["angle"], j["dihedral"], j["length"], + ", разгрузка" if j["relief"] else "")) + deltas[j["name"]] = st.slider("Δ %s" % j["name"], -30.0, 30.0, 0.0, 0.5, + key="d_" + j["name"], label_visibility="collapsed") + st.caption("поддерево: %d пластин" % len(j["subtree"])) + + th = st.number_input("Толщина листа, мм", 0.5, 10.0, + float(ctx["res"]["thickness_from_bends"]), 0.1) + + if st.button("Собрать деталь", type="primary"): + with st.spinner("Складываю и наращиваю…"): + r = fold_apply(ctx, deltas, thickness=th) + act = list(r["applied"]) + if act: + st.write("Применено: " + ", ".join("%s %+.1f°" % (n, d) for n, d, s in act)) + else: + st.write("Углы не изменены — контрольная пересборка.") + if r["is_solid"]: + c1, c2 = st.columns(2) + c1.metric("Объём, мм3", "%.1f" % r["volume"]) + c2.metric("Δ к эталону, %", "%.2f" % ((r["volume"]-r["ref_volume"])/r["ref_volume"]*100)) + if not act: + dev = deviations(read_step(pth), read_step(r["path"]), 6) + inside = float((dev <= 0.05).sum())/len(dev)*100 + (st.success if inside >= 99.9 else st.error)( + "Метрология: %.3f %% точек в допуске — %s" + % (inside, "ПРИНЯТО" if inside >= 99.9 else "НЕ ПРИНЯТО")) + w = check_watertight(r["path"]) + if w["ok"]: + st.success("Целостность: замкнуто, солид — годен для отправки поставщику") + else: + st.error("Целостность: свободных рёбер %d (настоящих дыр %d), солидов %d — " + "портал поставщика такой файл отклонит. Метрология этого не ловит: " + "точки лежат на поверхности верно, но деталь не замкнута." + % (w["free_edges"], w["free_real"], w["solids"])) + st.download_button("Скачать деталь STEP", open(r["path"], "rb").read(), + file_name="recada_folded.step") + try: + import streamlit.components.v1 as _c + _v, _t = mesh_arrays(read_step(r["path"])) + st.caption("Мышь: вращать · колесо: приблизить · правая кнопка: сдвинуть") + _c.html(viewer_html(_v, _t), height=540) + except Exception as _e: + st.caption("Просмотр недоступен: %s" % _e) + else: + st.warning("Тело не построилось — сохранена оболочка. Причина: поверхность гиба " + "переносится жёстко и не перестраивается под новый угол, поэтому в стыке " + "появляется разрыв. Оболочку можно открыть во FreeCAD и оценить форму.") + st.download_button("Скачать оболочку STEP", open(r["path"], "rb").read(), + file_name="recada_shell.step") + + # ---------------- ГЕНЕРАЦИЯ КОДА ---------------- + with tab_code: + st.write("Загрузите STEP — инструмент обмерит деталь, подберёт неизвестные параметры " + "замкнутой петлёй (генерация → метрология → правка) и выдаст параметрический " + "Python-код, воспроизводящий деталь.") + upc = st.file_uploader("STEP-файл детали", type=["step", "stp"], key="code") + do_fit = st.checkbox("Подбирать неизвестные параметры (петля с метрологией)", value=True) + if upc and st.button("Сгенерировать код", type="primary"): + pth = save_upload(upc) + with st.spinner("Обмеряю…"): + res = measure(pth) + groups = _axis_groups(res["bends"]) + st.write(f'толщина **{res["thickness_from_bends"]} мм** · гибов **{len(res["bends"])}** · ' + f'отверстий **{res.get("holes_total", 0)}** · направлений осей гибов **{len(groups)}**') + + fitted = None + if do_fit and len(groups) == 1 and res["bends"]: + with st.spinner("Подбираю параметры (петля генерация → метрология)…"): + try: + fitted = fit(pth, verbose=False) + except Exception as e: + st.warning(f"Подбор не сошёлся: {e}") + elif do_fit and len(groups) > 1: + st.info("Оси гибов не параллельны — деталь пространственная. " + "Запускаю реконструкцию через оболочку (щуп + наращивание на толщину).") + try: + with st.spinner("Расщепляю на оболочки и наращиваю…"): + rb = rebuild_spatial(pth) + dev = deviations(read_step(pth), read_step(rb["path"]), 6) + inside = float((dev <= 0.05).sum()) / len(dev) * 100 + c1, c2, c3 = st.columns(3) + c1.metric("Точек в допуске", "%.3f %%" % inside) + c2.metric("Макс. отклонение, мм", "%.4f" % dev.max()) + c3.metric("Δ объёма, %", "%.3f" % ((rb["volume"]-rb["ref_volume"])/rb["ref_volume"]*100)) + (st.success if inside >= 99.9 else st.error)( + ("ПРИНЯТО" if inside >= 99.9 else "НЕ ПРИНЯТО") + + " · оболочка %s: %d/%d граней, торцов %d" + % (rb["side"], rb["faces_A"], rb["faces_B"], rb["faces_thin"])) + st.download_button("Скачать реконструкцию STEP", open(rb["path"], "rb").read(), + file_name="rebuilt.step") + st.caption("Толщина — настоящий параметр: та же форма пересобирается в 2.0 мм " + "командой python recada.py rebuild деталь.step 2.0") + except Exception as e: + st.warning("Реконструкция не удалась: %s" % e) + + code = make_generator(res) + if fitted: + vals = fitted["x"] + lines = code.split("\n") + out = [] + fi = 0 + for ln in lines: + if ln.startswith("FLAT") and fi < len(vals) - 1: + name = ln.split("=")[0].strip() + out.append(f"{name} = {vals[fi]:.3f} # подобрано петлёй (RMS {fitted['rms']:.5f} мм)") + fi += 1 + elif ln.startswith("WIDTH"): + out.append(f"WIDTH = {vals[-1]:.3f} # подобрано петлёй") + else: + out.append(ln) + code = "\n".join(out) + c1, c2 = st.columns(2) + c1.metric("Невязка RMS, мм", f'{fitted["rms"]:.5f}') + c2.metric("Подобрано параметров", len(vals)) + + dev = deviations(_rs(pth), _rs(fitted["fitted_step"]), 6) + inside = float((dev <= 0.05).sum()) / len(dev) * 100 + (st.success if inside >= 99.9 else st.error)( + f"Метрология: {inside:.3f} % точек в допуске 0.05 мм — " + + ("ПРИНЯТО" if inside >= 99.9 else "НЕ ПРИНЯТО")) + + st.subheader("Код для FreeCAD") + _sides = None + try: + if len(groups) > 1: + _sh, _fc, _inf, _G, _v = probe(pth) + _A, _B, _thin = split_shells(_sh, _fc, _G, res["thickness_from_bends"]) + _sides = sorted(_A if len(_A) >= len(_B) else _B) + except Exception: + pass + if len(groups) > 1: + with st.spinner("Готовлю самодостаточный код (скелет вшивается в текст)…"): + try: + fc_code = make_freecad_code_selfcontained(pth) + except Exception as _e: + st.warning("Не удалось вшить скелет: %s" % _e) + fc_code = make_freecad_code(res, step_path=upc.name, sides=_sides) + else: + fc_code = make_freecad_code(res, step_path=upc.name, sides=_sides) + if len(fc_code) > 6000: + head = fc_code.split("SKELETON = (")[0] + "SKELETON = ( # ...вшитая геометрия, %d символов...\n)" % len(fc_code) + st.code(head, language="python") + st.info("Скелет детали вшит в код (%d КБ) — показана только шапка. " + "Скачайте файл кнопкой ниже: он самодостаточен, внешние файлы не нужны." + % (len(fc_code)//1024)) + else: + st.code(fc_code, language="python") + st.download_button("Скачать код для FreeCAD", fc_code, file_name="recada_freecad.py") + st.caption("Скопируйте код (кнопка в правом верхнем углу блока) и вставьте в " + "консоль FreeCAD: View → Panels → Python console. " + "В пространственном варианте подставьте полный путь к STEP в переменную STEP.") + + with st.expander("Черновик build123d (для конвейера, не для FreeCAD)"): + st.code(code, language="python") + st.download_button("Скачать generator.py", code, file_name="generator.py") + st.download_button("Скачать params.json", + json.dumps(make_params(res), ensure_ascii=False, indent=2), + file_name="params.json") + st.caption("Проверьте семантику: что параметр (меняется от машины), что правило " + "(R = 1×t, D8.5 под М8), что артефакт производства (не воспроизводится).") + + + # ---------------- МЕТРОЛОГИЯ ---------------- + with tab_metro: + st.write("Сравнение двух STEP по контрольным точкам поверхности " + "(point-sampling + distToShape). Булевы операции не используются.") + a = st.file_uploader("Эталон (оригинал)", type=["step", "stp"], key="ref") + b = st.file_uploader("Кандидат (сгенерированный)", type=["step", "stp"], key="cand") + tol = st.slider("Допуск, мм", 0.01, 0.50, 0.05, 0.01) + per_face = st.slider("Точек на грань", 3, 20, 6) + if a and b and st.button("Сравнить", type="primary"): + pa, pb = save_upload(a), save_upload(b) + with st.spinner("Считаю отклонения…"): + + + import numpy as np + ref, cand = read_step(pa), read_step(pb) + dev = deviations(ref, cand, per_face) + vr, vc = volume(ref), volume(cand) + inside = float((dev <= tol).sum()) / len(dev) * 100 + c1, c2, c3 = st.columns(3) + c1.metric("Точек в допуске", f"{inside:.3f} %") + c2.metric("Макс. отклонение, мм", f"{dev.max():.4f}") + c3.metric("Δ объёма, %", f"{(vc - vr) / vr * 100:.4f}") + ok = inside >= 99.9 and abs(vc - vr) / vr * 100 < 1 + (st.success if ok else st.error)("ПРИНЯТО" if ok else "НЕ ПРИНЯТО (порог 99.9 %)") + st.caption(f"точек: {len(dev)} · среднее {dev.mean():.5f} мм · " + f"95-й процентиль {np.percentile(dev, 95):.5f} мм") + st.warning("Объём — не критерий: сдвиг детали на 0.3 мм даёт совпадение объёма " + "0.0000 % при 74 % точек в допуске.") + + + # ---------------- СПРАВКА ---------------- + with tab_help: + st.markdown(""" + ### Что делает инструмент + 1. Читает STEP, обходит B-Rep, строит **граф смежности граней** (networkx). + 2. **Толщина** — по соосным цилиндрам (Rвнеш − Rвнут), контроль по парам плоскостей. + 3. **Гибы** — пары соосных цилиндров с ΔR = t: радиусы, поворот, двугранный угол, ось, длина. + 4. **Разгрузки** — гиб с числом контуров > 1 или рёбер > 4 считается прерванным вырезом. + 5. **Отверстия** — частичные круглые рёбра сшиваются в окружности, пара окружностей = отверстие. + 6. **Развёртка** — аналитически: BA = φ·(R + K·t), K по таблице R/t. + 7. **Приёмка** — point-sampling: точки на эталоне → расстояние до кандидата. + + ### Проверено + - Реальная деталь Defender LRDE-2BR1: t = 3.00 мм, 7 гибов R3/R6, 7 отверстий. + - Замкнутый цикл: параметры → тело → обмер вернул те же числа. + - Метрология: копия 100 %, полка +2 мм — 78.6 %, сдвиг 0.3 мм — 74 %. + """) + +elif __name__ == "__main__": + _cli() diff --git a/tool/УСТАНОВКА.md b/tool/УСТАНОВКА.md new file mode 100644 index 0000000..4cab33b --- /dev/null +++ b/tool/УСТАНОВКА.md @@ -0,0 +1,121 @@ +# Инструмент реверса RECADA — установка (FreeCAD НЕ нужен) + +Инструмент работает на ядре OpenCASCADE через библиотеку OCP — это то же ядро, +что внутри FreeCAD, но отдельная библиотека. **FreeCAD не требуется ни для чего.** + +## 1. Python +Поставьте **Python 3.12** с python.org (3.13/3.14 пока без готовых колёс OCP). +При установке отметьте **«Add python.exe to PATH»**. + +Проверка в PowerShell: +```powershell +python --version +``` + +## 2. Папка и окружение +```powershell +mkdir C:\CAD\recada-tool +cd C:\CAD\recada-tool +# положить сюда recada.py +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +``` +Если PowerShell ругается на политику: +```powershell +Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +``` + +## 3. Библиотеки (~700 МБ, 5–10 минут) +```powershell +pip install cadquery-ocp build123d networkx numpy scipy streamlit +``` + +## 4. Запуск браузерного интерфейса +```powershell +streamlit run recada.py +``` +Откроется http://localhost:8501 + +## Вкладки +| Вкладка | Что делает | +|---|---| +| **Обмер детали** | STEP → толщина, гибы (оба угла), отверстия, разгрузки, припуски развёртки, measurements.json | +| **Генерация кода** | STEP → обмер → подбор неизвестных параметров петлёй → готовый параметрический Python-код + params.json | +| **Приёмка** | два STEP → % точек в допуске, вердикт | +| **Справка** | как устроен алгоритм | + +## Командная строка (без браузера) +```powershell +python recada.py probe деталь.step # обмер +python recada.py fit деталь.step # подбор параметров + код +python recada.py metro эталон.step кандидат.step +python recada.py unfold деталь.step +``` + +## Как достигается «100 % повторение» +По методике из документа (раздел «Критерий 100% копии»): +1. Обмер даёт скелет: толщина, радиусы, углы, оси, отверстия. +2. Генератор строит деталь из параметров. +3. **Двусторонняя** метрология (эталон→кандидат И кандидат→эталон) считает невязку. +4. Оптимизатор Powell/CMA подбирает неизвестные параметры до нулевой невязки. +5. Приёмка: ≥ 99.9 % точек в допуске + Δ объёма < 1 %. + +**Проверено:** на эталонной детали с известными параметрами (40 / 60 / 25 / ширина 80) +петля восстановила их точно: RMS 0.00000 мм, 100 % точек, Δ объёма 0.0000 %. + +## Важные уроки, встроенные в инструмент +- **Односторонняя метрология обманывает.** Первый прогон дал RMS 0 и 100 % точек + при детали ДЛИННЕЕ эталона на 1.8 мм — точки эталона лежали на кандидате, + а лишний металл не был виден. Поэтому невязка двусторонняя. +- **Объём — не критерий.** Сдвиг детали на 0.3 мм даёт Δ объёма 0.0000 %. +- **Два угла гиба.** Поворот (развёртка цилиндра) и двугранный (180 − поворот). + Старый Unfolder V1 показывал 45° и 80.6° там, где истина 135° и 99.39°. + +## Ограничения +- Автоподбор работает для деталей с **параллельными осями гибов** (одна цепочка). + Для пространственных (как Defender LRDE-2BR1, 3 направления осей) выдаётся + скелет кода с группировкой по осям и TODO — сборка цепочек вручную. +- Отверстия попадают в код комментариями с координатами; перенос в hole_array() + с привязкой к базам (datums) — ручной шаг. +- Разгрузки распознаются, но генерируются правилом (features.py), а не копируются. + +--- + +## Обновление от 01.09 (важные исправления) + +**1. Метрология мерила до тела, а не до поверхности.** +`distToShape` до солида возвращает 0 для точек ВНУТРИ материала — значит «толстый» +кандидат мог пройти приёмку. Теперь замер идёт до оболочки (`surface_of`). +Разница на проверке: сдвиг детали на 0.3 мм давал 74 % точек в допуске, теперь 29.8 %. +Изменение толщины 3 → 4 мм тоже ловится (29.8 %), раньше могло проскочить. + +**2. Дубли отверстий.** Центры окружностей склеиваются с допуском 0.05 мм. +На Defender было «7 отверстий», стало правильные 6 (одно D20 двоилось). + +**3. Развёртка в DXF** — кнопка на вкладке обмера и команда: +``` +python recada.py dxf деталь.step unfold.dxf +``` +Контур заготовки + линии гиба на слое BEND с подписями «R3 90°». + +**4. Скелет кода для пространственных деталей** содержит упорядоченную цепочку +гибов: ось, точка прохождения, угол, пометка разгрузки. + +## Что НЕ получилось (честный статус) +Автоматическая сборка пространственных деталей (как Defender LRDE-2BR1 — +три направления осей гибов) требует расщепления детали на две оболочки. +Пробовали три алгоритма: +- по габаритам граней (торец = узкая полоса) — нашёл 18 из ~120 торцов; +- по эквидистантным партнёрам (плоскость-плоскость на t, цилиндры с ΔR = t) — + 67 граней из 187, граф не развалился надвое; +- виртуальным щупом (точка − нормаль × t должна лежать на поверхности) — + 38 граней, критерий слишком строг на скруглениях и малых гранях. + +Причина: топология реальной детали сложнее канонического листа — одной грани +с одной стороны соответствует несколько с другой, есть места прямого смыкания. +Следующий подход: обход графа от базовой грани с локальным замером толщины +и мягким критерием (доля точек, а не все), плюс отдельная обработка мелких +скруглений контура. Оценка: 1–2 дня работы. + +До этого пространственные детали проходят обмер, развёртку по гибам и метрологию, +но код выдаётся скелетом с TODO, а не готовым. diff --git a/tool/ИНСТРУМЕНТЫ.md b/tool/ИНСТРУМЕНТЫ.md new file mode 100644 index 0000000..6a1c397 --- /dev/null +++ b/tool/ИНСТРУМЕНТЫ.md @@ -0,0 +1,77 @@ +# Инструменты, использованные при создании RECADA-реверса + +Всё бесплатное и open source. Платные строки проекта — только серверы и токены. + +## Установка целиком +``` +pip install cadquery-ocp build123d networkx numpy scipy streamlit ezdxf +``` + +## Ядро геометрии + +| Инструмент | Роль в проекте | +|---|---| +| **OCP** (cadquery-ocp 7.9.3) | Python-биндинг ядра OpenCASCADE. Чтение и запись STEP, обход B-Rep, все булевы операции, наращивание, сшивка, метрология. Ровно то ядро, что внутри FreeCAD, но как библиотека — поэтому FreeCAD в цепочке не нужен | +| **build123d 0.11.1** | Параметрическое построение (кубики листового металла). Проверено: операций Bend/Flange/Hem в нём НЕТ, писали свои | +| **networkx 3.6** | Графы: AAG (смежность граней), FTG (переходы гибки), двухцветная раскраска для разделения оболочек, поиск поддеревьев при повороте шарниров | +| **numpy** | Векторная математика: нормали, оси, углы, проекции | +| **scipy** | Оптимизатор Powell — подбор неизвестных параметров по метрологической невязке | +| **ezdxf** | Экспорт развёртки в DXF с линиями гиба на отдельном слое | +| **streamlit 1.62** | Браузерный интерфейс: загрузка STEP, ползунки углов, вердикты, скачивание | + +## Конкретные модули OpenCASCADE, которые пригодились + +| Модуль | Зачем | +|---|---| +| `STEPControl_Reader/Writer` | чтение и запись STEP | +| `TopExp_Explorer`, `TopTools_IndexedDataMapOfShapeListOfShape` | обход топологии, поиск смежности граней и свободных рёбер | +| `BRepAdaptor_Surface`, `BRepLProp_SLProps` | тип поверхности, точка и нормаль в UV | +| `BRepTopAdaptor_FClass2d` | проверка, попадает ли UV-точка внутрь обрезанной грани (без неё метрология врала) | +| `BRepExtrema_DistShapeShape` | расстояние точка→поверхность — основа метрологии | +| `BRepBuilderAPI_Sewing` | сшивка граней в оболочку | +| `BRepBuilderAPI_MakeSolid` + флаг Closed | превращение замкнутой оболочки в тело (без этого STEP пишет поверхность, и портал отклоняет) | +| `BRepOffset_MakeOffset` | наращивание на толщину; поэлементно надёжнее, чем всей оболочкой | +| `BRepAlgoAPI_Fuse / Common` | объединение кусков, разрез полупространством | +| `BRepPrimAPI_MakeHalfSpace / MakePrism / MakeBox` | растяжение детали: разрез, призма сечения | +| `BRepFill_Filling` | заплатки на свободные контуры (даёт сплайн — распознавание листа ломает, применять осторожно) | +| `ShapeAnalysis_FreeBounds` | сборка свободных рёбер в контуры | +| `ShapeUpgrade_UnifySameDomain` | склейка граней одной поверхности после булевых | +| `GProp/BRepGProp` | объёмы и площади | +| `Bnd_Box/BRepBndLib` | габариты | + +## Методы (что из v25 реально пригодилось) + +- **AAG** — граф смежности граней, основа всего обмера +- **Виртуальный щуп** — точка минус нормаль на толщину: попала на поверхность → грань оболочечная. Дал разделение на две стороны листа там, где не сработали три других алгоритма +- **Парование + двухцветная раскраска** — щуп сообщает, на какую грань попал; граф парования оказался двудольным, стороны разделились 21/20 +- **FTG** — фланцы узлы, гибы рёбра +- **Оригами-кинематика** — жёсткие поддеревья на шарнирах по осям гибов +- **Метрология point-sampling** — расстояние от точек эталона до поверхности кандидата +- **Два угла гиба** — поворот и двугранный (180 − поворот) +- **Аналитическая развёртка** — BA = φ·(R + K·t), K по таблице R/t + +## Отвергнуто на практике + +- **pythonocc-core отдельно** — то же ядро уже есть через OCP +- **CadQuery** — дублирует build123d +- **Analysis Situs** — тяжёлая C++-платформа ради того, что делают ~100 строк на networkx +- **Наращивание оболочки целиком** — оставляет дыры; поэлементно надёжнее +- **Заплатки BRepFill** для зоны гиба — дают сплайновую грань, распознавание листа ломается +- **Растяжение по глобальной оси** для наклонной стенки — даёт ступеньку в контуре; тянуть надо вдоль плоскости стенки + +## Три ошибки, найденные и исправленные по ходу + +1. **Метрология мерила до тела, а не до поверхности.** `distToShape` до солида возвращает 0 для точек внутри материала — «толстая» деталь проходила приёмку. Проверка: сдвиг 0.3 мм давал 74 % точек в допуске, после исправления 29.8 % +2. **Односторонняя выборка не видит лишний металл.** Кандидат длиннее эталона давал RMS 0 и 100 %. Невязка сделана двусторонней +3. **Не было проверки замкнутости.** Деталь может быть точной и дырявой одновременно; метрология этого не ловит. Добавлен подсчёт свободных рёбер — предсказал отказ портала до самого отказа + +## Три критерия приёмки (итог) + +| Критерий | Чем проверяется | Что ловит | +|---|---|---| +| Метрология | point-sampling двусторонний, до поверхности | точность геометрии | +| Целостность | свободные рёбра = 0, солидов ≥ 1 | дыры, незамкнутость | +| Производство | загрузка на портал поставщика | распознавание как листовой детали | + +Третий критерий главный и не заменяется первыми двумя. Проверено: номинальная +реконструкция прошла портал Blechcon с развёрткой 299 × 168 мм — как оригинал. diff --git a/tool/ЗАПУСК.md b/tool/ЗАПУСК.md new file mode 100644 index 0000000..9aa2c25 --- /dev/null +++ b/tool/ЗАПУСК.md @@ -0,0 +1,62 @@ +# Запуск + +Рабочая папка: `C:\CAD COWORK` + +## Одной командой + +**cmd:** +``` +"C:\CAD COWORK\go.bat" +``` + +**PowerShell:** +```powershell +& "C:\CAD COWORK\go.ps1" +``` + +Скрипт сам переносит свежий `recada.py` из Downloads, если он там лежит, +и запускает интерфейс на `http://localhost:8501`. + +## Вручную + +**cmd:** +``` +cd /d "C:\CAD COWORK\tool" +.venv\Scripts\activate +streamlit run recada.py --server.maxUploadSize 2000 +``` + +**PowerShell** (без активации окружения, чтобы не возиться с политикой скриптов): +```powershell +cd "C:\CAD COWORK\tool" +.\.venv\Scripts\python.exe -m streamlit run recada.py --server.maxUploadSize 2000 +``` + +Если порт занят — добавьте `--server.port 8502`. + +## Вкладки + +| Вкладка | Что делает | +|---|---| +| Рабочее окно | дерево сборки, групповой реверс, операции, подъём платформы, пакет производства | +| Изделие | продуктовые параметры: высота, длина, поперечины, проверки | +| Просмотр 3D | свой просмотрщик: разлёт, сечение, измерение, выбор деталей | +| Сборка | разбор сборки, выгрузка детали отдельным STEP | +| Операции | зеркало, сверление, заглушка, обрезка, скругления | +| Обмер детали | толщина, гибы, отверстия, разгрузки, DXF развёртки | +| Параметрическая модель | углы гибов ползунками, сборка тела | +| Генерация кода | самодостаточный Python для FreeCAD | +| Приёмка | метрология двух файлов | + +## Командная строка + +``` +python recada.py probe|metro|unfold|dxf|rebuild|fold|fold2|stretch|check|fit файл.step +``` + +Примеры: +``` +python recada.py check "C:\CAD COWORK\parts\LRDE-2BR1\LRDE-2BR1_nominal_v2.step" +python recada.py fold2 деталь.step bend_94=10 +python recada.py stretch деталь.step y 20 +``` diff --git a/vehicles/.gitkeep b/vehicles/.gitkeep new file mode 100644 index 0000000..198ecfb --- /dev/null +++ b/vehicles/.gitkeep @@ -0,0 +1 @@ +Пока пусто. См. README.md — назначение папки. diff --git a/vehicles/_vorlage_fahrzeugpass.json b/vehicles/_vorlage_fahrzeugpass.json new file mode 100644 index 0000000..5a9c01a --- /dev/null +++ b/vehicles/_vorlage_fahrzeugpass.json @@ -0,0 +1,22 @@ +{ + "vehicle": "ford_ranger_p703_dc", + "name": "Ford Ranger P703 Double Cab", + "coordinate_system": "OEM", + "sources": [ + {"type": "aufbaurichtlinien", "doc": "Ford BEMM", "version": "", "pages": []}, + {"type": "oem_cad", "file": "", "format": "STEP"}, + {"type": "scan", "file": "", "accuracy_mm": 0.1}, + {"type": "brackets", "note": "подтверждено металлом"} + ], + "roof": {"length_mm": null, "width_mm": null, "profile": "rinne|reling|gewinde"}, + "mount_points": [ + {"id": "L1", "xyz": [null, null, null], "normal": [null, null, null], + "type": "rinne", "max_load_kg": null, "source_page": null} + ], + "forbidden_zones": [ + {"id": "antenne", "shape": "box", "xyz_min": [], "xyz_max": [], "source_page": null} + ], + "limits": {"roof_load_kg": null, "roof_load_offroad_factor": 0.5, + "cog_height_max_mm": null, "vehicle_height_mm": null}, + "fbx": {"file": "", "scale_factor": null, "calibrated_by": "radstand|breite|dachhoehe"} +}