# components.schemas.AlignmentOptionsRequest.description
Опции выравнивания модели по скану.

# components.schemas.AlignmentOptionsRequest.properties.coordinate_system.description
Система координат входных alignment точек: 'y-up' (по умолчанию) или 'z-up'. При 'z-up' сервер нормализует alignment в Y-up.

* `y-up` - y-up
* `z-up` - z-up

# components.schemas.AlignmentOptionsRequest.properties.corners.description
4 точки проёма [BL, BR, TR, TL] в формате [[x,y,z], ...]. Для multipart допускается JSON-строка.

# components.schemas.AlignmentOptionsRequest.properties.point_1.description
Координаты первой точки [x, y, z] в метрах

# components.schemas.AlignmentOptionsRequest.properties.point_2.description
Координаты второй точки [x, y, z] в метрах

# components.schemas.AlignmentOptionsRequest.properties.wall_normal.description
Нормаль стены [x, y, z]. Для multipart допускается JSON-строка.

# components.schemas.BrandingRequest.description
Брендирование AR-интерфейса

# components.schemas.BrandingRequest.properties.company_name.description
Название компании для отображения

# components.schemas.BrandingRequest.properties.logo_file.description
Файл логотипа клиента. Разрешены raster-форматы PNG, JPG/JPEG, WEBP, GIF, BMP, TIFF. SVG в v1 не поддерживается. Минимум 64x64 px; допустимое соотношение сторон от 1:4 до 4:1; максимальный upload 15 MB; максимум decoded image 25 MP. Большие изображения сервер уменьшает сам до max side 512 px и сохраняет как PNG.

# components.schemas.BrandingRequest.properties.primary_color.description
Основной цвет интерфейса в формате #RRGGBB

# components.schemas.CallbackEventRequest.description
Настройки callback для одного события

# components.schemas.CallbackEventRequest.properties.params.description
Параметры, которые будут добавлены к URL при вызове

# components.schemas.CallbackEventRequest.properties.url.description
URL или deeplink для вызова (https://... или myapp://...)

# components.schemas.CallbackRequest.description
Callback-и для событий AR-сессии

# components.schemas.CallbackRequest.properties.custom_data.description
Произвольные данные клиента (будут добавлены ко всем callback-ам)

# components.schemas.CallbackRequest.properties.on_cancel_ar_session.description
Вызвать при отмене AR-сессии пользователем

# components.schemas.CallbackRequest.properties.on_error_ar_session.description
Вызвать при ошибке AR-сессии

# components.schemas.CallbackRequest.properties.on_finish_ar_session.description
Вызвать при успешном завершении AR-сессии

# components.schemas.CallbackRequest.properties.on_return_to_client.description
Вызвать при возврате пользователя в клиентское приложение

# components.schemas.CallbackRequest.properties.on_start_ar_session.description
Вызвать при запуске AR-сессии на устройстве

# components.schemas.CallbackRequest.properties.return_from_arsession_appname.description
Название приложения для возврата после завершения AR-сессии.

# components.schemas.CallbackRequest.properties.return_from_arsession_url.description
URL или deeplink для возврата после завершения AR-сессии (например: https://... или myapp://...).

# components.schemas.ClientInfoRequest.description
Информация о клиентском приложении

# components.schemas.ClientInfoRequest.properties.app.description
Идентификатор приложения-клиента (например: 'revit-plugin', 'web-viewer')

# components.schemas.ClientInfoRequest.properties.project_name.description
Название проекта

# components.schemas.ClientInfoRequest.properties.version.description
Версия приложения-клиента (например: '2.1.0')

# components.schemas.ConversionRequest.description
DRF serializer that rejects unknown fields instead of dropping them.

# components.schemas.ConversionRequest.properties.mode.description
Прямая конвертация без упрощения: IFC→GLB (IfcOpenShell), GLB→USDZ (usd-core), USDZ→Reality через обязательный callback конвертера. Сессия получает ready только после сохранения Reality.

* `fast` - fast

# components.schemas.ErrorResponse.description
Стандартный формат ошибки

# components.schemas.ErrorResponse.properties.details.description
Детали ошибки по полям

# components.schemas.ErrorResponse.properties.error.description
Описание ошибки

# components.schemas.GipVisionDrawingRemarkRead.properties.marker_size_ratio.description
Размер маркера как доля меньшей стороны канонического неповёрнутого файла или страницы.

# components.schemas.GipVisionProjectUser.properties.username.description
Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.

# components.schemas.GipVisionTask.properties.ar_project.title
UUID проекта

# components.schemas.GipVisionTask.properties.priority.description
* `low` - Низкий
* `normal` - Обычный
* `high` - Высокий
* `critical` - Критичный

# components.schemas.GipVisionTaskTag.properties.group.description
* `record_type` - Тип записи
* `source` - Источник
* `custom` - Пользовательский

# components.schemas.GipVisionTaskWorkOrderAsset.properties.display_name.title
Название файла

# components.schemas.GipVisionTaskWorkOrderAsset.properties.file_size.title
Размер файла

# components.schemas.GipVisionTaskWorkOrderAsset.properties.folder.title
Папка проекта

# components.schemas.GipVisionTaskWorkOrderAsset.properties.original_filename.title
Исходное имя файла

# components.schemas.HealthCheckResponse.properties.scopes.description
Доступные scope для валидного X-API-Key

# components.schemas.HealthCheckResponse.properties.user.description
Имя пользователя для валидного X-API-Key

# components.schemas.IfcSitePlacementRequest.properties.coordinate_system.description
Система координат именно для чисел translation и rotation_deg. Используйте 'z-up', если значения взяты из IfcSite.ObjectPlacement обычного IFC/Revit. Используйте 'y-up' только если вы сами заранее пересчитали эти значения в систему координат GLB/GIP Vision. Если GLB уже находится в правильных локальных координатах, не передавайте ifc_site_placement вообще: сервер примет файл как есть.

* `y-up` - y-up
* `z-up` - z-up

# components.schemas.IfcSitePlacementRequest.properties.rotation_deg.description
Поворот исходного IfcSite относительно локальных осей модели. Формат значения: Euler XYZ в градусах [rx, ry, rz]. Если в IFC поворот задан через Axis/RefDirection, передайте эквивалентный Euler-поворот. Для обычного горизонтального поворота IFC вокруг вертикальной оси в z-up заполняется rz, а rx и ry остаются нулевыми. Если у IfcSite нет поворота, поле можно не передавать; будет использовано [0, 0, 0].

# components.schemas.IfcSitePlacementRequest.properties.translation.description
Смещение исходного IfcSite относительно локального начала модели. Заполняйте из IfcSite.ObjectPlacement.RelativePlacement.Location.Coordinates. Формат значения: массив [x, y, z]. Если значения скопированы из IFC/Revit, обычно нужно оставить units='mm' и coordinate_system='z-up'. Сервер применит к GLB обратное смещение, чтобы привести модель к локальным координатам.

# components.schemas.IfcSitePlacementRequest.properties.units.description
Единицы измерения значений translation. Используйте 'mm', если координаты скопированы из IFC/Revit в миллиметрах. Используйте 'm', если вы заранее перевели координаты в метры. По умолчанию используется 'mm'.

* `mm` - mm
* `m` - m

# components.schemas.PublicModelFile.properties.display_name.title
Отображаемое имя файла

# components.schemas.PublicModelFile.properties.file_size.description
Размер исходного файла в байтах.

# components.schemas.PublicModelFile.properties.file_type.description
Тип исходного файла: ifc или glb.

# components.schemas.PublicModelIfcUploadRequest.properties.conversion.description
Опциональные настройки конвертации. Если не передано, используется fast mode.

# components.schemas.PublicModelIfcUploadRequest.properties.display_name.description
Отображаемое имя модели.

# components.schemas.PublicModelIfcUploadRequest.properties.model_file.description
IFC-файл для загрузки и конвертации.

# components.schemas.PublicProject.properties.is_archived.description
Archived projects are hidden from normal views but can be restored

# components.schemas.PublicProject.properties.is_favorite.description
Отметить проект как избранный

# components.schemas.PublicProject.properties.is_marking_km_project.description
Если включено, IFC-модель проекта может синхронизироваться в модуль GIP Vision Marking KM

# components.schemas.PublicProject.properties.is_marking_km_project.title
Проект маркировки КМ

# components.schemas.PublicProject.properties.is_sharable.description
Если включено, проект доступен без авторизации в GIP VISION, но не попадает в публичные витрины

# components.schemas.PublicProject.properties.is_sharable.title
Доступен по публичной ссылке

# components.schemas.PublicProject.properties.object_uuid.title
UUID проекта

# components.schemas.PublicProject.properties.project_name.title
Наименование проекта

# components.schemas.PublicProject.properties.project_type.description
Режим проекта: обычный AR, СОД или маркировка КМ

* `ar` - AR
* `sod` - SOD
* `km_marking` - KM marking

# components.schemas.PublicProject.properties.project_type.title
Тип AR-проекта

# components.schemas.PublicProjectCreateRequest.properties.billing_scope.description
Billing scope: auto выбирает активный workspace, personal — личную квоту, company — квоту workspace.

* `auto` - auto
* `personal` - personal
* `company` - company

# components.schemas.PublicProjectCreateRequest.properties.is_sharable.description
Разрешить шаринг проекта.

# components.schemas.PublicProjectCreateRequest.properties.is_sharable.title
Доступен по публичной ссылке

# components.schemas.PublicProjectCreateRequest.properties.project_name.description
Название создаваемого GIP Vision проекта.

# components.schemas.PublicProjectCreateRequest.properties.project_name.title
Наименование проекта

# components.schemas.PublicProjectCreateRequest.properties.project_type.description
Основной тип проекта.

* `ar` - AR
* `sod` - SOD
* `km_marking` - KM marking

# components.schemas.PublicTaskBcfExportRequest.properties.board_id.description
Опционально ограничить экспорт доской.

# components.schemas.PublicTaskBcfExportRequest.properties.task_ids.description
UUID задач для экспорта. Если не передано, экспортируются задачи проекта или доски.

# components.schemas.PublicTaskBcfImportRequest.properties.board_id.description
Доска для импортированных задач.

# components.schemas.PublicTaskBcfImportRequest.properties.stage_id.description
Колонка для импортированных задач.

# components.schemas.PublicTaskBoardCreateRequest.properties.title.description
Название доски задач.

# components.schemas.PublicTaskCreateRequest.properties.kind.description
Тип задачи. Public API поддерживает только обычные задачи и наряды; замечания на чертежах создаются только из приватного интерфейса СОД.

* `general` - Обычная задача
* `work_order` - Наряд на работу

# components.schemas.PublicTaskCreateRequest.properties.priority.description
* `low` - Низкий
* `normal` - Обычный
* `high` - Высокий
* `critical` - Критичный

# components.schemas.PublicTaskCreateRequest.properties.stage_id.description
Колонка доски. Если не передана, используется первая активная.

# components.schemas.PublicTaskCreateRequest.properties.title.description
Название задачи.

# components.schemas.SessionDiagnosticSnapshotRequest.properties.battery_state.description
* `unknown` - unknown
* `unplugged` - unplugged
* `charging` - charging
* `full` - full

# components.schemas.SessionDiagnosticSnapshotRequest.properties.thermal_state.description
* `unknown` - unknown
* `nominal` - nominal
* `fair` - fair
* `serious` - serious
* `critical` - critical

# components.schemas.SessionDiagnosticSnapshotRequest.properties.tracking_limited_reason.description
* `unknown` - unknown
* `initializing` - initializing
* `excessive_motion` - excessive_motion
* `insufficient_features` - insufficient_features
* `relocalizing` - relocalizing

# components.schemas.SessionDiagnosticSnapshotRequest.properties.tracking_state.description
* `unknown` - unknown
* `normal` - normal
* `limited` - limited
* `not_available` - not_available

# components.schemas.SessionDiagnosticSnapshotRequest.properties.world_mapping_status.description
* `unknown` - unknown
* `not_available` - not_available
* `limited` - limited
* `extending` - extending
* `mapped` - mapped

# components.schemas.SessionDiagnosticSummaryRequest.properties.max_thermal_state.description
* `unknown` - unknown
* `nominal` - nominal
* `fair` - fair
* `serious` - serious
* `critical` - critical

# components.schemas.SessionResolveByCodeRequestRequest.properties.onetime_code.description
Одноразовый код из ответа POST /session/by_*

# components.schemas.SessionResolveByCodeResponse.properties.expires_at.description
Время истечения (UTC) для сессии или действующего spatial-кода.

# components.schemas.SessionResolveByCodeResponse.properties.spatial_project_id.description
ID публичного SpatialChunkProject (для публичных проектов)

# components.schemas.SessionResponse.description
Базовый ответ при успешном создании сессии

# components.schemas.SessionResponse.properties.deeplink.description
Deeplink для открытия в GIP Vision (gipvision://session/...)

# components.schemas.SessionResponse.properties.expires_at.description
Время истечения сессии (UTC) - подготовленные данные будут храниться на серверах GIP VISION 24 часа, после чего удалены, нужно повторно создать запрос на AR-session

# components.schemas.SessionResponse.properties.onetime_code.description
Одноразовый код (6 цифр) для получения deeplink

# components.schemas.SessionScanRequestRequest.description
POST /api/gip-vision/v1/session/by_scan/

Создание AR-сессии с размещением модели по 3D-скану помещения (LiDAR).
Режим LiDARScanner — модель привязывается к координатам скана.

# components.schemas.SessionScanRequestRequest.properties.alignment.description
Опции выравнивания модели по скану. Нужно передать один из режимов: point_1+point_2 или corners+wall_normal. coordinate_system='z-up' автоматически нормализуется в Y-up.

# components.schemas.SessionScanRequestRequest.properties.branding.description
Брендирование AR-интерфейса

# components.schemas.SessionScanRequestRequest.properties.callback.description
Параметры возврата в приложение-источник

# components.schemas.SessionScanRequestRequest.properties.client.description
Информация о клиентском приложении

# components.schemas.SessionScanRequestRequest.properties.conversion.description
Параметры конвертации и упрощения модели (fast/custom)

# components.schemas.SessionScanRequestRequest.properties.ifc_site_placement.description
Опциональная нормализация GLB по параметрам IfcSite.ObjectPlacement. Передавайте это поле только для GLB, который был экспортирован из IFC с глобальным смещением или поворотом IfcSite, и это смещение уже запечено в GLB. Сервер применит обратную трансформацию и сохранит модель в локальных координатах. Если GLB уже локальный и корректно ориентирован, не передавайте это поле. Для IFC uploads поле не нужно: сервер сам читает IfcSite.ObjectPlacement из IFC. Для USDZ uploads поле не применяется.

# components.schemas.SessionScanRequestRequest.properties.model_file.description
Один файл 3D модели (GLB, IFC, USDZ). Нельзя использовать одновременно с model_files. Для GLB публичный API принимает максимум 5000 mesh-node элементов.

# components.schemas.SessionScanRequestRequest.properties.model_files.description
Несколько файлов 3D модели. Передавайте multipart parts с именем model_files. Разрешены GLB и IFC. Нельзя использовать одновременно с model_file. Каждый GLB должен содержать не больше 5000 mesh-node элементов.

# components.schemas.SessionScanRequestRequest.properties.ui.description
Session-level UI override для RoomCapture. Для multipart передавайте ui.presentation JSON-строкой; пример скрытия кнопки: {'buttons': {'roomcapture_ar_mode': false}}.

# components.schemas.SessionUIButtonPatchRequest.properties.active_color.description
Код цвета из functions/spec colors.

* `red` - Красный
* `orange` - Оранжевый
* `yellow` - Желтый
* `green` - Зеленый
* `mint` - Мятный
* `teal` - Бирюзовый
* `cyan` - Циан
* `blue` - Синий
* `indigo` - Индиго
* `purple` - Фиолетовый
* `pink` - Розовый
* `brown` - Коричневый
* `gray` - Серый
* `graphite` - Графитовый
* `slate` - Сланцевый
* `steel` - Стальной
* `sky` - Небесный
* `azure` - Лазурный
* `navy` - Темно-синий
* `violet` - Виолетовый
* `lavender` - Лавандовый
* `magenta` - Маджента
* `rose` - Розово-красный
* `coral` - Коралловый
* `amber` - Янтарный

# components.schemas.SessionUIButtonPatchRequest.properties.startup_enabled.description
Стартовое состояние функции при открытии AR-сессии.

# components.schemas.SessionUIButtonPatchRequest.properties.visible.description
Показать кнопку на основной ленте. false скрывает с ленты, но оставляет в overflow.

# components.schemas.SessionUIPresentationPatchRequest.properties.device.description
Если не передано, patch применяется ко всем iPhone/iPad ориентациям.

* `iphone_portrait` - iphone_portrait
* `iphone_landscape` - iphone_landscape
* `ipad_portrait` - ipad_portrait
* `ipad_landscape` - ipad_landscape

# components.schemas.SessionUIPresentationPatchRequest.properties.mode.description
* `ribbon` - ribbon
* `contour` - contour

# components.schemas.SessionUIPresentationTargetRequest.properties.device.description
Если не передано, patch применяется ко всем iPhone/iPad ориентациям.

* `iphone_portrait` - iphone_portrait
* `iphone_landscape` - iphone_landscape
* `ipad_portrait` - ipad_portrait
* `ipad_landscape` - ipad_landscape

# components.schemas.SessionUIPresentationTargetRequest.properties.mode.description
* `ribbon` - ribbon
* `contour` - contour

# components.schemas.SessionUISettingsRequest.properties.presentation.description
Короткий session-level patch для кнопок RoomCapture. Самый простой вариант: {'buttons': {'roomcapture_ar_mode': false}} — скрывает кнопку с основной ленты, но оставляет функцию доступной через overflow. Для multipart передавайте это поле как JSON-строку в ui.presentation. Без device/mode patch применяется ко всем iPhone/iPad ориентациям в mode=ribbon. Session-level startup_enabled имеет приоритет над persistent startup_enabled для разрешённой функции. Session-level контракт разрешает только visible, startup_enabled и active_color; раскладка берётся из persistent presentation v4.

# components.schemas.SessionUsageFeedback.properties.comment.description
Необязательный комментарий разработчику, до 2000 символов.

# components.schemas.SessionUsageFeedback.properties.rating.description
Оценка качества AR-сессии от 1 до 5.

# components.schemas.SessionUsageFeedbackRequest.properties.comment.description
Необязательный комментарий разработчику, до 2000 символов.

# components.schemas.SessionUsageFeedbackRequest.properties.rating.description
Оценка качества AR-сессии от 1 до 5.

# components.schemas.SessionUsageFinishRequestRequest.properties.diagnostics_final.description
Финальное системное состояние сессии.

# components.schemas.SessionUsageFinishRequestRequest.properties.diagnostics_windows.description
Оставшиеся неподтверждённые окна, суммарно до 16 KiB.

# components.schemas.SessionUsageFinishRequestRequest.properties.event_at.description
Фактическое время завершения на клиенте. Если не передано, используется серверное время.

# components.schemas.SessionUsageFinishRequestRequest.properties.feedback.description
Оценка и необязательный комментарий при выходе из AR-сессии.

# components.schemas.SessionUsageFinishRequestRequest.properties.status.description
Итоговый статус использования AR-сессии.

* `finished` - finished
* `cancelled` - cancelled
* `error` - error

# components.schemas.SessionUsageFinishResponse.properties.duration_seconds.description
Длительность использования AR-сессии в секундах.

# components.schemas.SessionUsageFinishResponse.properties.ended_at.description
Время завершения использования AR-сессии.

# components.schemas.SessionUsageFinishResponse.properties.feedback.description
Сохранённый отзыв или null, если отзыв не отправляли.

# components.schemas.SessionUsageFinishResponse.properties.started_at.description
Время старта использования AR-сессии.

# components.schemas.SessionUsageFinishResponse.properties.status.description
Итоговый статус использования AR-сессии.

* `finished` - finished
* `cancelled` - cancelled
* `error` - error
* `timed_out` - timed_out

# components.schemas.SessionUsageFinishResponse.properties.usage_id.description
ID использования AR-сессии.

# components.schemas.SessionUsageHeartbeatRequestRequest.properties.diagnostics_windows.description
Неподтверждённые компактные диагностические окна, суммарно до 16 KiB.

# components.schemas.SessionUsageHeartbeatRequestRequest.properties.event_at.description
Фактическое время heartbeat на клиенте. Если не передано, используется серверное время.

# components.schemas.SessionUsageStartRequestRequest.properties.client_session_id.description
Стабильный UUID клиентского запуска для идемпотентного повторного start.

# components.schemas.SessionUsageStartRequestRequest.properties.diagnostics_initial.description
Начальное системное состояние без кадров и координат.

# components.schemas.SessionUsageStartRequestRequest.properties.event_at.description
Фактическое время старта на клиенте. Если не передано, используется серверное время.

# components.schemas.SessionUsageStartRequestRequest.properties.model_file_id.description
ID ModelFile, для которого начинается AR-сессия.

# components.schemas.SessionUsageStartRequestRequest.properties.session_type.description
Тип запуска использования модели на клиенте.

* `by_scan` - by_scan

# components.schemas.SessionUsageStartRequestRequest.properties.spatial_project_id.description
Опциональный ID SpatialChunkProject, если использование связано с конкретным spatial project.

# components.schemas.SessionUsageStartResponse.properties.ar_project_id.description
ID ARProject, если usage связан с существующим проектом.

# components.schemas.SessionUsageStartResponse.properties.model_file_id.description
ID ModelFile, если usage связан с существующей моделью.

# components.schemas.SessionUsageStartResponse.properties.started_at.description
Время старта использования AR-сессии.

# components.schemas.SessionUsageStartResponse.properties.usage_id.description
ID использования AR-сессии.

# components.schemas.SessionVirtualAnchorCircleRequestRequest.description
Базовые поля для всех session запросов — источник модели

# components.schemas.SessionVirtualAnchorCircleRequestRequest.properties.branding.description
Брендирование AR-интерфейса.

# components.schemas.SessionVirtualAnchorCircleRequestRequest.properties.callback.description
Параметры возврата в приложение-источник.

# components.schemas.SessionVirtualAnchorCircleRequestRequest.properties.client.description
Информация о клиентском приложении.

# components.schemas.SessionVirtualAnchorCircleRequestRequest.properties.ifc_site_placement.description
Опциональная нормализация GLB по параметрам IfcSite.ObjectPlacement. Передавайте это поле только для GLB, который был экспортирован из IFC с глобальным смещением или поворотом IfcSite, и это смещение уже запечено в GLB. Сервер применит обратную трансформацию и сохранит модель в локальных координатах. Если GLB уже локальный и корректно ориентирован, не передавайте это поле. Для IFC uploads поле не нужно: сервер сам читает IfcSite.ObjectPlacement из IFC. Для USDZ uploads поле не применяется.

# components.schemas.SessionVirtualAnchorCircleRequestRequest.properties.model_file.description
Один файл 3D модели (GLB, IFC, USDZ). Нельзя использовать одновременно с model_files. Для GLB публичный API принимает максимум 5000 mesh-node элементов.

# components.schemas.SessionVirtualAnchorCircleRequestRequest.properties.model_files.description
Несколько файлов 3D модели. Передавайте multipart parts с именем model_files. Разрешены GLB и IFC. Нельзя использовать одновременно с model_file. Каждый GLB должен содержать не больше 5000 mesh-node элементов.

# components.schemas.SessionVirtualAnchorCircleRequestRequest.properties.ui.description
Session-level UI override для RoomCapture. Для multipart передавайте ui.presentation JSON-строкой; пример скрытия кнопки: {'buttons': {'roomcapture_ar_mode': false}}.

# components.schemas.SessionVirtualAnchorCircleRequestRequest.properties.virtual_anchor.description
Минимальные данные virtual anchor. Backend не создаёт VirtualAnchor в БД, но вычисляет полный iOS-compatible geometry для session detail.

# components.schemas.SessionVirtualAnchorCornerRequestRequest.description
Базовые поля для всех session запросов — источник модели

# components.schemas.SessionVirtualAnchorCornerRequestRequest.properties.branding.description
Брендирование AR-интерфейса.

# components.schemas.SessionVirtualAnchorCornerRequestRequest.properties.callback.description
Параметры возврата в приложение-источник.

# components.schemas.SessionVirtualAnchorCornerRequestRequest.properties.client.description
Информация о клиентском приложении.

# components.schemas.SessionVirtualAnchorCornerRequestRequest.properties.ifc_site_placement.description
Опциональная нормализация GLB по параметрам IfcSite.ObjectPlacement. Передавайте это поле только для GLB, который был экспортирован из IFC с глобальным смещением или поворотом IfcSite, и это смещение уже запечено в GLB. Сервер применит обратную трансформацию и сохранит модель в локальных координатах. Если GLB уже локальный и корректно ориентирован, не передавайте это поле. Для IFC uploads поле не нужно: сервер сам читает IfcSite.ObjectPlacement из IFC. Для USDZ uploads поле не применяется.

# components.schemas.SessionVirtualAnchorCornerRequestRequest.properties.model_file.description
Один файл 3D модели (GLB, IFC, USDZ). Нельзя использовать одновременно с model_files. Для GLB публичный API принимает максимум 5000 mesh-node элементов.

# components.schemas.SessionVirtualAnchorCornerRequestRequest.properties.model_files.description
Несколько файлов 3D модели. Передавайте multipart parts с именем model_files. Разрешены GLB и IFC. Нельзя использовать одновременно с model_file. Каждый GLB должен содержать не больше 5000 mesh-node элементов.

# components.schemas.SessionVirtualAnchorCornerRequestRequest.properties.ui.description
Session-level UI override для RoomCapture. Для multipart передавайте ui.presentation JSON-строкой; пример скрытия кнопки: {'buttons': {'roomcapture_ar_mode': false}}.

# components.schemas.SessionVirtualAnchorCornerRequestRequest.properties.virtual_anchor.description
Минимальные данные углового virtual anchor. Backend не создаёт VirtualAnchor в БД, но вычисляет полный iOS-compatible corner_geometry для session detail.

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.description
Базовые поля для всех session запросов — источник модели

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.properties.branding.description
Брендирование AR-интерфейса.

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.properties.callback.description
Параметры возврата в приложение-источник.

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.properties.client.description
Информация о клиентском приложении.

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.properties.ifc_site_placement.description
Опциональная нормализация GLB по параметрам IfcSite.ObjectPlacement. Передавайте это поле только для GLB, который был экспортирован из IFC с глобальным смещением или поворотом IfcSite, и это смещение уже запечено в GLB. Сервер применит обратную трансформацию и сохранит модель в локальных координатах. Если GLB уже локальный и корректно ориентирован, не передавайте это поле. Для IFC uploads поле не нужно: сервер сам читает IfcSite.ObjectPlacement из IFC. Для USDZ uploads поле не применяется.

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.properties.model_file.description
Один файл 3D модели (GLB, IFC, USDZ). Нельзя использовать одновременно с model_files. Для GLB публичный API принимает максимум 5000 mesh-node элементов.

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.properties.model_files.description
Несколько файлов 3D модели. Передавайте multipart parts с именем model_files. Разрешены GLB и IFC. Нельзя использовать одновременно с model_file. Каждый GLB должен содержать не больше 5000 mesh-node элементов.

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.properties.ui.description
Session-level UI override для RoomCapture. Для multipart передавайте ui.presentation JSON-строкой; пример скрытия кнопки: {'buttons': {'roomcapture_ar_mode': false}}.

# components.schemas.SessionVirtualAnchorRectangleRequestRequest.properties.virtual_anchor.description
Минимальные данные virtual anchor. Backend не создаёт VirtualAnchor в БД, но вычисляет полный iOS-compatible geometry для session detail.

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.description
Базовые поля для всех session запросов — источник модели

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.properties.branding.description
Брендирование AR-интерфейса.

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.properties.callback.description
Параметры возврата в приложение-источник.

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.properties.client.description
Информация о клиентском приложении.

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.properties.ifc_site_placement.description
Опциональная нормализация GLB по параметрам IfcSite.ObjectPlacement. Передавайте это поле только для GLB, который был экспортирован из IFC с глобальным смещением или поворотом IfcSite, и это смещение уже запечено в GLB. Сервер применит обратную трансформацию и сохранит модель в локальных координатах. Если GLB уже локальный и корректно ориентирован, не передавайте это поле. Для IFC uploads поле не нужно: сервер сам читает IfcSite.ObjectPlacement из IFC. Для USDZ uploads поле не применяется.

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.properties.model_file.description
Один файл 3D модели (GLB, IFC, USDZ). Нельзя использовать одновременно с model_files. Для GLB публичный API принимает максимум 5000 mesh-node элементов.

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.properties.model_files.description
Несколько файлов 3D модели. Передавайте multipart parts с именем model_files. Разрешены GLB и IFC. Нельзя использовать одновременно с model_file. Каждый GLB должен содержать не больше 5000 mesh-node элементов.

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.properties.ui.description
Session-level UI override для RoomCapture. Для multipart передавайте ui.presentation JSON-строкой; пример скрытия кнопки: {'buttons': {'roomcapture_ar_mode': false}}.

# components.schemas.SessionVirtualAnchorSegmentRequestRequest.properties.virtual_anchor.description
Минимальные данные virtual anchor отрезка. Backend не создаёт VirtualAnchor в БД, но вычисляет полный iOS-compatible segment_geometry для session detail.

# components.schemas.TagStandard52h13RenderRequestRequest.properties.id.description
Числовой код AprilTag family tagStandard52h13. Допустимый диапазон: 0..48713.

# components.schemas.TagStandard52h13RenderRequestRequest.properties.size_mm.description
Размер стороны квадратного маркера в миллиметрах. Относится только к самому маркеру без текстовой области.

# components.schemas.TagStandard52h13RenderRequestRequest.properties.text.description
Опциональный текст над маркером. Допускается до 3 строк; максимум 120 символов.

# components.schemas.VirtualAnchorCircleSessionGeometryRequest.properties.diameter_point_1.description
Первая точка диаметра окружности [x, y, z].

# components.schemas.VirtualAnchorCircleSessionGeometryRequest.properties.diameter_point_2.description
Вторая точка диаметра окружности [x, y, z].

# components.schemas.VirtualAnchorCircleSessionGeometryRequest.properties.plane_normal.description
Нормаль поверхности/плоскости окружности [x, y, z].

# components.schemas.VirtualAnchorCircleSessionPayloadRequest.properties.circle_geometry.description
Минимальная geometry окружности. Передайте две точки диаметра и plane_normal; center, radius, normal_tip и diameter_direction вычислит backend.

# components.schemas.VirtualAnchorCircleSessionPayloadRequest.properties.coordinate_system.description
Система координат входных точек. Сервер нормализует geometry в Y-UP и отдаёт iOS уже вычисленные center/radius/directions.

* `y-up` - y-up
* `z-up` - z-up

# components.schemas.VirtualAnchorCornerSessionGeometryRequest.properties.corner_point.description
Нижняя точка угла на пересечении пола и стен [x, y, z].

# components.schemas.VirtualAnchorCornerSessionGeometryRequest.properties.up_axis.description
Направление вверх от точки угла [x, y, z].

# components.schemas.VirtualAnchorCornerSessionGeometryRequest.properties.wall_axis_a.description
Направление первой стены от угла [x, y, z].

# components.schemas.VirtualAnchorCornerSessionGeometryRequest.properties.wall_axis_b.description
Направление второй стены от угла [x, y, z].

# components.schemas.VirtualAnchorCornerSessionPayloadRequest.properties.coordinate_system.description
Система координат входных точек. Сервер нормализует geometry в Y-UP и отдаёт iOS уже вычисленные center/radius/directions.

* `y-up` - y-up
* `z-up` - z-up

# components.schemas.VirtualAnchorCornerSessionPayloadRequest.properties.corner_geometry.description
Минимальная geometry угла. Передайте нижнюю точку угла, up_axis и две оси стен; center, normals, bisector_direction, wall_angle_deg и preview_height_m вычислит backend.

# components.schemas.VirtualAnchorRectangleSessionGeometryRequest.properties.opposite_short_side_center.description
Центр противоположной короткой стороны [x, y, z].

# components.schemas.VirtualAnchorRectangleSessionGeometryRequest.properties.plane_normal.description
Нормаль поверхности/плоскости прямоугольника [x, y, z].

# components.schemas.VirtualAnchorRectangleSessionGeometryRequest.properties.short_side_point_1.description
Первая точка короткой стороны [x, y, z].

# components.schemas.VirtualAnchorRectangleSessionGeometryRequest.properties.short_side_point_2.description
Вторая точка короткой стороны [x, y, z].

# components.schemas.VirtualAnchorRectangleSessionPayloadRequest.properties.center.description
v1.1: центр прямоугольника [x, y, z]. Используется вместо rectangle_geometry.

# components.schemas.VirtualAnchorRectangleSessionPayloadRequest.properties.coordinate_system.description
Система координат входных точек. Сервер нормализует geometry в Y-UP и отдаёт iOS уже вычисленные center/radius/directions.

* `y-up` - y-up
* `z-up` - z-up

# components.schemas.VirtualAnchorRectangleSessionPayloadRequest.properties.height.description
v1.1: длина стороны, перпендикулярной short_side_direction, в метрах.

# components.schemas.VirtualAnchorRectangleSessionPayloadRequest.properties.plane_normal.description
v1.1: нормаль плоскости прямоугольника [x, y, z].

# components.schemas.VirtualAnchorRectangleSessionPayloadRequest.properties.rectangle_geometry.description
Минимальная geometry прямоугольника. Передайте две точки короткой стороны и центр противоположной короткой стороны, а также plane_normal; остальные поля вычислит backend.

# components.schemas.VirtualAnchorRectangleSessionPayloadRequest.properties.short_side_direction.description
v1.1: направление короткой стороны [x, y, z].

# components.schemas.VirtualAnchorRectangleSessionPayloadRequest.properties.width.description
v1.1: длина короткой стороны в метрах.

# components.schemas.VirtualAnchorSegmentSessionGeometryRequest.properties.placement_source.description
Источник размещения public segment session: только model_surface.

* `model_surface` - model_surface

# components.schemas.VirtualAnchorSegmentSessionGeometryRequest.properties.plane_normal.description
Нормаль поверхности/плоскости отрезка [x, y, z].

# components.schemas.VirtualAnchorSegmentSessionGeometryRequest.properties.segment_point_1.description
Первая точка отрезка [x, y, z].

# components.schemas.VirtualAnchorSegmentSessionGeometryRequest.properties.segment_point_2.description
Вторая точка отрезка [x, y, z].

# components.schemas.VirtualAnchorSegmentSessionPayloadRequest.properties.coordinate_system.description
Система координат входных точек. Сервер нормализует geometry в Y-UP и отдаёт iOS уже вычисленные center/radius/directions.

* `y-up` - y-up
* `z-up` - z-up

# components.schemas.VirtualAnchorSegmentSessionPayloadRequest.properties.placement_source.description
v1.1: источник размещения; поддерживается model_surface.

* `model_surface` - model_surface

# components.schemas.VirtualAnchorSegmentSessionPayloadRequest.properties.plane_normal.description
v1.1: нормаль плоскости отрезка [x, y, z].

# components.schemas.VirtualAnchorSegmentSessionPayloadRequest.properties.segment_geometry.description
Минимальная geometry отрезка. Передайте две точки отрезка и plane_normal; center, normal_tip, segment_direction и length_m вычислит backend.

# components.schemas.VirtualAnchorSegmentSessionPayloadRequest.properties.segment_point_1.description
v1.1: первая точка отрезка [x, y, z]. Используется вместо segment_geometry.

# components.schemas.VirtualAnchorSegmentSessionPayloadRequest.properties.segment_point_2.description
v1.1: вторая точка отрезка [x, y, z]. Используется вместо segment_geometry.

# components.securitySchemes.BearerAuth.description
JWT access token в заголовке Authorization: Bearer <token>

# info.description
# GIP Vision API v1.1

API для работы с 3D моделями, конвертации и AR-визуализации.

Версия 1.1 — обратно совместимое расширение API v1. URL остаётся
`/api/gip-vision/v1/`: существующим клиентам не нужно менять пути или payload.
Дополнительно поддерживаются JSON-строки для векторов `point_1`/`point_2` и
короткая форма virtual anchor из публичного Quick Start.

## Начало работы

### 1. Получение API ключа

1. Чтобы получить доступ к API, нужно зарегистрироваться здесь: [https://app.gip-vision.pro/register-gipvision](https://app.gip-vision.pro/register-gipvision)
2. Войдите в свой аккаунт
3. Перейдите в раздел **API Ключи**
4. Нажмите **Создать ключ**
5. Сохраните ключ в надёжном месте — он показывается только один раз!

### 2. Авторизация запросов

Добавляйте заголовок `X-API-Key` к каждому запросу:

```bash
curl -X GET \
  -H "X-API-Key: gv_sk_ваш_ключ_здесь" \
  https://api-cpsk-superapp.gip.su/api/gip-vision/v1/health/
```

### 3. Формат ключа

API ключи имеют формат: `gv_sk_` + 43 символа

Пример: `gv_sk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2`

### 4. Схема API (скачивание)

- JSON: [https://api-cpsk-superapp.gip.su/api/gip-vision/v1/openapi.json](https://api-cpsk-superapp.gip.su/api/gip-vision/v1/openapi.json)
- YAML: [https://api-cpsk-superapp.gip.su/api/gip-vision/v1/openapi.yaml](https://api-cpsk-superapp.gip.su/api/gip-vision/v1/openapi.yaml)

## Ошибки авторизации

| Код | Описание |
|-----|----------|
| 401 | Неверный или отсутствующий API ключ |
| 403 | API ключ не имеет доступа к данному модулю |

## Rate Limits

- **100 запросов в минуту** на один API ключ
- При превышении лимита возвращается ошибка `429 Too Many Requests`

## Версионирование

API использует стабильный URL `/api/gip-vision/v1/`. Минорная версия контракта — `1.1`.

Форматы запросов API v1, существовавшие до 1.1, продолжают поддерживаться без изменений.

## Маркеры

- `POST /api/gip-vision/v1/markers/tag-standard-52h13/render/`
- Требует `X-API-Key`
- Принимает числовой `id` для AprilTag family `tagStandard52h13`
- Возвращает `image/jpeg`
- Поддерживает печатный размер маркера в миллиметрах и опциональный текст над маркером

## Проекты, модели и задачи

Public protected методы ниже предназначены для внешних интеграций и используют тот же scoped доступ, что
и остальные protected методы GIP Vision API.

Для вызова этих методов API key должен принадлежать пользователю с активным тарифом GIP Vision или
активированным промокодом. Если доступ не активирован, API возвращает `402 Payment Required` с кодом
`billing_access_required`.

### Проекты

- `GET /api/gip-vision/v1/projects/` — возвращает проекты, доступные текущему пользователю ключа.
- `POST /api/gip-vision/v1/projects/` — создаёт новый GIP Vision проект.

Минимальный payload создания проекта:

```json
{
  "project_name": "Объект А",
  "project_type": "ar",
  "is_sharable": true
}
```

### Модели проекта

- `GET /api/gip-vision/v1/projects/{project_id}/models/` — возвращает загруженные модели проекта с
  ограниченным набором полей: `id`, `project_id`, `display_name`, `file_type`, `file_size`,
  `uploaded_at`, `conversion_status`, `active_spatial_project_id`.
- `POST /api/gip-vision/v1/projects/{project_id}/models/ifc/` — принимает multipart IFC-файл,
  создаёт `ModelFile` и сразу ставит модель в очередь конвертации.

Пример multipart upload:

```bash
curl -X POST "https://api-cpsk-superapp.gip.su/api/gip-vision/v1/projects/385-842-513/models/ifc/" \
  -H "X-API-Key: gv_sk_ваш_ключ_здесь" \
  -F "model_file=@/absolute/path/model.ifc" \
  -F "display_name=Раздел АР"
```

Ответ возвращается со статусом `202 Accepted` и содержит `model_file`, `spatial_project_id`,
`task_id`, `conversion_status`.

### Доски и задачи проекта

- `GET /api/gip-vision/v1/projects/{project_id}/task-boards/` — список досок задач проекта.
- `POST /api/gip-vision/v1/projects/{project_id}/task-boards/` — создать доску задач проекта.
- `POST /api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/tasks/` — создать задачу
  в доске. Если `stage_id` не передан, используется первая активная колонка доски.
- `GET /api/gip-vision/v1/projects/{project_id}/tasks/` — получить задачи проекта. Поддерживаются
  query-фильтры `board_id`, `is_done`, `is_archived`.
- `GET /api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/` — получить данные одной задачи.

Минимальный payload создания задачи:

```json
{
  "title": "Проверить коллизию",
  "description_html": "<p>Комментарий из интеграции</p>",
  "priority": "normal",
  "element_guid": "3xP0Q..."
}
```

Для AR/BIM контекста можно передать `spatial_project_id`, `viewer_model_file_id`,
`viewer_camera_state` и `spatial_context`.

### BCF export/import

- `POST /api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/` — скачать `.bcfzip` по списку
  `task_ids`, по доске или по всему проекту.
- `POST /api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/` — загрузить `.bcfzip` и создать
  или обновить задачи проекта. Если `board_id`/`stage_id` не переданы, backend использует основную
  доску и первую активную колонку.

Пример BCF export:

```json
{
  "task_ids": ["0a455972-3e2d-4489-98f2-5c5b30e4b2cc"]
}
```

Пример BCF import:

```bash
curl -X POST "https://api-cpsk-superapp.gip.su/api/gip-vision/v1/projects/385-842-513/tasks/bcf/import/" \
  -H "X-API-Key: gv_sk_ваш_ключ_здесь" \
  -F "bcf_file=@/absolute/path/issues.bcfzip"
```

## Размещение по virtual anchor

Public protected методы:

- `POST /api/gip-vision/v1/session/by_virtual_anchor/circle-directional/`
- `POST /api/gip-vision/v1/session/by_virtual_anchor/rectangle-short-side/`
- `POST /api/gip-vision/v1/session/by_virtual_anchor/corner-directional/`
- `POST /api/gip-vision/v1/session/by_virtual_anchor/segment-directional/`

Все методы требуют `X-API-Key` или `Authorization: Bearer <token>`. Пользователь не должен заранее
создавать `VirtualAnchor`: передайте `model_file`, минимальные точки анкера и нормаль поверхности
`plane_normal` в `virtual_anchor`. Backend принимает модель целиком, конвертирует её обычным session pipeline
и возвращает
`gipvision://session/by_virtual_anchor/{session_id}`.

Эти методы не создают запись `VirtualAnchor`, не режут `GLB`/`USDZ` на chunks, не запускают crop pipeline
и не запускают Reality conversion. iOS получает переданную geometry через
`GET /api/gip_vision/session/by_virtual_anchor/{session_id}/`.

Для rectangle и segment доступны обе формы payload:

- исходная вложенная `rectangle_geometry`/`segment_geometry`;
- короткая форма v1.1 с `center`, `width`, `height`, `short_side_direction` либо
  `segment_point_1`, `segment_point_2`, `plane_normal`.

Если переданы обе формы, приоритет имеет исходная вложенная geometry.

## Поддержка

- Email: support@gip-vision.pro
- Документация: https://docs.gip-vision.pro/

# paths./api/gip-vision/v1/health/.get.description
Возвращает публичный минимальный статус доступности GIP Vision API. Если передан валидный `X-API-Key` со scope `gip_vision`, дополнительно возвращает информацию о пользователе и scope ключа.

# paths./api/gip-vision/v1/health/.get.summary
Проверка доступности API

# paths./api/gip-vision/v1/health/.get.tags[]
Сервис и доступ

# paths./api/gip-vision/v1/health/.get.x-codeSamples[].source
curl --request GET \
  'https://api-cpsk-superapp.gip.su/api/gip-vision/v1/health/' \
  --header 'X-API-Key: YOUR_API_KEY'

# paths./api/gip-vision/v1/markers/tag-standard-52h13/render/.post.description
Генерирует AprilTag family `tagStandard52h13` по числовому коду и возвращает JPG для печати. Размер маркера задаётся в миллиметрах; при необходимости можно вывести до 3 строк текста над маркером.

# paths./api/gip-vision/v1/markers/tag-standard-52h13/render/.post.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/markers/tag-standard-52h13/render/.post.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/markers/tag-standard-52h13/render/.post.summary
Сгенерировать JPG для AprilTag tagStandard52h13

# paths./api/gip-vision/v1/projects/.get.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/.get.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/.get.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/.get.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/.get.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/.get.summary
Получить проекты пользователя

# paths./api/gip-vision/v1/projects/.get.x-codeSamples[].source
curl --request GET \
  'https://api-cpsk-superapp.gip.su/api/gip-vision/v1/projects/' \
  --header 'X-API-Key: YOUR_API_KEY'

# paths./api/gip-vision/v1/projects/.post.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/.post.responses.400.description
Некорректные параметры запроса.

# paths./api/gip-vision/v1/projects/.post.responses.400.content.application/json.examples.error.value.detail
Проверьте параметры запроса.

# paths./api/gip-vision/v1/projects/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/.post.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/.post.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/.post.summary
Создать проект

# paths./api/gip-vision/v1/projects/.post.x-codeSamples[].source
curl --request POST \
  'https://api-cpsk-superapp.gip.su/api/gip-vision/v1/projects/' \
  --header 'X-API-Key: YOUR_API_KEY'

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.summary
Получить модели проекта с ограниченным набором полей

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.tags[]
Модели проекта

# paths./api/gip-vision/v1/projects/{project_id}/models/.get.x-codeSamples[].source
curl --request GET \
  'https://api-cpsk-superapp.gip.su/api/gip-vision/v1/projects/{project_id}/models/' \
  --header 'X-API-Key: YOUR_API_KEY'

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.responses.403.description
Нет доступа к проекту.

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.responses.409.description
Конвертация уже выполняется.

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.summary
Загрузить IFC модель и поставить конвертацию в очередь

# paths./api/gip-vision/v1/projects/{project_id}/models/ifc/.post.tags[]
Модели проекта

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.summary
Получить доски задач проекта

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.get.tags[]
Задачи проекта

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.400.description
Некорректные параметры запроса.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.400.content.application/json.examples.error.value.detail
Проверьте параметры запроса.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.summary
Создать доску задач проекта

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/.post.tags[]
Задачи проекта

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.400.description
Некорректные параметры запроса.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.400.content.application/json.examples.error.value.detail
Проверьте параметры запроса.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.summary
Создать задачу в доске проекта

# paths./api/gip-vision/v1/projects/{project_id}/task-boards/{board_id}/.post.tags[]
Задачи проекта

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.summary
Получить задачи проекта

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.tags[]
Задачи проекта

# paths./api/gip-vision/v1/projects/{project_id}/tasks/.get.x-codeSamples[].source
curl --request GET \
  'https://api-cpsk-superapp.gip.su/api/gip-vision/v1/projects/{project_id}/tasks/' \
  --header 'X-API-Key: YOUR_API_KEY'

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.400.description
Некорректные параметры запроса.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.400.content.application/json.examples.error.value.detail
Проверьте параметры запроса.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/export/.post.summary
Скачать BCF-архив задач проекта

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.400.description
Некорректные параметры запроса.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.400.content.application/json.examples.error.value.detail
Проверьте параметры запроса.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/bcf/import/.post.summary
Импортировать BCF-архив в задачи проекта

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.responses.402.description
Нужен активный тариф или промокод GIP Vision.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.responses.404.description
Ресурс не найден или недоступен.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.responses.404.content.application/json.examples.error.value.detail
Ресурс не найден.

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.summary
Получить задачу проекта

# paths./api/gip-vision/v1/projects/{project_id}/tasks/{task_id}/.get.tags[]
Задачи проекта

# paths./api/gip-vision/v1/session/by_scan/.post.description
Создаёт AR-сессию с размещением модели по 3D-скану помещения (LiDAR). Конвертация по `model_file` или нескольким multipart `model_files` требует авторизацию (`X-API-Key` или `Bearer`). Uploaded assets проходят IFC/GLB → USDZ → Reality и отдаются в detail через `model_assets`. Статус `ready` означает, что все обязательные Reality-артефакты сохранены; USDZ остаётся fallback и сам по себе не завершает сессию. Для GLB действует лимит публичного API: не больше 5000 mesh-node элементов на файл. Неавторизованный доступ поддерживается только в shortcut-режиме через внутреннее поле `spatial_project_id` для публичного SpatialProject (переиспользование готовых файлов без конвертации).

### Нормализация GLB по IfcSite.ObjectPlacement

Используйте `ifc_site_placement` только для GLB, который уже был экспортирован из IFC, где смещение
или поворот `IfcSite.ObjectPlacement` запечены в координаты GLB. Для IFC uploads поле не передаётся:
сервер сам читает `IfcSite.ObjectPlacement` из IFC.

Порядок применения:
1. Из исходного IFC считайте `IfcSite.ObjectPlacement.RelativePlacement.Location.Coordinates`, `Axis`
   и `RefDirection`.
2. Передайте `translation` этими координатами, обычно в миллиметрах и `z-up`.
3. Если есть поворот через `RefDirection`, передайте эквивалентный yaw как
   `rotation_deg=[0, 0, <yaw_deg>]`.
4. Сервер применит обратную трансформацию к GLB и сохранит модель в локальных координатах.

Multipart-поля для загрузки уже экспортированного GLB:
```bash
-F "model_file=@/path/to/source_with_site_offset.glb" \
-F "ifc_site_placement.translation=<site_x_mm>" \
-F "ifc_site_placement.translation=<site_y_mm>" \
-F "ifc_site_placement.translation=<site_z_mm>" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=<site_yaw_deg>" \
-F "ifc_site_placement.units=mm" \
-F "ifc_site_placement.coordinate_system=z-up"
```

Команды ниже помогают проверить, что исходный GLB отличается из-за site placement, а результат после
локализации совпадает с эталонным локальным GLB. Нужны `IfcConvert`, `ifcopenshell`, `trimesh` и `numpy`.

Демо-файлы для самостоятельной проверки:
- [source_with_site_offset.ifc](/static/gip_vision/demo/site_placement/source_with_site_offset.ifc) —
  исходный IFC с ненулевым `IfcSite.ObjectPlacement`.
- [reference_local_origin.ifc](/static/gip_vision/demo/site_placement/reference_local_origin.ifc) —
  тот же пример в локальном начале координат.
- [source_as_is.glb](/static/gip_vision/demo/site_placement/source_as_is.glb) —
  GLB, экспортированный из исходного IFC без нормализации.
- [reference_local.glb](/static/gip_vision/demo/site_placement/reference_local.glb) —
  GLB из локального эталонного IFC.
- [source_site_local.glb](/static/gip_vision/demo/site_placement/source_site_local.glb) —
  результирующий GLB после обнуления site placement.

```bash
export WORK_DIR="/tmp/gip_vision_site_placement_demo"
mkdir -p "$WORK_DIR"

# Укажите host вашего API. Для production это обычно https://api-cpsk-superapp.gip.su
export DEMO_BASE_URL="https://<api-host>/static/gip_vision/demo/site_placement"
export SOURCE_IFC="$WORK_DIR/source_with_site_offset.ifc"
export REFERENCE_IFC="$WORK_DIR/reference_local_origin.ifc"

curl -L "$DEMO_BASE_URL/source_with_site_offset.ifc" -o "$SOURCE_IFC"
curl -L "$DEMO_BASE_URL/reference_local_origin.ifc" -o "$REFERENCE_IFC"

python - <<'PY'
from pathlib import Path
import math
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
ifc = ifcopenshell.open(str(source))
site = ifc.by_type("IfcSite")[0]
relative = site.ObjectPlacement.RelativePlacement
coords = tuple(float(value) for value in relative.Location.Coordinates)
axis = None if relative.Axis is None else tuple(float(value) for value in relative.Axis.DirectionRatios)
ref = None if relative.RefDirection is None else tuple(float(value) for value in relative.RefDirection.DirectionRatios)
yaw_deg = None if ref is None else math.degrees(math.atan2(ref[1], ref[0]))
print("translation_mm=", coords)
print("axis=", axis)
print("ref_direction=", ref)
print("yaw_deg=", yaw_deg)
PY

IfcConvert --no-progress -y "$SOURCE_IFC" "$WORK_DIR/source_as_is.glb"
IfcConvert --no-progress -y "$REFERENCE_IFC" "$WORK_DIR/reference_local.glb"

python - <<'PY'
from pathlib import Path
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
work_dir = Path(os.environ["WORK_DIR"])
out = work_dir / "source_site_local.ifc"
ifc = ifcopenshell.open(str(source))
for site in ifc.by_type("IfcSite"):
    relative = site.ObjectPlacement.RelativePlacement
    relative.Location.Coordinates = (0.0, 0.0, 0.0)
    relative.Axis = None
    relative.RefDirection = None
ifc.write(str(out))
print(out)
PY

IfcConvert --no-progress -y "$WORK_DIR/source_site_local.ifc" "$WORK_DIR/source_site_local.glb"

python - <<'PY'
from pathlib import Path
import os
import numpy as np
import trimesh

work_dir = Path(os.environ["WORK_DIR"])

def stats(path):
    scene = trimesh.load(path, force="scene", process=False)
    bounds = np.asarray(scene.bounds, dtype=float)
    center = (bounds[0] + bounds[1]) / 2
    extents = bounds[1] - bounds[0]
    vertices = sum(len(getattr(geom, "vertices", [])) for geom in scene.geometry.values())
    faces = sum(len(getattr(geom, "faces", [])) for geom in scene.geometry.values())
    return center, extents, vertices, faces

reference = stats(work_dir / "reference_local.glb")
for name in ("source_as_is", "source_site_local"):
    current = stats(work_dir / f"{name}.glb")
    print(
        f"{name} vs reference_local:",
        "center_delta_m=", float(np.linalg.norm(current[0] - reference[0])),
        "extents_delta_m=", float(np.linalg.norm(current[1] - reference[1])),
        "vertices_delta=", current[2] - reference[2],
        "faces_delta=", current[3] - reference[3],
    )
PY
```

Ожидаемый результат: `source_as_is.glb` заметно отличается от `reference_local.glb`, а
`source_site_local.glb` после обнуления site placement имеет близкий к нулю `center_delta_m` и нулевые
дельты по топологии.

# paths./api/gip-vision/v1/session/by_scan/.post.summary
Создать сессию: размещение по скану

# paths./api/gip-vision/v1/session/by_scan/.post.x-codeSamples[].source
curl --request POST \
  'https://api-cpsk-superapp.gip.su/api/gip-vision/v1/session/by_scan/' \
  --header 'X-API-Key: YOUR_API_KEY'

# paths./api/gip-vision/v1/session/by_virtual_anchor/circle-directional/.post.description
Создаёт AR-сессию с размещением по готовой geometry virtual anchor окружности. Метод требует `X-API-Key` или `Bearer`, принимает `model_file` или несколько `model_files`, конвертирует каждую модель через USDZ в Reality; `ready` выставляется только после успешного Reality callback для всех `model_assets` и не создаёт VirtualAnchor в БД.

### Размещение по virtual anchor без предварительного создания анкера

Public protected методы `by_virtual_anchor` принимают модель целиком и минимальные точки анкера,
из которых backend собирает iOS-compatible geometry. Пользователь не должен заранее создавать `VirtualAnchor`
через private API.

Важно:
- backend не создаёт запись `VirtualAnchor`;
- backend не режет исходный GLB/USDZ/IFC на chunks и не запускает crop pipeline;
- модель или набор моделей конвертируется целиком в per-file `model_assets` (`Reality`, fallback `USDZ`);
- для GLB действует лимит публичного API: не больше 5000 mesh-node элементов на файл;
- можно передать один `model_file` или несколько multipart parts `model_files`;
- для `model_files` разрешены GLB и IFC, смешивать `model_file` и `model_files` нельзя;
- для request нужны минимальные точки анкера и `plane_normal` поверхности, а `center`, `radius`,
  `normal_tip`, `diameter_direction`, `corners`, segment fields и длины сторон вычисляет backend;
- iOS открывает deeplink `gipvision://session/by_virtual_anchor/{session_id}` и получает geometry
  из `GET /api/gip_vision/session/by_virtual_anchor/{session_id}/`.

Circle directional:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/circle-directional/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_file=@/absolute/path/model.glb" \
  -F "virtual_anchor.name=Circle placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=2" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=1"
```

Несколько моделей для одного virtual anchor:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/circle-directional/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_files=@/absolute/path/architecture.glb" \
  -F "model_files=@/absolute/path/engineering.ifc" \
  -F "virtual_anchor.name=Multi-model placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=2" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=1"
```

Rectangle short side:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/rectangle-short-side/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_file=@/absolute/path/model.glb" \
  -F "virtual_anchor.name=Rectangle placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F 'virtual_anchor.rectangle_geometry.short_side_point_1=[0,0,0]' \
  -F 'virtual_anchor.rectangle_geometry.short_side_point_2=[0,1,0]' \
  -F 'virtual_anchor.rectangle_geometry.opposite_short_side_center=[2,0.5,0]' \
  -F 'virtual_anchor.rectangle_geometry.plane_normal=[0,0,-1]'
```

### Нормализация GLB по IfcSite.ObjectPlacement

Используйте `ifc_site_placement` только для GLB, который уже был экспортирован из IFC, где смещение
или поворот `IfcSite.ObjectPlacement` запечены в координаты GLB. Для IFC uploads поле не передаётся:
сервер сам читает `IfcSite.ObjectPlacement` из IFC.

Порядок применения:
1. Из исходного IFC считайте `IfcSite.ObjectPlacement.RelativePlacement.Location.Coordinates`, `Axis`
   и `RefDirection`.
2. Передайте `translation` этими координатами, обычно в миллиметрах и `z-up`.
3. Если есть поворот через `RefDirection`, передайте эквивалентный yaw как
   `rotation_deg=[0, 0, <yaw_deg>]`.
4. Сервер применит обратную трансформацию к GLB и сохранит модель в локальных координатах.

Multipart-поля для загрузки уже экспортированного GLB:
```bash
-F "model_file=@/path/to/source_with_site_offset.glb" \
-F "ifc_site_placement.translation=<site_x_mm>" \
-F "ifc_site_placement.translation=<site_y_mm>" \
-F "ifc_site_placement.translation=<site_z_mm>" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=<site_yaw_deg>" \
-F "ifc_site_placement.units=mm" \
-F "ifc_site_placement.coordinate_system=z-up"
```

Команды ниже помогают проверить, что исходный GLB отличается из-за site placement, а результат после
локализации совпадает с эталонным локальным GLB. Нужны `IfcConvert`, `ifcopenshell`, `trimesh` и `numpy`.

Демо-файлы для самостоятельной проверки:
- [source_with_site_offset.ifc](/static/gip_vision/demo/site_placement/source_with_site_offset.ifc) —
  исходный IFC с ненулевым `IfcSite.ObjectPlacement`.
- [reference_local_origin.ifc](/static/gip_vision/demo/site_placement/reference_local_origin.ifc) —
  тот же пример в локальном начале координат.
- [source_as_is.glb](/static/gip_vision/demo/site_placement/source_as_is.glb) —
  GLB, экспортированный из исходного IFC без нормализации.
- [reference_local.glb](/static/gip_vision/demo/site_placement/reference_local.glb) —
  GLB из локального эталонного IFC.
- [source_site_local.glb](/static/gip_vision/demo/site_placement/source_site_local.glb) —
  результирующий GLB после обнуления site placement.

```bash
export WORK_DIR="/tmp/gip_vision_site_placement_demo"
mkdir -p "$WORK_DIR"

# Укажите host вашего API. Для production это обычно https://api-cpsk-superapp.gip.su
export DEMO_BASE_URL="https://<api-host>/static/gip_vision/demo/site_placement"
export SOURCE_IFC="$WORK_DIR/source_with_site_offset.ifc"
export REFERENCE_IFC="$WORK_DIR/reference_local_origin.ifc"

curl -L "$DEMO_BASE_URL/source_with_site_offset.ifc" -o "$SOURCE_IFC"
curl -L "$DEMO_BASE_URL/reference_local_origin.ifc" -o "$REFERENCE_IFC"

python - <<'PY'
from pathlib import Path
import math
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
ifc = ifcopenshell.open(str(source))
site = ifc.by_type("IfcSite")[0]
relative = site.ObjectPlacement.RelativePlacement
coords = tuple(float(value) for value in relative.Location.Coordinates)
axis = None if relative.Axis is None else tuple(float(value) for value in relative.Axis.DirectionRatios)
ref = None if relative.RefDirection is None else tuple(float(value) for value in relative.RefDirection.DirectionRatios)
yaw_deg = None if ref is None else math.degrees(math.atan2(ref[1], ref[0]))
print("translation_mm=", coords)
print("axis=", axis)
print("ref_direction=", ref)
print("yaw_deg=", yaw_deg)
PY

IfcConvert --no-progress -y "$SOURCE_IFC" "$WORK_DIR/source_as_is.glb"
IfcConvert --no-progress -y "$REFERENCE_IFC" "$WORK_DIR/reference_local.glb"

python - <<'PY'
from pathlib import Path
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
work_dir = Path(os.environ["WORK_DIR"])
out = work_dir / "source_site_local.ifc"
ifc = ifcopenshell.open(str(source))
for site in ifc.by_type("IfcSite"):
    relative = site.ObjectPlacement.RelativePlacement
    relative.Location.Coordinates = (0.0, 0.0, 0.0)
    relative.Axis = None
    relative.RefDirection = None
ifc.write(str(out))
print(out)
PY

IfcConvert --no-progress -y "$WORK_DIR/source_site_local.ifc" "$WORK_DIR/source_site_local.glb"

python - <<'PY'
from pathlib import Path
import os
import numpy as np
import trimesh

work_dir = Path(os.environ["WORK_DIR"])

def stats(path):
    scene = trimesh.load(path, force="scene", process=False)
    bounds = np.asarray(scene.bounds, dtype=float)
    center = (bounds[0] + bounds[1]) / 2
    extents = bounds[1] - bounds[0]
    vertices = sum(len(getattr(geom, "vertices", [])) for geom in scene.geometry.values())
    faces = sum(len(getattr(geom, "faces", [])) for geom in scene.geometry.values())
    return center, extents, vertices, faces

reference = stats(work_dir / "reference_local.glb")
for name in ("source_as_is", "source_site_local"):
    current = stats(work_dir / f"{name}.glb")
    print(
        f"{name} vs reference_local:",
        "center_delta_m=", float(np.linalg.norm(current[0] - reference[0])),
        "extents_delta_m=", float(np.linalg.norm(current[1] - reference[1])),
        "vertices_delta=", current[2] - reference[2],
        "faces_delta=", current[3] - reference[3],
    )
PY
```

Ожидаемый результат: `source_as_is.glb` заметно отличается от `reference_local.glb`, а
`source_site_local.glb` после обнуления site placement имеет близкий к нулю `center_delta_m` и нулевые
дельты по топологии.

# paths./api/gip-vision/v1/session/by_virtual_anchor/circle-directional/.post.summary
Создать сессию: размещение по virtual anchor, окружность

# paths./api/gip-vision/v1/session/by_virtual_anchor/corner-directional/.post.description
Создаёт AR-сессию с размещением по готовой geometry virtual anchor угла. Метод требует `X-API-Key` или `Bearer`, принимает `model_file` или несколько `model_files`, конвертирует каждую модель через USDZ в Reality; `ready` выставляется только после успешного Reality callback для всех `model_assets` и не создаёт VirtualAnchor в БД.

### Размещение по virtual anchor без предварительного создания анкера

Public protected методы `by_virtual_anchor` принимают модель целиком и минимальные точки анкера,
из которых backend собирает iOS-compatible geometry. Пользователь не должен заранее создавать `VirtualAnchor`
через private API.

Важно:
- backend не создаёт запись `VirtualAnchor`;
- backend не режет исходный GLB/USDZ/IFC на chunks и не запускает crop pipeline;
- модель или набор моделей конвертируется целиком в per-file `model_assets` (`Reality`, fallback `USDZ`);
- для GLB действует лимит публичного API: не больше 5000 mesh-node элементов на файл;
- можно передать один `model_file` или несколько multipart parts `model_files`;
- для `model_files` разрешены GLB и IFC, смешивать `model_file` и `model_files` нельзя;
- для request нужны минимальные точки анкера и `plane_normal` поверхности, а `center`, `radius`,
  `normal_tip`, `diameter_direction`, `corners`, segment fields и длины сторон вычисляет backend;
- iOS открывает deeplink `gipvision://session/by_virtual_anchor/{session_id}` и получает geometry
  из `GET /api/gip_vision/session/by_virtual_anchor/{session_id}/`.

Circle directional:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/circle-directional/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_file=@/absolute/path/model.glb" \
  -F "virtual_anchor.name=Circle placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=2" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=1"
```

Несколько моделей для одного virtual anchor:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/circle-directional/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_files=@/absolute/path/architecture.glb" \
  -F "model_files=@/absolute/path/engineering.ifc" \
  -F "virtual_anchor.name=Multi-model placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=2" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=1"
```

Rectangle short side:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/rectangle-short-side/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_file=@/absolute/path/model.glb" \
  -F "virtual_anchor.name=Rectangle placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F 'virtual_anchor.rectangle_geometry.short_side_point_1=[0,0,0]' \
  -F 'virtual_anchor.rectangle_geometry.short_side_point_2=[0,1,0]' \
  -F 'virtual_anchor.rectangle_geometry.opposite_short_side_center=[2,0.5,0]' \
  -F 'virtual_anchor.rectangle_geometry.plane_normal=[0,0,-1]'
```

### Нормализация GLB по IfcSite.ObjectPlacement

Используйте `ifc_site_placement` только для GLB, который уже был экспортирован из IFC, где смещение
или поворот `IfcSite.ObjectPlacement` запечены в координаты GLB. Для IFC uploads поле не передаётся:
сервер сам читает `IfcSite.ObjectPlacement` из IFC.

Порядок применения:
1. Из исходного IFC считайте `IfcSite.ObjectPlacement.RelativePlacement.Location.Coordinates`, `Axis`
   и `RefDirection`.
2. Передайте `translation` этими координатами, обычно в миллиметрах и `z-up`.
3. Если есть поворот через `RefDirection`, передайте эквивалентный yaw как
   `rotation_deg=[0, 0, <yaw_deg>]`.
4. Сервер применит обратную трансформацию к GLB и сохранит модель в локальных координатах.

Multipart-поля для загрузки уже экспортированного GLB:
```bash
-F "model_file=@/path/to/source_with_site_offset.glb" \
-F "ifc_site_placement.translation=<site_x_mm>" \
-F "ifc_site_placement.translation=<site_y_mm>" \
-F "ifc_site_placement.translation=<site_z_mm>" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=<site_yaw_deg>" \
-F "ifc_site_placement.units=mm" \
-F "ifc_site_placement.coordinate_system=z-up"
```

Команды ниже помогают проверить, что исходный GLB отличается из-за site placement, а результат после
локализации совпадает с эталонным локальным GLB. Нужны `IfcConvert`, `ifcopenshell`, `trimesh` и `numpy`.

Демо-файлы для самостоятельной проверки:
- [source_with_site_offset.ifc](/static/gip_vision/demo/site_placement/source_with_site_offset.ifc) —
  исходный IFC с ненулевым `IfcSite.ObjectPlacement`.
- [reference_local_origin.ifc](/static/gip_vision/demo/site_placement/reference_local_origin.ifc) —
  тот же пример в локальном начале координат.
- [source_as_is.glb](/static/gip_vision/demo/site_placement/source_as_is.glb) —
  GLB, экспортированный из исходного IFC без нормализации.
- [reference_local.glb](/static/gip_vision/demo/site_placement/reference_local.glb) —
  GLB из локального эталонного IFC.
- [source_site_local.glb](/static/gip_vision/demo/site_placement/source_site_local.glb) —
  результирующий GLB после обнуления site placement.

```bash
export WORK_DIR="/tmp/gip_vision_site_placement_demo"
mkdir -p "$WORK_DIR"

# Укажите host вашего API. Для production это обычно https://api-cpsk-superapp.gip.su
export DEMO_BASE_URL="https://<api-host>/static/gip_vision/demo/site_placement"
export SOURCE_IFC="$WORK_DIR/source_with_site_offset.ifc"
export REFERENCE_IFC="$WORK_DIR/reference_local_origin.ifc"

curl -L "$DEMO_BASE_URL/source_with_site_offset.ifc" -o "$SOURCE_IFC"
curl -L "$DEMO_BASE_URL/reference_local_origin.ifc" -o "$REFERENCE_IFC"

python - <<'PY'
from pathlib import Path
import math
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
ifc = ifcopenshell.open(str(source))
site = ifc.by_type("IfcSite")[0]
relative = site.ObjectPlacement.RelativePlacement
coords = tuple(float(value) for value in relative.Location.Coordinates)
axis = None if relative.Axis is None else tuple(float(value) for value in relative.Axis.DirectionRatios)
ref = None if relative.RefDirection is None else tuple(float(value) for value in relative.RefDirection.DirectionRatios)
yaw_deg = None if ref is None else math.degrees(math.atan2(ref[1], ref[0]))
print("translation_mm=", coords)
print("axis=", axis)
print("ref_direction=", ref)
print("yaw_deg=", yaw_deg)
PY

IfcConvert --no-progress -y "$SOURCE_IFC" "$WORK_DIR/source_as_is.glb"
IfcConvert --no-progress -y "$REFERENCE_IFC" "$WORK_DIR/reference_local.glb"

python - <<'PY'
from pathlib import Path
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
work_dir = Path(os.environ["WORK_DIR"])
out = work_dir / "source_site_local.ifc"
ifc = ifcopenshell.open(str(source))
for site in ifc.by_type("IfcSite"):
    relative = site.ObjectPlacement.RelativePlacement
    relative.Location.Coordinates = (0.0, 0.0, 0.0)
    relative.Axis = None
    relative.RefDirection = None
ifc.write(str(out))
print(out)
PY

IfcConvert --no-progress -y "$WORK_DIR/source_site_local.ifc" "$WORK_DIR/source_site_local.glb"

python - <<'PY'
from pathlib import Path
import os
import numpy as np
import trimesh

work_dir = Path(os.environ["WORK_DIR"])

def stats(path):
    scene = trimesh.load(path, force="scene", process=False)
    bounds = np.asarray(scene.bounds, dtype=float)
    center = (bounds[0] + bounds[1]) / 2
    extents = bounds[1] - bounds[0]
    vertices = sum(len(getattr(geom, "vertices", [])) for geom in scene.geometry.values())
    faces = sum(len(getattr(geom, "faces", [])) for geom in scene.geometry.values())
    return center, extents, vertices, faces

reference = stats(work_dir / "reference_local.glb")
for name in ("source_as_is", "source_site_local"):
    current = stats(work_dir / f"{name}.glb")
    print(
        f"{name} vs reference_local:",
        "center_delta_m=", float(np.linalg.norm(current[0] - reference[0])),
        "extents_delta_m=", float(np.linalg.norm(current[1] - reference[1])),
        "vertices_delta=", current[2] - reference[2],
        "faces_delta=", current[3] - reference[3],
    )
PY
```

Ожидаемый результат: `source_as_is.glb` заметно отличается от `reference_local.glb`, а
`source_site_local.glb` после обнуления site placement имеет близкий к нулю `center_delta_m` и нулевые
дельты по топологии.

# paths./api/gip-vision/v1/session/by_virtual_anchor/corner-directional/.post.summary
Создать сессию: размещение по virtual anchor, угол

# paths./api/gip-vision/v1/session/by_virtual_anchor/rectangle-short-sid.post.description
Создаёт AR-сессию с размещением по готовой geometry virtual anchor прямоугольника. Метод требует `X-API-Key` или `Bearer`, принимает `model_file` или несколько `model_files`, конвертирует каждую модель через USDZ в Reality; `ready` выставляется только после успешного Reality callback для всех `model_assets` и не создаёт VirtualAnchor в БД.

### Размещение по virtual anchor без предварительного создания анкера

Public protected методы `by_virtual_anchor` принимают модель целиком и минимальные точки анкера,
из которых backend собирает iOS-compatible geometry. Пользователь не должен заранее создавать `VirtualAnchor`
через private API.

Важно:
- backend не создаёт запись `VirtualAnchor`;
- backend не режет исходный GLB/USDZ/IFC на chunks и не запускает crop pipeline;
- модель или набор моделей конвертируется целиком в per-file `model_assets` (`Reality`, fallback `USDZ`);
- для GLB действует лимит публичного API: не больше 5000 mesh-node элементов на файл;
- можно передать один `model_file` или несколько multipart parts `model_files`;
- для `model_files` разрешены GLB и IFC, смешивать `model_file` и `model_files` нельзя;
- для request нужны минимальные точки анкера и `plane_normal` поверхности, а `center`, `radius`,
  `normal_tip`, `diameter_direction`, `corners`, segment fields и длины сторон вычисляет backend;
- iOS открывает deeplink `gipvision://session/by_virtual_anchor/{session_id}` и получает geometry
  из `GET /api/gip_vision/session/by_virtual_anchor/{session_id}/`.

Circle directional:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/circle-directional/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_file=@/absolute/path/model.glb" \
  -F "virtual_anchor.name=Circle placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=2" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=1"
```

Несколько моделей для одного virtual anchor:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/circle-directional/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_files=@/absolute/path/architecture.glb" \
  -F "model_files=@/absolute/path/engineering.ifc" \
  -F "virtual_anchor.name=Multi-model placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=2" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=1"
```

Rectangle short side:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/rectangle-short-side/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_file=@/absolute/path/model.glb" \
  -F "virtual_anchor.name=Rectangle placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F 'virtual_anchor.rectangle_geometry.short_side_point_1=[0,0,0]' \
  -F 'virtual_anchor.rectangle_geometry.short_side_point_2=[0,1,0]' \
  -F 'virtual_anchor.rectangle_geometry.opposite_short_side_center=[2,0.5,0]' \
  -F 'virtual_anchor.rectangle_geometry.plane_normal=[0,0,-1]'
```

### Нормализация GLB по IfcSite.ObjectPlacement

Используйте `ifc_site_placement` только для GLB, который уже был экспортирован из IFC, где смещение
или поворот `IfcSite.ObjectPlacement` запечены в координаты GLB. Для IFC uploads поле не передаётся:
сервер сам читает `IfcSite.ObjectPlacement` из IFC.

Порядок применения:
1. Из исходного IFC считайте `IfcSite.ObjectPlacement.RelativePlacement.Location.Coordinates`, `Axis`
   и `RefDirection`.
2. Передайте `translation` этими координатами, обычно в миллиметрах и `z-up`.
3. Если есть поворот через `RefDirection`, передайте эквивалентный yaw как
   `rotation_deg=[0, 0, <yaw_deg>]`.
4. Сервер применит обратную трансформацию к GLB и сохранит модель в локальных координатах.

Multipart-поля для загрузки уже экспортированного GLB:
```bash
-F "model_file=@/path/to/source_with_site_offset.glb" \
-F "ifc_site_placement.translation=<site_x_mm>" \
-F "ifc_site_placement.translation=<site_y_mm>" \
-F "ifc_site_placement.translation=<site_z_mm>" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=<site_yaw_deg>" \
-F "ifc_site_placement.units=mm" \
-F "ifc_site_placement.coordinate_system=z-up"
```

Команды ниже помогают проверить, что исходный GLB отличается из-за site placement, а результат после
локализации совпадает с эталонным локальным GLB. Нужны `IfcConvert`, `ifcopenshell`, `trimesh` и `numpy`.

Демо-файлы для самостоятельной проверки:
- [source_with_site_offset.ifc](/static/gip_vision/demo/site_placement/source_with_site_offset.ifc) —
  исходный IFC с ненулевым `IfcSite.ObjectPlacement`.
- [reference_local_origin.ifc](/static/gip_vision/demo/site_placement/reference_local_origin.ifc) —
  тот же пример в локальном начале координат.
- [source_as_is.glb](/static/gip_vision/demo/site_placement/source_as_is.glb) —
  GLB, экспортированный из исходного IFC без нормализации.
- [reference_local.glb](/static/gip_vision/demo/site_placement/reference_local.glb) —
  GLB из локального эталонного IFC.
- [source_site_local.glb](/static/gip_vision/demo/site_placement/source_site_local.glb) —
  результирующий GLB после обнуления site placement.

```bash
export WORK_DIR="/tmp/gip_vision_site_placement_demo"
mkdir -p "$WORK_DIR"

# Укажите host вашего API. Для production это обычно https://api-cpsk-superapp.gip.su
export DEMO_BASE_URL="https://<api-host>/static/gip_vision/demo/site_placement"
export SOURCE_IFC="$WORK_DIR/source_with_site_offset.ifc"
export REFERENCE_IFC="$WORK_DIR/reference_local_origin.ifc"

curl -L "$DEMO_BASE_URL/source_with_site_offset.ifc" -o "$SOURCE_IFC"
curl -L "$DEMO_BASE_URL/reference_local_origin.ifc" -o "$REFERENCE_IFC"

python - <<'PY'
from pathlib import Path
import math
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
ifc = ifcopenshell.open(str(source))
site = ifc.by_type("IfcSite")[0]
relative = site.ObjectPlacement.RelativePlacement
coords = tuple(float(value) for value in relative.Location.Coordinates)
axis = None if relative.Axis is None else tuple(float(value) for value in relative.Axis.DirectionRatios)
ref = None if relative.RefDirection is None else tuple(float(value) for value in relative.RefDirection.DirectionRatios)
yaw_deg = None if ref is None else math.degrees(math.atan2(ref[1], ref[0]))
print("translation_mm=", coords)
print("axis=", axis)
print("ref_direction=", ref)
print("yaw_deg=", yaw_deg)
PY

IfcConvert --no-progress -y "$SOURCE_IFC" "$WORK_DIR/source_as_is.glb"
IfcConvert --no-progress -y "$REFERENCE_IFC" "$WORK_DIR/reference_local.glb"

python - <<'PY'
from pathlib import Path
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
work_dir = Path(os.environ["WORK_DIR"])
out = work_dir / "source_site_local.ifc"
ifc = ifcopenshell.open(str(source))
for site in ifc.by_type("IfcSite"):
    relative = site.ObjectPlacement.RelativePlacement
    relative.Location.Coordinates = (0.0, 0.0, 0.0)
    relative.Axis = None
    relative.RefDirection = None
ifc.write(str(out))
print(out)
PY

IfcConvert --no-progress -y "$WORK_DIR/source_site_local.ifc" "$WORK_DIR/source_site_local.glb"

python - <<'PY'
from pathlib import Path
import os
import numpy as np
import trimesh

work_dir = Path(os.environ["WORK_DIR"])

def stats(path):
    scene = trimesh.load(path, force="scene", process=False)
    bounds = np.asarray(scene.bounds, dtype=float)
    center = (bounds[0] + bounds[1]) / 2
    extents = bounds[1] - bounds[0]
    vertices = sum(len(getattr(geom, "vertices", [])) for geom in scene.geometry.values())
    faces = sum(len(getattr(geom, "faces", [])) for geom in scene.geometry.values())
    return center, extents, vertices, faces

reference = stats(work_dir / "reference_local.glb")
for name in ("source_as_is", "source_site_local"):
    current = stats(work_dir / f"{name}.glb")
    print(
        f"{name} vs reference_local:",
        "center_delta_m=", float(np.linalg.norm(current[0] - reference[0])),
        "extents_delta_m=", float(np.linalg.norm(current[1] - reference[1])),
        "vertices_delta=", current[2] - reference[2],
        "faces_delta=", current[3] - reference[3],
    )
PY
```

Ожидаемый результат: `source_as_is.glb` заметно отличается от `reference_local.glb`, а
`source_site_local.glb` после обнуления site placement имеет близкий к нулю `center_delta_m` и нулевые
дельты по топологии.

# paths./api/gip-vision/v1/session/by_virtual_anchor/rectangle-short-sid.post.summary
Создать сессию: размещение по virtual anchor, прямоугольник

# paths./api/gip-vision/v1/session/by_virtual_anchor/segment-directional.post.description
Создаёт AR-сессию с размещением по готовой geometry virtual anchor отрезка. Метод требует `X-API-Key` или `Bearer`, принимает `model_file` или несколько `model_files`, конвертирует каждую модель через USDZ в Reality; `ready` выставляется только после успешного Reality callback для всех `model_assets` и не создаёт VirtualAnchor в БД.

### Размещение по virtual anchor без предварительного создания анкера

Public protected методы `by_virtual_anchor` принимают модель целиком и минимальные точки анкера,
из которых backend собирает iOS-compatible geometry. Пользователь не должен заранее создавать `VirtualAnchor`
через private API.

Важно:
- backend не создаёт запись `VirtualAnchor`;
- backend не режет исходный GLB/USDZ/IFC на chunks и не запускает crop pipeline;
- модель или набор моделей конвертируется целиком в per-file `model_assets` (`Reality`, fallback `USDZ`);
- для GLB действует лимит публичного API: не больше 5000 mesh-node элементов на файл;
- можно передать один `model_file` или несколько multipart parts `model_files`;
- для `model_files` разрешены GLB и IFC, смешивать `model_file` и `model_files` нельзя;
- для request нужны минимальные точки анкера и `plane_normal` поверхности, а `center`, `radius`,
  `normal_tip`, `diameter_direction`, `corners`, segment fields и длины сторон вычисляет backend;
- iOS открывает deeplink `gipvision://session/by_virtual_anchor/{session_id}` и получает geometry
  из `GET /api/gip_vision/session/by_virtual_anchor/{session_id}/`.

Circle directional:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/circle-directional/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_file=@/absolute/path/model.glb" \
  -F "virtual_anchor.name=Circle placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=2" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=1"
```

Несколько моделей для одного virtual anchor:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/circle-directional/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_files=@/absolute/path/architecture.glb" \
  -F "model_files=@/absolute/path/engineering.ifc" \
  -F "virtual_anchor.name=Multi-model placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_1=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=2" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.diameter_point_2=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=0" \
  -F "virtual_anchor.circle_geometry.plane_normal=1"
```

Rectangle short side:
```bash
curl -X POST "{server_url}/api/gip-vision/v1/session/by_virtual_anchor/rectangle-short-side/" \
  -H "X-API-Key: $GIP_VISION_API_KEY" \
  -F "model_file=@/absolute/path/model.glb" \
  -F "virtual_anchor.name=Rectangle placement" \
  -F "virtual_anchor.coordinate_system=y-up" \
  -F 'virtual_anchor.rectangle_geometry.short_side_point_1=[0,0,0]' \
  -F 'virtual_anchor.rectangle_geometry.short_side_point_2=[0,1,0]' \
  -F 'virtual_anchor.rectangle_geometry.opposite_short_side_center=[2,0.5,0]' \
  -F 'virtual_anchor.rectangle_geometry.plane_normal=[0,0,-1]'
```

### Нормализация GLB по IfcSite.ObjectPlacement

Используйте `ifc_site_placement` только для GLB, который уже был экспортирован из IFC, где смещение
или поворот `IfcSite.ObjectPlacement` запечены в координаты GLB. Для IFC uploads поле не передаётся:
сервер сам читает `IfcSite.ObjectPlacement` из IFC.

Порядок применения:
1. Из исходного IFC считайте `IfcSite.ObjectPlacement.RelativePlacement.Location.Coordinates`, `Axis`
   и `RefDirection`.
2. Передайте `translation` этими координатами, обычно в миллиметрах и `z-up`.
3. Если есть поворот через `RefDirection`, передайте эквивалентный yaw как
   `rotation_deg=[0, 0, <yaw_deg>]`.
4. Сервер применит обратную трансформацию к GLB и сохранит модель в локальных координатах.

Multipart-поля для загрузки уже экспортированного GLB:
```bash
-F "model_file=@/path/to/source_with_site_offset.glb" \
-F "ifc_site_placement.translation=<site_x_mm>" \
-F "ifc_site_placement.translation=<site_y_mm>" \
-F "ifc_site_placement.translation=<site_z_mm>" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=0" \
-F "ifc_site_placement.rotation_deg=<site_yaw_deg>" \
-F "ifc_site_placement.units=mm" \
-F "ifc_site_placement.coordinate_system=z-up"
```

Команды ниже помогают проверить, что исходный GLB отличается из-за site placement, а результат после
локализации совпадает с эталонным локальным GLB. Нужны `IfcConvert`, `ifcopenshell`, `trimesh` и `numpy`.

Демо-файлы для самостоятельной проверки:
- [source_with_site_offset.ifc](/static/gip_vision/demo/site_placement/source_with_site_offset.ifc) —
  исходный IFC с ненулевым `IfcSite.ObjectPlacement`.
- [reference_local_origin.ifc](/static/gip_vision/demo/site_placement/reference_local_origin.ifc) —
  тот же пример в локальном начале координат.
- [source_as_is.glb](/static/gip_vision/demo/site_placement/source_as_is.glb) —
  GLB, экспортированный из исходного IFC без нормализации.
- [reference_local.glb](/static/gip_vision/demo/site_placement/reference_local.glb) —
  GLB из локального эталонного IFC.
- [source_site_local.glb](/static/gip_vision/demo/site_placement/source_site_local.glb) —
  результирующий GLB после обнуления site placement.

```bash
export WORK_DIR="/tmp/gip_vision_site_placement_demo"
mkdir -p "$WORK_DIR"

# Укажите host вашего API. Для production это обычно https://api-cpsk-superapp.gip.su
export DEMO_BASE_URL="https://<api-host>/static/gip_vision/demo/site_placement"
export SOURCE_IFC="$WORK_DIR/source_with_site_offset.ifc"
export REFERENCE_IFC="$WORK_DIR/reference_local_origin.ifc"

curl -L "$DEMO_BASE_URL/source_with_site_offset.ifc" -o "$SOURCE_IFC"
curl -L "$DEMO_BASE_URL/reference_local_origin.ifc" -o "$REFERENCE_IFC"

python - <<'PY'
from pathlib import Path
import math
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
ifc = ifcopenshell.open(str(source))
site = ifc.by_type("IfcSite")[0]
relative = site.ObjectPlacement.RelativePlacement
coords = tuple(float(value) for value in relative.Location.Coordinates)
axis = None if relative.Axis is None else tuple(float(value) for value in relative.Axis.DirectionRatios)
ref = None if relative.RefDirection is None else tuple(float(value) for value in relative.RefDirection.DirectionRatios)
yaw_deg = None if ref is None else math.degrees(math.atan2(ref[1], ref[0]))
print("translation_mm=", coords)
print("axis=", axis)
print("ref_direction=", ref)
print("yaw_deg=", yaw_deg)
PY

IfcConvert --no-progress -y "$SOURCE_IFC" "$WORK_DIR/source_as_is.glb"
IfcConvert --no-progress -y "$REFERENCE_IFC" "$WORK_DIR/reference_local.glb"

python - <<'PY'
from pathlib import Path
import os
import ifcopenshell

source = Path(os.environ["SOURCE_IFC"])
work_dir = Path(os.environ["WORK_DIR"])
out = work_dir / "source_site_local.ifc"
ifc = ifcopenshell.open(str(source))
for site in ifc.by_type("IfcSite"):
    relative = site.ObjectPlacement.RelativePlacement
    relative.Location.Coordinates = (0.0, 0.0, 0.0)
    relative.Axis = None
    relative.RefDirection = None
ifc.write(str(out))
print(out)
PY

IfcConvert --no-progress -y "$WORK_DIR/source_site_local.ifc" "$WORK_DIR/source_site_local.glb"

python - <<'PY'
from pathlib import Path
import os
import numpy as np
import trimesh

work_dir = Path(os.environ["WORK_DIR"])

def stats(path):
    scene = trimesh.load(path, force="scene", process=False)
    bounds = np.asarray(scene.bounds, dtype=float)
    center = (bounds[0] + bounds[1]) / 2
    extents = bounds[1] - bounds[0]
    vertices = sum(len(getattr(geom, "vertices", [])) for geom in scene.geometry.values())
    faces = sum(len(getattr(geom, "faces", [])) for geom in scene.geometry.values())
    return center, extents, vertices, faces

reference = stats(work_dir / "reference_local.glb")
for name in ("source_as_is", "source_site_local"):
    current = stats(work_dir / f"{name}.glb")
    print(
        f"{name} vs reference_local:",
        "center_delta_m=", float(np.linalg.norm(current[0] - reference[0])),
        "extents_delta_m=", float(np.linalg.norm(current[1] - reference[1])),
        "vertices_delta=", current[2] - reference[2],
        "faces_delta=", current[3] - reference[3],
    )
PY
```

Ожидаемый результат: `source_as_is.glb` заметно отличается от `reference_local.glb`, а
`source_site_local.glb` после обнуления site placement имеет близкий к нулю `center_delta_m` и нулевые
дельты по топологии.

# paths./api/gip-vision/v1/session/by_virtual_anchor/segment-directional.post.summary
Создать сессию: размещение по virtual anchor, отрезок

# paths./api/gip-vision/v1/session/resolve-by-onetime-code/.post.description
Возвращает результат по одноразовому коду (`onetime_code`) от POST /session/by_scan/ или от публичного `SpatialChunkProject`.

Алгоритм обработки `onetime_code`:
- `ok`: код привязан к активной сессии `by_scan`/`by_virtual_anchor` — возвращается `deeplink` и `expires_at`.
  Код многоразовый и доступен до `expires_at`.
- `ok_spatial`: код привязан к `SpatialChunkProject` — возвращается `spatial_project_id` и `expires_at` (TTL 30 дней).
  Доступ разрешён только для публичного проекта или для авторизованного пользователя,
  у которого есть доступ к проекту (`owner` / `shared`).
- `ok_virtual_anchor`/`ok_visualization_set`: код привязан к session-like сущности — возвращается `deeplink` и `expires_at`.
- `not_found`/`ambiguous`/`invalid`/`expired`/`expired_spatial`: возвращаются ошибки по коду HTTP-статусу.

# paths./api/gip-vision/v1/session/resolve-by-onetime-code/.post.responses.200.description
Двух вариантов успешного ответа:
- `by_scan` сессия: `deeplink` + `expires_at`.
- Доступный Spatial project: `spatial_project_id` + `expires_at`.

# paths./api/gip-vision/v1/session/resolve-by-onetime-code/.post.responses.400.description
Недопустимый формат кода (не 6 цифр)

# paths./api/gip-vision/v1/session/resolve-by-onetime-code/.post.responses.404.description
Код не найден

# paths./api/gip-vision/v1/session/resolve-by-onetime-code/.post.responses.409.description
Неоднозначная привязка кода

# paths./api/gip-vision/v1/session/resolve-by-onetime-code/.post.responses.410.description
Срок действия кода истёк

# paths./api/gip-vision/v1/session/resolve-by-onetime-code/.post.summary
Получить результат по одноразовому коду

# paths./api/gip-vision/v1/session/usage/start/.post.description
Создаёт usage-запись для фактического открытия AR-сессии в приложении по `model_file_id` и возвращает backend-generated `usage_id` для heartbeat и finish.

# paths./api/gip-vision/v1/session/usage/start/.post.responses.403.description
Нет доступа к AR-проекту указанной модели

# paths./api/gip-vision/v1/session/usage/start/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/session/usage/start/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/session/usage/start/.post.summary
Начать использование AR-сессии

# paths./api/gip-vision/v1/session/usage/start/.post.x-codeSamples[].source
curl --request POST \
  'https://api-cpsk-superapp.gip.su/api/gip-vision/v1/session/usage/start/' \
  --header 'X-API-Key: YOUR_API_KEY'

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.description
Явно завершает usage записи AR-сессии и считает длительность.

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.responses.403.description
Отзыв может оставить только аутентифицированный владелец usage с текущим доступом к AR-проекту

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.responses.404.description
Usage не найден

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.responses.409.description
Отзыв для usage уже был сохранён с другими данными

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.responses.400.description
Некорректные параметры запроса.

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.responses.400.content.application/json.examples.error.value.detail
Проверьте параметры запроса.

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/session/usage/{usage_id}/finish/.post.summary
Завершить использование AR-сессии

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.description
Обновляет last_seen для активного usage. Предназначен для частого polling/heartbeat с клиента примерно каждые 10 секунд.

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.responses.404.description
Usage не найден

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.responses.409.description
Usage уже завершён

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.responses.400.description
Некорректные параметры запроса.

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.responses.400.content.application/json.examples.error.value.detail
Проверьте параметры запроса.

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.responses.401.description
Авторизация не пройдена.

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.responses.401.content.application/json.examples.error.value.detail
Учетные данные не были предоставлены.

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.responses.403.description
Недостаточно прав для операции.

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.responses.403.content.application/json.examples.error.value.detail
У вас недостаточно прав для выполнения данного действия.

# paths./api/gip-vision/v1/session/usage/{usage_id}/heartbeat/.post.summary
Heartbeat использования AR-сессии

# tags[].name
Сервис и доступ

# tags[].description
Проверка доступности API и валидности ключа.

# tags[].description
Создание AR-сессий и работа с one-time кодами.

# tags[].description
Генерация печатных маркеров и служебных tag-изображений.

# tags[].description
Получение и создание GIP Vision проектов внешними интеграциями.

# tags[].name
Модели проекта

# tags[].description
Получение ограниченного списка моделей и загрузка IFC с постановкой на конвертацию.

# tags[].name
Задачи проекта

# tags[].description
Доски задач, создание задач и чтение задач проекта.

# tags[].description
Скачивание и импорт BCF-архивов задач проекта.