commit a71b6760f5957003f0a79fd47ef8971443b930ab Author: Курнат Андрей Date: Sun Oct 18 22:05:57 2026 +0300 Initial commit: QInventor CAD workbench for Windows diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a128cdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +runtime/ +downloads/ +data/ +logs/ +release/ +artifacts/ +__pycache__/ +*.pyc +*.FCStd1 +*.FCBak +scripts/launcher/bin/ +scripts/launcher/obj/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a149e48 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# QInventor + +Windows desktop CAD built as an independent Python/Qt workbench on the unmodified FreeCAD portable runtime (Open CASCADE). Russian UI. All source belongs here; do not edit C:\Repos\QCad or other repositories. + +## Layout and ownership + +- `src/QInventor/Init.py`, `InitGui.py`: FreeCAD workbench startup. +- `src/QInventor/qi_ui.py`: custom ribbon, dialogs, welcome/help, workbench integration. +- `src/QInventor/qi_commands.py`: command registry; calls services and native workbench commands. +- `src/QInventor/qi_model.py`: parametric part operations, examples, project persistence, assembly/BOM and drawing services. +- `scripts/`: reproducible bootstrap, sync, launcher/package, smoke automation. +- `tests/`: FreeCAD real-kernel integration and GUI smoke tests. + +Use `from PySide import QtCore, QtGui`; import QtWidgets with compatibility fallback. Prefer FreeCAD transactions for mutations. Reject invalid dimensions before modifying documents. Preserve original native tool access. No fake-success buttons or mesh-only replacements for solid geometry. No claims of Autodesk Inventor file compatibility or full commercial feature parity. + +Workbench services should be testable through FreeCAD Python. GUI tests may use Qt APIs/macros and save actual screenshots. The portable runtime licenses and upstream notices remain intact. No external service requirement. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..06e4567 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 QInventor contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/QInventor.cmd b/QInventor.cmd new file mode 100644 index 0000000..7fb9c9c --- /dev/null +++ b/QInventor.cmd @@ -0,0 +1,9 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" +if exist "%~dp0QInventor.exe" ( + start "" "%~dp0QInventor.exe" %* + exit /b +) +echo Не найден QInventor.exe. Соберите приложение через scripts\build-launcher.ps1. +pause diff --git a/QInventor.exe b/QInventor.exe new file mode 100644 index 0000000..0f6282b Binary files /dev/null and b/QInventor.exe differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..233e73d --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# QInventor для Windows + +Параметрическое CAD-приложение с русской лентой, настоящими эскизами, точными +твёрдыми телами, сборками и ассоциативными чертежами. Основа — FreeCAD 1.1.4 / +Open CASCADE. Все необходимые CAD-библиотеки поставляются в `runtime`. + +## Запуск + +Откройте **QInventor.exe** в этой папке. Установка Python, .NET или Autodesk +Inventor не требуется. Для переноса копируйте всю папку, включая `runtime`, +`src`, `scripts`, `data/runtime-path.txt` и `examples`. + +Настройки находятся в `data`, журналы запуска — в `logs`. Проекты сохраняются +в выбранную пользователем папку. `FCStd` хранит эскизы, размеры, выражения, +операции, сборки и чертежи. Для обмена геометрией используйте STEP или IGES; +для 3D-печати — STL. Исходные IPT/IAM/IDW Autodesk Inventor не поддерживаются. + +## Первый эскиз и деталь + +1. Нажмите **Новая деталь → Создать эскиз**. +2. Выберите XY / XZ / YZ или заранее выделите плоскую грань детали. +3. **L** — непрерывная линия, **C** — окружность. Прямоугольник и дуга — на ленте. +4. **D** — универсальный размер: выберите геометрию, разместите размерную + линию, задайте значение. Размеры управляют геометрией; зависимости определяют + совпадение, направление, касание и равенство. +5. **Esc** отменяет текущий инструмент. **Завершить эскиз** выходит из редактора. +6. **E** открывает выдавливание: профиль, операция, направление, длина, + предпросмотр. Подтвердите результат. +7. Двойной щелчок по эскизу открывает редактирование. После изменения размера + последующие операции перестраиваются. Именованные параметры примеров + доступны также через **Параметры**. + +Размеры активного эскиза собраны в отдельной панели справа: выберите строку +для подсветки или дважды щёлкните для изменения. Стрелка у **Размер D** +позволяет явно выбрать горизонтальный размер, вертикальный, длину, радиус, +диаметр или угол. Кнопки завершения и отмены закреплены под лентой; +панель задач не перекрывает рабочее поле. + +Мышь: левая кнопка — выбор; колесо — масштаб; средняя — перемещение; +Shift + средняя — вращение. Ctrl+S сохраняет, Ctrl+Z / Ctrl+Y отменяют и повторяют +операцию. Подробности: [docs/workflows.md](docs/workflows.md). + +## Возможности + +- Общая таблица модельных и пользовательских параметров: единицы, формулы, + вычисленные значения, комментарии, связанные размеры и отмена одним шагом. +- Эскизы на плоскостях и гранях, геометрические зависимости, управляющие и + справочные размеры, ввод возле курсора, решатель ограничений. +- Связанные проекции рёбер и контуров грани в эскиз; преобразование проекции + в независимую геометрию с сохранением текущего построения. +- Смещение окружностей, отрезков и выпуклых линейных контуров; связанные + зеркала, прямоугольные и круговые массивы отрезков/окружностей с русскими + параметрами, предпросмотром, отменой и сохранением зависимостей. +- Выдавливание, вырез, вращение, отверстия, скругления, фаски, массивы, + зеркальное отражение; остальные операции — в исходной среде PartDesign. +- Параметрические заготовки, пластина с отверстиями, кронштейн. +- Сборки с компонентами, жёсткими/вращательными/скользящими/цилиндрическими + соединениями и решателем; сводная и структурированная спецификация вложенных + экземпляров, CSV и масса по материалам конечных деталей. +- Лист A4 со связанными проекциями и спецификацией: + **Чертёж → Оформление → Спецификация на листе**. Выберите лист или откройте + единственный лист документа, проверьте источники и сводный/структурный состав, + затем разместите таблицу. Геометрия, количество и масса связаны с моделью; + лист сохраняется в FCStd и экспортируется в PDF, SVG и DXF. +- Материалы, объём, масса, центр масс, сохранение истории, STEP/IGES/BREP/STL. +- Исходные среды FreeCAD доступны через **Вид → Все инструменты** и штатный + переключатель сред. Они включают дополнительные инструменты моделирования, + таблиц, CAM и FEM. + +QInventor воспроизводит основной пользовательский процесс Inventor на другом +геометрическом ядре. Полное совпадение всех инструментов, алгоритмов решателя, +файловых форматов и поведения Inventor **не подтверждено и не заявляется**. +Границы соответствия перечислены в [docs/inventor-compatibility.md](docs/inventor-compatibility.md). + +## Разработка и проверки + +Цель развития, очередь работ и критерии готовности: +[docs/development-roadmap.md](docs/development-roadmap.md). + +Исходный код интерфейса и сервисов находится в `src/QInventor`. В PowerShell: + +```powershell +.\scripts\bootstrap.ps1 +.\scripts\build-launcher.ps1 +.\scripts\test.ps1 +.\scripts\package.ps1 +``` + +Тесты запускаются в отдельном профиле настоящего CAD-процесса, проверяют +геометрию, изменение размеров, сохранение/повторное открытие, STEP, эскизный +процесс, сборку и чертёж. Отчёт — `artifacts/validation.json`, изображения и +проверочные модели — `artifacts/validation`. +Полный прогон с подготовкой нативных чертежей занимает около 40 минут; +штатный лимит `scripts/test.ps1` — 60 минут. +Повреждённые циклические графы проверяются в отдельном FreeCADCmd с собственным +профилем: графический процесс не создаёт их сцену. В отчёте сохраняются +результаты проверок и код завершения дочернего CAD-процесса. + +Лицензия собственного кода — MIT. Лицензии CAD-движка сохранены в `runtime`; +см. [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..dc05416 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,18 @@ +# Third-party components + +QInventor uses the unmodified official FreeCAD 1.1.4 Windows x86-64 Python 3.11 +portable distribution, including Open CASCADE, Qt, Coin3D, Python, Sketcher, +PartDesign, Assembly, TechDraw, Draft, CAM, FEM and their dependencies. + +FreeCAD source and corresponding release: https://github.com/FreeCAD/FreeCAD/tree/1.1.4 +FreeCAD license: https://github.com/FreeCAD/FreeCAD/blob/1.1.4/LICENSE +The upstream distribution's license, COPYING and dependency notices remain in +`runtime`. This project's MIT license applies only to QInventor's own source. +Users can replace the runtime with a compatible official distribution; QInventor +does not modify or statically link FreeCAD. Runtime source is available at the +release link above. + +Autodesk and Autodesk Inventor are trademarks of Autodesk, Inc. QInventor is +an independent application and has no affiliation with Autodesk. Native Inventor +IPT/IAM/IDW interoperability and identical internal proprietary algorithms are +not implemented. Use neutral STEP/IGES formats to exchange models. diff --git a/docs/development-roadmap.md b/docs/development-roadmap.md new file mode 100644 index 0000000..1039cac --- /dev/null +++ b/docs/development-roadmap.md @@ -0,0 +1,318 @@ +# Развитие QInventor до практического аналога Inventor + +Дата исходного аудита: 12 октября 2026 года. Цель активна. + +## Результат, к которому идём + +Самостоятельное Windows-приложение с русским интерфейсом, в котором инженер +может пройти связанный процесс: эскиз → параметрическая деталь → сборка → +спецификация → оформленный чертёж → сохранение и обмен точной геометрией. +Сценарий должен оставаться работоспособным после изменения размеров, +отмены/повтора действий и повторного открытия проекта. + +Архитектура сохраняется: собственные Python/Qt-сервисы и интерфейс поверх +неизменённого portable FreeCAD/Open CASCADE. Исходники, проверки и сборка +принадлежат этому репозиторию. Дополнительные внешние сервисы не нужны. + +Ориентир — поведение рабочих инструментов Inventor. Совместимость IPT/IAM/IDW, +совпадение закрытого ядра Autodesk и полная коммерческая эквивалентность +не входят в доказанный результат. Продвинутые инженерные модули требуют +отдельных этапов реализации и проверки. + +## Что обнаружено в текущем коде + +| Область | Реальная основа | Основной следующий шаг | +|---|---|---| +| Эскиз | Sketcher, универсальный размер, проекция, связанные offset/зеркала/массивы, отменяемый предпросмотр | Дуги и составные контуры; конфликты ограничений; интерактивность выбора | +| Деталь | Нативные PartDesign-операции; собственное ассоциативное выдавливание; общая таблица параметров и выражений | Единый процесс остальных операций с предпросмотром и повторным редактированием | +| Сборка | Native Assembly, App::Link, закрепление и соединения с решателем | Проверка ошибок решателя; вставка и замена компонентов; вложенные сборки | +| Спецификация | Сводный и структурный состав вложенных экземпляров, материалы, масса, CSV и ассоциативная таблица на A4 | Приёмка переносимого выпуска; исключения, сохранённая нумерация, продолжение и выноски | +| Чертёж | Три связанные TechDraw-проекции детали/сборки, русский A4, PDF/SVG/DXF и таблица состава | Основные надписи, размеры, разрезы, компоновка и большие сборочные листы | +| Поставка | Launcher, sync, portable runtime, проверка снимка исходников при упаковке | Регрессионные проекты, проверка переносимой сборки после каждого этапа | + +Ключевые точки архитектуры: `src/QInventor/qi_model.py`, `qi_commands.py`, +`qi_ui.py`, `qi_parameters.py`, `qi_parameters_ui.py`, `qi_bom.py`, `qi_bom_ui.py`, `qi_extrude.py`, `qi_projection.py`, `qi_sketch_offset.py`, +`qi_sketch_patterns.py`; интеграционные сценарии находятся в `tests`. +Вращение, отверстия, скругления, фаски и 3D-массивы уже доступны через штатные +команды PartDesign. Их следует улучшать как существующие сценарии. + +## Очередь реализации + +### 1. Надёжность связанного процесса + +- Устранить блокировку создания чертежа сборки без выделенной детали. +- Сохранять отсутствие проекта при отмене выбора плоскости нового эскиза. +- Исправить редактирование координат со знаком, углов и размеров, + управляемых выражениями, без потери параметрических связей. +- Проверять состояние исходной геометрии до создания листа и экспорта. +- Отвергать неуспешное соединение по результату реального Assembly solver; + откатывать новый объект и положения компонентов. +- Проверить весь путь: сборка → чертёж → изменение детали → обновление → + сохранение/открытие → экспорт, включая недопустимые изменения. +- Сделать область чертежа явной: весь документ/сборка или выбранная деталь. + Сейчас команда строит виды всего документа независимо от выделения. + +Готовность: свежие kernel/GUI-проверки проходят; ошибки не оставляют ложного +результата, изменённой геометрии или лишнего шага в истории отмены. + +### 2. Параметрическая деталь для повседневной работы + +- Единая таблица модельных и пользовательских параметров: имя, размерность, + выражение, вычисленное значение; атомарное применение с проверкой перестроения. +- Единый русский процесс вращения, отверстий, скруглений, фасок и массивов + с выбором геометрии, предпросмотром и повторным редактированием. +- Расширить эскизные операции на дуги и составные контуры; диагностировать + недоопределённость и конфликтующие ограничения. + +Готовность: корпус, кронштейн, вал и фланец создаются через интерфейс; +изменение управляющего параметра обновляет downstream-операции. Неверные +значения, размерности и циклические зависимости не портят проект. + +### 3. Сборки и спецификации + +- Вставка из файлов/открытых документов, позиционирование, замена компонента + с сохранением поддерживаемых связей и понятной диагностикой остальных. +- Вложенные сборки, плоская и структурированная спецификация, обозначения, + позиции, исключение компонентов и корректная масса повторных экземпляров. +- Проверка пересечений твёрдых тел, управление видимостью и степенями свободы. + +Готовность: сборка с повторными деталями и подсборкой сохраняется, открывается, +решается; изменение исходника обновляет экземпляры. BOM учитывает структуру +и количество. STEP сохраняет экземпляры и их положения. + +### 4. Производственный чертёж + +- Формат/масштаб, редактируемая основная надпись, проекции и их размещение. +- Редактируемые поля основной надписи и явный выбор области листа. При + создании листа материал определяется по источникам видов; для разных материалов используется + «См. спецификацию». +- Размеры, оси, разрезы и выносные виды в доступном TechDraw-процессе. +- Таблица спецификации и позиции компонентов на сборочном листе. +- Продолжение спецификации на нескольких листах, большие сборки и другие + форматы. Первая таблица собственного A4 ограничена четырьмя сводными или + пятью отображаемыми структурными строками; переполнение должно давать отказ, + а не обрезанный экспорт. Позиционные выноски и общие шаблоны ещё требуют + отдельной реализации и проверки. +- Обновление всех ассоциативных видов после изменения детали или сборки. + +Готовность: детальный и сборочный листы проходят визуальную проверку; +PDF/SVG содержат настоящую геометрию и читаемые подписи. Проект хранит +редактируемые виды и связи, а не только изображение результата. + +### 5. Расширенные инженерные возможности + +Листовой металл с развёрткой; локальная библиотека стандартных деталей; +конфигурации деталей/сборок; простые правила проектирования; каркасы; +кинематика. Для каждого модуля сначала определить поддерживаемые сценарии, +ограничения и эталонные проекты. Наличие среды FreeCAD само по себе не +означает завершённого модуля QInventor. + +### 6. Подтверждение готовности и поставка + +- Свежий полный `scripts/test.ps1` с зафиксированными хешами исходников. +- Сохранение/повторное открытие регрессионных FCStd, проверка solids и связей. +- Реальные GUI-сценарии с Qt-событиями и снимками окон; визуальная проверка. +- `scripts/package.ps1`, запуск распакованной копии с новым и существующим + профилем, проверка отсутствия внешних зависимостей и сохранения лицензий. + +Этап принимается по конкретным рабочим проектам и результатам проверок. +Цель не считается выполненной только по наличию кнопок, количеству команд +или успешному запуску приложения. + +## Источники ориентира + +- [Autodesk: возможности Inventor](https://www.autodesk.com/ca-en/products/inventor/features) +- [Autodesk: работа с параметрами](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-595F9D8E-9EEA-4446-91AB-830E5BAE548E.htm) +- [Текущие границы соответствия](inventor-compatibility.md) +- [Рабочие сценарии QInventor](workflows.md) + +## Журнал этапов + +- 12.10.2026: проведён исходный аудит архитектуры, создана активная цель и + очередь работ. Свежая базовая проверка: 16 групп, 110 вложенных проверок, + 0 ошибок; native exit code 0. Начат этап надёжности связанного процесса. +- 12.10.2026: исправлены отмена выбора плоскости без создания пустого проекта, + координаты со знаком и углы эскиза, защита формул/справочных размеров, + повторное применение без потери точности и лишнего Undo. Чертёж сборки + создаётся без выделения тела; ошибочная исходная геометрия отвергается до + создания листа. Жёсткие соединения по началам компонентов проверяются по + фактическим мировым положениям, включая прямые и косвенные конфликты с + откатом всей сборки. Подготовка чертежа дожидается фонового расчёта и + пересчитывает проекции по окончательным размерам после быстрых изменений. +- Итог этого набора: `scripts/test.ps1` — 19 групп, 129 вложенных проверок, + 0 ошибок, native exit code 0, неизменный снимок 46 исходных файлов. + [Отчёт этого этапа](../artifacts/validation-previous-20261013-192524.json). Новые сценарии проверяют + настоящие Qt-действия, undo/redo, FCStd, ассоциативность и PDF/SVG. + Текущая рабочая среда синхронизирована. Новый portable-архив на этом этапе + не создавался. Общая цель остаётся активной; следующие крупные работы — + единая таблица параметров, полноценные сборочные сценарии и оформление листов. +- 13.10.2026: реализована общая таблица модельных и пользовательских параметров: + длина, угол и число; формулы, комментарии, переименование и удаление. + Новые переменные и связи применяются одним шагом Undo. Геометрия и + зависимости предварительно проверяются на временной нативной копии; + отклонение сохраняет существующий Redo, в том числе у детали с чертежом. + Проверены сохранение FCStd, внешние зависимости документов и циклы. + Таблица защищена от удаления скрытой строки и применения устаревшего + индекса ограничения после изменений вне окна; добавлено явное обновление. + Готовность проекций проверяется отдельно на каждой стадии расчёта. + Непосредственный экспорт DXF после быстрых изменений обновляет виды: + регрессия измеряет координаты экспортированных линий, а не только наличие файла. + Экспорт существующего листа проверяет нативные ошибки его зависимостей, + включая историю связанной детали в другом документе, до записи файлов. + Это предотвращает экспорт старой сохранённой геометрии ошибочной операции. + Независимые листы и проекции эскизов/линий сохраняют поддержку экспорта. +- Итог этапа таблицы параметров: свежий `scripts/test.ps1 -TimeoutSeconds 1800` + прошёл 21 группу и 155 вложенных проверок без ошибок; native exit code 0. + Снимок всех 50 исходных файлов совпал с текущим кодом. Таблица отдельно + проверена 16 сценариями ядра и 9 GUI-сценариями. Регрессия связанного чертежа + подтверждает актуальную DXF-геометрию после быстрых изменений и отказ + PDF/SVG/DXF до записи при ошибке операции исходной связанной детали. + [Полный отчёт](../artifacts/validation-previous-20261013-211849.json). Исходная рабочая среда + синхронизирована; portable-выпуск по этому снимку проверен свежим и + повторным запуском обычного launcher с сохранением FCStd и параметров. + Общая цель остаётся активной: дальнейшие этапы включают единый процесс + остальных операций детали, развитие сборок и производственных чертежей. + Ближайший приоритет — иерархическая спецификация вложенных сборок с + материалом и массой конечных деталей; нативная вставка FCStd и повторных + компонентов уже доступна и должна сохраняться. +- 13.10.2026: добавлены сводная и структурная спецификации вложенных сборок. + Количество внутри родителя и общее количество разделены; итоговая масса + считается по конечным деталям без повторного сложения родительских строк. + Нативные жёсткие и гибкие подсборки, внешние ссылки, масштабы и массивы + сохраняют фактические экземпляры и мировые положения. Скрытые элементы + остаются в физическом составе. Материал и плотность каждого экземпляра + определяют массу и центр масс; native PropertyDensity переводится из + внутренних единиц в кг/м³. + Определения деталей, развёрнутые элементы массива и исходники булевых + операций не становятся дополнительными деталями. Циклы, отсутствующие + ссылки, ошибочные операции и плотности отклоняются до записи CSV. + CSV заменяется атомарно после проверки всей выбранной области. + Основная надпись использует материалы источников видов; посторонняя деталь + документа не меняет материал листа. Пустой контейнер конструкционной + геометрии не мешает просмотру массы единственной физической сборки. +- Точечные проверки этого этапа: 13 сценариев ядра, 3 группы реальных Qt + действий и 8 независимых проверок ранее найденных ошибок прошли успешно; + native-процессы завершились с кодом 0. FCStd, изменение исходника, + Undo/Redo и STEP проверены на сборке из семи твёрдых деталей; количество + и положения в STEP измерены после чтения экспортированного файла. + Итоговая полная регрессия прошла 23 группы и 172 вложенные проверки + без ошибок, native exit code 0. Контрольные суммы всех 54 исходных файлов + совпали с проверенным снимком. [Полный отчёт](../artifacts/validation-previous-20261014-224048.json). + Дополнительно исправлен обработчик входа/выхода штатного редактора сборки: + он обрабатывает только доступный объект эскиза. Проверка выбора ребра + использует один жест мыши с обработкой событий между нажатием и отпусканием; + клавиша D направляется фактическому Qt-фокусу внутри нативного viewport. + Сохраняются проверка реального выбора, штатный диалог 40 мм, решатель и solid. + Точечный прогон ленты прошёл 11/11 проверок, native exit code 0. + Следующий приоритет после поставки этого этапа — ассоциативная таблица + спецификации и позиции на сборочном листе, затем единый процесс остальных + операций детали. Рендер и экспорт таблицы на листе ещё требуют реализации + и проверки; наличие нативных типов TechDraw не подтверждает готовность. + +- 14.10.2026, текущий этап: реализована ассоциативная спецификация на русском + A4 через нативные Spreadsheet::SheetPython и TechDraw::DrawViewSpreadsheet. + Область берётся из источников проекций листа. Чертёж основной сборки хранит + ссылку на её корень, поэтому изменение состава обновляет виды и таблицу. + Проверены реальные метрики шрифта, число экземпляров, масса, FCStd и + восстановление значения ячейки при Undo до дополнительного пересчёта. + Отдельные проверки ядра прошли 10/10; DXF — 12/12 на настоящем повторном + импорте. Независимая проверка восьми исходных/дополненных DXF подтверждает + корректные идентификаторы и ссылки записей без скрытого ремонта при чтении. + [Доказательства DXF](../artifacts/drawing-bom-dxf-review/README.md). + Первый полный прогон остановлен лимитом 1800 секунд после 17 успешных групп + без ошибок; снимок 59 исходных файлов не изменился. Повторный прогон того же + набора завершился с 24 успешными группами и одной ошибкой: экспорт при + переполнении A4 скрывал точную причину за общим сообщением. CAD завершился + с кодом 0; все 59 исходных файлов сохранили контрольные суммы. Исправлена + диагностика ошибки таблицы, конфликт имён dxf/DXF в отчёте и штатный + 30-минутный лимит теста, недостаточный для измеренного 40-минутного прогона. + Новый прогон требует подтверждения полного исправленного снимка. Этап ещё + не принят: остаются полный результат, упаковка и свежий/повторный запуск + точной распакованной копии. Доступный ZIP пока относится к предыдущему этапу. + [Текущее состояние](../artifacts/drawing-bom-development-checkpoint-20261014.json). + +- 15.10.2026: проверка будущего вращения выявила также дефект действующей + таблицы параметров. Нативное `copyObject` при предварительной проверке + теряет сохранённую топологическую ссылку эскиза на грань. На реальном + Revolution → Groove → эскиз на грани → Pocket копия принимает изменение + угла 180° → 90°, а исходная модель теряет опору. Отмена и пересчёт + восстанавливают геометрию и экземпляры сборки, но прежний Redo исчезает. + Выпуск удерживается до исправления предварительной проверки и добавления + этого сценария в регрессию. Проход существующих тестов недостаточен для + данного случая. + [Измеренный пример](../artifacts/revolve-native-probe/draft/downstream-comparison-runs/20261015-000602-3ecc43185eba4da5ac81dd26e6b52f8f/comparison-report.json). + Отдельно воспроизведён отказ допустимого изменения высоты через ту же + таблицу после создания спецификации: пробная копия исключает проекции, + но ошибочно оставляет требующий их выходной лист. Исходный проект и Redo + при таком отказе сохраняются. Исправление должно одновременно обеспечить + изменение детали, пересчёт массы и ячеек, Undo/Redo и повторное открытие, + сохранив проверку обычных входных таблиц и выражений. + [Сквозной сценарий](../artifacts/parameter-drawing-bom-integration/README.md). + Отдельный кандидат исправления прошёл 10 проверок геометрии, входных + таблиц, истории и повторного открытия, а также четыре проверки выражений + масштаба чертежа и пользовательских ячеек спецификации. Все процессы + завершились с кодом 0; исходники проекта не менялись. Перенос кандидата + в основной модуль и новая полная регрессия ещё обязательны. + [Независимая проверка кандидата](../artifacts/parameter-preflight-regression-draft/acceptance-v2-20261015.md). + Исправление перенесено в основной модуль. Первый прогон новой регрессии + обнаружил также штатную ошибку Undo при смене типа вычисленной ячейки + таблицы. Защита до применения проверена на обычных таблицах и спецификации: + шесть сценариев прошли, включая немедленный Undo и исполнение прежнего Redo. + Меняющие тип ячейки операции пока отклоняются; формулы не переписываются. + Итоговые проверки параметров прошли 33/33 на основном коде, включая + четыре сценария повторного открытия и изменения FCStd в новом процессе. + Ядро завершилось с кодом 0, снимок 60 исходных файлов сохранился; + ошибок восстановления Undo в журнале нет. Полный GUI-прогон 26 групп + запущен на этом снимке; упаковка и обычный запуск точного ZIP ещё впереди. + [Итоговая проверка параметров](../artifacts/parameter-preflight-integration-plan/production-runs/20261015-010323-c67ad7fd65534c41858f0107cec2df59/production-focused-report.json). + Полный прогон на этом снимке не имеет подтверждённого завершения: сохранены + 13 успешных групп без ошибок и отдельный результат новой регрессии 9/9, + но процесс и родительская сессия уже отсутствуют. Проходом полного набора + этот результат не считается. Дополнительный native-сценарий выявил потерю + Redo при эквивалентной записи формулы с префиксом собственного документа; + выпуск удерживается до исправления и повторной проверки итогового кода. + [Воспроизведение no-op](../artifacts/parameter-canonical-noop/runs/20261015-154741-1506ba00e5734e39ba8cf5ead46dcdb3/report.json). + Исправление канонических формул внесено: реальные отличия зависимостей, + комментариев, имён и переменных сохраняются как изменения, а эквивалентная + формула не открывает транзакцию. Обновлённый набор параметров прошёл 34/34, + native exit 0, снимок 60 исходников неизменен. + [Проверка исправленного кода](../artifacts/parameter-preflight-integration-plan/production-runs/20261015-155710-caa40c30f0a64deda709970c55d8c374/production-focused-report.json). + Независимый STEP-тест выявил потерю переноса и поворота корня размещённой + сборки и в штатном экспорте документа, и при явном выборе корня. Прежний + STEP-тест использовал корень без преобразования и этот случай не покрывал. + Изолированная нативная экспортная иерархия сохраняет мировую геометрию; + исправление перенесено в основной STEP-экспортёр. Восемь проверок product API + и чтение девяти STEP-файлов новым процессом прошли с native exit 0. + Подтверждены общие определения повторных компонентов, исходные имена, + мировые координаты и точная сохранность исходного проекта/Redo, включая + холодный запрос геометрии и ошибку уже начавшейся записи файла. + [Проверка STEP](../artifacts/step-world-export-candidate/README.md). + Исправлен также экспорт BREP/IGES/STL: мировая геометрия извлекается + по отдельным экземплярам, не вызывая меняющее исходные данные чтение + составной Shape контейнера. Писатель получает отдельный BRep-снимок, + а готовый файл заменяет прежний атомарно. Постоянная регрессия прошла + 45 сценариев с массивами, масштабом, зеркальностью, масками, явным + скрытым элементом и отказами записи. Пустая геометрия отклоняется + до изменения файла. Сохранена измеренная штатная семантика форматов: + STEP включает физические элементы массива, геометрические форматы + учитывают маску; явный выбор скрытого элемента сохраняет его геометрию. + Итоговый основной код прошёл 12 групп, включая полный headless kernel + 20/20, и чтение 15 STEP-файлов новым процессом, native exit 0. + Полный GUI-прогон 15.10.2026 завершён: 26/26 групп, native exit 0, + все 61 исходный файл неизменны, ошибок восстановления в журнале нет. + Свежие PDF трёх сценариев проверены после рендеринга Poppler: таблицы, + количества и массы читаемы, наложения и обрезки нет. Упаковка и проверка + обычного запуска точного portable-архива ещё обязательны. + [Проверка основного кода](../artifacts/step-cold-root-probe/runs/20261015-163917-c4c46544873547e0ae015636af68378c/fresh-report.json). + +- Следующий этап после приёмки спецификации на листе — единый процесс + вращения и вращательного выреза для валов, втулок и фланцев. Нативный + прототип Revolution/Groove дополнен собственным русским Qt-интерфейсом. + Свежий и повторный запуск прошли по три сценария с native exit 0: + видимый предпросмотр канавки, создание и редактирование одним Undo, + Cancel и эквивалентная формула без потери исполняемого Redo, отклонение + угла 361° до изменения проекта. Это пока отдельный проверенный кандидат; + самостоятельные модули продукта, полная регрессия и portable-проверка + ещё обязательны. Неожиданная ошибка уже начатого нативного изменения + остаётся отдельным ограничением точного восстановления истории. + [Прототип и ограничения](../artifacts/revolve-native-probe/README.md). diff --git a/docs/inventor-compatibility.md b/docs/inventor-compatibility.md new file mode 100644 index 0000000..d42d649 --- /dev/null +++ b/docs/inventor-compatibility.md @@ -0,0 +1,77 @@ +# Соответствие рабочему процессу Autodesk Inventor + +Требование пользователя — аналог Inventor с тем же процессом эскизов и +размеров. Реализация использует другое ядро; это ограничение нельзя устранить +переименованием команд. Воспроизводимое поведение отделяется от неподтверждённой +полной совместимости. + +| Сценарий | Реализация | +|---|---| +| Создать 2D-эскиз (Start 2D Sketch) → плоскость/грань | Собственная команда выбора и штатный редактор Sketcher | +| Линия, окружность, прямоугольник, дуга | Настоящие геометрические инструменты Sketcher | +| Универсальный размер (General Dimension) | Команда `Sketcher_Dimension`: выбор, размещение, ввод | +| Автоматические зависимости | Штатные автоматические ограничения (`Autoconstraints`) и предотвращение избыточных ограничений (`AvoidRedundant`) | +| Динамический ввод (Dynamic Input) | Параметры возле курсора: положение и размеры | +| Управляющие и справочные размеры, выражения | Штатные ограничения и механизм выражений | +| Таблица параметров (Parameters) | Общая таблица размеров и переменных документа; значения/формулы, длина/угол/число, переименование, комментарии, атомарное применение и FCStd | +| Завершить эскиз (Finish Sketch) | Явная кнопка с возвратом к модели | +| Проецировать геометрию (Project Geometry) | Связанная проекция рёбер и контуров грани; вспомогательный режим и разрыв связи | +| Смещение (Offset), выбор контура и равное расстояние | Связанные окружности, отдельные отрезки и выпуклые замкнутые линейные контуры; русские параметры и отменяемый предпросмотр | +| Зеркало эскиза (Mirror) | Связанные отрезки и окружности относительно оси эскиза или выбранной линии | +| Прямоугольный / круговой массив эскиза | Связанные отрезки и окружности; количество включает исходник, шаг/центр/угол и редактирование параметров | +| Выдавливание с предпросмотром | Панель параметров QInventor и твердотельные операции OCCT | +| Объединение / вырез / пересечение / новое тело (Join / Cut / Intersect / New Solid) | Соответствующие операции с геометрией | +| Прямое / обратное / симметричное направление (Forward / Reverse / Symmetric) | Выбор направления выдавливания | +| По длине / сквозь всё (Distance / Through All) | Выбор способа завершения | +| Двустороннее несимметричное выдавливание (Two-sided asymmetric) | Две независимые длины с выражениями | +| До грани (To) | До выбранной плоской грани, включая наклонную; проверка границ или продолжение грани | +| До следующей (To Next) | Ближайшая полностью перекрывающая профиль плоская грань | +| Панорама и вращение | Средняя кнопка мыши / Shift + средняя кнопка мыши | +| Сборки с соединениями | Штатный решатель среды сборок (Assembly) | +| Спецификация и масса вложенной сборки | Сводный и структурированный состав реальных экземпляров; количество по уровням, материалы конечных деталей, центр масс и CSV | +| Ассоциативный чертёж | Штатная среда чертежей (TechDraw) | +| Спецификация на ассоциативном листе | Чертёж → Оформление → Спецификация на листе; сводный/структурный состав фактических источников проекций, позиции, количество по уровням, материалы и итоговая масса без повторного учёта подсборок; нативная таблица TechDraw/Spreadsheet, FCStd и экспорт PDF/SVG/DXF | + +Спецификация использует выбранный лист либо единственный лист документа; +проекции должны иметь одну область источников. Изменения модели пересчитывают +таблицу, а смена области источников требует явного обновления. Предпросмотр +не размещает таблицу до применения; применённые изменения поддерживают Undo/Redo. + +Штатный A4 рассчитан максимум на 4 строки состава в сводном или 5 в структурном +режиме, кроме заголовка и итога; дополнительно проверяются текст и отсутствие +перекрытия проекций. Автоматическое продолжение на другие листы +и произвольная компоновка длинной спецификации пока не реализованы. PDF/SVG +передают векторный лист; DXF — геометрию проекций и редактируемые LINE/TEXT +таблицы. Это не заявляет поддержку формата IDW или полного оформления Inventor. + +## Что не подтверждено + +Нет побайтовой совместимости IPT/IAM/IDW, Autodesk ShapeManager и его решателя; +не проведено сравнение с установленной копией Inventor. Различаются имена и +часть свойств встроенных операций, обработка конфликтных ограничений, привязки, +выбор профилей и ряд сочетаний клавиш. Режим «До следующей» пока отвергает +неоднозначные пересекающиеся опоры и криволинейные конечные поверхности. +В смещении пока отсутствуют дуги, сплайны, вогнутые и открытые составные +контуры. В зеркалах и массивах эскиза пока поддерживаются отрезки и окружности; +нет общей длины вместо шага (Fitted), подавления экземпляров и произвольного +угла между направлениями прямоугольного массива. Выбор предваряет русский +диалог параметров; интерактивное размещение курсором отличается от Inventor. +Полный набор iLogic/iParts/iAssemblies, Content Center, +Frame Generator, специализированный листовой металл, маршрутные системы, +интеграция Vault и полная коммерческая симуляция не реализованы как эквиваленты +Inventor. Наличие дополнительных сред FreeCAD не доказывает их равенство Inventor. + +В графическом процессе FreeCAD воспроизведён аварийный выход при прямом +создании циклического графа App::Link. Диагностика такого повреждённого графа +через Python API проверяется отдельно от отображения его сцены. + +## Источники пользовательского поведения + +- [Autodesk: создание и редактирование эскиза](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-E96FB3C4-136B-4551-BF45-F54594543ECB.htm) +- [Autodesk: размеры](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-026FE8D3-AFBB-4834-BC18-73D9CFF78185.htm) +- [Autodesk: автоматические ограничения](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-A85A7A30-7D81-4C75-8769-CAD034EEA930.htm) +- [Autodesk: динамический ввод (Dynamic Input)](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-5DC9930A-7A1F-4260-83E1-323858856056.htm) +- [Autodesk: выдавливание (Extrude)](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-B0CFA8E7-D3F5-48A9-BE6F-C8DF668301BD.htm) +- [Autodesk: смещение геометрии](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-962CEE71-8BE4-4DA6-B51E-341319C64FA4.htm) +- [Autodesk: зеркала и массивы эскиза](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-AAD3A699-FED8-4614-A31D-0A2316328CEE.htm) +- [FreeCAD: универсальный размер](https://github.com/FreeCAD/FreeCAD-documentation/blob/main/wiki/Sketcher_Dimension.md) diff --git a/docs/workflows.md b/docs/workflows.md new file mode 100644 index 0000000..55b672b --- /dev/null +++ b/docs/workflows.md @@ -0,0 +1,233 @@ +# Рабочие процессы QInventor + +## Общая таблица параметров + +Кнопка **3D-модель → Параметры** открывает параметры всего активного документа; +предварительно выделять деталь не нужно. Завершите редактирование эскиза или +панели операции перед применением таблицы. + +Таблица показывает объект, имя параметра, единицы, формулу/значение, +вычисленный результат и комментарий. Она включает размеры эскизов, свойства +операций и нативные наборы переменных. Справочные и недоступные в текущем +режиме свойства доступны для чтения. Имена модельных свойств сохраняются для +стабильности выражений; пользовательские параметры можно переименовать. + +1. Нажмите **Добавить параметр**, задайте имя, тип и значение. Поддерживаются + длина, угол и безразмерное число; например `Width = 40 mm` и `Depth = 20 mm`. +2. В строке нужного размера введите выражение, например + `QIUserParameters.Width / 2` для радиуса и `QIUserParameters.Depth` для + длины выдавливания. Ссылку выбранной строки можно скопировать снизу окна. + Параметры одного набора могут ссылаться друг на друга: `Width / 2`. +3. Нажмите **Применить**. Новые переменные и связи можно задать одним + применением; модель перестраивается, изменения образуют один шаг отмены. +4. Исправьте поле в том же окне, если получена ошибка единиц, циклической + зависимости, решателя или геометрии. Отклонённый набор сохраняет модель + и историю Undo/Redo. Закрытие окна отбрасывает неприменённые правки. + +Переименование пользовательской переменной обновляет её штатные зависимости. +Удаление используемого параметра отклоняется, пока его потребители не удалены +или не перевязаны в том же наборе изменений. Комментарии сохраняются в FCStd +и участвуют в отмене. Для связей между документами предварительно сохраните +оба проекта; внешние потребители пересчитываются вместе с источником. + +Чертёж и его спецификация могут существовать до изменения параметров детали. +Обычные таблицы, управляющие геометрией, сохраняют свои связи; выходной лист +спецификации нельзя использовать как вход модели. Если изменение меняет тип +вычисленной ячейки таблицы, например с целого на дробное число, оно отклоняется +до применения: штатная отмена FreeCAD не восстанавливает такую ячейку корректно. +Формулы пользователя при этом сохраняются. + +После изменения модели вне таблицы нажмите **Обновить**. Эта кнопка загружает +актуальные значения и сбрасывает неприменённые правки окна. Если в таблице +остались правки, применение устаревших строк блокируется с пояснением; +так старый индекс ограничения не изменит другой размер после правки эскиза. +Поиск скрывает строки, а удаление действует только на выбранную видимую строку. + +## Размеры и эскизы + +Создание эскиза — явная операция с выбором плоскости или плоской грани. +Редактор содержит геометрию и список ограничений; зелёная геометрия указывает +на полностью определённый эскиз. Не фиксируйте всю геометрию вместо размеров, +если модель должна оставаться параметрической. + +В рабочем месте QInventor дерево закреплено слева, размеры и задачи — справа. +Панели не перекрывают полотно. Кнопки **Завершить эскиз** и **Отменить эскиз** +находятся в нижней строке ленты и доступны независимо от прокрутки инструментов. +Завершение и отмена возвращают штатную камеру без повторного автоматического +вписывания; команды вида остаются на вкладке **Вид**. + +Универсальный размер D выбирает длину, расстояние, угол или диаметр по +выбранной геометрии. Расположение курсора определяет горизонтальный, +вертикальный или наклонный размер; M переключает допустимые варианты. +Стрелка рядом с **Размер D** открывает явный выбор: горизонтальный, +вертикальный, длина, радиус, диаметр или угол. Справа в **Размеры эскиза** +показаны только размеры активного эскиза, их значения, формулы и тип связи. +Щелчок по строке подсвечивает размер; двойной щелчок или **Изменить размер** +открывает исходный редактор значения FreeCAD. Флажок **Скрытые** добавляет +скрытые размеры в список. +Управляющее ограничение изменяет эскиз; справочное только измеряет. В выражениях +доступна штатная система FreeCAD. Полностью определённые примеры имеют именованные +размеры «Длина», «Ширина», «Радиус» и связанные свойства заготовки. Имена свойств +`QILength` / `QIWidth` / `QIRadius` остаются стабильными для выражений. + +Закончите текущий инструмент Esc, затем нажмите **Завершить эскиз**. Эта +кнопка сохраняет построение и выбирает профиль для следующей операции. +**Отменить эскиз** восстанавливает геометрию и зависимости существующего +эскиза до начала редактирования; при отмене нового эскиза удаляется только +новое построение. Имена ограничений отображаются по-русски. Внутренние +идентификаторы параметров остаются стабильными для выражений. +При редактировании существующего эскиза последующие операции пересчитываются. +Для пустого или незамкнутого профиля выдавливание сообщает причину ошибки. + +**Проецировать геометрию** переносит выбранное ребро или все контуры грани +в открытый эскиз с сохранением связи с исходной деталью. Изменение исходной +геометрии обновляет проекцию. В обычном режиме замкнутый контур участвует +в профиле; режим **Вспомогательная** оставляет его для привязок и размеров. +**Разорвать связь** превращает выбранную проекцию в независимую геометрию, +сохраняя её текущую форму и связанные ограничения. Для проекции целой грани +связь разрывается сразу для всех её контуров. Действие поддерживает отмену. + +В блоке **Изменить** находятся **Смещение геометрии**, **Зеркальное отражение**, +**Прямоугольный массив**, **Круговой массив** и **Параметры повторений**. +Сначала выберите геометрию открытого эскиза; Ctrl добавляет элементы к выбору. +Диалог содержит русские поля, **Предпросмотр**, **Создать** и **Отмена**. +Повторный предпросмотр заменяет предыдущий вариант. Отмена возвращает +геометрию, зависимости, историю отмены и исходный признак сохранения. +Подтверждение создаёт один шаг отмены. + +Смещение поддерживает окружность, отдельный отрезок и выпуклый замкнутый +контур из отрезков. **Выбирать связанный замкнутый контур** расширяет выбор +по совпадающим концам линий; для отдельного отрезка отключите этот флажок. +Положительное расстояние расширяет окружность и контур, отрицательное +уменьшает. Для отдельного отрезка положительное расстояние означает левую +сторону в направлении от начала к концу. **Сохранить равное расстояние** +связывает результат с исходной геометрией; без флажка получается независимая +копия. Размер смещения остаётся настоящим управляющим ограничением эскиза. +Дуги, сплайны, вогнутые и открытые многосегментные цепочки пока не поддерживаются. + +Связанные зеркала и массивы поддерживают отрезки и окружности. Ось зеркала — +горизонтальная/вертикальная ось эскиза или его отрезок, в том числе +вспомогательный. Если линия зеркала была включена в выбор, она служит осью +и не копируется. Количество массива включает исходную геометрию. +Прямоугольный массив задаётся количеством и шагом в двух перпендикулярных +направлениях; угол поворачивает первое направление. Круговой массив задаётся +центром, количеством и общим углом. При 360° экземпляры равномерно распределены +по окружности без повторения исходника; при меньшем угле они включают оба +конца сектора. Отрицательные шаг или угол меняют сторону повторения. +Изменение размеров исходной геометрии обновляет копии, в том числе после +сохранения и повторного открытия FCStd. + +**Параметры повторений** позволяет изменить уже созданную операцию. +Шаг, угол и центр остаются редактируемыми через её управляющие размеры. +Изменение количества или оси зеркала требует перестроения: оно разрешено, +пока после операции не добавлена другая геометрия и её экземпляры не используются +сторонними зависимостями. При нарушении этих условий приложение сообщает +причину и сохраняет модель. Режим общей длины массива, подавление отдельных +экземпляров и неортогональные направления пока не реализованы. +Если управляющий размер связан формулой, числовой диалог не заменяет эту +формулу и не перестраивает количество с потерей связи. Изменяйте выражение +в редакторе ограничений эскиза; несвязанные поля операции остаются доступными. + +## Деталь + +Операции применяются к активному телу детали (Body). Для выреза профиль должен пересекать +тело. Объединение должно давать одно связное твёрдое тело; для независимой +геометрии выбирайте **Новое тело**. Вращение, отверстия, скругления и фаски +открывают собственные панели параметров CAD-движка. Свойства операции и её +профиль доступны в дереве модели. Сохранение в FCStd сохраняет историю. + +В **Выдавить** можно задать прямое, обратное, симметричное или двустороннее +направление. В двустороннем режиме длины независимы. Режимы завершения: +**По длине**, **Сквозь всё**, **До грани**, **До следующей**. Для **До грани** +выберите плоскую конечную грань; наклонная грань обрезает объём по своей +плоскости. **Продолжить грань** разрешает выход профиля за её края. Без этого +флажка вся геометрия должна укладываться в выбранную грань, включая отверстия. +**До следующей** ищет ближайшую плоскую грань, полностью перекрывающую профиль; +криволинейные и неоднозначные опоры вызывают понятную ошибку. Изменение опоры +перестраивает операцию. Двойной щелчок по выдавливанию открывает его параметры. + +## Сборка + +Новая сборка создаёт объект встроенной среды сборок (Assembly) и группу соединений. +Добавьте компоненты из открытых документов, закрепите базовый компонент, +затем выберите компоненты/элементы соединения и нужный тип связи. +**Решить** пересчитывает сборку. Дополнительные типы соединений и симуляция +доступны в штатной среде сборок (Assembly). Пример сборки содержит закреплённую плиту, +неподвижный и вращающийся штифты, связанные экземпляры деталей и спецификацию. + +### Спецификация и физические свойства + +**Сборка → Спецификация** открывает состав документа без предварительного +выделения. Выберите весь документ или нужную сборку. **Сводная** показывает +конечные детали и их общее количество; **Структурная** сохраняет уровни +подсборок. Количество внутри родителя и количество во всей выбранной области +отображаются отдельно. Итоговая масса не складывает повторно массу родителя +и его дочерних строк. + +Повторные экземпляры учитываются по реальным связям CAD. Одинаковые подписи +или обозначения разных исходников не объединяют их в одну деталь. Материал +и плотность берутся у конечных деталей; сборка из нескольких материалов +показывает суммарную массу и среднюю плотность. Скрытие компонента само по +себе не убирает его из состава. +Материал назначается конечной детали или её экземпляру. Для изменения +материалов сборки выбирайте входящие в неё детали. + +После изменения геометрии или материалов нажмите **Обновить**. **Экспорт CSV** +пересчитывает выбранную область и сохраняет текущее представление. Ошибка +источника отклоняет экспорт до замены прежнего файла. **Масса и объём** доступна +для выбранной детали или сборки; для единственной сборки выделение не нужно. +При создании чертежа сборки из разных материалов поле материала основной +надписи указывает **См. спецификацию**. + +## Чертёж + +Создайте чертёж из существующей детали или сборки. QInventor добавляет +ассоциативные виды спереди, сверху и в изометрии на собственный русский A4. +Изменение модели обновляет проекции. Откройте страницу двойным щелчком, +выберите нужные рёбра вида и инструмент **Размер**. Полная среда чертежей (TechDraw) +содержит разрезы, выносные виды, аннотации и оформление. PDF/SVG/DXF сохраняют +лист; исходный проект FCStd сохраняет редактируемую страницу. + +### Спецификация на листе + +Откройте **Чертёж → Оформление → Спецификация на листе**. Если листов несколько, +выберите один в дереве; для единственного листа выделение не требуется. +Если листа ещё нет, сначала создайте чертёж. Завершите активное редактирование +детали или сборки перед работой с таблицей. + +В окне показаны фактические источники проекций выбранного листа. Все проекции +должны показывать одну область состава. Спецификация относится к этой области; +посторонние детали документа в неё не добавляются. **Сводная — только детали** +показывает общее количество конечных деталей. **Структурная — состав сборок** +сохраняет позиции подсборок, количество внутри родителя и общее количество. +Итоговая масса учитывает конечные экземпляры один раз. + +Проверьте строки и нажмите **Разместить на листе**. Для существующей таблицы +кнопка называется **Обновить на листе**. **Обновить предпросмотр** перечитывает +источники и строки перед применением. **Закрыть** или Esc отменяют неприменённые +изменения; уже применённое изменение можно отменить Ctrl+Z и повторить Ctrl+Y. + +После перестроения модели таблица на листе пересчитывает состав и массу. +Для просмотра актуальных строк в открытом окне нажмите **Обновить предпросмотр**. +Если источники проекций изменены, проверьте новый состав и явно обновите таблицу. +Проект FCStd сохраняет таблицу и её связи. **Экспорт чертежа** сохраняет весь лист +в PDF/SVG; DXF содержит проекции, сетку таблицы и редактируемые текстовые строки. +Ошибка модели или таблицы отклоняет экспорт и сохраняет прежний файл. + +На штатном A4 помещаются до **4 строк состава в сводном** или **5 в структурном** +режиме. Заголовок и строка «Итого» в это число не входят. Длинный текст также +должен помещаться в столбцах, а проекции не должны перекрывать таблицу. +При переполнении сократите область состава или +текст исходных обозначений/наименований; приложение сохраняет прежнюю таблицу. + +## Обмен и восстановление + +STEP / IGES передают точную геометрию, но не историю эскизов другого CAD. +STL — треугольная сетка. Пользовательские настройки отдельны от установленных +на компьютере сред FreeCAD. Резервные FCStd1/FCBak управляются CAD-движком. +Экспорт учитывает мировое размещение выбранной детали или сборки, включая +преобразования родительских контейнеров. STEP сохраняет физические элементы +массива; BREP, IGES и STL учитывают его маску элементов. Явно выбранный скрытый +элемент экспортируется отдельно. Если маска не оставляет геометрии, экспорт +геометрического формата отклоняется. Ошибка записи сохраняет прежний файл. +Если приложение не запускается, сохраните `logs/launcher.log` и `logs/cad.log`. diff --git a/examples/assembly.FCStd b/examples/assembly.FCStd new file mode 100644 index 0000000..a549f5d Binary files /dev/null and b/examples/assembly.FCStd differ diff --git a/examples/assembly.step b/examples/assembly.step new file mode 100644 index 0000000..80fee7a --- /dev/null +++ b/examples/assembly.step @@ -0,0 +1,496 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('FreeCAD Model'),'2;1'); +FILE_NAME('Open CASCADE Shape Model','2026-10-15T16:56:15',('Author'),( + ''),'Open CASCADE STEP processor 7.8','FreeCAD','Unknown'); +FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); +ENDSEC; +DATA; +#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', + 'automotive_design',2000,#2); +#2 = APPLICATION_CONTEXT( + 'core data for automotive mechanical design processes'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10); +#4 = PRODUCT_DEFINITION_SHAPE('','',#5); +#5 = PRODUCT_DEFINITION('design','',#6,#9); +#6 = PRODUCT_DEFINITION_FORMATION('','',#7); +#7 = PRODUCT('Экспорт модели','Экспорт модели' + ,'',(#8)); +#8 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#10 = SHAPE_REPRESENTATION('',(#11,#15),#19); +#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14); +#12 = CARTESIAN_POINT('',(0.,0.,0.)); +#13 = DIRECTION('',(0.,0.,1.)); +#14 = DIRECTION('',(1.,0.,-0.)); +#15 = AXIS2_PLACEMENT_3D('',#16,#17,#18); +#16 = CARTESIAN_POINT('',(0.,0.,0.)); +#17 = DIRECTION('',(0.,0.,1.)); +#18 = DIRECTION('',(1.,0.,0.)); +#19 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#23)) GLOBAL_UNIT_ASSIGNED_CONTEXT( +(#20,#21,#22)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#20 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#21 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#22 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#23 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#20, + 'distance_accuracy_value','confusion accuracy'); +#24 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); +#25 = SHAPE_DEFINITION_REPRESENTATION(#26,#32); +#26 = PRODUCT_DEFINITION_SHAPE('','',#27); +#27 = PRODUCT_DEFINITION('design','',#28,#31); +#28 = PRODUCT_DEFINITION_FORMATION('','',#29); +#29 = PRODUCT('Опора в сборе','Опора в сборе','',( + #30)); +#30 = PRODUCT_CONTEXT('',#2,'mechanical'); +#31 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#32 = SHAPE_REPRESENTATION('',(#11,#33,#37,#41),#45); +#33 = AXIS2_PLACEMENT_3D('',#34,#35,#36); +#34 = CARTESIAN_POINT('',(0.,0.,0.)); +#35 = DIRECTION('',(0.,0.,1.)); +#36 = DIRECTION('',(1.,0.,0.)); +#37 = AXIS2_PLACEMENT_3D('',#38,#39,#40); +#38 = CARTESIAN_POINT('',(20.,30.,0.)); +#39 = DIRECTION('',(0.,0.,1.)); +#40 = DIRECTION('',(1.,0.,0.)); +#41 = AXIS2_PLACEMENT_3D('',#42,#43,#44); +#42 = CARTESIAN_POINT('',(100.,30.,0.)); +#43 = DIRECTION('',(0.,0.,1.)); +#44 = DIRECTION('',(1.,0.,0.)); +#45 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#49)) GLOBAL_UNIT_ASSIGNED_CONTEXT( +(#46,#47,#48)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#46 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#47 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#48 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#49 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#46, + 'distance_accuracy_value','confusion accuracy'); +#50 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#51,#53); +#51 = ( REPRESENTATION_RELATIONSHIP('','',#32,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#52) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#52 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15); +#53 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#54); +#54 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('45','Опора в сборе','', + #5,#27,$); +#55 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#29)); +#56 = SHAPE_DEFINITION_REPRESENTATION(#57,#63); +#57 = PRODUCT_DEFINITION_SHAPE('','',#58); +#58 = PRODUCT_DEFINITION('design','',#59,#62); +#59 = PRODUCT_DEFINITION_FORMATION('','',#60); +#60 = PRODUCT('Плита с отверстиями', + 'Плита с отверстиями','',(#61)); +#61 = PRODUCT_CONTEXT('',#2,'mechanical'); +#62 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#63 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#64),#292); +#64 = MANIFOLD_SOLID_BREP('',#65); +#65 = CLOSED_SHELL('',(#66,#106,#137,#168,#207,#246,#258,#275)); +#66 = ADVANCED_FACE('',(#67),#101,.F.); +#67 = FACE_BOUND('',#68,.F.); +#68 = EDGE_LOOP('',(#69,#79,#87,#95)); +#69 = ORIENTED_EDGE('',*,*,#70,.T.); +#70 = EDGE_CURVE('',#71,#73,#75,.T.); +#71 = VERTEX_POINT('',#72); +#72 = CARTESIAN_POINT('',(0.,0.,0.)); +#73 = VERTEX_POINT('',#74); +#74 = CARTESIAN_POINT('',(0.,0.,8.)); +#75 = LINE('',#76,#77); +#76 = CARTESIAN_POINT('',(0.,0.,0.)); +#77 = VECTOR('',#78,1.); +#78 = DIRECTION('',(0.,0.,1.)); +#79 = ORIENTED_EDGE('',*,*,#80,.T.); +#80 = EDGE_CURVE('',#73,#81,#83,.T.); +#81 = VERTEX_POINT('',#82); +#82 = CARTESIAN_POINT('',(120.,0.,8.)); +#83 = LINE('',#84,#85); +#84 = CARTESIAN_POINT('',(0.,0.,8.)); +#85 = VECTOR('',#86,1.); +#86 = DIRECTION('',(1.,0.,0.)); +#87 = ORIENTED_EDGE('',*,*,#88,.F.); +#88 = EDGE_CURVE('',#89,#81,#91,.T.); +#89 = VERTEX_POINT('',#90); +#90 = CARTESIAN_POINT('',(120.,0.,0.)); +#91 = LINE('',#92,#93); +#92 = CARTESIAN_POINT('',(120.,0.,0.)); +#93 = VECTOR('',#94,1.); +#94 = DIRECTION('',(0.,0.,1.)); +#95 = ORIENTED_EDGE('',*,*,#96,.F.); +#96 = EDGE_CURVE('',#71,#89,#97,.T.); +#97 = LINE('',#98,#99); +#98 = CARTESIAN_POINT('',(0.,0.,0.)); +#99 = VECTOR('',#100,1.); +#100 = DIRECTION('',(1.,0.,0.)); +#101 = PLANE('',#102); +#102 = AXIS2_PLACEMENT_3D('',#103,#104,#105); +#103 = CARTESIAN_POINT('',(0.,0.,0.)); +#104 = DIRECTION('',(0.,1.,0.)); +#105 = DIRECTION('',(1.,0.,0.)); +#106 = ADVANCED_FACE('',(#107),#132,.F.); +#107 = FACE_BOUND('',#108,.F.); +#108 = EDGE_LOOP('',(#109,#119,#125,#126)); +#109 = ORIENTED_EDGE('',*,*,#110,.T.); +#110 = EDGE_CURVE('',#111,#113,#115,.T.); +#111 = VERTEX_POINT('',#112); +#112 = CARTESIAN_POINT('',(0.,60.,0.)); +#113 = VERTEX_POINT('',#114); +#114 = CARTESIAN_POINT('',(0.,60.,8.)); +#115 = LINE('',#116,#117); +#116 = CARTESIAN_POINT('',(0.,60.,0.)); +#117 = VECTOR('',#118,1.); +#118 = DIRECTION('',(0.,0.,1.)); +#119 = ORIENTED_EDGE('',*,*,#120,.T.); +#120 = EDGE_CURVE('',#113,#73,#121,.T.); +#121 = LINE('',#122,#123); +#122 = CARTESIAN_POINT('',(0.,60.,8.)); +#123 = VECTOR('',#124,1.); +#124 = DIRECTION('',(0.,-1.,0.)); +#125 = ORIENTED_EDGE('',*,*,#70,.F.); +#126 = ORIENTED_EDGE('',*,*,#127,.F.); +#127 = EDGE_CURVE('',#111,#71,#128,.T.); +#128 = LINE('',#129,#130); +#129 = CARTESIAN_POINT('',(0.,60.,0.)); +#130 = VECTOR('',#131,1.); +#131 = DIRECTION('',(0.,-1.,0.)); +#132 = PLANE('',#133); +#133 = AXIS2_PLACEMENT_3D('',#134,#135,#136); +#134 = CARTESIAN_POINT('',(0.,60.,0.)); +#135 = DIRECTION('',(1.,0.,0.)); +#136 = DIRECTION('',(0.,-1.,0.)); +#137 = ADVANCED_FACE('',(#138),#163,.F.); +#138 = FACE_BOUND('',#139,.F.); +#139 = EDGE_LOOP('',(#140,#141,#149,#157)); +#140 = ORIENTED_EDGE('',*,*,#88,.T.); +#141 = ORIENTED_EDGE('',*,*,#142,.T.); +#142 = EDGE_CURVE('',#81,#143,#145,.T.); +#143 = VERTEX_POINT('',#144); +#144 = CARTESIAN_POINT('',(120.,60.,8.)); +#145 = LINE('',#146,#147); +#146 = CARTESIAN_POINT('',(120.,0.,8.)); +#147 = VECTOR('',#148,1.); +#148 = DIRECTION('',(0.,1.,0.)); +#149 = ORIENTED_EDGE('',*,*,#150,.F.); +#150 = EDGE_CURVE('',#151,#143,#153,.T.); +#151 = VERTEX_POINT('',#152); +#152 = CARTESIAN_POINT('',(120.,60.,0.)); +#153 = LINE('',#154,#155); +#154 = CARTESIAN_POINT('',(120.,60.,0.)); +#155 = VECTOR('',#156,1.); +#156 = DIRECTION('',(0.,0.,1.)); +#157 = ORIENTED_EDGE('',*,*,#158,.F.); +#158 = EDGE_CURVE('',#89,#151,#159,.T.); +#159 = LINE('',#160,#161); +#160 = CARTESIAN_POINT('',(120.,0.,0.)); +#161 = VECTOR('',#162,1.); +#162 = DIRECTION('',(0.,1.,0.)); +#163 = PLANE('',#164); +#164 = AXIS2_PLACEMENT_3D('',#165,#166,#167); +#165 = CARTESIAN_POINT('',(120.,0.,0.)); +#166 = DIRECTION('',(-1.,0.,0.)); +#167 = DIRECTION('',(0.,1.,0.)); +#168 = ADVANCED_FACE('',(#169,#180,#191),#202,.F.); +#169 = FACE_BOUND('',#170,.F.); +#170 = EDGE_LOOP('',(#171,#172,#173,#179)); +#171 = ORIENTED_EDGE('',*,*,#96,.T.); +#172 = ORIENTED_EDGE('',*,*,#158,.T.); +#173 = ORIENTED_EDGE('',*,*,#174,.T.); +#174 = EDGE_CURVE('',#151,#111,#175,.T.); +#175 = LINE('',#176,#177); +#176 = CARTESIAN_POINT('',(120.,60.,0.)); +#177 = VECTOR('',#178,1.); +#178 = DIRECTION('',(-1.,0.,0.)); +#179 = ORIENTED_EDGE('',*,*,#127,.T.); +#180 = FACE_BOUND('',#181,.F.); +#181 = EDGE_LOOP('',(#182)); +#182 = ORIENTED_EDGE('',*,*,#183,.F.); +#183 = EDGE_CURVE('',#184,#184,#186,.T.); +#184 = VERTEX_POINT('',#185); +#185 = CARTESIAN_POINT('',(26.,30.,0.)); +#186 = CIRCLE('',#187,6.); +#187 = AXIS2_PLACEMENT_3D('',#188,#189,#190); +#188 = CARTESIAN_POINT('',(20.,30.,0.)); +#189 = DIRECTION('',(0.,0.,1.)); +#190 = DIRECTION('',(1.,0.,0.)); +#191 = FACE_BOUND('',#192,.F.); +#192 = EDGE_LOOP('',(#193)); +#193 = ORIENTED_EDGE('',*,*,#194,.F.); +#194 = EDGE_CURVE('',#195,#195,#197,.T.); +#195 = VERTEX_POINT('',#196); +#196 = CARTESIAN_POINT('',(106.,30.,0.)); +#197 = CIRCLE('',#198,6.); +#198 = AXIS2_PLACEMENT_3D('',#199,#200,#201); +#199 = CARTESIAN_POINT('',(100.,30.,0.)); +#200 = DIRECTION('',(0.,0.,1.)); +#201 = DIRECTION('',(1.,0.,0.)); +#202 = PLANE('',#203); +#203 = AXIS2_PLACEMENT_3D('',#204,#205,#206); +#204 = CARTESIAN_POINT('',(60.,30.,0.)); +#205 = DIRECTION('',(0.,0.,1.)); +#206 = DIRECTION('',(1.,0.,0.)); +#207 = ADVANCED_FACE('',(#208,#219,#230),#241,.T.); +#208 = FACE_BOUND('',#209,.T.); +#209 = EDGE_LOOP('',(#210,#211,#212,#218)); +#210 = ORIENTED_EDGE('',*,*,#80,.T.); +#211 = ORIENTED_EDGE('',*,*,#142,.T.); +#212 = ORIENTED_EDGE('',*,*,#213,.T.); +#213 = EDGE_CURVE('',#143,#113,#214,.T.); +#214 = LINE('',#215,#216); +#215 = CARTESIAN_POINT('',(120.,60.,8.)); +#216 = VECTOR('',#217,1.); +#217 = DIRECTION('',(-1.,0.,0.)); +#218 = ORIENTED_EDGE('',*,*,#120,.T.); +#219 = FACE_BOUND('',#220,.T.); +#220 = EDGE_LOOP('',(#221)); +#221 = ORIENTED_EDGE('',*,*,#222,.F.); +#222 = EDGE_CURVE('',#223,#223,#225,.T.); +#223 = VERTEX_POINT('',#224); +#224 = CARTESIAN_POINT('',(26.,30.,8.)); +#225 = CIRCLE('',#226,6.); +#226 = AXIS2_PLACEMENT_3D('',#227,#228,#229); +#227 = CARTESIAN_POINT('',(20.,30.,8.)); +#228 = DIRECTION('',(0.,0.,1.)); +#229 = DIRECTION('',(1.,0.,0.)); +#230 = FACE_BOUND('',#231,.T.); +#231 = EDGE_LOOP('',(#232)); +#232 = ORIENTED_EDGE('',*,*,#233,.F.); +#233 = EDGE_CURVE('',#234,#234,#236,.T.); +#234 = VERTEX_POINT('',#235); +#235 = CARTESIAN_POINT('',(106.,30.,8.)); +#236 = CIRCLE('',#237,6.); +#237 = AXIS2_PLACEMENT_3D('',#238,#239,#240); +#238 = CARTESIAN_POINT('',(100.,30.,8.)); +#239 = DIRECTION('',(0.,0.,1.)); +#240 = DIRECTION('',(1.,0.,0.)); +#241 = PLANE('',#242); +#242 = AXIS2_PLACEMENT_3D('',#243,#244,#245); +#243 = CARTESIAN_POINT('',(60.,30.,8.)); +#244 = DIRECTION('',(0.,0.,1.)); +#245 = DIRECTION('',(1.,0.,0.)); +#246 = ADVANCED_FACE('',(#247),#253,.F.); +#247 = FACE_BOUND('',#248,.F.); +#248 = EDGE_LOOP('',(#249,#250,#251,#252)); +#249 = ORIENTED_EDGE('',*,*,#150,.T.); +#250 = ORIENTED_EDGE('',*,*,#213,.T.); +#251 = ORIENTED_EDGE('',*,*,#110,.F.); +#252 = ORIENTED_EDGE('',*,*,#174,.F.); +#253 = PLANE('',#254); +#254 = AXIS2_PLACEMENT_3D('',#255,#256,#257); +#255 = CARTESIAN_POINT('',(120.,60.,0.)); +#256 = DIRECTION('',(0.,-1.,0.)); +#257 = DIRECTION('',(-1.,0.,0.)); +#258 = ADVANCED_FACE('',(#259),#270,.F.); +#259 = FACE_BOUND('',#260,.F.); +#260 = EDGE_LOOP('',(#261,#267,#268,#269)); +#261 = ORIENTED_EDGE('',*,*,#262,.T.); +#262 = EDGE_CURVE('',#223,#184,#263,.T.); +#263 = LINE('',#264,#265); +#264 = CARTESIAN_POINT('',(26.,30.,8.)); +#265 = VECTOR('',#266,1.); +#266 = DIRECTION('',(-0.,-0.,-1.)); +#267 = ORIENTED_EDGE('',*,*,#183,.T.); +#268 = ORIENTED_EDGE('',*,*,#262,.F.); +#269 = ORIENTED_EDGE('',*,*,#222,.F.); +#270 = CYLINDRICAL_SURFACE('',#271,6.); +#271 = AXIS2_PLACEMENT_3D('',#272,#273,#274); +#272 = CARTESIAN_POINT('',(20.,30.,8.)); +#273 = DIRECTION('',(0.,0.,1.)); +#274 = DIRECTION('',(1.,0.,-0.)); +#275 = ADVANCED_FACE('',(#276),#287,.F.); +#276 = FACE_BOUND('',#277,.F.); +#277 = EDGE_LOOP('',(#278,#284,#285,#286)); +#278 = ORIENTED_EDGE('',*,*,#279,.T.); +#279 = EDGE_CURVE('',#234,#195,#280,.T.); +#280 = LINE('',#281,#282); +#281 = CARTESIAN_POINT('',(106.,30.,8.)); +#282 = VECTOR('',#283,1.); +#283 = DIRECTION('',(-0.,-0.,-1.)); +#284 = ORIENTED_EDGE('',*,*,#194,.T.); +#285 = ORIENTED_EDGE('',*,*,#279,.F.); +#286 = ORIENTED_EDGE('',*,*,#233,.F.); +#287 = CYLINDRICAL_SURFACE('',#288,6.); +#288 = AXIS2_PLACEMENT_3D('',#289,#290,#291); +#289 = CARTESIAN_POINT('',(100.,30.,8.)); +#290 = DIRECTION('',(0.,0.,1.)); +#291 = DIRECTION('',(1.,0.,-0.)); +#292 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#296)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#293,#294,#295)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#293 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#294 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#295 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#296 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#293, + 'distance_accuracy_value','confusion accuracy'); +#297 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#298,#300); +#298 = ( REPRESENTATION_RELATIONSHIP('','',#63,#32) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#299) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#299 = ITEM_DEFINED_TRANSFORMATION('','',#11,#33); +#300 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#301 + ); +#301 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('42','Основание','',#27, + #58,$); +#302 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#60)); +#303 = SHAPE_DEFINITION_REPRESENTATION(#304,#310); +#304 = PRODUCT_DEFINITION_SHAPE('','',#305); +#305 = PRODUCT_DEFINITION('design','',#306,#309); +#306 = PRODUCT_DEFINITION_FORMATION('','',#307); +#307 = PRODUCT('Штифт','Штифт','',(#308)); +#308 = PRODUCT_CONTEXT('',#2,'mechanical'); +#309 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#310 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#311),#364); +#311 = MANIFOLD_SOLID_BREP('',#312); +#312 = CLOSED_SHELL('',(#313,#346,#355)); +#313 = ADVANCED_FACE('',(#314),#341,.T.); +#314 = FACE_BOUND('',#315,.F.); +#315 = EDGE_LOOP('',(#316,#326,#333,#334)); +#316 = ORIENTED_EDGE('',*,*,#317,.T.); +#317 = EDGE_CURVE('',#318,#320,#322,.T.); +#318 = VERTEX_POINT('',#319); +#319 = CARTESIAN_POINT('',(5.8,0.,0.)); +#320 = VERTEX_POINT('',#321); +#321 = CARTESIAN_POINT('',(5.8,0.,35.)); +#322 = LINE('',#323,#324); +#323 = CARTESIAN_POINT('',(5.8,0.,0.)); +#324 = VECTOR('',#325,1.); +#325 = DIRECTION('',(0.,0.,1.)); +#326 = ORIENTED_EDGE('',*,*,#327,.T.); +#327 = EDGE_CURVE('',#320,#320,#328,.T.); +#328 = CIRCLE('',#329,5.8); +#329 = AXIS2_PLACEMENT_3D('',#330,#331,#332); +#330 = CARTESIAN_POINT('',(0.,0.,35.)); +#331 = DIRECTION('',(0.,0.,1.)); +#332 = DIRECTION('',(1.,0.,0.)); +#333 = ORIENTED_EDGE('',*,*,#317,.F.); +#334 = ORIENTED_EDGE('',*,*,#335,.F.); +#335 = EDGE_CURVE('',#318,#318,#336,.T.); +#336 = CIRCLE('',#337,5.8); +#337 = AXIS2_PLACEMENT_3D('',#338,#339,#340); +#338 = CARTESIAN_POINT('',(0.,0.,0.)); +#339 = DIRECTION('',(0.,0.,1.)); +#340 = DIRECTION('',(1.,0.,0.)); +#341 = CYLINDRICAL_SURFACE('',#342,5.8); +#342 = AXIS2_PLACEMENT_3D('',#343,#344,#345); +#343 = CARTESIAN_POINT('',(0.,0.,0.)); +#344 = DIRECTION('',(-0.,-0.,-1.)); +#345 = DIRECTION('',(1.,0.,0.)); +#346 = ADVANCED_FACE('',(#347),#350,.F.); +#347 = FACE_BOUND('',#348,.F.); +#348 = EDGE_LOOP('',(#349)); +#349 = ORIENTED_EDGE('',*,*,#335,.T.); +#350 = PLANE('',#351); +#351 = AXIS2_PLACEMENT_3D('',#352,#353,#354); +#352 = CARTESIAN_POINT('',(-2.35765540093E-16,-2.367650478352E-16,0.)); +#353 = DIRECTION('',(0.,0.,1.)); +#354 = DIRECTION('',(1.,0.,0.)); +#355 = ADVANCED_FACE('',(#356),#359,.T.); +#356 = FACE_BOUND('',#357,.T.); +#357 = EDGE_LOOP('',(#358)); +#358 = ORIENTED_EDGE('',*,*,#327,.T.); +#359 = PLANE('',#360); +#360 = AXIS2_PLACEMENT_3D('',#361,#362,#363); +#361 = CARTESIAN_POINT('',(-2.35765540093E-16,-2.367650478352E-16,35.)); +#362 = DIRECTION('',(0.,0.,1.)); +#363 = DIRECTION('',(1.,0.,0.)); +#364 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#368)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#365,#366,#367)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#365 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#366 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#367 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#368 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#365, + 'distance_accuracy_value','confusion accuracy'); +#369 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#370,#372); +#370 = ( REPRESENTATION_RELATIONSHIP('','',#310,#32) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#371) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#371 = ITEM_DEFINED_TRANSFORMATION('','',#11,#37); +#372 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#373 + ); +#373 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('43','Штифт001','',#27,#305,$ + ); +#374 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#307)); +#375 = SHAPE_DEFINITION_REPRESENTATION(#376,#382); +#376 = PRODUCT_DEFINITION_SHAPE('','',#377); +#377 = PRODUCT_DEFINITION('design','',#378,#381); +#378 = PRODUCT_DEFINITION_FORMATION('','',#379); +#379 = PRODUCT('Штифт','Штифт','',(#380)); +#380 = PRODUCT_CONTEXT('',#2,'mechanical'); +#381 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#382 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#383),#436); +#383 = MANIFOLD_SOLID_BREP('',#384); +#384 = CLOSED_SHELL('',(#385,#418,#427)); +#385 = ADVANCED_FACE('',(#386),#413,.T.); +#386 = FACE_BOUND('',#387,.F.); +#387 = EDGE_LOOP('',(#388,#398,#405,#406)); +#388 = ORIENTED_EDGE('',*,*,#389,.T.); +#389 = EDGE_CURVE('',#390,#392,#394,.T.); +#390 = VERTEX_POINT('',#391); +#391 = CARTESIAN_POINT('',(5.8,0.,0.)); +#392 = VERTEX_POINT('',#393); +#393 = CARTESIAN_POINT('',(5.8,0.,35.)); +#394 = LINE('',#395,#396); +#395 = CARTESIAN_POINT('',(5.8,0.,0.)); +#396 = VECTOR('',#397,1.); +#397 = DIRECTION('',(0.,0.,1.)); +#398 = ORIENTED_EDGE('',*,*,#399,.T.); +#399 = EDGE_CURVE('',#392,#392,#400,.T.); +#400 = CIRCLE('',#401,5.8); +#401 = AXIS2_PLACEMENT_3D('',#402,#403,#404); +#402 = CARTESIAN_POINT('',(0.,0.,35.)); +#403 = DIRECTION('',(0.,0.,1.)); +#404 = DIRECTION('',(1.,0.,0.)); +#405 = ORIENTED_EDGE('',*,*,#389,.F.); +#406 = ORIENTED_EDGE('',*,*,#407,.F.); +#407 = EDGE_CURVE('',#390,#390,#408,.T.); +#408 = CIRCLE('',#409,5.8); +#409 = AXIS2_PLACEMENT_3D('',#410,#411,#412); +#410 = CARTESIAN_POINT('',(0.,0.,0.)); +#411 = DIRECTION('',(0.,0.,1.)); +#412 = DIRECTION('',(1.,0.,0.)); +#413 = CYLINDRICAL_SURFACE('',#414,5.8); +#414 = AXIS2_PLACEMENT_3D('',#415,#416,#417); +#415 = CARTESIAN_POINT('',(0.,0.,0.)); +#416 = DIRECTION('',(-0.,-0.,-1.)); +#417 = DIRECTION('',(1.,0.,0.)); +#418 = ADVANCED_FACE('',(#419),#422,.F.); +#419 = FACE_BOUND('',#420,.F.); +#420 = EDGE_LOOP('',(#421)); +#421 = ORIENTED_EDGE('',*,*,#407,.T.); +#422 = PLANE('',#423); +#423 = AXIS2_PLACEMENT_3D('',#424,#425,#426); +#424 = CARTESIAN_POINT('',(-2.35765540093E-16,-2.367650478352E-16,0.)); +#425 = DIRECTION('',(0.,0.,1.)); +#426 = DIRECTION('',(1.,0.,0.)); +#427 = ADVANCED_FACE('',(#428),#431,.T.); +#428 = FACE_BOUND('',#429,.T.); +#429 = EDGE_LOOP('',(#430)); +#430 = ORIENTED_EDGE('',*,*,#399,.T.); +#431 = PLANE('',#432); +#432 = AXIS2_PLACEMENT_3D('',#433,#434,#435); +#433 = CARTESIAN_POINT('',(-2.35765540093E-16,-2.367650478352E-16,35.)); +#434 = DIRECTION('',(0.,0.,1.)); +#435 = DIRECTION('',(1.,0.,0.)); +#436 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#440)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#437,#438,#439)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#437 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#438 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#439 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#440 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#437, + 'distance_accuracy_value','confusion accuracy'); +#441 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#442,#444); +#442 = ( REPRESENTATION_RELATIONSHIP('','',#382,#32) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#443) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#443 = ITEM_DEFINED_TRANSFORMATION('','',#11,#41); +#444 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#445 + ); +#445 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('44','Штифт002','',#27,#377,$ + ); +#446 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#379)); +ENDSEC; +END-ISO-10303-21; diff --git a/examples/bracket.FCStd b/examples/bracket.FCStd new file mode 100644 index 0000000..46eb1c6 Binary files /dev/null and b/examples/bracket.FCStd differ diff --git a/examples/bracket.step b/examples/bracket.step new file mode 100644 index 0000000..c27b032 --- /dev/null +++ b/examples/bracket.step @@ -0,0 +1,364 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('FreeCAD Model'),'2;1'); +FILE_NAME('Open CASCADE Shape Model','2026-10-15T16:56:14',('Author'),( + ''),'Open CASCADE STEP processor 7.8','FreeCAD','Unknown'); +FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); +ENDSEC; +DATA; +#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', + 'automotive_design',2000,#2); +#2 = APPLICATION_CONTEXT( + 'core data for automotive mechanical design processes'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10); +#4 = PRODUCT_DEFINITION_SHAPE('','',#5); +#5 = PRODUCT_DEFINITION('design','',#6,#9); +#6 = PRODUCT_DEFINITION_FORMATION('','',#7); +#7 = PRODUCT('Экспорт модели','Экспорт модели' + ,'',(#8)); +#8 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#10 = SHAPE_REPRESENTATION('',(#11,#15),#19); +#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14); +#12 = CARTESIAN_POINT('',(0.,0.,0.)); +#13 = DIRECTION('',(0.,0.,1.)); +#14 = DIRECTION('',(1.,0.,-0.)); +#15 = AXIS2_PLACEMENT_3D('',#16,#17,#18); +#16 = CARTESIAN_POINT('',(0.,0.,0.)); +#17 = DIRECTION('',(0.,0.,1.)); +#18 = DIRECTION('',(1.,0.,0.)); +#19 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#23)) GLOBAL_UNIT_ASSIGNED_CONTEXT( +(#20,#21,#22)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#20 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#21 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#22 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#23 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#20, + 'distance_accuracy_value','confusion accuracy'); +#24 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); +#25 = SHAPE_DEFINITION_REPRESENTATION(#26,#32); +#26 = PRODUCT_DEFINITION_SHAPE('','',#27); +#27 = PRODUCT_DEFINITION('design','',#28,#31); +#28 = PRODUCT_DEFINITION_FORMATION('','',#29); +#29 = PRODUCT('Кронштейн','Кронштейн','',(#30)); +#30 = PRODUCT_CONTEXT('',#2,'mechanical'); +#31 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#32 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#33),#327); +#33 = MANIFOLD_SOLID_BREP('',#34); +#34 = CLOSED_SHELL('',(#35,#75,#128,#181,#198,#215,#246,#277,#296,#313) + ); +#35 = ADVANCED_FACE('',(#36),#70,.F.); +#36 = FACE_BOUND('',#37,.F.); +#37 = EDGE_LOOP('',(#38,#48,#56,#64)); +#38 = ORIENTED_EDGE('',*,*,#39,.T.); +#39 = EDGE_CURVE('',#40,#42,#44,.T.); +#40 = VERTEX_POINT('',#41); +#41 = CARTESIAN_POINT('',(100.,0.,0.)); +#42 = VERTEX_POINT('',#43); +#43 = CARTESIAN_POINT('',(100.,0.,8.)); +#44 = LINE('',#45,#46); +#45 = CARTESIAN_POINT('',(100.,0.,0.)); +#46 = VECTOR('',#47,1.); +#47 = DIRECTION('',(0.,0.,1.)); +#48 = ORIENTED_EDGE('',*,*,#49,.T.); +#49 = EDGE_CURVE('',#42,#50,#52,.T.); +#50 = VERTEX_POINT('',#51); +#51 = CARTESIAN_POINT('',(100.,50.,8.)); +#52 = LINE('',#53,#54); +#53 = CARTESIAN_POINT('',(100.,0.,8.)); +#54 = VECTOR('',#55,1.); +#55 = DIRECTION('',(0.,1.,0.)); +#56 = ORIENTED_EDGE('',*,*,#57,.F.); +#57 = EDGE_CURVE('',#58,#50,#60,.T.); +#58 = VERTEX_POINT('',#59); +#59 = CARTESIAN_POINT('',(100.,50.,0.)); +#60 = LINE('',#61,#62); +#61 = CARTESIAN_POINT('',(100.,50.,0.)); +#62 = VECTOR('',#63,1.); +#63 = DIRECTION('',(0.,0.,1.)); +#64 = ORIENTED_EDGE('',*,*,#65,.F.); +#65 = EDGE_CURVE('',#40,#58,#66,.T.); +#66 = LINE('',#67,#68); +#67 = CARTESIAN_POINT('',(100.,0.,0.)); +#68 = VECTOR('',#69,1.); +#69 = DIRECTION('',(0.,1.,0.)); +#70 = PLANE('',#71); +#71 = AXIS2_PLACEMENT_3D('',#72,#73,#74); +#72 = CARTESIAN_POINT('',(100.,0.,0.)); +#73 = DIRECTION('',(-1.,0.,0.)); +#74 = DIRECTION('',(0.,1.,0.)); +#75 = ADVANCED_FACE('',(#76,#101,#112),#123,.F.); +#76 = FACE_BOUND('',#77,.F.); +#77 = EDGE_LOOP('',(#78,#86,#87,#95)); +#78 = ORIENTED_EDGE('',*,*,#79,.T.); +#79 = EDGE_CURVE('',#80,#40,#82,.T.); +#80 = VERTEX_POINT('',#81); +#81 = CARTESIAN_POINT('',(0.,0.,0.)); +#82 = LINE('',#83,#84); +#83 = CARTESIAN_POINT('',(0.,0.,0.)); +#84 = VECTOR('',#85,1.); +#85 = DIRECTION('',(1.,0.,0.)); +#86 = ORIENTED_EDGE('',*,*,#65,.T.); +#87 = ORIENTED_EDGE('',*,*,#88,.T.); +#88 = EDGE_CURVE('',#58,#89,#91,.T.); +#89 = VERTEX_POINT('',#90); +#90 = CARTESIAN_POINT('',(0.,50.,0.)); +#91 = LINE('',#92,#93); +#92 = CARTESIAN_POINT('',(100.,50.,0.)); +#93 = VECTOR('',#94,1.); +#94 = DIRECTION('',(-1.,0.,0.)); +#95 = ORIENTED_EDGE('',*,*,#96,.T.); +#96 = EDGE_CURVE('',#89,#80,#97,.T.); +#97 = LINE('',#98,#99); +#98 = CARTESIAN_POINT('',(0.,50.,0.)); +#99 = VECTOR('',#100,1.); +#100 = DIRECTION('',(0.,-1.,0.)); +#101 = FACE_BOUND('',#102,.F.); +#102 = EDGE_LOOP('',(#103)); +#103 = ORIENTED_EDGE('',*,*,#104,.F.); +#104 = EDGE_CURVE('',#105,#105,#107,.T.); +#105 = VERTEX_POINT('',#106); +#106 = CARTESIAN_POINT('',(25.,25.,0.)); +#107 = CIRCLE('',#108,5.); +#108 = AXIS2_PLACEMENT_3D('',#109,#110,#111); +#109 = CARTESIAN_POINT('',(20.,25.,0.)); +#110 = DIRECTION('',(0.,0.,1.)); +#111 = DIRECTION('',(1.,0.,0.)); +#112 = FACE_BOUND('',#113,.F.); +#113 = EDGE_LOOP('',(#114)); +#114 = ORIENTED_EDGE('',*,*,#115,.F.); +#115 = EDGE_CURVE('',#116,#116,#118,.T.); +#116 = VERTEX_POINT('',#117); +#117 = CARTESIAN_POINT('',(85.,25.,0.)); +#118 = CIRCLE('',#119,5.); +#119 = AXIS2_PLACEMENT_3D('',#120,#121,#122); +#120 = CARTESIAN_POINT('',(80.,25.,0.)); +#121 = DIRECTION('',(0.,0.,1.)); +#122 = DIRECTION('',(1.,0.,0.)); +#123 = PLANE('',#124); +#124 = AXIS2_PLACEMENT_3D('',#125,#126,#127); +#125 = CARTESIAN_POINT('',(50.,25.,0.)); +#126 = DIRECTION('',(0.,0.,1.)); +#127 = DIRECTION('',(1.,0.,0.)); +#128 = ADVANCED_FACE('',(#129,#154,#165),#176,.T.); +#129 = FACE_BOUND('',#130,.T.); +#130 = EDGE_LOOP('',(#131,#141,#147,#148)); +#131 = ORIENTED_EDGE('',*,*,#132,.F.); +#132 = EDGE_CURVE('',#133,#135,#137,.T.); +#133 = VERTEX_POINT('',#134); +#134 = CARTESIAN_POINT('',(8.,-8.881784197001E-16,8.)); +#135 = VERTEX_POINT('',#136); +#136 = CARTESIAN_POINT('',(8.,50.,8.)); +#137 = LINE('',#138,#139); +#138 = CARTESIAN_POINT('',(8.,-8.881784197001E-16,8.)); +#139 = VECTOR('',#140,1.); +#140 = DIRECTION('',(0.,1.,-1.110223024625E-16)); +#141 = ORIENTED_EDGE('',*,*,#142,.T.); +#142 = EDGE_CURVE('',#133,#42,#143,.T.); +#143 = LINE('',#144,#145); +#144 = CARTESIAN_POINT('',(0.,0.,8.)); +#145 = VECTOR('',#146,1.); +#146 = DIRECTION('',(1.,0.,0.)); +#147 = ORIENTED_EDGE('',*,*,#49,.T.); +#148 = ORIENTED_EDGE('',*,*,#149,.T.); +#149 = EDGE_CURVE('',#50,#135,#150,.T.); +#150 = LINE('',#151,#152); +#151 = CARTESIAN_POINT('',(100.,50.,8.)); +#152 = VECTOR('',#153,1.); +#153 = DIRECTION('',(-1.,0.,0.)); +#154 = FACE_BOUND('',#155,.T.); +#155 = EDGE_LOOP('',(#156)); +#156 = ORIENTED_EDGE('',*,*,#157,.F.); +#157 = EDGE_CURVE('',#158,#158,#160,.T.); +#158 = VERTEX_POINT('',#159); +#159 = CARTESIAN_POINT('',(25.,25.,8.)); +#160 = CIRCLE('',#161,5.); +#161 = AXIS2_PLACEMENT_3D('',#162,#163,#164); +#162 = CARTESIAN_POINT('',(20.,25.,8.)); +#163 = DIRECTION('',(0.,0.,1.)); +#164 = DIRECTION('',(1.,0.,0.)); +#165 = FACE_BOUND('',#166,.T.); +#166 = EDGE_LOOP('',(#167)); +#167 = ORIENTED_EDGE('',*,*,#168,.F.); +#168 = EDGE_CURVE('',#169,#169,#171,.T.); +#169 = VERTEX_POINT('',#170); +#170 = CARTESIAN_POINT('',(85.,25.,8.)); +#171 = CIRCLE('',#172,5.); +#172 = AXIS2_PLACEMENT_3D('',#173,#174,#175); +#173 = CARTESIAN_POINT('',(80.,25.,8.)); +#174 = DIRECTION('',(0.,0.,1.)); +#175 = DIRECTION('',(1.,0.,0.)); +#176 = PLANE('',#177); +#177 = AXIS2_PLACEMENT_3D('',#178,#179,#180); +#178 = CARTESIAN_POINT('',(50.,25.,8.)); +#179 = DIRECTION('',(0.,0.,1.)); +#180 = DIRECTION('',(1.,0.,0.)); +#181 = ADVANCED_FACE('',(#182),#193,.F.); +#182 = FACE_BOUND('',#183,.F.); +#183 = EDGE_LOOP('',(#184,#190,#191,#192)); +#184 = ORIENTED_EDGE('',*,*,#185,.T.); +#185 = EDGE_CURVE('',#158,#105,#186,.T.); +#186 = LINE('',#187,#188); +#187 = CARTESIAN_POINT('',(25.,25.,8.)); +#188 = VECTOR('',#189,1.); +#189 = DIRECTION('',(-0.,-0.,-1.)); +#190 = ORIENTED_EDGE('',*,*,#104,.T.); +#191 = ORIENTED_EDGE('',*,*,#185,.F.); +#192 = ORIENTED_EDGE('',*,*,#157,.F.); +#193 = CYLINDRICAL_SURFACE('',#194,5.); +#194 = AXIS2_PLACEMENT_3D('',#195,#196,#197); +#195 = CARTESIAN_POINT('',(20.,25.,8.)); +#196 = DIRECTION('',(0.,0.,1.)); +#197 = DIRECTION('',(1.,0.,0.)); +#198 = ADVANCED_FACE('',(#199),#210,.F.); +#199 = FACE_BOUND('',#200,.F.); +#200 = EDGE_LOOP('',(#201,#207,#208,#209)); +#201 = ORIENTED_EDGE('',*,*,#202,.T.); +#202 = EDGE_CURVE('',#169,#116,#203,.T.); +#203 = LINE('',#204,#205); +#204 = CARTESIAN_POINT('',(85.,25.,8.)); +#205 = VECTOR('',#206,1.); +#206 = DIRECTION('',(-0.,-0.,-1.)); +#207 = ORIENTED_EDGE('',*,*,#115,.T.); +#208 = ORIENTED_EDGE('',*,*,#202,.F.); +#209 = ORIENTED_EDGE('',*,*,#168,.F.); +#210 = CYLINDRICAL_SURFACE('',#211,5.); +#211 = AXIS2_PLACEMENT_3D('',#212,#213,#214); +#212 = CARTESIAN_POINT('',(80.,25.,8.)); +#213 = DIRECTION('',(0.,0.,1.)); +#214 = DIRECTION('',(1.,0.,0.)); +#215 = ADVANCED_FACE('',(#216),#241,.T.); +#216 = FACE_BOUND('',#217,.T.); +#217 = EDGE_LOOP('',(#218,#219,#227,#235)); +#218 = ORIENTED_EDGE('',*,*,#132,.T.); +#219 = ORIENTED_EDGE('',*,*,#220,.T.); +#220 = EDGE_CURVE('',#135,#221,#223,.T.); +#221 = VERTEX_POINT('',#222); +#222 = CARTESIAN_POINT('',(8.,50.,53.)); +#223 = LINE('',#224,#225); +#224 = CARTESIAN_POINT('',(8.,50.,8.)); +#225 = VECTOR('',#226,1.); +#226 = DIRECTION('',(-1.110223024625E-16,1.110223024625E-16,1.)); +#227 = ORIENTED_EDGE('',*,*,#228,.T.); +#228 = EDGE_CURVE('',#221,#229,#231,.T.); +#229 = VERTEX_POINT('',#230); +#230 = CARTESIAN_POINT('',(8.,4.107825191113E-15,53.)); +#231 = LINE('',#232,#233); +#232 = CARTESIAN_POINT('',(8.,50.,53.)); +#233 = VECTOR('',#234,1.); +#234 = DIRECTION('',(0.,-1.,1.110223024625E-16)); +#235 = ORIENTED_EDGE('',*,*,#236,.T.); +#236 = EDGE_CURVE('',#229,#133,#237,.T.); +#237 = LINE('',#238,#239); +#238 = CARTESIAN_POINT('',(8.,4.107825191113E-15,53.)); +#239 = VECTOR('',#240,1.); +#240 = DIRECTION('',(1.110223024625E-16,-1.110223024625E-16,-1.)); +#241 = PLANE('',#242); +#242 = AXIS2_PLACEMENT_3D('',#243,#244,#245); +#243 = CARTESIAN_POINT('',(8.,25.,30.5)); +#244 = DIRECTION('',(1.,0.,0.)); +#245 = DIRECTION('',(0.,0.,1.)); +#246 = ADVANCED_FACE('',(#247),#272,.F.); +#247 = FACE_BOUND('',#248,.F.); +#248 = EDGE_LOOP('',(#249,#257,#258,#266)); +#249 = ORIENTED_EDGE('',*,*,#250,.T.); +#250 = EDGE_CURVE('',#251,#221,#253,.T.); +#251 = VERTEX_POINT('',#252); +#252 = CARTESIAN_POINT('',(-4.996003610813E-15,50.,53.)); +#253 = LINE('',#254,#255); +#254 = CARTESIAN_POINT('',(-4.996003610813E-15,50.,53.)); +#255 = VECTOR('',#256,1.); +#256 = DIRECTION('',(1.,-1.110223024625E-16,1.110223024625E-16)); +#257 = ORIENTED_EDGE('',*,*,#228,.T.); +#258 = ORIENTED_EDGE('',*,*,#259,.F.); +#259 = EDGE_CURVE('',#260,#229,#262,.T.); +#260 = VERTEX_POINT('',#261); +#261 = CARTESIAN_POINT('',(-4.996003610813E-15,4.996003610813E-15,53.)); +#262 = LINE('',#263,#264); +#263 = CARTESIAN_POINT('',(-4.996003610813E-15,4.996003610813E-15,53.)); +#264 = VECTOR('',#265,1.); +#265 = DIRECTION('',(1.,-1.110223024625E-16,1.110223024625E-16)); +#266 = ORIENTED_EDGE('',*,*,#267,.F.); +#267 = EDGE_CURVE('',#251,#260,#268,.T.); +#268 = LINE('',#269,#270); +#269 = CARTESIAN_POINT('',(-4.996003610813E-15,50.,53.)); +#270 = VECTOR('',#271,1.); +#271 = DIRECTION('',(0.,-1.,1.110223024625E-16)); +#272 = PLANE('',#273); +#273 = AXIS2_PLACEMENT_3D('',#274,#275,#276); +#274 = CARTESIAN_POINT('',(-4.996003610813E-15,50.,53.)); +#275 = DIRECTION('',(1.110223024625E-16,-1.110223024625E-16,-1.)); +#276 = DIRECTION('',(0.,-1.,1.110223024625E-16)); +#277 = ADVANCED_FACE('',(#278),#291,.T.); +#278 = FACE_BOUND('',#279,.T.); +#279 = EDGE_LOOP('',(#280,#281,#282,#283,#284,#285)); +#280 = ORIENTED_EDGE('',*,*,#79,.T.); +#281 = ORIENTED_EDGE('',*,*,#39,.T.); +#282 = ORIENTED_EDGE('',*,*,#142,.F.); +#283 = ORIENTED_EDGE('',*,*,#236,.F.); +#284 = ORIENTED_EDGE('',*,*,#259,.F.); +#285 = ORIENTED_EDGE('',*,*,#286,.T.); +#286 = EDGE_CURVE('',#260,#80,#287,.T.); +#287 = LINE('',#288,#289); +#288 = CARTESIAN_POINT('',(-4.996003610813E-15,4.996003610813E-15,53.)); +#289 = VECTOR('',#290,1.); +#290 = DIRECTION('',(1.110223024625E-16,-1.110223024625E-16,-1.)); +#291 = PLANE('',#292); +#292 = AXIS2_PLACEMENT_3D('',#293,#294,#295); +#293 = CARTESIAN_POINT('',(36.470588235294,7.23096237934E-16, + 12.970588235294)); +#294 = DIRECTION('',(-0.,-1.,-0.)); +#295 = DIRECTION('',(0.,0.,-1.)); +#296 = ADVANCED_FACE('',(#297),#308,.T.); +#297 = FACE_BOUND('',#298,.T.); +#298 = EDGE_LOOP('',(#299,#300,#301,#302)); +#299 = ORIENTED_EDGE('',*,*,#96,.T.); +#300 = ORIENTED_EDGE('',*,*,#286,.F.); +#301 = ORIENTED_EDGE('',*,*,#267,.F.); +#302 = ORIENTED_EDGE('',*,*,#303,.F.); +#303 = EDGE_CURVE('',#89,#251,#304,.T.); +#304 = LINE('',#305,#306); +#305 = CARTESIAN_POINT('',(0.,50.,8.)); +#306 = VECTOR('',#307,1.); +#307 = DIRECTION('',(-1.110223024625E-16,1.110223024625E-16,1.)); +#308 = PLANE('',#309); +#309 = AXIS2_PLACEMENT_3D('',#310,#311,#312); +#310 = CARTESIAN_POINT('',(-2.303982247705E-15,25.,26.5)); +#311 = DIRECTION('',(-1.,-0.,-0.)); +#312 = DIRECTION('',(0.,0.,-1.)); +#313 = ADVANCED_FACE('',(#314),#322,.T.); +#314 = FACE_BOUND('',#315,.T.); +#315 = EDGE_LOOP('',(#316,#317,#318,#319,#320,#321)); +#316 = ORIENTED_EDGE('',*,*,#57,.F.); +#317 = ORIENTED_EDGE('',*,*,#88,.T.); +#318 = ORIENTED_EDGE('',*,*,#303,.T.); +#319 = ORIENTED_EDGE('',*,*,#250,.T.); +#320 = ORIENTED_EDGE('',*,*,#220,.F.); +#321 = ORIENTED_EDGE('',*,*,#149,.F.); +#322 = PLANE('',#323); +#323 = AXIS2_PLACEMENT_3D('',#324,#325,#326); +#324 = CARTESIAN_POINT('',(36.470588235294,50.,12.970588235294)); +#325 = DIRECTION('',(0.,1.,0.)); +#326 = DIRECTION('',(0.,-0.,1.)); +#327 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#331)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#328,#329,#330)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#328 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#329 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#330 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#331 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#328, + 'distance_accuracy_value','confusion accuracy'); +#332 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#333,#335); +#333 = ( REPRESENTATION_RELATIONSHIP('','',#32,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#334) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#334 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15); +#335 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#336 + ); +#336 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('41','Кронштейн','',#5, + #27,$); +#337 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#29)); +ENDSEC; +END-ISO-10303-21; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..305ab9e --- /dev/null +++ b/manifest.json @@ -0,0 +1,17054 @@ +{ + "sources": [ + { + "path": "src\\QInventor\\Init.py", + "sha256": "7d2681813d2efe5a3b79147da578b67290117e61575e4e1735dd6a9aab7a37e1" + }, + { + "path": "src\\QInventor\\InitGui.py", + "sha256": "0f6fa12520a656980d3a3d61582cfcbebfa3f41728d63b75ad2fb001116a850e" + }, + { + "path": "src\\QInventor\\qi_bom_ui.py", + "sha256": "522ea28fcce9f7a22e0ea107304ca32406846ec99af16b6adf04ef3da008811d" + }, + { + "path": "src\\QInventor\\qi_bom.py", + "sha256": "548b7196392650b790f8263440cc12039eb1aae20c19ce6158e4588f9cc2053d" + }, + { + "path": "src\\QInventor\\qi_commands.py", + "sha256": "19816cbda6a9cc15023484022d0a13f84bd176d6e9eae0b681b968da21732f61" + }, + { + "path": "src\\QInventor\\qi_drawing_bom_export.py", + "sha256": "1e0fbe90cafcf2321ce4eae90868211be7db105767f98bcf42789950d52dab2f" + }, + { + "path": "src\\QInventor\\qi_drawing_bom_ui.py", + "sha256": "8a50502dd648a42a2e6e4b436972c67b71ec0683f0c4276b13f313357d0c103a" + }, + { + "path": "src\\QInventor\\qi_drawing_bom.py", + "sha256": "143dfa91623c6ac08c80e02e1671f6628ec18a5fd551bda362d7d99d252a4d11" + }, + { + "path": "src\\QInventor\\qi_extrude.py", + "sha256": "03b5b7eea4adbf168d623995e84e0d3af4ccea58494b3b34542b10447ef0c072" + }, + { + "path": "src\\QInventor\\qi_io.py", + "sha256": "77ea249f6d3a374e89ac294e0de8a386356ed47f908ba494cbe8fea37c34df1f" + }, + { + "path": "src\\QInventor\\qi_localization.py", + "sha256": "84041c5888157e7844b99ebda3fddf9e86531667c9e1aa56cde917795f1b91e1" + }, + { + "path": "src\\QInventor\\qi_messages.py", + "sha256": "5c0c41cf0c1bd60f30d0aec0da6934c2af70a88765d86c07e500046714eeecb5" + }, + { + "path": "src\\QInventor\\qi_model.py", + "sha256": "08c094382cfadd5e94e3ab6d1ce01ad1214985cc2a3222814ab5b2da26c4d762" + }, + { + "path": "src\\QInventor\\qi_parameters_ui.py", + "sha256": "4e10337d61bb38f229736f0e877d16cc99106943fa3a8f4333fa905aad7ec19f" + }, + { + "path": "src\\QInventor\\qi_parameters.py", + "sha256": "9f384e40b930b6ebc4e7f068651c0797134818803b82704258e26215997c01c2" + }, + { + "path": "src\\QInventor\\qi_projection.py", + "sha256": "a4923dd69f7dcb3230a4b60bd001698d90d6d02db5aa016362034ee73520925d" + }, + { + "path": "src\\QInventor\\qi_sketch_offset.py", + "sha256": "ffb0f2a000b91528587110d7f5d47b7e26902b499e1b35220e3c09fc89786cb9" + }, + { + "path": "src\\QInventor\\qi_sketch_patterns.py", + "sha256": "c98dfd369c639436f0aed0664cf43cc6345b6639c102309efb90041fe65d4e1e" + }, + { + "path": "src\\QInventor\\qi_sketch_preview.py", + "sha256": "4858dc39046bad95306179212fe44a63e2e1437259a9c6d14bc847b68c7aab76" + }, + { + "path": "src\\QInventor\\qi_sketch_ui.py", + "sha256": "d505b802c65f1566a64d7687e290a64226aff91958db59b8d621ac69909ead37" + }, + { + "path": "src\\QInventor\\qi_step_export.py", + "sha256": "754f7f8b841f87a696ddf800e19f947d5e8712fc53faf8fce4d2d0d0f3ee51f6" + }, + { + "path": "src\\QInventor\\qi_ui.py", + "sha256": "7432d1939919b30e5fc81cd333caf220ca3b490d71851328aa26d8070a7ac307" + }, + { + "path": "src\\QInventor\\qi_workbench.py", + "sha256": "312d49f40d78e8bde48cd8fa57232cd380565fe5b30a688e4612d57bde210c59" + }, + { + "path": "src\\QInventor\\resources\\qinventor-a4.svg", + "sha256": "f77419e8559cb24b29a62ebcdc02d781f52c6910c0299d4820d1dd8c820c4128" + }, + { + "path": "src\\QInventor\\resources\\qinventor.ico", + "sha256": "ea229e71d0fcdd749c35977fc0f11720a972c3ee04b3d9042d850182e0a6ed1c" + }, + { + "path": "src\\QInventor\\resources\\qinventor.png", + "sha256": "3de618b2d07a1fd36a14f71d3f0b85f36a5dcc22ccf070b4629491da6e6f9e32" + }, + { + "path": "src\\QInventor\\resources\\qinventor.svg", + "sha256": "12e95fa28c4aaf927311a777b3928d0518799e51663aab2f0f036015210dc954" + } + ], + "application": "QInventor", + "launcherSha256": "cd5a44ae9cd094a77088a58efc12e48b26549abf5ed40edd8a15f6450ef554de", + "engine": "FreeCAD 1.1.4 / Open CASCADE", + "builtAtUtc": "2026-10-15T13:59:40.0135156Z", + "validation": { + "status": "passed", + "version": [ + "1", + "1", + "4", + "20260928 (Git shallow)", + "Unknown", + "2026/09/28 13:46:34", + "(HEAD detached at 4fd3bf320)", + "4fd3bf320d9566a27e60069fc8387448aaa3a094" + ], + "started": "2026-10-15T16:40:59.566097+03:00", + "checks": [ + { + "name": "native_and_custom_commands", + "passed": true, + "result": { + "custom": [ + "QI_NewPart", + "QI_NewAssembly", + "QI_Open", + "QI_Save", + "QI_SaveAs", + "QI_NewSketch", + "QI_FinishSketch", + "QI_CancelSketch", + "QI_Line", + "QI_Polyline", + "QI_Rectangle", + "QI_Circle", + "QI_Arc", + "QI_Dimension", + "QI_Horizontal", + "QI_Vertical", + "QI_Coincident", + "QI_Parallel", + "QI_Perpendicular", + "QI_Equal", + "QI_Tangent", + "QI_Fix", + "QI_Construction", + "QI_Trim", + "QI_ProjectGeometry", + "QI_BreakProjection", + "QI_SketchOffset", + "QI_SketchMirror", + "QI_SketchRectangularPattern", + "QI_SketchCircularPattern", + "QI_EditSketchPattern", + "QI_Pocket", + "QI_Revolve", + "QI_Fillet", + "QI_Chamfer", + "QI_Hole", + "QI_Pattern", + "QI_Mirror", + "QI_Pad", + "QI_Box", + "QI_Cylinder", + "QI_Plate", + "QI_Demo", + "QI_AssemblyDemo", + "QI_Dimensions", + "QI_Material", + "QI_Properties", + "QI_Export", + "QI_BOM", + "QI_AssemblyActivate", + "QI_AssemblyInsert", + "QI_AssemblyGround", + "QI_AssemblyFixed", + "QI_AssemblyRevolute", + "QI_AssemblySlider", + "QI_AssemblyCylindrical", + "QI_AssemblySolve", + "QI_Drawing", + "QI_DrawingBOM", + "QI_DrawingExport", + "QI_DrawingView", + "QI_DrawingDimension", + "QI_DrawingProjection", + "QI_ViewFit", + "QI_Axonometric", + "QI_Front", + "QI_Top", + "QI_ToggleTree", + "QI_NativeTools", + "QI_Help" + ], + "native_count": 772, + "command_api": [ + "__class__", + "__delattr__", + "__dir__", + "__doc__", + "__eq__", + "__format__", + "__ge__", + "__getattribute__", + "__getstate__", + "__gt__", + "__hash__", + "__init__", + "__init_subclass__", + "__le__", + "__lt__", + "__ne__", + "__new__", + "__reduce__", + "__reduce_ex__", + "__repr__", + "__setattr__", + "__sizeof__", + "__str__", + "__subclasshook__", + "createCustomCommand", + "findCustomCommand", + "get", + "getAction", + "getInfo", + "getShortcut", + "isActive", + "listAll", + "listByShortcut", + "removeCustomCommand", + "resetShortcut", + "run", + "setShortcut", + "update" + ], + "native_object_api": [ + "__class__", + "__delattr__", + "__dir__", + "__doc__", + "__eq__", + "__format__", + "__ge__", + "__getattribute__", + "__getstate__", + "__gt__", + "__hash__", + "__init__", + "__init_subclass__", + "__le__", + "__lt__", + "__ne__", + "__new__", + "__reduce__", + "__reduce_ex__", + "__repr__", + "__setattr__", + "__sizeof__", + "__str__", + "__subclasshook__", + "createCustomCommand", + "findCustomCommand", + "get", + "getAction", + "getInfo", + "getShortcut", + "isActive", + "listAll", + "listByShortcut", + "removeCustomCommand", + "resetShortcut", + "run", + "setShortcut", + "update" + ] + } + }, + { + "name": "russian_workflow_ribbon", + "passed": true, + "result": [ + "Файл", + "3D-модель", + "Эскиз", + "Сборка", + "Чертёж", + "Вид" + ] + }, + { + "name": "native_mouse_keyboard_sketch_ribbon_suite", + "passed": true, + "result": { + "status": "passed", + "passed": 11, + "failed": 0, + "checks": [ + { + "name": "Blank XY sketch enters native edit via QI_NewSketch", + "passed": true, + "result": { + "native_editor": "Sketcher::SketchObject", + "plane": "XY", + "canvas": [ + 1174, + 608 + ], + "camera": "#Inventor V2.1 ascii\n\n\nOrthographicCamera {\n viewportMapping ADJUST_CAMERA\n position 0 0 246\n orientation 0 0 1 0\n nearDistance 245.74402\n farDistance 246.24602\n aspectRatio 1\n focalDistance 250\n height 4.1421356\n\n}\n", + "exposed": true, + "edit_controls": { + "QI_FinishSketch": { + "visible_without_scrolling": true, + "caption": "Завершить\nэскиз" + }, + "QI_CancelSketch": { + "visible_without_scrolling": true, + "caption": "Отменить\nэскиз" + } + }, + "navigation": "Gui::RevitNavigationStyle" + } + }, + { + "name": "Rectangle drawn through native Qt mouse events creates native geometry", + "passed": true, + "result": { + "geometry_count": 4, + "constraints": [ + "Coincident", + "Coincident", + "Coincident", + "Coincident", + "Horizontal", + "Horizontal", + "Vertical", + "Vertical" + ], + "closed": true + } + }, + { + "name": "D/E and document shortcuts have one owner across workbenches", + "passed": true, + "result": { + "native_keys": { + "L": [], + "C": [], + "D": [], + "E": [], + "Ctrl+N": [], + "Ctrl+O": [], + "Ctrl+S": [] + }, + "ribbon_keys": { + "QI_NewPart": "Ctrl+N", + "QI_Open": "Ctrl+O", + "QI_Save": "Ctrl+S", + "QI_Line": "L", + "QI_Circle": "C", + "QI_Dimension": "D", + "QI_Pad": "E" + } + } + }, + { + "name": "Native Qt mouse events select an edge; D places a driving datum through its dialog", + "passed": true, + "result": { + "driving_value_mm": 40, + "dialogs": [ + "Введите длину" + ], + "constraints": 9, + "selection_method": "native viewport Qt mouse events; actual selection verified", + "coordinates": { + "expected_edge": "Edge3", + "local_point": [ + -1.3148558000000001, + 0.577719, + 0.0 + ], + "world_point": [ + -1.3148558000000001, + 0.577719, + 0.0 + ], + "projected_physical": [ + 492, + 486 + ], + "qt_point": [ + 394, + 219 + ], + "qt_size": [ + 1174, + 608 + ], + "physical_size": [ + 1467, + 760 + ], + "ratios": [ + 1.2495741056218057, + 1.25 + ], + "input_delivery": "Qt events on native viewport", + "os_pointer_access": false, + "input_state": { + "wait_for_window_active": true, + "main_active": true, + "focus_window_title": "QInventor — Проверка построения мышью*", + "renderer_mouse_tracking": true, + "renderer_focus_policy": "11", + "renderer_has_focus": true, + "native_parent_mouse_tracking": true, + "edited_object": "Sketch", + "sketch_visible": false, + "main_frame": [ + 0, + 0, + 1440, + 970 + ], + "canvas_global_origin": [ + 266, + 332 + ], + "screens": [ + [ + 0, + 0, + 1536, + 960 + ] + ] + }, + "preselection_object": "Sketch", + "preselection_subelements": [ + "Edge3" + ], + "selection_after_press": [], + "selection": [ + [ + "Sketch", + [ + "Edge3" + ] + ] + ], + "dimension_key_state": { + "active_main_window": true, + "renderer_has_focus": true, + "focused_class": "Gui::View3DInventorViewer", + "focused_object_name": "", + "shortcut_enabled": true, + "edited_object": "Sketch", + "focus_in_viewport": true + }, + "dimension_key_signals": [ + "D" + ] + }, + "untranslated_candidates": [] + } + }, + { + "name": "Finish sketch exits active tool; E previews and creates real solid", + "passed": true, + "result": { + "solids": 1, + "volume_mm3": 17484.251519999994 + } + }, + { + "name": "Cancel existing native sketch restores original geometry and constraints", + "passed": true, + "result": { + "original_geometry": 4, + "restored_geometry": 4, + "constraints": 11 + } + }, + { + "name": "L and C draw actual native line/circle and cancel new sketch removes it", + "passed": true, + "result": [ + { + "key": "L", + "native_geometry": "LineSegment" + }, + { + "key": "C", + "native_geometry": "Circle" + } + ] + }, + { + "name": "Sketch on Body face inside App::Part remains in Body and attaches to its feature", + "passed": true, + "result": { + "parent": "PartDesign::Body", + "support": "PartDesign::Feature", + "map_mode": "FlatFace", + "restored_volume_mm3": 8000.0 + } + }, + { + "name": "Projection and Break Link buttons create defining linked geometry, regenerate and undo", + "passed": true, + "result": { + "controls": { + "QI_ProjectGeometry": { + "caption": "Проецировать геометрию", + "tooltip": "Выберите ребро или грань: контуры будут связаны с исходной деталью. Режим «Вспомогательная» исключает их из профиля.", + "activation": "actual Qt mouse click" + }, + "QI_BreakProjection": { + "caption": "Разорвать связь", + "tooltip": "Преобразовать выбранные проекции в независимую геометрию, сохранив её положение и зависимости.", + "activation": "actual Qt mouse click" + } + }, + "source_face": "Face6", + "native_geo_ids": [ + -3, + -4, + -5, + -6 + ], + "source_length_mm": 30.0, + "original_area_mm2": 199.99999999999997, + "updated_area_mm2": 300.0, + "detached_geometry_count": 4, + "undo_restored_link": true, + "selection_method": "native Selection API observer boundary; controls clicked with Qt", + "break_selection": [ + [ + "Sketch", + [ + "ExternalEdge1" + ] + ] + ], + "untranslated_candidates": [] + } + }, + { + "name": "Russian native enum popup preserves canonical values and commits selected property", + "passed": true, + "result": { + "painted_option": "Вращательное", + "canonical_value": "Revolute", + "source_item_preserved": "Revolute" + } + }, + { + "name": "Supplemental Russian translations and visible surface audit", + "passed": true, + "result": { + "catalog_sources": 385, + "samples": { + "Sketcher": "Эскизы", + "Pick point or edge": "Выберите точку или ребро", + "Switch mode": "Сменить режим", + "QILength": "Длина", + "MapMode": "Режим привязки", + "New Assembly": "Новая сборка", + "Constraint12": "Ограничение 12" + }, + "untranslated_candidates": [] + } + } + ], + "input_delivery": "Qt events on native viewport", + "os_pointer_access": false + } + }, + { + "name": "associative_sketch_projection_suite", + "passed": true, + "result": { + "passed": true, + "checks": [ + { + "name": "Defining projection is linked and regenerates", + "passed": true, + "evidence": { + "native_ids": [ + -3, + -4, + -5, + -6 + ], + "source_length_mm": 30.0, + "updated_profile_area_mm2": 300 + } + }, + { + "name": "Single edge and vertex projections", + "passed": true, + "evidence": { + "edge_ids": [ + -3 + ], + "vertex_ids": [ + -4 + ], + "profile_excludes_references": true + } + }, + { + "name": "Construction references regenerate and are excluded from extrusion", + "passed": true, + "evidence": { + "reference_perimeter_mm": 80.0, + "extrusion_rejected": true + } + }, + { + "name": "Face-supported sketch and parametric extrusion", + "passed": true, + "evidence": { + "map_mode": "FlatFace", + "updated_join_volume_mm3": 3300.0 + } + }, + { + "name": "Body selection captures preceding solid feature", + "passed": true, + "evidence": { + "source_type": "PartDesign::AdditiveBox", + "body_reference_cycle_avoided": true + } + }, + { + "name": "XZ and YZ world placement", + "passed": true, + "evidence": { + "projection_extrusion_volumes_mm3": { + "XZ": 320.0000000000002, + "YZ": 159.99999999999997 + } + } + }, + { + "name": "Face projection includes inner hole boundaries", + "passed": true, + "evidence": { + "projected_edges": 5, + "wires": 2, + "hole_preserved_volume_mm3": 1874.336293856408 + } + }, + { + "name": "Face attachment in rotated Part and Body preserves world placement", + "passed": true, + "evidence": { + "placed_part_and_body": true, + "parametric_world_placement_volume_mm3": 3300.0 + } + }, + { + "name": "Break Link retains native circles and arcs", + "passed": true, + "evidence": { + "circle_radius_preserved_mm": 4, + "arc_length_preserved_mm": 15.707963267948966 + } + }, + { + "name": "Break Link preserves native reference dimensions", + "passed": true, + "evidence": { + "reference_dimension_mm": 10.0, + "name_and_reference_mode_retained": true + } + }, + { + "name": "Break Link retains geometry and stops dependency", + "passed": true, + "evidence": { + "old_to_independent_ids": { + "-3": 0, + "-4": 1, + "-5": 2, + "-6": 3 + }, + "independent_area_mm2": 200 + } + }, + { + "name": "Break Link preserves construction state", + "passed": true, + "evidence": { + "independent_construction_curves": 4, + "profile_empty": true + } + }, + { + "name": "Selective unlink preserves grouped links to the same source", + "passed": true, + "evidence": { + "same_source_independent_face_and_retained_edge_link": true + } + }, + { + "name": "Native vertex selection and independent point unlink", + "passed": true, + "evidence": { + "vertex_selection_name": "Vertex1", + "independent_point_preserved": true, + "point_constraints_undo_redo_save_reopen": true + } + }, + { + "name": "Break Link preserves constraints expressions and other references", + "passed": true, + "evidence": { + "constraints_retained": 3, + "expression": "Base.Width / 2", + "remaining_external_constraint_remapped": -3, + "save_reopen_preserved": true + } + }, + { + "name": "Projection and Break Link native undo redo", + "passed": true, + "evidence": { + "projection_and_unlink_undo_redo": true + } + }, + { + "name": "Associated and independent geometry save reopen", + "passed": true, + "evidence": { + "associated_file": "C:\\Repos\\qinventor\\artifacts\\validation\\associated-projection.FCStd", + "independent_file": "C:\\Repos\\qinventor\\artifacts\\validation\\independent-projection.FCStd", + "independent_area_after_reload_mm2": 300 + } + }, + { + "name": "Invalid mixed batches reject atomically", + "passed": true, + "evidence": { + "invalid_batches_rejected_without_mutation": 7 + } + }, + { + "name": "Cyclic and cross-document sources reject", + "passed": true, + "evidence": { + "invalid_reference_sources_rejected": 3 + } + }, + { + "name": "Native solver failure aborts projection transaction", + "passed": true, + "evidence": { + "failed_native_solver_transaction_rolled_back": true + } + }, + { + "name": "Native external selection names map to GeoIds", + "passed": true, + "evidence": { + "ExternalEdge1": -3, + "ExternalEdge4": -6 + } + } + ], + "summary": { + "total": 21, + "passed": 21, + "failed": 0 + }, + "gui_exercised": false + } + }, + { + "name": "associative_sketch_offset_suite", + "passed": true, + "result": { + "passed": true, + "checks": [ + { + "name": "Circle offset native association and signed source edit", + "passed": true, + "evidence": [ + { + "signed_distance_mm": 2.0, + "updated_radius_mm": 10.0 + }, + { + "signed_distance_mm": -2.0, + "updated_radius_mm": 6.0 + } + ] + }, + { + "name": "Driving offset dimension and native expression", + "passed": true, + "evidence": { + "native_dimension_name": "Смещение", + "expression_drives_offset_mm": 6 + } + }, + { + "name": "Directed single-line association survives resize and rotation", + "passed": true, + "evidence": [ + { + "signed_distance_mm": 2.0, + "source_rotation_deg": 45, + "source_length_mm": 20, + "helper_geometry_excluded": true + }, + { + "signed_distance_mm": -2.0, + "source_rotation_deg": 45, + "source_length_mm": 20, + "helper_geometry_excluded": true + } + ] + }, + { + "name": "Closed polygon outward and inward association for both windings", + "passed": true, + "evidence": [ + { + "clockwise": false, + "signed_distance_mm": 3.0, + "offset_profile_area_mm2": 576.0000000003402 + }, + { + "clockwise": false, + "signed_distance_mm": -3.0, + "offset_profile_area_mm2": 95.99999999572618 + }, + { + "clockwise": true, + "signed_distance_mm": 3.0, + "offset_profile_area_mm2": 576.0 + }, + { + "clockwise": true, + "signed_distance_mm": -3.0, + "offset_profile_area_mm2": 95.99999999999997 + } + ] + }, + { + "name": "Inventor-style loop expansion excludes auxiliary geometry", + "passed": true, + "evidence": { + "one_selected_edge_expands_to": 4, + "auxiliary_connectors_and_unrelated_circle_excluded": true + } + }, + { + "name": "Inclined polygon edges retain exact normal distances", + "passed": true, + "evidence": { + "inclined_edges_associated_after_source_resize": 3, + "perpendicular_offset_mm": 2 + } + }, + { + "name": "Constrain Offset off produces independent geometry", + "passed": true, + "evidence": { + "unchecked_constrain_offset_removes_source_association": true, + "independent_polygon_remains_closed": true + } + }, + { + "name": "Native offset undo redo retains source association", + "passed": true, + "evidence": { + "native_undo_steps": 1, + "redo_retains_association": true + } + }, + { + "name": "Preview cancellation errors and nested transaction rollback", + "passed": true, + "evidence": { + "outer_cancel_restores_sketch": true, + "inner_failure_aborts_outer_preview": true, + "nested_commit_is_one_undo_step": true + } + }, + { + "name": "Save reopen and source edit need only native Sketcher", + "passed": true, + "evidence": [ + { + "kind": "circle", + "saved_file": "offset-circle.FCStd", + "native_source_edit_after_reopen": true + }, + { + "kind": "line", + "saved_file": "offset-line.FCStd", + "native_source_edit_after_reopen": true + }, + { + "kind": "polygon", + "saved_file": "offset-polygon.FCStd", + "native_source_edit_after_reopen": true + } + ] + }, + { + "name": "Only offset distance visible; native visibility survives history and persistence", + "passed": true, + "evidence": [ + { + "kind": "circle", + "hidden_association_labels": 1, + "driving_offset_visible": true, + "source_visibility_unchanged": true, + "native_undo_redo_and_save_reopen": true, + "hidden_constraints_still_solve_source_and_distance_edits": true + }, + { + "kind": "line", + "hidden_association_labels": 7, + "driving_offset_visible": true, + "source_visibility_unchanged": true, + "native_undo_redo_and_save_reopen": true, + "hidden_constraints_still_solve_source_and_distance_edits": true + }, + { + "kind": "polygon", + "hidden_association_labels": 23, + "driving_offset_visible": true, + "source_visibility_unchanged": true, + "native_undo_redo_and_save_reopen": true, + "hidden_constraints_still_solve_source_and_distance_edits": true + } + ] + }, + { + "name": "Closed defining offset extrudes on XY XZ YZ in placed Part", + "passed": true, + "evidence": { + "annular_profile_extrudes_in_placed_part": { + "XY": 528.0, + "XZ": 528.0, + "YZ": 528.0 + }, + "construction_connectors_excluded": true + } + }, + { + "name": "Invalid distance selection and collapse reject atomically", + "passed": true, + "evidence": { + "invalid_inputs_rejected_without_changes": 17 + } + }, + { + "name": "Unsupported topology rejects without copied fake geometry", + "passed": true, + "evidence": { + "unsupported_open_concave_branch_curve_topologies_rejected": 7 + } + }, + { + "name": "Real conflicting source constraints reject offset", + "passed": true, + "evidence": { + "real_native_solver_conflict_rejected_atomically": true + } + } + ], + "summary": { + "total": 15, + "passed": 15, + "failed": 0 + }, + "gui_exercised": false + } + }, + { + "name": "associative_sketch_patterns_suite", + "passed": true, + "result": { + "status": "passed", + "passed": 17, + "failed": 0, + "skipped": 0, + "checks": [ + { + "name": "mirror_circle_native_association", + "passed": true, + "status": "passed", + "detail": { + "radius_and_position_associative": true, + "axis_edit": true + } + }, + { + "name": "mirror_line_selected_moving_axis", + "passed": true, + "status": "passed", + "detail": { + "both_endpoints_linked": true, + "construction_axis_edit_associative": true + } + }, + { + "name": "rectangular_circle_native_dimensions", + "passed": true, + "status": "passed", + "detail": { + "copies": 5, + "source_dimensions_and_pattern_parameters_associative": true, + "native_expressions": 11 + } + }, + { + "name": "rectangular_signed_steps_and_line_endpoints", + "passed": true, + "status": "passed", + "detail": { + "signed_steps": true, + "native_dimension_positive_to_negative": true, + "line_endpoints_linked": true + } + }, + { + "name": "unconstrained_seed_association", + "passed": true, + "status": "passed", + "detail": { + "association_without_original_dimensions": true, + "drag_and_later_radius_constraint_propagate": true + } + }, + { + "name": "circular_full_partial_negative_and_source_edit", + "passed": true, + "status": "passed", + "detail": { + "full_circle_distinct_occurrences": 4, + "negative_partial_angle": -179.99999999999997, + "source_changes_associative": true + } + }, + { + "name": "circular_lines_and_native_step_edit", + "passed": true, + "status": "passed", + "detail": { + "line_orientation_rotates": true, + "native_step_angle_drives_every_point": true + } + }, + { + "name": "counts_and_native_undo_redo", + "passed": true, + "status": "passed", + "detail": { + "create_is_one_undo_step": true, + "counts_editable": [ + 8, + 6 + ], + "undo_redo_restores_geometry_and_registry": true + } + }, + { + "name": "native_fcstd_save_reopen_all_patterns", + "passed": true, + "status": "passed", + "detail": { + "native_fcstd_reopened": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\mirror.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\rectangular.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\circular.FCStd" + ], + "geometry_ids_constraints_expressions_and_association_persist": true + } + }, + { + "name": "native_helper_visibility_persistence_association", + "passed": true, + "status": "passed", + "detail": { + "patterns": [ + { + "kind": "mirror", + "native_hidden_helpers": 2, + "visible_master_dimensions": 0, + "source_visibility_unchanged": true, + "native_undo_redo_reopen_persist": true, + "hidden_constraints_still_active_and_associative": true + }, + { + "kind": "rectangular", + "native_hidden_helpers": 11, + "visible_master_dimensions": 3, + "source_visibility_unchanged": true, + "native_undo_redo_reopen_persist": true, + "hidden_constraints_still_active_and_associative": true + }, + { + "kind": "circular", + "native_hidden_helpers": 21, + "visible_master_dimensions": 3, + "source_visibility_unchanged": true, + "native_undo_redo_reopen_persist": true, + "hidden_constraints_still_active_and_associative": true + } + ] + } + }, + { + "name": "selection_parameters_unsupported_curves_atomic", + "passed": true, + "status": "passed", + "detail": { + "rejected_inputs": 14, + "russian_errors": true, + "no_geometry_constraint_metadata_or_undo_changes": true + } + }, + { + "name": "count_dependencies_atomic_rejection", + "passed": true, + "status": "passed", + "detail": { + "reference_constraint_dependency_rejected": true, + "later_geometry_rejected_for_count_edit": true, + "spacing_still_editable": true + } + }, + { + "name": "persistent_geometry_ids_after_renumbering", + "passed": true, + "status": "passed", + "detail": { + "native_persistent_geometry_ids_survive_index_renumbering": true + } + }, + { + "name": "native_expression_bound_master_dimensions", + "passed": true, + "status": "passed", + "detail": { + "external_expression_not_silently_removed": true, + "bound_dimension_edits_rejected_atomically": true, + "other_parameters_remain_editable": true, + "external_dimension_update_still_associative": true + } + }, + { + "name": "nested_preview_repeat_cancel_commit", + "passed": true, + "status": "passed", + "detail": { + "repeated_preview_registry_clean": true, + "cancel_restores_all_native_data": true, + "commit_is_exactly_one_undo_step": true + } + }, + { + "name": "real_native_solver_conflict_atomic_rollback", + "passed": true, + "status": "passed", + "detail": { + "real_conflicting_constraint_injected_after_geometry_creation": true, + "transaction_restores_geometry_constraints_history": true + } + }, + { + "name": "pattern_holes_real_partdesign_solid_reopen", + "passed": true, + "status": "passed", + "detail": { + "genuine_partdesign_pocket_solid": true, + "radius_and_count_regenerate_volume": true, + "reopened_volume_mm3": 57989.38070170254 + } + } + ], + "artifacts": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\mirror.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\rectangular.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\circular.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\mirror-hidden-helper-constraints.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\rectangular-hidden-helper-constraints.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\circular-hidden-helper-constraints.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\patterned-holes.FCStd" + ], + "source": "C:\\Repos\\qinventor\\src\\QInventor\\qi_sketch_patterns.py", + "success": true + } + }, + { + "name": "native_sketch_operations_dialog_suite", + "passed": true, + "result": { + "status": "passed", + "passed": 15, + "failed": 0, + "checks": [ + { + "name": "mirror: repeated Qt preview replaces copies and Cancel restores sketch/history/dirty flag", + "passed": true, + "result": { + "initial_preview": { + "circle_count": 2, + "native_geometry_count": 2, + "constraint_count": 5, + "centers_and_radii_mm": [ + [ + -7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "mirror", + "parameters": { + "axis": "vertical" + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 2 + ], + "helper_geometry_ids": [], + "constraint_names": [ + "Зеркало1Связь1", + "Зеркало1Связь2" + ], + "dimension_names": {}, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 1 + ], + "generated_ids": [ + 1 + ], + "helper_ids": [], + "all_geometry_ids": [ + 1 + ], + "constraint_ids": [ + 3, + 4 + ] + } + ], + "solver_result": 0 + }, + "revised_preview": { + "circle_count": 2, + "native_geometry_count": 2, + "constraint_count": 5, + "centers_and_radii_mm": [ + [ + 7.0, + -4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "mirror", + "parameters": { + "axis": "horizontal" + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 3 + ], + "helper_geometry_ids": [], + "constraint_names": [ + "Зеркало1Связь1", + "Зеркало1Связь2" + ], + "dimension_names": {}, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 1 + ], + "generated_ids": [ + 1 + ], + "helper_ids": [], + "all_geometry_ids": [ + 1 + ], + "constraint_ids": [ + 3, + 4 + ] + } + ], + "solver_result": 0 + }, + "russian_dialog": { + "window_title": "Зеркальное отражение", + "visible_labels": [ + "Выбрано элементов: 1", + "Связанное повторение отрезков и окружностей.", + "Ось отражения:" + ], + "untranslated_candidates": [], + "rendered_dialog": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-mirror-russian-dialog.png" + }, + "cancel_restored": true, + "undo_before": { + "undo_count": 0, + "redo_count": 0, + "undo_names": [], + "redo_names": [] + }, + "undo_after": { + "undo_count": 0, + "redo_count": 0, + "undo_names": [], + "redo_names": [] + }, + "modified_before": false, + "modified_after": false + } + }, + { + "name": "mirror: Qt Create is one undo step; saved native association regenerates after reopen", + "passed": true, + "result": { + "preview": { + "circle_count": 2, + "native_geometry_count": 2, + "constraint_count": 5, + "centers_and_radii_mm": [ + [ + -7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "mirror", + "parameters": { + "axis": "vertical" + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 2 + ], + "helper_geometry_ids": [], + "constraint_names": [ + "Зеркало1Связь1", + "Зеркало1Связь2" + ], + "dimension_names": {}, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 1 + ], + "generated_ids": [ + 1 + ], + "helper_ids": [], + "all_geometry_ids": [ + 1 + ], + "constraint_ids": [ + 3, + 4 + ] + } + ], + "solver_result": 0 + }, + "created": { + "circle_count": 2, + "native_geometry_count": 2, + "constraint_count": 5, + "centers_and_radii_mm": [ + [ + -7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "mirror", + "parameters": { + "axis": "vertical" + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 3 + ], + "helper_geometry_ids": [], + "constraint_names": [ + "Зеркало1Связь1", + "Зеркало1Связь2" + ], + "dimension_names": {}, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 1 + ], + "generated_ids": [ + 1 + ], + "helper_ids": [], + "all_geometry_ids": [ + 1 + ], + "constraint_ids": [ + 3, + 4 + ] + } + ], + "solver_result": 0 + }, + "reopened": { + "circle_count": 2, + "native_geometry_count": 2, + "constraint_count": 5, + "centers_and_radii_mm": [ + [ + -7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "mirror", + "parameters": { + "axis": "vertical" + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 3 + ], + "helper_geometry_ids": [], + "constraint_names": [ + "Зеркало1Связь1", + "Зеркало1Связь2" + ], + "dimension_names": {}, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 1 + ], + "generated_ids": [ + 1 + ], + "helper_ids": [], + "all_geometry_ids": [ + 1 + ], + "constraint_ids": [ + 3, + 4 + ] + } + ], + "solver_result": 0 + }, + "source_driving_radius_mm": 3, + "regenerated_after_reopen": { + "circle_count": 2, + "native_geometry_count": 2, + "constraint_count": 5, + "centers_and_radii_mm": [ + [ + -7.0, + 4.0, + 3.0 + ], + [ + 7.0, + 4.0, + 3.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "mirror", + "parameters": { + "axis": "vertical" + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 3 + ], + "helper_geometry_ids": [], + "constraint_names": [ + "Зеркало1Связь1", + "Зеркало1Связь2" + ], + "dimension_names": {}, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 1 + ], + "generated_ids": [ + 1 + ], + "helper_ids": [], + "all_geometry_ids": [ + 1 + ], + "constraint_ids": [ + 3, + 4 + ] + } + ], + "solver_result": 0 + }, + "saved_file": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-operation-mirror-created.FCStd", + "single_undo_step": true, + "modified_after_create": true + } + }, + { + "name": "rectangular: repeated Qt preview replaces copies and Cancel restores sketch/history/dirty flag", + "passed": true, + "result": { + "initial_preview": { + "circle_count": 6, + "native_geometry_count": 8, + "constraint_count": 23, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 19.0, + 2.0 + ], + [ + 19.0, + 4.0, + 2.0 + ], + [ + 19.0, + 19.0, + 2.0 + ], + [ + 31.0, + 4.0, + 2.0 + ], + [ + 31.0, + 19.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 3, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 4, + 5, + 6, + 7, + 8 + ], + "helper_geometry_ids": [ + 2, + 3 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ] + } + ], + "solver_result": 0 + }, + "revised_preview": { + "circle_count": 8, + "native_geometry_count": 10, + "constraint_count": 29, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 19.0, + 2.0 + ], + [ + 19.0, + 4.0, + 2.0 + ], + [ + 19.0, + 19.0, + 2.0 + ], + [ + 31.0, + 4.0, + 2.0 + ], + [ + 31.0, + 19.0, + 2.0 + ], + [ + 43.0, + 4.0, + 2.0 + ], + [ + 43.0, + 19.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 4, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ], + "helper_geometry_ids": [ + 9, + 10 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20", + "Массив1Связь21", + "Массив1Связь22", + "Массив1Связь23", + "Массив1Связь24", + "Массив1Связь25", + "Массив1Связь26" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28 + ] + } + ], + "solver_result": 0 + }, + "russian_dialog": { + "window_title": "Прямоугольный массив", + "visible_labels": [ + "Выбрано элементов: 1", + "Связанное повторение отрезков и окружностей.", + "Количество в направлении 1:", + "Шаг в направлении 1:", + "Количество в направлении 2:", + "Шаг в направлении 2:", + "Угол направления 1:", + "Количество включает исходную геометрию. Направление 2 перпендикулярно направлению 1. Отрицательный шаг меняет сторону." + ], + "untranslated_candidates": [], + "rendered_dialog": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-rectangular-russian-dialog.png" + }, + "cancel_restored": true, + "undo_before": { + "undo_count": 0, + "redo_count": 0, + "undo_names": [], + "redo_names": [] + }, + "undo_after": { + "undo_count": 0, + "redo_count": 0, + "undo_names": [], + "redo_names": [] + }, + "modified_before": false, + "modified_after": false + } + }, + { + "name": "rectangular: Qt Create is one undo step; saved native association regenerates after reopen", + "passed": true, + "result": { + "preview": { + "circle_count": 6, + "native_geometry_count": 8, + "constraint_count": 23, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 19.0, + 2.0 + ], + [ + 19.0, + 4.0, + 2.0 + ], + [ + 19.0, + 19.0, + 2.0 + ], + [ + 31.0, + 4.0, + 2.0 + ], + [ + 31.0, + 19.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 3, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 4, + 5, + 6, + 7, + 8 + ], + "helper_geometry_ids": [ + 2, + 3 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ] + } + ], + "solver_result": 0 + }, + "created": { + "circle_count": 6, + "native_geometry_count": 8, + "constraint_count": 23, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 19.0, + 2.0 + ], + [ + 19.0, + 4.0, + 2.0 + ], + [ + 19.0, + 19.0, + 2.0 + ], + [ + 31.0, + 4.0, + 2.0 + ], + [ + 31.0, + 19.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 3, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 11, + 12, + 13, + 14, + 15 + ], + "helper_geometry_ids": [ + 9, + 10 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ] + } + ], + "solver_result": 0 + }, + "reopened": { + "circle_count": 6, + "native_geometry_count": 8, + "constraint_count": 23, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 19.0, + 2.0 + ], + [ + 19.0, + 4.0, + 2.0 + ], + [ + 19.0, + 19.0, + 2.0 + ], + [ + 31.0, + 4.0, + 2.0 + ], + [ + 31.0, + 19.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 3, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 11, + 12, + 13, + 14, + 15 + ], + "helper_geometry_ids": [ + 9, + 10 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ] + } + ], + "solver_result": 0 + }, + "source_driving_radius_mm": 3, + "regenerated_after_reopen": { + "circle_count": 6, + "native_geometry_count": 8, + "constraint_count": 23, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 3.0 + ], + [ + 7.0, + 19.0, + 3.0 + ], + [ + 19.0, + 4.0, + 3.0 + ], + [ + 19.0, + 19.0, + 3.0 + ], + [ + 31.0, + 4.0, + 3.0 + ], + [ + 31.0, + 19.0, + 3.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 3, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 11, + 12, + 13, + 14, + 15 + ], + "helper_geometry_ids": [ + 9, + 10 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ] + } + ], + "solver_result": 0 + }, + "saved_file": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-operation-rectangular-created.FCStd", + "single_undo_step": true, + "modified_after_create": true + } + }, + { + "name": "circular: repeated Qt preview replaces copies and Cancel restores sketch/history/dirty flag", + "passed": true, + "result": { + "initial_preview": { + "circle_count": 4, + "native_geometry_count": 9, + "constraint_count": 22, + "centers_and_radii_mm": [ + [ + -7.0, + -4.0, + 2.0 + ], + [ + -4.0, + 7.0, + 2.0 + ], + [ + 4.0, + -7.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 4, + "center": [ + 0.0, + 0.0 + ], + "angle": 360.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 4, + 6, + 8 + ], + "helper_geometry_ids": [ + 2, + 3, + 5, + 7, + 9 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7 + ], + "generated_ids": [ + 3, + 5, + 7 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21 + ] + } + ], + "solver_result": 0 + }, + "revised_preview": { + "circle_count": 6, + "native_geometry_count": 13, + "constraint_count": 32, + "centers_and_radii_mm": [ + [ + -8.01426, + 0.878429, + 2.0 + ], + [ + -7.0, + -4.0, + 2.0 + ], + [ + -5.967345, + 5.421328, + 2.0 + ], + [ + -1.641107, + 7.893464, + 2.0 + ], + [ + 3.311978, + 7.350565, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 6, + "center": [ + 0.0, + 0.0 + ], + "angle": 180.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 12, + 14, + 16, + 18, + 20 + ], + "helper_geometry_ids": [ + 10, + 11, + 13, + 15, + 17, + 19, + 21 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19", + "КруговойМассив1Связь20", + "КруговойМассив1Связь21", + "КруговойМассив1Связь22", + "КруговойМассив1Связь23", + "КруговойМассив1Связь24", + "КруговойМассив1Связь25", + "КруговойМассив1Связь26", + "КруговойМассив1Связь27", + "КруговойМассив1Связь28", + "КруговойМассив1Связь29" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7, + 9, + 11 + ], + "generated_ids": [ + 3, + 5, + 7, + 9, + 11 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8, + 10, + 12 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31 + ] + } + ], + "solver_result": 0 + }, + "russian_dialog": { + "window_title": "Круговой массив", + "visible_labels": [ + "Выбрано элементов: 1", + "Связанное повторение отрезков и окружностей.", + "Количество элементов:", + "Центр — X:", + "Центр — Y:", + "Общий угол:", + "Количество включает исходную геометрию. При 360° копии распределяются по всей окружности; при другом угле — между концами сектора." + ], + "untranslated_candidates": [], + "rendered_dialog": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-circular-russian-dialog.png" + }, + "cancel_restored": true, + "undo_before": { + "undo_count": 0, + "redo_count": 0, + "undo_names": [], + "redo_names": [] + }, + "undo_after": { + "undo_count": 0, + "redo_count": 0, + "undo_names": [], + "redo_names": [] + }, + "modified_before": false, + "modified_after": false + } + }, + { + "name": "circular: Qt Create is one undo step; saved native association regenerates after reopen", + "passed": true, + "result": { + "preview": { + "circle_count": 4, + "native_geometry_count": 9, + "constraint_count": 22, + "centers_and_radii_mm": [ + [ + -7.0, + -4.0, + 2.0 + ], + [ + -4.0, + 7.0, + 2.0 + ], + [ + 4.0, + -7.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 4, + "center": [ + 0.0, + 0.0 + ], + "angle": 360.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 4, + 6, + 8 + ], + "helper_geometry_ids": [ + 2, + 3, + 5, + 7, + 9 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7 + ], + "generated_ids": [ + 3, + 5, + 7 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21 + ] + } + ], + "solver_result": 0 + }, + "created": { + "circle_count": 4, + "native_geometry_count": 9, + "constraint_count": 22, + "centers_and_radii_mm": [ + [ + -7.0, + -4.0, + 2.0 + ], + [ + -4.0, + 7.0, + 2.0 + ], + [ + 4.0, + -7.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 4, + "center": [ + 0.0, + 0.0 + ], + "angle": 360.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 12, + 14, + 16 + ], + "helper_geometry_ids": [ + 10, + 11, + 13, + 15, + 17 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7 + ], + "generated_ids": [ + 3, + 5, + 7 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21 + ] + } + ], + "solver_result": 0 + }, + "reopened": { + "circle_count": 4, + "native_geometry_count": 9, + "constraint_count": 22, + "centers_and_radii_mm": [ + [ + -7.0, + -4.0, + 2.0 + ], + [ + -4.0, + 7.0, + 2.0 + ], + [ + 4.0, + -7.0, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 4, + "center": [ + 0.0, + 0.0 + ], + "angle": 360.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 12, + 14, + 16 + ], + "helper_geometry_ids": [ + 10, + 11, + 13, + 15, + 17 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7 + ], + "generated_ids": [ + 3, + 5, + 7 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21 + ] + } + ], + "solver_result": 0 + }, + "source_driving_radius_mm": 3, + "regenerated_after_reopen": { + "circle_count": 4, + "native_geometry_count": 9, + "constraint_count": 22, + "centers_and_radii_mm": [ + [ + -7.0, + -4.0, + 3.0 + ], + [ + -4.0, + 7.0, + 3.0 + ], + [ + 4.0, + -7.0, + 3.0 + ], + [ + 7.0, + 4.0, + 3.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 4, + "center": [ + 0.0, + 0.0 + ], + "angle": 360.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 12, + 14, + 16 + ], + "helper_geometry_ids": [ + 10, + 11, + 13, + 15, + 17 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7 + ], + "generated_ids": [ + 3, + 5, + 7 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21 + ] + } + ], + "solver_result": 0 + }, + "saved_file": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-operation-circular-created.FCStd", + "single_undo_step": true, + "modified_after_create": true + } + }, + { + "name": "Offset: repeated circle preview replaces geometry; Cancel restores constraints/history/dirty flag", + "passed": true, + "result": { + "initial_preview": { + "circle_count": 2, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 4.0 + ] + ], + "constraint_types": [ + "DistanceX", + "DistanceY", + "Radius", + "Coincident", + "Distance" + ], + "solver_result": 0 + }, + "revised_preview": { + "circle_count": 2, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 6.0 + ] + ], + "constraint_types": [ + "DistanceX", + "DistanceY", + "Radius", + "Coincident", + "Distance" + ], + "solver_result": 0 + }, + "russian_dialog": { + "window_title": "Смещение геометрии", + "visible_labels": [ + "Выбрано элементов: 1", + "Расстояние смещения:", + "Поддерживаются окружность, отдельный отрезок и выпуклый замкнутый контур из отрезков. Положительное расстояние расширяет окружность и контур. Для отдельного отрезка оно задаёт левую сторону по направлению от начала к концу; отрицательное — противоположную." + ], + "untranslated_candidates": [], + "rendered_dialog": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-offset-russian-dialog.png" + }, + "cancel_restored": true, + "undo_before": { + "undo_count": 0, + "redo_count": 0, + "undo_names": [], + "redo_names": [] + }, + "undo_after": { + "undo_count": 0, + "redo_count": 0, + "undo_names": [], + "redo_names": [] + } + } + }, + { + "name": "Offset: Qt Create saves native distance association; source radius regenerates after reopen", + "passed": true, + "result": { + "preview": { + "circle_count": 2, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 4.0 + ] + ], + "constraint_types": [ + "DistanceX", + "DistanceY", + "Radius", + "Coincident", + "Distance" + ], + "solver_result": 0 + }, + "created": { + "circle_count": 2, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 4.0 + ] + ], + "constraint_types": [ + "DistanceX", + "DistanceY", + "Radius", + "Coincident", + "Distance" + ], + "solver_result": 0 + }, + "reopened": { + "circle_count": 2, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 4.0, + 4.0 + ] + ], + "constraint_types": [ + "DistanceX", + "DistanceY", + "Radius", + "Coincident", + "Distance" + ], + "solver_result": 0 + }, + "regenerated_after_reopen": { + "circle_count": 2, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 3.0 + ], + [ + 7.0, + 4.0, + 5.0 + ] + ], + "constraint_types": [ + "DistanceX", + "DistanceY", + "Radius", + "Coincident", + "Distance" + ], + "solver_result": 0 + }, + "saved_file": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-operation-offset-created.FCStd", + "single_undo_step": true, + "modified_after_create": true + } + }, + { + "name": "Invalid repeated preview shows a Russian error and rolls back geometry/history", + "passed": true, + "result": { + "russian_error": "Шаг прямоугольного массива должен быть ненулевым. Для обратного направления задайте отрицательный шаг.", + "rollback_restored": true, + "modified_before": false, + "modified_after_error": false + } + }, + { + "name": "Qt Edit Pattern preserves association ID; cancel and undo restore previous pattern", + "passed": true, + "result": { + "cancelled_edit_preview": { + "circle_count": 8, + "native_geometry_count": 10, + "constraint_count": 29, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 19.0, + 2.0 + ], + [ + 19.0, + 4.0, + 2.0 + ], + [ + 19.0, + 19.0, + 2.0 + ], + [ + 31.0, + 4.0, + 2.0 + ], + [ + 31.0, + 19.0, + 2.0 + ], + [ + 43.0, + 4.0, + 2.0 + ], + [ + 43.0, + 19.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 4, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ], + "helper_geometry_ids": [ + 9, + 10 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20", + "Массив1Связь21", + "Массив1Связь22", + "Массив1Связь23", + "Массив1Связь24", + "Массив1Связь25", + "Массив1Связь26" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28 + ] + } + ], + "solver_result": 0 + }, + "committed_edit": { + "circle_count": 8, + "native_geometry_count": 10, + "constraint_count": 29, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 19.0, + 2.0 + ], + [ + 19.0, + 4.0, + 2.0 + ], + [ + 19.0, + 19.0, + 2.0 + ], + [ + 31.0, + 4.0, + 2.0 + ], + [ + 31.0, + 19.0, + 2.0 + ], + [ + 43.0, + 4.0, + 2.0 + ], + [ + 43.0, + 19.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 4, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ], + "helper_geometry_ids": [ + 18, + 19 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20", + "Массив1Связь21", + "Массив1Связь22", + "Массив1Связь23", + "Массив1Связь24", + "Массив1Связь25", + "Массив1Связь26" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28 + ] + } + ], + "solver_result": 0 + }, + "pattern_id_preserved": "p1", + "cancel_and_undo_restored": true + } + }, + { + "name": "Detached native preview preserves existing Redo; cancel removes overlay and Ctrl+Y restores source radius", + "passed": true, + "result": { + "preview": { + "circle_count": 2, + "native_geometry_count": 2, + "constraint_count": 5, + "centers_and_radii_mm": [ + [ + -7.0, + 4.0, + 5.0 + ], + [ + 7.0, + 4.0, + 5.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "mirror", + "parameters": { + "axis": "vertical" + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 2 + ], + "helper_geometry_ids": [], + "constraint_names": [ + "Зеркало1Связь1", + "Зеркало1Связь2" + ], + "dimension_names": {}, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 1 + ], + "generated_ids": [ + 1 + ], + "helper_ids": [], + "all_geometry_ids": [ + 1 + ], + "constraint_ids": [ + 3, + 4 + ] + } + ], + "solver_result": 0 + }, + "temporary_document": "QIPreviewa4747a12e1df496db7ef15a278aadcae", + "temporary_document_removed": true, + "overlay_removed": true, + "source_global_placement": "Placement [Pos=(35.9826,-11.9001,17), Yaw-Pitch-Roll=(25,20,0)]", + "native_edit_transform": "Matrix ((1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1))", + "model_global_mirror_center_mm": [ + 28.33057928663428, + -11.054818230587575, + 19.39414100327968 + ], + "overlay_world_center_mm": [ + -7.0, + 4.0, + 0.0 + ], + "overlay_sample_count": 128, + "maximum_radius_error_mm": 5.766050561462066E-07, + "maximum_plane_error_mm": 0.0, + "history_before": { + "undo_count": 1, + "redo_count": 1, + "undo_names": [ + "Радиус исходной окружности — 5 мм" + ], + "redo_names": [ + "Радиус исходной окружности — 6 мм" + ] + }, + "history_after_redo": { + "undo_count": 2, + "redo_count": 0, + "undo_names": [ + "Радиус исходной окружности — 6 мм", + "Радиус исходной окружности — 5 мм" + ], + "redo_names": [] + }, + "modified_before": true, + "radius_after_native_redo_mm": 6.0, + "redo_delivery": "native Std_Redo keyboard shortcut Ctrl+Y", + "native_shortcuts": [ + "Ctrl+Y" + ] + } + }, + { + "name": "Detached offset retains placed SubShapeBinder context; cancel preserves Redo and native Ctrl+Y updates projection", + "passed": true, + "result": { + "original_local_endpoints_mm": [ + [ + 8.365163037378078, + 7.417819582470548, + 0.0 + ], + [ + 5.776972586352871, + 17.07707784536123, + 0.0 + ] + ], + "original_external_endpoints_mm": [ + [ + 8.365163037378078, + 7.417819582470548, + 0.0 + ], + [ + 5.776972586352871, + 17.07707784536123, + 0.0 + ] + ], + "shadow_before_native_operation": { + "local": [ + [ + 8.365163037378078, + 7.417819582470548, + 0.0 + ], + [ + 5.776972586352871, + 17.07707784536123, + 0.0 + ] + ], + "external": [ + [ + 8.365163037378078, + 7.417819582470548, + 0.0 + ], + [ + 5.776972586352871, + 17.07707784536123, + 0.0 + ] + ], + "geometry_count": 1, + "context_objects": [ + "X_Axis", + "Y_Axis", + "Z_Axis", + "XY_Plane", + "XZ_Plane", + "YZ_Plane", + "Origin001", + "Origin", + "SourceBox", + "SourceComponent", + "X_Axis001", + "Y_Axis001", + "Z_Axis001", + "XY_Plane001", + "XZ_Plane001", + "YZ_Plane001", + "Origin003", + "Origin002", + "X_Axis002", + "Y_Axis002", + "Z_Axis002", + "XY_Plane002", + "XZ_Plane002", + "YZ_Plane002", + "Origin005", + "Origin004", + "Binder", + "Sketch", + "Body", + "SketchComponent" + ] + }, + "expected_offset_local_endpoints_mm": [ + [ + 6.433311384799942, + 6.9001814922655065, + 0.0 + ], + [ + 3.8451209337747345, + 16.55943975515619, + 0.0 + ] + ], + "actual_offset_local_endpoints_mm": [ + [ + 6.433311384799942, + 6.9001814922655065, + 0.0 + ], + [ + 3.8451209337747345, + 16.55943975515619, + 0.0 + ] + ], + "expected_offset_world_endpoints_mm": [ + [ + -2.0000000000000018, + -1.7763568394002505E-15, + 0.0 + ], + [ + -2.0, + 10.0, + 0.0 + ] + ], + "expected_native_edit_endpoints_mm": [ + [ + 6.433311384799942, + 6.9001814922655065, + 0.0 + ], + [ + 3.8451209337747345, + 16.55943975515619, + 0.0 + ] + ], + "actual_overlay_native_edit_endpoints_mm": [ + [ + 6.433311462402344, + 6.900181293487549, + 0.0 + ], + [ + 3.845120906829834, + 16.55944061279297, + 0.0 + ] + ], + "maximum_overlay_endpoint_error_mm": 8.580599454290849E-07, + "detached_framebuffer": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-linked-binder-detached-framebuffer.png", + "overlay_midpoint_pixel": [ + 597, + 322 + ], + "orange_pixels_at_expected_offset_midpoint": [ + { + "position": [ + 594, + 317 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 595, + 317 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 317 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 594, + 318 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 595, + 318 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 318 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 594, + 319 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 595, + 319 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 319 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 595, + 320 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 320 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 597, + 320 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 595, + 321 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 321 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 597, + 321 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 595, + 322 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 322 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 597, + 322 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 595, + 323 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 323 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 597, + 323 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 324 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 597, + 324 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 598, + 324 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 325 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 597, + 325 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 598, + 325 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 596, + 326 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 597, + 326 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 598, + 326 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 597, + 327 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 598, + 327 + ], + "rgb": [ + 245, + 120, + 26 + ] + }, + { + "position": [ + 599, + 327 + ], + "rgb": [ + 245, + 120, + 26 + ] + } + ], + "native_edit_transform": "Matrix ((1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1))", + "source_global_placement": "Placement [Pos=(-10,-5,0), Yaw-Pitch-Roll=(-15,-0,0)]", + "preview_result": { + "source_ids": [ + 0 + ], + "created_ids": [ + 1 + ], + "helper_ids": [ + 2, + 3 + ], + "constraint_ids": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "dimension_ids": [ + 9 + ], + "distance": 2.0, + "loop_select": false, + "constrain": true, + "kind": "line" + }, + "russian_dialog": { + "window_title": "Смещение геометрии", + "visible_labels": [ + "Выбрано элементов: 1", + "Расстояние смещения:", + "Поддерживаются окружность, отдельный отрезок и выпуклый замкнутый контур из отрезков. Положительное расстояние расширяет окружность и контур. Для отдельного отрезка оно задаёт левую сторону по направлению от начала к концу; отрицательное — противоположную." + ], + "untranslated_candidates": [], + "rendered_dialog": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-linked-binder-offset-dialog.png" + }, + "temporary_document": "QIPreview869b13260d124d47b57d7947634114cf", + "cancel_preserved_geometry_constraints_history_dirty_flag": true, + "history_before": { + "undo_count": 0, + "redo_count": 1, + "undo_names": [], + "redo_names": [ + "Изменить ширину связанного основания" + ] + }, + "history_after_native_redo": { + "undo_count": 1, + "redo_count": 0, + "undo_names": [ + "Изменить ширину связанного основания" + ], + "redo_names": [] + }, + "source_width_after_native_redo_mm": 12.0, + "linked_line_length_after_native_redo_mm": 12.000000000000002, + "actual_native_offset_framebuffer": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-linked-binder-actual-offset-framebuffer.png", + "actual_native_offset_local_endpoints_mm": [ + [ + 6.433311384799942, + 6.9001814922655065, + 0.0 + ], + [ + 3.8451209337747345, + 16.55943975515619, + 0.0 + ] + ], + "native_offset_midpoint_pixel": [ + 597, + 322 + ], + "green_pixels_at_native_offset_midpoint": [ + { + "position": [ + 594, + 317 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 595, + 317 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 317 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 594, + 318 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 595, + 318 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 318 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 594, + 319 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 595, + 319 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 319 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 595, + 320 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 320 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 597, + 320 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 595, + 321 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 321 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 597, + 321 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 595, + 322 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 322 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 597, + 322 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 595, + 323 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 323 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 597, + 323 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 324 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 597, + 324 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 598, + 324 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 325 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 597, + 325 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 598, + 325 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 596, + 326 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 597, + 326 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 598, + 326 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 597, + 327 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 598, + 327 + ], + "rgb": [ + 47, + 158, + 68 + ] + }, + { + "position": [ + 599, + 327 + ], + "rgb": [ + 47, + 158, + 68 + ] + } + ], + "identical_native_camera": { + "position": [ + 12.591839790344238, + 8.626729011535645, + 24.723207473754883 + ], + "orientation": [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + "height": 41.446414947509766 + } + } + }, + { + "name": "rectangular: existing Point helpers survive detached Edit preview; Cancel preserves registry/formulas/Redo", + "passed": true, + "result": { + "shadow_before_set_parameters": { + "signature": { + "geometry": [ + [ + "Circle", + [ + 7.0, + 4.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "LineSegment", + [ + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0 + ] + ], + [ + "Point", + [ + 12.0, + 15.0, + 0.0, + 12.0, + 15.0, + 0.0 + ] + ], + [ + "Circle", + [ + 19.0, + 4.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "Circle", + [ + 31.0, + 4.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "Circle", + [ + 7.0, + 19.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "Circle", + [ + 19.0, + 19.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "Circle", + [ + 31.0, + 19.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ] + ], + "constraints": [ + [ + "DistanceX", + 0, + 3, + -2000, + 0, + -2000, + 0, + 7.0, + "ЦентрX" + ], + [ + "DistanceY", + 0, + 3, + -2000, + 0, + -2000, + 0, + 4.0, + "ЦентрY" + ], + [ + "Radius", + 0, + 0, + -2000, + 0, + -2000, + 0, + 2.0, + "Радиус" + ], + [ + "Coincident", + 1, + 1, + -1, + 1, + -2000, + 0, + 0.0, + "Массив1Связь1" + ], + [ + "Distance", + 1, + 0, + -2000, + 0, + -2000, + 0, + 1.0, + "Массив1Связь2" + ], + [ + "Angle", + 1, + 0, + -2000, + 0, + -2000, + 0, + 0.0, + "Массив1Направление" + ], + [ + "DistanceX", + 2, + 1, + -2000, + 0, + -2000, + 0, + 12.0, + "Массив1ШагX" + ], + [ + "DistanceY", + 2, + 1, + -2000, + 0, + -2000, + 0, + 15.0, + "Массив1ШагY" + ], + [ + "Equal", + 0, + 0, + 3, + 0, + -2000, + 0, + 0.0, + "Массив1Связь6" + ], + [ + "DistanceX", + 0, + 3, + 3, + 3, + -2000, + 0, + 12.0, + "Массив1Связь7" + ], + [ + "DistanceY", + 0, + 3, + 3, + 3, + -2000, + 0, + 0.0, + "Массив1Связь8" + ], + [ + "Equal", + 0, + 0, + 4, + 0, + -2000, + 0, + 0.0, + "Массив1Связь9" + ], + [ + "DistanceX", + 0, + 3, + 4, + 3, + -2000, + 0, + 24.0, + "Массив1Связь10" + ], + [ + "DistanceY", + 0, + 3, + 4, + 3, + -2000, + 0, + 0.0, + "Массив1Связь11" + ], + [ + "Equal", + 0, + 0, + 5, + 0, + -2000, + 0, + 0.0, + "Массив1Связь12" + ], + [ + "DistanceX", + 0, + 3, + 5, + 3, + -2000, + 0, + 0.0, + "Массив1Связь13" + ], + [ + "DistanceY", + 0, + 3, + 5, + 3, + -2000, + 0, + 15.0, + "Массив1Связь14" + ], + [ + "Equal", + 0, + 0, + 6, + 0, + -2000, + 0, + 0.0, + "Массив1Связь15" + ], + [ + "DistanceX", + 0, + 3, + 6, + 3, + -2000, + 0, + 12.0, + "Массив1Связь16" + ], + [ + "DistanceY", + 0, + 3, + 6, + 3, + -2000, + 0, + 15.0, + "Массив1Связь17" + ], + [ + "Equal", + 0, + 0, + 7, + 0, + -2000, + 0, + 0.0, + "Массив1Связь18" + ], + [ + "DistanceX", + 0, + 3, + 7, + 3, + -2000, + 0, + 24.0, + "Массив1Связь19" + ], + [ + "DistanceY", + 0, + 3, + 7, + 3, + -2000, + 0, + 15.0, + "Массив1Связь20" + ] + ], + "datum_label_positions": [ + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 24.347156524658203, + 0.0 + ], + [ + 3.0, + -13.0 + ], + [ + 38.0, + -4.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ] + ], + "construction": [ + false, + true, + true, + false, + false, + false, + false, + false + ], + "property_names": [ + "ArcFitTolerance", + "AttacherEngine", + "AttacherType", + "AttachmentOffset", + "AttachmentSupport", + "Constraints", + "Exports", + "ExpressionEngine", + "ExternalGeo", + "ExternalGeometry", + "ExternalTypes", + "FullyConstrained", + "Geometry", + "InternalShape", + "Label", + "Label2", + "MakeInternals", + "MapMode", + "MapPathParameter", + "MapReversed", + "Placement", + "QISketchPatterns", + "Shape", + "ShapeMaterial", + "Visibility", + "_ElementMapVersion", + "_ExternalGeoVersion" + ], + "patterns": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 3, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 4, + 5, + 6, + 7, + 8 + ], + "helper_geometry_ids": [ + 2, + 3 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ] + } + ] + }, + "registry": "{\"version\": 1, \"patterns\": [{\"id\": \"p1\", \"kind\": \"rectangular\", \"parameters\": {\"count_x\": 3, \"count_y\": 2, \"spacing_x\": 12.0, \"spacing_y\": 15.0, \"angle\": 0.0}, \"source_geometry_ids\": [1], \"created_geometry_ids\": [4, 5, 6, 7, 8], \"helper_geometry_ids\": [2, 3], \"constraint_names\": [\"Массив1Связь1\", \"Массив1Связь2\", \"Массив1Направление\", \"Массив1ШагX\", \"Массив1ШагY\", \"Массив1Связь6\", \"Массив1Связь7\", \"Массив1Связь8\", \"Массив1Связь9\", \"Массив1Связь10\", \"Массив1Связь11\", \"Массив1Связь12\", \"Массив1Связь13\", \"Массив1Связь14\", \"Массив1Связь15\", \"Массив1Связь16\", \"Массив1Связь17\", \"Массив1Связь18\", \"Массив1Связь19\", \"Массив1Связь20\"], \"dimension_names\": {\"Направление\": \"Массив1Направление\", \"ШагX\": \"Массив1ШагX\", \"ШагY\": \"Массив1ШагY\"}, \"associative\": true}]}", + "formulas": [ + [ + ".Constraints.Массив1Связь10", + "2 * .Constraints.Массив1ШагX * cos(.Constraints.Массив1Направление) - 0 * .Constraints.Массив1ШагY * sin(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь11", + "2 * .Constraints.Массив1ШагX * sin(.Constraints.Массив1Направление) + 0 * .Constraints.Массив1ШагY * cos(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь13", + "0 * .Constraints.Массив1ШагX * cos(.Constraints.Массив1Направление) - 1 * .Constraints.Массив1ШагY * sin(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь14", + "0 * .Constraints.Массив1ШагX * sin(.Constraints.Массив1Направление) + 1 * .Constraints.Массив1ШагY * cos(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь16", + "1 * .Constraints.Массив1ШагX * cos(.Constraints.Массив1Направление) - 1 * .Constraints.Массив1ШагY * sin(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь17", + "1 * .Constraints.Массив1ШагX * sin(.Constraints.Массив1Направление) + 1 * .Constraints.Массив1ШагY * cos(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь19", + "2 * .Constraints.Массив1ШагX * cos(.Constraints.Массив1Направление) - 1 * .Constraints.Массив1ШагY * sin(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь2", + "1 mm" + ], + [ + ".Constraints.Массив1Связь20", + "2 * .Constraints.Массив1ШагX * sin(.Constraints.Массив1Направление) + 1 * .Constraints.Массив1ШагY * cos(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь7", + "1 * .Constraints.Массив1ШагX * cos(.Constraints.Массив1Направление) - 0 * .Constraints.Массив1ШагY * sin(.Constraints.Массив1Направление)" + ], + [ + ".Constraints.Массив1Связь8", + "1 * .Constraints.Массив1ШагX * sin(.Constraints.Массив1Направление) + 0 * .Constraints.Массив1ШагY * cos(.Constraints.Массив1Направление)" + ] + ], + "point_count": 1 + }, + "revised_native_preview": { + "circle_count": 8, + "native_geometry_count": 10, + "constraint_count": 29, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 2.0 + ], + [ + 7.0, + 19.0, + 2.0 + ], + [ + 19.0, + 4.0, + 2.0 + ], + [ + 19.0, + 19.0, + 2.0 + ], + [ + 31.0, + 4.0, + 2.0 + ], + [ + 31.0, + 19.0, + 2.0 + ], + [ + 43.0, + 4.0, + 2.0 + ], + [ + 43.0, + 19.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 4, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ], + "helper_geometry_ids": [ + 9, + 10 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20", + "Массив1Связь21", + "Массив1Связь22", + "Массив1Связь23", + "Массив1Связь24", + "Массив1Связь25", + "Массив1Связь26" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28 + ] + } + ], + "solver_result": 0 + }, + "temporary_document": "QIPreview4ed32be399f7421c87145d667488634e", + "original_registry_preserved": true, + "original_formulas_preserved": true, + "source_datum_label_positions_before": [ + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 24.347156524658203, + 0.0 + ], + [ + 3.0, + -13.0 + ], + [ + 38.0, + -4.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ] + ], + "rendered_dialog": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-rectangular-existing-pattern-edit-dialog.png", + "cancel_preserved_native_data_history_dirty_flag": true, + "source_datum_label_positions_after_cancel": [ + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 24.347156524658203, + 0.0 + ], + [ + 3.0, + -13.0 + ], + [ + 38.0, + -4.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ] + ], + "history_before": { + "undo_count": 1, + "redo_count": 1, + "undo_names": [ + "Прямоугольный массив" + ], + "redo_names": [ + "Изменить радиус исходной окружности массива" + ] + }, + "history_after_native_redo": { + "undo_count": 2, + "redo_count": 0, + "undo_names": [ + "Изменить радиус исходной окружности массива", + "Прямоугольный массив" + ], + "redo_names": [] + }, + "source_radius_after_native_redo_mm": 3.0, + "regenerated_pattern_after_native_redo": { + "circle_count": 6, + "native_geometry_count": 8, + "constraint_count": 23, + "centers_and_radii_mm": [ + [ + 7.0, + 4.0, + 3.0 + ], + [ + 7.0, + 19.0, + 3.0 + ], + [ + 19.0, + 4.0, + 3.0 + ], + [ + 19.0, + 19.0, + 3.0 + ], + [ + 31.0, + 4.0, + 3.0 + ], + [ + 31.0, + 19.0, + 3.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "rectangular", + "parameters": { + "count_x": 3, + "count_y": 2, + "spacing_x": 12.0, + "spacing_y": 15.0, + "angle": 0.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 4, + 5, + 6, + 7, + 8 + ], + "helper_geometry_ids": [ + 2, + 3 + ], + "constraint_names": [ + "Массив1Связь1", + "Массив1Связь2", + "Массив1Направление", + "Массив1ШагX", + "Массив1ШагY", + "Массив1Связь6", + "Массив1Связь7", + "Массив1Связь8", + "Массив1Связь9", + "Массив1Связь10", + "Массив1Связь11", + "Массив1Связь12", + "Массив1Связь13", + "Массив1Связь14", + "Массив1Связь15", + "Массив1Связь16", + "Массив1Связь17", + "Массив1Связь18", + "Массив1Связь19", + "Массив1Связь20" + ], + "dimension_names": { + "Направление": "Массив1Направление", + "ШагX": "Массив1ШагX", + "ШагY": "Массив1ШагY" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "generated_ids": [ + 3, + 4, + 5, + 6, + 7 + ], + "helper_ids": [ + 1, + 2 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ] + } + ], + "solver_result": 0 + } + } + }, + { + "name": "circular: existing Point helpers survive detached Edit preview; Cancel preserves registry/formulas/Redo", + "passed": true, + "result": { + "shadow_before_set_parameters": { + "signature": { + "geometry": [ + [ + "Circle", + [ + 7.0, + 4.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "Point", + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + ], + [ + "LineSegment", + [ + 0.0, + 0.0, + 0.0 + ], + [ + 7.0, + 4.0, + 0.0 + ] + ], + [ + "Circle", + [ + -4.0, + 7.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "LineSegment", + [ + 0.0, + 0.0, + 0.0 + ], + [ + -4.0, + 7.0, + 0.0 + ] + ], + [ + "Circle", + [ + -7.0, + -4.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "LineSegment", + [ + 0.0, + 0.0, + 0.0 + ], + [ + -7.0, + -4.0, + 0.0 + ] + ], + [ + "Circle", + [ + 4.0, + -7.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ], + 2.0 + ], + [ + "LineSegment", + [ + 0.0, + 0.0, + 0.0 + ], + [ + 4.0, + -7.0, + 0.0 + ] + ] + ], + "constraints": [ + [ + "DistanceX", + 0, + 3, + -2000, + 0, + -2000, + 0, + 7.0, + "ЦентрX" + ], + [ + "DistanceY", + 0, + 3, + -2000, + 0, + -2000, + 0, + 4.0, + "ЦентрY" + ], + [ + "Radius", + 0, + 0, + -2000, + 0, + -2000, + 0, + 2.0, + "Радиус" + ], + [ + "DistanceX", + 1, + 1, + -2000, + 0, + -2000, + 0, + 0.0, + "КруговойМассив1ЦентрX" + ], + [ + "DistanceY", + 1, + 1, + -2000, + 0, + -2000, + 0, + 0.0, + "КруговойМассив1ЦентрY" + ], + [ + "Coincident", + 2, + 1, + 1, + 1, + -2000, + 0, + 0.0, + "КруговойМассив1Связь3" + ], + [ + "Coincident", + 2, + 2, + 0, + 3, + -2000, + 0, + 0.0, + "КруговойМассив1Связь4" + ], + [ + "Equal", + 0, + 0, + 3, + 0, + -2000, + 0, + 0.0, + "КруговойМассив1Связь5" + ], + [ + "Coincident", + 4, + 1, + 1, + 1, + -2000, + 0, + 0.0, + "КруговойМассив1Связь6" + ], + [ + "Coincident", + 4, + 2, + 3, + 3, + -2000, + 0, + 0.0, + "КруговойМассив1Связь7" + ], + [ + "Equal", + 2, + 0, + 4, + 0, + -2000, + 0, + 0.0, + "КруговойМассив1Связь8" + ], + [ + "Angle", + 2, + 0, + 4, + 0, + -2000, + 0, + 1.570796327, + "КруговойМассив1УголШага" + ], + [ + "Equal", + 0, + 0, + 5, + 0, + -2000, + 0, + 0.0, + "КруговойМассив1Связь10" + ], + [ + "Coincident", + 6, + 1, + 1, + 1, + -2000, + 0, + 0.0, + "КруговойМассив1Связь11" + ], + [ + "Coincident", + 6, + 2, + 5, + 3, + -2000, + 0, + 0.0, + "КруговойМассив1Связь12" + ], + [ + "Equal", + 2, + 0, + 6, + 0, + -2000, + 0, + 0.0, + "КруговойМассив1Связь13" + ], + [ + "Angle", + 2, + 0, + 6, + 0, + -2000, + 0, + 3.141592654, + "КруговойМассив1Связь14" + ], + [ + "Equal", + 0, + 0, + 7, + 0, + -2000, + 0, + 0.0, + "КруговойМассив1Связь15" + ], + [ + "Coincident", + 8, + 1, + 1, + 1, + -2000, + 0, + 0.0, + "КруговойМассив1Связь16" + ], + [ + "Coincident", + 8, + 2, + 7, + 3, + -2000, + 0, + 0.0, + "КруговойМассив1Связь17" + ], + [ + "Equal", + 2, + 0, + 8, + 0, + -2000, + 0, + 0.0, + "КруговойМассив1Связь18" + ], + [ + "Angle", + 2, + 0, + 8, + 0, + -2000, + 0, + 4.71238898, + "КруговойМассив1Связь19" + ] + ], + "datum_label_positions": [ + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 14.0, + 0.0 + ], + [ + 14.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 11.363961219787598, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ] + ], + "construction": [ + false, + true, + true, + false, + true, + false, + true, + false, + true + ], + "property_names": [ + "ArcFitTolerance", + "AttacherEngine", + "AttacherType", + "AttachmentOffset", + "AttachmentSupport", + "Constraints", + "Exports", + "ExpressionEngine", + "ExternalGeo", + "ExternalGeometry", + "ExternalTypes", + "FullyConstrained", + "Geometry", + "InternalShape", + "Label", + "Label2", + "MakeInternals", + "MapMode", + "MapPathParameter", + "MapReversed", + "Placement", + "QISketchPatterns", + "Shape", + "ShapeMaterial", + "Visibility", + "_ElementMapVersion", + "_ExternalGeoVersion" + ], + "patterns": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 4, + "center": [ + 0.0, + 0.0 + ], + "angle": 360.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 4, + 6, + 8 + ], + "helper_geometry_ids": [ + 2, + 3, + 5, + 7, + 9 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7 + ], + "generated_ids": [ + 3, + 5, + 7 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21 + ] + } + ] + }, + "registry": "{\"version\": 1, \"patterns\": [{\"id\": \"p1\", \"kind\": \"circular\", \"parameters\": {\"count\": 4, \"center\": [0.0, 0.0], \"angle\": 360.0}, \"source_geometry_ids\": [1], \"created_geometry_ids\": [4, 6, 8], \"helper_geometry_ids\": [2, 3, 5, 7, 9], \"constraint_names\": [\"КруговойМассив1ЦентрX\", \"КруговойМассив1ЦентрY\", \"КруговойМассив1Связь3\", \"КруговойМассив1Связь4\", \"КруговойМассив1Связь5\", \"КруговойМассив1Связь6\", \"КруговойМассив1Связь7\", \"КруговойМассив1Связь8\", \"КруговойМассив1УголШага\", \"КруговойМассив1Связь10\", \"КруговойМассив1Связь11\", \"КруговойМассив1Связь12\", \"КруговойМассив1Связь13\", \"КруговойМассив1Связь14\", \"КруговойМассив1Связь15\", \"КруговойМассив1Связь16\", \"КруговойМассив1Связь17\", \"КруговойМассив1Связь18\", \"КруговойМассив1Связь19\"], \"dimension_names\": {\"ЦентрX\": \"КруговойМассив1ЦентрX\", \"ЦентрY\": \"КруговойМассив1ЦентрY\", \"УголШага\": \"КруговойМассив1УголШага\"}, \"associative\": true}]}", + "formulas": [ + [ + ".Constraints.КруговойМассив1Связь14", + "2 * .Constraints.КруговойМассив1УголШага" + ], + [ + ".Constraints.КруговойМассив1Связь19", + "3 * .Constraints.КруговойМассив1УголШага" + ] + ], + "point_count": 1 + }, + "revised_native_preview": { + "circle_count": 6, + "native_geometry_count": 13, + "constraint_count": 32, + "centers_and_radii_mm": [ + [ + -8.01426, + 0.878429, + 2.0 + ], + [ + -7.0, + -4.0, + 2.0 + ], + [ + -5.967345, + 5.421328, + 2.0 + ], + [ + -1.641107, + 7.893464, + 2.0 + ], + [ + 3.311978, + 7.350565, + 2.0 + ], + [ + 7.0, + 4.0, + 2.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 6, + "center": [ + 0.0, + 0.0 + ], + "angle": 180.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 12, + 14, + 16, + 18, + 20 + ], + "helper_geometry_ids": [ + 10, + 11, + 13, + 15, + 17, + 19, + 21 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19", + "КруговойМассив1Связь20", + "КруговойМассив1Связь21", + "КруговойМассив1Связь22", + "КруговойМассив1Связь23", + "КруговойМассив1Связь24", + "КруговойМассив1Связь25", + "КруговойМассив1Связь26", + "КруговойМассив1Связь27", + "КруговойМассив1Связь28", + "КруговойМассив1Связь29" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7, + 9, + 11 + ], + "generated_ids": [ + 3, + 5, + 7, + 9, + 11 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8, + 10, + 12 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31 + ] + } + ], + "solver_result": 0 + }, + "temporary_document": "QIPreviewbfb737b0690b4c579209a314501884be", + "original_registry_preserved": true, + "original_formulas_preserved": true, + "source_datum_label_positions_before": [ + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 14.0, + 0.0 + ], + [ + 14.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 11.363961219787598, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ] + ], + "rendered_dialog": "C:\\Repos\\qinventor\\artifacts\\validation\\sketch-circular-existing-pattern-edit-dialog.png", + "cancel_preserved_native_data_history_dirty_flag": true, + "source_datum_label_positions_after_cancel": [ + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 14.0, + 0.0 + ], + [ + 14.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 11.363961219787598, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ] + ], + "history_before": { + "undo_count": 1, + "redo_count": 1, + "undo_names": [ + "Круговой массив" + ], + "redo_names": [ + "Изменить радиус исходной окружности массива" + ] + }, + "history_after_native_redo": { + "undo_count": 2, + "redo_count": 0, + "undo_names": [ + "Изменить радиус исходной окружности массива", + "Круговой массив" + ], + "redo_names": [] + }, + "source_radius_after_native_redo_mm": 3.0, + "regenerated_pattern_after_native_redo": { + "circle_count": 4, + "native_geometry_count": 9, + "constraint_count": 22, + "centers_and_radii_mm": [ + [ + -7.0, + -4.0, + 3.0 + ], + [ + -4.0, + 7.0, + 3.0 + ], + [ + 4.0, + -7.0, + 3.0 + ], + [ + 7.0, + 4.0, + 3.0 + ] + ], + "metadata": [ + { + "id": "p1", + "kind": "circular", + "parameters": { + "count": 4, + "center": [ + 0.0, + 0.0 + ], + "angle": 360.0 + }, + "source_geometry_ids": [ + 1 + ], + "created_geometry_ids": [ + 4, + 6, + 8 + ], + "helper_geometry_ids": [ + 2, + 3, + 5, + 7, + 9 + ], + "constraint_names": [ + "КруговойМассив1ЦентрX", + "КруговойМассив1ЦентрY", + "КруговойМассив1Связь3", + "КруговойМассив1Связь4", + "КруговойМассив1Связь5", + "КруговойМассив1Связь6", + "КруговойМассив1Связь7", + "КруговойМассив1Связь8", + "КруговойМассив1УголШага", + "КруговойМассив1Связь10", + "КруговойМассив1Связь11", + "КруговойМассив1Связь12", + "КруговойМассив1Связь13", + "КруговойМассив1Связь14", + "КруговойМассив1Связь15", + "КруговойМассив1Связь16", + "КруговойМассив1Связь17", + "КруговойМассив1Связь18", + "КруговойМассив1Связь19" + ], + "dimension_names": { + "ЦентрX": "КруговойМассив1ЦентрX", + "ЦентрY": "КруговойМассив1ЦентрY", + "УголШага": "КруговойМассив1УголШага" + }, + "associative": true, + "source_ids": [ + 0 + ], + "created_ids": [ + 3, + 5, + 7 + ], + "generated_ids": [ + 3, + 5, + 7 + ], + "helper_ids": [ + 1, + 2, + 4, + 6, + 8 + ], + "all_geometry_ids": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "constraint_ids": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21 + ] + } + ], + "solver_result": 0 + } + } + }, + { + "name": "Russian transformation controls and Finish/Cancel remain visible at 1440 px", + "passed": true, + "result": { + "window_width": 1440, + "controls": { + "QI_SketchMirror": { + "caption": "Зеркальное отражение", + "tooltip": "Выберите геометрию эскиза и задайте ось отражения. Копии связаны с исходными размерами; доступен предпросмотр.", + "visible_without_scroll": true + }, + "QI_SketchRectangularPattern": { + "caption": "Прямоугольный массив", + "tooltip": "Повторить выбранную геометрию в двух перпендикулярных направлениях. Задайте количество, шаг и угол направления.", + "visible_without_scroll": true + }, + "QI_SketchCircularPattern": { + "caption": "Круговой массив", + "tooltip": "Повторить выбранную геометрию вокруг заданного центра. Задайте количество и общий угол; копии сохраняют связь.", + "visible_without_scroll": true + }, + "QI_SketchOffset": { + "caption": "Смещение геометрии", + "tooltip": "Создать смещённую геометрию: выбор контура, расстояние и связь с исходными размерами. Отрицательное расстояние меняет сторону.", + "visible_without_scroll": true + }, + "QI_EditSketchPattern": { + "caption": "Параметры повторений", + "tooltip": "Изменить ось зеркала, количество, шаг, центр или угол созданного повторения геометрии.", + "visible_without_scroll": true + }, + "QI_FinishSketch": { + "caption": "Завершить\nэскиз", + "tooltip": "Закрыть редактор и вернуться к модели", + "visible_without_scroll": true + }, + "QI_CancelSketch": { + "caption": "Отменить\nэскиз", + "tooltip": "Отменить новые построения; существующий эскиз вернётся к состоянию до редактирования", + "visible_without_scroll": true + } + }, + "untranslated_candidates": [] + } + } + ], + "input_delivery": "Qt button and keyboard events on native dialogs", + "selection_method": "explicit native Sketch Edge1 preselection through Selection API", + "persistence_boundary": "Qt QI_Save and native App.openDocument on the saved FCStd", + "os_pointer_access": false, + "native_shadow_guard": { + "module_file": "C:\\Repos\\qinventor\\src\\QInventor\\qi_sketch_preview.py", + "point_coordinate_attributes": [ + "X", + "Y", + "Z" + ] + } + } + }, + { + "name": "parametric_solid_kernel_suite", + "passed": true, + "result": { + "status": "passed", + "freecad_version": [ + "1", + "1", + "4", + "20260928 (Git shallow)", + "Unknown", + "2026/09/28 13:46:34", + "(HEAD detached at 4fd3bf320)", + "4fd3bf320d9566a27e60069fc8387448aaa3a094" + ], + "gui": true, + "source": "C:\\Repos\\qinventor\\src\\QInventor\\qi_model.py", + "passed": 20, + "failed": 0, + "skipped": 0, + "checks": [ + { + "name": "box_volume_constraints", + "passed": true, + "status": "passed", + "detail": { + "volume_mm3": 63999.999999999985, + "fully_constrained": true, + "feature": "PartDesign::Pad" + } + }, + { + "name": "box_edit_undo_redo", + "passed": true, + "status": "passed", + "detail": { + "volume_mm3": 15000.0, + "undo_redo": true, + "native_parameters": "App::VarSet" + } + }, + { + "name": "creation_undo_redo", + "passed": true, + "status": "passed", + "detail": { + "single_creation_undo": true + } + }, + { + "name": "cylinder_material", + "passed": true, + "status": "passed", + "detail": { + "material": "Алюминий", + "mass_kg": 0.0732870734229427 + } + }, + { + "name": "plate_parametric_holes", + "passed": true, + "status": "passed", + "detail": { + "volume_mm3": 83685.66394729842, + "fully_constrained": true, + "holes": 2 + } + }, + { + "name": "atomic_dimensions_and_rejection", + "passed": true, + "status": "passed", + "detail": { + "rejected_without_partial_changes": true + } + }, + { + "name": "invalid_dimension_inputs", + "passed": true, + "status": "passed", + "detail": { + "invalid_cases": 14, + "document_unchanged": true + } + }, + { + "name": "free_sketch_dimensions", + "passed": true, + "status": "passed", + "detail": { + "rectangle_driving_dimensions": [ + "Length", + "Width" + ], + "circle_radius": 9 + } + }, + { + "name": "all_origin_planes", + "passed": true, + "status": "passed", + "detail": { + "planes": [ + "XY", + "XZ", + "YZ" + ] + } + }, + { + "name": "pocket_and_rollback", + "passed": true, + "status": "passed", + "detail": { + "real_subtractive_volume_mm3": 19214.601836602546, + "failed_pocket_rolled_back": true + } + }, + { + "name": "save_and_reopen", + "passed": true, + "status": "passed", + "detail": { + "native_project": "C:\\Repos\\qinventor\\artifacts\\validation\\parametric-bracket.FCStd", + "editable_after_reopen": true + } + }, + { + "name": "step_round_trip", + "passed": true, + "status": "passed", + "detail": { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\bracket.step", + "imported_volume_mm3": 56743.36293856411, + "valid": true + } + }, + { + "name": "solid_export_formats", + "passed": true, + "status": "passed", + "detail": { + "formats": [ + ".iges", + ".brep", + ".stl" + ], + "stl_facets": 12 + } + }, + { + "name": "flat_exports_world_geometry_and_history", + "passed": true, + "status": "passed", + "detail": { + "checks": 45, + "analytical_geometry_and_history": [ + { + "case": "placed_assembly-default-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "A_local": [ + 10, + 20, + 30, + 90 + ], + "B_local": [ + 40, + 50, + 60, + 180 + ], + "A_world": [ + 80, + 210, + 330, + 180 + ], + "B_world": [ + 50, + 240, + 360, + 270 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 233.99999999999997, + "com_mm": [ + 55.666666666666664, + 234.52564102564102, + 360.2692307692308 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 234.00000000000003, + "com_mm": [ + 55.666666666666664, + 234.52564102564102, + 360.2692307692308 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 233.99999999999997, + "com_mm": [ + 55.666666666666664, + 234.52564102564102, + 360.2692307692308 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 0.0, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "placed_assembly-default-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "A_local": [ + 10, + 20, + 30, + 90 + ], + "B_local": [ + 40, + 50, + 60, + 180 + ], + "A_world": [ + 80, + 210, + 330, + 180 + ], + "B_world": [ + 50, + 240, + 360, + 270 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 12, + "volume_mm3": 0, + "native_volume_mm3": 233.99999999999994, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 234.00000000000003, + "com_mm": [ + 55.666666666666664, + 234.52564102564102, + 360.2692307692308 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 233.99999999999994, + "com_mm": [ + 55.66666666666665, + 234.52564102564102, + 360.26923076923083 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 0.0, + "com_distance_mm": 5.859285502108464E-14, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "placed_assembly-default-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "A_local": [ + 10, + 20, + 30, + 90 + ], + "B_local": [ + 40, + 50, + 60, + 180 + ], + "A_world": [ + 80, + 210, + 330, + 180 + ], + "B_world": [ + 50, + 240, + 360, + 270 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 24, + "volume_mm3": 234.0, + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ], + "original_redo_executed": true + }, + { + "case": "placed_assembly-explicit-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "A_local": [ + 10, + 20, + 30, + 90 + ], + "B_local": [ + 40, + 50, + 60, + 180 + ], + "A_world": [ + 80, + 210, + 330, + 180 + ], + "B_world": [ + 50, + 240, + 360, + 270 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 233.99999999999997, + "com_mm": [ + 55.666666666666664, + 234.52564102564102, + 360.2692307692308 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 234.00000000000003, + "com_mm": [ + 55.666666666666664, + 234.52564102564102, + 360.2692307692308 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 233.99999999999997, + "com_mm": [ + 55.666666666666664, + 234.52564102564102, + 360.2692307692308 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 0.0, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "placed_assembly-explicit-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "A_local": [ + 10, + 20, + 30, + 90 + ], + "B_local": [ + 40, + 50, + 60, + 180 + ], + "A_world": [ + 80, + 210, + 330, + 180 + ], + "B_world": [ + 50, + 240, + 360, + 270 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 12, + "volume_mm3": 0, + "native_volume_mm3": 233.99999999999994, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 234.00000000000003, + "com_mm": [ + 55.666666666666664, + 234.52564102564102, + 360.2692307692308 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 234.0, + "native_volume_mm3": 233.99999999999994, + "com_mm": [ + 55.66666666666665, + 234.52564102564102, + 360.26923076923083 + ], + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 0.0, + "com_distance_mm": 5.859285502108464E-14, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "placed_assembly-explicit-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "A_local": [ + 10, + 20, + 30, + 90 + ], + "B_local": [ + 40, + 50, + 60, + 180 + ], + "A_world": [ + 80, + 210, + 330, + 180 + ], + "B_world": [ + 50, + 240, + 360, + 270 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 24, + "volume_mm3": 234.0, + "bounds_mm": [ + 50.0, + 207.0, + 330.0, + 80.0, + 240.0, + 367.0 + ], + "original_redo_executed": true + }, + { + "case": "placed_body-default-brep", + "known_placements": { + "parent": [ + 50, + 60, + 70, + 90 + ], + "body": [ + 10, + 20, + 30, + 90 + ], + "independent_world": [ + 30, + 70, + 100, + 180 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 23.999999999999996, + "native_volume_mm3": 23.999999999999996, + "com_mm": [ + 29.0, + 68.5, + 102.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "comparison": { + "expected": { + "type": "Solid", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 24.0, + "native_volume_mm3": 24.0, + "com_mm": [ + 29.0, + 68.5, + 102.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 23.999999999999996, + "native_volume_mm3": 23.999999999999996, + "com_mm": [ + 29.0, + 68.5, + 102.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 3.552713678800501E-15, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "placed_body-default-iges", + "known_placements": { + "parent": [ + 50, + 60, + 70, + 90 + ], + "body": [ + 10, + 20, + 30, + 90 + ], + "independent_world": [ + 30, + 70, + 100, + 180 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 6, + "volume_mm3": 0, + "native_volume_mm3": 23.999999999999996, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Solid", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 24.0, + "native_volume_mm3": 24.0, + "com_mm": [ + 29.0, + 68.5, + 102.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 23.999999999999996, + "native_volume_mm3": 23.999999999999996, + "com_mm": [ + 29.0, + 68.50000000000001, + 101.99999999999997 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 3.552713678800501E-15, + "com_distance_mm": 3.1776437161565096E-14, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "placed_body-default-stl", + "known_placements": { + "parent": [ + 50, + 60, + 70, + 90 + ], + "body": [ + 10, + 20, + 30, + 90 + ], + "independent_world": [ + 30, + 70, + 100, + 180 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 12, + "volume_mm3": 24.0, + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ], + "original_redo_executed": true + }, + { + "case": "placed_body-explicit-brep", + "known_placements": { + "parent": [ + 50, + 60, + 70, + 90 + ], + "body": [ + 10, + 20, + 30, + 90 + ], + "independent_world": [ + 30, + 70, + 100, + 180 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 23.999999999999996, + "native_volume_mm3": 23.999999999999996, + "com_mm": [ + 29.0, + 68.5, + 102.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "comparison": { + "expected": { + "type": "Solid", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 24.0, + "native_volume_mm3": 24.0, + "com_mm": [ + 29.0, + 68.5, + 102.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 23.999999999999996, + "native_volume_mm3": 23.999999999999996, + "com_mm": [ + 29.0, + 68.5, + 102.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 3.552713678800501E-15, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "placed_body-explicit-iges", + "known_placements": { + "parent": [ + 50, + 60, + 70, + 90 + ], + "body": [ + 10, + 20, + 30, + 90 + ], + "independent_world": [ + 30, + 70, + 100, + 180 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 6, + "volume_mm3": 0, + "native_volume_mm3": 23.999999999999996, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Solid", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 24.0, + "native_volume_mm3": 24.0, + "com_mm": [ + 29.0, + 68.5, + 102.0 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 23.999999999999996, + "native_volume_mm3": 23.999999999999996, + "com_mm": [ + 29.0, + 68.50000000000001, + 101.99999999999997 + ], + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 3.552713678800501E-15, + "com_distance_mm": 3.1776437161565096E-14, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "placed_body-explicit-stl", + "known_placements": { + "parent": [ + 50, + 60, + 70, + 90 + ], + "body": [ + 10, + 20, + 30, + 90 + ], + "independent_world": [ + 30, + 70, + 100, + 180 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 12, + "volume_mm3": 24.0, + "bounds_mm": [ + 28.0, + 67.0, + 100.0, + 30.0, + 70.0, + 104.0 + ], + "original_redo_executed": true + }, + { + "case": "array-default-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 3, + "all_elements_visible": true + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0, + "native_volume_mm3": 864.0, + "com_mm": [ + 95.91666666666667, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0000000000002, + "native_volume_mm3": 864.0000000000002, + "com_mm": [ + 95.91666666666667, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0, + "native_volume_mm3": 864.0, + "com_mm": [ + 95.91666666666667, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "array-default-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 3, + "all_elements_visible": true + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 18, + "volume_mm3": 0, + "native_volume_mm3": 864.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0000000000002, + "native_volume_mm3": 864.0000000000002, + "com_mm": [ + 95.91666666666667, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0, + "native_volume_mm3": 864.0, + "com_mm": [ + 95.91666666666669, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 1.4210854715202004E-14, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "array-default-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 3, + "all_elements_visible": true + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 36, + "volume_mm3": 864.0, + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ], + "original_redo_executed": true + }, + { + "case": "array-explicit-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 3, + "all_elements_visible": true + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0, + "native_volume_mm3": 864.0, + "com_mm": [ + 95.91666666666667, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0000000000002, + "native_volume_mm3": 864.0000000000002, + "com_mm": [ + 95.91666666666667, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0, + "native_volume_mm3": 864.0, + "com_mm": [ + 95.91666666666667, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "array-explicit-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 3, + "all_elements_visible": true + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 18, + "volume_mm3": 0, + "native_volume_mm3": 864.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0000000000002, + "native_volume_mm3": 864.0000000000002, + "com_mm": [ + 95.91666666666667, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 3, + "shells": 3, + "faces": 18, + "volume_mm3": 864.0, + "native_volume_mm3": 864.0, + "com_mm": [ + 95.91666666666669, + 219.94444444444446, + 305.44444444444446 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 1.4210854715202004E-14, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "array-explicit-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 3, + "all_elements_visible": true + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 36, + "volume_mm3": 864.0, + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ], + "original_redo_executed": true + }, + { + "case": "scaled_mirror_subassembly-default-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "source_child_x": [ + 10, + 30 + ], + "source_child_rotations_z": [ + 0, + 90 + ], + "linked_subassembly_local_x": [ + 50, + 200 + ], + "linked_subassembly_scales": [ + 2, + -1 + ], + "independent_occurrences": 4, + "independent_volume_mm3": 432 + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.0, + "native_volume_mm3": 432.0, + "com_mm": [ + 97.91666666666669, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.00000000000001, + 270.0, + 296.0, + 103.00000000000004, + 390.0, + 308.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.00000000000006, + "native_volume_mm3": 431.9999999999999, + "com_mm": [ + 97.91666666666667, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.0, + "native_volume_mm3": 432.0, + "com_mm": [ + 97.91666666666669, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.00000000000001, + 270.0, + 296.0, + 103.00000000000004, + 390.0, + 308.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 5.684341886080802E-14, + "com_distance_mm": 1.4210854715202004E-14, + "max_bound_delta_mm": 4.263256414560601E-14, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "scaled_mirror_subassembly-default-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "source_child_x": [ + 10, + 30 + ], + "source_child_rotations_z": [ + 0, + 90 + ], + "linked_subassembly_local_x": [ + 50, + 200 + ], + "linked_subassembly_scales": [ + 2, + -1 + ], + "independent_occurrences": 4, + "independent_volume_mm3": 432 + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 24, + "volume_mm3": 0, + "native_volume_mm3": 432.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.00000000000006, + "native_volume_mm3": 431.9999999999999, + "com_mm": [ + 97.91666666666667, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.0, + "native_volume_mm3": 432.0, + "com_mm": [ + 97.91666666666667, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 5.684341886080802E-14, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "scaled_mirror_subassembly-default-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "source_child_x": [ + 10, + 30 + ], + "source_child_rotations_z": [ + 0, + 90 + ], + "linked_subassembly_local_x": [ + 50, + 200 + ], + "linked_subassembly_scales": [ + 2, + -1 + ], + "independent_occurrences": 4, + "independent_volume_mm3": 432 + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 48, + "volume_mm3": 432.0, + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ], + "original_redo_executed": true + }, + { + "case": "scaled_mirror_subassembly-explicit-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "source_child_x": [ + 10, + 30 + ], + "source_child_rotations_z": [ + 0, + 90 + ], + "linked_subassembly_local_x": [ + 50, + 200 + ], + "linked_subassembly_scales": [ + 2, + -1 + ], + "independent_occurrences": 4, + "independent_volume_mm3": 432 + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.0, + "native_volume_mm3": 432.0, + "com_mm": [ + 97.91666666666669, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.00000000000001, + 270.0, + 296.0, + 103.00000000000004, + 390.0, + 308.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.00000000000006, + "native_volume_mm3": 431.9999999999999, + "com_mm": [ + 97.91666666666667, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.0, + "native_volume_mm3": 432.0, + "com_mm": [ + 97.91666666666669, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.00000000000001, + 270.0, + 296.0, + 103.00000000000004, + 390.0, + 308.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 5.684341886080802E-14, + "com_distance_mm": 1.4210854715202004E-14, + "max_bound_delta_mm": 4.263256414560601E-14, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "scaled_mirror_subassembly-explicit-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "source_child_x": [ + 10, + 30 + ], + "source_child_rotations_z": [ + 0, + 90 + ], + "linked_subassembly_local_x": [ + 50, + 200 + ], + "linked_subassembly_scales": [ + 2, + -1 + ], + "independent_occurrences": 4, + "independent_volume_mm3": 432 + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 24, + "volume_mm3": 0, + "native_volume_mm3": 432.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.00000000000006, + "native_volume_mm3": 431.9999999999999, + "com_mm": [ + 97.91666666666667, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 4, + "shells": 4, + "faces": 24, + "volume_mm3": 432.0, + "native_volume_mm3": 432.0, + "com_mm": [ + 97.91666666666667, + 299.5833333333333, + 303.3333333333333 + ], + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 5.684341886080802E-14, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "scaled_mirror_subassembly-explicit-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "source_child_x": [ + 10, + 30 + ], + "source_child_rotations_z": [ + 0, + 90 + ], + "linked_subassembly_local_x": [ + 50, + 200 + ], + "linked_subassembly_scales": [ + 2, + -1 + ], + "independent_occurrences": 4, + "independent_volume_mm3": 432 + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 48, + "volume_mm3": 432.0, + "bounds_mm": [ + 94.0, + 270.0, + 296.0, + 103.0, + 390.0, + 308.0 + ], + "original_redo_executed": true + }, + { + "case": "masked_array_virtual-default-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0000000000002, + "native_volume_mm3": 672.0000000000002, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "masked_array_virtual-default-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 12, + "volume_mm3": 0, + "native_volume_mm3": 672.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0000000000002, + "native_volume_mm3": 672.0000000000002, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "masked_array_virtual-default-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 24, + "volume_mm3": 672.0, + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ], + "original_redo_executed": true + }, + { + "case": "masked_array_virtual-explicit-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0000000000002, + "native_volume_mm3": 672.0000000000002, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "masked_array_virtual-explicit-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 12, + "volume_mm3": 0, + "native_volume_mm3": 672.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0000000000002, + "native_volume_mm3": 672.0000000000002, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "masked_array_virtual-explicit-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 24, + "volume_mm3": 672.0, + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ], + "original_redo_executed": true + }, + { + "case": "masked_array_expanded-default-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0000000000002, + "native_volume_mm3": 672.0000000000002, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "masked_array_expanded-default-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 12, + "volume_mm3": 0, + "native_volume_mm3": 672.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0000000000002, + "native_volume_mm3": 672.0000000000002, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "masked_array_expanded-default-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 24, + "volume_mm3": 672.0, + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ], + "original_redo_executed": true + }, + { + "case": "masked_array_expanded-explicit-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0000000000002, + "native_volume_mm3": 672.0000000000002, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "masked_array_expanded-explicit-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 12, + "volume_mm3": 0, + "native_volume_mm3": 672.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0000000000002, + "native_volume_mm3": 672.0000000000002, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 2, + "shells": 2, + "faces": 12, + "volume_mm3": 672.0, + "native_volume_mm3": 672.0, + "com_mm": [ + 95.60714285714286, + 222.21428571428572, + 305.85714285714283 + ], + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 2.2737367544323206E-13, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "masked_array_expanded-explicit-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 2, + "all_elements_visible": false, + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 24, + "volume_mm3": 672.0, + "bounds_mm": [ + 91.0, + 200.0, + 300.0, + 100.0, + 226.0, + 312.0 + ], + "original_redo_executed": true + }, + { + "case": "hidden_element-explicit-brep", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 1, + "all_elements_visible": false, + "explicitly_selected_hidden_element": "Array_i1", + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 191.99999999999997, + "native_volume_mm3": 191.99999999999997, + "com_mm": [ + 97.0, + 212.0, + 304.0 + ], + "bounds_mm": [ + 94.0, + 210.0, + 300.0, + 100.0, + 214.0, + 308.0 + ] + }, + "comparison": { + "expected": { + "type": "Solid", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 192.00000000000006, + "native_volume_mm3": 192.00000000000006, + "com_mm": [ + 97.00000000000001, + 212.0, + 304.0 + ], + "bounds_mm": [ + 94.0, + 210.0, + 300.0, + 100.0, + 214.0, + 308.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 191.99999999999997, + "native_volume_mm3": 191.99999999999997, + "com_mm": [ + 97.0, + 212.0, + 304.0 + ], + "bounds_mm": [ + 94.0, + 210.0, + 300.0, + 100.0, + 214.0, + 308.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 8.526512829121202E-14, + "com_distance_mm": 1.4210854715202004E-14, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "hidden_element-explicit-iges", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 1, + "all_elements_visible": false, + "explicitly_selected_hidden_element": "Array_i1", + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "raw_readback": { + "type": "Compound", + "valid": true, + "solids": 0, + "shells": 0, + "faces": 6, + "volume_mm3": 0, + "native_volume_mm3": 192.0, + "com_mm": [ + 0.0, + 0.0, + 0.0 + ], + "bounds_mm": [ + 94.0, + 210.0, + 300.0, + 100.0, + 214.0, + 308.0 + ] + }, + "iges_topology_readback": "native sewShape of raw imported surfaces and closed-shell makeSolid", + "comparison": { + "expected": { + "type": "Solid", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 192.00000000000006, + "native_volume_mm3": 192.00000000000006, + "com_mm": [ + 97.00000000000001, + 212.0, + 304.0 + ], + "bounds_mm": [ + 94.0, + 210.0, + 300.0, + 100.0, + 214.0, + 308.0 + ] + }, + "actual": { + "type": "Compound", + "valid": true, + "solids": 1, + "shells": 1, + "faces": 6, + "volume_mm3": 192.0, + "native_volume_mm3": 192.0, + "com_mm": [ + 97.00000000000001, + 212.0, + 304.0 + ], + "bounds_mm": [ + 94.0, + 210.0, + 300.0, + 100.0, + 214.0, + 308.0 + ] + }, + "missing_volume_mm3": 0.0, + "excess_volume_mm3": 0.0, + "volume_delta_mm3": 5.684341886080802E-14, + "com_distance_mm": 0.0, + "max_bound_delta_mm": 0.0, + "matches": true + }, + "original_redo_executed": true + }, + { + "case": "hidden_element-explicit-stl", + "known_placements": { + "root": [ + 100, + 200, + 300, + 90 + ], + "array_local_x": [ + 0, + 10, + 20 + ], + "array_scales": [ + 1, + 2, + 3 + ], + "independent_occurrences": 1, + "all_elements_visible": false, + "explicitly_selected_hidden_element": "Array_i1", + "native_element_visibility": [ + 1, + 0, + 1 + ] + }, + "exact_snapshot_before_first_world_query": true, + "stl_facets": 12, + "volume_mm3": 192.0, + "bounds_mm": [ + 94.0, + 210.0, + 300.0, + 100.0, + 214.0, + 308.0 + ], + "original_redo_executed": true + }, + { + "case": "post-native-writer-fault.brep", + "original_exact_payload_history_active_doc": true, + "original_redo_executed": true, + "old_destination_exact": true, + "temporary_cleanup_exact": true, + "native_writer_completed_before_injected_failure": true, + "before_first_world_query": true + }, + { + "case": "post-native-writer-fault.iges", + "original_exact_payload_history_active_doc": true, + "original_redo_executed": true, + "old_destination_exact": true, + "temporary_cleanup_exact": true, + "native_writer_completed_before_injected_failure": true, + "before_first_world_query": true + }, + { + "case": "post-native-writer-fault.stl", + "original_exact_payload_history_active_doc": true, + "original_redo_executed": true, + "old_destination_exact": true, + "temporary_cleanup_exact": true, + "native_writer_completed_before_injected_failure": true, + "before_first_world_query": true + }, + { + "case": "all-hidden-array-rejection.brep", + "original_exact_payload_history_active_doc": true, + "original_redo_executed": true, + "old_destination_exact": true, + "temporary_cleanup_exact": true, + "empty_geometry_rejected": "Нет твердых тел для экспорта", + "before_first_world_query": true + }, + { + "case": "all-hidden-array-rejection.iges", + "original_exact_payload_history_active_doc": true, + "original_redo_executed": true, + "old_destination_exact": true, + "temporary_cleanup_exact": true, + "empty_geometry_rejected": "Нет твердых тел для экспорта", + "before_first_world_query": true + }, + { + "case": "all-hidden-array-rejection.stl", + "original_exact_payload_history_active_doc": true, + "original_redo_executed": true, + "old_destination_exact": true, + "temporary_cleanup_exact": true, + "empty_geometry_rejected": "Нет твердых тел для экспорта", + "before_first_world_query": true + } + ] + } + }, + { + "name": "export_rejects_non_solids", + "passed": true, + "status": "passed", + "detail": { + "rejected_selections": [ + "Sketch.step", + "Sketch.iges", + "Sketch.brep", + "Sketch.stl", + "Empty.step", + "Empty.iges", + "Empty.brep", + "Empty.stl", + "Wire.step", + "Wire.iges", + "Wire.brep", + "Wire.stl", + "Mixed.step", + "Mixed.iges", + "Mixed.brep", + "Mixed.stl" + ], + "existing_file_preserved": true + } + }, + { + "name": "export_rejects_cached_failed_feature", + "passed": true, + "status": "passed", + "detail": { + "native_error": "Cannot create a pad with a total length of zero.", + "cached_volume_mm3": 999.9999999999998, + "rejected_exports": 16, + "independent_and_recovered_exports": true + } + }, + { + "name": "assembly_solver_links_bom", + "passed": true, + "status": "passed", + "detail": { + "solver_status": 0, + "joint_types": [ + "Fixed", + "Revolute" + ], + "components": 3, + "bom_rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "QI-001", + "name": "Плита с отверстиями", + "quantity": 1, + "quantity_total": 1, + "material": "Алюминий", + "density_kg_m3": 2700.0, + "unit_mass_kg": 0.15063419510513718, + "mass_kg": 0.15063419510513718, + "source_id": "QInventorAssembly#Body", + "source_name": "Body", + "source_doc": "QInventorAssembly", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorAssembly#Body|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Алюминий', 2700.0, 55790.4426315, 17430.7964474)", + "materials": [ + "Алюминий" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "QI-002", + "name": "Штифт", + "quantity": 2, + "quantity_total": 2, + "material": "Сталь", + "density_kg_m3": 7850.000000000001, + "unit_mass_kg": 0.02903645284414249, + "mass_kg": 0.05807290568828498, + "source_id": "QInventorAssembly#Body001", + "source_name": "Body001", + "source_doc": "QInventorAssembly", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorAssembly#Body001|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 3698.91119034, 1486.85297109)", + "materials": [ + "Сталь" + ] + } + ] + } + }, + { + "name": "assembly_reopen_step", + "passed": true, + "status": "passed", + "detail": { + "reopened_joints": true, + "step_solids": 3 + } + }, + { + "name": "drawing_associativity", + "passed": true, + "status": "passed", + "detail": { + "views": 3, + "model_change_recomputed": true, + "gui_exports": true, + "direct_dxf_after_rapid_edits": true, + "dxf_front_width_mm": 100.0, + "dxf_front_height_mm": 50.0 + } + }, + { + "name": "complete_examples", + "passed": true, + "status": "passed", + "detail": { + "volumes_mm3": { + "bracket": 56743.36293856411, + "plate": 38743.362938564074, + "shaft": 45238.93421169302, + "pulley": 50265.48245743669 + } + } + } + ], + "artifacts": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\parametric-bracket.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\bracket.step", + "C:\\Repos\\qinventor\\artifacts\\validation\\solid.iges", + "C:\\Repos\\qinventor\\artifacts\\validation\\solid.brep", + "C:\\Repos\\qinventor\\artifacts\\validation\\solid.stl", + "C:\\Repos\\qinventor\\artifacts\\validation\\independent-good-body.step", + "C:\\Repos\\qinventor\\artifacts\\validation\\recovered-body.step", + "C:\\Repos\\qinventor\\artifacts\\validation\\assembly-bom.csv", + "C:\\Repos\\qinventor\\artifacts\\validation\\native-assembly.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\assembly.step", + "C:\\Repos\\qinventor\\artifacts\\validation\\associative-drawing.dxf", + "C:\\Repos\\qinventor\\artifacts\\validation\\associative-drawing.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\associative-drawing.svg", + "C:\\Repos\\qinventor\\artifacts\\validation\\associative-drawing.pdf" + ], + "success": true + } + }, + { + "name": "native_project_russian_label_suite", + "passed": true, + "result": { + "status": "passed", + "passed": 12, + "failed": 0, + "skipped": 0, + "checks": [ + { + "name": "native_label_roundtrip", + "passed": true, + "status": "passed", + "detail": { + "native_open_label": "ascii-part", + "restored_label": "Деталь — русское имя № 1", + "sha256": "12bb1c1827f0e229c2abc3b2853fe287b4a8467dab7f5d5007e8b1f9948ab09e", + "file_unchanged": true + } + }, + { + "name": "missing_invalid_other_formats", + "passed": true, + "status": "passed", + "detail": { + "rejected": 4, + "files_and_label_unchanged": true + } + }, + { + "name": "malformed_xml_missing_label", + "passed": true, + "status": "passed", + "detail": { + "rejected": 5, + "child_label_ignored": true + } + }, + { + "name": "reject_dtd_and_entities", + "passed": true, + "status": "passed", + "detail": { + "rejected_encodings": [ + "utf-8", + "utf-16", + "utf-32" + ] + } + }, + { + "name": "reject_oversize_without_extraction", + "passed": true, + "status": "passed", + "detail": { + "max_xml_bytes": 8388608, + "oversize_rejected": true, + "no_extraction": true + } + }, + { + "name": "gui_modified_state_and_history", + "passed": true, + "status": "passed", + "detail": { + "clean_preserved": true, + "dirty_preserved": true, + "undo_redo_unchanged": true, + "file_unchanged": true, + "title_marker_after_recompute": true, + "title_marker_cleared_on_save": true, + "native_save_return": null, + "states": { + "before_native_save": { + "gui_document_modified": true, + "window_modified": true + }, + "after_native_save": { + "gui_document_modified": true, + "window_modified": true + }, + "before_qi_save_as": { + "gui_document_modified": true, + "window_modified": true + }, + "after_qi_save_as": { + "gui_document_modified": false, + "window_modified": false + }, + "before_qi_save": { + "gui_document_modified": true, + "window_modified": true + }, + "after_qi_save": { + "gui_document_modified": false, + "window_modified": false + }, + "before_failed_save": { + "gui_document_modified": true, + "window_modified": true + }, + "after_failed_save": { + "gui_document_modified": true, + "window_modified": true + }, + "before_cancel_save_as": { + "gui_document_modified": true, + "window_modified": true + }, + "after_cancel_save_as": { + "gui_document_modified": true, + "window_modified": true + } + }, + "saved_geometry_reopened_mm": [ + 19, + 23 + ], + "failed_save_errors": [ + { + "title": "QInventor", + "text": "Не удалось выполнить команду. Проверьте выбранную геометрию и параметры." + } + ], + "failed_and_cancelled_saves_preserve_dirty_history_and_file": true + } + }, + { + "name": "gui_qi_open_fcstd", + "passed": true, + "status": "passed", + "detail": { + "suffix": ".fcstd", + "document": "ascii_part1", + "native_modified": false, + "opened_modified": false, + "geometry": { + "volume_mm3": 999.9999999999998 + }, + "saved_russian_label": "Деталь — русское имя № 1", + "existing_unsaved_edits_preserved": true, + "source_unchanged": true + } + }, + { + "name": "gui_qi_open_step", + "passed": true, + "status": "passed", + "detail": { + "suffix": ".step", + "document": "Unnamed", + "native_modified": true, + "opened_modified": true, + "geometry": { + "volume_mm3": 999.9999999999998 + }, + "source_unchanged": true + } + }, + { + "name": "gui_qi_open_iges", + "passed": true, + "status": "passed", + "detail": { + "suffix": ".iges", + "document": "Unnamed", + "native_modified": true, + "opened_modified": true, + "geometry": { + "volume_mm3": 999.9999999999998 + }, + "source_unchanged": true + } + }, + { + "name": "gui_qi_open_brep", + "passed": true, + "status": "passed", + "detail": { + "suffix": ".brep", + "document": "Обмен", + "native_modified": true, + "opened_modified": true, + "geometry": { + "volume_mm3": 999.9999999999998 + }, + "source_unchanged": true + } + }, + { + "name": "gui_qi_open_stl", + "passed": true, + "status": "passed", + "detail": { + "suffix": ".stl", + "document": "Unnamed", + "native_modified": true, + "opened_modified": true, + "geometry": { + "facets": 12, + "dimensions_mm": [ + 10.0, + 10.0, + 10.0 + ] + }, + "source_unchanged": true + } + }, + { + "name": "gui_qi_open_failures", + "passed": true, + "status": "passed", + "detail": { + "rejected": 6, + "errors": [ + { + "title": "QInventor", + "text": "Формат файла не поддерживается. Выберите FCStd, STEP, IGES, BREP или STL." + }, + { + "title": "QInventor", + "text": "Не удалось прочитать файл модели. Проверьте его наличие и доступ к нему." + }, + { + "title": "QInventor", + "text": "Файл модели пуст." + }, + { + "title": "QInventor", + "text": "Не удалось открыть модель. Файл повреждён или его содержимое не соответствует формату." + }, + { + "title": "QInventor", + "text": "Не удалось открыть модель. Файл повреждён или его содержимое не соответствует формату." + }, + { + "title": "QInventor", + "text": "Не удалось прочитать файл модели. Проверьте его наличие и доступ к нему." + } + ], + "previous_model_preserved": true, + "cancel_preserved": true + } + } + ], + "artifacts": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\ascii-part.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\title-indicator.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\Обмен.step", + "C:\\Repos\\qinventor\\artifacts\\validation\\Обмен.iges", + "C:\\Repos\\qinventor\\artifacts\\validation\\Обмен.brep", + "C:\\Repos\\qinventor\\artifacts\\validation\\Обмен.stl" + ], + "source": "C:\\Repos\\qinventor\\src\\QInventor\\qi_io.py", + "success": true + } + }, + { + "name": "unified_extrude_suite", + "passed": true, + "result": { + "passed": true, + "count": 41, + "gui": true, + "scenarios": [ + { + "name": "XY forward", + "passed": true, + "volume": 239.99999999999997, + "bbox": [ + 10.0, + 6.0, + 4.0 + ] + }, + { + "name": "XY reverse", + "passed": true, + "volume": 239.99999999999997, + "bbox": [ + 10.0, + 6.0, + 4.0 + ] + }, + { + "name": "XY symmetric", + "passed": true, + "volume": 239.99999999999997, + "bbox": [ + 10.0, + 6.0, + 4.0 + ] + }, + { + "name": "XZ plane placement", + "passed": true, + "volume": 240.0000000000001, + "bbox": [ + 10.000000000000002, + 4.000000000000001, + 6.000000000000002 + ] + }, + { + "name": "YZ plane placement", + "passed": true, + "volume": 240.00000000000006, + "bbox": [ + 4.000000000000002, + 6.000000000000002, + 10.000000000000002 + ] + }, + { + "name": "Nested wires preserve hole", + "passed": true, + "volume": 1143.5397259066845 + }, + { + "name": "Sketch edits, length expressions and FCStd reopen", + "passed": true, + "volumeAfterReopenAndEdit": 763.4070148223199, + "file": "C:\\Repos\\qinventor\\artifacts\\validation\\parametric-extrusion.FCStd" + }, + { + "name": "Join against previous solid Body Tip", + "passed": true, + "volume": 4500.0, + "base": "Pad" + }, + { + "name": "Cut reverse against previous solid Body Tip", + "passed": true, + "volume": 3915.1769983530758, + "base": "Pad" + }, + { + "name": "Cut through all", + "passed": true, + "volume": 3717.256661176918, + "base": "Pad" + }, + { + "name": "True OCCT intersection", + "passed": true, + "volume": 399.9999999999999, + "base": "Pad" + }, + { + "name": "Through all recalculates extent after base change", + "passed": true, + "volumeAfterBaseChange": 5947.610657883069 + }, + { + "name": "New Solid is independent and preserves body placement", + "passed": true, + "bodies": 2 + }, + { + "name": "New Solid remains a sibling inside transformed App Part", + "passed": true, + "samePartSiblingAndLocalPlacementPreserved": true + }, + { + "name": "Open profile and invalid lengths leave document unchanged", + "passed": true, + "objectsUnchanged": true + }, + { + "name": "Disjoint booleans are rejected before mutation", + "passed": true, + "objectsUnchanged": true + }, + { + "name": "Native undo redo retains geometry", + "passed": true, + "undoRedo": true + }, + { + "name": "Asymmetric persisted distances, expressions, undo and reopen", + "passed": true, + "length2ExpressionUndoAndReopen": true, + "volumeAfterReopen": 899.9999999999999 + }, + { + "name": "To face planar termination and support movement", + "passed": true, + "slope": 0, + "reverse": false, + "volumeAfterPlaneMove": 719.9999999999999 + }, + { + "name": "To face exact oblique termination", + "passed": true, + "slope": 0.5, + "reverse": false, + "volumeAfterPlaneMove": 870.0000000000003 + }, + { + "name": "To face exact oblique reverse termination", + "passed": true, + "slope": 0.5, + "reverse": true, + "volumeAfterPlaneMove": 870.0 + }, + { + "name": "To face exact oblique cap preserves nested holes", + "passed": true, + "obliqueAnnulusVolume": 5717.698630133525 + }, + { + "name": "To face finite support coverage and explicit extension", + "passed": true, + "finiteBoundaryRejected": true, + "extendedPlaneVolume": 750.0000000000002 + }, + { + "name": "To face cut references previous solid and survives undo redo", + "passed": true, + "sameBodyFaceReferenceWithoutDependencyCycle": true + }, + { + "name": "To face finite boundary honors holes in termination support", + "passed": true, + "terminationFaceHoleRespected": true + }, + { + "name": "To face across transformed containers and FCStd reopen", + "passed": true, + "globalPlacementsAndReopen": true, + "volumeAfterReopen": 968.2982453146791 + }, + { + "name": "Invalid target surfaces, directions and second length rejected in Russian", + "passed": true, + "invalidTargetsAndDirectionsLeaveDocumentUnchanged": true + }, + { + "name": "To next true planar termination on preceding solid", + "passed": true, + "nextWholePlanarFace": true, + "volumeAfterBaseChange": 5717.256661176918 + }, + { + "name": "To next explicit terminator tracks nearest complete face", + "passed": true, + "selectedTerminatorRegenerates": true, + "volume": 4780.000000000001 + }, + { + "name": "To next rejects ambiguous crossing support planes", + "passed": true, + "ambiguousCrossingPlanesRejected": true + }, + { + "name": "To next explicitly rejects curved terminators", + "passed": true, + "curvedNextLimitationExplicit": true + }, + { + "name": "Changing operation moves real history between solid bodies with undo redo", + "passed": true, + "operationChangeMovesFeatureAndUndoRestoresBodies": true + }, + { + "name": "GUI real preview update and cancel rollback", + "passed": true, + "previewDeleted": true, + "visibilityRestored": true + }, + { + "name": "GUI accept, parameter edit and undo", + "passed": true, + "acceptPreviewDeleted": true, + "editUndo": true + }, + { + "name": "GUI preview respects sketch and body placements", + "passed": true, + "rotatedSketchAndTranslatedBody": true + }, + { + "name": "GUI Control.showDialog and selected profile", + "passed": true, + "nativeTaskDialog": true, + "nativeOkCancelButtons": true, + "nativeFeatureEdit": true, + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\extrusion-task-panel.png" + }, + { + "name": "GUI asymmetric distances and direction inversion", + "passed": true, + "asymmetricSwapPreviewAcceptEditUndo": true + }, + { + "name": "GUI native selection of oblique face, automatic extension and edit undo", + "passed": true, + "nativeFaceSelectionAndEditUndo": true, + "previewEqualsAcceptedSolid": true, + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\extrusion-to-oblique-face.png" + }, + { + "name": "GUI selected next-face terminator", + "passed": true, + "nextTerminatorSelectionPreviewAcceptUndo": true + }, + { + "name": "GUI invalid target rolls back and cancels cleanly", + "passed": true, + "invalidTargetAcceptRollbackAndCancel": true + }, + { + "name": "GUI native operation edits migrate solid history and undo", + "passed": true, + "nativeOperationChangeAndBodyMigrationUndo": true + } + ] + } + }, + { + "name": "typed_parameter_editor_suite", + "passed": true, + "result": { + "passed": true, + "count": 9, + "gui": true, + "checks": [ + { + "name": "Signed coordinates, zero and undo redo", + "passed": true, + "evidence": { + "signed_x_mm": -18, + "zero_y_mm": 0, + "native_undo_redo": true + } + }, + { + "name": "Native degree and radian angle units", + "passed": true, + "evidence": { + "editor_unit": "degrees", + "kernel_constraint_unit": "radians", + "signed_angle_degrees": -30 + } + }, + { + "name": "Indexed and named formulas remain read only", + "passed": true, + "evidence": { + "indexed_and_named_formula_paths": [ + [ + ".Constraints.РадиусКруга", + "4 mm" + ], + [ + ".Constraints.ЦентрX", + "SourceSizes.X" + ] + ], + "no_op_preserves_undo": true, + "formula_updates_still_associative": true + } + }, + { + "name": "Native reference dimensions remain read only", + "passed": true, + "evidence": { + "reference_radius_read_only": true, + "rejected_edit_is_atomic": true + } + }, + { + "name": "Body VarSet delegation and user formulas", + "passed": true, + "evidence": { + "internal_delegation_remains_editable": true, + "source_formula_read_only": true, + "overridden_body_formula_read_only": true + } + }, + { + "name": "Invalid values and dimensional units rejected atomically", + "passed": true, + "evidence": { + "invalid_numbers_units_unknown_parameters_rejected": true, + "all_inputs_atomic": true + } + }, + { + "name": "Typed parameter FCStd persistence", + "passed": true, + "evidence": { + "native_project": "C:\\Repos\\qinventor\\artifacts\\validation\\typed-parameters.FCStd", + "typed_metadata_and_formulas_survive_reopen": true + } + }, + { + "name": "Drawing refuses cached invalid source geometry", + "passed": true, + "evidence": { + "cached_invalid_source_rejected_before_document_mutation": true, + "recovered_source_creates_three_native_views": true + } + }, + { + "name": "Qt parameter editor preserves readonly values, precision and undo", + "passed": true, + "evidence": { + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\typed-parameter-dialog.png", + "actual_apply_button": true, + "untouched_precision_preserved": true, + "one_native_undo_step": true + } + } + ] + } + }, + { + "name": "document_parameter_table_kernel_suite", + "passed": true, + "result": { + "passed": true, + "count": 16, + "checks": [ + { + "name": "Discover native model sources and stable identities", + "passed": true, + "evidence": { + "native_controller_source": true, + "no_discovery_mutation": true, + "label_independent_ids": true, + "rows": 11 + } + }, + { + "name": "Native user parameter types and Russian literals", + "passed": true, + "evidence": { + "native_types": [ + "Distance (signed length)", + "Angle", + "Float" + ], + "unit_conversions": true, + "russian_literals": true, + "one_undo_creation": true + } + }, + { + "name": "Create chain and model bindings in one batch", + "passed": true, + "evidence": { + "add_bind_chain_atomic": true, + "native_expressions": true, + "kernel_volume_mm3": 6433.9817545518945, + "undo_redo": true + } + }, + { + "name": "No-op and foreign CAD transactions", + "passed": true, + "evidence": { + "numeric_noop_no_undo": true, + "foreign_empty_and_pending_transaction_preserved": true + } + }, + { + "name": "Reject invalid inputs without losing existing redo", + "passed": true, + "evidence": { + "rejected_cases": 18, + "existing_redo_branch_usable": true, + "native_clamping_rejected": true + } + }, + { + "name": "Validate native expression dimensions", + "passed": true, + "evidence": { + "native_expression_quantity_checks": 4, + "native_functions_and_unit_conversion": true + } + }, + { + "name": "Native syntax missing references and cycles", + "passed": true, + "evidence": { + "native_syntax_missing_divide_zero_self_and_cycle_rejected": true + } + }, + { + "name": "Native rename and transactional comments", + "passed": true, + "evidence": { + "native_dependency_rename": true, + "comment_metadata_transactional": true, + "comment_only_undo": true + } + }, + { + "name": "Referenced delete and clear formulas", + "passed": true, + "evidence": { + "referenced_delete_rejected_with_redo": true, + "native_cached_dangling_formula_detected": true, + "unbind_and_delete_same_batch": true, + "clear_preserves_evaluated_value": true + } + }, + { + "name": "Unnamed angular and reference constraints", + "passed": true, + "evidence": { + "unnamed_native_paths": true, + "degrees_not_radians": true, + "reference_readonly": true + } + }, + { + "name": "Native solver and cached shape errors", + "passed": true, + "evidence": { + "native_unsolvable_constraints_rejected": true, + "native_invalid_feature_with_cached_valid_shape_rejected": true, + "rejected_kernel_edits_preserve_redo": true + } + }, + { + "name": "FCStd persistence and continued parametric rebuild", + "passed": true, + "evidence": { + "file": "C:\\Repos\\qinventor\\artifacts\\validation\\document-parameters.FCStd", + "native_saved_formulas_comments_values": true, + "reopened_edit_rebuilds_solid": true + } + }, + { + "name": "New native document-qualified expressions", + "passed": true, + "evidence": { + "new_qualified_expression_reads_proposed_values": true, + "rejected_qualified_expression_preserves_redo": true, + "native_quoted_document_and_object_labels": true, + "raw_cyrillic_document_and_object_identifiers": true + } + }, + { + "name": "Cross-document consumers and global Undo", + "passed": true, + "evidence": { + "external_consumers_recomputed": true, + "native_global_undo_redo": true, + "both_document_redo_preserved_after_rejected_delete": true, + "external_rename_dependency_preserved": true + } + }, + { + "name": "Real document cycles and FCStd requirements", + "passed": true, + "evidence": { + "real_unsaved_owner_and_target_rejected_before_mutation": true, + "valid_saved_external_binding": true, + "object_acyclic_document_cycle_rejected": true, + "redo_preserved": true, + "first_external_bound_user_parameter_preserves_source_name": true + } + }, + { + "name": "Generated plate rules and grouped native drawing", + "passed": true, + "evidence": { + "generated_hole_fit_rules_preserved": true, + "group_does_not_reimport_techdraw_in_clone": true, + "real_page_and_group_preserved": true, + "clone_cad_objects": 15 + } + } + ], + "summary": { + "total": 16, + "passed": 16, + "failed": 0 + }, + "gui_exercised": false + } + }, + { + "name": "native_parameter_preflight_fidelity_suite", + "passed": true, + "result": { + "passed": true, + "count": 10, + "checks": [ + { + "name": "Persistent topology rejects before live and retains executable Redo", + "passed": true, + "evidence": { + "rejections": [ + { + "message": "Перестроение объекта «FaceProfile» завершилось ошибкой: AttachEngine3D: subshape not found Groove.?Face8", + "original_exact_unchanged": true, + "undo": 0, + "redo": 1 + }, + { + "message": "Перестроение объекта «FaceProfile» завершилось ошибкой: AttachEngine3D: subshape not found Groove.?Face8", + "original_exact_unchanged": true, + "undo": 0, + "redo": 1 + } + ], + "original_redo_executed": true, + "native_redo_x_mm": 110.0 + } + }, + { + "name": "Valid reference retaining Pocket edit native shapes Undo Redo", + "passed": true, + "evidence": { + "volume_mm3": 1809.5573684677208, + "undo_native_shape_comparison": { + "revolution": { + "volume_mm3": 2010.6192982974676, + "area_mm2": 1023.7167544041137, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 9.797174393178826E-16 + ], + "volume_weighted_solid_com_mm": [ + 7.953604365817317E-17, + 9.999999999999998, + -3.3953054526271003 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "groove": { + "volume_mm3": 1834.6901096964393, + "area_mm2": 1070.5486074759096, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 9.79718964375792E-16 + ], + "volume_weighted_solid_com_mm": [ + 9.325808307180692E-17, + 10.0, + -3.2906556270324296 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "pocket": { + "volume_mm3": 1815.8405537749002, + "area_mm2": 1077.3981633974483, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 9.79718964375792E-16 + ], + "volume_weighted_solid_com_mm": [ + -5.936976608789613E-17, + 9.911764705882351, + -3.3160033356539773 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "body_local": { + "volume_mm3": 1815.8405537749006, + "area_mm2": 1077.3981633974483, + "bounds_mm": [ + -6.928203230275509, + 16.0, + 22.0, + 16.92820323027551, + 41.32050807568878, + 30.000000000000004 + ], + "volume_weighted_solid_com_mm": [ + 5.044117647058824, + 28.583840031628114, + 26.683996664346022 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "body_world": { + "volume_mm3": 1815.840553774901, + "area_mm2": 1077.398163397449, + "bounds_mm": [ + 58.67949192431122, + 193.07179676972447, + 322.0, + 84.0, + 216.9282032302755, + 330.0 + ], + "volume_weighted_solid_com_mm": [ + 71.41615996837189, + 205.0441176470588, + 326.683996664346 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "occurrence_a": { + "volume_mm3": 1815.840553774902, + "area_mm2": 1077.398163397449, + "bounds_mm": [ + 958.6794919243113, + 43.071796769724486, + 62.0, + 984.0, + 66.92820323027551, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 971.4161599683719, + 55.04411764705882, + 66.68399666434603 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "occurrence_b": { + "volume_mm3": 1815.8405537749024, + "area_mm2": 1077.398163397449, + "bounds_mm": [ + 1016.0, + 133.0717967697245, + 62.0, + 1041.3205080756888, + 156.92820323027553, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 1028.5838400316281, + 144.9558823529412, + 66.68399666434603 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "assembly_world": { + "volume_mm3": 3631.6811075498035, + "area_mm2": 2154.796326794898, + "bounds_mm": [ + 958.6794919243113, + 43.071796769724486, + 62.0, + 1041.3205080756888, + 156.92820323027553, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 1000.0, + 100.00000000000001, + 66.68399666434603 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + } + }, + "redo_native_shape_comparison": { + "revolution": { + "volume_mm3": 2010.6192982974676, + "area_mm2": 1023.7167544041137, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 9.797174393178826E-16 + ], + "volume_weighted_solid_com_mm": [ + 7.953604365817317E-17, + 9.999999999999998, + -3.3953054526271003 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "groove": { + "volume_mm3": 1834.6901096964393, + "area_mm2": 1070.5486074759096, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 9.79718964375792E-16 + ], + "volume_weighted_solid_com_mm": [ + 9.325808307180692E-17, + 10.0, + -3.2906556270324296 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "pocket": { + "volume_mm3": 1809.5573684677208, + "area_mm2": 1079.681348704628, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 1.133107779529596E-15 + ], + "volume_weighted_solid_com_mm": [ + -1.6107560374667792E-17, + 9.888888888888888, + -3.3245699223640366 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "body_local": { + "volume_mm3": 1809.5573684677204, + "area_mm2": 1079.681348704628, + "bounds_mm": [ + -6.928203230275509, + 16.0, + 22.0, + 16.92820323027551, + 41.32050807568878, + 30.000000000000004 + ], + "volume_weighted_solid_com_mm": [ + 5.055555555555555, + 28.564028992979452, + 26.675430077635962 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "body_world": { + "volume_mm3": 1809.5573684677215, + "area_mm2": 1079.6813487046286, + "bounds_mm": [ + 58.67949192431122, + 193.07179676972447, + 322.0, + 84.0, + 216.9282032302755, + 330.0 + ], + "volume_weighted_solid_com_mm": [ + 71.43597100702056, + 205.05555555555554, + 326.67543007763595 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "occurrence_a": { + "volume_mm3": 1809.5573684677222, + "area_mm2": 1079.6813487046286, + "bounds_mm": [ + 958.6794919243113, + 43.071796769724486, + 62.0, + 984.0, + 66.92820323027551, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 971.4359710070205, + 55.05555555555554, + 66.67543007763597 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "occurrence_b": { + "volume_mm3": 1809.5573684677233, + "area_mm2": 1079.6813487046286, + "bounds_mm": [ + 1016.0, + 133.0717967697245, + 62.0, + 1041.3205080756888, + 156.92820323027553, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 1028.5640289929795, + 144.94444444444446, + 66.67543007763597 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "assembly_world": { + "volume_mm3": 3619.1147369354444, + "area_mm2": 2159.3626974092567, + "bounds_mm": [ + 958.6794919243113, + 43.071796769724486, + 62.0, + 1041.3205080756888, + 156.92820323027553, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 1000.0, + 100.00000000000001, + 66.67543007763597 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + } + } + } + }, + { + "name": "Valid native270 without face support", + "passed": true, + "evidence": { + "native_type": "PartDesign::Revolution", + "actual_volume_mm3": 706.8583470577034, + "scope": "No downstream face attachment; 270 is not generally invalid" + } + }, + { + "name": "Ordinary user SheetPython remains genuine model input", + "passed": true, + "evidence": { + "actual_input_type": "Spreadsheet::SheetPython", + "native_input_contents_preserved": { + "A1": "=4 mm", + "alias": "Height" + }, + "cad": { + "parameter_key": "Parameters:QIHeight", + "height_mm": 8.0, + "volume_mm3": 48.0 + }, + "rejection": { + "message": "Не удалось применить параметры: Property 'MissingAlias' not found in 'UserInput.MissingAlias'", + "original_exact_unchanged": true, + "undo": 1, + "redo": 0 + } + } + }, + { + "name": "Parameters after real drawing BOM source cells mass and direct DXF", + "passed": true, + "evidence": { + "initial": { + "parameter_key": "Parameters:QIHeight", + "height_mm": 4.0, + "volume_mm3": 24.0, + "mass_kg": 0.0001884, + "cells": { + "A1": "'Поз.", + "A2": "'1", + "B1": "'Обозначение", + "B2": "'PARAM-BOX", + "C1": "'Наименование", + "C2": "'Брусок", + "C3": "'Итого", + "D1": "'Кол.", + "D2": "1", + "E1": "'Материал", + "E2": "'Сталь", + "F1": "'Масса, кг", + "F2": "'0,0001884", + "F3": "'0,0001884" + }, + "actual_mass_cell": "'0,0001884" + }, + "immediate": { + "parameter_key": "Parameters:QIHeight", + "height_mm": 8.0, + "volume_mm3": 48.0, + "mass_kg": 0.0003768, + "cells": { + "A1": "'Поз.", + "A2": "'1", + "B1": "'Обозначение", + "B2": "'PARAM-BOX", + "C1": "'Наименование", + "C2": "'Брусок", + "C3": "'Итого", + "D1": "'Кол.", + "D2": "1", + "E1": "'Материал", + "E2": "'Сталь", + "F1": "'Масса, кг", + "F2": "'0,0003768", + "F3": "'0,0003768" + }, + "actual_mass_cell": "'0,0003768" + }, + "one_native_undo_step": true, + "undo": { + "parameter_key": "Parameters:QIHeight", + "height_mm": 4.0, + "volume_mm3": 24.0, + "mass_kg": 0.0001884, + "cells": { + "A1": "'Поз.", + "A2": "'1", + "B1": "'Обозначение", + "B2": "'PARAM-BOX", + "C1": "'Наименование", + "C2": "'Брусок", + "C3": "'Итого", + "D1": "'Кол.", + "D2": "1", + "E1": "'Материал", + "E2": "'Сталь", + "F1": "'Масса, кг", + "F2": "'0,0001884", + "F3": "'0,0001884" + }, + "actual_mass_cell": "'0,0001884" + }, + "redo": { + "parameter_key": "Parameters:QIHeight", + "height_mm": 8.0, + "volume_mm3": 48.0, + "mass_kg": 0.0003768, + "cells": { + "A1": "'Поз.", + "A2": "'1", + "B1": "'Обозначение", + "B2": "'PARAM-BOX", + "C1": "'Наименование", + "C2": "'Брусок", + "C3": "'Итого", + "D1": "'Кол.", + "D2": "1", + "E1": "'Материал", + "E2": "'Сталь", + "F1": "'Масса, кг", + "F2": "'0,0003768", + "F3": "'0,0003768" + }, + "actual_mass_cell": "'0,0003768" + }, + "actual_direct_DXF": [ + { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\integrated-height8-direct.dxf", + "sha256": "2a7b7cf2ea0fab6b75c3feedbfd8c7aacc1d0c144dbcf8854093fade825ae880", + "texts": [ + "Поз.", + "1", + "Обозначение", + "PARAM-BOX", + "Наименование", + "Брусок", + "Итого", + "Кол.", + "1", + "Материал", + "Сталь", + "Масса, кг", + "0,0003768", + "0,0003768" + ], + "actual_native_front_width_mm": 2.0, + "actual_native_front_height_mm": 8.0 + }, + { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\integrated-height4-undo-direct.dxf", + "sha256": "446acedb1f778e66001001b0b48c2f8560c27c44c8247e9b7e3732d930169465", + "texts": [ + "Поз.", + "1", + "Обозначение", + "PARAM-BOX", + "Наименование", + "Брусок", + "Итого", + "Кол.", + "1", + "Материал", + "Сталь", + "Масса, кг", + "0,0001884", + "0,0001884" + ], + "actual_native_front_width_mm": 2.0, + "actual_native_front_height_mm": 4.0 + }, + { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\integrated-height8-redo-direct.dxf", + "sha256": "2a7b7cf2ea0fab6b75c3feedbfd8c7aacc1d0c144dbcf8854093fade825ae880", + "texts": [ + "Поз.", + "1", + "Обозначение", + "PARAM-BOX", + "Наименование", + "Брусок", + "Итого", + "Кол.", + "1", + "Материал", + "Сталь", + "Масса, кг", + "0,0003768", + "0,0003768" + ], + "actual_native_front_width_mm": 2.0, + "actual_native_front_height_mm": 8.0 + } + ], + "GUI_parameter_Apply_unproven_here": true + } + }, + { + "name": "Own output used as native CAD input is guarded before live", + "passed": true, + "evidence": { + "native_input_expression": "DrawingBOM.D2", + "valid_native_noncyclic_fixture": true, + "rejection": { + "message": "Лист спецификации «Данные спецификации» используется как вход «Размеры — Брусок001». Такую связь необходимо отделить от выходной спецификации перед изменением параметров", + "original_exact_unchanged": true, + "undo": 0, + "redo": 1 + }, + "original_redo_executed": true + } + }, + { + "name": "Native drawing scale and ordinary BOM cell expressions validate before live", + "passed": true, + "evidence": { + "native_front_scale_expression_and_BOM_note_delete_rejection": { + "message": "Не удалось применить параметры: Property 'ViewScale' not found in 'QIUserParameters.ViewScale'", + "original_exact_unchanged": true, + "undo": 5, + "redo": 1 + }, + "original_scale_redo_executed": true, + "valid_change_has_one_native_undo": true, + "first_undo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.2, + "formula": "=QIUserParameters.ViewScale * 2" + }, + "undo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.4, + "formula": "=QIUserParameters.ViewScale * 2" + }, + "redo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.6, + "formula": "=QIUserParameters.ViewScale * 2" + }, + "isolated_undo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.6, + "formula": "=QIUserParameters.ViewScale * 2" + }, + "BOM_user_note_formula_preserved": "=QIUserParameters.ViewScale * 2", + "isolated_BOM_user_note_delete_rejection": { + "message": "Перестроение объекта «Данные спецификации» завершилось ошибкой: One or more cells failed contains errors.", + "original_exact_unchanged": true, + "undo": 7, + "redo": 1 + }, + "isolated_BOM_user_note_original_redo_executed": true + } + }, + { + "name": "Native Spreadsheet and own BOM cell type changes preserve executable history", + "passed": true, + "evidence": [ + { + "kind": "Spreadsheet::Sheet", + "genuine_CAD_input": true, + "integer_to_float_rejection": { + "message": "Изменение параметров меняет тип вычисленной ячейки «UserInput.H1»; штатная отмена не сохраняет такую ячейку. Сохраните тип результата её формулы перед изменением параметров", + "original_exact_unchanged": true, + "undo": 1, + "redo": 1 + }, + "original_redo_x_mm": 11.0, + "stable_float_change_one_undo": true, + "undo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.2, + "formula": "=QIUserParameters.Scale * 2" + }, + "redo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.4, + "formula": "=QIUserParameters.Scale * 2" + }, + "float_to_integer_rejection": { + "message": "Изменение параметров меняет тип вычисленной ячейки «UserInput.H1»; штатная отмена не сохраняет такую ячейку. Сохраните тип результата её формулы перед изменением параметров", + "original_exact_unchanged": true, + "undo": 3, + "redo": 0 + } + }, + { + "kind": "Spreadsheet::SheetPython", + "genuine_CAD_input": true, + "integer_to_float_rejection": { + "message": "Изменение параметров меняет тип вычисленной ячейки «UserInput.H1»; штатная отмена не сохраняет такую ячейку. Сохраните тип результата её формулы перед изменением параметров", + "original_exact_unchanged": true, + "undo": 1, + "redo": 1 + }, + "original_redo_x_mm": 11.0, + "stable_float_change_one_undo": true, + "undo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.2, + "formula": "=QIUserParameters.Scale * 2" + }, + "redo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.4, + "formula": "=QIUserParameters.Scale * 2" + }, + "float_to_integer_rejection": { + "message": "Изменение параметров меняет тип вычисленной ячейки «UserInput.H1»; штатная отмена не сохраняет такую ячейку. Сохраните тип результата её формулы перед изменением параметров", + "original_exact_unchanged": true, + "undo": 3, + "redo": 0 + } + }, + { + "kind": "OwnBOM", + "genuine_CAD_input": false, + "integer_to_float_rejection": { + "message": "Изменение параметров меняет тип вычисленной ячейки «Данные спецификации.H1»; штатная отмена не сохраняет такую ячейку. Сохраните тип результата её формулы перед изменением параметров", + "original_exact_unchanged": true, + "undo": 4, + "redo": 1 + }, + "original_redo_x_mm": 11.0, + "stable_float_change_one_undo": true, + "undo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.2, + "formula": "=QIUserParameters.Scale * 2" + }, + "redo_before_recompute": { + "present": true, + "type": "App::PropertyFloat", + "value": 2.4, + "formula": "=QIUserParameters.Scale * 2" + }, + "float_to_integer_rejection": { + "message": "Изменение параметров меняет тип вычисленной ячейки «Данные спецификации.H1»; штатная отмена не сохраняет такую ячейку. Сохраните тип результата её формулы перед изменением параметров", + "original_exact_unchanged": true, + "undo": 6, + "redo": 0 + } + } + ] + }, + { + "name": "Native canonical formula no-op preserves executable history; distinct equal-value formulas remain edits", + "passed": true, + "evidence": { + "qualified_canonical_noop_exact_payload_history_active_document": true, + "independent_original_redo_executed": true, + "equal_value_other_dependency_and_arithmetic_are_real_edits": true, + "actual_comment_change_is_edit": true, + "rejected_formula_exact_state": { + "message": "Не удалось применить параметры: Document object 'MissingCanonicalSource' not found in 'ParameterCanonicalNoop#MissingCanonicalSource.Size'", + "original_exact_unchanged": true, + "undo": 0, + "redo": 1 + } + } + }, + { + "name": "Fresh native process FCStd history inputs outputs and real reedit", + "passed": true, + "evidence": { + "passed": true, + "gui_up": false, + "parameter_module": "C:\\Repos\\qinventor\\src\\QInventor\\qi_parameters.py", + "parameter_module_sha256": "9f384e40b930b6ebc4e7f068651c0797134818803b82704258e26215997c01c2", + "checks": [ + { + "name": "persistent_support_native_history_and_reedit", + "passed": true, + "structure": { + "serialized_placement_rounding": [ + { + "object": "XZ_Plane", + "property": "Placement", + "component": 1, + "index": 0, + "expected": 0.7071067811865475, + "actual": 0.7071067811865476, + "absolute_delta": 1.1102230246251565E-16 + }, + { + "object": "XZ_Plane001", + "property": "Placement", + "component": 1, + "index": 0, + "expected": 0.7071067811865475, + "actual": 0.7071067811865476, + "absolute_delta": 1.1102230246251565E-16 + }, + { + "object": "XZ_Plane002", + "property": "Placement", + "component": 1, + "index": 0, + "expected": 0.7071067811865475, + "actual": 0.7071067811865476, + "absolute_delta": 1.1102230246251565E-16 + } + ], + "base_absolute_tolerance_mm": 1E-09, + "quaternion_absolute_tolerance": 1E-12, + "all_nonplacement_fields_exact": true + }, + "shapes": { + "revolution": { + "volume_mm3": 2010.6192982974676, + "area_mm2": 1023.7167544041137, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 9.4E-16 + ], + "volume_weighted_solid_com_mm": [ + 1.0765503945384812E-16, + 9.999999999999998, + -3.3953054526271007 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "groove": { + "volume_mm3": 1834.6901096964393, + "area_mm2": 1070.5486074759096, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 9.4E-16 + ], + "volume_weighted_solid_com_mm": [ + 1.2287337249806946E-16, + 10.0, + -3.290655627032431 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "pocket": { + "volume_mm3": 1809.5573684677208, + "area_mm2": 1079.681348704628, + "bounds_mm": [ + -8.0, + 0.0, + -8.0, + 8.0, + 20.0, + 1.2449293598294708E-15 + ], + "volume_weighted_solid_com_mm": [ + -1.6678840012356495E-17, + 9.888888888888888, + -3.3245699223640366 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "body_local": { + "volume_mm3": 1809.5573684677224, + "area_mm2": 1079.6813487046286, + "bounds_mm": [ + -6.928203230275512, + 16.0, + 22.0, + 16.928203230275514, + 41.32050807568878, + 30.000000000000004 + ], + "volume_weighted_solid_com_mm": [ + 5.055555555555556, + 28.564028992979452, + 26.675430077635962 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "body_world": { + "volume_mm3": 1809.5573684677227, + "area_mm2": 1079.6813487046288, + "bounds_mm": [ + 58.67949192431122, + 193.07179676972447, + 322.0, + 84.0, + 216.9282032302755, + 330.0 + ], + "volume_weighted_solid_com_mm": [ + 71.43597100702056, + 205.05555555555554, + 326.67543007763595 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "occurrence_a": { + "volume_mm3": 1809.5573684677238, + "area_mm2": 1079.6813487046288, + "bounds_mm": [ + 958.6794919243113, + 43.07179676972448, + 62.0, + 984.0, + 66.92820323027551, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 971.4359710070205, + 55.05555555555554, + 66.67543007763597 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "occurrence_b": { + "volume_mm3": 1809.5573684677233, + "area_mm2": 1079.6813487046286, + "bounds_mm": [ + 1016.0, + 133.0717967697245, + 62.0, + 1041.3205080756888, + 156.92820323027553, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 1028.5640289929795, + 144.94444444444446, + 66.67543007763597 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + }, + "assembly_world": { + "volume_mm3": 3619.114736935446, + "area_mm2": 2159.362697409257, + "bounds_mm": [ + 958.6794919243113, + 43.07179676972448, + 62.0, + 1041.3205080756888, + 156.92820323027553, + 70.0 + ], + "volume_weighted_solid_com_mm": [ + 1000.0, + 100.00000000000001, + 66.67543007763597 + ], + "bidirectional_boolean_difference_volume_mm3": [ + 0.0, + 0.0 + ] + } + }, + "actual_reedit_volume_mm3": 1822.1237390820797 + }, + { + "name": "valid270_native90_reedit", + "passed": true, + "volume_mm3": 235.61944901923448 + }, + { + "name": "ordinary_user_sheet_real_input", + "passed": true, + "cad": { + "parameter_key": "Parameters:QIHeight", + "height_mm": 12.0, + "volume_mm3": 72.0 + } + }, + { + "name": "parameters_after_BOM_actual_reedit_and_direct_export", + "passed": true, + "restored": { + "parameter_key": "Parameters:QIHeight", + "height_mm": 8.0, + "volume_mm3": 48.0, + "mass_kg": 0.0003768, + "cells": { + "A1": "'Поз.", + "A2": "'1", + "B1": "'Обозначение", + "B2": "'PARAM-BOX", + "C1": "'Наименование", + "C2": "'Брусок", + "C3": "'Итого", + "D1": "'Кол.", + "D2": "1", + "E1": "'Материал", + "E2": "'Сталь", + "F1": "'Масса, кг", + "F2": "'0,0003768", + "F3": "'0,0003768" + }, + "actual_mass_cell": "'0,0003768" + }, + "immediate": { + "parameter_key": "Parameters:QIHeight", + "height_mm": 6.0, + "volume_mm3": 36.0, + "mass_kg": 0.0002826, + "cells": { + "A1": "'Поз.", + "A2": "'1", + "B1": "'Обозначение", + "B2": "'PARAM-BOX", + "C1": "'Наименование", + "C2": "'Брусок", + "C3": "'Итого", + "D1": "'Кол.", + "D2": "1", + "E1": "'Материал", + "E2": "'Сталь", + "F1": "'Масса, кг", + "F2": "'0,0002826", + "F3": "'0,0002826" + }, + "actual_mass_cell": "'0,0002826" + }, + "direct_DXF": { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\restored-height6-direct.dxf", + "sha256": "ff21b3ad86ef0eff449d37e0dac7044586abee3097eac269bbb2cba64cd5c0ba", + "texts": [ + "Поз.", + "1", + "Обозначение", + "PARAM-BOX", + "Наименование", + "Брусок", + "Итого", + "Кол.", + "1", + "Материал", + "Сталь", + "Масса, кг", + "0,0002826", + "0,0002826" + ], + "actual_native_front_width_mm": 2.0, + "actual_native_front_height_mm": 6.0 + } + } + ], + "source_stable": true, + "native_exit_code": 0, + "native_pid": 4500, + "report_path": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-preflight-reopen-p4grki90\\report.json" + } + } + ], + "gui": true, + "source_sha256": { + "src/QInventor/Init.py": "7d2681813d2efe5a3b79147da578b67290117e61575e4e1735dd6a9aab7a37e1", + "src/QInventor/InitGui.py": "0f6fa12520a656980d3a3d61582cfcbebfa3f41728d63b75ad2fb001116a850e", + "src/QInventor/qi_bom.py": "548b7196392650b790f8263440cc12039eb1aae20c19ce6158e4588f9cc2053d", + "src/QInventor/qi_bom_ui.py": "522ea28fcce9f7a22e0ea107304ca32406846ec99af16b6adf04ef3da008811d", + "src/QInventor/qi_commands.py": "19816cbda6a9cc15023484022d0a13f84bd176d6e9eae0b681b968da21732f61", + "src/QInventor/qi_drawing_bom.py": "143dfa91623c6ac08c80e02e1671f6628ec18a5fd551bda362d7d99d252a4d11", + "src/QInventor/qi_drawing_bom_export.py": "1e0fbe90cafcf2321ce4eae90868211be7db105767f98bcf42789950d52dab2f", + "src/QInventor/qi_drawing_bom_ui.py": "8a50502dd648a42a2e6e4b436972c67b71ec0683f0c4276b13f313357d0c103a", + "src/QInventor/qi_extrude.py": "03b5b7eea4adbf168d623995e84e0d3af4ccea58494b3b34542b10447ef0c072", + "src/QInventor/qi_io.py": "77ea249f6d3a374e89ac294e0de8a386356ed47f908ba494cbe8fea37c34df1f", + "src/QInventor/qi_localization.py": "84041c5888157e7844b99ebda3fddf9e86531667c9e1aa56cde917795f1b91e1", + "src/QInventor/qi_messages.py": "5c0c41cf0c1bd60f30d0aec0da6934c2af70a88765d86c07e500046714eeecb5", + "src/QInventor/qi_model.py": "08c094382cfadd5e94e3ab6d1ce01ad1214985cc2a3222814ab5b2da26c4d762", + "src/QInventor/qi_parameters.py": "9f384e40b930b6ebc4e7f068651c0797134818803b82704258e26215997c01c2", + "src/QInventor/qi_parameters_ui.py": "4e10337d61bb38f229736f0e877d16cc99106943fa3a8f4333fa905aad7ec19f", + "src/QInventor/qi_projection.py": "a4923dd69f7dcb3230a4b60bd001698d90d6d02db5aa016362034ee73520925d", + "src/QInventor/qi_sketch_offset.py": "ffb0f2a000b91528587110d7f5d47b7e26902b499e1b35220e3c09fc89786cb9", + "src/QInventor/qi_sketch_patterns.py": "c98dfd369c639436f0aed0664cf43cc6345b6639c102309efb90041fe65d4e1e", + "src/QInventor/qi_sketch_preview.py": "4858dc39046bad95306179212fe44a63e2e1437259a9c6d14bc847b68c7aab76", + "src/QInventor/qi_sketch_ui.py": "d505b802c65f1566a64d7687e290a64226aff91958db59b8d621ac69909ead37", + "src/QInventor/qi_step_export.py": "754f7f8b841f87a696ddf800e19f947d5e8712fc53faf8fce4d2d0d0f3ee51f6", + "src/QInventor/qi_ui.py": "7432d1939919b30e5fc81cd333caf220ca3b490d71851328aa26d8070a7ac307", + "src/QInventor/qi_workbench.py": "312d49f40d78e8bde48cd8fa57232cd380565fe5b30a688e4612d57bde210c59", + "src/QInventor/resources/qinventor-a4.svg": "f77419e8559cb24b29a62ebcdc02d781f52c6910c0299d4820d1dd8c820c4128", + "src/QInventor/resources/qinventor.ico": "ea229e71d0fcdd749c35977fc0f11720a972c3ee04b3d9042d850182e0a6ed1c", + "src/QInventor/resources/qinventor.png": "3de618b2d07a1fd36a14f71d3f0b85f36a5dcc22ccf070b4629491da6e6f9e32", + "src/QInventor/resources/qinventor.svg": "12e95fa28c4aaf927311a777b3928d0518799e51663aab2f0f036015210dc954", + "tests/assembly_suite.py": "988aaa37573ab5c6543fd4f8f67b7fa7fff604721dcc6fe31b7c71abea747709", + "tests/bom_gui_suite.py": "043aa3632033bf5a6cc4fba0b1167b9cc6e89610f1db562067b00b32c62640de", + "tests/bom_suite.py": "2a52ef5fb7f99f4f21d6858f0860eba2db0a18993f7092059029c3f73b145f9e", + "tests/command_workflows_suite.py": "9c2cd271b6cde306dc9c228755579c299ead6ab6d4773d83dfefe7b37ecdb950", + "tests/drawing_bom_gui_suite.py": "12a60a1f8550922b5e3b069df09e3416111e367ba82a1d2f25fed5abf7555393", + "tests/drawing_bom_suite.py": "3da48ba3aa9b7f70ac02732fe41699776542d72e840a4d5843bc98f9e72a2509", + "tests/extrude_suite.py": "83da33ea4ce4d5da538f46e0a03a88acdb4e46a1424da32d899602a4b735605a", + "tests/gui_suite.py": "87cee20dc57dffa1384be594d7e8ba96580808685c0ec9419c8fd3a3a4f9007b", + "tests/io_suite.py": "53f8662e37fac45216d2858e732a2e8bbccf3d344c607aa57a99d9cb8d630c45", + "tests/kernel_suite.py": "bfd56a485e11b76f4b402439185db813601d93ce001b54a68e0303826157f3f8", + "tests/parameter_preflight_suite.py": "dc0dd00f59ea60baa90ab6930d7b8a68ebcb33f5e5ea00f3354d94a67d75a966", + "tests/parameter_table_gui_suite.py": "c19beaaf1c0dda693dbdd964319b1c199ea51bb79a4300f0b15177c15b5f8489", + "tests/parameter_table_suite.py": "f0c9eb2975bbac0a5d6a0a6f50bf64bc8cbd094b09942c167207574d3e1ba895", + "tests/parameters_suite.py": "7bbcc44d6d9f1595ac2ab90e8a8f01fb1d0d0acfd32294241d3ef595713f985b", + "tests/probe.FCMacro": "928efa45b20a145f313cf2ae6dcbcbc7ee9f3609df67ec678078aed8e264bd07", + "tests/projection_suite.py": "77770dc7c82656794ed68770d26c5da7871f2a8949ea8acc7ee637f8caaa1d24", + "tests/ribbon_camera_probe.FCMacro": "ed96daba842fbd362bf2ce7ec8014e011a46cc0941fae48b05f78714aeefc3c6", + "tests/ribbon_gui.FCMacro": "bbaa7367526cb924f096a97bc3f6005a6184061955abfdd15d0e91d1f5986575", + "tests/ribbon_probe.FCMacro": "b519579dcb0cee106aa40caf674fa611e105168299e933fe9930ac20a6238872", + "tests/ribbon_suite.py": "b5d5459532bdfb7113da2fea228248e87aa0e7abfa8ecec116e1222141aae05b", + "tests/sketch_offset_suite.py": "97de15e62d59b7c3113dda26d48670163853acf8a404067d676c6db3a2cc82f1", + "tests/sketch_operations_gui_suite.py": "4b088da978f506f3afe04fac6c9a3762895eb8fb1f21b2e7f5d8f08a656e53a1", + "tests/sketch_patterns_suite.py": "2205b605bd5ab7b8077cbd2421bdb469105d04daa5806fdc02edadb7a5f68414", + "scripts/bootstrap.ps1": "aeecd11aa2b609fbbbc6d4bcaf6eb6a4c236913aff26cd5096b3cb5a33dd499c", + "scripts/build-launcher.ps1": "b78168056b1009576872bc7d93f35fb7db5d409e8dd24418cfe74b42c30c6c7e", + "scripts/launcher/Program.cs": "7cee5b394ba055bb63619a5aef4c9b9db82055c3ca1a61a2550da221a996cfc5", + "scripts/launcher/QInventor.Launcher.csproj": "b46bb80c4b39b66a66b9d2e25ba97c186a9ca48d9e6bc4cdd11531b9c89990d8", + "scripts/launcher/RuntimePath.cs": "13fd9e93326afef703fa1926f83ea519dceaccb3781526e1e5e2c23243f78057", + "scripts/package.ps1": "9352bd79ee9ac41a1766745a86fd1e1c9193163f544cf838e122dcf4b770fdbb", + "scripts/render-icon.py": "2063594fde4d169f141b3a386f455be1f116b3c8a01f8bb257f4ff575b02aa42", + "scripts/startup.FCMacro": "813b4e857b3e184f3a41c7b72debfd995e7ee36ad2532aa3f57a4c878d6fc38b", + "scripts/sync.ps1": "e1349b8d1527bab9f700d0e8418a6e6be6d3df97838103efc5c473d2bcb3a5de", + "scripts/test.ps1": "6e827d4aa63e150006b2a77e77243d8445612d684bb0cea79ef4d67811c599dd", + "scripts/translator-probe.py": "5ee83e97742a65eb147227cf6c020f4307122908a3c06e1417727ee603a819ae" + }, + "source_stable": true, + "parameter_module": "C:\\Repos\\qinventor\\src\\QInventor\\qi_parameters.py", + "parameter_module_sha256": "9f384e40b930b6ebc4e7f068651c0797134818803b82704258e26215997c01c2" + } + }, + { + "name": "document_parameter_table_gui_suite", + "passed": true, + "result": { + "status": "passed", + "passed": 9, + "failed": 0, + "checks": [ + { + "name": "Add user parameters, edit model formulas and rebuild native solid", + "passed": true, + "evidence": { + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-formulas.png", + "native_project": "C:\\Repos\\qinventor\\artifacts\\validation\\document-parameters.FCStd", + "new_user_and_model_formulas_one_undo": true, + "expression_chain_drives_solid": true, + "hidden_draft_applies": true, + "FCStd_reopen": true + } + }, + { + "name": "Units and cyclic formulas reject atomically and recover in the open table", + "passed": true, + "evidence": { + "units_error": "Параметр «Size» должен быть числом в мм", + "cycle_error": "Обнаружена циклическая зависимость. Измените формулы так, чтобы параметр не зависел от самого себя.", + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-cycle-error.png", + "errors_keep_document_and_drafts": true, + "corrected_apply_rebuilds_native_solid": true + } + }, + { + "name": "Reference dimensions, unchanged precision and Close discard drafts", + "passed": true, + "evidence": { + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-reference-and-draft.png", + "untouched_native_precision": 3.141592653589793, + "reference_not_editable": true, + "close_discards_unapplied_rows_and_values": true + } + }, + { + "name": "User rename updates formula consumers and guarded deletion", + "passed": true, + "evidence": { + "native_rename_updates_consumers": true, + "referenced_delete_rejected": "Параметр или объект, указанный в формуле, не найден. Проверьте ссылку выбранной строки.", + "unused_parameter_removed_in_one_undo": true + } + }, + { + "name": "Closed document refuses pending Apply", + "passed": true, + "evidence": { + "closed_document_has_no_write": true, + "dialog_error_stays_visible": true + } + }, + { + "name": "Native Sketcher task protects its edit transaction", + "passed": true, + "evidence": { + "actual_Sketcher_task_blocks_floating_apply": true, + "after_finishing_task_apply_succeeds": true + } + }, + { + "name": "Parameter command updates a grouped existing drawing, exports and reopens", + "passed": true, + "evidence": { + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-existing-grouped-drawing.png", + "opened_by_QI_Dimensions_without_selection": true, + "grouped_native_page_preserved": true, + "front_heights_mm": [ + 24.0, + 32.0, + 32.0 + ], + "new_expressions_drive_existing_projection": true, + "FCStd_group_and_association_reopen": true, + "rejected_geometry_preserves_usable_native_redo": "Параметр «Radius» должен быть больше нуля", + "no_hidden_validation_documents": true, + "pdf": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-grouped-drawing.pdf", + "svg": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-grouped-drawing.svg", + "project": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-grouped-drawing.FCStd" + } + }, + { + "name": "Filtering clears hidden user selection and prevents unseen deletion", + "passed": true, + "evidence": { + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-filter-protected-selection.png", + "hidden_selected_row_cleared": true, + "hidden_delete_slot_refuses_removal": true, + "filter_does_not_write_native_document": true + } + }, + { + "name": "Native Undo, external edits and unnamed index shifts protect table drafts", + "passed": true, + "evidence": { + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\parameter-table-stale-constraint-edit.png", + "undo_refresh_preserves_redo": true, + "external_native_property_rejects_stale_draft": true, + "unnamed_constraint_index_shift_rejects_old_target": true, + "refresh_is_read_only_and_discards_drafts": true, + "own_apply_updates_baseline": true, + "external_edit_delivery": "FreeCAD native transactions and public property/constraint APIs" + } + } + ], + "input_delivery": "actual Qt buttons, keyboard and Russian input-method events in QI_ParameterTable", + "kernel_boundary": "native Sketcher, PartDesign Pad, App::VarSet and FreeCAD expressions", + "os_pointer_access": false, + "count": 9 + } + }, + { + "name": "assembly_joint_validation_suite", + "passed": true, + "result": { + "passed": true, + "checks": [ + { + "name": "Joint rejects connection to the same component before mutation", + "passed": true, + "evidence": { + "message": "Выберите два разных компонента для соединения", + "unchanged": true + } + }, + { + "name": "Joint and grounding reject components outside the assembly", + "passed": true, + "evidence": { + "joint_message": "Выберите компоненты выбранной сборки", + "ground_message": "Выберите компоненты выбранной сборки", + "unchanged": true + } + }, + { + "name": "Joint and grounding reject components from another document", + "passed": true, + "evidence": { + "joint_message": "Выберите компоненты выбранной сборки", + "ground_message": "Выберите компоненты выбранной сборки", + "both_documents_unchanged": true + } + }, + { + "name": "Conflicting native joint rolls back objects and all component placements", + "passed": true, + "evidence": { + "message": "Соединение «Жёсткое соединение002» конфликтует с положением компонентов. Проверьте существующие соединения.", + "all_three_placements_restored": true, + "remaining_solver_status": 0 + } + }, + { + "name": "Conflicting indirect loop preserves both previous fixed joints", + "passed": true, + "evidence": { + "message": "Соединение «Жёсткое соединение001» конфликтует с положением компонентов. Проверьте существующие соединения.", + "indirect_loop_rejected": true, + "both_previous_joints_preserved": true, + "all_three_placements_restored": true + } + }, + { + "name": "Accepted native joint is one undo step and survives redo save reopen", + "passed": true, + "evidence": { + "native_joint": true, + "undo_redo": true, + "reopened": true, + "project": "C:\\Repos\\qinventor\\artifacts\\validation\\atomic-joint.FCStd", + "solver_status": 0 + } + } + ], + "summary": { + "total": 6, + "passed": 6, + "failed": 0 + }, + "gui_exercised": true + } + }, + { + "name": "nested_assembly_bom_kernel_suite", + "passed": true, + "result": { + "passed": true, + "count": 14, + "checks": [ + { + "name": "STEP world frames readonly payload shared identities and atomic writer", + "passed": true, + "evidence": { + "exchanges": [ + { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\placed-root-default.step", + "solids": 7, + "volume_mm3": 2120.000000000001, + "world_com_mm": [ + 995.1981132075472, + 225.56603773584908, + 4.830188679245285 + ], + "source_exact_and_redo_executes": true, + "products": [ + [ + "Экспорт модели", + "Экспорт модели" + ], + [ + "Assembly", + "Assembly" + ], + [ + "Узел", + "Узел" + ], + [ + "Болт", + "Болт" + ], + [ + "Корпус", + "Корпус" + ], + [ + "Узел", + "Узел" + ] + ], + "occurrences": [ + "Assembly", + "SubA", + "BoltA001", + "BoltB001", + "HousingOccurrence001", + "SubB", + "BoltA002", + "BoltB002", + "HousingOccurrence002", + "DirectBolt" + ] + }, + { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\placed-root-explicit.step", + "solids": 7, + "volume_mm3": 2120.000000000001, + "world_com_mm": [ + 995.1981132075472, + 225.56603773584908, + 4.830188679245285 + ], + "source_exact_and_redo_executes": true, + "products": [ + [ + "Экспорт модели", + "Экспорт модели" + ], + [ + "Assembly", + "Assembly" + ], + [ + "Узел", + "Узел" + ], + [ + "Болт", + "Болт" + ], + [ + "Корпус", + "Корпус" + ], + [ + "Узел", + "Узел" + ] + ], + "occurrences": [ + "Assembly", + "SubA", + "BoltA001", + "BoltB001", + "HousingOccurrence001", + "SubB", + "BoltA002", + "BoltB002", + "HousingOccurrence002", + "DirectBolt" + ] + }, + { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\placed-body-default.step", + "solids": 1, + "volume_mm3": 24.000000000000007, + "world_com_mm": [ + 28.200961894323342, + 70.11602540378443, + 102.00000000000001 + ], + "source_exact_and_redo_executes": true, + "products": [ + [ + "Экспорт модели", + "Экспорт модели" + ], + [ + "Брусок", + "Брусок" + ] + ], + "occurrences": [ + "Брусок" + ] + }, + { + "path": "C:\\Repos\\qinventor\\artifacts\\validation\\placed-body-explicit.step", + "solids": 1, + "volume_mm3": 24.000000000000007, + "world_com_mm": [ + 28.200961894323342, + 70.11602540378443, + 102.00000000000001 + ], + "source_exact_and_redo_executes": true, + "products": [ + [ + "Экспорт модели", + "Экспорт модели" + ], + [ + "Брусок", + "Брусок" + ] + ], + "occurrences": [ + "Брусок" + ] + } + ], + "native_writer_fault_old_bytes_and_source_exact": true, + "native_instance_identity_labels_and_world_geometry_proved": true + } + }, + { + "name": "Nested rigid/flexible quantities density and world COM", + "passed": true, + "evidence": { + "flat": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "BOLT", + "name": "Болт", + "quantity": 5, + "quantity_total": 5, + "material": "Сталь", + "density_kg_m3": 7850.000000000001, + "unit_mass_kg": 0.00018840000000000013, + "mass_kg": 0.0009420000000000007, + "source_id": "QInventorPart3#Body", + "source_name": "Body", + "source_doc": "QInventorPart3", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart3#Body|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "HOUSING", + "name": "Корпус", + "quantity": 2, + "quantity_total": 2, + "material": "Алюминий", + "density_kg_m3": 2700.0, + "unit_mass_kg": 0.0027000000000000014, + "mass_kg": 0.005400000000000003, + "source_id": "QInventorPart3#Body001", + "source_name": "Body001", + "source_doc": "QInventorPart3", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart3#Body001|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Алюминий', 2700.0, 1000.0, 600.0)", + "materials": [ + "Алюминий" + ] + } + ], + "mass": { + "volume_mm3": 2120.000000000001, + "area_mm2": 1460.0000000000002, + "mass_kg": 0.006342000000000003, + "density_kg_m3": 2991.509433962264, + "material": "Смешанные материалы", + "materials": [ + "Сталь", + "Алюминий" + ], + "center_mm": [ + 995.5198675496691, + 226.48533585619683, + 4.554399243140967 + ], + "dimensions_mm": [ + 10.0, + 202.00000000000003, + 10.000000000000002 + ], + "solids": 7, + "components": 7, + "assembly_count": 3 + }, + "independent_world_com": [ + 995.5198675496688, + 226.4853358561968, + 4.554399243140964 + ], + "rigid_and_flexible_native_links": true, + "read_only_persistence_history": true + } + }, + { + "name": "Structured local/cumulative multiplicity and explicit roots", + "passed": true, + "evidence": { + "rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "SubAssembly", + "name": "Узел", + "quantity": 2, + "quantity_total": 2, + "material": "Смешанные материалы", + "density_kg_m3": 2935.87786259542, + "unit_mass_kg": 0.0030767999999999993, + "mass_kg": 0.006153599999999999, + "source_id": "QInventorPart4#SubAssembly", + "source_name": "SubAssembly", + "source_doc": "QInventorPart4", + "is_assembly": true, + "kind": "assembly", + "row_id": "QInventorPart4#SubAssembly|1", + "variant_id": "('assembly', ((('QInventorPart4', 'Body'), ((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0), (('QInventorPart4', 'Body'), ((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0), (('QInventorPart4', 'Body001'), ((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Алюминий', 2700.0, 1000.0, 600.0)))", + "materials": [ + "Алюминий", + "Сталь" + ] + }, + { + "position": "1.1", + "parent_position": "1", + "level": 1, + "part_number": "BOLT", + "name": "Болт", + "quantity": 2, + "quantity_total": 4, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.00018840000000000003, + "mass_kg": 0.0007536000000000001, + "source_id": "QInventorPart4#Body", + "source_name": "Body", + "source_doc": "QInventorPart4", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart4#Body|1.1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "1.2", + "parent_position": "1", + "level": 1, + "part_number": "HOUSING", + "name": "Корпус", + "quantity": 1, + "quantity_total": 2, + "material": "Алюминий", + "density_kg_m3": 2700.0, + "unit_mass_kg": 0.0026999999999999993, + "mass_kg": 0.0053999999999999986, + "source_id": "QInventorPart4#Body001", + "source_name": "Body001", + "source_doc": "QInventorPart4", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart4#Body001|1.2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Алюминий', 2700.0, 1000.0, 600.0)", + "materials": [ + "Алюминий" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "BOLT", + "name": "Болт", + "quantity": 1, + "quantity_total": 1, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.00018840000000000003, + "mass_kg": 0.00018840000000000003, + "source_id": "QInventorPart4#Body", + "source_name": "Body", + "source_doc": "QInventorPart4", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart4#Body|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + } + ], + "scoped_subassembly": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "BOLT", + "name": "Болт", + "quantity": 2, + "quantity_total": 2, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.00018840000000000003, + "mass_kg": 0.00037680000000000005, + "source_id": "QInventorPart4#Body", + "source_name": "Body", + "source_doc": "QInventorPart4", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart4#Body|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "HOUSING", + "name": "Корпус", + "quantity": 1, + "quantity_total": 1, + "material": "Алюминий", + "density_kg_m3": 2700.0, + "unit_mass_kg": 0.0026999999999999993, + "mass_kg": 0.0026999999999999993, + "source_id": "QInventorPart4#Body001", + "source_name": "Body001", + "source_doc": "QInventorPart4", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart4#Body001|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Алюминий', 2700.0, 1000.0, 600.0)", + "materials": [ + "Алюминий" + ] + } + ], + "summary_never_double_counts": true, + "actual_scope_roots": [ + "Assembly", + "SubA", + "SubB" + ] + } + }, + { + "name": "CAD source identity independent of labels and part numbers", + "passed": true, + "evidence": { + "rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "SAME", + "name": "Одинаковая подпись", + "quantity": 1, + "quantity_total": 1, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.00018840000000000003, + "mass_kg": 0.00018840000000000003, + "source_id": "QInventorPart5#Body", + "source_name": "Body", + "source_doc": "QInventorPart5", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart5#Body|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "SAME", + "name": "Одинаковая подпись001", + "quantity": 1, + "quantity_total": 1, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.00037680000000000005, + "mass_kg": 0.00037680000000000005, + "source_id": "QInventorPart5#Body001", + "source_name": "Body001", + "source_doc": "QInventorPart5", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart5#Body001|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 48.0, 80.0)", + "materials": [ + "Сталь" + ] + } + ], + "labels_and_numbers_do_not_define_identity": true + } + }, + { + "name": "Native hidden arrays mirror and nonuniform scales", + "passed": true, + "evidence": { + "virtual_hidden_array_elements_counted": true, + "array_variants": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "Bolt", + "name": "Bolt", + "quantity": 1, + "quantity_total": 1, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.00018839999999999997, + "mass_kg": 0.00018839999999999997, + "source_id": "QInventorPart6#Bolt", + "source_name": "Bolt", + "source_doc": "QInventorPart6", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart6#Bolt|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "Bolt", + "name": "Bolt", + "quantity": 1, + "quantity_total": 1, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.0015071999999999998, + "mass_kg": 0.0015071999999999998, + "source_id": "QInventorPart6#Bolt", + "source_name": "Bolt", + "source_doc": "QInventorPart6", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart6#Bolt|2", + "variant_id": "(((4.0, 0.0, 0.0, 4.0, 0.0, 4.0), 1), 'Сталь', 7850.0, 192.0, 208.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "3", + "parent_position": "", + "level": 0, + "part_number": "Bolt", + "name": "Bolt", + "quantity": 1, + "quantity_total": 1, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.005086800000000001, + "mass_kg": 0.005086800000000001, + "source_id": "QInventorPart6#Bolt", + "source_name": "Bolt", + "source_doc": "QInventorPart6", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart6#Bolt|3", + "variant_id": "(((9.0, 0.0, 0.0, 9.0, 0.0, 9.0), 1), 'Сталь', 7850.0, 648.0, 468.0)", + "materials": [ + "Сталь" + ] + } + ], + "mirrored_positive_physical_mass": true, + "nonuniform_scaling": true + } + }, + { + "name": "Expanded and virtual arrays preserve materials without duplicate roots", + "passed": true, + "evidence": { + "both_native_array_representations": [ + { + "expanded_elements": false, + "rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "Bolt", + "name": "Bolt", + "quantity": 3, + "quantity_total": 3, + "material": "ABS", + "density_kg_m3": 999.9999999999999, + "unit_mass_kg": 2.3999999999999997E-05, + "mass_kg": 7.199999999999999E-05, + "source_id": "QInventorPart7#Bolt", + "source_name": "Bolt", + "source_doc": "QInventorPart7", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart7#Bolt|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'ABS', 1000.0, 24.0, 52.0)", + "materials": [ + "ABS" + ] + } + ], + "components": 3, + "actual_material_service_used": true, + "array_view_properties": [ + "ChildViewProvider", + "DisplayMode", + "DrawStyle", + "LineWidth", + "MaterialList", + "OnTopWhenSelected", + "OverrideColorList", + "OverrideMaterial", + "OverrideMaterialList", + "PointSize", + "Selectable", + "SelectionStyle", + "ShapeMaterial", + "ShowInTree", + "Visibility" + ], + "assembly_view_properties": [ + "DisplayMode", + "OnTopWhenSelected", + "SelectionStyle", + "ShowInTree", + "TransformOrigin", + "Visibility" + ] + }, + { + "expanded_elements": true, + "rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "Bolt", + "name": "Bolt", + "quantity": 3, + "quantity_total": 3, + "material": "ABS", + "density_kg_m3": 999.9999999999999, + "unit_mass_kg": 2.3999999999999997E-05, + "mass_kg": 7.199999999999999E-05, + "source_id": "QInventorPart8#Bolt", + "source_name": "Bolt", + "source_doc": "QInventorPart8", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart8#Bolt|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'ABS', 1000.0, 24.0, 52.0)", + "materials": [ + "ABS" + ] + } + ], + "components": 3, + "actual_material_service_used": true, + "array_view_properties": [ + "ChildViewProvider", + "DisplayMode", + "DrawStyle", + "LineWidth", + "MaterialList", + "OnTopWhenSelected", + "OverrideColorList", + "OverrideMaterial", + "OverrideMaterialList", + "PointSize", + "Selectable", + "SelectionStyle", + "ShapeMaterial", + "ShowInTree", + "Visibility" + ], + "assembly_view_properties": [ + "DisplayMode", + "OnTopWhenSelected", + "SelectionStyle", + "ShowInTree", + "TransformOrigin", + "Visibility" + ] + } + ], + "parent_array_material_used": true, + "expanded_elements_not_extra_document_roots": true, + "container_density_does_not_replace_leaf_density": true + } + }, + { + "name": "Effective scale cancellation rootless transforms and mirror identity", + "passed": true, + "evidence": { + "uniform_scale_chain_cancels": true, + "rootless_effective_transform": true, + "mirror_variant_separate": true, + "rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "Bolt", + "name": "Bolt", + "quantity": 2, + "quantity_total": 2, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.00018839999999999997, + "mass_kg": 0.00037679999999999994, + "source_id": "QInventorPart9#Bolt", + "source_name": "Bolt", + "source_doc": "QInventorPart9", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart9#Bolt|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "Bolt", + "name": "Bolt", + "quantity": 1, + "quantity_total": 1, + "material": "Сталь", + "density_kg_m3": 7850.0, + "unit_mass_kg": 0.00018839999999999997, + "mass_kg": 0.00018839999999999997, + "source_id": "QInventorPart9#Bolt", + "source_name": "Bolt", + "source_doc": "QInventorPart9", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart9#Bolt|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), -1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + } + ] + } + }, + { + "name": "Container Boolean history native mirroring and helper groups", + "passed": true, + "evidence": { + "native_inputs_not_counted_inside_container": true, + "actual_source_link_counted": true, + "helper_sketch_group_omitted": true, + "native_Mirroring_Source_consumed": true + } + }, + { + "name": "External source recompute and generic native container links", + "passed": true, + "evidence": { + "external_dependency_recomputed": true, + "generic_AppPart_link_traversed": true, + "scope_selector_contains_only_addressable_local_occurrences": [ + "Assembly", + "ExternalContainer" + ] + } + }, + { + "name": "Consumed Boolean history and native density units", + "passed": true, + "evidence": { + "Boolean_base_and_tool_are_not_independent_parts": true, + "explicit_base_occurrence_counted": true, + "native_density_quantity_converted_to_kg_m3": true + } + }, + { + "name": "Cached native feature errors density and scoped validation", + "passed": true, + "evidence": { + "cached_valid_BRep_invalid_feature_rejected": true, + "explicit_scope_ignores_unrelated_failed_body": true, + "invalid_density_rejected": 4, + "wire_solid_compound_rejected": true + } + }, + { + "name": "Missing targets and native link/assembly cycles", + "passed": true, + "evidence": { + "missing_and_wrong_assembly_target_rejected": true, + "missing_wrong_target_gui_exercised": true, + "native_cycle_evidence": { + "passed": true, + "service_path": "C:\\Repos\\qinventor\\src\\QInventor\\qi_bom.py", + "service_sha256": "548b7196392650b790f8263440cc12039eb1aae20c19ce6158e4588f9cc2053d", + "suite_sha256": "2a52ef5fb7f99f4f21d6858f0860eba2db0a18993f7092059029c3f73b145f9e", + "freecad_version": [ + "1", + "1", + "4", + "20260928 (Git shallow)", + "Unknown", + "2026/09/28 13:46:34", + "(HEAD detached at 4fd3bf320)", + "4fd3bf320d9566a27e60069fc8387448aaa3a094" + ], + "gui_up": false, + "user_cfg": "C:\\Repos\\qinventor\\artifacts\\validation\\bom-cycle-02rn2633\\user.cfg", + "system_cfg": "C:\\Repos\\qinventor\\artifacts\\validation\\bom-cycle-02rn2633\\system.cfg", + "evidence": { + "real_two_link_cycle_guarded_before_delegated_geometry": true, + "real_cross_assembly_cycle_guarded": true, + "rejection_count": 4, + "rejections": [ + "Обнаружен цикл в составе сборки «A»", + "Обнаружен цикл в составе сборки «A»", + "Обнаружен цикл в составе сборки «A»", + "Обнаружен цикл в составе сборки «AssemblyA»" + ], + "gui_up": false + }, + "native_exit_code": 0, + "native_pid": 21276, + "engine_path": "C:\\Repos\\qinventor\\runtime\\FreeCAD_1.1.4-Windows-x86_64-py311\\bin\\FreeCADCmd.exe", + "report_path": "C:\\Repos\\qinventor\\artifacts\\validation\\bom-cycle-02rn2633\\cycle-report.json", + "execution_boundary": "owned headless FreeCADCmd; parent missing/wrong targets remain GUI" + } + } + }, + { + "name": "Atomic CSV validation old bytes and empty scope", + "passed": true, + "evidence": { + "flat_csv": "C:\\Repos\\qinventor\\artifacts\\validation\\nested-flat.csv", + "structured_csv": "C:\\Repos\\qinventor\\artifacts\\validation\\nested-structured.csv", + "old_bytes_preserved_on_failure": true, + "no_parent_directory_on_invalid_input": true, + "empty_export_rejected": true + } + }, + { + "name": "FCStd source edits Undo/Redo and STEP instance proof", + "passed": true, + "evidence": { + "fcstd": "C:\\Repos\\qinventor\\artifacts\\validation\\nested-assembly.FCStd", + "step": "C:\\Repos\\qinventor\\artifacts\\validation\\nested-assembly.step", + "step_leaf_solids": 7, + "step_volume_mm3": 2240.0, + "source_edit_and_undo_redo_updates_all_occurrences": true, + "step_world_centers": [ + [ + 102.0, + 1.5, + 2.0 + ], + [ + 112.0, + 1.5, + 2.0 + ], + [ + 125.0, + 5.0, + 5.0 + ], + [ + 202.0, + 1.5, + 2.0 + ], + [ + 212.0, + 1.5, + 2.0 + ], + [ + 225.0, + 5.0, + 5.0 + ], + [ + 302.0, + 1.5, + 2.0 + ] + ] + } + } + ], + "summary": { + "total": 14, + "passed": 14, + "failed": 0 + }, + "gui_exercised": true, + "cycle_execution_boundary": "owned headless FreeCADCmd child" + } + }, + { + "name": "nested_assembly_bom_gui_suite", + "passed": true, + "result": { + "status": "passed", + "passed": 3, + "failed": 0, + "checks": [ + { + "name": "Native insertion, flat and structured quantities, materials and nested scope", + "passed": true, + "evidence": { + "native_inserted_subassemblies": 2, + "native_inserted_direct_bolts": 1, + "flat_quantities": { + "GUI-HOUSING": 2, + "GUI-BOLT": 5 + }, + "mixed_mass_kg": 0.006633075116533993, + "prototype_excluded": true, + "construction_sketch_container_has_no_BOM_rows": true, + "assembly_material_rejected_before_chooser_without_mutation": [ + { + "type": "Assembly::AssemblyObject", + "message": "Выберите конечную деталь или её экземпляр. Материалы сборки задаются у входящих деталей." + }, + { + "type": "Assembly::AssemblyLink", + "message": "Выберите конечную деталь или её экземпляр. Материалы сборки задаются у входящих деталей." + } + ], + "physical_counts_ignore_visibility": true, + "structured_local_and_total_quantities": true, + "selected_live_nested_instance": true, + "screenshots": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\bom-gui-flat.png", + "C:\\Repos\\qinventor\\artifacts\\validation\\bom-gui-structured.png" + ] + } + }, + { + "name": "Russian mass properties, native source refresh, actual CSV dialogs and FCStd reopen", + "passed": true, + "evidence": { + "mass_properties_russian_labels": { + "Объём, мм³": "2157.08", + "Площадь поверхности, мм²": "1545.58", + "Масса, кг": "0.00663308", + "Средняя плотность, кг/м³": "3075.03", + "Материал": "Смешанные материалы", + "Материалы деталей": "Сталь, Алюминий", + "Центр масс, мм": "6.74; 4.13743; 5.06692", + "Габариты, мм": "21; 10.9989; 11.7998", + "Твёрдых тел": "7", + "Экземпляров деталей": "7", + "Сборок": "3" + }, + "source_edit_refresh": true, + "export_refreshes_current_model": true, + "native_save_dialog_keyboard_and_button": true, + "flat_csv": "C:\\Repos\\qinventor\\artifacts\\validation\\bom-gui-flat.csv", + "structured_csv": "C:\\Repos\\qinventor\\artifacts\\validation\\bom-gui-structured.csv", + "FCStd_external_sources_reopened": true, + "mass_kg_after_reopen": 0.012033075116533992, + "screenshots": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\bom-gui-updated-and-exported.png", + "C:\\Repos\\qinventor\\artifacts\\validation\\bom-gui-reopened.png" + ] + } + }, + { + "name": "Invalid cached source, native task and closed document protect BOM export", + "passed": true, + "evidence": { + "invalid_cached_source_blocks_export": true, + "previous_csv_unchanged": "8dc2bb6168478bb57ea4ef83a0992357d21edde7da78a289a1250e477f4d881f", + "native_task_blocks_refresh": true, + "closed_document_refuses_refresh_and_export": true + } + } + ], + "input_delivery": "Qt native Assembly insertion list, BOM controls, native file and property dialogs", + "fixture_boundary": "native Assembly::AssemblyLink/App::Link instances of saved FCStd sources", + "os_pointer_access": false, + "fixture_attempts": [ + { + "number": 1, + "initial_docs": [], + "clicks": [ + { + "requested_source": { + "document": "QInventorAssembly", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Подсборка" + }, + "item_source": { + "document": "QInventorAssembly", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Подсборка" + }, + "item_at_click": { + "document": "QInventorAssembly", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Подсборка" + }, + "rectangle": [ + 24, + 18, + 229, + 18 + ], + "viewport": [ + 253, + 198 + ], + "before": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + }, + "after": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [ + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + } + ], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "GroundedJoint" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + } + }, + { + "requested_source": { + "document": "QInventorAssembly", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Подсборка" + }, + "item_source": { + "document": "QInventorAssembly", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Подсборка" + }, + "item_at_click": { + "document": "QInventorAssembly", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Подсборка" + }, + "rectangle": [ + 24, + 18, + 229, + 18 + ], + "viewport": [ + 253, + 198 + ], + "before": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [ + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + } + ], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "GroundedJoint" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + }, + "after": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [ + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + } + ], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "GroundedJoint" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + } + }, + { + "requested_source": { + "document": "QInventorPart", + "name": "Body001", + "type": "PartDesign::Body", + "label": "Болт" + }, + "item_source": { + "document": "QInventorPart", + "name": "Body001", + "type": "PartDesign::Body", + "label": "Болт" + }, + "item_at_click": { + "document": "QInventorPart", + "name": "Body001", + "type": "PartDesign::Body", + "label": "Болт" + }, + "rectangle": [ + 24, + 162, + 229, + 18 + ], + "viewport": [ + 253, + 198 + ], + "before": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [ + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + } + ], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "GroundedJoint" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + }, + "after": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [ + { + "document": "QInventorAssembly1", + "name": "Болт", + "type": "App::Link", + "label": "Болт005" + } + ], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "GroundedJoint" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "Болт", + "type": "App::Link", + "label": "Болт005" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + } + } + ], + "fixture_directory": "C:\\Repos\\qinventor\\artifacts\\validation\\bom-gui-fixture-47a83b90a0d34e11a7aab8fd904907d3", + "before_set_edit": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": null, + "QInventorPart": null + }, + "selection": [], + "active_assembly": null, + "view_assembly": null, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + }, + "expected_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "after_set_edit": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + }, + "after_native_command": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + }, + "before_accept": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [ + { + "document": "QInventorAssembly1", + "name": "Болт", + "type": "App::Link", + "label": "Болт005" + } + ], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "GroundedJoint" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "Болт", + "type": "App::Link", + "label": "Болт005" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + }, + "after_accept": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": { + "native_class": "ViewProviderAssembly", + "name": "", + "representation": "" + }, + "QInventorPart": null + }, + "selection": [], + "active_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_assembly": { + "document": "QInventorAssembly1", + "name": "Assembly", + "type": "Assembly::AssemblyObject", + "label": "Сборка спецификации" + }, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "GroundedJoint" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "Болт", + "type": "App::Link", + "label": "Болт005" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + }, + "actual_links": [ + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "Болт", + "type": "App::Link", + "label": "Болт005" + } + ], + "before_count_assert": { + "app_document": "QInventorAssembly1", + "gui_document": "QInventorAssembly1", + "in_edit": { + "QInventorAssembly": null, + "QInventorAssembly1": null, + "QInventorPart": null + }, + "selection": [], + "active_assembly": null, + "view_assembly": null, + "view_part": null, + "assembly_groups": { + "QInventorAssembly#Assembly": [ + { + "document": "QInventorAssembly", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "Основание закреплено" + }, + { + "document": "QInventorAssembly", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + }, + { + "document": "QInventorAssembly", + "name": "Component002", + "type": "App::Link", + "label": "Болт001" + } + ], + "QInventorAssembly1#Assembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints", + "type": "Assembly::JointGroup", + "label": "Joints" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "GroundedJoint", + "type": "App::FeaturePython", + "label": "GroundedJoint" + }, + { + "document": "QInventorAssembly1", + "name": "Подсборка001", + "type": "Assembly::AssemblyLink", + "label": "Подсборка" + }, + { + "document": "QInventorAssembly1", + "name": "Болт", + "type": "App::Link", + "label": "Болт005" + } + ], + "QInventorAssembly1#PrototypeAssembly": [ + { + "document": "QInventorAssembly1", + "name": "Joints001", + "type": "Assembly::JointGroup", + "label": "Joints001" + }, + { + "document": "QInventorAssembly1", + "name": "Component", + "type": "App::Link", + "label": "Корпус" + }, + { + "document": "QInventorAssembly1", + "name": "Component001", + "type": "App::Link", + "label": "Болт" + } + ] + } + } + } + ], + "count": 3 + } + }, + { + "name": "drawing_bom_kernel_suite", + "passed": true, + "result": { + "passed": true, + "checks": [ + { + "name": "Native table explicit projection scope DAG and no-op", + "passed": true, + "evidence": { + "native_types": [ + "Spreadsheet::SheetPython", + "TechDraw::DrawViewSpreadsheet" + ], + "draft_is_read_only": true, + "one_create_undo": true, + "no_page_backlink": true, + "snapshot": { + "mode": "flat", + "rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "BOLT", + "name": "Болт", + "quantity": 5, + "quantity_total": 5, + "material": "Сталь", + "density_kg_m3": 7850.000000000001, + "unit_mass_kg": 0.00018840000000000013, + "mass_kg": 0.0009420000000000007, + "source_id": "QInventorPart#Body", + "source_name": "Body", + "source_doc": "QInventorPart", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart#Body|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "HOUSING", + "name": "Корпус", + "quantity": 2, + "quantity_total": 2, + "material": "Алюминий", + "density_kg_m3": 2700.0, + "unit_mass_kg": 0.0027000000000000014, + "mass_kg": 0.005400000000000003, + "source_id": "QInventorPart#Body001", + "source_name": "Body001", + "source_doc": "QInventorPart", + "is_assembly": false, + "kind": "part", + "row_id": "QInventorPart#Body001|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Алюминий', 2700.0, 1000.0, 600.0)", + "materials": [ + "Алюминий" + ] + } + ], + "summary": { + "volume_mm3": 2120.000000000001, + "area_mm2": 1460.0000000000002, + "mass_kg": 0.006342000000000003, + "density_kg_m3": 2991.509433962264, + "material": "Смешанные материалы", + "materials": [ + "Сталь", + "Алюминий" + ], + "center_mm": [ + 995.5198675496691, + 226.48533585619683, + 4.554399243140967 + ], + "dimensions_mm": [ + 10.0, + 202.00000000000003, + 10.000000000000002 + ], + "solids": 7, + "components": 7, + "assembly_count": 3 + }, + "cells": { + "A1": "'Поз.", + "B1": "'Обозначение", + "C1": "'Наименование", + "D1": "'Кол.", + "E1": "'Материал", + "F1": "'Масса, кг", + "A2": "'1", + "B2": "'BOLT", + "C2": "'Болт", + "D2": "5", + "E2": "'Сталь", + "F2": "'0,000942", + "A3": "'2", + "B3": "'HOUSING", + "C3": "'Корпус", + "D3": "2", + "E3": "'Алюминий", + "F3": "'0,0054", + "C4": "'Итого", + "F4": "'0,006342" + }, + "layout": { + "x_mm": 218.0, + "y_mm": 86.0, + "width_mm": 137.28000000000003, + "height_mm": 28.160000000000004, + "column_widths_native": [ + 35, + 84, + 104, + 45, + 65, + 96 + ], + "row_height_native": 22, + "scale": 3.2, + "text_size_native": 9.0, + "font": "DejaVu Sans", + "last_row": 4, + "cell_start": "A1", + "cell_end": "F4", + "reserved_cells": "A:G", + "minimum_text_height_mm": 2.8800000000000003, + "printed_mass_significant_digits": 8, + "mixed_assembly_material_abbreviation": "Смеш." + }, + "source_names": [ + "Assembly" + ], + "source_labels": [ + "Assembly" + ], + "geometry_view_names": [ + "FrontView", + "TopView", + "IsoView" + ], + "sheet_name": "DrawingBOM", + "view_name": "BOMView", + "native_symbol_bounds_mm": { + "width": 137.28000000000003, + "height": 28.160000000000004 + } + } + } + }, + { + "name": "Automatic source labels native formulas and Undo Redo", + "passed": true, + "evidence": { + "automatic_source_edit": true, + "native_formula_outside_generated_range": 7, + "callback_max_depth": 1, + "all_execute_returns_false": true, + "label_only_observer_and_literal_formula_prefix": true, + "undo_redo_preserved": true + } + }, + { + "name": "Explicit scope repair and unrelated invalid body", + "passed": true, + "evidence": { + "explicit_rebind_repairs_old_sheet_invalid": true, + "unrelated_invalid_body_ignored": true, + "mixed_projection_sources_rejected_before_mutation": true, + "material_scope": "Алюминий" + } + }, + { + "name": "Native invalid last-good cells and nonhealing validation", + "passed": true, + "evidence": { + "density_error_native_invalid": true, + "cached_valid_pad_rejected": true, + "last_good_cells_preserved": true, + "validate_does_not_heal": true + } + }, + { + "name": "Structured quantities and overflow preserving redo", + "passed": true, + "evidence": { + "structured_local_total_quantities": true, + "mixed_material_print_abbreviation": "Смеш.", + "stable_E_cell_type_immediate_native_undo_redo": true, + "D_quantity_native_integer": true, + "quantity_total_metadata_numeric": true, + "native_overflow_reject_preserves_redo_branch": true + } + }, + { + "name": "Changing row count preserves user notes", + "passed": true, + "evidence": { + "removed_computed_rows_cleared": true, + "printed_range_updates": true, + "outside_notes_preserved": true + } + }, + { + "name": "Native top-root membership updates in one recompute", + "passed": true, + "evidence": { + "actual_projection_source": "Assembly", + "native_solid_counts": [ + 7, + 4, + 7 + ], + "sheet_components": [ + 7, + 4, + 7 + ], + "one_sheet_execute_per_native_recompute": true, + "fresh_native_symbol": true + } + }, + { + "name": "Insert Undo Redo and FCStd fresh native process", + "passed": true, + "evidence": { + "create_undo_removes_sheet_and_view": true, + "redo_restores_proxy": true, + "fcstd": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-associative.FCStd", + "fresh_native_process": { + "passed": true, + "gui_up": false, + "service_path": "C:\\Repos\\qinventor\\src\\QInventor\\qi_drawing_bom.py", + "service_sha256": "143dfa91623c6ac08c80e02e1671f6628ec18a5fd551bda362d7d99d252a4d11", + "restored_proxy": "DrawingBOMProxy", + "initial": { + "mode": "flat", + "rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "BOLT", + "name": "Болт", + "quantity": 5, + "quantity_total": 5, + "material": "Сталь", + "density_kg_m3": 7850.000000000001, + "unit_mass_kg": 0.00018840000000000013, + "mass_kg": 0.0009420000000000007, + "source_id": "drawing_bom_associative#Body", + "source_name": "Body", + "source_doc": "drawing_bom_associative", + "is_assembly": false, + "kind": "part", + "row_id": "drawing_bom_associative#Body|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 24.0, 52.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "HOUSING", + "name": "Корпус", + "quantity": 2, + "quantity_total": 2, + "material": "Алюминий", + "density_kg_m3": 2700.0, + "unit_mass_kg": 0.0027000000000000014, + "mass_kg": 0.005400000000000003, + "source_id": "drawing_bom_associative#Body001", + "source_name": "Body001", + "source_doc": "drawing_bom_associative", + "is_assembly": false, + "kind": "part", + "row_id": "drawing_bom_associative#Body001|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Алюминий', 2700.0, 1000.0, 600.0)", + "materials": [ + "Алюминий" + ] + } + ], + "summary": { + "volume_mm3": 2120.000000000001, + "area_mm2": 1460.0000000000002, + "mass_kg": 0.006342000000000003, + "density_kg_m3": 2991.509433962264, + "material": "Смешанные материалы", + "materials": [ + "Сталь", + "Алюминий" + ], + "center_mm": [ + 995.5198675496691, + 226.48533585619683, + 4.554399243140967 + ], + "dimensions_mm": [ + 10.0, + 202.00000000000003, + 10.000000000000002 + ], + "solids": 7, + "components": 7, + "assembly_count": 3 + }, + "cells": { + "A1": "'Поз.", + "B1": "'Обозначение", + "C1": "'Наименование", + "D1": "'Кол.", + "E1": "'Материал", + "F1": "'Масса, кг", + "A2": "'1", + "B2": "'BOLT", + "C2": "'Болт", + "D2": "5", + "E2": "'Сталь", + "F2": "'0,000942", + "A3": "'2", + "B3": "'HOUSING", + "C3": "'Корпус", + "D3": "2", + "E3": "'Алюминий", + "F3": "'0,0054", + "C4": "'Итого", + "F4": "'0,006342" + }, + "layout": { + "x_mm": 218.0, + "y_mm": 86.0, + "width_mm": 137.28000000000003, + "height_mm": 28.160000000000004, + "column_widths_native": [ + 35, + 84, + 104, + 45, + 65, + 96 + ], + "row_height_native": 22, + "scale": 3.2, + "text_size_native": 9.0, + "font": "DejaVu Sans", + "last_row": 4, + "cell_start": "A1", + "cell_end": "F4", + "reserved_cells": "A:G", + "minimum_text_height_mm": 2.8800000000000003, + "printed_mass_significant_digits": 8, + "mixed_assembly_material_abbreviation": "Смеш." + }, + "source_names": [ + "Assembly" + ], + "source_labels": [ + "Assembly" + ], + "geometry_view_names": [ + "FrontView", + "TopView", + "IsoView" + ], + "sheet_name": "DrawingBOM", + "view_name": "BOMView", + "native_symbol_bounds_mm": { + "width": 137.28000000000003, + "height": 28.160000000000004 + } + }, + "edited": { + "mode": "flat", + "rows": [ + { + "position": "1", + "parent_position": "", + "level": 0, + "part_number": "BOLT", + "name": "Болт", + "quantity": 5, + "quantity_total": 5, + "material": "Сталь", + "density_kg_m3": 7850.000000000001, + "unit_mass_kg": 0.00037680000000000027, + "mass_kg": 0.0018840000000000013, + "source_id": "drawing_bom_associative#Body", + "source_name": "Body", + "source_doc": "drawing_bom_associative", + "is_assembly": false, + "kind": "part", + "row_id": "drawing_bom_associative#Body|1", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Сталь', 7850.0, 48.0, 92.0)", + "materials": [ + "Сталь" + ] + }, + { + "position": "2", + "parent_position": "", + "level": 0, + "part_number": "HOUSING", + "name": "Корпус", + "quantity": 2, + "quantity_total": 2, + "material": "Алюминий", + "density_kg_m3": 2700.0, + "unit_mass_kg": 0.0027000000000000014, + "mass_kg": 0.005400000000000003, + "source_id": "drawing_bom_associative#Body001", + "source_name": "Body001", + "source_doc": "drawing_bom_associative", + "is_assembly": false, + "kind": "part", + "row_id": "drawing_bom_associative#Body001|2", + "variant_id": "(((1.0, 0.0, 0.0, 1.0, 0.0, 1.0), 1), 'Алюминий', 2700.0, 1000.0, 600.0)", + "materials": [ + "Алюминий" + ] + } + ], + "summary": { + "volume_mm3": 2240.000000000001, + "area_mm2": 1660.0000000000002, + "mass_kg": 0.007284000000000004, + "density_kg_m3": 3251.7857142857147, + "material": "Смешанные материалы", + "materials": [ + "Сталь", + "Алюминий" + ], + "center_mm": [ + 995.9052718286656, + 227.58649093904452, + 4.741350906095553 + ], + "dimensions_mm": [ + 10.0, + 202.00000000000003, + 10.000000000000002 + ], + "solids": 7, + "components": 7, + "assembly_count": 3 + }, + "cells": { + "A1": "'Поз.", + "B1": "'Обозначение", + "C1": "'Наименование", + "D1": "'Кол.", + "E1": "'Материал", + "F1": "'Масса, кг", + "A2": "'1", + "B2": "'BOLT", + "C2": "'Болт", + "D2": "5", + "E2": "'Сталь", + "F2": "'0,001884", + "A3": "'2", + "B3": "'HOUSING", + "C3": "'Корпус", + "D3": "2", + "E3": "'Алюминий", + "F3": "'0,0054", + "C4": "'Итого", + "F4": "'0,007284" + }, + "layout": { + "x_mm": 218.0, + "y_mm": 86.0, + "width_mm": 137.28000000000003, + "height_mm": 28.160000000000004, + "column_widths_native": [ + 35, + 84, + 104, + 45, + 65, + 96 + ], + "row_height_native": 22, + "scale": 3.2, + "text_size_native": 9.0, + "font": "DejaVu Sans", + "last_row": 4, + "cell_start": "A1", + "cell_end": "F4", + "reserved_cells": "A:G", + "minimum_text_height_mm": 2.8800000000000003, + "printed_mass_significant_digits": 8, + "mixed_assembly_material_abbreviation": "Смеш." + }, + "source_names": [ + "Assembly" + ], + "source_labels": [ + "Assembly" + ], + "geometry_view_names": [ + "FrontView", + "TopView", + "IsoView" + ], + "sheet_name": "DrawingBOM", + "view_name": "BOMView", + "native_symbol_bounds_mm": { + "width": 137.28000000000003, + "height": 28.160000000000004 + } + }, + "undo_redo": true, + "native_exit_code": 0, + "report_path": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-reopen-kb1j_hw6\\report.json", + "engine": "C:\\Repos\\qinventor\\runtime\\FreeCAD_1.1.4-Windows-x86_64-py311\\bin\\FreeCADCmd.exe", + "native_pid": 21452 + } + } + }, + { + "name": "Ordinary mass precision and untruncated text errors", + "passed": true, + "evidence": { + "ordinary_cylinder_mass_printed": "'0,0015536746", + "oversized_text_rejected_without_truncation": true + } + }, + { + "name": "Actual native table format position and missing-source guards", + "passed": true, + "evidence": { + "actual_native_xy_checked": true, + "native_font_change_rejected_and_repaired": true, + "native_rotation_rejected_and_repaired": true, + "lost_sheet_source_not_treated_as_absent_table": true, + "validation_read_only": true, + "native_undo_restores_source": true + } + } + ], + "summary": { + "total": 10, + "passed": 10, + "failed": 0 + }, + "gui_exercised": true + } + }, + { + "name": "drawing_bom_gui_suite", + "passed": true, + "result": { + "status": "passed", + "passed": 3, + "failed": 0, + "checks": [ + { + "name": "Actual ribbon preview cancel insertion mode update native Undo/Redo and A4 bounds", + "passed": true, + "evidence": { + "missing_and_ambiguous_page_rejected_before_dialog": true, + "preview_cancel_exact_native_state": true, + "page_sources_scope": [ + "Assembly" + ], + "single_native_transaction_create_and_update": true, + "undo_redo_mode": true, + "flat_symbol": { + "texts": [ + "Поз.", + "1", + "2", + "", + "Обозначение", + "BOLT", + "HOUSING", + "", + "Наименование", + "Болт", + "Корпус", + "Итого", + "Кол.", + "5", + "2", + "", + "Материал", + "Сталь", + "Алюминий", + "", + "Масса, кг", + "0,000942", + "0,0054", + "0,006342" + ], + "printed_bounds_mm": [ + 149.34249999999997, + 71.9025, + 286.6575, + 100.0975 + ], + "hash": "7524315da86dbb5c648ab293a320a3ec72312d8449bc6c83e4692b63ecefb083" + }, + "structured_symbol": { + "texts": [ + "Поз.", + "1", + "1.1", + "1.2", + "2", + "", + "Обозначение", + "SubAssembly", + "BOLT", + "HOUSING", + "BOLT", + "", + "Наименование", + "Узел", + "Болт", + "Корпус", + "Болт", + "Итого", + "Кол.", + "2", + "2", + "1", + "1", + "", + "Всего", + "2", + "4", + "2", + "1", + "", + "Материал", + "Смеш.", + "Сталь", + "Алюминий", + "Сталь", + "", + "Масса, кг", + "0,0061536", + "0,0007536", + "0,0054", + "0,0001884", + "0,006342" + ], + "printed_bounds_mm": [ + 149.25249995000001, + 66.84249994999999, + 286.74750005, + 105.15750005000001 + ], + "hash": "0439233204940ad7ce427f5e3ab0301ef8877883d9ea1b9bad38cea567260e65" + }, + "csv_caption_survives_language_change": true, + "screenshot": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-preview.png" + } + }, + { + "name": "Live seven-leaf source cells Symbol native PDF/SVG/DXF and independent FCStd restore", + "passed": true, + "evidence": { + "automatic_source_edit_cells_and_symbol": true, + "outside_source_ignored": true, + "updated": { + "svg": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-updated.svg", + "pdf": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-updated.pdf", + "dxf": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-updated.dxf", + "dxf_evidence": { + "version": "AC1021", + "actual_UTF8_text": [ + "Поз.", + "1", + "2", + "Обозначение", + "BOLT", + "HOUSING", + "Наименование", + "Болт", + "Корпус", + "Итого", + "Кол.", + "5", + "2", + "Материал", + "Сталь", + "Алюминий", + "Масса, кг", + "0,001884", + "0,0054", + "0,007284" + ], + "native_editable_text_readback": [ + "Поз.", + "1", + "2", + "Обозначение", + "BOLT", + "HOUSING", + "Наименование", + "Болт", + "Корпус", + "Итого", + "Кол.", + "5", + "2", + "Материал", + "Сталь", + "Алюминий", + "Масса, кг", + "0,001884", + "0,0054", + "0,007284" + ], + "native_text_properties": [ + { + "name": "Text", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text001", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text002", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text003", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text004", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text005", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text006", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text007", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text008", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text009", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text010", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text011", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text012", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text013", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text014", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text015", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text016", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text017", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text018", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text019", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + } + ], + "native_text_edit_restore": true, + "native_projection_entities_unchanged": true, + "native_projection_layers": [ + "FrontView", + "IsoView", + "TopView" + ], + "grid_paper_bounds_mm": [ + 149.36, + 71.92, + 286.64, + 100.08 + ], + "native_reader_stats": { + "dxfVersion": "AC1021", + "dxfEncoding": "utf_8", + "scalingSource": "$INSUNITS", + "fileUnits": "Millimeters", + "finalScalingFactor": 1.0, + "importTimeSeconds": 0.9060992, + "totalEntitiesCreated": 195, + "entityCounts": { + "LINE": 195, + "TEXT": 20 + }, + "importSettings": { + "Import hidden blocks": "No", + "Import layout objects": "No", + "Import points": "Yes", + "Import texts and dimensions": "Yes", + "Join geometry": "No", + "Manual scaling factor": "1.000000", + "Use colors from the DXF file": "Yes", + "Use layers": "Yes" + }, + "unsupportedFeatures": {}, + "systemBlockCounts": { + "Other System Blocks": 2 + } + } + }, + "rendered": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-updated-svg.png" + ], + "actual_SVG_grid_bounds_mm": [ + 149.377, + 109.937, + 286.621822, + 138.062064 + ], + "pdf_visual_review": "Separate offline renderer; the CAD runtime does not bundle QtPdf" + }, + "FCStd": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-bbd0614872a64bbd91261f1962cc1316.FCStd", + "independent_native_restore": { + "native_exit_code": 0, + "report": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-restore-_wo4e3mp\\report.json", + "mass_kg_after_edit": 0.007284000000000004 + }, + "reopened": { + "svg": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-reopened.svg", + "pdf": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-reopened.pdf", + "dxf": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-reopened.dxf", + "dxf_evidence": { + "version": "AC1021", + "actual_UTF8_text": [ + "Поз.", + "1", + "2", + "Обозначение", + "BOLT", + "HOUSING", + "Наименование", + "Болт", + "Корпус", + "Итого", + "Кол.", + "5", + "2", + "Материал", + "Сталь", + "Алюминий", + "Масса, кг", + "0,000942", + "0,0054", + "0,006342" + ], + "native_editable_text_readback": [ + "Поз.", + "1", + "2", + "Обозначение", + "BOLT", + "HOUSING", + "Наименование", + "Болт", + "Корпус", + "Итого", + "Кол.", + "5", + "2", + "Материал", + "Сталь", + "Алюминий", + "Масса, кг", + "0,000942", + "0,0054", + "0,006342" + ], + "native_text_properties": [ + { + "name": "Text", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text001", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text002", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text003", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text004", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text005", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text006", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text007", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text008", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text009", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text010", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text011", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text012", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text013", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text014", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text015", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text016", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text017", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text018", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + }, + { + "name": "Text019", + "type": "App::FeaturePython", + "property_type": "App::PropertyStringList", + "editor_flags": [] + } + ], + "native_text_edit_restore": true, + "native_projection_entities_unchanged": true, + "native_projection_layers": [ + "FrontView", + "IsoView", + "TopView" + ], + "grid_paper_bounds_mm": [ + 149.36, + 71.92, + 286.64, + 100.08 + ], + "native_reader_stats": { + "dxfVersion": "AC1021", + "dxfEncoding": "utf_8", + "scalingSource": "$INSUNITS", + "fileUnits": "Millimeters", + "finalScalingFactor": 1.0, + "importTimeSeconds": 0.8685385, + "totalEntitiesCreated": 195, + "entityCounts": { + "LINE": 195, + "TEXT": 20 + }, + "importSettings": { + "Import hidden blocks": "No", + "Import layout objects": "No", + "Import points": "Yes", + "Import texts and dimensions": "Yes", + "Join geometry": "No", + "Manual scaling factor": "1.000000", + "Use colors from the DXF file": "Yes", + "Use layers": "Yes" + }, + "unsupportedFeatures": {}, + "systemBlockCounts": { + "Other System Blocks": 2 + } + } + }, + "rendered": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-reopened-svg.png" + ], + "actual_SVG_grid_bounds_mm": [ + 149.377, + 109.937, + 286.621822, + 138.062064 + ], + "pdf_visual_review": "Separate offline renderer; the CAD runtime does not bundle QtPdf" + } + } + }, + { + "name": "Cached native errors A4 overflow and post-writer fault protect table and old exports", + "passed": true, + "evidence": { + "cached_invalid_shape_rejects_UI_and_export": true, + "A4_overflow_rejects_without_writes": true, + "last_good_table_cells_preserved": true, + "native_writer_fault_after_output": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\.qinventor-7oauva9g.svg", + "C:\\Repos\\qinventor\\artifacts\\validation\\.qinventor-5u7rz7w8.dxf" + ], + "previous_PDF_SVG_DXF_bytes_preserved": true, + "owned_temps_removed": true, + "user_companion_file_untouched": "C:\\Repos\\qinventor\\artifacts\\validation\\drawing-bom-protected.geometry-check.svg" + } + } + ], + "input_delivery": "actual Qt ribbon and drawing BOM controls; native CAD/PDF/SVG/DXF", + "os_pointer_access": false, + "count": 3 + } + }, + { + "name": "assembly_drawing_command_workflow_suite", + "passed": true, + "result": { + "status": "passed", + "passed": 6, + "failed": 0, + "checks": [ + { + "name": "actual_plane_dialog_cancel_without_document", + "passed": true, + "evidence": { + "cancel_clicked": true, + "dialog_seen": true, + "documents_preserved": true, + "no_active_document": true + } + }, + { + "name": "assembly_drawing_command_definition_edit_and_fcstd_exports", + "passed": true, + "evidence": { + "selection_required": false, + "linked_components": 3, + "views": 3, + "definition_edits_update_native_projections": true, + "fcstd_reopen_preserves_associativity": true, + "pdf_bytes": 225610, + "svg_bytes": 29847 + } + }, + { + "name": "empty_document_drawing_rejected_without_mutation", + "passed": true, + "evidence": { + "empty_rejected": true, + "objects_and_history_preserved": true + } + }, + { + "name": "assembly_title_material_follows_actual_view_sources", + "passed": true, + "evidence": { + "drawing_source": "Component", + "material": "Сталь", + "unrelated_aluminum_part_ignored_by_title": true + } + }, + { + "name": "failed_cached_native_solid_drawing_rejected_without_mutation", + "passed": true, + "evidence": { + "native_error": "Cannot create a pad with a total length of zero.", + "cached_volume_mm3": 999.9999999999998, + "page_not_created": true, + "objects_and_history_preserved": true + } + }, + { + "name": "existing_page_cached_linked_source_exports_rejected", + "passed": true, + "evidence": { + "linked_cached_native_failure_rejected": { + "dxf": "Объект «Выдавливание» содержит ошибку перестроения. Исправьте операцию перед экспортом чертежа.", + "pdf": "Объект «Выдавливание» содержит ошибку перестроения. Исправьте операцию перед экспортом чертежа.", + "svg": "Объект «Выдавливание» содержит ошибку перестроения. Исправьте операцию перед экспортом чертежа." + }, + "cached_volume_mm3": 999.9999999999998, + "existing_outputs_and_absent_directories_preserved": true, + "native_wire_page_exports_despite_unrelated_failed_source": { + "dxf": 7004, + "pdf": 214154, + "svg": 7099 + } + } + } + ], + "artifacts": [ + "C:\\Repos\\qinventor\\artifacts\\validation\\command-plane-cancel-dialog.png", + "C:\\Repos\\qinventor\\artifacts\\validation\\command-assembly-drawing-snapshots.json", + "C:\\Repos\\qinventor\\artifacts\\validation\\command-assembly-drawing-before.png", + "C:\\Repos\\qinventor\\artifacts\\validation\\command-assembly-drawing-updated.png", + "C:\\Repos\\qinventor\\artifacts\\validation\\command-assembly-drawing.FCStd", + "C:\\Repos\\qinventor\\artifacts\\validation\\command-assembly-drawing.svg", + "C:\\Repos\\qinventor\\artifacts\\validation\\command-assembly-drawing.pdf", + "C:\\Repos\\qinventor\\artifacts\\validation\\command-assembly-drawing-reopened.png", + "C:\\Repos\\qinventor\\artifacts\\validation\\command-assembly-title-material-scope.png" + ] + } + }, + { + "name": "sketch_dimension_finish_pad_workflow", + "passed": true, + "result": { + "sketch": "Sketch", + "volume_mm3": 12000.0 + } + }, + { + "name": "cancel_new_and_existing_sketch", + "passed": true, + "result": { + "existing_restored": true, + "new_removed": true + } + }, + { + "name": "drawing_pdf_svg_export", + "passed": true, + "result": { + "page": "Drawing", + "pdf_bytes": 163077, + "views": 3 + } + }, + { + "name": "russian_native_and_custom_surfaces", + "passed": true, + "result": { + "supplemental_sources": 385, + "visible_untranslated": 0, + "native_menu_titles": [ + "Файл", + "Изменить", + "Вид", + "Инструменты", + "Макросы", + "QInventor", + "Окна", + "Справка" + ], + "parameter_backend_error_russian": true, + "deleted_parameter_object_error_russian": true + } + }, + { + "name": "shipped_examples", + "passed": true, + "result": [ + "bracket.FCStd", + "bracket.step", + "assembly.FCStd", + "assembly.step" + ] + }, + { + "name": "source_snapshot_consistent", + "passed": true, + "result": true + } + ], + "passed": 26, + "failed": 0, + "source_sha256": [ + { + "path": "scripts\\bootstrap.ps1", + "sha256": "aeecd11aa2b609fbbbc6d4bcaf6eb6a4c236913aff26cd5096b3cb5a33dd499c" + }, + { + "path": "scripts\\build-launcher.ps1", + "sha256": "b78168056b1009576872bc7d93f35fb7db5d409e8dd24418cfe74b42c30c6c7e" + }, + { + "path": "scripts\\launcher\\Program.cs", + "sha256": "7cee5b394ba055bb63619a5aef4c9b9db82055c3ca1a61a2550da221a996cfc5" + }, + { + "path": "scripts\\launcher\\QInventor.Launcher.csproj", + "sha256": "b46bb80c4b39b66a66b9d2e25ba97c186a9ca48d9e6bc4cdd11531b9c89990d8" + }, + { + "path": "scripts\\launcher\\RuntimePath.cs", + "sha256": "13fd9e93326afef703fa1926f83ea519dceaccb3781526e1e5e2c23243f78057" + }, + { + "path": "scripts\\package.ps1", + "sha256": "9352bd79ee9ac41a1766745a86fd1e1c9193163f544cf838e122dcf4b770fdbb" + }, + { + "path": "scripts\\render-icon.py", + "sha256": "2063594fde4d169f141b3a386f455be1f116b3c8a01f8bb257f4ff575b02aa42" + }, + { + "path": "scripts\\startup.FCMacro", + "sha256": "813b4e857b3e184f3a41c7b72debfd995e7ee36ad2532aa3f57a4c878d6fc38b" + }, + { + "path": "scripts\\sync.ps1", + "sha256": "e1349b8d1527bab9f700d0e8418a6e6be6d3df97838103efc5c473d2bcb3a5de" + }, + { + "path": "scripts\\test.ps1", + "sha256": "6e827d4aa63e150006b2a77e77243d8445612d684bb0cea79ef4d67811c599dd" + }, + { + "path": "scripts\\translator-probe.py", + "sha256": "5ee83e97742a65eb147227cf6c020f4307122908a3c06e1417727ee603a819ae" + }, + { + "path": "src\\QInventor\\Init.py", + "sha256": "7d2681813d2efe5a3b79147da578b67290117e61575e4e1735dd6a9aab7a37e1" + }, + { + "path": "src\\QInventor\\InitGui.py", + "sha256": "0f6fa12520a656980d3a3d61582cfcbebfa3f41728d63b75ad2fb001116a850e" + }, + { + "path": "src\\QInventor\\qi_bom.py", + "sha256": "548b7196392650b790f8263440cc12039eb1aae20c19ce6158e4588f9cc2053d" + }, + { + "path": "src\\QInventor\\qi_bom_ui.py", + "sha256": "522ea28fcce9f7a22e0ea107304ca32406846ec99af16b6adf04ef3da008811d" + }, + { + "path": "src\\QInventor\\qi_commands.py", + "sha256": "19816cbda6a9cc15023484022d0a13f84bd176d6e9eae0b681b968da21732f61" + }, + { + "path": "src\\QInventor\\qi_drawing_bom.py", + "sha256": "143dfa91623c6ac08c80e02e1671f6628ec18a5fd551bda362d7d99d252a4d11" + }, + { + "path": "src\\QInventor\\qi_drawing_bom_export.py", + "sha256": "1e0fbe90cafcf2321ce4eae90868211be7db105767f98bcf42789950d52dab2f" + }, + { + "path": "src\\QInventor\\qi_drawing_bom_ui.py", + "sha256": "8a50502dd648a42a2e6e4b436972c67b71ec0683f0c4276b13f313357d0c103a" + }, + { + "path": "src\\QInventor\\qi_extrude.py", + "sha256": "03b5b7eea4adbf168d623995e84e0d3af4ccea58494b3b34542b10447ef0c072" + }, + { + "path": "src\\QInventor\\qi_io.py", + "sha256": "77ea249f6d3a374e89ac294e0de8a386356ed47f908ba494cbe8fea37c34df1f" + }, + { + "path": "src\\QInventor\\qi_localization.py", + "sha256": "84041c5888157e7844b99ebda3fddf9e86531667c9e1aa56cde917795f1b91e1" + }, + { + "path": "src\\QInventor\\qi_messages.py", + "sha256": "5c0c41cf0c1bd60f30d0aec0da6934c2af70a88765d86c07e500046714eeecb5" + }, + { + "path": "src\\QInventor\\qi_model.py", + "sha256": "08c094382cfadd5e94e3ab6d1ce01ad1214985cc2a3222814ab5b2da26c4d762" + }, + { + "path": "src\\QInventor\\qi_parameters.py", + "sha256": "9f384e40b930b6ebc4e7f068651c0797134818803b82704258e26215997c01c2" + }, + { + "path": "src\\QInventor\\qi_parameters_ui.py", + "sha256": "4e10337d61bb38f229736f0e877d16cc99106943fa3a8f4333fa905aad7ec19f" + }, + { + "path": "src\\QInventor\\qi_projection.py", + "sha256": "a4923dd69f7dcb3230a4b60bd001698d90d6d02db5aa016362034ee73520925d" + }, + { + "path": "src\\QInventor\\qi_sketch_offset.py", + "sha256": "ffb0f2a000b91528587110d7f5d47b7e26902b499e1b35220e3c09fc89786cb9" + }, + { + "path": "src\\QInventor\\qi_sketch_patterns.py", + "sha256": "c98dfd369c639436f0aed0664cf43cc6345b6639c102309efb90041fe65d4e1e" + }, + { + "path": "src\\QInventor\\qi_sketch_preview.py", + "sha256": "4858dc39046bad95306179212fe44a63e2e1437259a9c6d14bc847b68c7aab76" + }, + { + "path": "src\\QInventor\\qi_sketch_ui.py", + "sha256": "d505b802c65f1566a64d7687e290a64226aff91958db59b8d621ac69909ead37" + }, + { + "path": "src\\QInventor\\qi_step_export.py", + "sha256": "754f7f8b841f87a696ddf800e19f947d5e8712fc53faf8fce4d2d0d0f3ee51f6" + }, + { + "path": "src\\QInventor\\qi_ui.py", + "sha256": "7432d1939919b30e5fc81cd333caf220ca3b490d71851328aa26d8070a7ac307" + }, + { + "path": "src\\QInventor\\qi_workbench.py", + "sha256": "312d49f40d78e8bde48cd8fa57232cd380565fe5b30a688e4612d57bde210c59" + }, + { + "path": "src\\QInventor\\resources\\qinventor-a4.svg", + "sha256": "f77419e8559cb24b29a62ebcdc02d781f52c6910c0299d4820d1dd8c820c4128" + }, + { + "path": "src\\QInventor\\resources\\qinventor.ico", + "sha256": "ea229e71d0fcdd749c35977fc0f11720a972c3ee04b3d9042d850182e0a6ed1c" + }, + { + "path": "src\\QInventor\\resources\\qinventor.png", + "sha256": "3de618b2d07a1fd36a14f71d3f0b85f36a5dcc22ccf070b4629491da6e6f9e32" + }, + { + "path": "src\\QInventor\\resources\\qinventor.svg", + "sha256": "12e95fa28c4aaf927311a777b3928d0518799e51663aab2f0f036015210dc954" + }, + { + "path": "tests\\assembly_suite.py", + "sha256": "988aaa37573ab5c6543fd4f8f67b7fa7fff604721dcc6fe31b7c71abea747709" + }, + { + "path": "tests\\bom_gui_suite.py", + "sha256": "043aa3632033bf5a6cc4fba0b1167b9cc6e89610f1db562067b00b32c62640de" + }, + { + "path": "tests\\bom_suite.py", + "sha256": "2a52ef5fb7f99f4f21d6858f0860eba2db0a18993f7092059029c3f73b145f9e" + }, + { + "path": "tests\\command_workflows_suite.py", + "sha256": "9c2cd271b6cde306dc9c228755579c299ead6ab6d4773d83dfefe7b37ecdb950" + }, + { + "path": "tests\\drawing_bom_gui_suite.py", + "sha256": "12a60a1f8550922b5e3b069df09e3416111e367ba82a1d2f25fed5abf7555393" + }, + { + "path": "tests\\drawing_bom_suite.py", + "sha256": "3da48ba3aa9b7f70ac02732fe41699776542d72e840a4d5843bc98f9e72a2509" + }, + { + "path": "tests\\extrude_suite.py", + "sha256": "83da33ea4ce4d5da538f46e0a03a88acdb4e46a1424da32d899602a4b735605a" + }, + { + "path": "tests\\gui_suite.py", + "sha256": "87cee20dc57dffa1384be594d7e8ba96580808685c0ec9419c8fd3a3a4f9007b" + }, + { + "path": "tests\\io_suite.py", + "sha256": "53f8662e37fac45216d2858e732a2e8bbccf3d344c607aa57a99d9cb8d630c45" + }, + { + "path": "tests\\kernel_suite.py", + "sha256": "bfd56a485e11b76f4b402439185db813601d93ce001b54a68e0303826157f3f8" + }, + { + "path": "tests\\parameter_preflight_suite.py", + "sha256": "dc0dd00f59ea60baa90ab6930d7b8a68ebcb33f5e5ea00f3354d94a67d75a966" + }, + { + "path": "tests\\parameter_table_gui_suite.py", + "sha256": "c19beaaf1c0dda693dbdd964319b1c199ea51bb79a4300f0b15177c15b5f8489" + }, + { + "path": "tests\\parameter_table_suite.py", + "sha256": "f0c9eb2975bbac0a5d6a0a6f50bf64bc8cbd094b09942c167207574d3e1ba895" + }, + { + "path": "tests\\parameters_suite.py", + "sha256": "7bbcc44d6d9f1595ac2ab90e8a8f01fb1d0d0acfd32294241d3ef595713f985b" + }, + { + "path": "tests\\probe.FCMacro", + "sha256": "928efa45b20a145f313cf2ae6dcbcbc7ee9f3609df67ec678078aed8e264bd07" + }, + { + "path": "tests\\projection_suite.py", + "sha256": "77770dc7c82656794ed68770d26c5da7871f2a8949ea8acc7ee637f8caaa1d24" + }, + { + "path": "tests\\ribbon_camera_probe.FCMacro", + "sha256": "ed96daba842fbd362bf2ce7ec8014e011a46cc0941fae48b05f78714aeefc3c6" + }, + { + "path": "tests\\ribbon_gui.FCMacro", + "sha256": "bbaa7367526cb924f096a97bc3f6005a6184061955abfdd15d0e91d1f5986575" + }, + { + "path": "tests\\ribbon_probe.FCMacro", + "sha256": "b519579dcb0cee106aa40caf674fa611e105168299e933fe9930ac20a6238872" + }, + { + "path": "tests\\ribbon_suite.py", + "sha256": "b5d5459532bdfb7113da2fea228248e87aa0e7abfa8ecec116e1222141aae05b" + }, + { + "path": "tests\\sketch_offset_suite.py", + "sha256": "97de15e62d59b7c3113dda26d48670163853acf8a404067d676c6db3a2cc82f1" + }, + { + "path": "tests\\sketch_operations_gui_suite.py", + "sha256": "4b088da978f506f3afe04fac6c9a3762895eb8fb1f21b2e7f5d8f08a656e53a1" + }, + { + "path": "tests\\sketch_patterns_suite.py", + "sha256": "2205b605bd5ab7b8077cbd2421bdb469105d04daa5806fdc02edadb7a5f68414" + } + ], + "finished": "2026-10-15T16:56:17.295721+03:00", + "native_exit_code": 0 + }, + "version": "1.0.0", + "officialRuntimeArchiveSha256": "4828741fc91ee37fafcdb97a1abacb18b04ba451ac4372d9ff7a7349b36f4d6d", + "platform": "Windows x64" +} diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 new file mode 100644 index 0000000..1c2fbec --- /dev/null +++ b/scripts/bootstrap.ps1 @@ -0,0 +1,55 @@ +param([switch]$SkipDownload) +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +$version = '1.1.4' +$asset = "FreeCAD_${version}-Windows-x86_64-py311.7z" +$expected = '4828741fc91ee37fafcdb97a1abacb18b04ba451ac4372d9ff7a7349b36f4d6d' +$archive = Join-Path $root "downloads\$asset" +$runtime = Join-Path $root 'runtime' +New-Item -ItemType Directory -Path (Join-Path $root 'downloads') -Force | Out-Null +if (-not (Test-Path -LiteralPath $archive)) { + if ($SkipDownload) { throw "Не найден дистрибутив CAD: $archive" } + Invoke-WebRequest "https://github.com/FreeCAD/FreeCAD/releases/download/$version/$asset" -OutFile $archive +} +if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expected) { + throw 'Контрольная сумма архива FreeCAD не совпадает. Загруженный файл не использован.' +} +$sevenZip = 'C:\Program Files\7-Zip\7z.exe' +if (-not (Test-Path -LiteralPath $sevenZip)) { throw 'Для подготовки CAD-библиотек установите 7-Zip.' } + +function Test-QIRuntimeFiles([string]$ArchivePath, [string]$RuntimePath) { + $listing = & $sevenZip l -slt -ba $ArchivePath + if ($LASTEXITCODE -ne 0) { throw 'Не удалось прочитать состав официального архива CAD.' } + $runtimeAbsolute = [IO.Path]::GetFullPath($RuntimePath).TrimEnd([IO.Path]::DirectorySeparatorChar) + $entryPath = ''; $entrySize = 0L; $isFolder = $false; $files = 0 + foreach ($line in @($listing) + @('')) { + if ($line.StartsWith('Path = ')) { $entryPath = $line.Substring(7) } + elseif ($line.StartsWith('Size = ')) { $entrySize = [long]$line.Substring(7) } + elseif ($line.StartsWith('Attributes = ')) { $isFolder = $line.Substring(13).Contains('D') } + elseif ($line -eq '' -and $entryPath) { + if (-not $isFolder -and -not $entryPath.EndsWith('.pyc', [StringComparison]::OrdinalIgnoreCase)) { + $target = [IO.Path]::GetFullPath((Join-Path $runtimeAbsolute $entryPath)) + if (-not $target.StartsWith($runtimeAbsolute + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Путь в архиве CAD выходит за пределы папки runtime.' + } + if (-not [IO.File]::Exists($target) -or ([IO.FileInfo]::new($target)).Length -ne $entrySize) { + Write-Host "Восстановление CAD-библиотек: $entryPath" + return $false + } + $files++ + } + $entryPath = ''; $entrySize = 0L; $isFolder = $false + } + } + return $files -gt 0 +} + +# An interrupted extraction can leave the launcher EXE while omitting DLLs or +# workbenches. Compare every original non-bytecode file, not just the EXE. +if (-not (Test-QIRuntimeFiles $archive $runtime)) { + & $sevenZip x $archive "-o$runtime" -y -bsp0 -bso0 + if ($LASTEXITCODE -ne 0) { throw 'Не удалось распаковать библиотеки CAD.' } + if (-not (Test-QIRuntimeFiles $archive $runtime)) { throw 'Не удалось восстановить полный комплект CAD-библиотек.' } +} +& (Join-Path $PSScriptRoot 'sync.ps1') +Write-Host "Библиотеки QInventor проверены и готовы к запуску: $root" diff --git a/scripts/build-launcher.ps1 b/scripts/build-launcher.ps1 new file mode 100644 index 0000000..f9e7476 --- /dev/null +++ b/scripts/build-launcher.ps1 @@ -0,0 +1,14 @@ +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +$publish = Join-Path $root 'artifacts\launcher' +dotnet publish (Join-Path $PSScriptRoot 'launcher\QInventor.Launcher.csproj') -c Release -r win-x64 -o $publish --nologo +if ($LASTEXITCODE -ne 0) { throw 'Не удалось собрать программу запуска QInventor.' } +$builtLauncher = Join-Path $publish 'QInventor.exe' +$installedLauncher = Join-Path $root 'QInventor.exe' +# A running CAD session locks the launcher on Windows. An identical rebuild +# needs no replacement and can still be packaged while that session is open. +if (-not (Test-Path -LiteralPath $installedLauncher) -or + (Get-FileHash -LiteralPath $builtLauncher).Hash -ne (Get-FileHash -LiteralPath $installedLauncher).Hash) { + Copy-Item -LiteralPath $builtLauncher -Destination $installedLauncher -Force +} +Write-Host 'QInventor.exe готов к запуску.' diff --git a/scripts/launcher/Program.cs b/scripts/launcher/Program.cs new file mode 100644 index 0000000..aaefa85 --- /dev/null +++ b/scripts/launcher/Program.cs @@ -0,0 +1,117 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Nodes; + +static class Program +{ + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type); + + static int Main(string[] args) + { + // Preserve a drive root (for example F:\\ on a portable USB drive). + var root = Path.TrimEndingDirectorySeparator(AppContext.BaseDirectory); + var rootPrefix = Path.EndsInDirectorySeparator(root) ? root : root + Path.DirectorySeparatorChar; + var log = Path.Combine(root, "logs", "launcher.log"); + try + { + Directory.CreateDirectory(Path.GetDirectoryName(log)!); + Directory.CreateDirectory(Path.Combine(root, "data")); + var marker = Path.Combine(root, "data", "runtime-path.txt"); + var engine = File.Exists(marker) + ? Path.GetFullPath(Path.Combine(root, File.ReadAllText(marker).Trim())) + : Directory.EnumerateFiles(Path.Combine(root, "runtime"), "FreeCAD.exe", SearchOption.AllDirectories) + .First(path => string.Equals(Path.GetFileName(Path.GetDirectoryName(path)), "bin", StringComparison.OrdinalIgnoreCase)); + if (!engine.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase) || !File.Exists(engine)) + throw new IOException("Не найдены библиотеки CAD. Выполните scripts\\bootstrap.ps1."); + var nativeEngine = RuntimePath.ForEngine(root, engine); + var start = new ProcessStartInfo(nativeEngine) { UseShellExecute = false, WorkingDirectory = root }; + start.Environment["QI_ROOT"] = root; + start.Environment.Remove("QI_TEST"); + var selfTest = args.Contains("--self-test", StringComparer.Ordinal); + var profile = Path.Combine(root, "data"); + if (selfTest) + { + start.Environment["QI_TEST"] = "1"; + profile = Path.Combine(root, "artifacts", "test-profiles", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(profile); + var report = Path.Combine(root, "artifacts", "validation.json"); + if (File.Exists(report)) + File.Move(report, Path.Combine(root, "artifacts", $"validation-previous-{DateTime.UtcNow:yyyyMMdd-HHmmss-fffffff}.json")); + } + start.ArgumentList.Add("--user-cfg"); + start.ArgumentList.Add(Path.Combine(profile, "user.cfg")); + start.ArgumentList.Add("--system-cfg"); + start.ArgumentList.Add(Path.Combine(profile, "system.cfg")); + start.ArgumentList.Add("--write-log"); + start.ArgumentList.Add("--log-file"); + start.ArgumentList.Add(Path.Combine(root, "logs", "cad.log")); + // Apply first-run native view preferences before restored sketches + // exist. Setting them afterwards would mark clean projects dirty. + start.ArgumentList.Add(Path.Combine(root, "scripts", "startup.FCMacro")); + foreach (var arg in args) + { + if (arg != "--self-test") start.ArgumentList.Add(arg); + } + File.AppendAllText(log, $"{DateTimeOffset.Now:O} Starting QInventor {engine}\nNative runtime path: {nativeEngine}\n"); + using var process = Process.Start(start) ?? throw new IOException("Не удалось запустить CAD-процесс."); + process.WaitForExit(); + File.AppendAllText(log, $"{DateTimeOffset.Now:O} Exit {process.ExitCode}\n"); + if (process.ExitCode != 0) throw new IOException($"CAD-процесс завершился с кодом {process.ExitCode}. Подробности: logs\\cad.log."); + if (selfTest) + { + using var report = JsonDocument.Parse(File.ReadAllText(Path.Combine(root, "artifacts", "validation.json"))); + if (report.RootElement.GetProperty("status").GetString() != "passed") + { + File.AppendAllText(log, $"{DateTimeOffset.Now:O} Проверка приложения завершилась с ошибками; artifacts\\validation.json\n"); + return 2; + } + } + return 0; + } + catch (Exception ex) + { + try { File.AppendAllText(log, $"{DateTimeOffset.Now:O} {ex}\n"); } catch { } + if (args.Contains("--self-test", StringComparer.Ordinal)) + { + // Native failures can interrupt a suite before Python writes its + // final status. Preserve completed checks and make that failure + // explicit instead of leaving a misleading running report. + try + { + var reportPath = Path.Combine(root, "artifacts", "validation.json"); + Directory.CreateDirectory(Path.GetDirectoryName(reportPath)!); + var report = new JsonObject(); + if (File.Exists(reportPath)) + { + try + { + report = JsonNode.Parse(File.ReadAllText(reportPath)) as JsonObject ?? new JsonObject(); + } + catch (JsonException) + { + // A native crash can interrupt Python's live JSON + // write. Preserve its remaining bytes, then still + // produce a readable failure report. + var damaged = Path.Combine(root, "artifacts", $"validation-truncated-{DateTime.UtcNow:yyyyMMdd-HHmmss-fffffff}.json"); + File.Copy(reportPath, damaged); + } + } + report["status"] = "failed"; + var previousFailed = report["failed"] is JsonValue count && count.TryGetValue(out var failures) ? failures : 0; + report["failed"] = Math.Max(1, previousFailed); + report["launcher_error"] = ex.Message; + report["finished"] = DateTime.UtcNow.ToString("o"); + var staged = reportPath + ".launcher-tmp"; + File.WriteAllText(staged, report.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); + File.Move(staged, reportPath, overwrite: true); + } + catch { } + return 1; + } + MessageBoxW(IntPtr.Zero, "Не удалось запустить QInventor.\n\nПроверьте, что приложение перенесено вместе с папками runtime, src и scripts. Подробности ошибки сохранены в журнале.\n\nЖурнал: " + log, "QInventor", 0x10); + return 1; + } + } +} diff --git a/scripts/launcher/QInventor.Launcher.csproj b/scripts/launcher/QInventor.Launcher.csproj new file mode 100644 index 0000000..8b1f0d5 --- /dev/null +++ b/scripts/launcher/QInventor.Launcher.csproj @@ -0,0 +1,20 @@ + + + WinExe + net10.0 + QInventor + QInventor + enable + enable + 1.0.0 + QInventor + QInventor — параметрическое проектирование + true + true + true + true + true + ../../src/QInventor/resources/qinventor.ico + none + + diff --git a/scripts/launcher/RuntimePath.cs b/scripts/launcher/RuntimePath.cs new file mode 100644 index 0000000..37a7e12 --- /dev/null +++ b/scripts/launcher/RuntimePath.cs @@ -0,0 +1,112 @@ +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Win32.SafeHandles; + +// Some upstream DXF readers still use narrow std::ifstream for resource files. +// Give the native runtime an ASCII executable path while retaining the real +// portable folder for settings, projects and QI_ROOT. No CAD files are copied. +static class RuntimePath +{ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern uint GetShortPathNameW(string longPath, StringBuilder shortPath, uint capacity); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern SafeFileHandle CreateFileW(string name, uint access, uint share, IntPtr security, + uint creation, uint flags, IntPtr template); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool DeviceIoControl(SafeFileHandle file, uint control, byte[] input, uint inputSize, + IntPtr output, uint outputSize, out uint returned, IntPtr overlapped); + + static bool IsAscii(string value) => value.All(c => c < 128); + + static string? ShortAsciiPath(string path) + { + if (IsAscii(path)) return path; + var buffer = new StringBuilder(32768); + var length = GetShortPathNameW(path, buffer, (uint)buffer.Capacity); + return length > 0 && length < buffer.Capacity && IsAscii(buffer.ToString()) + ? buffer.ToString() : null; + } + + public static string ForEngine(string root, string engine, bool allowShortPath = true) + { + if (allowShortPath && ShortAsciiPath(engine) is { } shortEngine) return shortEngine; + var relativeEngine = Path.GetRelativePath(root, engine); + if (relativeEngine.StartsWith(".." + Path.DirectorySeparatorChar) || Path.IsPathRooted(relativeEngine)) + throw new IOException("Путь CAD-движка находится за пределами переносимой папки."); + var key = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(root.ToUpperInvariant())))[..32]; + var candidates = new[] + { + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + Path.GetTempPath(), + Environment.GetFolderPath(Environment.SpecialFolder.CommonDocuments), + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + }; + Exception? lastError = null; + foreach (var parent in candidates.Where(p => !string.IsNullOrWhiteSpace(p)).Distinct()) + { + var asciiParent = ShortAsciiPath(Path.TrimEndingDirectorySeparator(parent)); + if (asciiParent is null) continue; + var cache = Path.Combine(asciiParent, "QInventor", "RuntimeLinks"); + var link = Path.Combine(cache, key); + try + { + Directory.CreateDirectory(cache); + var info = new DirectoryInfo(link); + if (info.Exists) + { + var target = info.ResolveLinkTarget(false)?.FullName; + if (target is null || !string.Equals(Path.TrimEndingDirectorySeparator(Path.GetFullPath(target)), + root, StringComparison.OrdinalIgnoreCase)) + throw new IOException("Существующая ссылка CAD-движка указывает на другую папку."); + } + else + { + // Never remove an existing or dangling junction. Only create + // a fresh directory whose exact target is the validated root. + if (info.LinkTarget is not null) + throw new IOException("Служебная ссылка CAD-движка недоступна."); + Directory.CreateDirectory(link); + CreateJunction(link, root); + } + var aliasedEngine = Path.Combine(link, relativeEngine); + if (!File.Exists(aliasedEngine)) + throw new IOException("Служебная ссылка CAD-движка не содержит исполняемый файл."); + return aliasedEngine; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or Win32Exception) + { + lastError = ex; + } + } + throw new IOException("Не удалось подготовить служебный путь CAD-движка. Перенесите папку QInventor в путь из латинских символов.", lastError); + } + + static void CreateJunction(string link, string target) + { + if (target.StartsWith(@"\\")) + throw new IOException("Сетевая папка без короткого пути не поддерживается CAD-движком."); + var substitute = Encoding.Unicode.GetBytes(@"\??\" + target); + var print = Encoding.Unicode.GetBytes(target); + var buffer = new byte[16 + substitute.Length + 2 + print.Length + 2]; + void Write16(int offset, int value) => BitConverter.GetBytes(checked((ushort)value)).CopyTo(buffer, offset); + BitConverter.GetBytes(0xA0000003u).CopyTo(buffer, 0); // IO_REPARSE_TAG_MOUNT_POINT + Write16(4, buffer.Length - 8); + Write16(8, 0); + Write16(10, substitute.Length); + Write16(12, substitute.Length + 2); + Write16(14, print.Length); + substitute.CopyTo(buffer, 16); + print.CopyTo(buffer, 18 + substitute.Length); + using var handle = CreateFileW(link, 0x40000000, 0, IntPtr.Zero, 3, + 0x00200000 | 0x02000000, IntPtr.Zero); + if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error()); + if (!DeviceIoControl(handle, 0x000900A4, buffer, (uint)buffer.Length, + IntPtr.Zero, 0, out _, IntPtr.Zero)) + throw new Win32Exception(Marshal.GetLastWin32Error()); + } +} diff --git a/scripts/package.ps1 b/scripts/package.ps1 new file mode 100644 index 0000000..4e3d86a --- /dev/null +++ b/scripts/package.ps1 @@ -0,0 +1,90 @@ +param([switch]$SkipTests) +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +& (Join-Path $PSScriptRoot 'build-launcher.ps1') +& (Join-Path $PSScriptRoot 'sync.ps1') +if (-not $SkipTests) { & (Join-Path $PSScriptRoot 'test.ps1') } +$report = Join-Path $root 'artifacts\validation.json' +if (-not (Test-Path -LiteralPath $report)) { throw 'Нет отчёта проверки. Сначала выполните scripts/test.ps1.' } +$validated = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json +if ($validated.status -ne 'passed') { throw 'Отчёт проверки CAD содержит ошибки.' } +if ($null -eq $validated.native_exit_code -or $validated.native_exit_code -ne 0) { + throw 'Не подтверждено корректное завершение CAD-процесса. Запустите scripts/test.ps1.' +} +if (-not $validated.source_sha256 -or $validated.source_sha256.Count -eq 0) { + throw 'В отчёте проверки нет снимка исходного кода. Выполните scripts/test.ps1.' +} +# Compare the complete current set too: checking only saved entries would let +# newly added, untested commands enter a -SkipTests package unnoticed. +$currentSources = foreach ($directory in @('src', 'tests', 'scripts')) { + Get-ChildItem -LiteralPath (Join-Path $root $directory) -File -Recurse | + Where-Object { + $_.Name -notmatch '\.(pyc|FCBak)$' -and + $_.FullName.Substring($root.Length + 1) -notmatch '(^|[\\/])(bin|obj|__pycache__)([\\/]|$)' + } | ForEach-Object { $_.FullName.Substring($root.Length + 1) } +} +$validatedPaths = @($validated.source_sha256 | ForEach-Object { $_.path } | Sort-Object) +$currentPaths = @($currentSources | Sort-Object) +if (($validatedPaths -join "`n") -cne ($currentPaths -join "`n")) { + throw 'Состав исходных файлов изменился после проверки. Выполните scripts/test.ps1.' +} +foreach ($file in $validated.source_sha256) { + $current = (Get-FileHash -LiteralPath (Join-Path $root $file.path) -Algorithm SHA256).Hash.ToLowerInvariant() + if ($current -ne $file.sha256) { throw "Исходный файл изменился после проверки: $($file.path). Выполните scripts/test.ps1." } +} +$enginePath = Join-Path $root ((Get-Content -LiteralPath (Join-Path $root 'data\runtime-path.txt') -Raw).Trim()) +$engineRoot = Split-Path -Parent (Split-Path -Parent $enginePath) +$workbench = Join-Path $engineRoot 'Mod\QInventor' +if (-not (Test-Path -LiteralPath $workbench)) { $workbench = Join-Path (Split-Path -Parent $enginePath) 'Mod\QInventor' } +$sourceRoot = Join-Path $root 'src\QInventor' +$sourceFiles = @(Get-ChildItem -LiteralPath $sourceRoot -File -Recurse | + Where-Object { $_.Extension -ne '.pyc' -and $_.FullName -notmatch '[\\/]__pycache__[\\/]' }) +$moduleFiles = @(Get-ChildItem -LiteralPath $workbench -File -Recurse | + Where-Object { $_.Extension -ne '.pyc' -and $_.FullName -notmatch '[\\/]__pycache__[\\/]' }) +$sourceNames = @($sourceFiles | ForEach-Object { $_.FullName.Substring($sourceRoot.Length + 1) } | Sort-Object) +$moduleNames = @($moduleFiles | ForEach-Object { $_.FullName.Substring($workbench.Length + 1) } | Sort-Object) +if (($sourceNames -join "`n") -cne ($moduleNames -join "`n")) { + throw 'Рабочая среда CAD содержит файлы, которых нет в проверенном снимке исходного кода.' +} +foreach ($file in $sourceFiles) { + $nativeCopy = Join-Path $workbench $file.FullName.Substring($sourceRoot.Length + 1) + if ((Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash -ne (Get-FileHash -LiteralPath $nativeCopy -Algorithm SHA256).Hash) { + throw "Рабочая среда CAD отличается от проверенного исходного кода: $($file.Name)" + } +} +$release = Join-Path $root 'release' +New-Item -ItemType Directory -Path $release -Force | Out-Null +$sourceHashes = Get-ChildItem -LiteralPath (Join-Path $root 'src') -File -Recurse | + Where-Object { $_.Extension -ne '.pyc' } | ForEach-Object { + @{ path = $_.FullName.Substring($root.Length+1); sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() } + } +$manifest = @{ + application = 'QInventor'; version = '1.0.0'; platform = 'Windows x64'; + engine = 'FreeCAD 1.1.4 / Open CASCADE'; + officialRuntimeArchiveSha256 = '4828741fc91ee37fafcdb97a1abacb18b04ba451ac4372d9ff7a7349b36f4d6d'; + builtAtUtc = [DateTime]::UtcNow.ToString('o'); + launcherSha256 = (Get-FileHash -LiteralPath (Join-Path $root 'QInventor.exe') -Algorithm SHA256).Hash.ToLowerInvariant(); + validation = $validated; sources = @($sourceHashes) +} +$manifest | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $root 'manifest.json') -Encoding utf8 +Copy-Item -LiteralPath $report -Destination (Join-Path $release 'validation.json') -Force +$archive = Join-Path $release 'QInventor-1.0.0-win-x64-portable.zip' +if (Test-Path -LiteralPath $archive) { + Move-Item -LiteralPath $archive -Destination (Join-Path $release "QInventor-previous-$([DateTime]::Now.ToString('yyyyMMdd-HHmmss')).zip") +} +$files = @('QInventor.exe','QInventor.cmd','README.md','LICENSE','THIRD_PARTY_NOTICES.md','manifest.json', + 'runtime','src','scripts','tests','docs','examples','data\runtime-path.txt') +Push-Location $root +try { + & 'C:\Program Files\7-Zip\7z.exe' a -tzip $archive @files '-xr!__pycache__' '-xr!*.pyc' '-xr!*.pdb' '-xr!*.FCStd1' '-xr!*.FCBak' '-xr!obj' '-xr!artifacts' '-x!scripts\launcher\bin\*' -mx1 -bsp0 -bso0 + if ($LASTEXITCODE -ne 0) { throw 'Не удалось создать переносимый архив.' } +} finally { Pop-Location } +# Do not globally exclude bin: the runtime's bin directory contains the CAD +# executables. The launcher development bin is filtered by exact archive paths. +& 'C:\Program Files\7-Zip\7z.exe' t $archive -bsp0 -bso0 +if ($LASTEXITCODE -ne 0) { throw 'Проверка целостности переносимого архива завершилась с ошибкой.' } +Get-FileHash -LiteralPath $archive -Algorithm SHA256 | + Select-Object @{Name='Алгоритм';Expression={$_.Algorithm}}, + @{Name='Контрольная сумма';Expression={$_.Hash}}, + @{Name='Файл';Expression={$_.Path}} | Format-List +Write-Host "Переносимый выпуск: $archive" diff --git a/scripts/render-icon.py b/scripts/render-icon.py new file mode 100644 index 0000000..50c5832 --- /dev/null +++ b/scripts/render-icon.py @@ -0,0 +1,14 @@ +"""Rasterize the application's own vector icon with the bundled Qt runtime.""" +import os +from pathlib import Path +os.environ['QT_QPA_PLATFORM'] = 'offscreen' +from PySide6 import QtCore, QtGui, QtSvg +app = QtGui.QGuiApplication([]) +resources = Path(__file__).resolve().parents[1] / 'src' / 'QInventor' / 'resources' +renderer = QtSvg.QSvgRenderer(str(resources / 'qinventor.svg')) +image = QtGui.QImage(256, 256, QtGui.QImage.Format_ARGB32) +image.fill(QtCore.Qt.transparent) +painter = QtGui.QPainter(image) +renderer.render(painter) +painter.end() +assert image.save(str(resources / 'qinventor.png')) diff --git a/scripts/startup.FCMacro b/scripts/startup.FCMacro new file mode 100644 index 0000000..7c04414 --- /dev/null +++ b/scripts/startup.FCMacro @@ -0,0 +1,93 @@ +import os +import sys +import traceback +import FreeCAD as App +import FreeCADGui as Gui +from PySide import QtCore + +ROOT = os.path.abspath(os.environ.get('QI_ROOT') or os.getcwd()) +for path in (os.path.join(ROOT, 'src', 'QInventor'), os.path.join(ROOT, 'tests')): + if path not in sys.path: + sys.path.insert(0, path) + +# These preferences reproduce the visible drafting workflow, not proprietary +# Autodesk solver internals. Apply defaults once so user changes persist. +settings = App.ParamGet('User parameter:BaseApp/Preferences/Mod/QInventor') +App.ParamGet('User parameter:BaseApp/Preferences/General').SetString('Language', 'Russian') +dimension_display = App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher') +if not any(item[1] == 'ShowDimensionalName' for item in (dimension_display.GetContents() or [])): + # Keep parameter aliases in the model and constraint list, but show a + # compact value on the canvas by default. Preserve explicit user choices. + dimension_display.SetBool('ShowDimensionalName', False) + App.saveParameter() +if not settings.GetBool('DefaultsV1', False): + App.ParamGet('User parameter:BaseApp/Preferences/General').SetString('AutoloadModule', 'QInventorWorkbench') + App.ParamGet('User parameter:BaseApp/Preferences/Units').SetInt('UserSchema', 0) + view = App.ParamGet('User parameter:BaseApp/Preferences/View') + view.SetString('NavigationStyle', 'Gui::RevitNavigationStyle') + view.SetUnsigned('BackgroundColor', 3908102143) + view.SetUnsigned('BackgroundColor2', 4177066239) + view.SetBool('Simple', False) + sketch = App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher') + for key, value in [('LeaveSketchWithEscape', False), ('ContinuousCreationMode', True), + ('ContinuousConstraintMode', True), ('ShowDialogOnDistanceConstraint', True)]: + sketch.SetBool(key, value) + general = App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher/General') + general.SetBool('RestoreCamera', True) + general.SetBool('ForceOrtho', True) + App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher/Tools').SetInt('OnViewParameterVisibility', 2) + App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher/dimensioning').SetBool('SingleDimensioningTool', True) + settings.SetBool('DefaultsV1', True) + App.saveParameter() + +# Early development profiles used FreeCAD's historical Open Inventor mouse +# mode. Migrate that one old default, preserving other user navigation choices. +if not settings.GetBool('NavigationV2', False): + view = App.ParamGet('User parameter:BaseApp/Preferences/View') + if view.GetString('NavigationStyle') == 'Gui::InventorNavigationStyle': + view.SetString('NavigationStyle', 'Gui::RevitNavigationStyle') + settings.SetBool('NavigationV2', True) + App.saveParameter() + + +def start(): + try: + # The first launch has already created native menus before this macro. + # Changing the preference alone does not retranslate those QAction labels. + Gui.updateLocale() + import qi_localization + qi_localization.install() + # Native opening names a document after its filename. Keep the saved + # Russian title when a project was passed to the portable launcher. + import qi_io + for document in App.listDocuments().values(): + if document.FileName: + qi_io.restore_document_label(document, document.FileName) + Gui.activateWorkbench('QInventorWorkbench') + if os.environ.get('QI_TEST'): + import gui_suite + gui_suite.run(ROOT) + elif not App.ActiveDocument: + import qi_ui + qi_ui.show_welcome() + except Exception: + text = traceback.format_exc() + App.Console.PrintError(text) + os.makedirs(os.path.join(ROOT, 'logs'), exist_ok=True) + with open(os.path.join(ROOT, 'logs', 'startup-error.txt'), 'w', encoding='utf-8') as f: + f.write(text) + if os.environ.get('QI_TEST'): + import json + result_path = os.path.join(ROOT, 'artifacts', 'validation.json') + result = {} + if os.path.exists(result_path): + with open(result_path, encoding='utf-8') as f: + result = json.load(f) + result.update(status='failed', fatal_error=text) + result['failed'] = result.get('failed', 0) + 1 + with open(result_path, 'w', encoding='utf-8') as f: + json.dump(result, f, indent=2, ensure_ascii=False) + QtCore.QTimer.singleShot(100, Gui.getMainWindow().close) + + +QtCore.QTimer.singleShot(800, start) diff --git a/scripts/sync.ps1 b/scripts/sync.ps1 new file mode 100644 index 0000000..f1f30a5 --- /dev/null +++ b/scripts/sync.ps1 @@ -0,0 +1,46 @@ +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +$exe = Get-ChildItem -LiteralPath (Join-Path $root 'runtime') -Filter FreeCAD.exe -Recurse | Where-Object { $_.Directory.Name -eq 'bin' } | Select-Object -First 1 +if (-not $exe) { throw 'Сначала выполните scripts/bootstrap.ps1.' } +$engineRoot = Split-Path -Parent $exe.DirectoryName +$modules = Join-Path $engineRoot 'Mod' +if (-not (Test-Path -LiteralPath $modules)) { $modules = Join-Path $exe.DirectoryName 'Mod' } +$destination = Join-Path $modules 'QInventor' +New-Item -ItemType Directory -Path $destination -Force | Out-Null +New-Item -ItemType Directory -Path (Join-Path $root 'data'),(Join-Path $root 'logs'),(Join-Path $root 'artifacts') -Force | Out-Null +$source = Join-Path $root 'src\QInventor' +$ownedManifest = Join-Path $root 'data\workbench-files.json' +$ownedFiles = @(Get-ChildItem -LiteralPath $source -File -Recurse | + Where-Object { $_.Extension -ne '.pyc' -and $_.FullName -notmatch '[\\/]__pycache__[\\/]' } | + ForEach-Object { $_.FullName.Substring($source.Length + 1) } | Sort-Object) +$destinationAbsolute = [IO.Path]::GetFullPath($destination) +$destinationPrefix = $destinationAbsolute + [IO.Path]::DirectorySeparatorChar +if (Test-Path -LiteralPath $ownedManifest) { + $previousOwnedFiles = @(Get-Content -LiteralPath $ownedManifest -Raw | ConvertFrom-Json) + foreach ($previous in $previousOwnedFiles) { + if ($previous -notin $ownedFiles) { + $obsolete = [IO.Path]::GetFullPath((Join-Path $destinationAbsolute $previous)) + if (-not $obsolete.StartsWith($destinationPrefix, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Недопустимый путь в списке файлов рабочей среды QInventor.' + } + if ([IO.File]::Exists($obsolete)) { Remove-Item -LiteralPath $obsolete } + } + } +} +foreach ($file in $ownedFiles) { + $target = Join-Path $destinationAbsolute $file + New-Item -ItemType Directory -Path (Split-Path -Parent $target) -Force | Out-Null + Copy-Item -LiteralPath (Join-Path $source $file) -Destination $target -Force +} +# This is our own module only. Avoid stale Python bytecode when two edits have +# equal sizes and fall in the same timestamp second; preserve upstream caches. +foreach ($cached in Get-ChildItem -LiteralPath $destinationAbsolute -File -Filter '*.pyc' -Recurse) { + if (-not $cached.FullName.StartsWith($destinationPrefix, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Путь кэша выходит за пределы рабочей среды QInventor.' + } + Remove-Item -LiteralPath $cached.FullName +} +ConvertTo-Json -InputObject $ownedFiles | Set-Content -LiteralPath $ownedManifest -Encoding utf8 +$relative = $exe.FullName.Substring($root.Length + 1) +Set-Content -LiteralPath (Join-Path $root 'data\runtime-path.txt') -Value $relative -Encoding utf8 +Write-Host "Рабочая среда QInventor обновлена: $destination" diff --git a/scripts/test.ps1 b/scripts/test.ps1 new file mode 100644 index 0000000..052e810 --- /dev/null +++ b/scripts/test.ps1 @@ -0,0 +1,62 @@ +param([int]$TimeoutSeconds = 3600) +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +& (Join-Path $PSScriptRoot 'sync.ps1') +$report = Join-Path $root 'artifacts\validation.json' +if (Test-Path -LiteralPath $report) { + Move-Item -LiteralPath $report -Destination (Join-Path $root "artifacts\validation-previous-$([DateTime]::Now.ToString('yyyyMMdd-HHmmss')).json") +} +$engine = Join-Path $root ((Get-Content -LiteralPath (Join-Path $root 'data\runtime-path.txt') -Raw).Trim()) +$env:QI_ROOT = $root +$previousTest = $env:QI_TEST +$env:QI_TEST = '1' +$profile = Join-Path $root ("artifacts\test-profiles\" + [Guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $profile -Force | Out-Null +$params = @('--user-cfg', ('"' + (Join-Path $profile 'user.cfg') + '"'), + '--system-cfg', ('"' + (Join-Path $profile 'system.cfg') + '"'), + '--log-file', ('"' + (Join-Path $root 'logs\validation.log') + '"'), + ('"' + (Join-Path $PSScriptRoot 'startup.FCMacro') + '"')) +$process = $null +try { + $process = Start-Process -FilePath $engine -ArgumentList $params -WorkingDirectory $root -WindowStyle Hidden -PassThru + $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) + while (-not $process.WaitForExit(1000)) { + if ([DateTime]::UtcNow -gt $deadline) { + Stop-Process -Id $process.Id -ErrorAction SilentlyContinue + throw "Истекло время проверки интерфейса. Подробности: logs\validation.log и artifacts\validation.json." + } + } +} finally { + if ($null -eq $previousTest) { Remove-Item Env:QI_TEST -ErrorAction SilentlyContinue } + else { $env:QI_TEST = $previousTest } +} +if (-not (Test-Path -LiteralPath $report)) { + Get-Content -LiteralPath (Join-Path $root 'logs\validation.log') -Tail 60 + throw 'Проверка не создала отчёт.' +} +$result = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json +$result | Add-Member -NotePropertyName native_exit_code -NotePropertyValue $process.ExitCode -Force +if ($process.ExitCode -ne 0) { + # A native crash during shutdown can follow Python's final passing report. + # Persist the process outcome so -SkipTests cannot package that report. + $result.status = 'failed' + $result.failed = [Math]::Max(1, [int]$result.failed) + $result | Add-Member -NotePropertyName native_error -NotePropertyValue "CAD-процесс завершился с кодом $($process.ExitCode). Подробности: logs\validation.log." -Force +} +$stagedReport = "$report.parent-tmp" +$result | ConvertTo-Json -Depth 40 | Set-Content -LiteralPath $stagedReport -Encoding utf8 +Move-Item -LiteralPath $stagedReport -Destination $report -Force +if ($process.ExitCode -ne 0 -or $result.status -ne 'passed') { + Write-Host "Проверка не пройдена: $($result.failed) ошибок. Отчёт: $report" + foreach ($check in $result.checks | Where-Object { -not $_.passed }) { + $detail = [string]$check.error + if ($detail.Length -gt 1200) { $detail = $detail.Substring(0, 1200) + '…' } + Write-Host "$($check.name): $detail" + } + throw 'Проверка QInventor завершилась с ошибками.' +} +$result | Select-Object @{Name='Состояние';Expression={'Пройдена'}}, + @{Name='Успешных проверок';Expression={$_.passed}}, + @{Name='Ошибок';Expression={$_.failed}}, + @{Name='Версия';Expression={$_.version}} | Format-List +Write-Host "Отчёт проверки: $report" diff --git a/scripts/translator-probe.py b/scripts/translator-probe.py new file mode 100644 index 0000000..a5d64e8 --- /dev/null +++ b/scripts/translator-probe.py @@ -0,0 +1,12 @@ +from PySide6 import QtCore +app = QtCore.QCoreApplication([]) +class Probe(QtCore.QTranslator): + def isEmpty(self): return False + def translate(self, context, source, disambiguation=None, n=-1): + if source == 'known': return 'известно' + return None +translator = Probe(app) +print('installed', app.installTranslator(translator)) +print('known', app.translate('test', 'known')) +print('unknown', app.translate('test', 'unknown %1')) +print('base', QtCore.QTranslator().translate('test', 'unknown')) diff --git a/src/QInventor/Init.py b/src/QInventor/Init.py new file mode 100644 index 0000000..dcd7f5b --- /dev/null +++ b/src/QInventor/Init.py @@ -0,0 +1 @@ +"""QInventor application module. Geometry is provided by FreeCAD/Open CASCADE.""" diff --git a/src/QInventor/InitGui.py b/src/QInventor/InitGui.py new file mode 100644 index 0000000..0e4ad3a --- /dev/null +++ b/src/QInventor/InitGui.py @@ -0,0 +1,9 @@ +import os +import sys +import FreeCAD + +module_dir = os.path.join(FreeCAD.getResourceDir(), 'Mod', 'QInventor') +if module_dir not in sys.path: + sys.path.insert(0, module_dir) +import qi_workbench +qi_workbench.register(Workbench) diff --git a/src/QInventor/qi_bom.py b/src/QInventor/qi_bom.py new file mode 100644 index 0000000..74e6897 --- /dev/null +++ b/src/QInventor/qi_bom.py @@ -0,0 +1,613 @@ +"""BOM and physical properties of real native CAD occurrences. + +Native Part.getShape resolves each hierarchical occurrence's actual BRep and +world transform, including external links, arrays and flexible AssemblyLinks. +Only leaf solids supply materials/densities. Native definitions and feature +history are never counted as additional assembly components. +""" +import csv +import math +import os +from pathlib import Path +import tempfile + +import FreeCAD as App +import Part + + +_LINK_TYPES = {"App::Link", "App::LinkElement", "Assembly::AssemblyLink"} +_IGNORE_TYPES = {"App::Origin", "App::OriginFeature", "App::Plane", "App::Line", "App::LocalCoordinateSystem", + "Assembly::JointGroup", "Assembly::JointObject", "Assembly::BomGroup", "Assembly::BomObject"} +_EPS = 1e-9 + + +def _key(obj): + return obj.Document.Name, obj.Name + + +def _document(doc): + try: + valid = doc is not None and doc.Name in App.listDocuments() and App.getDocument(doc.Name) is doc + except Exception: + valid = False + if not valid: + raise ValueError("Документ спецификации закрыт или не выбран") + return doc + + +def _object(obj): + try: + valid = obj is not None and obj.Document.Name in App.listDocuments() and obj.Document.getObject(obj.Name) is obj + except Exception: + valid = False + if not valid: + raise ValueError("Объект спецификации удалён или недоступен") + return obj + + +def _direct_target(obj): + if obj.TypeId not in _LINK_TYPES: + return None + target = getattr(obj, "LinkedObject", None) + if isinstance(target, (tuple, list)): + target = target[0] if target else None + if target is None and obj.TypeId == "App::LinkElement": + # A virtual/expanded native array element delegates its immediate target. + target = obj.getLinkedObject(False) + if target is obj: + raise ValueError("Циклическая ссылка компонента «%s»" % obj.Label) + if target is None: + raise ValueError("Компонент «%s» не содержит доступной ссылки на исходную деталь или сборку" % obj.Label) + return _object(target) + + +def _container(obj): + return obj.TypeId in ("Assembly::AssemblyObject", "Assembly::AssemblyLink", "App::Part", "App::DocumentObjectGroup") + + +def _definition_library(obj): + return obj.TypeId in ("App::Part", "App::DocumentObjectGroup") and obj.Name == "Definitions" + + +def _ignored(obj): + return (obj.TypeId in _IGNORE_TYPES or obj.TypeId.startswith("TechDraw::") + or obj.TypeId.startswith("PartDesign::Plane") or obj.TypeId.startswith("PartDesign::Line") + or obj.TypeId.startswith("PartDesign::CoordinateSystem") or _definition_library(obj)) + + +def _potential(obj): + if _ignored(obj): + return False + if obj.TypeId in _LINK_TYPES or _container(obj): + return True + if obj.isDerivedFrom("PartDesign::Body"): + return bool(obj.Tip and not obj.Tip.isDerivedFrom("Sketcher::SketchObject")) + return obj.isDerivedFrom("Part::Feature") and not obj.isDerivedFrom("Sketcher::SketchObject") + + +def _definition_source(obj): + chain = [] + seen = set() + while obj.TypeId in _LINK_TYPES: + if _key(obj) in seen: + raise ValueError("Обнаружена циклическая ссылка компонентов CAD") + seen.add(_key(obj)); chain.append(obj) + target = _direct_target(obj) + if obj.TypeId == "Assembly::AssemblyLink" and target.TypeId != "Assembly::AssemblyObject": + raise ValueError("Ссылка сборки «%s» должна указывать на собственную сборку CAD" % obj.Label) + obj = target + return obj, chain + + +def _check_cycles(obj, stack=()): + """Read direct links BEFORE delegated Shape/Group/getLinkedObject recursion.""" + key = _key(obj) + if key in stack: + raise ValueError("Обнаружен цикл в составе сборки «%s»" % obj.Label) + stack = stack + (key,) + if obj.TypeId in _LINK_TYPES: + target = _direct_target(obj) + if obj.TypeId == "Assembly::AssemblyLink" and target.TypeId != "Assembly::AssemblyObject": + raise ValueError("Ссылка сборки «%s» указывает на объект, который не является сборкой" % obj.Label) + _check_cycles(target, stack) + # AssemblyLink.Group is its actual live rigid/flexible occurrence set. + if obj.TypeId == "Assembly::AssemblyLink": + for child in obj.Group: + if not _ignored(child): + _check_cycles(child, stack) + elif _container(obj): + for child in obj.Group: + if not _ignored(child): + _check_cycles(child, stack) + + +def _scale_values(obj): + values = [] + if obj.TypeId in ("App::Link", "App::LinkElement"): + if hasattr(obj, "Scale"): + values.append(float(obj.Scale)) + if hasattr(obj, "ScaleVector"): + vector = obj.ScaleVector + values.extend((float(vector.x), float(vector.y), float(vector.z))) + if hasattr(obj, "ScaleList"): + for vector in obj.ScaleList: + values.extend((float(vector.x), float(vector.y), float(vector.z))) + if any(not math.isfinite(value) or abs(value) <= 1e-14 for value in values): + raise ValueError("Масштаб компонента «%s» должен быть конечным и невырожденным" % obj.Label) + return values + + +def _dependencies(roots): + pending = list(roots) + seen = set() + objects = [] + while pending: + obj = pending.pop() + if _key(obj) in seen: + continue + seen.add(_key(obj)); objects.append(obj) + _scale_values(obj) + if obj.TypeId in _LINK_TYPES: + pending.append(_direct_target(obj)) + pending.extend(obj.OutList) + return objects + + +def _sync(roots): + for obj in roots: + _check_cycles(obj) + dependencies = _dependencies(roots) + docs = [] + for obj in dependencies: + for doc in obj.Document.getDependentDocuments(): + if doc not in docs: + docs.append(doc) + for doc in docs: + if any("Touched" in obj.State for obj in dependencies if obj.Document is doc): + doc.recompute() + _validate_ready(roots) + + +def _validate_ready(roots): + """Validate already computed native dependencies without recomputing. + + Native Python execute callbacks use this path after their linked geometry + has executed. Touched is not an error during an active native recompute. + """ + for obj in roots: + _check_cycles(obj) + # Scope validation to selected native roots/history, not unrelated bodies. + for obj in _dependencies(roots): + if any(flag in obj.State for flag in ("Invalid", "Error")): + raise ValueError("Объект «%s» содержит ошибку перестроения: %s" % (obj.Label, obj.getStatusString())) + for _path, expression in getattr(obj, "ExpressionEngine", ()): + try: + obj.evalExpression(expression) + except Exception as exc: + raise ValueError("Выражение объекта «%s» содержит недоступную или неверную зависимость" % obj.Label) from exc + + +def _history_sources(obj): + """Native finished Part features consume these direct geometric inputs.""" + if not obj.TypeId.startswith("Part::"): + return [] + result = [] + for name in ("Base", "Tool", "Shapes", "Links", "Source"): + if name not in obj.PropertiesList: + continue + value = getattr(obj, name) + values = list(value) if isinstance(value, (tuple, list)) else [value] + for target in values: + if hasattr(target, "Document") and hasattr(target, "Name"): + result.append(target) + return result + + +def _physical_children(container): + children = list(container.Group) + consumed = {_key(source) for child in children for source in _history_sources(child)} + return [child for child in children if _key(child) not in consumed] + + +def document_bom_roots(doc): + """Top physical roots; definitions referenced by this document are excluded.""" + doc = _document(doc) + candidates = [obj for obj in doc.Objects if _potential(obj)] + candidate_keys = {_key(obj) for obj in candidates} + owned = set() + referenced = set() + # Read non-delegated membership first. A rootless A->B->A link cycle must + # be caught before both links are discarded as referenced definitions. + for obj in doc.Objects: + if obj.TypeId in ("Assembly::AssemblyObject", "App::Part", "App::DocumentObjectGroup", "PartDesign::Body"): + for child in obj.Group: + if _key(child) in candidate_keys: + owned.add(_key(child)) + for obj in candidates: + if _key(obj) not in owned: + _check_cycles(obj) + for obj in candidates: + if obj.TypeId == "Assembly::AssemblyLink": + for child in obj.Group: + owned.add(_key(child)) + elif obj.TypeId == "App::Link" and getattr(obj, "ElementCount", 0) > 0: + # Expanded array elements are actual native owned occurrences. + owned.update(_key(child) for child in obj.ElementList) + reachable = list(obj for obj in candidates if _key(obj) not in owned) + seen = set() + while reachable: + obj = reachable.pop() + if _key(obj) in seen: + continue + seen.add(_key(obj)) + if obj.TypeId in _LINK_TYPES: + target = _direct_target(obj) + if target.Document is doc: + referenced.add(_key(target)) + reachable.append(target) + if obj.TypeId == "Assembly::AssemblyLink": + reachable.extend(obj.Group) + elif _container(obj) and not _definition_library(obj): + reachable.extend(obj.Group) + # Native Boolean/compound feature sources are history, not three + # physical parts. Actual assembly links to a source are counted later. + for target in _history_sources(obj): + if target.Document is doc: + referenced.add(_key(target)) + roots = [obj for obj in candidates if _key(obj) not in owned and _key(obj) not in referenced] + # A part document can be used by another document without losing its own BOM. + return roots + + +def _scene_context(obj): + root = obj + names = [] + seen = set() + while True: + try: + parent = root.getParentGeoFeatureGroup() + except AttributeError: + parent = None + if parent is None or parent is root or _key(parent) in seen: + break + seen.add(_key(parent)); names.insert(0, root.Name) + root = parent + return root, "".join(name + "." for name in names) + + +def _only_solids(shape): + if shape.ShapeType == "Solid": + return True + if shape.ShapeType in ("Compound", "CompSolid"): + children = shape.childShapes() + return bool(children) and all(_only_solids(child) for child in children) + return False + + +def _material(source, chain): + # Leaf-link overrides apply to their occurrences, including a parent array + # with expanded elements. A container's density never replaces leaf data. + candidates = [obj for obj in chain if not _container(_definition_source(obj)[0])] + [source] + try: + parent = source.getParentGeoFeatureGroup() + except AttributeError: + parent = None + if parent is not None and parent.isDerivedFrom("PartDesign::Body"): + candidates.append(parent) + owner = next((obj for obj in candidates if "QIDensity" in obj.PropertiesList), None) + if owner is None: + return "Сталь", 7850.0, True # preserve the existing solid-service default + try: + value = owner.QIDensity + if hasattr(value, "Unit") and value.Unit != App.Units.Unit(): + value = value.getValueAs("kg/m^3") + density = float(getattr(value, "Value", value)) + except (TypeError, ValueError, OverflowError, ArithmeticError) as exc: + raise ValueError("Плотность детали «%s» должна быть числом в кг/м³" % source.Label) from exc + if not math.isfinite(density) or density <= 0: + raise ValueError("Плотность детали «%s» должна быть конечной и больше нуля" % source.Label) + material = str(getattr(owner, "QIMaterial", "Не указан")) or "Не указан" + return material, density, False + + +def _empty(): + return {"volume_mm3": 0.0, "area_mm2": 0.0, "mass_kg": 0.0, "density_kg_m3": 0.0, + "material": "Не указан", "materials": [], "center_mm": (0.0, 0.0, 0.0), + "dimensions_mm": (0.0, 0.0, 0.0), "solids": 0, "components": 0, "assembly_count": 0} + + +def _aggregate(nodes): + leaves = [] + assembly_count = 0 + pending = list(nodes) + while pending: + node = pending.pop() + if node["is_assembly"]: + assembly_count += 1; pending.extend(node["children"]) + else: + leaves.append(node) + if not leaves: + return _empty() + total = _empty() + box = App.BoundBox() + weighted = App.Vector() + materials = [] + for node in leaves: + props = node["properties"] + total["volume_mm3"] += props["volume_mm3"] + total["area_mm2"] += props["area_mm2"] + total["mass_kg"] += props["mass_kg"] + total["solids"] += props["solids"] + box.add(node["shape"].BoundBox) + weighted += App.Vector(*props["center_mm"]) * props["mass_kg"] + if props["material"] not in materials: + materials.append(props["material"]) + if not math.isfinite(total["mass_kg"]) or total["mass_kg"] <= 0: + raise ValueError("Не удалось вычислить конечную положительную массу компонентов") + center = weighted / total["mass_kg"] + total.update(density_kg_m3=total["mass_kg"] / (total["volume_mm3"] * 1e-9), + material=materials[0] if len(materials) == 1 else "Смешанные материалы", materials=materials, + center_mm=(center.x, center.y, center.z), dimensions_mm=(box.XLength, box.YLength, box.ZLength), + components=len(leaves), assembly_count=assembly_count) + return total + + +def _signature(number): + return float(format(float(number), ".12g")) + + +def _scale_signature(matrix): + # Gram matrix ignores a rigid world rotation but retains effective shear / + # anisotropic scaling. Determinant parity distinguishes mirrored geometry. + rows = [[getattr(matrix, "A%d%d" % (r, c)) for c in (1, 2, 3)] for r in (1, 2, 3)] + gram = tuple(_signature(sum(rows[k][r] * rows[k][c] for k in range(3))) for r in range(3) for c in range(r, 3)) + determinant = (rows[0][0] * (rows[1][1] * rows[2][2] - rows[1][2] * rows[2][1]) + - rows[0][1] * (rows[1][0] * rows[2][2] - rows[1][2] * rows[2][0]) + + rows[0][2] * (rows[1][0] * rows[2][1] - rows[1][1] * rows[2][0])) + if not math.isfinite(determinant) or abs(determinant) <= 1e-14: + raise ValueError("Преобразование компонента должно быть конечным и невырожденным") + return gram, -1 if determinant < 0 else 1 + + +def _nodes(obj, context, path, stack=(), inherited_chain=(), array_element=False): + if _ignored(obj): + return [] + if obj.TypeId == "App::Link" and getattr(obj, "ElementCount", 0) > 0 and not array_element: + result = [] + # Numeric paths include hidden elements even when ElementList is empty. + for index in range(obj.ElementCount): + elements = obj.ElementList + element = elements[index] if index < len(elements) else obj + result.extend(_nodes(element, context, path + str(index) + ".", stack, + inherited_chain + (obj,), array_element=True)) + return result + source, chain = _definition_source(obj) + source_key = _key(source) + if source_key in stack: + raise ValueError("Обнаружен цикл в составе сборки «%s»" % obj.Label) + stack = stack + (source_key,) + complete_chain = inherited_chain + tuple(chain) + is_assembly = _container(source) + if is_assembly: + container = obj if obj.TypeId == "Assembly::AssemblyLink" else source + children = _physical_children(container) + descendants = [] + for child in children: + if _potential(child): + descendants.extend(_nodes(child, context, path + child.Name + ".", stack, complete_chain)) + props = _aggregate(descendants) + if not descendants: + if obj.TypeId in ("App::Link", "App::LinkElement", "Assembly::AssemblyLink"): + raise ValueError("Компонент «%s» ссылается на контейнер без твёрдых деталей" % obj.Label) + return [] + # A missing/wrong AssemblyLink target has already been rejected even if + # it exposes an apparently empty child group. + node = {"source": source, "occurrence": obj, "is_assembly": True, "children": descendants, + "properties": props, "shape": None, "path": path} + child_keys = tuple(child["group_key"] for child in descendants) + node["group_key"] = (source_key, "assembly", child_keys) + return [node] + if not _potential(source): + if obj.TypeId in _LINK_TYPES and not _ignored(source): + raise ValueError("Компонент «%s» не содержит готовой твёрдой детали" % obj.Label) + return [] + try: + shape, matrix, _subobject = Part.getShape(context, path, retType=1) + except Exception as exc: + raise ValueError("Не удалось получить геометрию компонента «%s»" % obj.Label) from exc + if (shape is None or shape.isNull() or not shape.isValid() or not shape.Solids or not _only_solids(shape)): + raise ValueError("Компонент «%s» не содержит корректной геометрии только из твёрдых тел" % obj.Label) + volumes = [abs(float(solid.Volume)) for solid in shape.Solids] + volume = sum(volumes) + if not math.isfinite(volume) or volume <= _EPS: + raise ValueError("Объём компонента «%s» должен быть конечным и больше нуля" % obj.Label) + center = App.Vector() + for solid, value in zip(shape.Solids, volumes): + center += solid.CenterOfMass * value + center /= volume + material, density, assumed = _material(source, complete_chain) + mass = volume * 1e-9 * density + if not math.isfinite(mass) or mass <= 0: + raise ValueError("Масса компонента «%s» должна быть конечной и больше нуля" % obj.Label) + box = shape.BoundBox + props = {"volume_mm3": volume, "area_mm2": shape.Area, "mass_kg": mass, "density_kg_m3": density, + "material": material, "materials": [material], "center_mm": (center.x, center.y, center.z), + "dimensions_mm": (box.XLength, box.YLength, box.ZLength), "solids": len(shape.Solids), + "components": 1, "assembly_count": 0, "material_assumed": assumed} + if context is obj and not path and obj.TypeId in ("App::Link", "App::LinkElement"): + # Part.getShape's rootless matrix omits baked link scale. The native + # resolved-link transform includes effective scale, including 2 * .5. + _linked_source, matrix = obj.getLinkedObject(True, App.Matrix()) + variant = _scale_signature(matrix) + return [{"source": source, "occurrence": obj, "is_assembly": False, "children": [], "properties": props, + "shape": shape, "path": path, "group_key": (source_key, variant, material, density, + _signature(volume), _signature(shape.Area))}] + + +def _trees(doc, root=None): + doc = _document(doc) + if root is not None: + root = _object(root) + if root.Document is not doc: + raise ValueError("Область спецификации должна принадлежать выбранному документу") + roots = [root] + else: + roots = document_bom_roots(doc) + return snapshot_from_roots(roots, recompute=True)["trees"] + + +def snapshot_from_roots(roots, recompute=False): + """One native occurrence snapshot for an explicit set of drawing sources. + + Sources may belong to linked documents. No whole-document discovery is + performed here. The prepared path is safe inside a native Python execute. + """ + roots = [_object(obj) for obj in roots] + if len({_key(obj) for obj in roots}) != len(roots): + raise ValueError("Область спецификации содержит повторную ссылку на один объект") + if recompute: + _sync(roots) + else: + _validate_ready(roots) + trees = [] + for obj in roots: + context, prefix = _scene_context(obj) + trees.extend(_nodes(obj, context, prefix)) + return {"trees": trees, "summary": _aggregate(trees)} + + +def available_bom_roots(doc): + """Physical top roots and their nested assembly occurrences for GUI scope.""" + roots = document_bom_roots(doc) + for obj in roots: + _check_cycles(obj) + result = list(roots) + pending = list(roots) + seen = {_key(obj) for obj in roots} + while pending: + obj = pending.pop(0) + source, _chain = _definition_source(obj) + if not _container(source) or obj.TypeId in ("App::Link", "App::LinkElement"): + continue + for child in obj.Group if obj.TypeId == "Assembly::AssemblyLink" else source.Group: + if not _potential(child): + continue + child_source, _chain = _definition_source(child) + if child.Document is doc and _container(child_source) and _key(child) not in seen: + seen.add(_key(child)); result.append(child); pending.append(child) + return result + + +def bom_summary(doc, root=None): + return _aggregate(_trees(doc, root)) + + +def mass_properties(obj): + obj = _object(obj) + _sync([obj]) + context, prefix = _scene_context(obj) + nodes = _nodes(obj, context, prefix) + props = _aggregate(nodes) + if not props["solids"]: + raise ValueError("Объект не содержит твёрдых компонентов для расчёта массы") + if len(nodes) == 1 and not nodes[0]["is_assembly"]: + props["material_assumed"] = nodes[0]["properties"].get("material_assumed", False) + return props + + +def _row(group, position, parent, level, parent_count=1): + node = group[0] + source = node["source"] + total_count = len(group) + if total_count % parent_count: + raise ValueError("Невозможно представить неодинаковый состав экземпляров одной структурной строкой") + props = _aggregate(group) + number_source = source + if not hasattr(source, "QIPartNumber"): + try: + body = source.getParentGeoFeatureGroup() + except AttributeError: + body = None + if body is not None and body.isDerivedFrom("PartDesign::Body"): + number_source = body + source_id = source.Document.Name + "#" + source.Name + return {"position": position, "parent_position": parent, "level": level, + "part_number": str(getattr(number_source, "QIPartNumber", source.Name)), "name": source.Label, + "quantity": total_count // parent_count, "quantity_total": total_count, + "material": props["material"], "density_kg_m3": props["density_kg_m3"], + "unit_mass_kg": props["mass_kg"] / total_count, "mass_kg": props["mass_kg"], + "source_id": source_id, "source_name": source.Name, "source_doc": source.Document.Name, + "is_assembly": node["is_assembly"], "kind": "assembly" if node["is_assembly"] else "part", + "row_id": source_id + "|" + position, "variant_id": repr(node["group_key"][1:]), + "materials": props["materials"]} + + +def _groups(nodes): + groups = {} + for node in nodes: + groups.setdefault(node["group_key"], []).append(node) + return list(groups.values()) + + +def rows_from_snapshot(snapshot, mode="flat"): + """Render rows from the same validated native snapshot used for summary.""" + if mode not in ("flat", "structured"): + raise ValueError("Представление спецификации должно быть flat или structured") + trees = snapshot["trees"] + if mode == "flat": + leaves = [] + pending = list(reversed(trees)) + while pending: + node = pending.pop() + if node["is_assembly"]: + pending.extend(reversed(node["children"])) + else: + leaves.append(node) + return [_row(group, str(index), "", 0) for index, group in enumerate(_groups(leaves), 1)] + # The BOM of one selected assembly describes its contents, not itself. + nodes = trees[0]["children"] if len(trees) == 1 and trees[0]["is_assembly"] else trees + rows = [] + + def append_level(level_nodes, parent="", level=0, parent_count=1): + for index, group in enumerate(_groups(level_nodes), 1): + position = (parent + "." if parent else "") + str(index) + rows.append(_row(group, position, parent, level, parent_count)) + if group[0]["is_assembly"]: + append_level([child for node in group for child in node["children"]], position, level + 1, len(group)) + + append_level(nodes) + return rows + + +def bom_rows(doc, mode="flat", root=None): + return rows_from_snapshot({"trees": _trees(doc, root)}, mode) + + +def export_bom(doc, path, mode="flat", root=None): + """Validate the entire scoped BOM before creating/replacing a UTF-8 CSV.""" + rows = bom_rows(doc, mode, root) + if not rows: + raise ValueError("В выбранной области нет твёрдых компонентов для экспорта спецификации") + path = Path(path).absolute() + if path.suffix.lower() != ".csv": + raise ValueError("Спецификация сохраняется в файл CSV") + path.parent.mkdir(parents=True, exist_ok=True) + temporary = None + try: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8-sig", newline="", dir=str(path.parent), + prefix="." + path.name + ".", suffix=".tmp", delete=False) as stream: + temporary = Path(stream.name) + writer = csv.writer(stream, delimiter=";") + writer.writerow(["Поз.", "Уровень", "Родитель", "Обозначение", "Наименование", "Количество", "Всего", "Материал", + "Масса единицы, кг", "Масса, кг", "Источник", "Тип"]) + for row in rows: + writer.writerow([row["position"], row["level"], row["parent_position"], row["part_number"], row["name"], + row["quantity"], row["quantity_total"], row["material"], format(row["unit_mass_kg"], ".17g"), + format(row["mass_kg"], ".17g"), row["source_id"], "Сборка" if row["is_assembly"] else "Деталь"]) + stream.flush(); os.fsync(stream.fileno()) + os.replace(str(temporary), str(path)) + temporary = None + finally: + if temporary is not None and temporary.exists(): + temporary.unlink() + return str(path) diff --git a/src/QInventor/qi_bom_ui.py b/src/QInventor/qi_bom_ui.py new file mode 100644 index 0000000..17399b5 --- /dev/null +++ b/src/QInventor/qi_bom_ui.py @@ -0,0 +1,265 @@ +"""Russian read-only BOM views for real native component instances.""" +from __future__ import annotations + +import os + +import FreeCAD as App +import FreeCADGui as Gui +from PySide import QtCore, QtGui +try: + from PySide import QtWidgets +except ImportError: + QtWidgets = QtGui + + +_dialogs = {} + + +def _service(): + import qi_bom + return qi_bom + + +def _number(value): + """Keep twelve significant digits; only the display uses a decimal comma.""" + return format(float(value), ".12g").replace(".", ",") + + +class BOMTable(QtWidgets.QDialog): + COLUMNS = (("position", "Поз."), ("part_number", "Обозначение"), ("name", "Наименование"), + ("quantity", "Кол-во"), ("quantity_total", "Всего"), ("material", "Материал"), + ("unit_mass_kg", "Масса единицы, кг"), ("mass_kg", "Масса, кг")) + + def __init__(self, doc, parent=None): + super().__init__(parent or Gui.getMainWindow()) + self.document = doc + self.document_name = doc.Name + self.rows = [] + self.summary_data = None + self.setObjectName("QI_BOMTable") + self.setWindowTitle("Спецификация — " + doc.Label) + self.setModal(False) + self.resize(1180, 620) + self.setMinimumSize(900, 420) + layout = QtWidgets.QVBoxLayout(self) + instructions = QtWidgets.QLabel("Состав и масса рассчитываются по экземплярам деталей. После изменения модели обновите спецификацию.") + instructions.setWordWrap(True) + layout.addWidget(instructions) + controls = QtWidgets.QHBoxLayout() + controls.addWidget(QtWidgets.QLabel("Область:")) + self.root_selector = QtWidgets.QComboBox() + self.root_selector.setObjectName("QI_BOMRoot") + self.root_selector.setMinimumContentsLength(22) + controls.addWidget(self.root_selector, 1) + controls.addWidget(QtWidgets.QLabel("Представление:")) + self.mode_selector = QtWidgets.QComboBox() + self.mode_selector.setObjectName("QI_BOMMode") + self.mode_selector.addItem("Сводная — только детали", "flat") + self.mode_selector.addItem("Структурная — состав сборок", "structured") + controls.addWidget(self.mode_selector) + self.refresh_button = QtWidgets.QPushButton("Обновить") + self.refresh_button.setObjectName("QI_BOMRefresh") + self.refresh_button.clicked.connect(self.refresh) + controls.addWidget(self.refresh_button) + layout.addLayout(controls) + + self.tree = QtWidgets.QTreeWidget() + self.tree.setObjectName("QI_BOMRows") + self.tree.setColumnCount(len(self.COLUMNS)) + self.tree.setHeaderLabels([label for _key, label in self.COLUMNS]) + self.tree.setAlternatingRowColors(True) + self.tree.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.tree.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.tree.header().setStretchLastSection(False) + self.tree.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) + self.tree.header().setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch) + self.tree.headerItem().setToolTip(3, "Количество в непосредственной родительской сборке.") + self.tree.headerItem().setToolTip(4, "Количество с учётом повторных экземпляров всех родительских сборок.") + self.tree.headerItem().setToolTip(7, "Суммарная масса этой позиции с учётом её общего количества.") + layout.addWidget(self.tree, 1) + self.summary = QtWidgets.QLabel("") + self.summary.setObjectName("QI_BOMSummary") + self.summary.setWordWrap(True) + layout.addWidget(self.summary) + self.status = QtWidgets.QLabel("") + self.status.setObjectName("QI_BOMStatus") + self.status.setWordWrap(True) + self.status.setMinimumHeight(32) + layout.addWidget(self.status) + self.buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close) + self.buttons.setObjectName("QI_BOMButtons") + self.export_button = self.buttons.addButton("Экспорт CSV", QtWidgets.QDialogButtonBox.ActionRole) + self.export_button.setObjectName("QI_BOMExport") + self.export_button.clicked.connect(self.export_csv) + self.buttons.button(QtWidgets.QDialogButtonBox.Close).setText("Закрыть") + self.buttons.rejected.connect(self.close) + layout.addWidget(self.buttons) + self.root_selector.currentIndexChanged.connect(self._changed) + self.mode_selector.currentIndexChanged.connect(self._changed) + self.refresh() + + def _document(self): + if App.listDocuments().get(self.document_name) != self.document: + raise ValueError("Документ уже закрыт. Закройте спецификацию и откройте существующий проект.") + if Gui.Control.activeDialog(): + raise ValueError("Завершите редактирование детали или сборки перед обновлением спецификации.") + return self.document + + def _root(self): + name = self.root_selector.currentData() or "" + if not name: + return None + root = self.document.getObject(str(name)) + if root is None: + raise ValueError("Выбранная сборка уже удалена. Нажмите «Обновить» и выберите область спецификации.") + return root + + def _roots(self): + previous = self.root_selector.currentData() or "" + roots = _service().available_bom_roots(self.document) + label_counts = {root.Label: sum(candidate.Label == root.Label for candidate in roots) for root in roots} + self.root_selector.blockSignals(True) + try: + self.root_selector.clear() + self.root_selector.addItem("Весь документ", "") + for root in roots: + label = root.Label + (" [" + root.Name + "]" if label_counts[root.Label] > 1 else "") + self.root_selector.addItem(label, root.Name) + self.root_selector.setItemData(self.root_selector.count() - 1, + root.Document.Label + " / " + root.Name, QtCore.Qt.ToolTipRole) + index = self.root_selector.findData(previous) + self.root_selector.setCurrentIndex(index if index >= 0 else 0) + finally: + self.root_selector.blockSignals(False) + return bool(previous and index < 0) + + def _changed(self, *_args): + self.refresh(reload_roots=False) + + def _render(self, rows, summary): + self.tree.clear() + self.rows = list(rows) + self.summary_data = dict(summary) + structured = self.mode_selector.currentData() == "structured" + self.tree.setRootIsDecorated(structured) + self.tree.headerItem().setToolTip(3, "Количество в непосредственной родительской сборке." if structured + else "Общее количество экземпляров детали в выбранной области.") + items = {} + for row in self.rows: + texts = [] + for key, _label in self.COLUMNS: + value = row.get(key, row.get("quantity", 0) if key == "quantity_total" else "") + texts.append(_number(value) if key in ("unit_mass_kg", "mass_kg") else str(value)) + item = QtWidgets.QTreeWidgetItem(texts) + item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) + item.setData(0, QtCore.Qt.UserRole, dict(row)) + source = str(row.get("source_id", "")) + for column in range(len(self.COLUMNS)): + item.setToolTip(column, source) + if column in (3, 4, 6, 7): + item.setTextAlignment(column, QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + if row.get("is_assembly"): + font = item.font(column) + font.setBold(True) + item.setFont(column, font) + items[str(row.get("position", ""))] = item + for row in self.rows: + item = items[str(row.get("position", ""))] + parent = items.get(str(row.get("parent_position", ""))) if structured else None + if parent is not None: + parent.addChild(item) + else: + self.tree.addTopLevelItem(item) + self.tree.expandAll() + materials = summary.get("materials", []) + if isinstance(materials, str): + materials = [materials] + material_text = ", ".join(str(value) for value in materials) or str(summary.get("material", "—")) + self.summary.setText("Всего деталей: %s. Масса: %s кг. Материалы: %s." % + (summary.get("components", 0), _number(summary["mass_kg"]), material_text)) + self.export_button.setEnabled(bool(self.rows)) + + def _error(self, error): + from qi_messages import error_text + App.Console.PrintError("QInventor BOM: {}\n".format(error)) + explanation = error_text(error, "Проверьте компоненты, материалы и состояние модели.") + self.status.setText("Спецификация не обновлена. " + explanation + + (" Показаны данные предыдущего расчёта." if self.rows else "")) + self.export_button.setEnabled(False) + + def refresh(self, *_args, reload_roots=True): + try: + self._document() + changed_root = self._roots() if reload_roots else False + root = self._root() + mode = str(self.mode_selector.currentData()) + rows = _service().bom_rows(self.document, mode=mode, root=root) + summary = _service().bom_summary(self.document, root=root) + self._render(rows, summary) + if changed_root: + self.status.setText("Выбранная сборка удалена. Показана спецификация всего документа.") + else: + self.status.setText("Спецификация обновлена." if rows else "В выбранной области нет деталей для спецификации.") + return True + except Exception as exc: + self._error(exc) + return False + + def export_csv(self): + try: + self._document() + # Validate the still-selected scope before any automatic refresh. + # A deleted root must never silently change the exported assembly. + self._root() + if not self.refresh(reload_roots=False) or not self.rows: + return + root = self._root() + mode = str(self.mode_selector.currentData()) + suffix = "сводная" if mode == "flat" else "структурная" + path, _selected = QtWidgets.QFileDialog.getSaveFileName( + self, "Экспорт спецификации", self.document.Label + "-" + suffix + ".csv", "CSV (*.csv)") + if not path: + return + if not path.lower().endswith(".csv"): + path += ".csv" + # The service rebuilds the selected representation and atomically + # replaces the target only after all component validation succeeds. + saved = _service().export_bom(self.document, path, mode=mode, root=root) + rows = _service().bom_rows(self.document, mode=mode, root=root) + summary = _service().bom_summary(self.document, root=root) + self._render(rows, summary) + self.status.setText("Спецификация сохранена: " + os.path.abspath(saved)) + except Exception as exc: + self._error(exc) + + +def show_bom(doc=None): + """Show the document BOM independently of selection and model editing.""" + doc = doc or App.ActiveDocument + if doc is None: + raise ValueError("Создайте или откройте документ перед просмотром спецификации.") + if Gui.Control.activeDialog(): + raise ValueError("Завершите редактирование детали или сборки перед просмотром спецификации.") + existing = _dialogs.get(doc.Name) + if existing is not None: + try: + if existing.isVisible() and existing.document == doc: + existing.refresh() + existing.raise_() + existing.activateWindow() + return existing + except RuntimeError: + pass + dialog = BOMTable(doc) + _dialogs[doc.Name] = dialog + + def forget(_result=None): + if _dialogs.get(dialog.document_name) is dialog: + _dialogs.pop(dialog.document_name, None) + dialog.deleteLater() + + dialog.finished.connect(forget) + dialog.show() + dialog.raise_() + dialog.activateWindow() + return dialog diff --git a/src/QInventor/qi_commands.py b/src/QInventor/qi_commands.py new file mode 100644 index 0000000..6d2a03c --- /dev/null +++ b/src/QInventor/qi_commands.py @@ -0,0 +1,667 @@ +"""QInventor commands: parametric services and genuine FreeCAD tool dispatch.""" +from __future__ import annotations + +import os +import traceback + +import FreeCAD as App +import FreeCADGui as Gui +from PySide import QtCore, QtGui +try: + from PySide import QtWidgets +except ImportError: + QtWidgets = QtGui + + +COMMANDS = {} +_SKETCH_SESSION = None +_SHORTCUTS = {} + + +def _command_icon(name, fallback="document-new"): + if name in Gui.listCommands(): + return Gui.Command.get(name).getInfo().get("pixmap") or fallback + return fallback + + +def configure_shortcuts(): + """Move owned keys once through the native command API; no duplicate actions.""" + for name in ("Std_New", "Std_Open", "Std_Save", "Sketcher_Dimension", "Sketcher_ConstrainEqual", + "Sketcher_ConstrainDistanceX", "Sketcher_ConstrainCoincident", + "Sketcher_ConstrainCoincidentUnified"): + if name in Gui.listCommands(): + command = Gui.Command.get(name) + if name not in _SHORTCUTS: + _SHORTCUTS[name] = command.getShortcut() + command.setShortcut("") + + +def begin_sketch_edit(sketch, created=False, body_created=False): + """Take the native object snapshot synchronously when an edit session opens.""" + global _SKETCH_SESSION + key = (sketch.Document.Name, sketch.Name) + if _SKETCH_SESSION and _SKETCH_SESSION["key"] == key: + return + _SKETCH_SESSION = {"key": key, "content": sketch.dumpContent(), + "created": created, "body_created": body_created, + "body": sketch.getParentGeoFeatureGroup()} + + +def end_sketch_edit(): + global _SKETCH_SESSION + _SKETCH_SESSION = None + + +def _model(): + import qi_model + return qi_model + + +def _ui(): + import qi_ui + return qi_ui + + +def _document(): + if App.ActiveDocument is None: + raise ValueError("Создайте или откройте документ перед выполнением команды.") + return App.ActiveDocument + + +def _selection(): + return [obj for obj in Gui.Selection.getSelection() if obj.Document == App.ActiveDocument] + + +def _editing(): + active = Gui.activeDocument() + editing = active.getInEdit() if active else None + return getattr(editing, "Object", editing) + + +def _selected_sketch(): + selection = _selection() + if len(selection) != 1 or not selection[0].isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Выберите один эскиз в дереве модели.") + return selection[0] + + +def _selected_body(): + for obj in _selection(): + if obj.isDerivedFrom("PartDesign::Body"): + return obj + body = _model()._parent_body(obj) + if body: + return body + active = Gui.activeDocument() + body = active.activeView().getActiveObject("pdbody") if active else None + if body and body.isDerivedFrom("PartDesign::Body"): + return body + return None + + +def _solid_target(): + selected = _selection() + if len(selected) == 1 and hasattr(selected[0], "Shape") and not selected[0].Shape.isNull(): + return selected[0] + body = _selected_body() + if body and not body.Shape.isNull(): + return body + bodies = [o for o in _document().Objects if o.isDerivedFrom("PartDesign::Body") and not o.Shape.isNull()] + if len(bodies) == 1: + return bodies[0] + raise ValueError("Выберите деталь или тело в дереве модели.") + + +def _properties_target(): + selected = _selection() + if len(selected) == 1: + return selected[0] + if not selected: + import qi_bom + doc = _document() + roots = [obj for obj in qi_bom.document_bom_roots(doc) + if obj.isDerivedFrom("App::Part") + and qi_bom.bom_summary(doc, root=obj)["components"]] + if len(roots) == 1: + return roots[0] + return _solid_target() + + +def _fit_after_layout(doc=None): + doc = doc or App.ActiveDocument + if not doc: + return + name = doc.Name + editing = _editing() + edit_name = editing.Name if editing else None + def fit(): + active = App.ActiveDocument + current_edit = _editing() + current_name = current_edit.Name if current_edit else None + if active and active.Name == name and current_name == edit_name and Gui.activeDocument(): + view = Gui.activeDocument().activeView() + if not (current_edit and current_edit.isDerivedFrom("Sketcher::SketchObject") and current_edit.GeometryCount == 0): + view.fitAll() + view.redraw() + QtCore.QTimer.singleShot(200, fit) + + +def _native(name, workbench=None, needs_sketch=False, selection_kind=None): + """Dispatch only a registered upstream tool; never substitute a fake result.""" + _document() + if needs_sketch and not (_editing() and _editing().isDerivedFrom("Sketcher::SketchObject")): + raise ValueError("Откройте эскиз двойным щелчком или создайте новый эскиз.") + if selection_kind == "sketch": + sketch = _selected_sketch() + if _editing(): + raise ValueError("Сначала нажмите «Завершить эскиз».") + if sketch.GeometryCount == 0: + raise ValueError("Эскиз пуст. Постройте замкнутый профиль перед созданием объёма.") + commands = set(Gui.listCommands()) + if name not in commands and workbench: + previous = Gui.activeWorkbench().name() + try: + Gui.activateWorkbench(workbench) + finally: + Gui.activateWorkbench(previous) + commands = set(Gui.listCommands()) + if name not in commands: + raise RuntimeError("Инструмент {} не зарегистрирован в установленном ядре FreeCAD.".format(name)) + if not Gui.Command.get(name).isActive(): + raise ValueError("Инструмент сейчас недоступен. Проверьте выбор геометрии и завершите открытую панель операции.") + Gui.runCommand(name) + + +def _sketch_native(name, hint=""): + _native(name, "SketcherWorkbench", needs_sketch=True) + _ui().set_sketch_tool_hint(hint) + + +def _file_open(): + path, _ = QtWidgets.QFileDialog.getOpenFileName(Gui.getMainWindow(), "Открыть модель", "", "Модели (*.FCStd *.step *.stp *.iges *.igs *.brep *.stl);;Все файлы (*.*)") + if not path: + return + path = os.path.abspath(path) + suffix = os.path.splitext(path)[1].lower() + if suffix not in (".fcstd", ".step", ".stp", ".iges", ".igs", ".brep", ".stl"): + raise ValueError("Формат файла не поддерживается. Выберите FCStd, STEP, IGES, BREP или STL.") + try: + with open(path, "rb") as stream: + if not stream.read(1): + raise ValueError("Файл модели пуст.") + except OSError as exc: + raise ValueError("Не удалось прочитать файл модели. Проверьте его наличие и доступ к нему.") from exc + + before = set(App.listDocuments()) + previous = App.ActiveDocument.Name if App.ActiveDocument else None + try: + if suffix == ".fcstd": + # Re-select an already open project without reloading unsaved edits. + wanted = os.path.normcase(os.path.realpath(path)) + doc = next((d for d in App.listDocuments().values() if d.FileName + and os.path.normcase(os.path.realpath(d.FileName)) == wanted), None) + if doc is None: + doc = App.openDocument(path) + from qi_io import restore_document_label + restore_document_label(doc, path) + App.setActiveDocument(doc.Name) + else: + # Gui.open handles macros and scene files. Gui.loadFile additionally + # invokes importOptions dialogs; use the actual geometry importer. + if suffix == ".stl": + import Mesh + Mesh.open(path) + else: + import Part + Part.open(path) + doc = App.ActiveDocument + if doc is None or doc.Name in before: + raise ValueError("Импорт не создал новый документ модели.") + geometry = any( + (hasattr(obj, "Shape") and not obj.Shape.isNull() and obj.Shape.isValid()) + or (hasattr(obj, "Mesh") and obj.Mesh.CountFacets > 0) + for obj in doc.Objects) + if not geometry: + raise ValueError("Файл не содержит доступной геометрии модели.") + view = Gui.getDocument(doc.Name).activeView() + if view: + view.viewAxonometric() + view.fitAll() + _fit_after_layout(doc) + return doc + except Exception as exc: + # Failed native restoration can leave an empty document alive. Remove + # only documents created by this open and keep the user's former model. + for name in set(App.listDocuments()) - before: + App.closeDocument(name) + if previous in App.listDocuments(): + App.setActiveDocument(previous) + raise ValueError("Не удалось открыть модель. Файл повреждён или его содержимое не соответствует формату.") from exc + + +def _save(save_as=False): + doc = _document() + path = "" if save_as else doc.FileName + if not path: + path, _ = QtWidgets.QFileDialog.getSaveFileName(Gui.getMainWindow(), "Сохранить проект QInventor", doc.Label + ".FCStd", "Проект CAD (*.FCStd)") + if path: + if not path.lower().endswith(".fcstd"): + path += ".FCStd" + _model().save_project(doc, path) + _ui().update_title() + + +def _new_sketch(): + if _editing() and _editing().isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Завершите или отмените текущий эскиз перед созданием нового.") + doc = App.ActiveDocument + body = _selected_body() + if body and body.Document != doc: + body = None + selected = [item for item in Gui.Selection.getSelectionEx() if doc and item.Object.Document == doc] + support = None + if len(selected) == 1: + faces = [n for n in selected[0].SubElementNames if n.startswith("Face")] + if len(faces) == 1: + obj = selected[0].Object + if obj.isDerivedFrom("App::Link"): + raise ValueError("Эскиз на компоненте сборки создаётся в исходной детали. Выберите исходное тело детали в дереве модели.") + face = obj.Shape.getElement(faces[0]) + import Part + if not isinstance(face.Surface, Part.Plane): + raise ValueError("Для эскиза выберите плоскую грань или одну из базовых плоскостей.") + selected_body = _model()._parent_body(obj) + if selected_body: + body = selected_body + support_object = obj.Tip if obj.isDerivedFrom("PartDesign::Body") and obj.Tip else obj + support = (support_object, [faces[0]]) + plane = None + if not support: + plane = _ui().choose_plane() + if not plane: + return + if doc is None: + doc = _model().new_part() + # An untouched native view can span less than two millimetres. Give the + # first sketch a useful drafting scale before Sketcher stores its camera. + # Be conservative: any existing model/unknown object preserves the view. + empty_types = {"PartDesign::Body", "App::Part", "App::Origin", "App::Line", "App::Plane"} + blank_scene = not support and all( + obj.TypeId in empty_types + or (obj.isDerivedFrom("Sketcher::SketchObject") and obj.GeometryCount == 0) + for obj in doc.Objects) + body_created = body is None + doc.openTransaction("Создать эскиз") + try: + if body is None: + body = doc.addObject("PartDesign::Body", "Body") + body.Label = "Деталь" + sketch = body.newObject("Sketcher::SketchObject", "Sketch") + sketch.Label = "Эскиз" + if support: + sketch.AttachmentSupport = support + sketch.MapMode = "FlatFace" + elif plane == "XZ": + sketch.Placement.Rotation = App.Rotation(App.Vector(1, 0, 0), 90) + elif plane == "YZ": + sketch.Placement.Rotation = App.Rotation(App.Vector(1, 1, 1), 120) + if hasattr(sketch.ViewObject, "Autoconstraints"): + sketch.ViewObject.Autoconstraints = True + if hasattr(sketch.ViewObject, "AvoidRedundant"): + sketch.ViewObject.AvoidRedundant = True + doc.recompute() + doc.commitTransaction() + except Exception: + doc.abortTransaction() + raise + Gui.Selection.clearSelection() + view = Gui.activeDocument().activeView() + view.setActiveObject("pdbody", body) + if blank_scene: + view.setCameraType("Orthographic") + view.getCameraNode().height.setValue(120.0) + begin_sketch_edit(sketch, created=True, body_created=body_created) + Gui.activeDocument().setEdit(sketch.Name) + _ui().select_tab("Эскиз") + + +def _new_assembly(): + doc = _model().new_assembly() + assembly = doc.getObject("Assembly") + if assembly and assembly.isDerivedFrom("Assembly::AssemblyObject"): + Gui.activeDocument().setEdit(assembly.Name) + _ui().select_tab("Сборка") + return doc + + +def _extrude(): + if _editing() and _editing().isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Сначала нажмите «Завершить эскиз».") + import qi_extrude + qi_extrude.show_dialog() + + +def _finish_sketch(): + doc = _document() + edit = _editing() + if not edit or not edit.isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Сейчас нет открытого эскиза.") + _native("Sketcher_LeaveSketch", "SketcherWorkbench", needs_sketch=True) + doc.recompute() + Gui.Selection.clearSelection() + Gui.Selection.addSelection(edit) + _ui().select_tab("3D-модель") + + +def _cancel_sketch(): + global _SKETCH_SESSION + doc = _document() + edit = _editing() + if not edit or not edit.isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Сейчас нет открытого эскиза.") + session = _SKETCH_SESSION + if not session or session["key"] != (doc.Name, edit.Name): + raise ValueError("Не найдено исходное состояние эскиза. Завершите редактирование и откройте эскиз снова.") + _SKETCH_SESSION = None + _native("Sketcher_LeaveSketch", "SketcherWorkbench", needs_sketch=True) + with _model()._transaction(doc, "Отменить изменения эскиза"): + if session["created"]: + body = session["body"] + doc.removeObject(edit.Name) + if session["body_created"] and body and not body.Group: + doc.removeObject(body.Name) + else: + edit.restoreContent(session["content"]) + Gui.Selection.clearSelection() + _ui().select_tab("3D-модель") + + +def _primitive(kind): + values = _ui().primitive_dialog(kind) + if values is None: + return + functions = {"box": "create_box", "cylinder": "create_cylinder", "plate": "create_plate_with_holes"} + obj = getattr(_model(), functions[kind])(**values) + Gui.Selection.clearSelection() + Gui.Selection.addSelection(obj) + Gui.activeDocument().activeView().viewAxonometric() + Gui.activeDocument().activeView().fitAll() + _fit_after_layout() + + +def _demo(assembly=False): + if assembly: + _model().create_assembly_demo() + else: + _model().make_demo() + Gui.activeDocument().activeView().viewAxonometric() + Gui.activeDocument().activeView().fitAll() + _fit_after_layout() + + +def _material(): + obj = _solid_target() + import qi_bom + source, _chain = qi_bom._definition_source(obj) + if source.isDerivedFrom("App::Part") or source.isDerivedFrom("App::DocumentObjectGroup"): + raise ValueError("Выберите конечную деталь или её экземпляр. Материалы сборки задаются у входящих деталей.") + materials = list(_model().MATERIALS) + name, ok = QtWidgets.QInputDialog.getItem(Gui.getMainWindow(), "Материал детали", "Материал:", materials, 0, False) + if ok: + _model().set_material(obj, name) + _ui().show_physical_properties(obj) + + +def _export(): + doc = _document() + filters = "STEP (*.step);;STL (*.stl);;IGES (*.iges);;BREP (*.brep)" + path, selected_filter = QtWidgets.QFileDialog.getSaveFileName(Gui.getMainWindow(), "Экспортировать модель", doc.Label + ".step", filters) + if not path: + return + extensions = {"STEP": ".step", "STL": ".stl", "IGES": ".iges", "BREP": ".brep"} + extension = extensions[selected_filter.split()[0]] + if not os.path.splitext(path)[1]: + path += extension + selected = _selection() + _model().export_model(doc, path, objects=selected or None) + Gui.getMainWindow().statusBar().showMessage("Модель экспортирована: " + path, 10000) + + +def _drawing(): + page = _model().create_drawing(_document()) + Gui.activeDocument().setEdit(page.Name) + _ui().select_tab("Чертёж") + + +def _drawing_export(): + doc = _document() + pages = [o for o in _selection() if o.isDerivedFrom("TechDraw::DrawPage")] + if not pages: + pages = [o for o in doc.Objects if o.isDerivedFrom("TechDraw::DrawPage")] + if len(pages) != 1: + raise ValueError("Выберите один лист чертежа в дереве модели.") + path, selected_filter = QtWidgets.QFileDialog.getSaveFileName(Gui.getMainWindow(), "Экспорт чертежа", doc.Label + ".pdf", "PDF (*.pdf);;SVG (*.svg);;DXF (*.dxf)") + if path: + extension = {"PDF": ".pdf", "SVG": ".svg", "DXF": ".dxf"}[selected_filter.split()[0]] + if not os.path.splitext(path)[1]: + path += extension + _model().export_drawing(pages[0], path) + Gui.getMainWindow().statusBar().showMessage("Чертёж экспортирован: " + path, 10000) + + +def _drawing_bom_page(): + doc = _document() + selected_pages = [obj for obj in _selection() if obj.isDerivedFrom("TechDraw::DrawPage")] + if selected_pages: + if len(selected_pages) != 1: + raise ValueError("Выберите один лист чертежа для размещения спецификации.") + return selected_pages[0] + pages = [obj for obj in doc.Objects if obj.isDerivedFrom("TechDraw::DrawPage")] + if len(pages) != 1: + raise ValueError("Выберите один существующий лист чертежа. Если листа ещё нет, создайте чертёж.") + return pages[0] + + +def _drawing_bom(): + page = _drawing_bom_page() + _ui().show_drawing_bom(page) + + +def _bom(): + _ui().show_bom(_document()) + + +def _view(method): + _document() + getattr(Gui.activeDocument().activeView(), method)() + + +class Command: + def __init__(self, title, action, icon="", tooltip="", shortcut="", requires_doc=False, native=None, availability=None): + self.title = title + self.action = action + self.icon = icon.replace(":/icons/", "").removesuffix(".svg") + self.tooltip = tooltip or title + self.shortcut = shortcut + self.requires_doc = requires_doc + self.native = native + self.availability = availability + + def GetResources(self): + resources = {"MenuText": self.title, "ToolTip": self.tooltip, "Pixmap": self.icon, "CmdType": "ForEdit"} + # The visible ribbon owns shortcuts across native workbench changes. + # QAction accelerators would disappear with the QInventor menu or + # conflict with native Sketcher_Dimension when the editor activates. + return resources + + def IsActive(self): + if self.requires_doc and App.ActiveDocument is None: + return False + if self.availability and not self.availability(): + return False + if self.native: + return self.native in Gui.listCommands() and Gui.Command.get(self.native).isActive() + return True + + def Activated(self): + try: + self.action() + _ui().update_title() + except Exception as exc: + App.Console.PrintError("QInventor: {}\n{}\n".format(exc, traceback.format_exc())) + if os.environ.get("QI_TEST"): + raise + from qi_messages import error_text + QtWidgets.QMessageBox.warning(Gui.getMainWindow(), "QInventor", error_text(exc)) + + +def register_commands(): + if COMMANDS: + return list(COMMANDS) + + def add(key, title, action, icon="", tooltip="", shortcut="", requires_doc=False, native=None, availability=None): + command = Command(title, action, _command_icon(native, icon) if native else icon, tooltip, shortcut, requires_doc, native, availability) + COMMANDS["QI_" + key] = command + Gui.addCommand("QI_" + key, command) + + configure_shortcuts() + add("NewPart", "Новая деталь", lambda: _model().new_part(), ":/icons/PartDesign_Body.svg", "Создать параметрическую деталь (Ctrl+N)", "Ctrl+N", availability=lambda: not Gui.Control.activeDialog()) + add("NewAssembly", "Новая сборка", _new_assembly, ":/icons/Geoassembly.svg") + add("Open", "Открыть", _file_open, ":/icons/document-open.svg", shortcut="Ctrl+O", availability=lambda: not Gui.Control.activeDialog()) + add("Save", "Сохранить", _save, ":/icons/document-save.svg", shortcut="Ctrl+S", requires_doc=True) + add("SaveAs", "Сохранить как", lambda: _save(True), ":/icons/document-save-as.svg", requires_doc=True) + add("NewSketch", "Создать эскиз", _new_sketch, ":/icons/Sketcher_NewSketch.svg", "Выбрать XY / XZ / YZ или плоскую грань и открыть редактор эскиза") + add("FinishSketch", "Завершить эскиз", _finish_sketch, _command_icon("Sketcher_LeaveSketch"), "Закрыть редактор и вернуться к модели", requires_doc=True) + add("CancelSketch", "Отменить эскиз", _cancel_sketch, _command_icon("Std_Delete"), "Отменить новые построения; существующий эскиз вернётся к состоянию до редактирования", requires_doc=True) + sketch_commands = [ + ("Line", "Линия", "Sketcher_CreatePolyline", "L"), + ("Polyline", "Отрезок", "Sketcher_CreateLine", ""), + ("Rectangle", "Прямоугольник", "Sketcher_CreateRectangle", ""), + ("Circle", "Окружность", "Sketcher_CreateCircle", "C"), + ("Arc", "Дуга", "Sketcher_CreateArc", ""), + ("Dimension", "Размер", "Sketcher_Dimension", "D"), + ("Horizontal", "Горизонтально", "Sketcher_ConstrainHorizontal", ""), + ("Vertical", "Вертикально", "Sketcher_ConstrainVertical", ""), + ("Coincident", "Совпадение", "Sketcher_ConstrainCoincident", ""), + ("Parallel", "Параллельно", "Sketcher_ConstrainParallel", ""), + ("Perpendicular", "Перпендикулярно", "Sketcher_ConstrainPerpendicular", ""), + ("Equal", "Равенство", "Sketcher_ConstrainEqual", ""), + ("Tangent", "Касательная", "Sketcher_ConstrainTangent", ""), + ("Fix", "Зафиксировать", "Sketcher_ConstrainBlock", ""), + ("Construction", "Вспомогательная", "Sketcher_ToggleConstruction", ""), + ("Trim", "Обрезать", "Sketcher_Trimming", ""), + ] + sketch_hints = { + "Line": "Линия: укажите начальную и следующие точки. Esc — закончить текущую цепочку.", + "Polyline": "Отрезок: укажите начальную и конечную точки. Esc — закончить инструмент.", + "Rectangle": "Прямоугольник: укажите два противоположных угла. Затем задайте размеры D. Esc — закончить инструмент.", + "Circle": "Окружность: укажите центр и радиус. Затем задайте диаметр или радиус. Esc — закончить инструмент.", + "Dimension": "Размер D: выберите геометрию, разместите размер и введите значение. M — вариант размера; Esc — закончить инструмент.", + } + for key, title, native, shortcut in sketch_commands: + add(key, title, lambda n=native, h=sketch_hints.get(key, ""): _sketch_native(n, h), native, title + (" (" + shortcut + ")" if shortcut else ""), shortcut, True, native=native) + dimension_commands = [ + ("DimensionHorizontal", "Горизонтальный размер", "Sketcher_ConstrainDistanceX", + "Горизонтальный размер: выберите линию или две точки и введите расстояние по горизонтали. Esc — закончить инструмент."), + ("DimensionVertical", "Вертикальный размер", "Sketcher_ConstrainDistanceY", + "Вертикальный размер: выберите линию или две точки и введите расстояние по вертикали. Esc — закончить инструмент."), + ("DimensionLength", "Длина", "Sketcher_ConstrainDistance", + "Длина: выберите отрезок или две точки и задайте расстояние. Esc — закончить инструмент."), + ("DimensionRadius", "Радиус", "Sketcher_ConstrainRadius", + "Радиус: выберите окружность или дугу и задайте радиус. Esc — закончить инструмент."), + ("DimensionDiameter", "Диаметр", "Sketcher_ConstrainDiameter", + "Диаметр: выберите окружность или дугу и задайте диаметр. Esc — закончить инструмент."), + ("DimensionAngle", "Угол", "Sketcher_ConstrainAngle", + "Угол: выберите отрезок или две линии и задайте угол. Esc — закончить инструмент."), + ] + for key, title, native, hint in dimension_commands: + add(key, title, lambda n=native, h=hint: _sketch_native(n, h), native, + hint, requires_doc=True, native=native) + add("ProjectGeometry", "Проецировать геометрию", + lambda: __import__("qi_projection").start_projection(), + _command_icon("Sketcher_Projection"), + "Выберите ребро или грань: контуры будут связаны с исходной деталью. Режим «Вспомогательная» исключает их из профиля.", + requires_doc=True, native="Sketcher_Projection", + availability=lambda: bool(_editing() and _editing().isDerivedFrom("Sketcher::SketchObject"))) + add("BreakProjection", "Разорвать связь", + lambda: __import__("qi_projection").break_selected_links(), + _command_icon("Sketcher_External"), + "Преобразовать выбранные проекции в независимую геометрию, сохранив её положение и зависимости.", + requires_doc=True, + availability=lambda: bool(_editing() and _editing().isDerivedFrom("Sketcher::SketchObject"))) + add("SketchOffset", "Смещение геометрии", + lambda: __import__("qi_sketch_ui").show_offset(), + _command_icon("Sketcher_Offset"), + "Создать смещённую геометрию: выбор контура, расстояние и связь с исходными размерами. Отрицательное расстояние меняет сторону.", + requires_doc=True, + availability=lambda: bool(_editing() and _editing().isDerivedFrom("Sketcher::SketchObject"))) + for key, kind, title, native, tooltip in ( + ("SketchMirror", "mirror", "Зеркальное отражение", "Sketcher_Symmetry", + "Выберите геометрию эскиза и задайте ось отражения. Копии связаны с исходными размерами; доступен предпросмотр."), + ("SketchRectangularPattern", "rectangular", "Прямоугольный массив", "Sketcher_RectangularArray", + "Повторить выбранную геометрию в двух перпендикулярных направлениях. Задайте количество, шаг и угол направления."), + ("SketchCircularPattern", "circular", "Круговой массив", "Sketcher_Rotate", + "Повторить выбранную геометрию вокруг заданного центра. Задайте количество и общий угол; копии сохраняют связь."), + ): + add(key, title, lambda k=kind: __import__("qi_sketch_ui").show_pattern(k), + _command_icon(native), tooltip, requires_doc=True, + availability=lambda: bool(_editing() and _editing().isDerivedFrom("Sketcher::SketchObject"))) + add("EditSketchPattern", "Параметры повторений", + lambda: __import__("qi_sketch_ui").edit_pattern(), + _command_icon("Sketcher_Dimension"), + "Изменить ось зеркала, количество, шаг, центр или угол созданного повторения геометрии.", + requires_doc=True, + availability=lambda: bool(_editing() and _editing().isDerivedFrom("Sketcher::SketchObject"))) + features = [ + ("Pocket", "Вырезать", "PartDesign_Pocket", "", "sketch"), + ("Revolve", "Вращение", "PartDesign_Revolution", "", "sketch"), + ("Fillet", "Скругление", "PartDesign_Fillet", "", None), + ("Chamfer", "Фаска", "PartDesign_Chamfer", "", None), + ("Hole", "Отверстие", "PartDesign_Hole", "", "sketch"), + ("Pattern", "Массив", "PartDesign_LinearPattern", "", None), + ("Mirror", "Зеркало", "PartDesign_Mirrored", "", None), + ] + for key, title, native, shortcut, selection_kind in features: + add(key, title, lambda n=native, s=selection_kind: _native(n, "PartDesignWorkbench", selection_kind=s), native, title + (" (" + shortcut + ")" if shortcut else ""), shortcut, True, native=native) + add("Pad", "Выдавить", _extrude, "PartDesign_Pad", "Выдавливание E: объединение / вырезание / пересечение / новое тело, направление и длина", "E", True, availability=lambda: not (_editing() and _editing().isDerivedFrom("Sketcher::SketchObject"))) + add("Box", "Параллелепипед", lambda: _primitive("box"), _command_icon("Part_Box")) + add("Cylinder", "Цилиндр", lambda: _primitive("cylinder"), _command_icon("Part_Cylinder")) + add("Plate", "Пластина", lambda: _primitive("plate"), ":/icons/PartDesign_Pocket.svg", "Параметрическая пластина с двумя отверстиями") + add("Demo", "Кронштейн", _demo, ":/icons/PartDesign_Body.svg", "Создать готовый пример детали") + add("AssemblyDemo", "Пример сборки", lambda: _demo(True), "Geoassembly") + add("Dimensions", "Параметры", lambda: _ui().show_parameters(_document()), "Constraint_Dimension", + "Пользовательские и модельные параметры, единицы и выражения", requires_doc=True, + availability=lambda: not Gui.Control.activeDialog()) + add("Material", "Материал", _material, ":/icons/Arch_Material.svg", requires_doc=True) + add("Properties", "Масса и объём", lambda: _ui().show_physical_properties(_properties_target()), _command_icon("Std_Measure", "document-properties"), requires_doc=True) + add("Export", "Экспорт 3D", _export, _command_icon("Std_Export"), requires_doc=True) + add("BOM", "Спецификация", _bom, _command_icon("Std_ProjectInfo", "document-properties"), requires_doc=True) + assembly_commands = [ + ("AssemblyActivate", "Активировать", "Assembly_ActivateAssembly"), + ("AssemblyInsert", "Компонент", "Assembly_InsertLink"), + ("AssemblyGround", "Закрепить", "Assembly_ToggleGrounded"), + ("AssemblyFixed", "Жёсткое", "Assembly_CreateJointFixed"), + ("AssemblyRevolute", "Вращение", "Assembly_CreateJointRevolute"), + ("AssemblySlider", "Перемещение", "Assembly_CreateJointSlider"), + ("AssemblyCylindrical", "Цилиндрическое", "Assembly_CreateJointCylindrical"), + ("AssemblySolve", "Решить", "Assembly_SolveAssembly"), + ] + for key, title, native in assembly_commands: + add(key, title, lambda n=native: _native(n, "AssemblyWorkbench"), native, requires_doc=True, native=native) + add("Drawing", "Создать чертёж", _drawing, _command_icon("TechDraw_PageDefault"), requires_doc=True) + add("DrawingBOM", "Спецификация на листе", _drawing_bom, + _command_icon("TechDraw_SpreadsheetView", "document-properties"), + "Разместить или обновить спецификацию по источникам выбранного листа. Сводное и структурное представления, предварительный просмотр.", + requires_doc=True, availability=lambda: not Gui.Control.activeDialog()) + add("DrawingExport", "Экспорт листа", _drawing_export, _command_icon("Std_Export"), "Сохранить чертёж в PDF / SVG / DXF", requires_doc=True) + drawing_commands = [("DrawingView", "Вид детали", "TechDraw_View"), ("DrawingDimension", "Размер", "TechDraw_Dimension"), ("DrawingProjection", "Проекции", "TechDraw_ProjectionGroup")] + for key, title, native in drawing_commands: + add(key, title, lambda n=native: _native(n, "TechDrawWorkbench"), native, requires_doc=True, native=native) + add("ViewFit", "Вписать", lambda: _view("fitAll"), "zoom-all", requires_doc=True) + add("Axonometric", "Изометрия", lambda: _view("viewAxonometric"), ":/icons/view-isometric.svg", requires_doc=True) + add("Front", "Спереди", lambda: _view("viewFront"), ":/icons/view-front.svg", requires_doc=True) + add("Top", "Сверху", lambda: _view("viewTop"), ":/icons/view-top.svg", requires_doc=True) + add("ToggleTree", "Дерево модели", lambda: _ui().toggle_tree(), _command_icon("Std_TreeSelection", "tree")) + add("NativeTools", "Все инструменты", lambda: _ui().show_native_tools(), ":/icons/preferences-general.svg", "Выбрать исходную рабочую среду и все её инструменты") + add("Help", "Начало работы", lambda: _ui().show_welcome(), ":/icons/help-browser.svg") + return list(COMMANDS) diff --git a/src/QInventor/qi_drawing_bom.py b/src/QInventor/qi_drawing_bom.py new file mode 100644 index 0000000..0085d9b --- /dev/null +++ b/src/QInventor/qi_drawing_bom.py @@ -0,0 +1,543 @@ +"""Associative native spreadsheet BOM on an existing TechDraw page. + +Dependency direction is page -> table view -> sheet -> geometry views -> CAD. +The native Python execute reads one prepared qi_bom snapshot and returns False +so Spreadsheet's own evaluated cells/formulas continue to execute. +""" +import json +import math +import re +import xml.etree.ElementTree as ET +from collections import Counter + +import FreeCAD as App +from PySide import QtCore, QtGui +import qi_bom as bom + + +_MARKER = "QInventor.DrawingBOM.1" +_ROW_HEIGHT = 22 +_TEXT_SIZE = 9.0 +_FONT = "DejaVu Sans" +_WIDTHS = {"flat": [35, 84, 104, 45, 65, 96], + "structured": [35, 84, 104, 45, 45, 65, 96]} +_SCALES = {"flat": 3.2, "structured": 2.9} +_HEADERS = {"flat": ["Поз.", "Обозначение", "Наименование", "Кол.", "Материал", "Масса, кг"], + "structured": ["Поз.", "Обозначение", "Наименование", "Кол.", "Всего", "Материал", "Масса, кг"]} +_OBSERVER = None +_FONT_APPLICATION = None +_FONT_METRICS = None + + +def _prepare_font_metrics(): + """Prepare the native renderer's glyph metrics outside execute callbacks.""" + global _FONT_APPLICATION, _FONT_METRICS + if _FONT_METRICS is not None: + return + application = QtCore.QCoreApplication.instance() + if application is None: + # Console FreeCAD has no Qt application. This application owns no + # windows; retaining it keeps the native font engine alive on FCStd + # restore and later source recomputes. App.GuiUp remains False. + _FONT_APPLICATION = QtGui.QGuiApplication(["QInventor drawing font metrics"]) + application = _FONT_APPLICATION + if not isinstance(application, QtGui.QGuiApplication): + raise ValueError("Нативный движок шрифтов Qt недоступен для спецификации") + metrics = [] + for bold in (False, True): + font = QtGui.QFont(_FONT) + font.setPixelSize(int(_TEXT_SIZE)) + font.setBold(bold) + metrics.append(QtGui.QFontMetricsF(font)) + _FONT_METRICS = metrics + + +def _page(page): + page = bom._object(page) + if page.TypeId != "TechDraw::DrawPage": + raise ValueError("Выберите существующий лист TechDraw для спецификации") + if page.Template is None: + raise ValueError("У листа нет шаблона A4") + if abs(float(page.Template.Width) - 297) > 1e-6 or abs(float(page.Template.Height) - 210) > 1e-6: + raise ValueError("Спецификация первого этапа поддерживает горизонтальный лист A4") + return page + + +def _mode(mode): + if mode not in _HEADERS: + raise ValueError("Представление спецификации должно быть flat или structured") + return mode + + +def _geometry_views(page): + views = [view for view in page.Views if view.isDerivedFrom("TechDraw::DrawViewPart")] + if not views: + raise ValueError("На листе нет проекций с источниками геометрии") + return views + + +def _sources(views, expected=None): + if not views: + raise ValueError("Проекции спецификации удалены или недоступны") + sources = None + first_keys = None + for view in views: + bom._object(view) + current = [bom._object(obj) for obj in view.Source] + keys = [bom._key(obj) for obj in current] + if not keys or len(set(keys)) != len(keys): + raise ValueError("Проекция содержит пустую область или повторные источники") + if sources is None: + sources, first_keys = current, set(keys) + elif set(keys) != first_keys: + raise ValueError("Проекции листа должны показывать одну и ту же область спецификации") + if expected is not None and {bom._key(obj) for obj in expected} != first_keys: + raise ValueError("Источники проекций изменились. Явно обновите спецификацию листа") + return sources + + +def _source_signature(sources, owner): + return json.dumps(sorted(("@" if obj.Document is owner else (obj.Document.FileName or obj.Document.Name), obj.Name) + for obj in sources), ensure_ascii=False) + + +def _literal(value): + value = str(value) + if any(ord(character) < 32 for character in value): + raise ValueError("Текст спецификации содержит перенос строки или управляющий символ") + try: + value.encode("utf-8") + except UnicodeError as exc: + raise ValueError("Текст спецификации содержит недопустимый символ") from exc + return "'" + value + + +def _mass(value): + value = float(value) + if not math.isfinite(value) or value < 0: + raise ValueError("Масса строки спецификации должна быть конечной и неотрицательной") + return format(value, ".8g").replace(".", ",") + + +def _cells(rows, summary, mode): + values = {} + for column, header in enumerate(_HEADERS[mode]): + values[chr(65 + column) + "1"] = _literal(header) + for index, row in enumerate(rows, 2): + items = [row["position"], row["part_number"], row["name"], row["quantity"]] + if mode == "structured": + items.append(row["quantity_total"]) + material = "Смеш." if row["is_assembly"] and len(row["materials"]) > 1 else row["material"] + items += [material, _mass(row["mass_kg"])] + for column, value in enumerate(items): + # D quantities remain native numbers. E switches between material + # and structured totals, so its printed totals retain the string + # type: native Spreadsheet Undo cannot restore String -> Integer + # cell-property changes safely. Raw quantity_total stays an int. + # Mass literals also retain precision beyond the default decimals. + numeric = isinstance(value, int) and not (mode == "structured" and column == 4) + values[chr(65 + column) + str(index)] = str(value) if numeric else _literal(value) + last = len(rows) + 2 + values["C" + str(last)] = _literal("Итого") + values[chr(64 + len(_HEADERS[mode])) + str(last)] = _literal(_mass(summary["mass_kg"])) + return values, last + + +def _layout(rows, cells, last, mode): + widths = _WIDTHS[mode] + scale = _SCALES[mode] + # Native QGIViewSymbol uses 0.1 mm per SVG coordinate. These bounds are + # conservative; the real GUI/export suite additionally measures its render. + width = sum(widths) * .1 * scale + height = last * _ROW_HEIGHT * .1 * scale + if width > 138 or height > 48: + raise ValueError("Спецификация не помещается в область A4. Используйте более короткую область или другой лист") + if _FONT_METRICS is None: + raise ValueError("Нативные метрики шрифта не подготовлены. Обновите спецификацию") + for address, content in cells.items(): + column = ord(address[0]) - 65 + text = content[1:] if content.startswith("'") else content + metrics = _FONT_METRICS[1 if address[1:] == "1" else 0] + bounds = metrics.tightBoundingRect(text) + advance = max(metrics.horizontalAdvance(text), bounds.right() - min(0.0, bounds.left())) + if advance + 8 > widths[column]: + raise ValueError("Текст «%s» не помещается в столбец A4. Сократите текст или выберите другую область" % text) + return {"x_mm": 218.0, "y_mm": 86.0, "width_mm": width, "height_mm": height, + "column_widths_native": widths, "row_height_native": _ROW_HEIGHT, "scale": scale, + "text_size_native": _TEXT_SIZE, "font": _FONT, "last_row": last, + "cell_start": "A1", "cell_end": chr(64 + len(widths)) + str(last), + "reserved_cells": "A:G", "minimum_text_height_mm": _TEXT_SIZE * .1 * scale, + "printed_mass_significant_digits": 8, "mixed_assembly_material_abbreviation": "Смеш."} + + +def _build(views, mode, expected=None, recompute=False): + mode = _mode(mode) + sources = _sources(views, expected) + native = bom.snapshot_from_roots(sources, recompute=recompute) + rows = bom.rows_from_snapshot(native, mode) + if not rows: + raise ValueError("В источниках проекций нет твёрдых деталей для спецификации") + cells, last = _cells(rows, native["summary"], mode) + layout = _layout(rows, cells, last, mode) + return {"mode": mode, "rows": rows, "summary": native["summary"], "cells": cells, "layout": layout, + "sources": sources, "source_names": [obj.Name for obj in sources], + "source_labels": [obj.Label for obj in sources], "geometry_views": list(views), + "geometry_view_names": [view.Name for view in views], "sheet_name": None, "view_name": None} + + +def _data(plan): + # Document names change on FCStd open. Persist only computed display data + # and physical values; native PropertyLinks carry source identities. + names = ("position", "parent_position", "level", "part_number", "name", "quantity", "quantity_total", + "material", "density_kg_m3", "unit_mass_kg", "mass_kg", "is_assembly") + payload = {"mode": plan["mode"], "rows": [{key: row[key] for key in names} for row in plan["rows"]], + "summary": plan["summary"], "cells": plan["cells"], "layout": plan["layout"]} + def canonical(value): + if isinstance(value, float): + return float(format(value, ".12g")) + if isinstance(value, dict): + return {key: canonical(item) for key, item in value.items()} + if isinstance(value, (tuple, list)): + return [canonical(item) for item in value] + return value + return json.dumps(canonical(payload), ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + +def _write(sheet, plan): + old = json.loads(sheet.QIBOMCells or "{}") + if any(not re.fullmatch(r"[A-G][1-9][0-9]*", address) for address in old): + raise ValueError("Сохранённый диапазон спецификации повреждён") + for address in old.keys() - plan["cells"].keys(): + if sheet.getContents(address): + sheet.clear(address) + for address, content in plan["cells"].items(): + if sheet.getContents(address) != content: + sheet.set(address, content) + encoded_cells = json.dumps(plan["cells"], ensure_ascii=False, sort_keys=True) + if sheet.QIBOMCells != encoded_cells: + sheet.QIBOMCells = encoded_cells + encoded = _data(plan) + if sheet.QIBOMData != encoded: + sheet.QIBOMData = encoded + if sheet.QIBOMError: + sheet.QIBOMError = "" + for view in sheet.InList: + if view.TypeId == "TechDraw::DrawViewSpreadsheet" and view.Source is sheet: + if view.CellEnd != plan["layout"]["cell_end"]: + view.CellEnd = plan["layout"]["cell_end"] + # Reverse native membership is read-only; no sheet->page backlink + # is added to the dependency graph. Preserve all other title text. + for page in view.InList: + if page.TypeId == "TechDraw::DrawPage" and page.Template is not None: + texts = dict(page.Template.EditableTexts) + material = plan["summary"]["materials"] + value = material[0] if len(material) == 1 else "См. спецификацию" + if "Material" in texts and texts["Material"] != value: + texts["Material"] = value + page.Template.EditableTexts = texts + + +class DrawingBOMProxy: + def __init__(self, sheet): + _prepare_font_metrics() + sheet.Proxy = self + + def execute(self, sheet): + try: + plan = _build(sheet.QIBOMGeometryViews, str(sheet.QIBOMMode), sheet.QIBOMSources, False) + if sheet.QIBOMScope != _source_signature(plan["sources"], sheet.Document): + raise ValueError("Состав источников изменился. Явно обновите спецификацию листа") + _write(sheet, plan) + except Exception as exc: + message = str(exc) if isinstance(exc, ValueError) else "Не удалось обновить спецификацию листа" + if sheet.QIBOMError != message: + sheet.QIBOMError = message + raise ValueError(message) from exc + return False + + def onDocumentRestored(self, sheet): + _prepare_font_metrics() + _install_observer() + + def dumps(self): + return _MARKER + + def loads(self, state): + pass + + +def table_for_page(page): + page = _page(page) + tables = [view for view in page.Views if view.TypeId == "TechDraw::DrawViewSpreadsheet" + and (getattr(view, "QIBOMType", "") == _MARKER or + (view.Source is not None and view.Source.TypeId == "Spreadsheet::SheetPython" + and getattr(view.Source, "QIBOMType", "") == _MARKER))] + if len(tables) > 1: + raise ValueError("На листе обнаружено несколько спецификаций QInventor") + if tables and (tables[0].Source is None or tables[0].Source.TypeId != "Spreadsheet::SheetPython" + or getattr(tables[0].Source, "QIBOMType", "") != _MARKER): + raise ValueError("Источник таблицы спецификации удалён или заменён") + return tables[0] if tables else None + + +def table_snapshot(page, mode=None): + page = _page(page) + table = table_for_page(page) + if mode is not None or table is None: + return _preflight(page, mode or "flat") + return validate_table(page, recompute=False) + + +def preview_bom_table(page, mode="flat"): + return table_snapshot(page, mode) + + +def validate_table(page, recompute=False): + page = _page(page) + view = table_for_page(page) + if view is None: + return None + sheet = view.Source + views = _geometry_views(page) + if set(views) != set(sheet.QIBOMGeometryViews): + raise ValueError("Состав проекций изменился. Явно обновите спецификацию листа") + if recompute: + bom._sync(_sources(views)) + page.Document.recompute() + if not isinstance(sheet.Proxy, DrawingBOMProxy): + raise ValueError("Не восстановлен обработчик спецификации. Откройте проект в QInventor") + plan = _build(views, str(sheet.QIBOMMode), sheet.QIBOMSources, False) + if sheet.QIBOMScope != _source_signature(plan["sources"], sheet.Document): + raise ValueError("Состав источников изменился. Явно обновите спецификацию листа") + if sheet.QIBOMError or any(flag in sheet.State for flag in ("Invalid", "Error")): + raise ValueError(sheet.QIBOMError or "Спецификация содержит ошибку пересчёта") + if sheet.QIBOMData != _data(plan) or any(sheet.getContents(key) != value for key, value in plan["cells"].items()): + raise ValueError("Спецификация устарела. Обновите лист перед экспортом") + if view.CellStart != "A1" or view.CellEnd != plan["layout"]["cell_end"]: + raise ValueError("Диапазон таблицы изменён. Обновите спецификацию") + _validate_format(view, plan) + _validate_symbol(view, plan) + import qi_model + validator = getattr(qi_model, "validate_drawing_bom_layout", None) + if validator is not None: + actual_layout = dict(plan["layout"], x_mm=float(view.X), y_mm=float(view.Y), + width_mm=plan["native_symbol_bounds_mm"]["width"], + height_mm=plan["native_symbol_bounds_mm"]["height"]) + validator(page, actual_layout) + plan.update(sheet_name=sheet.Name, view_name=view.Name) + return plan + + +def _validate_format(view, plan): + layout = plan["layout"] + sheet = view.Source + if App.GuiUp and ("LegacyScaling" not in view.ViewObject.PropertiesList or view.ViewObject.LegacyScaling): + raise ValueError("Таблица использует старый масштаб FreeCAD. Обновите спецификацию") + if (view.Font != layout["font"] or abs(float(view.TextSize) - layout["text_size_native"]) > 1e-9 + or str(view.ScaleType) != "Custom" or abs(float(view.Scale) - layout["scale"]) > 1e-9 + or abs(float(view.Rotation)) > 1e-9 + or any(sheet.getColumnWidth(chr(65 + index)) != width + for index, width in enumerate(layout["column_widths_native"])) + or any(sheet.getRowHeight(str(row)) != layout["row_height_native"] + for row in range(1, layout["last_row"] + 1))): + raise ValueError("Формат таблицы изменён. Обновите спецификацию перед экспортом") + + +def _validate_symbol(view, plan): + try: + root = ET.fromstring(view.Symbol) + except ET.ParseError as exc: + raise ValueError("Нативное изображение спецификации ещё не готово") from exc + printed = Counter("".join(node.itertext()).strip() for node in root.iter() + if node.tag.rsplit("}", 1)[-1] == "text") + expected = Counter(content[1:] if content.startswith("'") else content for content in plan["cells"].values()) + if any(printed[value] < count for value, count in expected.items()): + raise ValueError("Нативное изображение спецификации содержит устаревшие или неполные ячейки") + rects = [node for node in root.iter() if node.tag.rsplit("}", 1)[-1] == "rect"] + if not rects: + raise ValueError("Нативное изображение спецификации не содержит таблицы") + try: + left = min(float(node.attrib.get("x", 0)) for node in rects) + top = min(float(node.attrib.get("y", 0)) for node in rects) + right = max(float(node.attrib.get("x", 0)) + float(node.attrib["width"]) for node in rects) + bottom = max(float(node.attrib.get("y", 0)) + float(node.attrib["height"]) for node in rects) + except (KeyError, ValueError) as exc: + raise ValueError("Нативные размеры таблицы недоступны") from exc + width, height = (right - left) * .1 * view.Scale, (bottom - top) * .1 * view.Scale + if not all(math.isfinite(value) for value in (width, height)) or width > 138.1 or height > 48.1: + raise ValueError("Нативная таблица выходит за область спецификации A4") + plan["native_symbol_bounds_mm"] = {"width": width, "height": height} + + +def _transaction(doc): + if App.getActiveTransaction() is not None: + raise ValueError("Завершите текущую операцию перед изменением спецификации") + doc.openTransaction("Спецификация на листе") + + +def _preflight(page, mode): + _prepare_font_metrics() + views = _geometry_views(page) + # Reject a known failed native source before recomputing any dependent + # table or HLR cache. Validation must retain the user's last-good sheet. + bom._validate_ready(_sources(views)) + plan = _build(views, mode, recompute=True) + import qi_model + planner = getattr(qi_model, "plan_drawing_bom_layout", None) + if planner is not None: + qi_model.prepare_drawing(page, validate_bom=False) + plan["page_layout"] = planner(page, plan["layout"]) + return plan + + +def _apply_page_layout(page, view, plan): + proposed = plan.get("page_layout") + if proposed is None: + return + if float(page.Scale) != proposed["page_scale"]: + old_scale = float(page.Scale) + page.Scale = proposed["page_scale"] + texts = dict(page.Template.EditableTexts) + old_text = "%.6g:1" % old_scale if old_scale >= 1 else "1:%.6g" % (1 / old_scale) + if texts.get("Scale") == old_text: + scale = proposed["page_scale"] + texts["Scale"] = "%.6g:1" % scale if scale >= 1 else "1:%.6g" % (1 / scale) + page.Template.EditableTexts = texts + for name, position in proposed["view_positions"].items(): + projection = page.Document.getObject(name) + if projection is None or projection not in page.Views: + raise ValueError("Проекция компоновки удалена") + if float(projection.X) != position[0]: + projection.X = position[0] + if float(projection.Y) != position[1]: + projection.Y = position[1] + view.X, view.Y = proposed["table_position"] + + +def _format(sheet, plan): + for index, width in enumerate(plan["layout"]["column_widths_native"]): + if sheet.getColumnWidth(chr(65 + index)) != width: + sheet.setColumnWidth(chr(65 + index), width) + for row in range(1, plan["layout"]["last_row"] + 1): + if sheet.getRowHeight(str(row)) != _ROW_HEIGHT: + sheet.setRowHeight(str(row), _ROW_HEIGHT) + sheet.setStyle("A1:" + chr(64 + len(_HEADERS[plan["mode"]])) + "1", "bold", "add") + + +def _configure_view(view, plan): + layout = plan["layout"] + view.CellStart = "A1"; view.CellEnd = layout["cell_end"] + view.Font = _FONT; view.TextSize = _TEXT_SIZE + view.ScaleType = "Custom"; view.Scale = layout["scale"] + view.Rotation = 0 + view.X = layout["x_mm"]; view.Y = layout["y_mm"] + if App.GuiUp: + if "LegacyScaling" not in view.ViewObject.PropertiesList: + raise ValueError("Нативный вид таблицы не поддерживает требуемый масштаб") + view.ViewObject.LegacyScaling = False + if "Caption" in view.PropertiesList: + view.Caption = "" + + +def create_bom_table(page, mode="flat"): + page = _page(page) + if table_for_page(page) is not None: + raise ValueError("На листе уже есть спецификация. Используйте обновление") + plan = _preflight(page, mode) + doc = page.Document + _transaction(doc) + try: + import Spreadsheet # noqa: F401; registers real native types + import TechDraw # noqa: F401 + sheet = doc.addObject("Spreadsheet::SheetPython", "DrawingBOM") + sheet.Label = "Данные спецификации" + for kind, name in (("String", "QIBOMType"), ("String", "QIBOMData"), ("String", "QIBOMCells"), ("String", "QIBOMScope"), + ("String", "QIBOMError"), ("LinkListGlobal", "QIBOMGeometryViews"), ("LinkListGlobal", "QIBOMSources"), + ("Enumeration", "QIBOMMode")): + sheet.addProperty("App::Property" + kind, name, "Спецификация") + sheet.setEditorMode(name, 2) + sheet.QIBOMType = _MARKER + sheet.QIBOMMode = ["flat", "structured"]; sheet.QIBOMMode = mode + sheet.QIBOMGeometryViews = plan["geometry_views"]; sheet.QIBOMSources = plan["sources"] + sheet.QIBOMScope = _source_signature(plan["sources"], doc) + _format(sheet, plan) + DrawingBOMProxy(sheet) + view = doc.addObject("TechDraw::DrawViewSpreadsheet", "BOMView") + view.Label = "Спецификация" + view.addProperty("App::PropertyString", "QIBOMType", "Спецификация") + view.QIBOMType = _MARKER + view.setEditorMode("QIBOMType", 2) + view.Source = sheet + page.addView(view) + _configure_view(view, plan) + _apply_page_layout(page, view, plan) + _write(sheet, plan) + doc.recompute() + import qi_model + if hasattr(qi_model, "plan_drawing_bom_layout"): + qi_model.prepare_drawing(page, validate_bom=False) + validate_table(page) + doc.commitTransaction() + _install_observer() + return view + except Exception: + doc.abortTransaction() + doc.recompute() + raise + + +def update_bom_table(page, mode=None): + page = _page(page) + view = table_for_page(page) + if view is None: + return create_bom_table(page, mode or "flat") + sheet = view.Source + plan = _preflight(page, mode or str(sheet.QIBOMMode)) + try: + current = validate_table(page) + if current["mode"] == plan["mode"] and _data(current) == _data(plan): + return view + except ValueError: + pass # explicit update may rebind a repaired/changed projection scope + _transaction(page.Document) + try: + sheet.QIBOMGeometryViews = plan["geometry_views"]; sheet.QIBOMSources = plan["sources"] + sheet.QIBOMScope = _source_signature(plan["sources"], page.Document) + sheet.QIBOMMode = plan["mode"] + _format(sheet, plan); _configure_view(view, plan); _apply_page_layout(page, view, plan); _write(sheet, plan) + page.Document.recompute() + import qi_model + if hasattr(qi_model, "plan_drawing_bom_layout"): + qi_model.prepare_drawing(page, validate_bom=False) + validate_table(page) + page.Document.commitTransaction() + return view + except Exception: + page.Document.abortTransaction() + page.Document.recompute() + raise + + +class _MetadataObserver: + def slotChangedObject(self, obj, prop): + if prop not in ("Label", "QIPartNumber", "QIMaterial", "QIDensity", "Group"): + return + for doc in App.listDocuments().values(): + for sheet in doc.Objects: + if sheet.TypeId != "Spreadsheet::SheetPython" or getattr(sheet, "QIBOMType", "") != _MARKER: + continue + try: + if obj in bom._dependencies(sheet.QIBOMSources): + sheet.touch() + except Exception: + pass # execute/validate reports the real broken dependency + + +def _install_observer(): + global _OBSERVER + if _OBSERVER is None: + _OBSERVER = _MetadataObserver() + App.addDocumentObserver(_OBSERVER) + + +_install_observer() diff --git a/src/QInventor/qi_drawing_bom_export.py b/src/QInventor/qi_drawing_bom_export.py new file mode 100644 index 0000000..4c47fe1 --- /dev/null +++ b/src/QInventor/qi_drawing_bom_export.py @@ -0,0 +1,214 @@ +"""Complete the native DXF page with its genuine spreadsheet symbol. + +FreeCAD 1.1.4 writes the projected geometry but omits DrawViewSpreadsheet. +Keep that native output and add editable LINE/TEXT entities from the same +validated Symbol that TechDraw paints. Coordinates remain paper millimetres. +""" +import math +import re +import xml.etree.ElementTree as ET +from pathlib import Path + + +def _number(value): + value = float(value) + if not math.isfinite(value): + raise ValueError("Координаты спецификации недопустимы") + return format(value, ".12g") + + +def _dxf_text(value): + # The completed file uses the DXF 2007 UTF-8 contract. Native FreeCAD's + # modern importer leaves CIF Unicode escapes literally in editable Text, + # so write the genuine Unicode text. Reject control sequences that another + # CAD could interpret differently from the spreadsheet's literal content. + value = str(value) + if any(ord(character) < 32 for character in value): + raise ValueError("Текст спецификации содержит недопустимый управляющий символ") + if "%%" in value or re.search(r"\\[Uu]\+[0-9A-Fa-f]{4}", value): + raise ValueError("Текст спецификации содержит управляющую запись DXF. Используйте обычные символы") + value.encode("utf-8") + return value + + +def _section(pairs, name): + for index in range(len(pairs) - 1): + if pairs[index] == (0, "SECTION") and pairs[index + 1] == (2, name): + end = next((cursor for cursor in range(index + 2, len(pairs)) + if pairs[cursor] == (0, "ENDSEC")), None) + if end is not None: + return index + 2, end + raise ValueError("Нативный DXF не содержит раздел " + name) + + +def _table_range(pairs, table_name): + start, end = _section(pairs, "TABLES") + for index in range(start, end - 1): + if pairs[index] != (0, "TABLE") or pairs[index + 1] != (2, table_name): + continue + finish = next(cursor for cursor in range(index + 2, end) if pairs[cursor] == (0, "ENDTAB")) + first_record = next((cursor for cursor in range(index + 2, finish) if pairs[cursor][0] == 0), finish) + return index + 2, first_record, finish + raise ValueError("Нативный DXF не содержит таблицу " + table_name) + + +def _table_handle(pairs, table_name): + header, first_record, _finish = _table_range(pairs, table_name) + value = next((value for code, value in pairs[header:first_record] if code == 5), "") + if not re.fullmatch(r"[0-9A-Fa-f]+", value): + raise ValueError("Нативный DXF не содержит идентификатор таблицы " + table_name) + return value + + +def _model_space_handle(pairs): + _header, first_record, finish = _table_range(pairs, "BLOCK_RECORD") + for index in range(first_record, finish): + if pairs[index] != (0, "BLOCK_RECORD"): + continue + end = next((cursor for cursor in range(index + 1, finish) if pairs[cursor][0] == 0), finish) + record = pairs[index + 1:end] + if not any(code == 2 and value.upper() == "*MODEL_SPACE" for code, value in record): + continue + value = next((value for code, value in record if code == 5), "") + if re.fullmatch(r"[0-9A-Fa-f]+", value): + return value + raise ValueError("Нативный DXF не содержит идентификатор пространства модели") + + +def _add_table_record(pairs, table_name, name, record): + header, first_record, finish = _table_range(pairs, table_name) + if (2, name) in pairs[first_record:finish]: + return + count = next((cursor for cursor in range(header, first_record) if pairs[cursor][0] == 70), None) + if count is None: + raise ValueError("Нативный DXF не содержит размер таблицы " + table_name) + pairs[count] = (70, str(int(pairs[count][1]) + 1)) + pairs[finish:finish] = record + + +def _symbol_geometry(view, layout): + if abs(float(getattr(view, "Rotation", 0))) > 1e-9: + raise ValueError("Поворот спецификации изменён. Обновите таблицу перед экспортом DXF") + if bool(getattr(getattr(view, "ViewObject", None), "LegacyScaling", False)): + raise ValueError("Режим масштаба спецификации изменён. Обновите таблицу перед экспортом DXF") + root = ET.fromstring(str(view.Symbol)) + if any(node.attrib.get("transform") for node in root.iter()): + raise ValueError("Спецификация содержит неподдерживаемое преобразование SVG") + rectangles = [node for node in root.iter() if node.tag.rsplit("}", 1)[-1] == "rect"] + texts = [node for node in root.iter() if node.tag.rsplit("}", 1)[-1] == "text"] + if not rectangles or not texts: + raise ValueError("Нативная спецификация не содержит таблицу для DXF") + factor = float(view.Scale) * 0.1 + if not math.isfinite(factor) or factor <= 0: + raise ValueError("Масштаб спецификации недопустим") + left = min(float(node.attrib.get("x", 0)) for node in rectangles) + top = min(float(node.attrib.get("y", 0)) for node in rectangles) + right = max(float(node.attrib.get("x", 0)) + float(node.attrib["width"]) for node in rectangles) + bottom = max(float(node.attrib.get("y", 0)) + float(node.attrib["height"]) for node in rectangles) + if abs((right - left) * factor - float(layout["width_mm"])) > 0.05 or \ + abs((bottom - top) * factor - float(layout["height_mm"])) > 0.05: + raise ValueError("Размеры нативной таблицы изменились перед экспортом DXF") + center_x, center_y = (left + right) / 2, (top + bottom) / 2 + + def point(x, y): + return float(view.X) + (x - center_x) * factor, float(view.Y) - (y - center_y) * factor + + lines = [] + for node in rectangles: + x, y = float(node.attrib.get("x", 0)), float(node.attrib.get("y", 0)) + width, height = float(node.attrib["width"]), float(node.attrib["height"]) + corners = [point(x, y), point(x + width, y), point(x + width, y + height), point(x, y + height)] + lines.extend((corners[index], corners[(index + 1) % 4]) for index in range(4)) + strings = [] + for node in texts: + value = "".join(node.itertext()) + if not value: + continue + style = node.attrib.get("style", "") + anchor = node.attrib.get("text-anchor", "") + match = re.search(r"(?:^|;)\s*text-anchor\s*:\s*(\w+)", style) + anchor = match.group(1) if match else anchor + height = float(node.attrib["font-size"]) * factor + if not math.isfinite(height) or height <= 0: + raise ValueError("Высота текста спецификации недопустима") + strings.append({"point": point(float(node.attrib["x"]), float(node.attrib["y"])), + "height": height, "value": _dxf_text(value), + "alignment": {"middle": 1, "end": 2}.get(anchor, 0), + "bold": bool(re.search(r"font-weight\s*:\s*(?:bold|[6-9]00)", style))}) + return lines, strings + + +def complete_dxf(page, path): + """Add the verified table to an owned temporary native DXF, never the destination.""" + import qi_drawing_bom + view = qi_drawing_bom.table_for_page(page) + if view is None: + return None + snapshot = qi_drawing_bom.validate_table(page, recompute=False) + lines, texts = _symbol_geometry(view, snapshot["layout"]) + path = Path(path) + raw = path.read_text(encoding="utf-8").splitlines() + if len(raw) % 2: + raise ValueError("Нативный DXF содержит неполные пары данных") + pairs = [(int(raw[index].strip()), raw[index + 1].strip() if int(raw[index].strip()) != 1 + else raw[index + 1]) for index in range(0, len(raw), 2)] + start, end = _section(pairs, "HEADER") + version = next((index for index in range(start, end - 1) + if pairs[index] == (9, "$ACADVER") and pairs[index + 1][0] == 1), None) + if version is None: + raise ValueError("Нативный DXF не содержит версию формата для Unicode") + # UTF-8 is defined by AC1021 and later, irrespective of the legacy + # $DWGCODEPAGE value. Keep every native projected geometry entity intact. + if pairs[version + 1][1] in ("AC1012", "AC1014", "AC1015", "AC1018"): + pairs[version + 1] = (1, "AC1021") + elif not re.fullmatch(r"AC\d{4}", pairs[version + 1][1]) or int(pairs[version + 1][1][2:]) < 1021: + raise ValueError("Нативная версия DXF не поддерживается для Unicode-спецификации") + _section(pairs, "ENTITIES") + # DIMSTYLE also uses code 5 for its second arrow-block name, which may be + # empty or non-hexadecimal. Only actual hexadecimal handle values count. + handle = max((int(value, 16) for code, value in pairs + if code in (5, 105) and re.fullmatch(r"[0-9A-Fa-f]+", value)), default=0xFFFF) + + def allocate_handle(): + nonlocal handle + handle += 1 + return format(handle, "X") + + layer_owner = _table_handle(pairs, "LAYER") + style_owner = _table_handle(pairs, "STYLE") + model_owner = _model_space_handle(pairs) + _add_table_record(pairs, "LAYER", "QI_BOM", + [(0, "LAYER"), (5, allocate_handle()), (330, layer_owner), + (100, "AcDbSymbolTableRecord"), (100, "AcDbLayerTableRecord"), + (2, "QI_BOM"), (70, "0"), (62, "7"), (6, "CONTINUOUS")]) + for style, font in (("QI_BOM", "arial.ttf"), ("QI_BOM_BOLD", "arialbd.ttf")): + _add_table_record(pairs, "STYLE", style, + [(0, "STYLE"), (5, allocate_handle()), (330, style_owner), + (100, "AcDbSymbolTableRecord"), (100, "AcDbTextStyleTableRecord"), + (2, style), (70, "0"), (40, "0"), (41, "1"), + (50, "0"), (71, "0"), (42, "2.5"), (3, font), (4, "")]) + + def common(kind): + return [(0, kind), (5, allocate_handle()), (330, model_owner), + (100, "AcDbEntity"), (8, "QI_BOM")] + + entities = [] + for first, last in lines: + entities.extend(common("LINE") + [(100, "AcDbLine"), + (10, _number(first[0])), (20, _number(first[1])), (30, "0"), + (11, _number(last[0])), (21, _number(last[1])), (31, "0")]) + for text in texts: + x, y = text["point"] + entities.extend(common("TEXT") + [(100, "AcDbText"), (10, _number(x)), (20, _number(y)), (30, "0"), + (40, _number(text["height"])), (1, text["value"]), (50, "0"), (41, "1"), + (7, "QI_BOM_BOLD" if text["bold"] else "QI_BOM"), (71, "0"), + (72, str(text["alignment"])), (11, _number(x)), (21, _number(y)), (31, "0"), + (100, "AcDbText"), (73, "0")]) + _, end = _section(pairs, "ENTITIES") + pairs[end:end] = entities + for index in range(len(pairs) - 1): + if pairs[index] == (9, "$HANDSEED") and pairs[index + 1][0] == 5: + pairs[index + 1] = (5, format(handle + 1, "X")) + break + path.write_text("".join("%3d\n%s\n" % pair for pair in pairs), encoding="utf-8", newline="\n") + return {"lines": len(lines), "texts": len(texts), "layer": "QI_BOM"} diff --git a/src/QInventor/qi_drawing_bom_ui.py b/src/QInventor/qi_drawing_bom_ui.py new file mode 100644 index 0000000..78b529e --- /dev/null +++ b/src/QInventor/qi_drawing_bom_ui.py @@ -0,0 +1,253 @@ +"""Russian preview and explicit insertion of a native drawing BOM table.""" +from __future__ import annotations + +import FreeCAD as App +import FreeCADGui as Gui +from PySide import QtCore, QtGui +try: + from PySide import QtWidgets +except ImportError: + QtWidgets = QtGui + + +_dialogs = {} + + +def _service(): + import qi_drawing_bom + return qi_drawing_bom + + +def _number(value): + return format(float(value), ".12g").replace(".", ",") + + +def _cell_text(value): + value = str(value) + return value[1:] if value.startswith("'") else value + + +class DrawingBOMDialog(QtWidgets.QDialog): + def __init__(self, page, parent=None): + super().__init__(parent or Gui.getMainWindow()) + self.page = page + self.page_name = page.Name + self.document_name = page.Document.Name + self.document = page.Document + self.snapshot = None + self.rows = [] + self.setObjectName("QI_DrawingBOM") + self.setWindowTitle("Спецификация на листе — " + page.Label) + self.setModal(False) + self.resize(1020, 570) + self.setMinimumSize(820, 450) + layout = QtWidgets.QVBoxLayout(self) + self.page_label = QtWidgets.QLabel("Лист: " + page.Label) + self.page_label.setObjectName("QI_DrawingBOMPage") + self.page_label.setTextFormat(QtCore.Qt.PlainText) + layout.addWidget(self.page_label) + instructions = QtWidgets.QLabel("Проверьте состав перед размещением. Таблица связана с источниками видов этого листа и обновляется при перестроении модели.") + instructions.setWordWrap(True) + layout.addWidget(instructions) + layout.addWidget(QtWidgets.QLabel("Источники листа:")) + self.scope = QtWidgets.QPlainTextEdit() + self.scope.setObjectName("QI_DrawingBOMScope") + self.scope.setReadOnly(True) + self.scope.setMaximumHeight(66) + layout.addWidget(self.scope) + controls = QtWidgets.QHBoxLayout() + controls.addWidget(QtWidgets.QLabel("Представление:")) + self.mode_selector = QtWidgets.QComboBox() + self.mode_selector.setObjectName("QI_DrawingBOMMode") + self.mode_selector.addItem("Сводная — только детали", "flat") + self.mode_selector.addItem("Структурная — состав сборок", "structured") + controls.addWidget(self.mode_selector, 1) + self.refresh_button = QtWidgets.QPushButton("Обновить предпросмотр") + self.refresh_button.setObjectName("QI_DrawingBOMRefresh") + controls.addWidget(self.refresh_button) + layout.addLayout(controls) + self.table = QtWidgets.QTableWidget(0, 6) + self.table.setObjectName("QI_DrawingBOMPreview") + self.table.setAlternatingRowColors(True) + self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.table.verticalHeader().hide() + self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) + layout.addWidget(self.table, 1) + self.summary = QtWidgets.QLabel("") + self.summary.setObjectName("QI_DrawingBOMSummary") + self.summary.setTextFormat(QtCore.Qt.PlainText) + self.summary.setWordWrap(True) + layout.addWidget(self.summary) + self.status = QtWidgets.QLabel("") + self.status.setObjectName("QI_DrawingBOMStatus") + self.status.setTextFormat(QtCore.Qt.PlainText) + self.status.setWordWrap(True) + self.status.setMinimumHeight(36) + layout.addWidget(self.status) + self.buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close) + self.apply_button = self.buttons.addButton("Разместить на листе", QtWidgets.QDialogButtonBox.ActionRole) + self.apply_button.setObjectName("QI_DrawingBOMApply") + self.apply_button.setEnabled(False) + self.buttons.button(QtWidgets.QDialogButtonBox.Close).setText("Закрыть") + self.buttons.rejected.connect(self.close) + layout.addWidget(self.buttons) + self.mode_selector.currentIndexChanged.connect(self.refresh) + self.refresh_button.clicked.connect(self.refresh) + self.apply_button.clicked.connect(self.apply) + self._initial_preview() + + def _page(self): + doc = App.listDocuments().get(self.document_name) + if doc != self.document or doc.getObject(self.page_name) != self.page: + raise ValueError("Лист удалён или документ уже закрыт. Откройте существующий лист чертежа.") + if Gui.Control.activeDialog(): + raise ValueError("Завершите редактирование детали или сборки перед изменением спецификации на листе.") + return self.page + + def _source_text(self, source): + name = str(source) + doc = self.document + if "#" in name: + document_name, object_name = name.split("#", 1) + doc = App.listDocuments().get(document_name) + else: + object_name = name + obj = doc.getObject(object_name) if doc else None + if obj is not None: + return obj.Label + " (" + obj.Document.Name + "." + obj.Name + ")" + return name + + def _render(self, snapshot): + self.snapshot = dict(snapshot) + self.rows = [dict(row) for row in snapshot["rows"]] + self.scope.setPlainText("\n".join(self._source_text(source) for source in snapshot.get("source_names", []))) + structured = snapshot["mode"] == "structured" + columns = [("position", "Поз."), ("part_number", "Обозначение"), ("name", "Наименование"), + ("quantity", "Кол-во")] + if structured: + columns.append(("quantity_total", "Всего")) + columns += [("material", "Материал"), ("mass_kg", "Масса, кг")] + self.table.clear() + self.table.setColumnCount(len(columns)) + cells = snapshot.get("cells", {}) + self.table.setHorizontalHeaderLabels([_cell_text(cells.get(chr(65 + column) + "1", label)) + for column, (_key, label) in enumerate(columns)]) + self.table.setRowCount(len(self.rows)) + self.table.horizontalHeader().setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch) + self.table.horizontalHeaderItem(3).setToolTip("Количество в родительской сборке." if structured + else "Общее количество экземпляров детали.") + for row_index, row in enumerate(self.rows): + for column, (key, _label) in enumerate(columns): + value = row.get(key, "") + fallback = _number(value) if key == "mass_kg" else str(value) + text = _cell_text(cells.get(chr(65 + column) + str(row_index + 2), fallback)) + item = QtWidgets.QTableWidgetItem(text) + item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) + item.setData(QtCore.Qt.UserRole, dict(row)) + item.setToolTip(str(row.get("source_id", ""))) + if key in ("quantity", "quantity_total", "mass_kg"): + item.setTextAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + if row.get("is_assembly"): + font = item.font() + font.setBold(True) + item.setFont(font) + self.table.setItem(row_index, column, item) + summary = snapshot["summary"] + self.summary.setText("Всего деталей: %s. Масса: %s кг." % (summary["components"], _number(summary["mass_kg"]))) + existing = _service().table_for_page(self.page) is not None + self.apply_button.setText("Обновить на листе" if existing else "Разместить на листе") + self.apply_button.setEnabled(bool(self.rows)) + + def _error(self, error, action="Предпросмотр не обновлён."): + from qi_messages import error_text + App.Console.PrintError("QInventor drawing BOM: {}\n".format(error)) + explanation = error_text(error, "Проверьте источники видов, состояние модели и свободное место на листе A4.") + native_failure = "содержит ошибку перестроения:" + if native_failure in explanation: + explanation = explanation.split(native_failure, 1)[0] + ( + "содержит ошибку перестроения. Исправьте исходную операцию и нажмите «Обновить предпросмотр».") + self.status.setText(action + " " + explanation + (" Показан предыдущий расчёт." if self.snapshot else "")) + self.apply_button.setEnabled(False) + + def _initial_preview(self): + try: + page = self._page() + existing = _service().table_for_page(page) + mode = str(existing.Source.QIBOMMode) if existing else "flat" + snapshot = _service().table_snapshot(page, mode=mode) + self.mode_selector.blockSignals(True) + try: + self.mode_selector.setCurrentIndex(self.mode_selector.findData(snapshot["mode"])) + finally: + self.mode_selector.blockSignals(False) + self._render(snapshot) + self.status.setText("Проверьте строки и нажмите «Обновить на листе»." if existing + else "Предпросмотр готов. Нажмите «Разместить на листе».") + except Exception as error: + self._error(error) + + def refresh(self, *_args): + try: + snapshot = _service().table_snapshot(self._page(), mode=str(self.mode_selector.currentData())) + self._render(snapshot) + self.status.setText("Предпросмотр обновлён. Изменения на листе ещё не применены.") + return True + except Exception as error: + self._error(error) + return False + + def apply(self): + try: + page = self._page() + mode = str(self.mode_selector.currentData()) + previous_sources = tuple(self.snapshot.get("source_names", [])) if self.snapshot else () + snapshot = _service().table_snapshot(page, mode=mode) + self._render(snapshot) + if previous_sources and previous_sources != tuple(snapshot.get("source_names", [])): + self.status.setText("Источники листа изменились. Предпросмотр обновлён; проверьте состав и повторите применение.") + return False + if _service().table_for_page(page) is None: + _service().create_bom_table(page, mode=mode) + action = "Спецификация размещена на листе." + else: + _service().update_bom_table(page, mode=mode) + action = "Спецификация на листе обновлена." + self._render(_service().table_snapshot(page)) + self.status.setText(action) + return True + except Exception as error: + self._error(error, "Спецификация не изменена.") + return False + + +def show_drawing_bom(page): + if page is None or not page.isDerivedFrom("TechDraw::DrawPage"): + raise ValueError("Выберите существующий лист чертежа для размещения спецификации.") + if Gui.Control.activeDialog(): + raise ValueError("Завершите редактирование детали или сборки перед просмотром спецификации на листе.") + key = (page.Document.Name, page.Name) + existing = _dialogs.get(key) + if existing is not None: + try: + if existing.isVisible() and existing.page == page: + existing.refresh() + existing.raise_() + existing.activateWindow() + return existing + except RuntimeError: + pass + dialog = DrawingBOMDialog(page) + _dialogs[key] = dialog + + def forget(_result=None): + if _dialogs.get(key) is dialog: + _dialogs.pop(key, None) + dialog.deleteLater() + + dialog.finished.connect(forget) + dialog.show() + dialog.raise_() + dialog.activateWindow() + return dialog diff --git a/src/QInventor/qi_extrude.py b/src/QInventor/qi_extrude.py new file mode 100644 index 0000000..93908b0 --- /dev/null +++ b/src/QInventor/qi_extrude.py @@ -0,0 +1,942 @@ +"""Inventor-style extrusion task panel and persistent OCCT features. + +The model API is usable in FreeCADCmd. GUI imports are deferred until a +task panel is requested; previews never become document history features. +""" +import math +import os + +import FreeCAD as App +import Part + + +OPERATIONS = ("Join", "Cut", "Intersect", "New Solid") +DIRECTIONS = ("Forward", "Reverse", "Symmetric", "Asymmetric") +EXTENTS = ("Distance", "Through all", "To face", "To next") +OPERATION_LABELS = {"Join": "Объединение", "Cut": "Вырезание", "Intersect": "Пересечение", "New Solid": "Новое тело"} +DIRECTION_LABELS = {"Forward": "Вперёд", "Reverse": "Обратное", "Symmetric": "Симметрично", "Asymmetric": "Асимметрично"} +EXTENT_LABELS = {"Distance": "Расстояние", "Through all": "Через всё", "To face": "До грани", "To next": "До следующей"} + + +def _normalize(value, labels): + text = str(value) + if text in labels: + return text + for key, label in labels.items(): + if text == label: + return key + raise ValueError("Неизвестный режим выдавливания.") + + +def _localize_enums(obj): + for name, labels in (("Operation", OPERATION_LABELS), ("Direction", DIRECTION_LABELS), ("Extent", EXTENT_LABELS)): + previous = str(getattr(obj, name)) + key = _normalize(previous, labels) if previous else next(iter(labels)) + setattr(obj, name, list(labels.values())) + setattr(obj, name, labels[key]) + + +def _positive_length(value): + try: + quantity = App.Units.Quantity(value) if isinstance(value, str) else value + if hasattr(quantity, "Unit") and quantity.Unit not in (App.Units.Unit("mm"), App.Units.Unit()): + raise ValueError() + number = float(getattr(quantity, "Value", quantity)) + except Exception as exc: + raise ValueError("Введите корректную положительную длину в миллиметрах.") from exc + if not math.isfinite(number) or number <= 0: + raise ValueError("Длина выдавливания должна быть положительным конечным числом.") + return number + + +def _body_of(profile): + for parent in profile.InList: + if parent.isDerivedFrom("PartDesign::Body") and profile in parent.Group: + return parent + return None + + +def _is_solid(obj): + if obj is None or not hasattr(obj, "Shape"): + return False + if any(str(flag).lower().startswith(("invalid", "error")) for flag in obj.State): + return False + shape = obj.Shape + return not shape.isNull() and shape.isValid() and len(shape.Solids) == 1 and shape.Volume > 1e-9 + + +def _base_for(profile): + """Capture the previous solid before a new feature changes the Body Tip.""" + body = _body_of(profile) + if body is None: + return None + if body.Tip != profile and _is_solid(body.Tip): + return body.Tip + members = list(body.Group) + # Native NewSketch may set Tip to the sketch. Search earlier body history. + if profile in members: + members = members[:members.index(profile)] + for obj in reversed(members): + if _is_solid(obj): + return obj + return None + + +def _profile_face(profile): + if profile is None or not profile.isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Выберите двумерный эскиз с замкнутым профилем.") + if any(str(flag).lower().startswith(("invalid", "error")) for flag in profile.State): + raise ValueError("Исправьте конфликтующие ограничения или повреждённые ссылки эскиза перед выдавливанием.") + shape = profile.Shape + if shape.isNull() or not shape.Wires: + raise ValueError("Эскиз не содержит замкнутого контура.") + if any(not wire.isClosed() for wire in shape.Wires): + raise ValueError("Все контуры эскиза должны быть замкнуты. Завершите или обрежьте открытые линии.") + # Bullseye sorts nested wires and preserves internal holes, regardless of + # the order and orientation in which sketch geometry was drawn. + try: + face = Part.makeFace(list(shape.Wires), "Part::FaceMakerBullseye") + except Exception as exc: + raise ValueError("Не удалось создать грань профиля. Проверьте замкнутость, самопересечения и вложенность контуров.") from exc + if face.isNull() or not face.isValid() or len(face.Faces) != 1: + raise ValueError("Нужен один связный профиль; внутренние замкнутые отверстия допускаются.") + if face.Area <= 1e-9: + raise ValueError("Площадь профиля должна быть больше нуля.") + return face + + +def _normal(profile): + direction = profile.Placement.Rotation.multVec(App.Vector(0, 0, 1)) + direction.normalize() + return direction + + +def _bbox_projection(shape, normal, origin): + bb = shape.BoundBox + values = [normal.dot(App.Vector(x, y, z) - origin) + for x in (bb.XMin, bb.XMax) for y in (bb.YMin, bb.YMax) + for z in (bb.ZMin, bb.ZMax)] + return min(values), max(values) + + +def _shape_in_profile_frame(obj, shape, profile): + """Shape includes obj.Placement, but not the placements of its parents.""" + parent_placement = obj.getGlobalPlacement().multiply(obj.Placement.inverse()) + body = _body_of(profile) + destination = body.getGlobalPlacement() if body else App.Placement() + shape = shape.copy() + shape.Placement = destination.inverse().multiply(parent_placement).multiply(shape.Placement) + return shape + + +def _container_ancestors(obj): + ancestors = [] + if obj is None: + return ancestors + for parent in obj.InList: + if hasattr(parent, "Group") and obj in parent.Group and hasattr(parent, "getGlobalPlacement"): + if parent not in ancestors: + ancestors.append(parent) + for grandparent in _container_ancestors(parent): + if grandparent not in ancestors: + ancestors.append(grandparent) + return ancestors + + +def _sync_support_links(obj): + if not {"Profile", "TargetFace", "Terminator", "SupportObjects"}.issubset(obj.PropertiesList): + # FCStd restore and native redo load properties one by one and may + # invoke onChanged before the complete feature schema is present. + return + # LinkSub links to a child do not touch dependent Python features when the + # parent App::Part placement changes. Native links to external ancestors + # retain this dependency across save/reopen. Never link our own ancestors: + # a Body contains this feature and such a link would form a history cycle. + excluded = _container_ancestors(obj) + _container_ancestors(obj.Profile) + targets = ([obj.TargetFace[0]] if obj.TargetFace and obj.TargetFace[0] else []) + if obj.Terminator is not None: + targets.append(obj.Terminator) + parents = [] + for target in targets: + for parent in _container_ancestors(target): + if parent not in excluded and parent not in parents: + parents.append(parent) + if list(obj.SupportObjects) != parents: + obj.SupportObjects = parents + + +def _canonical_target_link(profile, target_face): + """Anchor viewport Body faces to their preceding real feature, not Body.""" + if not target_face or len(target_face) != 2: + return target_face + obj, names = target_face + if obj is None or not obj.isDerivedFrom("PartDesign::Body"): + return target_face + names = [names] if isinstance(names, str) else list(names) + if len(names) != 1 or not str(names[0]).startswith("Face") or not _is_solid(obj.Tip): + raise ValueError("Выберите грань конечного твёрдого элемента тела.") + try: + selected = _shape_in_profile_frame(obj, obj.Shape.getElement(names[0]), profile) + matches = [index for index, face in enumerate(obj.Tip.Shape.Faces) + if _shape_in_profile_frame(obj.Tip, face, profile).isPartner(selected)] + if not matches: + # Body wrappers can copy the topology. Prove complete geometric + # coincidence with OCCT face intersections, never guess by index. + tolerance = max(1e-7, selected.Area * 1e-8) + for index, face in enumerate(obj.Tip.Shape.Faces): + candidate = _shape_in_profile_frame(obj.Tip, face, profile) + if abs(candidate.Area - selected.Area) <= tolerance and abs(candidate.common(selected).Area - selected.Area) <= tolerance: + matches.append(index) + except Exception as exc: + raise ValueError("Не удалось закрепить выбранную грань за элементом истории. Выберите грань элемента в дереве.") from exc + if len(matches) != 1: + raise ValueError("Выбранная грань тела неоднозначна. Выберите грань конечного элемента в дереве.") + return obj.Tip, ["Face%d" % (matches[0] + 1)] + + +def _target_face(profile, target_face): + target_face = _canonical_target_link(profile, target_face) + if not target_face or len(target_face) != 2: + raise ValueError("Выберите конечную плоскую грань в окне модели.") + obj, names = target_face + if isinstance(names, str): + names = [names] + if obj is None or obj.Document != profile.Document or len(names) != 1: + raise ValueError("Выберите одну грань объекта в текущем документе.") + if not str(names[0]).startswith("Face"): + raise ValueError("Конечная опора должна быть гранью, а не ребром или вершиной.") + try: + face = obj.Shape.getElement(names[0]) + if face.ShapeType != "Face" or face.isNull(): + raise ValueError() + face = _shape_in_profile_frame(obj, face, profile) + except Exception as exc: + raise ValueError("Выбранная грань больше не существует. Выберите конечную опору заново.") from exc + if not isinstance(face.Surface, Part.Plane): + raise ValueError("Завершение до криволинейной грани пока не поддерживается. Выберите плоскую грань.") + return face + + +def _canonical_terminator(terminator): + if terminator is not None and terminator.isDerivedFrom("PartDesign::Body"): + if not _is_solid(terminator.Tip): + raise ValueError("В теле завершения нет конечного твёрдого элемента.") + return terminator.Tip + return terminator + + +def _to_face_prism(face, vector, target, extend_face=False): + """Exact plane termination. Bounds only size the prism before OCCT trimming.""" + axis = target.Surface.Axis + point = target.Surface.Position + denominator = axis.dot(vector) + if abs(denominator) < 1e-9: + raise ValueError("Конечная плоскость параллельна направлению выдавливания и не может завершить тело.") + distance = axis.dot(point - face.CenterOfMass) / denominator + if distance <= 1e-7: + raise ValueError("Конечная грань находится на плоскости эскиза или в противоположном направлении.") + plane = Part.Face(target.Surface) + if face.section(plane).Vertexes or face.distToShape(plane)[0] <= 1e-7: + raise ValueError("Конечная плоскость пересекает профиль. Весь профиль должен лежать перед конечной гранью.") + # A finite face produces a halfspace whose boundary may fail to split a + # larger prism in OCCT. Build the halfspace from its unbounded plane. + halfspace = plane.makeHalfSpace(face.CenterOfMass) + bb = face.BoundBox + distances = [axis.dot(point - App.Vector(x, y, z)) / denominator + for x in (bb.XMin, bb.XMax) for y in (bb.YMin, bb.YMax) + for z in (bb.ZMin, bb.ZMax)] + # This envelope never defines the end surface: the infinite OCCT halfspace + # cuts it back to the selected supporting plane exactly, including obliques. + span = max(distances) + max(1.0, abs(max(distances)) * 0.01) + prism = face.extrude(vector * span).common(halfspace).removeSplitter() + caps = [cap for cap in prism.Faces if isinstance(cap.Surface, Part.Plane) + and cap.distToShape(plane)[0] <= 1e-7 + and abs(abs(cap.Surface.Axis.dot(axis)) - 1) < 1e-8] + cap_area = sum(cap.Area for cap in caps) + if cap_area <= 1e-9: + raise ValueError("Не удалось получить конечную грань выдавливания.") + if not extend_face: + covered = sum(cap.common(target).Area for cap in caps) + if abs(cap_area - covered) > max(1e-7, cap_area * 1e-8): + raise ValueError("Выбранная грань не перекрывает весь профиль. Включите «Продолжить грань» или выберите другую опору.") + return prism + + +def _to_next_prism(profile, face, vector, terminator): + if terminator is None or terminator.Document != profile.Document or not hasattr(terminator, "Shape"): + raise ValueError("Выберите тело или поверхность для завершения до следующей грани.") + shape = _shape_in_profile_frame(terminator, terminator.Shape, profile) + if shape.isNull() or not shape.isValid(): + raise ValueError("Объект завершения не содержит корректной геометрии.") + if any(not isinstance(target.Surface, Part.Plane) for target in shape.Faces): + raise ValueError("До следующей грани поддерживаются опоры из плоских граней. Для криволинейной опоры выберите другой способ завершения.") + candidates = [] + for target in shape.Faces: + try: + candidates.append(_to_face_prism(face, vector, target, False)) + except ValueError: + continue + if not candidates: + raise ValueError("В выбранном направлении нет следующей плоской грани, полностью перекрывающей профиль.") + nearest = min(candidates, key=lambda candidate: candidate.Volume) + # Volume alone cannot order oblique planes. A precise boolean containment + # proves this termination is first over the entire extruded cross-section. + if any(nearest.cut(candidate).Volume > max(1e-7, nearest.Volume * 1e-8) + for candidate in candidates): + raise ValueError("Следующая грань неоднозначна: опорные плоскости пересекаются над профилем. Выберите конечную грань явно.") + return nearest + + +def build_shape(profile, length, operation="Join", direction="Forward", + extent="Distance", base=None, length2=10.0, target_face=None, + extend_face=False, terminator=None): + """Return a real, validated single OCCT solid in the profile body frame.""" + length = _positive_length(length) + length2 = _positive_length(length2) + operation = _normalize(operation, OPERATION_LABELS) + direction = _normalize(direction, DIRECTION_LABELS) + extent = _normalize(extent, EXTENT_LABELS) + if direction == "Asymmetric": + length2 = _positive_length(length2) + if extent != "Distance": + raise ValueError("Асимметричное направление требует двух расстояний и доступно в режиме «Расстояние».") + if extent in ("To face", "To next") and direction not in ("Forward", "Reverse"): + raise ValueError("Для завершения до грани выберите направление «Вперёд» или «Обратное».") + face = _profile_face(profile) + normal = _normal(profile) + if operation in ("Cut", "Intersect") and not _is_solid(base): + raise ValueError("Для вырезания или пересечения требуется исходное твёрдое тело.") + if extent == "Through all": + if operation not in ("Cut", "Intersect") or not _is_solid(base): + raise ValueError("Сквозное выдавливание доступно для вырезания и пересечения существующего тела.") + low, high = _bbox_projection(base.Shape, normal, face.CenterOfMass) + margin = max(base.Shape.BoundBox.DiagonalLength * 0.01, 1.0) + if direction == "Forward": + length = max(high + margin, margin) + elif direction == "Reverse": + length = max(-low + margin, margin) + else: + length = 2 * (max(abs(low), abs(high)) + margin) + vector = normal * (-1 if direction == "Reverse" else 1) + try: + if extent == "To face": + prism = _to_face_prism(face, vector, _target_face(profile, target_face), extend_face) + elif extent == "To next": + if not _is_solid(base): + raise ValueError("До следующей грани требуется существующее исходное тело.") + prism = _to_next_prism(profile, face, vector, _canonical_terminator(terminator or base)) + else: + if direction == "Symmetric": + face.translate(normal * (-length / 2)) + elif direction == "Asymmetric": + face.translate(normal * -length2) + length += length2 + prism = face.extrude(vector * length) + if operation == "Join" and _is_solid(base): + result = base.Shape.fuse(prism) + elif operation == "Cut": + result = base.Shape.cut(prism) + elif operation == "Intersect": + result = base.Shape.common(prism) + else: + result = prism + result = result.removeSplitter() + except ValueError: + raise + except Exception as exc: + raise ValueError("Не удалось выполнить твердотельную операцию. Проверьте пересечение профиля с исходной деталью.") from exc + if result.isNull() or not result.isValid() or len(result.Solids) != 1 or result.Volume <= 1e-9: + raise ValueError("Операция должна создавать одно корректное твёрдое тело. Проверьте пересечение профиля с деталью.") + if operation == "Cut" and result.Volume >= base.Shape.Volume - 1e-8: + raise ValueError("Профиль не пересекает исходное тело в выбранном направлении.") + if operation == "Join" and _is_solid(base) and result.Volume <= base.Shape.Volume + 1e-8: + raise ValueError("Выдавливание не добавляет объём к исходному телу.") + return result + + +class ExtrusionProxy: + """Serializable parametric feature: sketch and base links drive regeneration.""" + def __init__(self, obj): + for name, type_name in (("Profile", "App::PropertyLinkGlobal"), + ("BaseFeature", "App::PropertyLink"), + ("Length", "App::PropertyLength"), + ("Length2", "App::PropertyLength"), + ("TargetFace", "App::PropertyLinkSubGlobal"), + ("ExtendFace", "App::PropertyBool"), + ("Terminator", "App::PropertyLinkGlobal"), + ("SupportObjects", "App::PropertyLinkListGlobal"), + ("Operation", "App::PropertyEnumeration"), + ("Direction", "App::PropertyEnumeration"), + ("Extent", "App::PropertyEnumeration"), + ("LastError", "App::PropertyString")): + if name not in obj.PropertiesList: + obj.addProperty(type_name, name, "Выдавливание") + obj.Operation = list(OPERATION_LABELS.values()) + obj.Direction = list(DIRECTION_LABELS.values()) + obj.Extent = list(EXTENT_LABELS.values()) + obj.Length2 = 10.0 + obj.setEditorMode("LastError", 1) + obj.setEditorMode("SupportObjects", 2) + obj.Proxy = self + + def onChanged(self, obj, prop): + if prop in ("TargetFace", "Terminator", "Profile") and "SupportObjects" in obj.PropertiesList: + _sync_support_links(obj) + + def execute(self, obj): + # Explicit links and properties are retained in FCStd, so reopening and + # editing an upstream dimensional constraint reevaluates this feature. + try: + obj.Shape = build_shape(obj.Profile, obj.Length, str(obj.Operation), + str(obj.Direction), str(obj.Extent), obj.BaseFeature, + obj.Length2, obj.TargetFace, obj.ExtendFace, obj.Terminator) + obj.LastError = "" + except Exception as exc: + obj.Shape = Part.Shape() + from qi_messages import error_text + obj.LastError = error_text(exc, "Геометрическое ядро не смогло построить выдавливание. Проверьте профиль, опоры и параметры.") + App.Console.PrintError("QInventor: {}\n".format(exc)) + raise + + def onDocumentRestored(self, obj): + obj.Proxy = self + if "Length2" not in obj.PropertiesList: + previous = {name: str(getattr(obj, name)) for name in ("Operation", "Direction", "Extent")} + self.__init__(obj) + for name, labels in (("Operation", OPERATION_LABELS), ("Direction", DIRECTION_LABELS), ("Extent", EXTENT_LABELS)): + setattr(obj, name, labels[_normalize(previous[name], labels)]) + if "SupportObjects" not in obj.PropertiesList: + obj.addProperty("App::PropertyLinkListGlobal", "SupportObjects", "Выдавливание") + obj.setEditorMode("SupportObjects", 2) + _sync_support_links(obj) + _localize_enums(obj) + + def dumps(self): + return None + + def loads(self, state): + return None + + +class ExtrusionViewProvider: + def __init__(self, view): + view.Proxy = self + + def getIcon(self): + return os.path.join(os.path.dirname(__file__), "resources", "qinventor.svg") + + def claimChildren(self): + return [self.Object.Profile] if getattr(self, "Object", None) and self.Object.Profile else [] + + def attach(self, view): + self.Object = view.Object + + def setEdit(self, view, mode): + show_dialog(view.Object) + return True + + def unsetEdit(self, view, mode): + return None + + def dumps(self): + return None + + def loads(self, state): + return None + + +def _new_body(doc, source_body=None): + body = doc.addObject("PartDesign::Body", "Body") + body.Label = "Тело" + if source_body is not None: + parent = next((obj for obj in source_body.InList if obj.isDerivedFrom("App::Part") + and source_body in obj.Group), None) + if parent is not None: + parent.addObject(body) + body.Placement = source_body.Placement + else: + body.Placement = source_body.getGlobalPlacement() + return body + + +def create_extrusion(profile, length, operation="Join", direction="Forward", + extent="Distance", _base=None, _transaction=True, + length2=10.0, target_face=None, extend_face=False, terminator=None): + """Create a persistent feature. Invalid input changes nothing in the document.""" + length = _positive_length(length) + length2 = _positive_length(length2) + operation = _normalize(operation, OPERATION_LABELS) + direction = _normalize(direction, DIRECTION_LABELS) + extent = _normalize(extent, EXTENT_LABELS) + if profile is None or not profile.isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Выберите двумерный эскиз с замкнутым профилем.") + profile.Document.recompute() + target_face = _canonical_target_link(profile, target_face) + terminator = _canonical_terminator(terminator) + base = _base if _base is not None else _base_for(profile) + result = build_shape(profile, length, operation, direction, extent, base, + length2, target_face, extend_face, terminator) + doc = profile.Document + source_body = _body_of(profile) + if _transaction: + doc.openTransaction("Выдавливание") + try: + body = source_body + if body is None or (operation == "New Solid" and _is_solid(base)): + if source_body is not None and _is_solid(base): + source_body.Tip = base + body = _new_body(doc, source_body) + obj = body.newObject("PartDesign::FeaturePython", "QI_Extrusion") + obj.Label = {"Join": "Выдавливание", "Cut": "Выдавливание — вырезать", + "Intersect": "Выдавливание — пересечение", "New Solid": "Выдавливание — новое тело"}[operation] + ExtrusionProxy(obj) + obj.Profile = profile + obj.BaseFeature = base if operation != "New Solid" else None + obj.Length = length + obj.Length2 = _positive_length(length2) + obj.TargetFace = target_face + obj.ExtendFace = bool(extend_face) + obj.Terminator = terminator + obj.Operation = OPERATION_LABELS[operation] + obj.Direction = DIRECTION_LABELS[direction] + obj.Extent = EXTENT_LABELS[extent] + obj.Shape = result + body.Tip = obj + if App.GuiUp: + ExtrusionViewProvider(obj.ViewObject) + if base is not None and operation != "New Solid": + base.Visibility = False + profile.Visibility = False + doc.recompute() + if obj.LastError or not _is_solid(obj) or not _is_solid(body): + raise ValueError(obj.LastError or "Не удалось создать твёрдое тело.") + if _transaction: + doc.commitTransaction() + return obj + except Exception: + if _transaction: + doc.abortTransaction() + raise + + +def _edit_base(feature): + if feature.BaseFeature is not None: + return feature.BaseFeature + if _body_of(feature) != _body_of(feature.Profile): + return _base_for(feature.Profile) + return None + + +def _update_feature(feature, arguments): + """Apply edits in the caller's transaction, including New Solid history.""" + profile, length, operation, direction, extent, base, length2, target_face, extend_face, terminator = arguments + target_face = _canonical_target_link(profile, target_face) + terminator = _canonical_terminator(terminator) + for support in ([target_face[0]] if target_face and target_face[0] else []) + ([terminator] if terminator else []): + if support == feature or feature in support.OutListRecursive: + raise ValueError("Конечная опора не должна зависеть от редактируемого выдавливания.") + arguments = (profile, length, operation, direction, extent, base, length2, target_face, extend_face, terminator) + build_shape(*arguments) + body, source_body = _body_of(feature), _body_of(profile) + if operation == "New Solid" and body == source_body and _is_solid(base): + if body.Tip != feature or list(body.Group)[-1] != feature: + raise ValueError("Перевести операцию в новое тело можно только в конце истории исходного тела.") + body.removeObject(feature) + body.Tip = base + target_body = _new_body(feature.Document, source_body) + target_body.addObject(feature) + target_body.Tip = feature + base.Visibility = True if App.GuiUp else base.Visibility + elif operation != "New Solid" and body != source_body: + if source_body is None or not _is_solid(base) or source_body.Tip != base: + raise ValueError("В исходном теле нет доступного конечного твёрдого элемента для этой операции.") + if body.Tip != feature or any(member != feature for member in body.Group): + raise ValueError("Изменить принадлежность тела можно только для единственного конечного выдавливания.") + body.removeObject(feature) + source_body.addObject(feature) + source_body.Tip = feature + feature.Document.removeObject(body.Name) + for name, value in (("Length", length), ("Length2", length2)): + # Preserve a formula until the user explicitly changes its value. + if abs(getattr(feature, name).Value - value) > 1e-9: + feature.setExpression(name, None) + setattr(feature, name, value) + feature.BaseFeature = None if operation == "New Solid" else base + feature.Operation = OPERATION_LABELS[operation] + feature.Direction = DIRECTION_LABELS[direction] + feature.Extent = EXTENT_LABELS[extent] + feature.TargetFace, feature.ExtendFace, feature.Terminator = target_face, extend_face, terminator + _sync_support_links(feature) + feature.Document.recompute() + if feature.LastError or not _is_solid(feature) or not _is_solid(_body_of(feature)): + raise ValueError(feature.LastError or "Некорректное выдавливание.") + return feature + + +def _gui_modules(): + import FreeCADGui as Gui + from PySide import QtCore, QtGui + try: + from PySide import QtWidgets + except ImportError: + QtWidgets = QtGui + return Gui, QtCore, QtWidgets + + +class ExtrusionTaskPanel: + def __init__(self, doc, profile=None, feature=None): + Gui, QtCore, QtWidgets = _gui_modules() + self.doc, self.feature = doc, feature + self._finished = False + self._preview = None + self._visibility = {} + self._base = _edit_base(feature) if feature is not None else None + self._target_face = feature.TargetFace if feature is not None else None + self._terminator = feature.Terminator if feature is not None else None + self._picking_target = False + self.form = QtWidgets.QWidget() + self.form.setWindowTitle("Выдавливание") + layout = QtWidgets.QVBoxLayout(self.form) + hint = QtWidgets.QLabel("Профиль → направление и расстояние → операция → ОК") + hint.setWordWrap(True) + layout.addWidget(hint) + grid = QtWidgets.QFormLayout() + self.profiles = QtWidgets.QComboBox() + self.profiles.setObjectName("QI_ExtrudeProfile") + candidates = [obj for obj in doc.Objects if obj.isDerivedFrom("Sketcher::SketchObject")] + if not candidates: + raise ValueError("Сначала создайте и завершите эскиз.") + self._profiles = candidates + for obj in candidates: + self.profiles.addItem(obj.Label, obj.Name) + if profile in candidates: + self.profiles.setCurrentIndex(candidates.index(profile)) + self.operation = QtWidgets.QComboBox() + self.operation.setObjectName("QI_ExtrudeOperation") + for label, value in (("Объединение", "Join"), ("Вырезание", "Cut"), + ("Пересечение", "Intersect"), ("Новое тело", "New Solid")): + self.operation.addItem(label, value) + self.extent = QtWidgets.QComboBox() + self.extent.setObjectName("QI_ExtrudeExtent") + for value, label in EXTENT_LABELS.items(): + self.extent.addItem(label, value) + self.direction = QtWidgets.QComboBox() + self.direction.setObjectName("QI_ExtrudeDirection") + for label, value in (("Вперёд", "Forward"), ("Обратное", "Reverse"), + ("Симметрично", "Symmetric"), ("Асимметрично", "Asymmetric")): + self.direction.addItem(label, value) + self.length = QtWidgets.QDoubleSpinBox() + self.length.setObjectName("QI_ExtrudeLength") + self.length.setRange(0.001, 1000000) + self.length.setDecimals(3) + self.length.setSuffix(" мм") + self.length.setValue(10.0) + self.length2 = QtWidgets.QDoubleSpinBox() + self.length2.setObjectName("QI_ExtrudeLength2") + self.length2.setRange(0.001, 1000000) + self.length2.setDecimals(3) + self.length2.setSuffix(" мм") + self.length2.setValue(10.0) + self.swap_distances = QtWidgets.QPushButton("Поменять расстояния местами") + self.swap_distances.clicked.connect(self._swap_distances) + self.target = QtWidgets.QLineEdit() + self.target.setObjectName("QI_ExtrudeTarget") + self.target.setReadOnly(True) + self.target.setPlaceholderText("Выберите опору в модели") + self.pick_target = QtWidgets.QPushButton("Выбрать в модели") + self.pick_target.setCheckable(True) + self.pick_target.clicked.connect(self._begin_target_selection) + target_row = QtWidgets.QWidget() + target_layout = QtWidgets.QVBoxLayout(target_row) + target_layout.setContentsMargins(0, 0, 0, 0) + target_layout.addWidget(self.target) + target_layout.addWidget(self.pick_target) + self.extend_face = QtWidgets.QCheckBox("Продолжить грань до полного профиля") + self.extend_face.setObjectName("QI_ExtrudeExtendFace") + grid.addRow("Профиль", self.profiles) + grid.addRow("Операция", self.operation) + grid.addRow("Протяжённость", self.extent) + grid.addRow("Направление", self.direction) + grid.addRow("Расстояние А", self.length) + grid.addRow("Расстояние Б", self.length2) + grid.addRow(self.swap_distances) + grid.addRow("Конечная опора", target_row) + grid.addRow(self.extend_face) + layout.addLayout(grid) + self.status = QtWidgets.QLabel() + self.status.setObjectName("QI_ExtrudeStatus") + self.status.setWordWrap(True) + layout.addWidget(self.status) + layout.addStretch() + if feature is not None: + for combo, value in ((self.operation, _normalize(feature.Operation, OPERATION_LABELS)), + (self.direction, _normalize(feature.Direction, DIRECTION_LABELS)), + (self.extent, _normalize(feature.Extent, EXTENT_LABELS))): + combo.setCurrentIndex(combo.findData(value)) + self.length.setValue(feature.Length.Value) + self.length2.setValue(feature.Length2.Value) + self.extend_face.setChecked(feature.ExtendFace) + self.profiles.setEnabled(False) + self.doc.openTransaction("Изменить выдавливание" if feature else "Выдавливание") + self._timer = QtCore.QTimer(self.form) + self._timer.setSingleShot(True) + self._timer.setInterval(80) + self._timer.timeout.connect(self.update_preview) + for combo in (self.profiles, self.operation, self.extent, self.direction): + combo.currentIndexChanged.connect(self._changed) + self.length.valueChanged.connect(self._changed) + self.length2.valueChanged.connect(self._changed) + self.extend_face.toggled.connect(self._changed) + Gui.Selection.addObserver(self) + self._refresh_controls() + self.update_preview() + + def _changed(self, *args): + if self.operation.currentData() not in ("Cut", "Intersect") and self.extent.currentData() == "Through all": + self.extent.setCurrentIndex(0) + self._refresh_controls() + self._timer.start() + + def _refresh_controls(self): + extent = self.extent.currentData() + asymmetric = self.direction.currentData() == "Asymmetric" + self.length.setEnabled(extent == "Distance") + self.length2.setEnabled(extent == "Distance" and asymmetric) + self.swap_distances.setEnabled(extent == "Distance" and asymmetric) + self.target.setEnabled(extent in ("To face", "To next")) + self.pick_target.setEnabled(extent in ("To face", "To next")) + self.extend_face.setEnabled(extent == "To face") + selected = self._target_face if extent == "To face" else self._terminator + if extent == "To face" and selected and selected[0]: + names = selected[1] if not isinstance(selected[1], str) else [selected[1]] + self.target.setText("%s · грань %s" % (selected[0].Label, str(names[0]).removeprefix("Face"))) + elif extent == "To next": + profile = self.doc.getObject(self.profiles.currentData()) + terminator = self._terminator or (self._base if self.feature is not None else _base_for(profile)) + self.target.setText(terminator.Label if terminator else "") + else: + self.target.setText("") + self.target.setCursorPosition(0) + self.target.setToolTip(self.target.text()) + + def _swap_distances(self): + first, second = self.length.value(), self.length2.value() + self.length.setValue(second) + self.length2.setValue(first) + + def _begin_target_selection(self, checked): + self._picking_target = checked + if not checked: + return + Gui, _, _ = _gui_modules() + Gui.Selection.clearSelection() + self.status.setText("Выберите плоскую грань в модели." if self.extent.currentData() == "To face" + else "Выберите тело или поверхность завершения в модели.") + + def addSelection(self, doc_name, object_name, sub_name, position): + if self._picking_target and doc_name == self.doc.Name: + self.set_target(self.doc.getObject(object_name), sub_name) + + def set_target(self, obj, sub_name=None): + """Receive a native viewport selection, without replacing the profile.""" + try: + if self.extent.currentData() == "To face": + profile = self.doc.getObject(self.profiles.currentData()) + if obj == self.feature or (self.feature is not None and self.feature in obj.OutListRecursive): + raise ValueError("Конечная грань не должна зависеть от редактируемого выдавливания.") + link = _canonical_target_link(profile, (obj, [sub_name])) + face = _target_face(profile, link) + self._target_face = link + # Autodesk activates face extension automatically when the + # finite face cannot contain the complete extruded profile. + if not self.extend_face.isChecked(): + axis = _normal(profile) * (-1 if self.direction.currentData() == "Reverse" else 1) + try: + _to_face_prism(_profile_face(profile), axis, face, False) + except ValueError as exc: + if "не перекрывает" in str(exc): + self.extend_face.setChecked(True) + elif self.extent.currentData() == "To next": + if obj is None or not hasattr(obj, "Shape"): + raise ValueError("Выберите объект с гранями для завершения.") + if obj == self.feature or (self.feature is not None and self.feature in obj.OutListRecursive): + raise ValueError("Опора не должна зависеть от редактируемого выдавливания.") + self._terminator = _canonical_terminator(obj) + self._picking_target = False + self.pick_target.setChecked(False) + self._changed() + except Exception as exc: + self.status.setStyleSheet("color: #bc3f3f;") + from qi_messages import error_text + self.status.setText(error_text(exc)) + + def _stop_selection(self): + Gui, _, _ = _gui_modules() + Gui.Selection.removeObserver(self) + self._picking_target = False + + def _arguments(self): + profile = self.doc.getObject(self.profiles.currentData()) + base = self._base if self.feature is not None else _base_for(profile) + return (profile, self.length.value(), self.operation.currentData(), self.direction.currentData(), + self.extent.currentData(), base, self.length2.value(), self._target_face, + self.extend_face.isChecked(), self._terminator) + + def _remember_visibility(self, obj): + if obj is not None and obj.Name not in self._visibility: + self._visibility[obj.Name] = obj.Visibility + + def _restore_visibility(self): + for name, value in self._visibility.items(): + obj = self.doc.getObject(name) + if obj is not None: + obj.Visibility = value + + def _remove_preview(self): + if self._preview is not None and self.doc.getObject(self._preview.Name): + self.doc.removeObject(self._preview.Name) + self._preview = None + + def update_preview(self): + if self._finished: + return + try: + self._restore_visibility() + self.doc.recompute() + args = self._arguments() + profile, length, operation, direction, extent, base = args[:6] + shape = build_shape(*args) + if self._preview is None: + self._preview = self.doc.addObject("Part::Feature", "QI_ExtrudePreview") + self._preview.Label = "Предпросмотр выдавливания" + self._preview.ViewObject.ShapeColor = (0.22, 0.65, 0.95) + self._preview.ViewObject.Transparency = 25 + self._preview.ViewObject.Selectable = False + self._preview.Shape = shape + source_body = _body_of(profile) + if source_body is not None: + self._preview.Placement = source_body.getGlobalPlacement().multiply(shape.Placement) + else: + self._preview.Placement = shape.Placement + self._preview.Visibility = True + for hidden in (base if operation != "New Solid" else None, self.feature): + self._remember_visibility(hidden) + if hidden is not None: + hidden.Visibility = False + self._refresh_controls() + self.status.setStyleSheet("color: #267840;") + volume_text = ("%.3f" % shape.Volume).replace(".", ",") + self.status.setText("Корректное твёрдое тело. Объём: %s мм³" % volume_text) + except Exception as exc: + if self._preview is not None: + self._preview.Visibility = False + self.status.setStyleSheet("color: #bc3f3f;") + from qi_messages import error_text + self.status.setText(error_text(exc)) + + def accept(self): + self._timer.stop() + try: + args = self._arguments() + profile, length, operation, direction, extent, base = args[:6] + # Validate before deleting the preview or changing persistent data. + build_shape(*args) + self._remove_preview() + self._restore_visibility() + if self.feature is None: + obj = create_extrusion(profile, length, operation, direction, extent, + _base=base, _transaction=False, length2=args[6], + target_face=args[7], extend_face=args[8], terminator=args[9]) + else: + obj = _update_feature(self.feature, args) + obj.Visibility = True + if base is not None and operation != "New Solid": + base.Visibility = False + profile.Visibility = False + self.doc.commitTransaction() + self._finished = True + self._stop_selection() + Gui, _, _ = _gui_modules() + if self.feature is not None: + Gui.activeDocument().resetEdit() + Gui.activeDocument().activeView().viewAxonometric() + return True + except Exception as exc: + self.doc.abortTransaction() + self._preview = None + self._visibility.clear() + self.doc.openTransaction("Выдавливание") + self.status.setStyleSheet("color: #bc3f3f;") + from qi_messages import error_text + self.status.setText(error_text(exc)) + return False + + def reject(self): + self._timer.stop() + if not self._finished: + self.doc.abortTransaction() + self._preview = None + self._finished = True + self._stop_selection() + if self.feature is not None: + Gui, _, _ = _gui_modules() + Gui.activeDocument().resetEdit() + return True + + def getStandardButtons(self): + _, _, QtWidgets = _gui_modules() + buttons = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel + return int(getattr(buttons, "value", buttons)) + + def isAllowedAlterDocument(self): + return False + + def isAllowedAlterSelection(self): + return True + + def isAllowedAlterView(self): + return True + + +def show_dialog(feature=None): + """Open one task panel; select a sketch first or use the sole closed sketch.""" + Gui, QtCore, QtWidgets = _gui_modules() + doc = App.ActiveDocument + if doc is None: + raise ValueError("Сначала создайте документ и эскиз.") + doc.recompute() + profile = feature.Profile if feature is not None else None + if profile is None: + for selected in Gui.Selection.getSelection(): + if selected.isDerivedFrom("Sketcher::SketchObject"): + profile = selected + break + if profile is None: + valid = [] + for obj in doc.Objects: + if obj.isDerivedFrom("Sketcher::SketchObject"): + try: + _profile_face(obj) + valid.append(obj) + except Exception: + pass + if len(valid) == 1: + profile = valid[0] + elif valid: + visible = [obj for obj in valid if obj.Visibility] + if len(visible) == 1: + profile = visible[0] + if feature is None and Gui.activeDocument().getInEdit() is not None: + Gui.runCommand("Sketcher_LeaveSketch") + if Gui.Control.activeDialog(): + raise ValueError("Завершите текущую команду перед созданием выдавливания.") + panel = ExtrusionTaskPanel(doc, profile, feature) + Gui.Control.showDialog(panel) + main = Gui.getMainWindow() + for dock in main.findChildren(QtWidgets.QDockWidget): + if dock.objectName() == "Tasks": + dock.show() + dock.raise_() + Gui.activeDocument().activeView().fitAll() + QtCore.QCoreApplication.processEvents() + return panel diff --git a/src/QInventor/qi_io.py b/src/QInventor/qi_io.py new file mode 100644 index 0000000..97a7d8a --- /dev/null +++ b/src/QInventor/qi_io.py @@ -0,0 +1,60 @@ +"""Small, read-only helpers for native FreeCAD project persistence.""" +from pathlib import Path +import sys +import xml.etree.ElementTree as ET +import zipfile + + +MAX_DOCUMENT_XML_BYTES = 8 * 1024 * 1024 + + +def restore_document_label(doc, path): + """Restore the saved FCStd document title overwritten by native opening. + + Reads only the capped Document.xml member, never extracts ZIP contents and + never writes the project. Invalid/missing files and other formats return False. + A successfully read label returns True, including a label already displayed. + """ + try: + path = Path(path) + if path.suffix.lower() != ".fcstd" or not path.is_file(): + return False + with zipfile.ZipFile(path, "r") as archive: + info = archive.getinfo("Document.xml") + if info.is_dir() or info.file_size > MAX_DOCUMENT_XML_BYTES: + return False + with archive.open(info, "r") as member: + data = member.read(MAX_DOCUMENT_XML_BYTES + 1) + if len(data) > MAX_DOCUMENT_XML_BYTES: + return False + # Remove UTF-16/UTF-32 padding before checking ASCII XML declarations. + # DTD/entity declarations are unnecessary in a native FCStd document. + declarations = data.replace(b"\x00", b"").upper() + if b"= 8 and len({widget.itemText(i) for i in range(widget.count())} & {"Part", "Part Design", "Sketcher", "Assembly", "TechDraw", "Draft", "Spreadsheet"}) >= 4)) + if workbench_combo: + for index in range(widget.count()): + replacement = translate_source(widget.itemText(index)) + if replacement: + widget.setItemText(index, replacement) + tooltip = translate_source(widget.toolTip()) + if tooltip: + widget.setToolTip(tooltip) + if widget.metaObject().className() == "Gui::InputHintWidget" and isinstance(widget, QtWidgets.QLabel): + # Native hint text is HTML with input-device icons; replace + # only known source phrases inside that display-only markup. + text = widget.text() + for source in ("pick point or edge", "switch mode", "pick start point", "pick end point", "pick center point"): + text = text.replace(source, translate_source(source.capitalize()) or translate_source(source) or source) + if text != widget.text(): + widget.setText(text) + except RuntimeError: + pass + for action in main.findChildren(QtGui.QAction): + replacement = translate_source(action.text()) + if replacement: + action.setText(replacement) + for getter, setter in [("toolTip", "setToolTip"), ("statusTip", "setStatusTip")]: + replacement = translate_source(getattr(action, getter)()) + if replacement: + getattr(action, setter)(replacement) + + +def install(): + global _translator, _timer + if _translator is None: + App.ParamGet("User parameter:BaseApp/Preferences/General").SetString("Language", "Russian") + Gui.updateLocale() + _translator = RussianSupplement(QtWidgets.QApplication.instance()) + QtWidgets.QApplication.instance().installTranslator(_translator) + _timer = QtCore.QTimer(Gui.getMainWindow()) + _timer.timeout.connect(localize_surface) + _timer.start(700) + localize_surface() + return _translator + + +def untranslated_surface(): + """Return visible untranslated source candidates for bounded GUI QA. + + Latin identifiers and CAD format names remain valid data. A candidate is + evidence for review, not an instruction to rewrite document/model values. + """ + output = [] + main = Gui.getMainWindow() + allowed = {"QInventor", "FreeCAD", "STEP", "STL", "IGES", "BREP", "SVG", "DXF", "PDF", "FCStd", "XY", "XZ", "YZ", "X", "Y", "Z", "ABS", "PLA", "L", "C", "D", "E", "M", "MMB", "LMB", "RMB"} + def consider(text, kind, name): + plain = re.sub(r"<[^>]+>", "", str(text or "")).strip() + if (plain and re.search(r"[A-Za-z]{2}", plain) and not re.search(r"[А-Яа-яЁё]", plain) + and plain not in allowed and not re.search(r"[A-Za-z]:[\\/]|\.(FCStd|step|stl|svg|csv|pdf)\b|::|^[A-Za-z_]+\d+$", plain)): + output.append({"source": plain, "kind": kind, "name": name}) + for widget in main.findChildren(QtWidgets.QWidget): + if not widget.isVisible(): + continue + if isinstance(widget, (QtWidgets.QLabel, QtWidgets.QAbstractButton)): + text = widget.text() + if widget.property("QILocalizedStyle"): + text = translate_source(text) or text + consider(text, widget.metaObject().className(), widget.objectName()) + elif isinstance(widget, (QtWidgets.QGroupBox, QtWidgets.QMenu)): + consider(widget.title(), widget.metaObject().className(), widget.objectName()) + elif isinstance(widget, QtWidgets.QComboBox) and ( + "workbench" in widget.objectName().lower() + or any(str(widget.itemData(i) or "").endswith("Workbench") for i in range(widget.count())) + or (widget.count() >= 8 and len({widget.itemText(i) for i in range(widget.count())} & {"Part", "Part Design", "Sketcher", "Assembly", "TechDraw", "Draft", "Spreadsheet"}) >= 4)): + consider(widget.currentText(), "workbench", widget.objectName()) + if isinstance(widget, QtWidgets.QAbstractItemView) and widget.metaObject().className().endswith("PropertyEditor"): + model = widget.model() + def scan(parent=QtCore.QModelIndex(), depth=0): + if depth > 4: + return + for row in range(model.rowCount(parent)): + item = model.index(row, 0, parent) + delegate = widget.itemDelegate() + displayed = delegate.display_text(item) if isinstance(delegate, DisplayDelegate) else model.data(item, QtCore.Qt.DisplayRole) + consider(displayed, "property", widget.objectName()) + scan(item, depth + 1) + if model: + scan() + unique = {} + for entry in output: + unique[(entry["source"], entry["kind"])] = entry + return list(unique.values()) diff --git a/src/QInventor/qi_messages.py b/src/QInventor/qi_messages.py new file mode 100644 index 0000000..f7483bc --- /dev/null +++ b/src/QInventor/qi_messages.py @@ -0,0 +1,18 @@ +"""Russian user messages; native diagnostic text remains in the CAD log.""" +import errno +import re + + +def error_text(error, fallback="Не удалось выполнить команду. Проверьте выбранную геометрию и параметры."): + if isinstance(error, PermissionError): + return "Нет доступа к файлу или папке. Проверьте права доступа и не открыт ли файл другой программой." + if isinstance(error, FileNotFoundError): + return "Файл или папка не найдены. Проверьте выбранный путь." + if isinstance(error, OSError) and (error.errno == errno.ENOSPC or getattr(error, "winerror", None) == 112): + return "На диске недостаточно свободного места. Освободите место и повторите операцию." + text = str(error).strip() + # A Cyrillic filename inside an English native exception does not make + # the explanation Russian. Preserve explanations that actually start in RU. + if re.match(r"^[\s\W]*[А-Яа-яЁё]", text): + return text + return fallback diff --git a/src/QInventor/qi_model.py b/src/QInventor/qi_model.py new file mode 100644 index 0000000..c3d0156 --- /dev/null +++ b/src/QInventor/qi_model.py @@ -0,0 +1,1277 @@ +"""Parametric QInventor services using the unmodified FreeCAD solid kernel. + +All lengths are millimetres; mass is kg. Builders use real Sketcher driving +constraints and PartDesign features. This module is importable in FreeCADCmd +and does not require a GUI except for PDF/page SVG export. +""" +from contextlib import contextmanager +import math +import os +from pathlib import Path +import sys +import tempfile +import time +import xml.etree.ElementTree as ET + +import FreeCAD as App +import Part +import Sketcher + + +MATERIALS = { + "Сталь": 7850.0, "Алюминий": 2700.0, "Латунь": 8500.0, + "Титан": 4500.0, "ABS": 1040.0, "PLA": 1240.0, "Дерево": 650.0, +} +_DIM_LABELS = { + "QILength": "Длина", "QIWidth": "Ширина", "QIHeight": "Высота", + "QIRadius": "Радиус", "QIThickness": "Толщина", + "QIHoleDiameter": "Диаметр отверстий", "QIHoleSpacing": "Расстояние отверстий", + "QIWallHeight": "Высота стенки", "Length": "Длина", "Width": "Ширина", + "Radius": "Радиус", "HoleRadius": "Радиус отверстия", "HoleX1": "Центр первого X", + "HoleX2": "Центр второго X", "HoleY1": "Центр первого Y", "HoleY2": "Центр второго Y", +} +_CONSTRAINT_NAMES = { + "Length": "Длина", "Width": "Ширина", "Radius": "Радиус", + "HoleRadius": "РадиусОтверстий", "HoleX1": "Центр1X", "HoleX2": "Центр2X", + "HoleY1": "Центр1Y", "HoleY2": "Центр2Y", +} +_TRANSACTIONS = {} + + +def _positive(value, label="Размер"): + try: + value = float(value) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError("%s должен быть числом" % label) from exc + if not math.isfinite(value) or value <= 1e-7 or value > 1e7: + raise ValueError("%s должен быть конечным числом от 0 до 10 000 000 мм" % label) + return value + + +def _doc(doc=None): + return doc or App.ActiveDocument or new_part() + + +@contextmanager +def _transaction(doc, label): + """Nest helpers in one undo step and restore the document after an error.""" + key = doc.Name + outer = _TRANSACTIONS.get(key, 0) == 0 + if outer: + doc.openTransaction(label) + _TRANSACTIONS[key] = _TRANSACTIONS.get(key, 0) + 1 + try: + yield + if outer: + doc.recompute() + doc.commitTransaction() + except Exception: + if outer: + doc.abortTransaction() + doc.recompute() + raise + finally: + _TRANSACTIONS[key] -= 1 + if not _TRANSACTIONS[key]: + del _TRANSACTIONS[key] + + +def _property(obj, name, value, prop_type="App::PropertyLength", label=None): + if name not in obj.PropertiesList: + obj.addProperty(prop_type, name, "Размеры QInventor", label or _DIM_LABELS.get(name, name)) + if prop_type == "App::PropertyLength" and obj.isDerivedFrom("PartDesign::Body"): + params = _parameters(obj) + if name not in params.PropertiesList: + params.addProperty(prop_type, name, "Размеры", label or _DIM_LABELS.get(name, name)) + setattr(params, name, value) + obj.setExpression(name, params.Name + "." + name) + else: + setattr(obj, name, value) + + +def _parameters(body): + """Independent native VarSet preserves an acyclic model dependency graph.""" + if "QIParameters" not in body.PropertiesList: + body.addProperty("App::PropertyLink", "QIParameters", "Размеры QInventor", "Управляющие размеры") + if body.QIParameters is None: + params = body.Document.addObject("App::VarSet", "Parameters") + params.Label = "Размеры — " + body.Label + body.QIParameters = params + return body.QIParameters + + +def _ref(body, name): + return _parameters(body).Name + "." + name + + +def _body(doc, label="Деталь"): + body = doc.addObject("PartDesign::Body", "Body") + body.Label = label + count = sum(1 for item in doc.Objects if item.isDerivedFrom("PartDesign::Body")) + _property(body, "QIPartNumber", "QI-%03d" % count, "App::PropertyString", "Обозначение детали") + set_material(body, "Сталь") + return body + + +def _parent_body(obj): + if obj.isDerivedFrom("PartDesign::Body"): + return obj + try: + parent = obj.getParentGeoFeatureGroup() + if parent and parent.isDerivedFrom("PartDesign::Body"): + return parent + except AttributeError: + pass + for parent in obj.InList: + if parent.isDerivedFrom("PartDesign::Body"): + return parent + return None + + +def _hide(obj): + if App.GuiUp and hasattr(obj, "ViewObject"): + obj.ViewObject.Visibility = False + + +def _show(obj): + if App.GuiUp and hasattr(obj, "ViewObject"): + obj.ViewObject.Visibility = True + + +def _checked_solid(obj): + shape = obj.Shape + if shape.isNull() or not shape.isValid() or len(shape.Solids) != 1 or shape.Volume <= 1e-9: + raise ValueError("Операция не создала корректное единое твердое тело") + return shape + + +def _named_constraint(sketch, constraint, name): + index = sketch.addConstraint(constraint) + sketch.renameConstraint(index, _CONSTRAINT_NAMES.get(name, name)) + return index + + +def _cpath(name): + return "Constraints." + _CONSTRAINT_NAMES.get(name, name) + + +def _constraint_alias(name): + return next((alias for alias, localized in _CONSTRAINT_NAMES.items() if localized == name), name) + + +def _rectangle(sketch, width, height): + v = App.Vector + pts = [v(0, 0, 0), v(width, 0, 0), v(width, height, 0), v(0, height, 0)] + sketch.addGeometry([Part.LineSegment(pts[i], pts[(i + 1) % 4]) for i in range(4)], False) + sketch.addConstraint([Sketcher.Constraint("Coincident", i, 2, (i + 1) % 4, 1) for i in range(4)]) + sketch.addConstraint([ + Sketcher.Constraint("Horizontal", 0), Sketcher.Constraint("Vertical", 1), + Sketcher.Constraint("Horizontal", 2), Sketcher.Constraint("Vertical", 3), + Sketcher.Constraint("Coincident", 0, 1, -1, 1), + ]) + _named_constraint(sketch, Sketcher.Constraint("Distance", 0, width), "Length") + _named_constraint(sketch, Sketcher.Constraint("Distance", 1, height), "Width") + + +def _circle(sketch, radius): + sketch.addGeometry(Part.Circle(App.Vector(), App.Vector(0, 0, 1), radius), False) + sketch.addConstraint(Sketcher.Constraint("Coincident", 0, 3, -1, 1)) + _named_constraint(sketch, Sketcher.Constraint("Radius", 0, radius), "Radius") + + +def _plane_rotation(plane): + plane = str(plane).upper() + if plane == "XY": + return App.Rotation() + if plane == "XZ": + return App.Rotation(App.Vector(1, 0, 0), 90) + if plane == "YZ": + return App.Rotation(App.Vector(1, 1, 1), 120) + raise ValueError("Плоскость должна быть XY, XZ или YZ") + + +def new_part(name="Деталь"): + doc = App.newDocument("QInventorPart") + doc.Label = str(name) + doc.UndoMode = 1 + return doc + + +def new_assembly(name="Сборка"): + doc = App.newDocument("QInventorAssembly") + doc.Label = str(name) + doc.UndoMode = 1 + with _transaction(doc, "Новая сборка"): + assembly = doc.addObject("Assembly::AssemblyObject", "Assembly") + assembly.Type = "Assembly" + assembly.newObject("Assembly::JointGroup", "Joints") + assembly.Label = str(name) + _property(assembly, "QIKind", "Assembly", "App::PropertyString", "Тип документа") + return doc + + +def create_sketch_profile(kind, width, height, plane="XY", doc=None, body=None): + """Make a fully constrained rectangle or circle; circle width is radius. + + Pass body explicitly to add a profile for another Pad/Pocket in that body. + Otherwise a new Body is created. The sketch initially lies on an origin plane. + """ + kind = str(kind).lower() + if kind not in ("rectangle", "circle"): + raise ValueError("Профиль должен быть rectangle или circle") + width = _positive(width, "Ширина/радиус") + height = _positive(height, "Высота") if kind == "rectangle" else width + rotation = _plane_rotation(plane) + doc = body.Document if body is not None else _doc(doc) + with _transaction(doc, "Параметрический эскиз"): + body = body or _body(doc) + sketch = body.newObject("Sketcher::SketchObject", "Sketch") + sketch.Label = "Прямоугольник" if kind == "rectangle" else "Окружность" + _property(sketch, "QIPlane", str(plane).upper(), "App::PropertyString", "Плоскость эскиза") + sketch.Placement = App.Placement(App.Vector(), rotation) + (_rectangle if kind == "rectangle" else lambda s, w, h: _circle(s, w))(sketch, width, height) + doc.recompute() + if sketch.solve() != 0: + raise ValueError("Не удалось решить ограничения эскиза") + return sketch + + +def position_sketch_on_top(profile, body=None): + body = body or _parent_body(profile) + if not body or body.Tip is None or body.Tip == profile: + candidates = [o for o in body.Group if hasattr(o, "Shape") and not o.Shape.isNull() + and len(o.Shape.Solids)] if body else [] + if not candidates: + raise ValueError("Выберите деталь с твердым телом") + solid = candidates[-1] + else: + solid = body.Tip + if getattr(profile, "QIPlane", "XY") != "XY": + raise ValueError("Размещение сверху поддерживает эскиз XY") + with _transaction(profile.Document, "Эскиз на верхней плоскости"): + placement = profile.Placement + placement.Base.z = solid.Shape.BoundBox.ZMax + profile.Placement = placement + return profile + + +def pad(profile, length): + length = _positive(length, "Длина выдавливания") + body = _parent_body(profile) + if not body or not profile.isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Выберите эскиз внутри детали") + doc = profile.Document + with _transaction(doc, "Выдавливание"): + feature = body.newObject("PartDesign::Pad", "Pad") + feature.Label = "Выдавливание" + feature.Profile = profile + feature.Length = length + doc.recompute() + _checked_solid(feature) + body.Tip = feature + _hide(profile) + _show(body) + return feature + + +def pocket(profile, length): + length = _positive(length, "Глубина кармана") + body = _parent_body(profile) + if not body or not profile.isDerivedFrom("Sketcher::SketchObject"): + raise ValueError("Выберите эскиз внутри детали") + solids = [o for o in body.Group if o != profile and hasattr(o, "Shape") + and not o.Shape.isNull() and len(o.Shape.Solids)] + if not solids: + raise ValueError("Для кармана сначала создайте твердое тело") + previous = solids[-1] + doc = profile.Document + with _transaction(doc, "Карман"): + feature = body.newObject("PartDesign::Pocket", "Pocket") + feature.Label = "Карман" + feature.Profile = profile + feature.Length = length + doc.recompute() + shape = _checked_solid(feature) + if shape.Volume >= previous.Shape.Volume - 1e-7: + raise ValueError("Карман не пересекает деталь: проверьте плоскость и глубину") + body.Tip = feature + _hide(previous) + _hide(profile) + _show(body) + return feature + + +def create_box(length, width, height, doc=None): + length, width, height = [_positive(v) for v in (length, width, height)] + doc = _doc(doc) + with _transaction(doc, "Параметрический брусок"): + body = _body(doc, "Брусок") + for name, value in (("QILength", length), ("QIWidth", width), ("QIHeight", height)): + _property(body, name, value) + sketch = create_sketch_profile("rectangle", length, width, doc=doc, body=body) + sketch.setExpression(_cpath("Length"), _ref(body, "QILength")) + sketch.setExpression(_cpath("Width"), _ref(body, "QIWidth")) + feature = pad(sketch, height) + feature.setExpression("Length", _ref(body, "QIHeight")) + doc.recompute() + _checked_solid(body) + return body + + +def create_cylinder(radius, height, doc=None): + radius, height = [_positive(v) for v in (radius, height)] + doc = _doc(doc) + with _transaction(doc, "Параметрический цилиндр"): + body = _body(doc, "Цилиндр") + _property(body, "QIRadius", radius) + _property(body, "QIHeight", height) + sketch = create_sketch_profile("circle", radius, radius, doc=doc, body=body) + sketch.setExpression(_cpath("Radius"), _ref(body, "QIRadius")) + feature = pad(sketch, height) + feature.setExpression("Length", _ref(body, "QIHeight")) + doc.recompute() + _checked_solid(body) + return body + + +def _validate_plate(length, width, diameter, spacing): + if diameter >= width or spacing <= diameter or spacing + diameter >= length: + raise ValueError("Отверстия должны помещаться внутри плиты и не пересекаться") + + +def create_plate_with_holes(length, width, thickness, hole_diameter, hole_spacing=None, doc=None): + length, width, thickness, hole_diameter = [_positive(v) for v in + (length, width, thickness, hole_diameter)] + hole_spacing = _positive(hole_spacing if hole_spacing is not None else length * 0.6) + _validate_plate(length, width, hole_diameter, hole_spacing) + doc = _doc(doc) + with _transaction(doc, "Плита с отверстиями"): + body = _body(doc, "Плита с отверстиями") + for name, value in (("QILength", length), ("QIWidth", width), ("QIThickness", thickness), + ("QIHoleDiameter", hole_diameter), ("QIHoleSpacing", hole_spacing)): + _property(body, name, value) + sketch = create_sketch_profile("rectangle", length, width, doc=doc, body=body) + sketch.setExpression(_cpath("Length"), _ref(body, "QILength")) + sketch.setExpression(_cpath("Width"), _ref(body, "QIWidth")) + extrusion = pad(sketch, thickness) + extrusion.setExpression("Length", _ref(body, "QIThickness")) + holes = body.newObject("Sketcher::SketchObject", "HoleSketch") + holes.Label = "Отверстия" + _property(holes, "QIPlane", "XY", "App::PropertyString", "Плоскость эскиза") + holes.Placement.Base.z = thickness + holes.setExpression("Placement.Base.z", _ref(body, "QIThickness")) + for index, x in enumerate(((length - hole_spacing) / 2, (length + hole_spacing) / 2)): + holes.addGeometry(Part.Circle(App.Vector(x, width / 2, 0), App.Vector(0, 0, 1), hole_diameter / 2), False) + suffix = str(index + 1) + _named_constraint(holes, Sketcher.Constraint("DistanceX", index, 3, x), "HoleX" + suffix) + _named_constraint(holes, Sketcher.Constraint("DistanceY", index, 3, width / 2), "HoleY" + suffix) + sign = " - " if index == 0 else " + " + holes.setExpression(_cpath("HoleX" + suffix), + "(%s%s%s) / 2" % (_ref(body, "QILength"), sign, _ref(body, "QIHoleSpacing"))) + holes.setExpression(_cpath("HoleY" + suffix), _ref(body, "QIWidth") + " / 2") + _named_constraint(holes, Sketcher.Constraint("Radius", 0, hole_diameter / 2), "HoleRadius") + holes.addConstraint(Sketcher.Constraint("Equal", 0, 1)) + holes.setExpression(_cpath("HoleRadius"), _ref(body, "QIHoleDiameter") + " / 2") + cut = pocket(holes, thickness) + cut.Label = "Сквозные отверстия" + cut.setExpression("Length", _ref(body, "QIThickness")) + doc.recompute() + _checked_solid(body) + return body + + +def _dimension_expression(obj, paths): + paths = {str(path).lstrip(".") for path in paths} + return next((str(expression) for path, expression in obj.ExpressionEngine + if str(path).lstrip(".") in paths), "") + + +def _dimension_source(obj, name): + body = _parent_body(obj) + target = body if body and name in body.PropertiesList else obj + if (target == body and name.startswith("QI") and "QIParameters" in body.PropertiesList + and body.QIParameters is not None and name in body.QIParameters.PropertiesList + and _dimension_expression(body, (name,)) == body.QIParameters.Name + "." + name): + return body.QIParameters + return target + + +def _dimension_metadata(row, expression="", reference=False): + row.update({"read_only": bool(expression or reference), "expression": expression, + "reason": "Справочный размер" if reference else "Управляется выражением" if expression else ""}) + return row + + +def get_dimensions(obj): + """Return typed parameters, including read-only formulas and reference dimensions.""" + body = _parent_body(obj) + target = body if body and any(n in body.PropertiesList for n in _DIM_LABELS if n.startswith("QI")) else obj + rows = [] + for name, label in _DIM_LABELS.items(): + if name.startswith("QI") and name in target.PropertiesList: + value = getattr(target, name) + source = _dimension_source(obj, name) + rows.append(_dimension_metadata( + {"name": name, "label": label, "value": float(value.Value), "unit": "мм", + "kind": "property", "signed": False}, _dimension_expression(source, (name,)))) + if rows: + return rows + if obj.isDerivedFrom("Sketcher::SketchObject"): + for index, constraint in enumerate(obj.Constraints): + name = _constraint_alias(constraint.Name) + if name and constraint.Type in ("Distance", "DistanceX", "DistanceY", "Radius", "Diameter", "Angle", "AngleViaPoint"): + angular = constraint.Type in ("Angle", "AngleViaPoint") + paths = ("Constraints." + constraint.Name, "Constraints[%d]" % index, + "Constraints.<<%s>>" % constraint.Name) + rows.append(_dimension_metadata( + {"name": name, "label": _DIM_LABELS.get(name, name), + "value": float(obj.getDatum(index).Value), "unit": "°" if angular else "мм", + "kind": "constraint", "index": index, "constraint_type": constraint.Type, + "signed": angular or constraint.Type in ("DistanceX", "DistanceY")}, + _dimension_expression(obj, paths), not constraint.Driving)) + elif obj.isDerivedFrom("PartDesign::Feature") and "Length" in obj.PropertiesList: + rows.append(_dimension_metadata( + {"name": "Length", "label": "Длина операции", "value": float(obj.Length.Value), + "unit": "мм", "kind": "property", "signed": False}, _dimension_expression(obj, ("Length",)))) + return rows + + +def _dimension_value(value, row): + label = row["label"] + if isinstance(value, bool): + raise ValueError(label + " должен быть числом") + try: + quantity = App.Units.Quantity(value) if isinstance(value, str) else value + unit = App.Units.Unit("deg" if row["unit"] == "°" else "mm") + if hasattr(quantity, "Unit") and quantity.Unit not in (unit, App.Units.Unit()): + raise ValueError() + number = float(getattr(quantity, "Value", quantity)) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError(label + " должен быть числом в " + row["unit"]) from exc + if not row.get("signed", False): + return _positive(number, label) + if not math.isfinite(number) or abs(number) > 1e7: + raise ValueError(label + " должен быть конечным числом от −10 000 000 до 10 000 000") + return number + + +def _assign_dimension(obj, row, value): + if row["kind"] == "constraint": + unit = App.Units.Angle if row["unit"] == "°" else App.Units.Length + obj.setDatum(row["index"], App.Units.Quantity(value, unit)) + else: + setattr(_dimension_source(obj, row["name"]), row["name"], value) + + +def set_dimensions(obj, values): + """Validate the final dimensions, then apply them in one rollback/undo step.""" + values = {str(name): value for name, value in values.items()} + available = {row["name"]: row for row in get_dimensions(obj)} + unknown = set(values) - set(available) + if unknown: + raise ValueError("Не найдены управляющие размеры: " + ", ".join(sorted(unknown))) + values = {name: _dimension_value(value, available[name]) for name, value in values.items()} + values = {name: value for name, value in values.items() + if not math.isclose(value, available[name]["value"], rel_tol=1e-12, abs_tol=1e-9)} + for name in values: + if available[name]["read_only"]: + raise ValueError("Размер «%s»: %s. Измените исходный размер или выражение." % + (available[name]["label"], available[name]["reason"].lower())) + if not values: + return False + body = _parent_body(obj) + if body and hasattr(body, "QIHoleDiameter"): + dims = {n: float(getattr(body, n).Value) for n in + ("QILength", "QIWidth", "QIHoleDiameter", "QIHoleSpacing")} + dims.update({n: value for n, value in values.items() if n in dims}) + _validate_plate(dims["QILength"], dims["QIWidth"], dims["QIHoleDiameter"], dims["QIHoleSpacing"]) + with _transaction(obj.Document, "Изменить размеры"): + for name, value in values.items(): + _assign_dimension(obj, available[name], value) + obj.Document.recompute() + if body and body.Tip and not body.Tip.isDerivedFrom("Sketcher::SketchObject"): + _checked_solid(body) + sketches = [o for o in body.Group if o.isDerivedFrom("Sketcher::SketchObject")] if body else [obj] + for sketch in sketches: + if sketch.isDerivedFrom("Sketcher::SketchObject") and sketch.solve() != 0: + raise ValueError("Невозможно решить ограничения с заданным размером") + return True + + +def set_dimension(obj, name, value): + set_dimensions(obj, {name: value}) + + +list_named_dimensions = get_dimensions +set_named_dimension = set_dimension + + +def set_material(obj, name): + if name not in MATERIALS: + raise ValueError("Неизвестный материал. Доступны: " + ", ".join(MATERIALS)) + obj = _parent_body(obj) or obj + with _transaction(obj.Document, "Материал"): + _property(obj, "QIMaterial", name, "App::PropertyString", "Материал") + _property(obj, "QIDensity", MATERIALS[name], "App::PropertyFloat", "Плотность, кг/м³") + if App.GuiUp and "ShapeColor" in obj.ViewObject.PropertiesList: + colors = {"Сталь": (0.72, 0.75, 0.8), "Алюминий": (0.82, 0.85, 0.88), + "Латунь": (0.82, 0.67, 0.27), "Титан": (0.54, 0.57, 0.65), + "ABS": (0.2, 0.42, 0.73), "PLA": (0.7, 0.8, 0.4), "Дерево": (0.64, 0.4, 0.2)} + obj.ViewObject.ShapeColor = colors[name] + + +def mass_properties(obj): + """Native solid/assembly properties, with each leaf's own material.""" + import qi_bom + return qi_bom.mass_properties(obj) + + +def _model_objects(doc): + assembly = doc.getObject("Assembly") + if assembly: + return [o for o in assembly.Group + if o.isDerivedFrom("App::Link") + or o.isDerivedFrom("Assembly::AssemblyLink") + or o.isDerivedFrom("Assembly::AssemblyObject")] + objects = [o for o in doc.Objects if o.isDerivedFrom("PartDesign::Body") + and not o.Shape.isNull() and len(o.Shape.Solids)] + if not objects: + objects = [o for o in doc.Objects if o.isDerivedFrom("Part::Feature") + and not o.Shape.isNull() and len(o.Shape.Solids) and not _parent_body(o)] + return objects + + +def add_component(assembly, source, placement=None, name=None): + """Create an actual App::Link instance, not a copied mesh or frozen solid.""" + if not assembly.isDerivedFrom("App::Part"): + raise ValueError("Выберите контейнер сборки") + with _transaction(assembly.Document, "Компонент сборки"): + link = assembly.Document.addObject("App::Link", "Component") + link.setLink(source) + link.Label = name or source.Label + assembly.addObject(link) + if placement is not None: + link.LinkPlacement = placement + _show(link) + return link + + +def _joint_module(): + module_path = str(Path(App.getHomePath()) / "Mod" / "Assembly") + if module_path not in sys.path: + sys.path.append(module_path) + import JointObject + return JointObject + + +def _assembly_members(assembly, components): + """Validate origin-based joint inputs before starting a document mutation.""" + if assembly is None or not assembly.isDerivedFrom("Assembly::AssemblyObject"): + raise ValueError("Требуется сборка Assembly") + for component in components: + if (component is None or component.Document != assembly.Document + or component not in assembly.Group or not hasattr(component, "Placement")): + raise ValueError("Выберите компоненты выбранной сборки") + if len({obj.Name for obj in components}) != len(components): + raise ValueError("Выберите два разных компонента для соединения") + group = next((o for o in assembly.Group if o.isDerivedFrom("Assembly::JointGroup")), None) + if group is None: + raise ValueError("В сборке отсутствует группа соединений") + return group + + +def _check_fixed_joint_residuals(group): + """Verify origin-based Fixed joints even when the native solver reports zero. + + Check earlier joints too: satisfying a new constraint must not silently break + an existing one. Native face/edge connector semantics remain upstream-owned. + """ + import UtilsAssembly + for joint in group.Group: + if getattr(joint, "JointType", "") != "Fixed" or getattr(joint, "Suppressed", False): + continue + references = (joint.Reference1, joint.Reference2) + if any(not reference or reference[0] is None for reference in references): + raise ValueError("Соединение «%s» не содержит двух компонентов" % joint.Label) + if any(any(subname for subname in reference[1]) for reference in references): + continue + first = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Reference1) + second = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Reference2) + if ((first.Base - second.Base).Length > 1e-6 + or not first.Rotation.isSame(second.Rotation, 1e-7)): + raise ValueError("Соединение «%s» конфликтует с положением компонентов. Проверьте существующие соединения." % joint.Label) + + +def ground_component(assembly, component): + """Use the native Assembly workbench grounded joint and its solver.""" + group = _assembly_members(assembly, [component]) + module = _joint_module() + with _transaction(assembly.Document, "Зафиксировать компонент"): + joint = group.newObject("App::FeaturePython", "GroundedJoint") + joint.Label = "Основание закреплено" + module.GroundedJoint(joint, component) + if App.GuiUp: + module.ViewProviderGroundedJoint(joint.ViewObject) + return joint + + +def create_joint(assembly, first, second, kind="Fixed", offset=None): + """Make a native joint between component origins, with optional first offset. + + Geometric face/edge mates remain available in the upstream Assembly tools. + An origin-based joint is associative to its components and can be edited there. + """ + group = _assembly_members(assembly, [first, second]) + module = _joint_module() + if kind not in module.JointTypes: + raise ValueError("Неизвестный тип соединения " + str(kind)) + with _transaction(assembly.Document, "Соединение компонентов"): + joint = group.newObject("App::FeaturePython", "Joint") + module.Joint(joint, module.JointTypes.index(kind)) + joint.Label = "Жёсткое соединение" if kind == "Fixed" else "Вращательное соединение" if kind == "Revolute" else kind + if App.GuiUp: + module.ViewProviderJoint(joint.ViewObject) + joint.Offset1 = offset or App.Placement() + refs = [(first, ["", ""]), (second, ["", ""])] + joint.Proxy.setJointConnectors(joint, refs) + assembly.Document.recompute() + status = assembly.solve(False) + if status != 0: + raise ValueError("Не удалось решить соединение компонентов (код %s). Проверьте закрепление и совместимость соединений." % status) + _check_fixed_joint_residuals(group) + return joint + + +def create_assembly_demo(doc=None): + doc = doc or new_assembly("Опора в сборе") + with _transaction(doc, "Пример сборки"): + assembly = doc.getObject("Assembly") + if assembly is None: + assembly = doc.addObject("Assembly::AssemblyObject", "Assembly") + assembly.Type = "Assembly" + assembly.newObject("Assembly::JointGroup", "Joints") + assembly.Label = "Опора в сборе" + _property(assembly, "QIKind", "Assembly", "App::PropertyString") + library = doc.addObject("App::Part", "Definitions") + library.Label = "Определения деталей" + plate = create_plate_with_holes(120, 60, 8, 12, 80, doc) + plate.QIPartNumber = "QI-001" + set_material(plate, "Алюминий") + pin = create_cylinder(5.8, 35, doc) + pin.Label = "Штифт" + pin.QIPartNumber = "QI-002" + for body in (plate, pin): + library.addObject(body) + _hide(body) + _hide(library) + base = add_component(assembly, plate, App.Placement(), "Основание") + ground_component(assembly, base) + for x in (20, 100): + placement = App.Placement(App.Vector(x, 30, 0), App.Rotation()) + link = add_component(assembly, pin, placement, "Штифт") + create_joint(assembly, base, link, "Fixed" if x == 20 else "Revolute", placement) + doc.recompute() + return doc + + +def bom_rows(doc, mode="flat", root=None): + """Leaf totals by default, or a structured native assembly composition.""" + import qi_bom + return qi_bom.bom_rows(doc, mode=mode, root=root) + + +def export_bom(doc, path, mode="flat", root=None): + import qi_bom + return qi_bom.export_bom(doc, path, mode=mode, root=root) + + +def create_drawing(doc, scale=None): + """Create native A4 TechDraw page with three associative projections.""" + import TechDraw # noqa: F401; registers native document object classes + if App.GuiUp: + # Register the TechDraw view providers BEFORE creating the objects. + # Loading TechDrawGui only during export leaves generic, unpainted views. + import TechDrawGui # noqa: F401 + objects = _validate_export(doc, _model_objects(doc), False) + if not objects: + raise ValueError("Для чертежа создайте деталь или сборку") + assembly = doc.getObject("Assembly") + if assembly is not None and assembly.TypeId == "Assembly::AssemblyObject": + # Keep the native assembly as the associative source. A snapshot of + # its current children would miss later insertions and removals. + shape = assembly.Shape + if not shape.isNull() and _only_solid_geometry(shape): + objects = _validate_export(doc, [assembly], True) + if scale is not None: + scale = _positive(scale, "Масштаб") + local_templates = [Path(__file__).parent / "resources" / "qinventor-a4.svg", + Path(__file__).resolve().parents[2] / "resources" / "qinventor-a4.svg"] + template = next((p for p in local_templates if p.is_file()), None) + if template is None: + candidates = list((Path(App.getResourceDir()) / "Mod" / "TechDraw" / "Templates").glob("*A4*Landscape*.svg")) + if not candidates: + raise FileNotFoundError("Не найден шаблон A4 TechDraw в runtime FreeCAD") + template = candidates[0] + bb = Part.makeCompound([o.Shape for o in objects]).BoundBox + raw_fit = min(1.0, 85.0 / max(bb.XLength, bb.YLength, 1), 65.0 / max(bb.ZLength, bb.YLength, 1)) + preferred = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.4, 0.5, 1.0] + fitting = [candidate for candidate in preferred if candidate <= raw_fit + 1e-12] + if scale is None and not fitting: + raise ValueError("Для большой модели задайте масштаб чертежа вручную") + effective_scale = scale if scale is not None else max(fitting) + scale_text = "%.6g:1" % effective_scale if effective_scale >= 1 else "1:%.6g" % (1 / effective_scale) + scoped_rows = {} + for obj in objects: + for row in bom_rows(doc, root=obj): + scoped_rows.setdefault((row["source_id"], row["variant_id"]), row) + rows = list(scoped_rows.values()) + if not rows: + raise ValueError("В источниках чертежа нет твёрдых деталей") + materials = sorted({row["material"] for row in rows}) + drawing_material = materials[0] if len(materials) == 1 else "См. спецификацию" + with _transaction(doc, "Чертеж A4"): + page = doc.addObject("TechDraw::DrawPage", "Drawing") + page.Label = "Чертеж — " + doc.Label + _property(page, "QIManagedDrawing", True, "App::PropertyBool") + page.setEditorMode("QIManagedDrawing", 2) + svg = doc.addObject("TechDraw::DrawSVGTemplate", "DrawingTemplate") + svg.Template = str(template) + texts = dict(svg.EditableTexts) + updates = {"Title": doc.Label, "Designation": rows[0]["part_number"] if len(rows) == 1 else "QI-СБ", + "Material": drawing_material, "Scale": scale_text} + for name, value in updates.items(): + if name in texts: + texts[name] = value + svg.EditableTexts = texts + page.Template = svg + page.Scale = effective_scale + for name, label, direction, x, y in ( + ("FrontView", "Спереди", App.Vector(0, -1, 0), 80, 135), + ("TopView", "Сверху", App.Vector(0, 0, 1), 80, 80), + ("IsoView", "Изометрия", App.Vector(1, -1, 1), 205, 123)): + view = doc.addObject("TechDraw::DrawViewPart", name) + view.Label = label + view.Source = objects + view.Direction = direction + page.addView(view) + # addView initializes a new GUI view at the page center. Place it + # afterwards so native insertion does not overwrite the layout. + view.X = x + view.Y = y + view.ScaleType = "Page" + if App.GuiUp: + view.ViewObject.Visibility = True + doc.recompute() + if App.GuiUp: + page.ViewObject.Visibility = True + return page + + +def _prepared_projection_rect(view): + """Read native projected edges in paper mm after the readiness wait.""" + if any(flag in view.State for flag in ("Touched", "Invalid", "Error")): + raise ValueError("Проекция «%s» ещё не подготовлена" % view.Label) + edges = list(view.getVisibleEdges()) + if not edges: + raise ValueError("Проекция «%s» не содержит подготовленной геометрии" % view.Label) + box = Part.makeCompound(edges).BoundBox + rect = (float(box.XMin), float(box.YMin), float(box.XMax), float(box.YMax)) + if not all(math.isfinite(value) for value in rect): + raise ValueError("Границы проекции «%s» недопустимы" % view.Label) + # Native getVisibleEdges already applies the effective view scale. Scale + # metadata can change before these cached edges, so callers must prepare. + return rect + + +def _paper_projection_rect(view): + box = _prepared_projection_rect(view) + return (box[0] + float(view.X), box[1] + float(view.Y), + box[2] + float(view.X), box[3] + float(view.Y)) + + +def _rect_overlap(first, second, margin=2.0): + return not (first[2] + margin <= second[0] or second[2] + margin <= first[0] + or first[3] + margin <= second[1] or second[3] + margin <= first[1]) + + +def validate_drawing_bom_layout(page, layout): + """Check printed table against the native geometry and the QInventor A4.""" + width, height = float(layout["width_mm"]), float(layout["height_mm"]) + x, y = float(layout["x_mm"]), float(layout["y_mm"]) + if not all(math.isfinite(value) for value in (width, height, x, y)) or min(width, height) <= 0: + raise ValueError("Размеры таблицы спецификации недопустимы") + table = (x - width / 2, y - height / 2, x + width / 2, y + height / 2) + if not (136 <= table[0] and table[2] <= 290 and 62 <= table[1] and table[3] <= 110): + raise ValueError("Спецификация не помещается над основной надписью A4") + for view in page.Views: + if view.isDerivedFrom("TechDraw::DrawViewPart"): + box = _paper_projection_rect(view) + if _rect_overlap(table, box): + raise ValueError("Спецификация пересекает проекцию «%s». Измените расположение или масштаб видов." % view.Label) + return True + + +def plan_drawing_bom_layout(page, table_layout): + """Plan an A4 layout using the actual, already prepared native projection.""" + layout = dict(table_layout) + base = {"page_scale": float(page.Scale), "view_positions": {}, + "table_position": [float(layout["x_mm"]), float(layout["y_mm"])]} + views = [view for view in page.Views if view.isDerivedFrom("TechDraw::DrawViewPart")] + roles = {} + for view in views: + for role, prefix in (("front", "FrontView"), ("top", "TopView"), ("iso", "IsoView")): + if view.Name.startswith(prefix): + roles.setdefault(role, []).append(view) + managed = bool(getattr(page, "QIManagedDrawing", False)) + extras = [view for view in page.Views if view not in views + and not (view.TypeId == "TechDraw::DrawViewSpreadsheet" + and str(getattr(view, "QIBOMType", "")).startswith("QInventor.DrawingBOM."))] + if not managed or extras or len(views) != 3 or any(len(roles.get(role, [])) != 1 for role in ("front", "top", "iso")): + validate_drawing_bom_layout(page, layout) + return base + width, height = float(layout["width_mm"]), float(layout["height_mm"]) + x, y = base["table_position"] + table = (x - width / 2, y - height / 2, x + width / 2, y + height / 2) + if not (136 <= table[0] and table[2] <= 290 and 62 <= table[1] and table[3] <= 110): + raise ValueError("Спецификация не помещается над основной надписью A4") + slots = {"front": (24, 128, 126, 198), "top": (24, 63, 105, 122), + "iso": (136, table[3] + 5, 290, 198)} + boxes = {role: _prepared_projection_rect(roles[role][0]) for role in slots} + page_scale = _positive(float(page.Scale), "Масштаб листа") + ratio = 1.0 + for role, slot in slots.items(): + box = boxes[role] + limits = [(slot[2] - slot[0] - 4) / (box[2] - box[0]) if box[2] > box[0] else 1.0, + (slot[3] - slot[1] - 4) / (box[3] - box[1]) if box[3] > box[1] else 1.0] + limit = min(limits) + if str(roles[role][0].ScaleType) == "Page": + ratio = min(ratio, limit) + elif limit < 1: + raise ValueError("Вид «%s» с отдельным масштабом не помещается рядом со спецификацией" % roles[role][0].Label) + if ratio <= 0: + raise ValueError("На листе не осталось места для проекций и спецификации") + if ratio < 1: + preferred = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.4, 0.5, 1.0, 2.0] + candidates = [scale for scale in preferred if scale <= page_scale * ratio + 1e-12] + if not candidates: + raise ValueError("Для размещения проекций требуется меньший масштаб листа") + base["page_scale"] = max(candidates) + for role, slot in slots.items(): + view, box = roles[role][0], boxes[role] + factor = base["page_scale"] / page_scale if str(view.ScaleType) == "Page" else 1.0 + base["view_positions"][view.Name] = [ + (slot[0] + slot[2]) / 2 - (box[0] + box[2]) * factor / 2, + (slot[1] + slot[3]) / 2 - (box[1] + box[3]) * factor / 2] + scale = base["page_scale"] + base["scale_text"] = "%.6g:1" % scale if scale >= 1 else "1:%.6g" % (1 / scale) + return base + + +def _drawing_bom_present(page): + return any(view.TypeId == "TechDraw::DrawViewSpreadsheet" and + (str(getattr(view, "QIBOMType", "")).startswith("QInventor.DrawingBOM.") or + str(getattr(getattr(view, "Source", None), "QIBOMType", "")).startswith("QInventor.DrawingBOM.")) + for view in page.Views) + + +def _validate_drawing_bom_scene(page): + if _drawing_bom_present(page): + import qi_drawing_bom + qi_drawing_bom.validate_table(page, recompute=False) + + +def prepare_drawing(page, validate_bom=True): + """Build the native page scene and wait for projected edges to be painted.""" + page.Document.recompute() + from PySide import QtCore + if App.GuiUp: + import FreeCADGui as Gui + import TechDrawGui # noqa: F401 + gui_doc = Gui.getDocument(page.Document.Name) + page.ViewObject.Visibility = True + page.ViewObject.show() + # setEdit follows the same native opening path as double-clicking a page. + gui_doc.setEdit(page.Name) + for view in page.Views: + if App.GuiUp: + view.ViewObject.Visibility = True + if hasattr(view, "requestPaint"): + view.requestPaint() + # HLR and face finding are asynchronous. Both the initial projection and + # the final-source refresh need their own bounded wait: sharing five + # seconds let the first pass consume almost all of the refresh budget. + stage_timeout = 15.0 + stage_started = time.monotonic() + deadline = stage_started + stage_timeout + pool = QtCore.QThreadPool.globalInstance() + idle_turns = 0 + refreshed = False + + def diagnostics(): + views = [] + for view in page.Views: + edges = len(view.getVisibleEdges()) if hasattr(view, "getVisibleEdges") else None + views.append({"name": view.Name, "state": list(view.State), "edges": edges, + "sources": [(source.Name, list(source.State)) for source in view.OutList]}) + return "stage=%s elapsed=%.3fs workers=%d views=%s" % ( + "final" if refreshed else "initial", time.monotonic() - stage_started, + pool.activeThreadCount(), views) + + while time.monotonic() < deadline: + if App.GuiUp: + Gui.updateGui() + QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50) + # FreeCAD 1.1.4 runs HLR and face finding in the default QtConcurrent + # pool. Existing edges can still belong to the previous model revision. + # Drain worker completions and queued callbacks before reading/exporting + # the new projection; onHlrFinished can start another face-finding task. + ready = pool.activeThreadCount() == 0 and all( + not hasattr(view, "getVisibleEdges") or len(view.getVisibleEdges()) > 0 + for view in page.Views) + idle_turns = idle_turns + 1 if ready else 0 + if idle_turns >= 3: + if App.GuiUp: + Gui.updateGui() + if pool.activeThreadCount() == 0: + if not refreshed: + App.Console.PrintLog("QInventor drawing readiness: " + diagnostics() + "\n") + # A second source edit made during HLR can be consumed by + # DrawViewPart::execute without starting another projection. + # Rebuild once after draining the previous tasks so every + # view uses the final source revision, not an intermediate one. + stage_started = time.monotonic() + deadline = stage_started + stage_timeout + for view in page.Views: + if hasattr(view, "getVisibleEdges"): + view.touch() + page.Document.recompute() + refreshed = True + idle_turns = 0 + continue + App.Console.PrintLog("QInventor drawing readiness: " + diagnostics() + "\n") + if validate_bom: + _validate_drawing_bom_scene(page) + return + QtCore.QThread.msleep(5) + App.Console.PrintError("QInventor drawing readiness timeout: " + diagnostics() + "\n") + raise RuntimeError("Проекции чертежа не готовы к экспорту") + + +def _svg_has_geometry(path): + root = ET.parse(str(path)).getroot() + content = next((node for node in root.iter() if node.attrib.get("id") == "DrawingContent"), None) + if content is None: + return False + return any(node.tag.rsplit("}", 1)[-1] in ("path", "line", "circle", "ellipse", "polyline", "polygon") + for node in content.iter()) + + +def _validate_drawing_sources(page): + """Reject failed native dependencies without restricting drawing geometry.""" + try: + doc = page.Document + valid = (doc is not None and doc.Name in App.listDocuments() + and App.getDocument(doc.Name) is doc + and page.isDerivedFrom("TechDraw::DrawPage")) + except Exception: + valid = False + if not valid: + raise ValueError("Лист чертежа закрыт или не выбран") + roots = [page] + list(page.Views) + recomputed = set() + for obj in reversed(_export_dependencies(roots)): + source_doc = obj.Document + if source_doc.Name not in recomputed: + source_doc.recompute() + recomputed.add(source_doc.Name) + if doc.Name not in recomputed: + doc.recompute() + for obj in _export_dependencies(roots): + # A failed native Pad can retain a valid Shape while its Body and views + # still report Valid. Check the real source history, including Links. + # Native TechDraw also supports sketches/wires; no solid requirement. + if any(flag in obj.State for flag in ("Invalid", "Error")): + # The native BOM sheet retains its last-good cells and the precise + # failure cause. Source recompute above refreshes or clears it; + # keep that actionable diagnosis while still rejecting Invalid. + if (obj.TypeId == "Spreadsheet::SheetPython" and + str(getattr(obj, "QIBOMType", "")).startswith("QInventor.DrawingBOM.")): + error = str(getattr(obj, "QIBOMError", "")).strip() + if error: + raise ValueError(error) + raise ValueError("Объект «%s» содержит ошибку перестроения. Исправьте операцию перед экспортом чертежа." % obj.Label) + + +def export_drawing(page, path): + path = Path(path).absolute() + suffix = path.suffix.lower() + if suffix not in (".dxf", ".svg", ".pdf"): + raise ValueError("Чертеж поддерживает SVG, PDF или DXF") + _validate_drawing_sources(page) + if suffix in (".svg", ".pdf") and not App.GuiUp: + raise RuntimeError("Экспорт листа SVG/PDF требует запущенного интерфейса FreeCAD") + if App.GuiUp or suffix == ".dxf": + # Preparation can fail while waiting for the native page scene. Do it + # before making directories or touching the destination file. + prepare_drawing(page) + path.parent.mkdir(parents=True, exist_ok=True) + temporary = None + preview = None + try: + descriptor, name = tempfile.mkstemp(prefix=".qinventor-", suffix=suffix, dir=str(path.parent)) + os.close(descriptor) + temporary = Path(name) + if suffix == ".dxf": + import TechDraw + TechDraw.writeDXFPage(page, str(temporary)) + if _drawing_bom_present(page): + import qi_drawing_bom_export + qi_drawing_bom_export.complete_dxf(page, temporary) + elif suffix == ".svg": + import TechDrawGui + TechDrawGui.exportPageAsSvg(page, str(temporary)) + if not _svg_has_geometry(temporary): + raise RuntimeError("Экспорт SVG не содержит проекций детали") + elif suffix == ".pdf": + import TechDrawGui + # Verify the very same page scene before accepting a PDF containing + # only a template and title block. A unique preview avoids changing + # an existing user file with a matching stem. + descriptor, name = tempfile.mkstemp(prefix=".qinventor-scene-", suffix=".svg", dir=str(path.parent)) + os.close(descriptor) + preview = Path(name) + TechDrawGui.exportPageAsSvg(page, str(preview)) + if not _svg_has_geometry(preview): + raise RuntimeError("Лист чертежа не содержит проекций детали") + TechDrawGui.exportPageAsPdf(page, str(temporary)) + if not temporary.is_file() or temporary.stat().st_size == 0: + raise IOError("FreeCAD не создал файл чертежа") + with temporary.open("r+b") as stream: + os.fsync(stream.fileno()) + os.replace(str(temporary), str(path)) + temporary = None + finally: + for candidate in (temporary, preview): + if candidate is not None and candidate.is_file(): + candidate.unlink() + return str(path) + + +def _export_dependencies(objects): + """Follow actual native links, including Body history and linked documents.""" + pending = list(objects) + seen = set() + dependencies = [] + while pending: + obj = pending.pop() + key = (obj.Document.Name, obj.Name) + if key in seen: + continue + seen.add(key) + dependencies.append(obj) + pending.extend(obj.OutList) + return dependencies + + +def _only_solid_geometry(shape): + if shape.ShapeType == "Solid": + return True + if shape.ShapeType in ("Compound", "CompSolid"): + children = shape.childShapes() + return bool(children) and all(_only_solid_geometry(child) for child in children) + return False + + +def _validate_export(doc, objects, explicit_selection, _defer_shape_checks=False): + roots = list(objects) + if not explicit_selection: + # A failed feature may clear its Body's shape, which would otherwise be + # silently omitted from _model_objects beside the remaining good bodies. + roots.extend(o for o in doc.Objects if o.isDerivedFrom("PartDesign::Body")) + dependencies = _export_dependencies(roots) + recomputed = set() + # Source documents precede their dependent Link documents. FreeCAD permits + # external components; recomputing only the assembly can retain old sources. + for obj in reversed(dependencies): + source_doc = obj.Document + if source_doc.Name not in recomputed: + source_doc.recompute() + recomputed.add(source_doc.Name) + if doc.Name not in recomputed: + doc.recompute() + for obj in _export_dependencies(roots): + # Invalid is a native DocumentObject.State flag. A failed native Pad + # retains a valid old Shape and even its Body can report Valid. + if "Invalid" in obj.State: + raise ValueError("Объект «%s» содержит ошибку перестроения. Исправьте операцию перед продолжением." % obj.Label) + if not explicit_selection: + objects = _model_objects(doc) + if not _defer_shape_checks: + for obj in objects: + shape = getattr(obj, "Shape", None) + if (shape is None or shape.isNull() or not shape.isValid() or + not shape.Solids or shape.Volume <= 1e-9 or not _only_solid_geometry(shape)): + raise ValueError("Объект «%s» не содержит корректного твердого тела. Выберите готовую деталь." % obj.Label) + return objects + + +def _export_scene_context(obj): + """Resolve an expanded array element through its actual owning occurrence.""" + import qi_bom as bom + if obj.TypeId == "App::LinkElement": + owners = [(candidate, index) for candidate in obj.Document.Objects if candidate.TypeId == "App::Link" + for index, element in enumerate(candidate.ElementList) if element is obj] + if len(owners) != 1: + raise ValueError("Не удалось однозначно определить массив выбранного элемента") + owner, index = owners[0] + context, prefix = bom._scene_context(owner) + return context, prefix + str(index) + "." + return bom._scene_context(obj) + + +def _export_world_shapes(objects): + """Detached native solids in each selected occurrence's world coordinates.""" + import qi_bom as bom + shapes = [] + def collect(obj, context, prefix, array_element=False): + if obj.TypeId == "App::Link" and getattr(obj, "ElementCount", 0) > 0 and not array_element: + for index in range(obj.ElementCount): + if obj.isElementVisible(str(index)) == 0: + continue + elements = obj.ElementList + element = elements[index] if index < len(elements) else obj + collect(element, context, prefix + str(index) + ".", True) + return + source, chain = bom._definition_source(obj) + if bom._container(source): + container = obj if obj.TypeId == "Assembly::AssemblyLink" else source + children = [child for child in bom._physical_children(container) if bom._potential(child)] + if not children: + raise ValueError("Выбранная сборка не содержит готовых твёрдых деталей") + for child in children: + collect(child, context, prefix + child.Name + ".") + return + shape = Part.getShape(context, prefix) + if (shape.isNull() or not shape.isValid() or not shape.Solids + or shape.Volume <= 1e-9 or not _only_solid_geometry(shape)): + raise ValueError("Объект «%s» не содержит корректного твердого тела. Выберите готовую деталь." % obj.Label) + # Writers may edit shared native TShape tolerances or triangulation. + # A BRep serialization boundary detaches those caches from the source. + local = shape.copy() + placement = local.Placement + local.Placement = App.Placement() + detached = Part.Shape() + detached.importBrepFromString(local.exportBrepToString()) + detached.Placement = placement + shapes.append(detached) + for obj in objects: + bom._check_cycles(obj) + context, prefix = _export_scene_context(obj) + collect(obj, context, prefix) + if not shapes: + raise ValueError("Нет твердых тел для экспорта") + return shapes + + +def export_model(doc, path, objects=None): + path = Path(path).absolute() + suffix = path.suffix.lower() + if suffix not in (".step", ".stp", ".stl", ".iges", ".igs", ".brep", ".brp"): + raise ValueError("Поддерживаются STEP, STL, IGES и BREP") + if suffix in (".step", ".stp"): + from qi_step_export import export_step + return export_step(doc, path, objects=objects) + explicit_selection = objects is not None + objects = list(objects) if explicit_selection else _model_objects(doc) + objects = _validate_export(doc, objects, explicit_selection, _defer_shape_checks=True) + if not objects: + raise ValueError("Нет твердых тел для экспорта") + shape = Part.makeCompound(_export_world_shapes(objects)) + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, name = tempfile.mkstemp(prefix="." + path.stem + "-", suffix=path.suffix, dir=str(path.parent)) + os.close(descriptor) + temporary = Path(name) + try: + if suffix == ".stl": + import MeshPart + mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.1, AngularDeflection=0.35, Relative=False) + mesh.write(str(temporary)) + elif suffix in (".brep", ".brp"): + shape.exportBrep(str(temporary)) + else: + shape.exportIges(str(temporary)) + if not temporary.is_file() or temporary.stat().st_size == 0: + raise IOError("FreeCAD не создал файл модели") + with temporary.open("r+b") as stream: + os.fsync(stream.fileno()) + os.replace(str(temporary), str(path)) + temporary = None + return str(path) + finally: + if temporary is not None and temporary.is_file(): + temporary.unlink() + + +def save_project(doc, path): + if doc.Partial: + raise ValueError("Частично загруженный проект нельзя сохранить. Откройте проект полностью.") + path = Path(path).absolute() + if path.suffix.lower() != ".fcstd": + path = path.with_suffix(".FCStd") + path.parent.mkdir(parents=True, exist_ok=True) + doc.recompute() + label = doc.Label + if doc.saveAs(str(path)) is False: + raise IOError("FreeCAD не сохранил проект") + if doc.Label != label: + doc.Label = label + if doc.save() is False: + raise IOError("FreeCAD не сохранил проект") + if not path.is_file() or path.stat().st_size == 0: + raise IOError("FreeCAD не сохранил проект") + if App.GuiUp: + import FreeCADGui as Gui + Gui.getDocument(doc.Name).Modified = False + return str(path) + + +def make_demo(kind="bracket"): + kind = str(kind).lower() + if kind == "assembly": + return create_assembly_demo() + doc = new_part({"bracket": "Кронштейн", "plate": "Плита", "shaft": "Вал", "pulley": "Втулка"}.get(kind, "Пример")) + try: + if kind in ("bracket", "plate"): + body = create_plate_with_holes(100, 50, 8, 10, 60, doc) + body.Label = doc.Label + if kind == "bracket": + with _transaction(doc, "Стенка кронштейна"): + _property(body, "QIWallHeight", 45.0) + sketch = create_sketch_profile("rectangle", 50, 45, "YZ", doc, body) + sketch.Label = "Стенка кронштейна" + sketch.Placement.Base.z = 8 + sketch.setExpression("Placement.Base.z", _ref(body, "QIThickness")) + sketch.setExpression(_cpath("Length"), _ref(body, "QIWidth")) + sketch.setExpression(_cpath("Width"), _ref(body, "QIWallHeight")) + feature = pad(sketch, 8) + feature.setExpression("Length", _ref(body, "QIThickness")) + doc.recompute() + _checked_solid(body) + _hide(body.Group[-3]) + set_material(body, "Алюминий") + elif kind == "shaft": + body = create_cylinder(12, 100, doc) + body.Label = "Вал" + elif kind == "pulley": + body = create_cylinder(30, 20, doc) + body.Label = "Втулка" + sketch = create_sketch_profile("circle", 10, 10, doc=doc, body=body) + position_sketch_on_top(sketch, body) + pocket(sketch, 20) + else: + raise ValueError("Пример должен быть bracket, plate, shaft, pulley или assembly") + doc.recompute() + return doc + except Exception: + # Keep the document open for diagnostics and preserve any completed steps. + raise diff --git a/src/QInventor/qi_parameters.py b/src/QInventor/qi_parameters.py new file mode 100644 index 0000000..e524fc1 --- /dev/null +++ b/src/QInventor/qi_parameters.py @@ -0,0 +1,857 @@ +"""Document parameter table backed exclusively by native FreeCAD expressions. + +The table contains dimensional Sketcher constraints, CAD feature dimensions and +native VarSet variables. Names/paths are native identities; labels are display +text. All values use FreeCAD base units (millimetres, degrees, dimensionless). +""" +import json +import io +import math +import re +import xml.etree.ElementTree as ET +import zipfile + +import FreeCAD as App + + +_TYPES = { + "App::PropertyLength": "length", "App::PropertyDistance": "length", + "App::PropertyAngle": "angle", "App::PropertyFloat": "scalar", + "App::PropertyInteger": "scalar", "App::PropertyFloatConstraint": "scalar", + "App::PropertyIntegerConstraint": "scalar", +} +_UNITS = {"length": "мм", "angle": "°", "scalar": ""} +_NATIVE_UNITS = {"length": "mm", "angle": "deg", "scalar": ""} +_DIMENSIONAL_CONSTRAINTS = {"Distance", "DistanceX", "DistanceY", "Radius", "Diameter", "Angle", "AngleViaPoint", "SnellsLaw"} +_SCALARS = {"Occurrences", "Occurrences2", "Number", "NumberOfOccurrences"} +_POSITIVE = {"Length", "Width", "Height", "Radius", "Radius1", "Radius2", "Radius3", + "Diameter", "Depth", "Size", "Size2", "Thickness"} +_USER_OBJECT = "QIUserParameters" +_USER_MARKER = "QIUserParameterSet" +_COMMENTS = "QIParameterComments" +_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +_QUALIFIED_REF = re.compile( + r'''(?P"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|''' + r'''(?P<<.*?>>|[^\W\d]\w*)#(?P<<.*?>>|[^\W\d]\w*)|''' + r'''(?P