Initial commit
This commit is contained in:
commit
a437c068c8
64 changed files with 561683 additions and 0 deletions
80
.gitattributes
vendored
Normal file
80
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Default behavior
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Code files – use LF line endings
|
||||||
|
*.js text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.scss text eol=lf
|
||||||
|
*.php text eol=lf
|
||||||
|
*.module text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.twig text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
|
||||||
|
# BUILD
|
||||||
|
dist/** text eol=lf
|
||||||
|
|
||||||
|
# Binary files
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.svg binary
|
||||||
|
*.webp binary
|
||||||
|
*.ico binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.ogg binary
|
||||||
|
*.wav binary
|
||||||
|
*.glb binary
|
||||||
|
*.gltf binary
|
||||||
|
*.bin binary
|
||||||
|
*.zip binary
|
||||||
|
*.pdf binary
|
||||||
|
*.exe binary
|
||||||
|
*.dll binary
|
||||||
|
*.obj binary
|
||||||
|
*.mtl binary
|
||||||
|
*.3ds binary
|
||||||
|
*.fbx binary
|
||||||
|
*.dae binary
|
||||||
|
*.stl binary
|
||||||
|
*.ply binary
|
||||||
|
*.hdr binary
|
||||||
|
*.pic binary
|
||||||
|
*.tga binary
|
||||||
|
*.psd binary
|
||||||
|
*.raw binary
|
||||||
|
*.arw binary
|
||||||
|
*.cr2 binary
|
||||||
|
*.nef binary
|
||||||
|
*.orf binary
|
||||||
|
*.sr2 binary
|
||||||
|
*.xmp binary
|
||||||
|
*.db binary
|
||||||
|
*.sav binary
|
||||||
|
*.blend binary
|
||||||
|
*.3dm binary
|
||||||
|
*.skp binary
|
||||||
|
*.max binary
|
||||||
|
*.c4d binary
|
||||||
|
*.lwo binary
|
||||||
|
*.lws binary
|
||||||
|
*.ma binary
|
||||||
|
*.mb binary
|
||||||
|
*.dxf binary
|
||||||
|
*.dwg binary
|
||||||
|
*.igs binary
|
||||||
|
*.iges binary
|
||||||
|
*.step binary
|
||||||
|
*.stp binary
|
||||||
|
*.sldprt binary
|
||||||
|
*.sldasm binary
|
||||||
|
*.wasm binary
|
||||||
|
*.pcd binary
|
||||||
|
*.xyz binary
|
||||||
|
*.ifc binary
|
||||||
0
.github/agents/ChatGPT.agent.md
vendored
Normal file
0
.github/agents/ChatGPT.agent.md
vendored
Normal file
32
.github/workflows/build-release.yml
vendored
Normal file
32
.github/workflows/build-release.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
name: Pack Drupal module
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pack:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
|
||||||
|
- name: Pack module zip
|
||||||
|
run: node scripts/pack-drupal-module.js
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: Release ${{ github.ref_name }}
|
||||||
|
files: ./dfg_3dviewer-drupal.zip
|
||||||
12
.gitignore
vendored
Executable file
12
.gitignore
vendored
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# editor backups
|
||||||
|
*.yml~
|
||||||
|
*.module~
|
||||||
|
*.php~
|
||||||
|
|
||||||
|
# local env
|
||||||
|
scripts/.env
|
||||||
|
|
||||||
|
# release artifact
|
||||||
|
/dfg_3dviewer-drupal.zip
|
||||||
|
.pack-drupal-staging/
|
||||||
|
node_modules
|
||||||
49
README.md
Normal file
49
README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# DFG 3D Viewer — Drupal Module
|
||||||
|
|
||||||
|
Drupal/WissKI integration for the [DFG 3D Viewer JavaScript library](https://github.com/your-org/js_library_dfg_3dviewer).
|
||||||
|
|
||||||
|
**This repository contains PHP/YAML only** — no bundled JavaScript, no npm build step on the server.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. **JS library** at `web/libraries/dfg-3dviewer/`:
|
||||||
|
- Download `dfg-3dviewer-library.zip` from the [library releases](https://github.com/your-org/js_library_dfg_3dviewer/releases), or
|
||||||
|
- Build from the `js_library_dfg_3dviewer` repo: `npm run build:library` and copy `dist/library/*` there
|
||||||
|
|
||||||
|
2. **This module** at `web/modules/custom/dfg_3dviewer/`
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
unzip dfg-3dviewer-library.zip -d web/libraries/dfg-3dviewer
|
||||||
|
# clone or unzip this module to web/modules/custom/dfg_3dviewer
|
||||||
|
drush en dfg_3dviewer -y
|
||||||
|
drush cr
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure at `/admin/config/dfg_3dviewer`. Settings are passed to the browser as `drupalSettings.dfg_3dviewer`.
|
||||||
|
|
||||||
|
## Package contents
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/` | Controllers, form, field formatters, queue worker |
|
||||||
|
| `config/` | Default config and schema |
|
||||||
|
| `dfg_3dviewer.*.yml` | Routing, libraries, permissions |
|
||||||
|
| `dfg_3dviewer.module` | Hooks, conversion workflow |
|
||||||
|
| `scripts/` | Model conversion shell/Python scripts |
|
||||||
|
|
||||||
|
## Library attachment
|
||||||
|
|
||||||
|
`dfg_3dviewer.libraries.yml` loads:
|
||||||
|
|
||||||
|
- `/libraries/dfg-3dviewer/dfg_3dviewer.min.js`
|
||||||
|
- `/libraries/dfg-3dviewer/assets/css/viewer.css`
|
||||||
|
|
||||||
|
## Release zip (maintainers)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/pack-drupal-module.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `dfg_3dviewer-drupal.zip`.
|
||||||
23
config/install/dfg_3dviewer.settings.yml
Normal file
23
config/install/dfg_3dviewer.settings.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
dfg_3dviewer_main_url: "https://dfg-repository.wisski.cloud"
|
||||||
|
dfg_3dviewer_metadata_url: "https://dfg-repository.wisski.cloud"
|
||||||
|
dfg_3dviewer_json_export_base_url: "https://repository.covher.eu"
|
||||||
|
dfg_3dviewer_basenamespace: ""
|
||||||
|
dfg_3dviewer_container: "DFG_3DViewer"
|
||||||
|
dfg_3dviewer_entitybundle: "bd3d7baa74856d141bcff7b4193fa128"
|
||||||
|
dfg_3dviewer_viewer_file_upload: "fbf95bddee5160d515b982b3fd2e05f7"
|
||||||
|
dfg_3dviewer_viewer_file_name: "faa602a0be629324806aef22892cdbe5"
|
||||||
|
dfg_3dviewer_api_3d_file_field: ""
|
||||||
|
dfg_3dviewer_image_generation: "f605dc6b727a1099b9e52b3ccbdf5673"
|
||||||
|
dfg_3dviewer_field_df: "field_df"
|
||||||
|
dfg_3dviewer_export_viewer: "field_df"
|
||||||
|
dfg_3dviewer_export_viewer_url: ""
|
||||||
|
dfg_3dviewer_lightweight: false
|
||||||
|
dfg_3dviewer_scale_container_x: "1"
|
||||||
|
dfg_3dviewer_scale_container_y: "1.4"
|
||||||
|
dfg_3dviewer_gallery_container: "block-bootstrap5-content"
|
||||||
|
dfg_3dviewer_gallery_image_class: "field--name-fd6a974b7120d422c7b21b5f1f2315d9"
|
||||||
|
dfg_3dviewer_gallery_image_id: ""
|
||||||
|
dfg_3dviewer_base_module_path: "/libraries/dfg-3dviewer/assets"
|
||||||
|
dfg_3dviewer_entity_id_uri: "/wisski/navigate/(.*)/view"
|
||||||
|
dfg_3dviewer_view_entity_path: "/wisski/navigate/"
|
||||||
|
dfg_3dviewer_attribute_id: "wisski_id"
|
||||||
73
config/schema/dfg_3dviewer.schema.yml
Normal file
73
config/schema/dfg_3dviewer.schema.yml
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
dfg_3dviewer.settings:
|
||||||
|
type: config_object
|
||||||
|
label: 'Configuration of DFG 3D Viewer'
|
||||||
|
mapping:
|
||||||
|
dfg_3dviewer_main_url:
|
||||||
|
type: string
|
||||||
|
label: 'Main URL'
|
||||||
|
dfg_3dviewer_basenamespace:
|
||||||
|
type: string
|
||||||
|
label: 'Default base namespace'
|
||||||
|
dfg_3dviewer_metadata_url:
|
||||||
|
type: string
|
||||||
|
label: 'Metadata URL'
|
||||||
|
dfg_3dviewer_json_export_base_url:
|
||||||
|
type: string
|
||||||
|
label: 'JSON export base URL'
|
||||||
|
dfg_3dviewer_container:
|
||||||
|
type: string
|
||||||
|
label: 'Container ID'
|
||||||
|
dfg_3dviewer_entitybundle:
|
||||||
|
type: string
|
||||||
|
label: 'Entity bundle ID'
|
||||||
|
dfg_3dviewer_viewer_file_upload:
|
||||||
|
type: string
|
||||||
|
label: 'Viewer file upload field ID'
|
||||||
|
dfg_3dviewer_viewer_file_name:
|
||||||
|
type: string
|
||||||
|
label: 'Viewer file name field ID'
|
||||||
|
dfg_3dviewer_api_3d_file_field:
|
||||||
|
type: string
|
||||||
|
label: 'API 3D file field'
|
||||||
|
dfg_3dviewer_image_generation:
|
||||||
|
type: string
|
||||||
|
label: 'Image generation field ID'
|
||||||
|
dfg_3dviewer_field_df:
|
||||||
|
type: string
|
||||||
|
label: 'Field DF'
|
||||||
|
dfg_3dviewer_export_viewer:
|
||||||
|
type: string
|
||||||
|
label: 'Export viewer field'
|
||||||
|
dfg_3dviewer_export_viewer_url:
|
||||||
|
type: string
|
||||||
|
label: 'Export viewer URL'
|
||||||
|
dfg_3dviewer_lightweight:
|
||||||
|
type: boolean
|
||||||
|
label: 'Lightweight mode'
|
||||||
|
dfg_3dviewer_scale_container_x:
|
||||||
|
type: string
|
||||||
|
label: 'Scale container X'
|
||||||
|
dfg_3dviewer_scale_container_y:
|
||||||
|
type: string
|
||||||
|
label: 'Scale container Y'
|
||||||
|
dfg_3dviewer_gallery_container:
|
||||||
|
type: string
|
||||||
|
label: 'Gallery container element'
|
||||||
|
dfg_3dviewer_gallery_image_class:
|
||||||
|
type: string
|
||||||
|
label: 'Gallery image class'
|
||||||
|
dfg_3dviewer_gallery_image_id:
|
||||||
|
type: string
|
||||||
|
label: 'Gallery image ID'
|
||||||
|
dfg_3dviewer_base_module_path:
|
||||||
|
type: string
|
||||||
|
label: 'Viewer asset base path'
|
||||||
|
dfg_3dviewer_entity_id_uri:
|
||||||
|
type: string
|
||||||
|
label: 'Entity ID URI regex'
|
||||||
|
dfg_3dviewer_view_entity_path:
|
||||||
|
type: string
|
||||||
|
label: 'View entity path'
|
||||||
|
dfg_3dviewer_attribute_id:
|
||||||
|
type: string
|
||||||
|
label: 'WissKI attribute ID'
|
||||||
6
dfg_3dviewer.info.yml
Executable file
6
dfg_3dviewer.info.yml
Executable file
|
|
@ -0,0 +1,6 @@
|
||||||
|
name: DFG 3D Viewer
|
||||||
|
type: module
|
||||||
|
description: Drupal integration for the DFG 3D Viewer JavaScript library (install separately to web/libraries/dfg-3dviewer/).
|
||||||
|
core_version_requirement: ^10 || ^11
|
||||||
|
dependencies:
|
||||||
|
- drupal:field
|
||||||
19
dfg_3dviewer.libraries.yml
Executable file
19
dfg_3dviewer.libraries.yml
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
dfg_3dviewer.viewer:
|
||||||
|
js:
|
||||||
|
/libraries/dfg-3dviewer/dfg_3dviewer.min.js:
|
||||||
|
attributes:
|
||||||
|
type: module
|
||||||
|
minified: true
|
||||||
|
css:
|
||||||
|
theme:
|
||||||
|
/libraries/dfg-3dviewer/assets/css/viewer.css:
|
||||||
|
weight: 10
|
||||||
|
dependencies:
|
||||||
|
- core/drupal
|
||||||
|
- core/drupalSettings
|
||||||
|
|
||||||
|
dfg_3dviewer.progress:
|
||||||
|
js:
|
||||||
|
scripts/progress.js: {}
|
||||||
|
dependencies:
|
||||||
|
- core/drupal
|
||||||
10
dfg_3dviewer.links.menu.yml
Executable file
10
dfg_3dviewer.links.menu.yml
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
dfg_3dviewer.config_content:
|
||||||
|
title: '3D Viewer Settings'
|
||||||
|
parent: dfg_3dviewer.menu_content
|
||||||
|
weight: -100
|
||||||
|
route_name: dfg_3dviewer.config_menu
|
||||||
|
dfg_3dviewer.menu_content:
|
||||||
|
title: 'DFG 3D Viewer'
|
||||||
|
parent: system.admin_config
|
||||||
|
weight: -100
|
||||||
|
route_name: dfg_3dviewer.menu
|
||||||
1438
dfg_3dviewer.module
Executable file
1438
dfg_3dviewer.module
Executable file
File diff suppressed because it is too large
Load diff
7
dfg_3dviewer.permissions.yml
Executable file
7
dfg_3dviewer.permissions.yml
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
access dfg3dmodels:
|
||||||
|
title: 'Access 3D Models via DFG Viewer'
|
||||||
|
administer dfg_3dviewer:
|
||||||
|
title: 'Administer DFG 3D Viewer settings'
|
||||||
|
restrict access: TRUE
|
||||||
|
access dfg_3dviewer editor:
|
||||||
|
title: 'Access 3D Viewer metadata editor'
|
||||||
54
dfg_3dviewer.routing.yml
Normal file
54
dfg_3dviewer.routing.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
entity.wisski_individual.manifest:
|
||||||
|
path: '/wisski/dfg_3dviewer/{wisski_individual}/savePreview'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\dfg_3dviewer\Controller\DFG3dController::editEntity'
|
||||||
|
requirements:
|
||||||
|
_access: 'TRUE'
|
||||||
|
dfg_3dviewer.config_menu:
|
||||||
|
path: '/admin/config/dfg_3dviewer'
|
||||||
|
defaults:
|
||||||
|
_form: '\Drupal\dfg_3dviewer\Form\DFG3dViewerConfigForm'
|
||||||
|
requirements:
|
||||||
|
_permission: 'administer dfg_3dviewer'
|
||||||
|
dfg_3dviewer.menu:
|
||||||
|
path: '/admin/config/dfg_3dviewer'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
|
||||||
|
_title: 'DFG 3D Viewer'
|
||||||
|
requirements:
|
||||||
|
_permission: 'administer dfg_3dviewer'
|
||||||
|
dfg_3dviewer.save_metadata:
|
||||||
|
path: '/api/editor/save-metadata'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\dfg_3dviewer\Controller\SaveMetadataController::save'
|
||||||
|
methods: [POST]
|
||||||
|
requirements:
|
||||||
|
_permission: 'access dfg 3dviewer editor'
|
||||||
|
dfg_3dviewer.thumbnail_upload:
|
||||||
|
path: '/api/editor/upload-thumbnail'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\dfg_3dviewer\Controller\ThumbnailUploadController::upload'
|
||||||
|
methods: [POST]
|
||||||
|
requirements:
|
||||||
|
_permission: 'access dfg 3dviewer editor'
|
||||||
|
dfg_3dviewer.xml_export:
|
||||||
|
path: '/api/editor/xml-export/{id}'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\dfg_3dviewer\Controller\XmlExportController::export'
|
||||||
|
methods: [GET, POST]
|
||||||
|
requirements:
|
||||||
|
_permission: 'access dfg 3dviewer editor'
|
||||||
|
dfg_3dviewer.status:
|
||||||
|
path: '/api/model/status/{id}'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\dfg_3dviewer\Controller\ModelController::status'
|
||||||
|
methods: [GET]
|
||||||
|
requirements:
|
||||||
|
_permission: 'access content'
|
||||||
|
dfg_3dviewer.create:
|
||||||
|
path: '/api/model/create'
|
||||||
|
defaults:
|
||||||
|
_controller: '\Drupal\dfg_3dviewer\Controller\ModelController::create'
|
||||||
|
methods: [POST]
|
||||||
|
requirements:
|
||||||
|
_permission: 'access content'
|
||||||
7
dfg_3dviewer.services.yml
Normal file
7
dfg_3dviewer.services.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
services:
|
||||||
|
dfg_3dviewer.model_format_manager:
|
||||||
|
class: Drupal\dfg_3dviewer\Service\ModelFormatManager
|
||||||
|
dfg_3dviewer.convert_process:
|
||||||
|
class: Drupal\dfg_3dviewer\Service\ConvertProcessService
|
||||||
|
arguments:
|
||||||
|
- '@logger.factory'
|
||||||
5
dfg_3dviewer.translation.yml
Executable file
5
dfg_3dviewer.translation.yml
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
dfg_3dviewer.admin.config:
|
||||||
|
title: 'DFG 3D Viewer Module'
|
||||||
|
base_route_name: dfg_3dviewer.admin.config
|
||||||
|
names:
|
||||||
|
- dfg_3dviewer.settings
|
||||||
6
scripts/.env.example
Normal file
6
scripts/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
BLENDER_BIN=''
|
||||||
|
# Optional override. If empty, scripts auto-detect the module root from this file location.
|
||||||
|
SPATH=
|
||||||
|
BACKUP_SETTINGS_PATH=/var/www/data/project/web/sites/default/settings.php
|
||||||
|
RENDER_RESOLUTION='1024x1024x16'
|
||||||
|
RENDER_SAMPLES='20'
|
||||||
14
scripts/2gltf2/2gltf2.bat
Executable file
14
scripts/2gltf2/2gltf2.bat
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
ECHO OFF
|
||||||
|
|
||||||
|
IF "%1"=="" GOTO USAGE
|
||||||
|
|
||||||
|
"C:/Program Files/Blender Foundation/Blender 2.93/blender.exe" -b -P 2gltf2.py -- %1
|
||||||
|
GOTO END
|
||||||
|
|
||||||
|
:USAGE
|
||||||
|
ECHO To glTF 2.0 converter.
|
||||||
|
ECHO Supported file formats: .abc .blend .dae .fbx. .obj .ply .stl .wrl .x3d
|
||||||
|
ECHO.
|
||||||
|
ECHO 2gltf2.bat [filename]
|
||||||
|
|
||||||
|
:END
|
||||||
127
scripts/2gltf2/2gltf2.py
Executable file
127
scripts/2gltf2/2gltf2.py
Executable file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#
|
||||||
|
# The MIT License (MIT)
|
||||||
|
#
|
||||||
|
# Copyright (c) since 2017 UX3D GmbH
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Imports
|
||||||
|
#
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
if '--' in sys.argv:
|
||||||
|
argv = sys.argv[sys.argv.index('--') + 1:]
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--input", help="Input file path")
|
||||||
|
parser.add_argument("--ext", help="Extenstion of imported file")
|
||||||
|
parser.add_argument("--org_ext", help="Original extenstion of imported file")
|
||||||
|
parser.add_argument("--output", help="Output file path")
|
||||||
|
parser.add_argument("--is_archive", help="Importing archive flag")
|
||||||
|
parser.add_argument("--resolution", help="Resolution preview images")
|
||||||
|
parser.add_argument("--samples", help="Samples rendering quality")
|
||||||
|
parser.add_argument("--compression", help="Compress object")
|
||||||
|
parser.add_argument("--compression_level", help="Compress object level")
|
||||||
|
args = parser.parse_known_args(argv)[0]
|
||||||
|
|
||||||
|
print('input: ', args.input)
|
||||||
|
print('ext: ', args.ext)
|
||||||
|
print('org_ext: ', args.org_ext)
|
||||||
|
print('output: ', args.output)
|
||||||
|
print('is_archive: ', args.is_archive)
|
||||||
|
print('resolution: ', args.resolution)
|
||||||
|
print('samples: ', args.samples)
|
||||||
|
print('compression: ', args.compression)
|
||||||
|
print('compression_level: ', args.compression_level)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Globals
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Functions
|
||||||
|
#
|
||||||
|
current_directory = os.getcwd()
|
||||||
|
compression = "false"
|
||||||
|
compression_level = 3
|
||||||
|
|
||||||
|
if args.ext:
|
||||||
|
extension = args.ext
|
||||||
|
if extension == "gltf":
|
||||||
|
format = "GLTF_EMBEDDED"
|
||||||
|
else:
|
||||||
|
format = "GLB"
|
||||||
|
|
||||||
|
if args.org_ext:
|
||||||
|
original_extension = args.ext
|
||||||
|
|
||||||
|
if args.compression:
|
||||||
|
compression = args.compression
|
||||||
|
|
||||||
|
if args.compression_level:
|
||||||
|
compression_level = int(args.compression_level)
|
||||||
|
|
||||||
|
root, current_extension = os.path.splitext(args.input)
|
||||||
|
current_basename = os.path.basename(root)
|
||||||
|
|
||||||
|
if current_extension == ".abc" or current_extension == ".blend" or current_extension == ".dae" or current_extension == ".fbx" or current_extension == ".obj" or current_extension == ".ply" or current_extension == ".stl" or current_extension == ".wrl" or current_extension == ".x3d":
|
||||||
|
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
|
||||||
|
if current_extension == ".abc":
|
||||||
|
bpy.ops.wm.alembic_import(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".blend":
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".dae":
|
||||||
|
bpy.ops.wm.collada_import(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".fbx":
|
||||||
|
bpy.ops.import_scene.fbx(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".obj":
|
||||||
|
bpy.ops.wm.obj_import(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".ply":
|
||||||
|
bpy.ops.wm.ply_import(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".stl":
|
||||||
|
bpy.ops.import_mesh.stl(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".wrl" or current_extension == ".x3d":
|
||||||
|
bpy.ops.import_scene.x3d(filepath=args.input)
|
||||||
|
|
||||||
|
#
|
||||||
|
if args.output:
|
||||||
|
export_file = str(args.output)
|
||||||
|
#export_file = root + current_basename + "." + extension
|
||||||
|
else:
|
||||||
|
root = root[::-1].replace(current_basename[::-1], "", 1)[::-1]
|
||||||
|
export_file = root + "gltf/" + current_basename + "." + extension
|
||||||
|
print("Writing: '" + export_file + "'")
|
||||||
|
if compression == 'true':
|
||||||
|
bpy.ops.export_scene.gltf(filepath=export_file,export_format=format,export_draco_mesh_compression_enable=True,export_draco_mesh_compression_level=compression_level)
|
||||||
|
else:
|
||||||
|
bpy.ops.export_scene.gltf(filepath=export_file,export_format=format)
|
||||||
9
scripts/2gltf2/2gltf2.sh
Executable file
9
scripts/2gltf2/2gltf2.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#TODO PATH for Blender
|
||||||
|
if [ "$#" -ge 1 ]
|
||||||
|
then
|
||||||
|
/var/lib/snapd/snap/blender/current/blender -b -P 2gltf2.py -- "$1" "$2" "$3"
|
||||||
|
else
|
||||||
|
echo Supported file formats: .abc .blend .dae .fbx. .obj .ply .stl .wrl .x3d
|
||||||
|
echo 2gltf2.sh [filename]
|
||||||
|
fi
|
||||||
BIN
scripts/2gltf2/glTF.png
Executable file
BIN
scripts/2gltf2/glTF.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
837
scripts/CityGML2OBJv2/CityGML2OBJs.py
Normal file
837
scripts/CityGML2OBJv2/CityGML2OBJs.py
Normal file
|
|
@ -0,0 +1,837 @@
|
||||||
|
##!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import config
|
||||||
|
# The MIT License (MIT)
|
||||||
|
|
||||||
|
# This code is part of the CityGML2OBJs package
|
||||||
|
|
||||||
|
# Copyright (c) 2014
|
||||||
|
# Filip Biljecki
|
||||||
|
# Delft University of Technology
|
||||||
|
# fbiljecki@gmail.com
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
import markup3dmodule
|
||||||
|
import polygon3dmodule
|
||||||
|
import componentseparationmodule as csm
|
||||||
|
from lxml import etree
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import numpy as np
|
||||||
|
import itertools
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import CityGMLTranslation as cgt
|
||||||
|
from decimal import Decimal
|
||||||
|
from config import setVersion
|
||||||
|
import time
|
||||||
|
|
||||||
|
# -- ARGUMENTS
|
||||||
|
# -i -- input directory (it will read and convert ALL CityGML files in a directory)
|
||||||
|
# -o -- output directory (it will output the generated OBJs in that directory in the way that Delft.gml becomes Delft.obj)
|
||||||
|
# -- SETTINGS of the converter (can be combined):
|
||||||
|
# -s 0 (default) -- converts all geometries in one class in one file under the same object (plain OBJ file).
|
||||||
|
# -s 1 -- differentiate between semantics, output each semantic class as one file, e.g. Delft-WallSurface.obj. Please note that in this case the grouped "plain" OBJ is not generated.
|
||||||
|
# if no thematic boundaries are found, this option is ignored.
|
||||||
|
# -g 0 (default) -- keeps all objects in the same bin.
|
||||||
|
# -g 1 -- it creates one object for every building.
|
||||||
|
# -v 1 -- validation
|
||||||
|
# -p 1 -- skip triangulation and write polygons. Polys with interior not supported.
|
||||||
|
# -t 1 -- translation (reduction) of coordinates so the smallest vertex (one with the minimum coordinates) is at (0, 0)
|
||||||
|
# -a 1 or 2 or 3 -- this is a very custom setting for adding the texture based on attributes, here you can see the settings for my particular case of the solar radiation. By default it is off.
|
||||||
|
|
||||||
|
# -- Text to be printed at the beginning of each OBJ
|
||||||
|
header = """# Converted from CityGML to OBJ with CityGML2OBJs.
|
||||||
|
# Conversion tool developed by Filip Biljecki, TU Delft <fbiljecki@gmail.com>, see more at Github:
|
||||||
|
# https://github.com/tudelft3d/CityGML2OBJs
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_index(point, list_vertices, shift=0):
|
||||||
|
"""Index the vertices.
|
||||||
|
The third option is for incorporating a local index (building-level) to the global one (dataset-level)."""
|
||||||
|
global vertices
|
||||||
|
"""Unique identifier and indexer of vertices."""
|
||||||
|
if point in list_vertices:
|
||||||
|
return list_vertices.index(point) + 1 + shift, list_vertices
|
||||||
|
else:
|
||||||
|
list_vertices.append(point)
|
||||||
|
return list_vertices.index(point) + 1 + shift, list_vertices
|
||||||
|
|
||||||
|
|
||||||
|
def write_vertices(list_vertices, cla):
|
||||||
|
"""Write the vertices in the OBJ format."""
|
||||||
|
global vertices_output
|
||||||
|
for each in list_vertices:
|
||||||
|
vertices_output[cla].append("v" + " " + str(each[0]) + " " + str(each[1]) + " " + str(each[2]) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_reccuring(list_vertices):
|
||||||
|
"""Removes recurring vertices, which messes up the triangulation.
|
||||||
|
Inspired by http://stackoverflow.com/a/1143432"""
|
||||||
|
# last_point = list_vertices[-1]
|
||||||
|
list_vertices_without_last = list_vertices[:-1]
|
||||||
|
found = set()
|
||||||
|
for item in list_vertices_without_last:
|
||||||
|
if str(item) not in found:
|
||||||
|
yield item
|
||||||
|
found.add(str(item))
|
||||||
|
|
||||||
|
|
||||||
|
def poly_to_obj(poly, cl, material=None):
|
||||||
|
"""Main conversion function of one polygon to one or more faces in OBJ,
|
||||||
|
in a specific semantic class. Supports assigning a material."""
|
||||||
|
global local_vertices
|
||||||
|
global vertices
|
||||||
|
global face_output
|
||||||
|
# -- Decompose the polygon into exterior and interior
|
||||||
|
e, i = markup3dmodule.polydecomposer(poly)
|
||||||
|
# -- Points forming the exterior LinearRing
|
||||||
|
epoints = markup3dmodule.GMLpoints(e[0])
|
||||||
|
# print(epoints)
|
||||||
|
# -- Clean recurring points, except the last one
|
||||||
|
last_ep = epoints[-1]
|
||||||
|
epoints_clean = list(remove_reccuring(epoints))
|
||||||
|
epoints_clean.append(last_ep)
|
||||||
|
# print("epoints: ", epoints)
|
||||||
|
# print("epoints_clean: ", epoints_clean)
|
||||||
|
|
||||||
|
# -- LinearRing(s) forming the interior
|
||||||
|
irings = []
|
||||||
|
for iring in i:
|
||||||
|
ipoints = markup3dmodule.GMLpoints(iring)
|
||||||
|
# -- Clean them in the same manner as the exterior ring
|
||||||
|
last_ip = ipoints[-1]
|
||||||
|
ipoints_clean = list(remove_reccuring(ipoints))
|
||||||
|
ipoints_clean.append(last_ip)
|
||||||
|
irings.append(ipoints_clean)
|
||||||
|
# print("irings: ", irings)
|
||||||
|
# -- If the polygon validation option is enabled
|
||||||
|
if VALIDATION:
|
||||||
|
# -- Check the polygon
|
||||||
|
valid = polygon3dmodule.isPolyValid(epoints_clean, True)
|
||||||
|
if valid:
|
||||||
|
for iring in irings:
|
||||||
|
if not polygon3dmodule.isPolyValid(iring, False):
|
||||||
|
valid = False
|
||||||
|
# -- If everything is valid send them to the Delaunay triangulation
|
||||||
|
if valid:
|
||||||
|
if SKIPTRI:
|
||||||
|
# -- Triangulation is skipped, polygons are converted directly to faces
|
||||||
|
# -- The last point is removed since it's equal to the first one
|
||||||
|
t = [epoints_clean[:-1]]
|
||||||
|
else:
|
||||||
|
# -- Triangulate polys
|
||||||
|
# t = polygon3dmodule.triangulation(epoints, irings)
|
||||||
|
try:
|
||||||
|
t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||||
|
except:
|
||||||
|
t = []
|
||||||
|
# t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||||
|
|
||||||
|
# -- Process the triangles/polygons
|
||||||
|
for tri in t:
|
||||||
|
# -- Face marker
|
||||||
|
f = "f "
|
||||||
|
# -- For each point in the triangle/polygon (face) get the index "v" or add it to the index
|
||||||
|
for ep in range(0, len(tri)):
|
||||||
|
v, local_vertices[cl] = get_index(tri[ep], local_vertices[cl], len(vertices[cl]))
|
||||||
|
f += str(v) + " "
|
||||||
|
# -- Add the material if invoked
|
||||||
|
if material:
|
||||||
|
face_output[cl].append("usemtl " + str(mtl(material, min_value, max_value, res)) + str("\n"))
|
||||||
|
# -- Store all together
|
||||||
|
face_output[cl].append(f + "\n")
|
||||||
|
else:
|
||||||
|
# Get the gml:id of the Polygon if it exists
|
||||||
|
polyid = poly.xpath("@g:id", namespaces={'g': ns_gml})
|
||||||
|
if polyid:
|
||||||
|
polyid = polyid[0]
|
||||||
|
print("\t\t!! Detected an invalid polygon (%s). Skipping..." % polyid)
|
||||||
|
else:
|
||||||
|
print("\t\t!! Detected an invalid polygon. Skipping...")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# -- Do exactly the same, but without the validation
|
||||||
|
try:
|
||||||
|
if SKIPTRI:
|
||||||
|
t = [epoints_clean[:-1]]
|
||||||
|
else:
|
||||||
|
t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||||
|
|
||||||
|
|
||||||
|
except:
|
||||||
|
|
||||||
|
t = []
|
||||||
|
|
||||||
|
for tri in t:
|
||||||
|
f = "f "
|
||||||
|
for ep in range(0, len(tri)):
|
||||||
|
v, local_vertices[cl] = get_index(tri[ep], local_vertices[cl], len(vertices[cl]))
|
||||||
|
f += str(v) + " "
|
||||||
|
|
||||||
|
if material:
|
||||||
|
face_output[cl].append("usemtl " + str(mtl(material, min_value, max_value, res)) + str("\n"))
|
||||||
|
face_output[cl].append(f + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Parse command-line arguments
|
||||||
|
PARSER = argparse.ArgumentParser(description='Convert a CityGML to OBJ.')
|
||||||
|
PARSER.add_argument('-i', '--directory',
|
||||||
|
help='Directory containing CityGML file(s).', required=True)
|
||||||
|
PARSER.add_argument('-o', '--results',
|
||||||
|
help='Directory where the OBJ file(s) should be written.', required=True)
|
||||||
|
PARSER.add_argument('-s', '--semantics',
|
||||||
|
help='Write one OBJ (0) or multiple OBJ per semantic class (1). 0 is default.', required=False)
|
||||||
|
PARSER.add_argument('-g', '--grouping',
|
||||||
|
help='Writes all buildings in one group (0) or multiple groups (1). 0 is default.', required=False)
|
||||||
|
PARSER.add_argument('-a', '--attribute',
|
||||||
|
help='Creates a texture regarding the value of an attribute of the surface. No material is default.',
|
||||||
|
required=False)
|
||||||
|
PARSER.add_argument('-v', '--validation',
|
||||||
|
help='Validates polygons, and if they are not valid give a warning and skip them. No validation is default.',
|
||||||
|
required=False)
|
||||||
|
PARSER.add_argument('-t', '--translate',
|
||||||
|
help='Translates all vertices, so that the smallest vertex is at zero. No translation is default.',
|
||||||
|
required=False)
|
||||||
|
PARSER.add_argument('-p', '--polypreserve',
|
||||||
|
help='Skip the triangulation (preserve polygons). Triangulation is default.', required=False)
|
||||||
|
|
||||||
|
# Changes by Th_Fr: 2 New optional parameters added, see description for details!
|
||||||
|
PARSER.add_argument('-tC', '--translateCityGML',
|
||||||
|
help='Perform a Translation of the CityGML Dataset into a local CRS before further processing. No translation is default.',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
PARSER.add_argument('-tCw', '--translateCityGMLwrite',
|
||||||
|
help='Perform a Translation of the CityGML Dataset into a local CRS before further processing. The translation parameters are stored in a designated .txt file. No Translation is default ',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
PARSER.add_argument('-sepC', '--separateComponents',
|
||||||
|
help='Save each building component into an individual file with the filename serving as an identifier.',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
PARSER.add_argument('-appW', '--approximateWindows',
|
||||||
|
help='Approximate windows by their convex hulls to save some processing time.',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
PARSER.add_argument('-addBB', '--addBoundingBox',
|
||||||
|
help='Add small triangles defining the bounding box to each of the components.',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
# Todo: Neue funktion muss noch fertig implementiert werden
|
||||||
|
PARSER.add_argument('-importBB', '--importBoundingBox',
|
||||||
|
help='Add small triangles defining an imported bounding box to each of the components.',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
PARSER.add_argument('-addBBJSON', '--addBoundingBoxJSON',
|
||||||
|
help='The bounding box of the building is additionally saved in a designated json-file',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
PARSER.add_argument('-tbw', '--translateBuildingWise', # todo: implementation yet to be completed
|
||||||
|
help='Translate into a local coordinate system building-wise.',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
# End of changes by Th_Fr
|
||||||
|
|
||||||
|
ARGS = vars(PARSER.parse_args())
|
||||||
|
DIRECTORY = os.path.join(ARGS['directory'], '')
|
||||||
|
RESULT = os.path.join(ARGS['results'], '')
|
||||||
|
|
||||||
|
SEMANTICS = ARGS['semantics']
|
||||||
|
if SEMANTICS == '1':
|
||||||
|
SEMANTICS = True
|
||||||
|
elif SEMANTICS == '0':
|
||||||
|
SEMANTICS = False
|
||||||
|
else:
|
||||||
|
SEMANTICS = False
|
||||||
|
|
||||||
|
OBJECTS = ARGS['grouping']
|
||||||
|
if OBJECTS == '1':
|
||||||
|
OBJECTS = True
|
||||||
|
elif OBJECTS == '0':
|
||||||
|
OBJECTS = False
|
||||||
|
else:
|
||||||
|
OBJECTS = False
|
||||||
|
|
||||||
|
ATTRIBUTE = ARGS['attribute']
|
||||||
|
if ATTRIBUTE == '1':
|
||||||
|
ATTRIBUTE = 1
|
||||||
|
elif ATTRIBUTE == '2':
|
||||||
|
ATTRIBUTE = 2
|
||||||
|
elif ATTRIBUTE == '3':
|
||||||
|
ATTRIBUTE = 3
|
||||||
|
elif ATTRIBUTE == '0':
|
||||||
|
ATTRIBUTE = False
|
||||||
|
else:
|
||||||
|
ATTRIBUTE = False
|
||||||
|
|
||||||
|
VALIDATION = ARGS['validation']
|
||||||
|
if VALIDATION == '1':
|
||||||
|
VALIDATION = True
|
||||||
|
elif VALIDATION == '0':
|
||||||
|
VALIDATION = False
|
||||||
|
else:
|
||||||
|
VALIDATION = False
|
||||||
|
|
||||||
|
TRANSLATE = ARGS['translate']
|
||||||
|
if TRANSLATE == '1':
|
||||||
|
TRANSLATE = True
|
||||||
|
elif TRANSLATE == '0':
|
||||||
|
TRANSLATE = False
|
||||||
|
else:
|
||||||
|
TRANSLATE = False
|
||||||
|
if TRANSLATE:
|
||||||
|
global smallest_point
|
||||||
|
|
||||||
|
SKIPTRI = ARGS['polypreserve']
|
||||||
|
if SKIPTRI == '1':
|
||||||
|
SKIPTRI = True
|
||||||
|
elif SKIPTRI == '0':
|
||||||
|
SKIPTRI = False
|
||||||
|
else:
|
||||||
|
SKIPTRI = False
|
||||||
|
|
||||||
|
# Changes By Th_Fr:
|
||||||
|
|
||||||
|
TRANSLATECGML = ARGS['translateCityGML']
|
||||||
|
if TRANSLATECGML == '1':
|
||||||
|
TRANSLATECGML = True
|
||||||
|
elif TRANSLATECGML == '0':
|
||||||
|
TRANSLATECGML = False
|
||||||
|
else:
|
||||||
|
TRANSLATECGML = False
|
||||||
|
|
||||||
|
TRANSLATECGMLW = ARGS['translateCityGMLwrite']
|
||||||
|
if TRANSLATECGMLW == '1':
|
||||||
|
TRANSLATECGMLW = True
|
||||||
|
elif TRANSLATECGMLW == '0':
|
||||||
|
TRANSLATECGMLW = False
|
||||||
|
else:
|
||||||
|
TRANSLATECGMLW = False
|
||||||
|
|
||||||
|
SEPARATERCOMPONENTS = ARGS['separateComponents']
|
||||||
|
if SEPARATERCOMPONENTS == '1':
|
||||||
|
SEPARATERCOMPONENTS = True
|
||||||
|
elif SEPARATERCOMPONENTS == '0':
|
||||||
|
SEPARATERCOMPONENTS = False
|
||||||
|
else:
|
||||||
|
SEPARATERCOMPONENTS = False
|
||||||
|
|
||||||
|
APPROXIMATEWINDOWS = ARGS['approximateWindows']
|
||||||
|
if APPROXIMATEWINDOWS == '1':
|
||||||
|
APPROXIMATEWINDOWS = True
|
||||||
|
elif APPROXIMATEWINDOWS == '0':
|
||||||
|
APPROXIMATEWINDOWS = False
|
||||||
|
else:
|
||||||
|
APPROXIMATEWINDOWS = False
|
||||||
|
|
||||||
|
ADDBOUNDINGBOX = ARGS['addBoundingBox']
|
||||||
|
if ADDBOUNDINGBOX == '1':
|
||||||
|
ADDBOUNDINGBOX = True
|
||||||
|
elif ADDBOUNDINGBOX == '0':
|
||||||
|
ADDBOUNDINGBOX = False
|
||||||
|
else:
|
||||||
|
ADDBOUNDINGBOX = False
|
||||||
|
|
||||||
|
|
||||||
|
IMPORTBOUNDINGBOX = ARGS['importBoundingBox']
|
||||||
|
if ADDBOUNDINGBOX == True:
|
||||||
|
IMPORTBOUNDINGBOX = None
|
||||||
|
|
||||||
|
ADDBOUNDINGBOXJSON = ARGS['addBoundingBoxJSON']
|
||||||
|
if ADDBOUNDINGBOXJSON == '1':
|
||||||
|
ADDBOUNDINGBOXJSON = True
|
||||||
|
elif ADDBOUNDINGBOXJSON == '0' and IMPORTBOUNDINGBOX is not None:
|
||||||
|
ADDBOUNDINGBOXJSON = False
|
||||||
|
else:
|
||||||
|
ADDBOUNDINGBOXJSON = False
|
||||||
|
|
||||||
|
if IMPORTBOUNDINGBOX is not None:
|
||||||
|
ADDBOUNDINGBOXJSON = True
|
||||||
|
|
||||||
|
TRANSLATEBUILDINGS = ARGS['translateBuildingWise'] # todo: muss noch implementiert werden
|
||||||
|
if TRANSLATEBUILDINGS == '1':
|
||||||
|
TRANSLATEBUILDINGS = True
|
||||||
|
elif TRANSLATEBUILDINGS == '0':
|
||||||
|
TRANSLATEBUILDINGS = False
|
||||||
|
else:
|
||||||
|
TRANSLATEBUILDINGS = False
|
||||||
|
|
||||||
|
# End of Changes by Th_Fr
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# -- Attribute stuff
|
||||||
|
|
||||||
|
# -- Number of classes (colours)
|
||||||
|
res = 101
|
||||||
|
# -- Configuration
|
||||||
|
# -- Color the surfaces based on the normalised kWh/m^2 value <irradiation>. The plain OBJ will be coloured for the total irradiation.
|
||||||
|
if ATTRIBUTE == 1:
|
||||||
|
min_value = 350.0 # 234.591880403
|
||||||
|
max_value = 1300.0 # 1389.97943395
|
||||||
|
elif ATTRIBUTE == 2:
|
||||||
|
min_value = 157.0136575
|
||||||
|
max_value = 83371.4359245
|
||||||
|
elif ATTRIBUTE == 3:
|
||||||
|
min_value = 24925.0
|
||||||
|
max_value = 103454.0
|
||||||
|
|
||||||
|
# -- Statistic parameter
|
||||||
|
atts = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Colouring function
|
||||||
|
def mtl(att, min_value, max_value, res):
|
||||||
|
"""Finds the corresponding material."""
|
||||||
|
ar = np.linspace(0, 1, res).tolist()
|
||||||
|
# -- Get rid of floating point errors
|
||||||
|
for i in range(0, len(ar)):
|
||||||
|
ar[i] = round(ar[i], 4)
|
||||||
|
# -- Normalise the attribute
|
||||||
|
v = float(att - min_value) / (max_value - min_value)
|
||||||
|
# -- Get the material
|
||||||
|
assigned_material = min(ar, key=lambda x: abs(x - v))
|
||||||
|
return str(assigned_material)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Start time
|
||||||
|
start_time = time.time()
|
||||||
|
# -- Start of the program
|
||||||
|
print("CityGML2OBJ. Searching for CityGML files...")
|
||||||
|
global _VERSION
|
||||||
|
# -- Find all CityGML files in the directory
|
||||||
|
os.chdir(DIRECTORY)
|
||||||
|
# -- Supported extensions
|
||||||
|
# Old version: types = ('*.gml', '*.GML', '*.xml', '*.XML')
|
||||||
|
types = ('*.gml', '*.xml')
|
||||||
|
files_found = []
|
||||||
|
for files in types:
|
||||||
|
files_found.extend(glob.glob(files))
|
||||||
|
for f in files_found:
|
||||||
|
FILENAME = f[:f.rfind('.')]
|
||||||
|
FULLPATH = os.path.join(DIRECTORY, f)
|
||||||
|
|
||||||
|
# -- Reading and parsing the CityGML file(s)
|
||||||
|
CITYGML = etree.parse(FULLPATH)
|
||||||
|
# -- Getting the root of the XML tree
|
||||||
|
root = CITYGML.getroot()
|
||||||
|
# -- Determine CityGML version
|
||||||
|
# If 1.0
|
||||||
|
if root.tag == "{http://www.opengis.net/citygml/1.0}CityModel":
|
||||||
|
print("CityGML 1.0")
|
||||||
|
config.setVersion(1)
|
||||||
|
# -- Name spaces
|
||||||
|
ns_citygml = "http://www.opengis.net/citygml/1.0"
|
||||||
|
ns_gml = "http://www.opengis.net/gml"
|
||||||
|
ns_bldg = "http://www.opengis.net/citygml/building/1.0"
|
||||||
|
ns_tran = "http://www.opengis.net/citygml/transportation/1.0"
|
||||||
|
ns_veg = "http://www.opengis.net/citygml/vegetation/1.0"
|
||||||
|
ns_gen = "http://www.opengis.net/citygml/generics/1.0"
|
||||||
|
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:1.0"
|
||||||
|
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||||
|
ns_dem = "http://www.opengis.net/citygml/relief/1.0"
|
||||||
|
ns_frn = "http://www.opengis.net/citygml/cityfurniture/1.0"
|
||||||
|
ns_tun = "http://www.opengis.net/citygml/tunnel/1.0"
|
||||||
|
ns_wtr = "http://www.opengis.net/citygml/waterbody/1.0"
|
||||||
|
ns_brid = "http://www.opengis.net/citygml/bridge/1.0"
|
||||||
|
ns_app = "http://www.opengis.net/citygml/appearance/1.0"
|
||||||
|
|
||||||
|
# added by Th_Fr
|
||||||
|
elif root.tag == "{http://www.opengis.net/citygml/3.0}CityModel":
|
||||||
|
print("CityGML 3.0")
|
||||||
|
config.setVersion(3)
|
||||||
|
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||||
|
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||||
|
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||||
|
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||||
|
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||||
|
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||||
|
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||||
|
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||||
|
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||||
|
na_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||||
|
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||||
|
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
ns_gco = "http://www.isotc211.org/2005/gco"
|
||||||
|
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||||
|
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||||
|
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||||
|
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||||
|
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||||
|
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||||
|
ns_brid = "http://www.opengis.net/citygml/bridge/3.0"
|
||||||
|
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||||
|
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||||
|
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||||
|
|
||||||
|
# -- Else probably means 2.0
|
||||||
|
else:
|
||||||
|
print("CityGML 2.0")
|
||||||
|
config.setVersion(2)
|
||||||
|
# -- Name spaces
|
||||||
|
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||||
|
|
||||||
|
ns_gml = "http://www.opengis.net/gml"
|
||||||
|
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||||
|
ns_tran = "http://www.opengis.net/citygml/transportation/2.0"
|
||||||
|
ns_veg = "http://www.opengis.net/citygml/vegetation/2.0"
|
||||||
|
ns_gen = "http://www.opengis.net/citygml/generics/2.0"
|
||||||
|
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||||
|
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||||
|
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||||
|
ns_frn = "http://www.opengis.net/citygml/cityfurniture/2.0"
|
||||||
|
ns_tun = "http://www.opengis.net/citygml/tunnel/2.0"
|
||||||
|
ns_wtr = "http://www.opengis.net/citygml/waterbody/2.0"
|
||||||
|
ns_brid = "http://www.opengis.net/citygml/bridge/2.0"
|
||||||
|
ns_app = "http://www.opengis.net/citygml/appearance/2.0"
|
||||||
|
|
||||||
|
nsmap = {
|
||||||
|
None: ns_citygml,
|
||||||
|
'gml': ns_gml,
|
||||||
|
'bldg': ns_bldg,
|
||||||
|
'tran': ns_tran,
|
||||||
|
'veg': ns_veg,
|
||||||
|
'gen': ns_gen,
|
||||||
|
'xsi': ns_xsi,
|
||||||
|
'xAL': ns_xAL,
|
||||||
|
'xlink': ns_xlink,
|
||||||
|
'dem': ns_dem,
|
||||||
|
'frn': ns_frn,
|
||||||
|
'tun': ns_tun,
|
||||||
|
'brid': ns_brid,
|
||||||
|
'app': ns_app
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Changes by Th_FR
|
||||||
|
|
||||||
|
if TRANSLATECGML:
|
||||||
|
cgt.translateToLocalCRS(CITYGML, FILENAME, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, RESULT,
|
||||||
|
write2file=False,
|
||||||
|
applyHeight=Decimal("0")) # Todo: by TH_Fr: Diese Funktion ist noch nicht fertig
|
||||||
|
|
||||||
|
if TRANSLATECGMLW:
|
||||||
|
cgt.translateToLocalCRS(CITYGML, FILENAME, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, RESULT,
|
||||||
|
write2file=True, applyHeight=Decimal("0"))
|
||||||
|
|
||||||
|
# End of changes by Th_FR
|
||||||
|
|
||||||
|
# -- Empty lists for cityobjects and buildings
|
||||||
|
cityObjects = []
|
||||||
|
buildings = []
|
||||||
|
other = []
|
||||||
|
|
||||||
|
# -- This denotes the dictionaries in which the surfaces are put.
|
||||||
|
output = {}
|
||||||
|
vertices_output = {}
|
||||||
|
face_output = {}
|
||||||
|
|
||||||
|
# -- This denotes the dictionaries in which all surfaces are put. It is later ignored in the semantic option was invoked.
|
||||||
|
output['All'] = []
|
||||||
|
output['All'].append(header)
|
||||||
|
if ATTRIBUTE:
|
||||||
|
output['All'].append("mtllib colormap.mtl\n")
|
||||||
|
vertices_output['All'] = []
|
||||||
|
face_output['All'] = []
|
||||||
|
|
||||||
|
# -- If the semantic option was invoked, this part adds additional dictionaries.
|
||||||
|
if SEMANTICS:
|
||||||
|
# -- Easy to modify list of thematic boundaries
|
||||||
|
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface',
|
||||||
|
'InteriorWallSurface', 'FloorSurface', 'OuterCeilingSurface', 'OuterFloorSurface', 'Door',
|
||||||
|
'Window']
|
||||||
|
for semanticSurface in semanticSurfaces:
|
||||||
|
output[semanticSurface] = []
|
||||||
|
output[semanticSurface].append(header)
|
||||||
|
# -- Add the material library
|
||||||
|
if ATTRIBUTE:
|
||||||
|
output[semanticSurface].append("mtllib colormap.mtl\n")
|
||||||
|
vertices_output[semanticSurface] = []
|
||||||
|
face_output[semanticSurface] = []
|
||||||
|
|
||||||
|
# -- Directory of vertices (indexing)
|
||||||
|
vertices = {}
|
||||||
|
vertices['All'] = []
|
||||||
|
if SEMANTICS:
|
||||||
|
for semanticSurface in semanticSurfaces:
|
||||||
|
vertices[semanticSurface] = []
|
||||||
|
vertices['Other'] = []
|
||||||
|
face_output['Other'] = []
|
||||||
|
output['Other'] = []
|
||||||
|
|
||||||
|
# -- Find all instances of cityObjectMember and put them in a list
|
||||||
|
for obj in root.getiterator('{%s}cityObjectMember' % ns_citygml):
|
||||||
|
cityObjects.append(obj)
|
||||||
|
|
||||||
|
print(FILENAME)
|
||||||
|
|
||||||
|
if len(cityObjects) > 0:
|
||||||
|
# -- Report the progress and contents of the CityGML file
|
||||||
|
print("\tThere are", len(cityObjects), "cityObject(s) in this CityGML file.")
|
||||||
|
# -- Store each building separately
|
||||||
|
for cityObject in cityObjects:
|
||||||
|
for child in cityObject.getchildren():
|
||||||
|
if child.tag == '{%s}Building' % ns_bldg:
|
||||||
|
buildings.append(child)
|
||||||
|
|
||||||
|
for cityObject in cityObjects:
|
||||||
|
for child in cityObject.getchildren():
|
||||||
|
if child.tag == '{%s}Road' % ns_tran or child.tag == '{%s}PlantCover' % ns_veg or \
|
||||||
|
child.tag == '{%s}GenericCityObject' % ns_gen or child.tag == '{%s}CityFurniture' % ns_frn or \
|
||||||
|
child.tag == '{%s}Relief' % ns_dem or child.tag == '{%s}Tunnel' % ns_tun or \
|
||||||
|
child.tag == '{%s}WaterBody' % ns_wtr or child.tag == '{%s}Bridge' % ns_brid:
|
||||||
|
other.append(child)
|
||||||
|
|
||||||
|
print("\tAnalysing objects and extracting the geometry...")
|
||||||
|
|
||||||
|
# -- Count the buildings
|
||||||
|
b_counter = 0
|
||||||
|
b_total = len(buildings)
|
||||||
|
print(" There are ", b_total, " buildings in the dataset")
|
||||||
|
|
||||||
|
# -- Do each building separately
|
||||||
|
for b in buildings:
|
||||||
|
# addd by th_fr
|
||||||
|
if SEPARATERCOMPONENTS:
|
||||||
|
json_filepath = RESULT + "index.json"
|
||||||
|
# todo: mus snoch implementiert werde
|
||||||
|
|
||||||
|
csm.addCRSToJSON(root, json_filepath)
|
||||||
|
csm.separateComponents(b, RESULT, APPROXIMATEWINDOWS=APPROXIMATEWINDOWS,
|
||||||
|
ADDBOUNDINGBOX=ADDBOUNDINGBOX, ADDBOUNDINGBOXJSON=ADDBOUNDINGBOXJSON,
|
||||||
|
TRANSLATEBUILDINGS=TRANSLATEBUILDINGS, IMPORTBOUNDINGBOX=IMPORTBOUNDINGBOX, b_counter=b_counter)
|
||||||
|
# End time
|
||||||
|
end_time = time.time()
|
||||||
|
# Calculate elapsed time
|
||||||
|
elapsed_time = end_time - start_time
|
||||||
|
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||||
|
b_counter += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# -- Build the local list of vertices to speed up the indexing
|
||||||
|
local_vertices = {}
|
||||||
|
local_vertices['All'] = []
|
||||||
|
|
||||||
|
if SEMANTICS:
|
||||||
|
for semanticSurface in semanticSurfaces:
|
||||||
|
local_vertices[semanticSurface] = []
|
||||||
|
|
||||||
|
# -- Increment the building counter
|
||||||
|
b_counter += 1
|
||||||
|
# -- If the object option is on, get the name for each building or create one
|
||||||
|
if OBJECTS:
|
||||||
|
ob = b.xpath("@g:id", namespaces={'g': ns_gml})
|
||||||
|
if not ob:
|
||||||
|
ob = b_counter
|
||||||
|
else:
|
||||||
|
ob = ob[0]
|
||||||
|
|
||||||
|
# -- Print progress for large files every 1000 buildings.
|
||||||
|
if b_counter == 1000:
|
||||||
|
print("\t1000... ", )
|
||||||
|
elif b_counter % 1000 == 0 and b_counter == (b_total - b_total % 1000):
|
||||||
|
print(str(b_counter) + "...")
|
||||||
|
elif b_counter > 0 and b_counter % 1000 == 0:
|
||||||
|
print(str(b_counter) + "... ", )
|
||||||
|
|
||||||
|
# -- Add the object identifier
|
||||||
|
if OBJECTS:
|
||||||
|
face_output['All'].append('o ' + str(ob) + '\n')
|
||||||
|
|
||||||
|
# -- Add the attribute for the building
|
||||||
|
if ATTRIBUTE:
|
||||||
|
for ch in b.getchildren():
|
||||||
|
if ch.tag == "{%s}yearlyIrradiation" % ns_citygml:
|
||||||
|
bAttVal = float(ch.text)
|
||||||
|
|
||||||
|
# -- OBJ with all surfaces in the same bin
|
||||||
|
polys = markup3dmodule.polygonFinder(b)
|
||||||
|
# -- Process each surface
|
||||||
|
polycounter = 0
|
||||||
|
for poly in polys:
|
||||||
|
if ATTRIBUTE:
|
||||||
|
poly_to_obj(poly, 'All', bAttVal)
|
||||||
|
if ATTRIBUTE == 3:
|
||||||
|
atts.append(bAttVal)
|
||||||
|
else:
|
||||||
|
# print etree.tostring(poly)
|
||||||
|
poly_to_obj(poly, 'All')
|
||||||
|
polycounter = polycounter + 1
|
||||||
|
|
||||||
|
# -- Semantic decomposition, with taking special care about the openings
|
||||||
|
if SEMANTICS:
|
||||||
|
# -- First take care about the openings since they can mix up
|
||||||
|
openings = []
|
||||||
|
openingpolygons = []
|
||||||
|
for child in b.getiterator():
|
||||||
|
if child.tag == '{%s}opening' % ns_bldg:
|
||||||
|
openings.append(child)
|
||||||
|
for o in child.findall('.//{%s}Polygon' % ns_gml):
|
||||||
|
openingpolygons.append(o)
|
||||||
|
|
||||||
|
# -- Process each opening
|
||||||
|
for o in openings:
|
||||||
|
for child in o.getiterator():
|
||||||
|
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||||
|
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||||
|
# print(unique_identifier)
|
||||||
|
if child.tag == '{%s}Window' % ns_bldg:
|
||||||
|
t = 'Window'
|
||||||
|
# print(t)
|
||||||
|
else:
|
||||||
|
t = 'Door'
|
||||||
|
# print(t)
|
||||||
|
polys = markup3dmodule.polygonFinder(o)
|
||||||
|
for poly in polys:
|
||||||
|
poly_to_obj(poly, t)
|
||||||
|
|
||||||
|
# -- Process other thematic boundaries
|
||||||
|
for cl in output:
|
||||||
|
cls = []
|
||||||
|
for child in b.getiterator():
|
||||||
|
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||||
|
cls.append(child)
|
||||||
|
# -- Is this the first feature of this object?
|
||||||
|
firstF = True
|
||||||
|
for feature in cls:
|
||||||
|
# -- If it is the first feature, print the object identifier
|
||||||
|
unique_identifier = feature.xpath("@g:id", namespaces={
|
||||||
|
'g': ns_gml})
|
||||||
|
if OBJECTS and firstF:
|
||||||
|
face_output[cl].append('o ' + str(ob) + "_" + str(unique_identifier) + '\n')
|
||||||
|
|
||||||
|
firstF = False
|
||||||
|
# -- This is not supposed to happen, but just to be sure...
|
||||||
|
if feature.tag == '{%s}Window' % ns_bldg or feature.tag == '{%s}Door' % ns_bldg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# print(f"unigue identifier: {str(ob) + str(unique_identifier)}")
|
||||||
|
|
||||||
|
# -- Find all polygons in this semantic boundary hierarchy
|
||||||
|
|
||||||
|
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||||
|
if ATTRIBUTE == 1 or ATTRIBUTE == 2:
|
||||||
|
# -- Flush the previous value
|
||||||
|
attVal = None
|
||||||
|
if cl == 'RoofSurface':
|
||||||
|
# print p.xpath("//@c:irradiation", namespaces={'c' : ns_citygml})
|
||||||
|
# -- Silly way but it works, as I can't get the above xpath to work for some reason
|
||||||
|
for ch in p.getchildren():
|
||||||
|
if ATTRIBUTE == 1:
|
||||||
|
if ch.tag == "{%s}irradiation" % ns_citygml:
|
||||||
|
attVal = float(ch.text)
|
||||||
|
atts.append(attVal)
|
||||||
|
elif ATTRIBUTE == 2:
|
||||||
|
if ch.tag == "{%s}totalIrradiation" % ns_citygml:
|
||||||
|
attVal = float(ch.text)
|
||||||
|
atts.append(attVal)
|
||||||
|
elif ATTRIBUTE == 3:
|
||||||
|
attVal = None
|
||||||
|
if cl == 'RoofSurface':
|
||||||
|
attVal = bAttVal
|
||||||
|
else:
|
||||||
|
# -- If the attribute option is off, pass no material
|
||||||
|
attVal = None
|
||||||
|
found_opening = False
|
||||||
|
for optest in openingpolygons:
|
||||||
|
if p == optest:
|
||||||
|
found_opening = True
|
||||||
|
break
|
||||||
|
# -- If there is an opening skip it
|
||||||
|
if found_opening:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# -- Finally process the polygon
|
||||||
|
poly_to_obj(p, cl, attVal)
|
||||||
|
|
||||||
|
# -- Merge the local list of vertices to the global
|
||||||
|
for cl in local_vertices:
|
||||||
|
for vertex in local_vertices[cl]:
|
||||||
|
vertices[cl].append(vertex)
|
||||||
|
|
||||||
|
if len(other) > 0:
|
||||||
|
vertices_output['Other'] = []
|
||||||
|
local_vertices = {}
|
||||||
|
local_vertices['Other'] = []
|
||||||
|
for oth in other:
|
||||||
|
# local_vertices = {}
|
||||||
|
# local_vertices['All'] = []
|
||||||
|
polys = markup3dmodule.polygonFinder(oth)
|
||||||
|
# -- Process each surface
|
||||||
|
for poly in polys:
|
||||||
|
poly_to_obj(poly, 'Other')
|
||||||
|
for vertex in local_vertices['Other']:
|
||||||
|
vertices['Other'].append(vertex)
|
||||||
|
|
||||||
|
print("\tExtraction done. Sorting geometry and writing file(s).")
|
||||||
|
|
||||||
|
# -- Translate (convert) the vertices to a local coordinate system
|
||||||
|
if TRANSLATE:
|
||||||
|
print("\tTranslating the coordinates of vertices.")
|
||||||
|
list_of_all_vertices = []
|
||||||
|
for cl in output:
|
||||||
|
if len(vertices[cl]) > 0:
|
||||||
|
for vtx in vertices[cl]:
|
||||||
|
list_of_all_vertices.append(vtx)
|
||||||
|
smallest_vtx = polygon3dmodule.smallestPoint(list_of_all_vertices)
|
||||||
|
dx = smallest_vtx[0]
|
||||||
|
dy = smallest_vtx[1]
|
||||||
|
dz = smallest_vtx[2]
|
||||||
|
for cl in output:
|
||||||
|
if len(vertices[cl]) > 0:
|
||||||
|
for idx, vtx in enumerate(vertices[cl]):
|
||||||
|
vertices[cl][idx][0] = vtx[0] - dx
|
||||||
|
vertices[cl][idx][1] = vtx[1] - dy
|
||||||
|
vertices[cl][idx][2] = vtx[2] - dz
|
||||||
|
|
||||||
|
# -- Write the OBJ(s)
|
||||||
|
os.chdir(RESULT)
|
||||||
|
# -- Theme by theme
|
||||||
|
for cl in output:
|
||||||
|
if len(vertices[cl]) > 0:
|
||||||
|
write_vertices(vertices[cl], cl)
|
||||||
|
output[cl].append("\n" + ''.join(vertices_output[cl]))
|
||||||
|
output[cl].append("\n" + ''.join(face_output[cl]))
|
||||||
|
if cl == 'All':
|
||||||
|
adj_suffix = ""
|
||||||
|
else:
|
||||||
|
adj_suffix = "-" + str(cl)
|
||||||
|
|
||||||
|
with open(RESULT + FILENAME + str(adj_suffix) + ".obj", "w") as obj_file:
|
||||||
|
obj_file.write(''.join(output[cl]))
|
||||||
|
|
||||||
|
print("\tOBJ file(s) written.")
|
||||||
|
|
||||||
|
# -- Print the range of attributes. Useful for defining the range of the colorbar.
|
||||||
|
if ATTRIBUTE:
|
||||||
|
print('\tRange of attributes:', min(atts), '--', max(atts))
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"\tThere is a problem with this file: no cityObjects have been found. Please check if the file complies to CityGML.")
|
||||||
|
|
||||||
|
# End time
|
||||||
|
end_time = time.time()
|
||||||
|
# Calculate elapsed time
|
||||||
|
elapsed_time = end_time - start_time
|
||||||
|
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||||
339
scripts/CityGML2OBJv2/CityGMLTranslation.py
Normal file
339
scripts/CityGML2OBJv2/CityGMLTranslation.py
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
##!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
|
||||||
|
# This code is part of the CityGML2OBJs package
|
||||||
|
|
||||||
|
# Copyright (c) 2023
|
||||||
|
# Thomas Fröch
|
||||||
|
# Technische Universität München (TUM)
|
||||||
|
# thomas.froech@tum.de
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
import markup3dmodule as m3dm
|
||||||
|
from decimal import Decimal, getcontext
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Setting the precision of Decimal
|
||||||
|
getcontext().prec = 28
|
||||||
|
|
||||||
|
def performStableAddition(number1, number2):
|
||||||
|
# Input: string number1, string number 2, output: string sum
|
||||||
|
# Determine the number of positions after the comma.
|
||||||
|
# print("Number 1 totally before: ", number1)
|
||||||
|
# print("Number 2 totally before: ", number2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
n_after_comma_number1 = len(number1.split(".")[1])
|
||||||
|
except:
|
||||||
|
n_after_comma_number1 = 0
|
||||||
|
try:
|
||||||
|
n_after_comma_number2 = len(number2.split(".")[1])
|
||||||
|
except:
|
||||||
|
n_after_comma_number2 = 0
|
||||||
|
lengths = [n_after_comma_number1, n_after_comma_number2]
|
||||||
|
|
||||||
|
# convert float numbers into integers by removing the comma
|
||||||
|
# print("Number 1 before all: ", number1)
|
||||||
|
# print("Number 2 before all: ", number2)
|
||||||
|
number1 = number1.replace(".", "")
|
||||||
|
number2 = number2.replace(".", "")
|
||||||
|
|
||||||
|
# print("Number 1 after: ", number1)
|
||||||
|
# print("Number 2 after: ", number2)
|
||||||
|
|
||||||
|
# find the number with more positions after the comma
|
||||||
|
abs_lengths = np.abs(lengths) # Erstellen eines Arrays mit den Beträgen
|
||||||
|
max_index = np.argmax(abs_lengths)
|
||||||
|
|
||||||
|
# Distinguish the two different cases
|
||||||
|
# Case 1: number one has more positions after the comma than number two
|
||||||
|
if max_index == 0:
|
||||||
|
# Find the difference of positions after the comma
|
||||||
|
n_positions_difference = (lengths[0] - lengths[1])
|
||||||
|
# print("n_positions_difference: ", n_positions_difference)
|
||||||
|
# Fill up the missing digits of number 2 with zeros
|
||||||
|
# print("number2 before: ", number2)
|
||||||
|
for i in range(n_positions_difference):
|
||||||
|
number2 = number2 + "0"
|
||||||
|
# print("number2_after", number2)
|
||||||
|
# print("Number1: ", number1)
|
||||||
|
# convert both stings into integers
|
||||||
|
number1_int = np.double(number1)
|
||||||
|
number2_int = np.double(number2)
|
||||||
|
|
||||||
|
# Add both numbers
|
||||||
|
number_sum = number1_int + number2_int
|
||||||
|
|
||||||
|
# convert back into string
|
||||||
|
format_string = "{:.0f}"
|
||||||
|
number_sum_string = format_string.format(number_sum)
|
||||||
|
|
||||||
|
# Insert the comma at the correct position
|
||||||
|
length_of_number = len(number_sum_string.replace("-",
|
||||||
|
""))
|
||||||
|
if length_of_number >= lengths[max_index]:
|
||||||
|
if n_after_comma_number1 != 0:
|
||||||
|
number_sum_string = number_sum_string[:-(lengths[0])] + "." + number_sum_string[-(lengths[0]):]
|
||||||
|
return number_sum_string
|
||||||
|
else:
|
||||||
|
number_sum_string = number_sum_string
|
||||||
|
return number_sum_string
|
||||||
|
elif length_of_number < n_after_comma_number1:
|
||||||
|
new_string = ""
|
||||||
|
for i in range(len(number_sum_string)):
|
||||||
|
if number_sum_string[i] == "-":
|
||||||
|
new_string += "0."
|
||||||
|
else:
|
||||||
|
new_string += number_sum_string[i]
|
||||||
|
number_sum_string = new_string
|
||||||
|
return number_sum_string
|
||||||
|
else:
|
||||||
|
number_sum_string = number_sum_string
|
||||||
|
return number_sum_string
|
||||||
|
|
||||||
|
# Case 2: number two has more positions after the comma than number one
|
||||||
|
else:
|
||||||
|
# find the difference of positions after the comma
|
||||||
|
n_positions_difference = lengths[1] - lengths[0]
|
||||||
|
|
||||||
|
# Fill up the missing digits of number 2 with zeros
|
||||||
|
for i in range(n_positions_difference):
|
||||||
|
number1 = number1 + "0"
|
||||||
|
|
||||||
|
# convert both stings into integers
|
||||||
|
number1_int = Decimal(number1)
|
||||||
|
number2_int = Decimal(number2)
|
||||||
|
|
||||||
|
# Add both numbers
|
||||||
|
number_sum = number1_int + number2_int
|
||||||
|
|
||||||
|
# convert back into string
|
||||||
|
format_string = "{:.0f}"
|
||||||
|
number_sum_string = format_string.format(number_sum)
|
||||||
|
|
||||||
|
# Insert the comma at the correct position
|
||||||
|
length_of_number = len(number_sum_string.replace("-",
|
||||||
|
""))
|
||||||
|
if length_of_number > n_after_comma_number2:
|
||||||
|
if n_after_comma_number2 != 0:
|
||||||
|
|
||||||
|
number_sum_string = number_sum_string[:-(lengths[1])] + "." + number_sum_string[-(lengths[1]):]
|
||||||
|
return number_sum_string
|
||||||
|
|
||||||
|
else:
|
||||||
|
number_sum_string = number_sum_string
|
||||||
|
return number_sum_string
|
||||||
|
|
||||||
|
elif length_of_number < n_after_comma_number2:
|
||||||
|
new_string = ""
|
||||||
|
for i in range(len(number_sum_string)):
|
||||||
|
if number_sum_string[i] == "-":
|
||||||
|
new_string += "0."
|
||||||
|
else:
|
||||||
|
new_string += number_sum_string[i]
|
||||||
|
number_sum_string = new_string
|
||||||
|
return number_sum_string
|
||||||
|
else:
|
||||||
|
number_sum_string = number_sum_string
|
||||||
|
return number_sum_string
|
||||||
|
|
||||||
|
|
||||||
|
# This function is used in order to extract all the envelopes from the CityGML-File
|
||||||
|
# These envelopes are going to be used in order to determine the translation parameters later
|
||||||
|
def getEnvelopes(root, ns_bldg, ns_gml, ns_citygml):
|
||||||
|
envelopes = []
|
||||||
|
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||||
|
envelopes.append(envelope)
|
||||||
|
return envelopes
|
||||||
|
|
||||||
|
|
||||||
|
# This function is used in order to calculate the translation parameters from the
|
||||||
|
# envelopes that were previously extracted from the envelopes
|
||||||
|
def getTranslationParameters(envelopes, ns_gml):
|
||||||
|
# Setting up some initial values
|
||||||
|
dx = Decimal("0")
|
||||||
|
dy = Decimal("0")
|
||||||
|
lowerCorner = []
|
||||||
|
upperCorner = []
|
||||||
|
|
||||||
|
# Iterating through all the envelopes in the CityGML-File
|
||||||
|
for envelope in envelopes:
|
||||||
|
# print(" Envelope: ", envelope)
|
||||||
|
# Finding the upper and the lower corner
|
||||||
|
for child in envelope.getchildren():
|
||||||
|
if child.tag == '{%s}lowerCorner' % ns_gml:
|
||||||
|
lowerCorner.append(child.text)
|
||||||
|
elif child.tag == '{%s}upperCorner' % ns_gml:
|
||||||
|
upperCorner.append(child.text)
|
||||||
|
# Converting into Decimal
|
||||||
|
pointCounter = 0
|
||||||
|
for point in lowerCorner:
|
||||||
|
dy = dy + (Decimal(point.split(" ")[0]))
|
||||||
|
dx = dx + (Decimal(point.split(" ")[1]))
|
||||||
|
pointCounter = pointCounter + 1
|
||||||
|
|
||||||
|
dyret = -dy / pointCounter
|
||||||
|
dxret = -dx / pointCounter
|
||||||
|
|
||||||
|
return [Decimal(str(int(dxret))), Decimal(str(int(dyret)))]
|
||||||
|
|
||||||
|
|
||||||
|
# This function is used to Parse the text from the CityGML file to Decimal numbers and
|
||||||
|
# to aplly the previously calculated translation parameters
|
||||||
|
# Notice: this code only allows ONE "gml:PosList-Element" for each Interior and exterior of a Polygon
|
||||||
|
# If there are more than just one,just the first one is going to be transformed
|
||||||
|
def splitAndApplyTrafo(coordString, transParam):
|
||||||
|
# Splitting the coordinate string by empty spaces
|
||||||
|
split = coordString.split(" ")
|
||||||
|
alalala = len(split)
|
||||||
|
# Apply the Trafo
|
||||||
|
if split[0] == '':
|
||||||
|
split.pop(0)
|
||||||
|
if split[-1] == "":
|
||||||
|
split.pop(-1)
|
||||||
|
counter = 0
|
||||||
|
length = int(len(split))
|
||||||
|
length_new = int(length / 3)
|
||||||
|
for i in range(length_new):
|
||||||
|
|
||||||
|
|
||||||
|
# print("y")
|
||||||
|
split[counter] = performStableAddition(split[counter], str(transParam[1]))
|
||||||
|
# print("x")
|
||||||
|
split[counter + 1] = performStableAddition(split[counter + 1], str(transParam[0]))
|
||||||
|
# print("z")
|
||||||
|
split[counter + 2] = performStableAddition(split[counter + 2], str(transParam[2]))
|
||||||
|
|
||||||
|
counter += 3
|
||||||
|
# converting back to a string
|
||||||
|
translated = ""
|
||||||
|
for i in split:
|
||||||
|
if len(translated) == 0:
|
||||||
|
translated = str(i)
|
||||||
|
else:
|
||||||
|
translated = translated + " " + str(i)
|
||||||
|
return translated
|
||||||
|
|
||||||
|
|
||||||
|
# This code is used in order to find all the coordinates that are defined in the CityGML-File
|
||||||
|
# Please Notice: the search for coordinates here has the same limitations as the search for coordinates that
|
||||||
|
# is used in the "CityGML2OBJ" functionality!
|
||||||
|
def applyTranslationToCityGML(CITYGML, root, transParam, ns_citygml, ns_gml, ns_frn, ns_veg, filename):
|
||||||
|
# Iterate over all the cityObjectMembers
|
||||||
|
for obj in root.getiterator('{%s}cityObjectMember' % ns_citygml):
|
||||||
|
# Iterate over all the children of cityObject Member
|
||||||
|
for child in obj.getchildren():
|
||||||
|
# Exclude all the implicitly referenced objects from the transformation
|
||||||
|
if child.findall(
|
||||||
|
'.//{%s}ImplicitGeometry' % ns_citygml) == []:
|
||||||
|
polys = m3dm.polygonFinder(child)
|
||||||
|
# Iterate over all the polygons of the children of cityObjectMember
|
||||||
|
for poly in polys:
|
||||||
|
# decompose all the polygons in the interior and exterior rings
|
||||||
|
exter, inter = m3dm.polydecomposer(poly)
|
||||||
|
# iterate over all the exterior rings
|
||||||
|
for e in exter:
|
||||||
|
# find all the coordinates that are stored as a "posList"
|
||||||
|
if len(e.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||||
|
points_tmp = e.findall('.//{%s}posList' % ns_gml)[0].text
|
||||||
|
points = points_tmp.replace('\n', ' ')
|
||||||
|
translated = splitAndApplyTrafo(points, transParam)
|
||||||
|
# print("Before: ", e.findall('.//{%s}posList' % ns_gml)[0].text)
|
||||||
|
e.findall('.//{%s}posList' % ns_gml)[0].text = translated
|
||||||
|
# print("Result: ", e.findall('.//{%s}posList' % ns_gml)[0].text)
|
||||||
|
# find all the coordinates that are stored as "pos"
|
||||||
|
elif len(e.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||||
|
points = e.findall('.//{%s}pos' % ns_gml)
|
||||||
|
counter = 0
|
||||||
|
for k in points:
|
||||||
|
translated = splitAndApplyTrafo(k.text, transParam)
|
||||||
|
e.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||||
|
counter = counter + 1
|
||||||
|
# iterate over all the interior rings
|
||||||
|
for i in inter:
|
||||||
|
# find all the coordinates that are stored as a "posList"
|
||||||
|
if len(i.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||||
|
points_tmp = i.findall('.//{%s}posList' % ns_gml)[0].text
|
||||||
|
points = points_tmp.replace('\n', ' ')
|
||||||
|
translated = splitAndApplyTrafo(points, transParam)
|
||||||
|
i.findall('.//{%s}posList' % ns_gml)[0].text = translated
|
||||||
|
# find all the coordinates that are stored as "pos"
|
||||||
|
elif len(i.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||||
|
points = i.findall('.//{%s}pos' % ns_gml)
|
||||||
|
counter = 0
|
||||||
|
for k in points:
|
||||||
|
translated = splitAndApplyTrafo(k.text, transParam)
|
||||||
|
i.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||||
|
counter = counter + 1
|
||||||
|
|
||||||
|
else: # This condition is used in order to transform the reference points of the implicitly defined geometries
|
||||||
|
# Step 1: find all the reference points:
|
||||||
|
referencePoints = child.findall('.//{%s}referencePoint' % ns_citygml)
|
||||||
|
for referencePoint in referencePoints:
|
||||||
|
points_tmp = referencePoint.findall('.//{%s}pos' % ns_gml)
|
||||||
|
points = points_tmp.replace('\n', ' ')
|
||||||
|
counter = 0
|
||||||
|
for l in points:
|
||||||
|
translated = splitAndApplyTrafo(l.text, transParam)
|
||||||
|
referencePoint.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||||
|
counter = counter + 1
|
||||||
|
|
||||||
|
# Iterate over all the envelopes
|
||||||
|
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||||
|
lowerCorner = envelope.findall('.//{%s}lowerCorner' % ns_gml)[0].text
|
||||||
|
upperCorner = envelope.findall('.//{%s}upperCorner' % ns_gml)[0].text
|
||||||
|
translatedLowerCorner = splitAndApplyTrafo(lowerCorner, transParam)
|
||||||
|
translatedUpperCorner = splitAndApplyTrafo(upperCorner, transParam)
|
||||||
|
envelope.findall('.//{%s}lowerCorner' % ns_gml)[0].text = translatedLowerCorner
|
||||||
|
envelope.findall('.//{%s}upperCorner' % ns_gml)[0].text = translatedUpperCorner
|
||||||
|
|
||||||
|
CITYGML.write(filename + "_local_" + ".gml")
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
# This function is used in order to write the previously calculated translation parameters to a
|
||||||
|
# designated .txt file. The use of this functionality is optional and can be activated by setting the
|
||||||
|
# optional "write2file" parameter to "True" when calling the "translateToLocalCRS" - function.
|
||||||
|
def writeTransparam2File(filename, directory, transParam):
|
||||||
|
textfileName = directory + filename + "_Translation_Parameters.txt"
|
||||||
|
f = open(textfileName, "w")
|
||||||
|
f.write("This file contains the translation parameters that were applied to the original CityGML file." + "\n" +
|
||||||
|
"Conversion tool developed by Filip Biljecki, TU Delft <fbiljecki@gmail.com>" + "\n" +
|
||||||
|
"Conversion tool extended by Thomas Fröch, TUM <thomas.froech@tum.de>" + "\n" +
|
||||||
|
"see more at Github:" + "\n" +
|
||||||
|
"https://github.com/tudelft3d/CityGML2OBJs" + "\n" + "\n")
|
||||||
|
key = ['y', 'x', 'z']
|
||||||
|
for i in range(len(transParam)):
|
||||||
|
f.write(key[i] + ': ' + str(transParam[i]) + ' ')
|
||||||
|
f.close()
|
||||||
|
print("Translation parameters written to: " + textfileName)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def translateToLocalCRS(CITYGML, file, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, directory, write2file=False,
|
||||||
|
applyHeight=Decimal("0")):
|
||||||
|
envelopes = getEnvelopes(root, ns_bldg, ns_gml, ns_citygml)
|
||||||
|
transParam = getTranslationParameters(envelopes, ns_gml)
|
||||||
|
transParam.append(applyHeight)
|
||||||
|
if write2file == True:
|
||||||
|
writeTransparam2File(file, directory, transParam)
|
||||||
|
applyTranslationToCityGML(CITYGML, root, transParam, ns_citygml, ns_gml, ns_frn, ns_veg, file)
|
||||||
|
return 0
|
||||||
21
scripts/CityGML2OBJv2/LICENSE
Normal file
21
scripts/CityGML2OBJv2/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Chair of Geoinformatics, Technical University of Munich
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
80
scripts/CityGML2OBJv2/README.md
Normal file
80
scripts/CityGML2OBJv2/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# :cityscape: CityGML2OBJ 2.0 :cityscape:
|
||||||
|
Command line converter of **CityGML (.gml)** to **OBJ (.obj)** files, while maintaining the semantics
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## :arrow_forward: How to run?
|
||||||
|
The `CityGML2OBJs.py` represents the starting point of the code, choose this file when configuring the runtime and pass the following parameters:
|
||||||
|
|
||||||
|
`-i your-input-citygml-path-here`
|
||||||
|
|
||||||
|
`-o your-output-obj-path-here`
|
||||||
|
|
||||||
|
Please make sure to use the absolute paths to the respective directories.
|
||||||
|
|
||||||
|
and Bob's your uncle! :construction_worker:
|
||||||
|
|
||||||
|
### :wrench: Optional features
|
||||||
|
|
||||||
|
| Optional feature | specification |
|
||||||
|
| -------- | -------- |
|
||||||
|
| Semanitcs Option|`-s 1`|
|
||||||
|
| Geometry Validation | `-v 1`|
|
||||||
|
| Object Preservation | `-g 1`|
|
||||||
|
| Skip the triangulation | `-p 1`|
|
||||||
|
| Conversion of the resulting dataset into a local coordinate system | `-t 1`|
|
||||||
|
| Translation of the CityGML dataset into a local coordinate system before further processing, without saving the translation parameters|`-tC 1`|
|
||||||
|
| Translation of the CityGML dataset into a local coordinate system before further processing, with saving the translation parameters to a designated .txt file|`-tCw 1`|
|
||||||
|
|
||||||
|
|
||||||
|
## :page_with_curl: Requirements
|
||||||
|
|
||||||
|
### Python packages:
|
||||||
|
|
||||||
|
+ [Numpy](http://docs.scipy.org/doc/numpy/user/install.html)
|
||||||
|
+ [Triangle](http://dzhelil.info/triangle/)
|
||||||
|
+ [lxml](http://lxml.de)
|
||||||
|
+ [Shapely](https://github.com/Toblerity/Shapely)
|
||||||
|
+ [Decimal](https://docs.python.org/3/library/decimal.html)
|
||||||
|
|
||||||
|
#### Optional:
|
||||||
|
|
||||||
|
+ [Matplotlib](http://matplotlib.org/users/installing.html)
|
||||||
|
|
||||||
|
### Tested:
|
||||||
|
|
||||||
|
Using Python 3.10 and Windows 10 OS
|
||||||
|
|
||||||
|
### CityGML Requirements:
|
||||||
|
|
||||||
|
#### Mandatory:
|
||||||
|
|
||||||
|
+ CityGML 1.0 or 2.0
|
||||||
|
+ Files must end with `.gml`, `.GML`, `.xml`, or `.XML`
|
||||||
|
+ Vertices in either `<gml:posList>` or `<gml:pos>`
|
||||||
|
+ Your files must be valid (e.g., free check with [CityDoctor](https://transfer.hft-stuttgart.de/gitlab/citydoctor/citydoctor2)
|
||||||
|
|
||||||
|
#### Optional, but recommended:
|
||||||
|
|
||||||
|
+ `<gml:id>` for each `<bldg:Building>` and other types of city objects
|
||||||
|
+ `<gml:id>` for each `<gml:Polygon>`
|
||||||
|
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
Information on the limitations can be found in this [Wiki Page](https://github.com/tum-gis/citygml2obj-2.0/wiki/Limitations)
|
||||||
|
|
||||||
|
## :handshake: Credits
|
||||||
|
We are indebted to [Filip Biljecki](https://github.com/fbiljecki), [Hugo Ledoux](https://github.com/hugoledoux) and [Ravi Peters](https://github.com/Ylannl) from [TU Delft](https://github.com/tudelft3d) for their initial version of the CityGML2OBJs converter. The archived version of the repo can still be found here: https://github.com/tudelft3d/CityGML2OBJs; the paper:
|
||||||
|
|
||||||
|
Biljecki, F., & Arroyo Ohori, K. (2015). Automatic semantic-preserving conversion between OBJ and CityGML. Eurographics Workshop on Urban Data Modelling and Visualisation 2015, pp. 25-30.
|
||||||
|
|
||||||
|
[[PDF]](https://filipbiljecki.com/publications/2015_udmv_citygml_obj.pdf) [[DOI]](http://doi.org/10.2312/udmv.20151345)
|
||||||
|
|
||||||
|
## :mailbox: Contact & Feedback
|
||||||
|
|
||||||
|
Feel free to open a discussion under Issues or write us an email
|
||||||
|
|
||||||
|
- [Thomas Froech](thomas.froech@tum.de)
|
||||||
|
- [Benedikt Schwab](benedikt.schwab@tum.de)
|
||||||
|
- [Olaf Wysocki](olaf.wysocki@tum.de)
|
||||||
65857
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.obj
Normal file
65857
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.obj
Normal file
File diff suppressed because it is too large
Load diff
483630
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.xml
Normal file
483630
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.xml
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
BIN
scripts/CityGML2OBJv2/__pycache__/markup3dmodule.cpython-38.pyc
Normal file
BIN
scripts/CityGML2OBJv2/__pycache__/markup3dmodule.cpython-38.pyc
Normal file
Binary file not shown.
BIN
scripts/CityGML2OBJv2/__pycache__/polygon3dmodule.cpython-38.pyc
Normal file
BIN
scripts/CityGML2OBJv2/__pycache__/polygon3dmodule.cpython-38.pyc
Normal file
Binary file not shown.
756
scripts/CityGML2OBJv2/componentseparationmodule.py
Normal file
756
scripts/CityGML2OBJv2/componentseparationmodule.py
Normal file
|
|
@ -0,0 +1,756 @@
|
||||||
|
import re
|
||||||
|
import config
|
||||||
|
import markup3dmodule as m3dm
|
||||||
|
import polygon3dmodule as p3dm
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import open3d as o3d
|
||||||
|
import open3d.core as o3c
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# Function to create triangles at each corner in 3D
|
||||||
|
def create_corner_triangles(box_points, triangle_size=1):
|
||||||
|
triangles = []
|
||||||
|
for i, point in enumerate(box_points):
|
||||||
|
x, y, z = point
|
||||||
|
if i == 0: # Bottom-left-front corner
|
||||||
|
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y + triangle_size, z]])
|
||||||
|
elif i == 1: # Bottom-right-front corner
|
||||||
|
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y + triangle_size, z]])
|
||||||
|
elif i == 2: # Top-left-front corner
|
||||||
|
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y - triangle_size, z]])
|
||||||
|
elif i == 3: # Bottom-left-back corner
|
||||||
|
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y + triangle_size, z]])
|
||||||
|
elif i == 4: # Top-right-back corner
|
||||||
|
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y - triangle_size, z]])
|
||||||
|
elif i == 5: # Top-left-back corner
|
||||||
|
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y - triangle_size, z]])
|
||||||
|
elif i == 6: # Bottom-right-back corner
|
||||||
|
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y + triangle_size, z]])
|
||||||
|
elif i == 7: # Top-right-front corner
|
||||||
|
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y - triangle_size, z]])
|
||||||
|
return triangles
|
||||||
|
|
||||||
|
|
||||||
|
def addTranslationParameters(e, i, trans_param):
|
||||||
|
# Convert lists to numpy arrays for easier manipulation
|
||||||
|
e = np.array(e)
|
||||||
|
# i = np.array(i)
|
||||||
|
i_translated = []
|
||||||
|
trans_param = np.array(trans_param)
|
||||||
|
# case distinction for empty translation parameters
|
||||||
|
if len(trans_param) > 0:
|
||||||
|
# Subtract the translation parameters from each point
|
||||||
|
if len(e) > 0:
|
||||||
|
e_translated = e - trans_param
|
||||||
|
elif len(e) == 0:
|
||||||
|
e_translated = np.asarray([])
|
||||||
|
# Iterate over all the different interior rings
|
||||||
|
if len(i) > 0:
|
||||||
|
for interior_ring in i:
|
||||||
|
interior_ring = np.asarray(interior_ring)
|
||||||
|
# Translate the interior ring
|
||||||
|
interior_ring_translated = interior_ring - trans_param
|
||||||
|
# Coollect the translated interior rings
|
||||||
|
i_translated.append(interior_ring_translated.tolist())
|
||||||
|
else:
|
||||||
|
i_translated = i
|
||||||
|
return e_translated.tolist(), i_translated
|
||||||
|
else:
|
||||||
|
return e.tolist(), i
|
||||||
|
|
||||||
|
def getBufferedBBoxPoints(b):
|
||||||
|
# Schritt 1: identifying all wallsurfaces and roof surfaces of the building
|
||||||
|
output = {}
|
||||||
|
specifyVersion()
|
||||||
|
# comprehensive list of semantic surfaces
|
||||||
|
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface', ]
|
||||||
|
|
||||||
|
for semanticSurface in semanticSurfaces:
|
||||||
|
output[semanticSurface] = []
|
||||||
|
data = []
|
||||||
|
for cl in output:
|
||||||
|
cls = []
|
||||||
|
for child in b.getiterator():
|
||||||
|
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||||
|
cls.append(child)
|
||||||
|
|
||||||
|
for feature in cls:
|
||||||
|
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||||
|
e, i = m3dm.polydecomposer(p)
|
||||||
|
epoints = m3dm.GMLpoints(e[0])
|
||||||
|
# -- Clean recurring points, except the last one
|
||||||
|
last_ep = epoints[-1]
|
||||||
|
epoints_clean = list(remove_reccuring(epoints))
|
||||||
|
epoints_clean.append(last_ep)
|
||||||
|
for point in epoints_clean:
|
||||||
|
data.append(point)
|
||||||
|
|
||||||
|
# Schritt 2: Idetify the Bounding volume
|
||||||
|
# 2.1 creating an open3d pointcloud from all the idetified vertex points
|
||||||
|
pcd = o3d.t.geometry.PointCloud(o3c.Tensor(data, o3c.float32))
|
||||||
|
# 2.2 obtain the axis aligned boundign box of the point cloud
|
||||||
|
axis_aligned_bb = pcd.get_axis_aligned_bounding_box()
|
||||||
|
# Schritt 3: Construct small triangles that describe the boundign box sufficienly
|
||||||
|
box_points = axis_aligned_bb.get_box_points().numpy().tolist()
|
||||||
|
# Convert the list to a numpy array for easier manipulation
|
||||||
|
box_points = np.array(box_points)
|
||||||
|
# Calculate the min and max coordinates
|
||||||
|
min_x, min_y, min_z = np.min(box_points, axis=0)
|
||||||
|
max_x, max_y, max_z = np.max(box_points, axis=0)
|
||||||
|
# Add a 3m buffer
|
||||||
|
buffer = 3
|
||||||
|
min_x -= buffer
|
||||||
|
min_y -= buffer
|
||||||
|
min_z -= buffer
|
||||||
|
max_x += buffer
|
||||||
|
max_y += buffer
|
||||||
|
max_z += buffer
|
||||||
|
# Define the buffered bounding box points
|
||||||
|
buffered_box_points = np.array([
|
||||||
|
[min_x, min_y, min_z],
|
||||||
|
[max_x, min_y, min_z],
|
||||||
|
[min_x, max_y, min_z],
|
||||||
|
[min_x, min_y, max_z],
|
||||||
|
[max_x, max_y, max_z],
|
||||||
|
[min_x, max_y, max_z],
|
||||||
|
[max_x, min_y, max_z],
|
||||||
|
[max_x, max_y, min_z]
|
||||||
|
])
|
||||||
|
return buffered_box_points
|
||||||
|
|
||||||
|
def obtainSRSInfo(root):
|
||||||
|
specifyVersion()
|
||||||
|
# obtain the envelope object
|
||||||
|
envelopes = []
|
||||||
|
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||||
|
envelopes.append(envelope)
|
||||||
|
|
||||||
|
# Extracting the srsName attribute from each Envelope
|
||||||
|
srs_names = [envelope.get('srsName') for envelope in envelopes]
|
||||||
|
srs_Dimensions = [envelope.get('srsDimension') for envelope in envelopes]
|
||||||
|
return srs_names, srs_Dimensions
|
||||||
|
|
||||||
|
|
||||||
|
# This function is used to create a corresponding json file defining the bbox of an object for each corresponding obj file
|
||||||
|
def writeBBOXJSON(b, overall_counter, path, b_counter, trans_param):
|
||||||
|
if len(trans_param) > 0:
|
||||||
|
translation_parameters = {
|
||||||
|
"d_x": str(trans_param[0]),
|
||||||
|
"d_y": str(trans_param[1]),
|
||||||
|
"d_z": str(trans_param[2])
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
translation_parameters = {
|
||||||
|
"d_x": str(0),
|
||||||
|
"d_y": str(0),
|
||||||
|
"d_z": str(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffered_box_points_global = getBufferedBBoxPoints(b)
|
||||||
|
|
||||||
|
# translate to the local coordinate system
|
||||||
|
buffered_box_points, _ = addTranslationParameters(buffered_box_points_global, [], trans_param=trans_param)
|
||||||
|
|
||||||
|
# From this set of points, obtain the minimum set that is necessary to describe the bounding box.
|
||||||
|
min_point = buffered_box_points[0]
|
||||||
|
max_point = buffered_box_points[4]
|
||||||
|
|
||||||
|
# Construct the json file path
|
||||||
|
json_file_path = str(path) + str(b_counter) + "_" + str(overall_counter) + "_bbox_" + ".json"
|
||||||
|
|
||||||
|
# Write the bbox to a designated json file
|
||||||
|
# Prüfen, ob die JSON-Datei existiert und laden
|
||||||
|
if os.path.exists(json_file_path):
|
||||||
|
with open(json_file_path, 'r') as json_file:
|
||||||
|
axis_aligned_bbox = json.load(json_file)
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
axis_aligned_bbox = {}
|
||||||
|
|
||||||
|
# Neuen Identifier hinzufügen
|
||||||
|
axis_aligned_bbox["axis_aligned_bbox"] = {
|
||||||
|
"min_point": str(min_point),
|
||||||
|
"max_point": str(max_point),
|
||||||
|
"translation_parameters": translation_parameters
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zuordnungen in JSON-Datei schreiben
|
||||||
|
with open(json_file_path, 'w') as json_file:
|
||||||
|
json.dump(axis_aligned_bbox, json_file, indent=4)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# This function is used to add information about the used spatial reference system to the json file
|
||||||
|
def addCRSToJSON(root, json_file_path):
|
||||||
|
specifyVersion()
|
||||||
|
# obtain the envelope object
|
||||||
|
envelopes = []
|
||||||
|
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||||
|
envelopes.append(envelope)
|
||||||
|
if envelopes:
|
||||||
|
# Extracting the srsName attribute from each Envelope
|
||||||
|
srs_names = [envelope.get('srsName') for envelope in envelopes]
|
||||||
|
srs_Dimensions = [envelope.get('srsDimension') for envelope in envelopes]
|
||||||
|
elif not envelopes:
|
||||||
|
srs_names = "Unkown"
|
||||||
|
srs_Dimensions = "Unkown"
|
||||||
|
|
||||||
|
|
||||||
|
used_srs = srs_names[0]
|
||||||
|
# Prüfen, ob die JSON-Datei existiert und laden
|
||||||
|
if os.path.exists(json_file_path):
|
||||||
|
with open(json_file_path, 'r') as json_file:
|
||||||
|
crs_info = json.load(json_file)
|
||||||
|
else:
|
||||||
|
crs_info = {}
|
||||||
|
|
||||||
|
# Neuen Identifier hinzufügen
|
||||||
|
crs_info["CRS"] = {
|
||||||
|
"srsName": used_srs,
|
||||||
|
"srsDimensions": srs_Dimensions
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zuordnungen in JSON-Datei schreiben
|
||||||
|
with open(json_file_path, 'w') as json_file:
|
||||||
|
json.dump(crs_info, json_file, indent=4)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def claculateCornerTriangles(b, trans_param):
|
||||||
|
buffered_box_points = getBufferedBBoxPoints(b)
|
||||||
|
|
||||||
|
# Translate the Bounding box into the local coordinate system
|
||||||
|
# buffered_box_points, _ = addTranslationParameters(buffered_box_points_global, [], trans_param=trans_param)
|
||||||
|
|
||||||
|
# Create triangles at the corners of the buffered bounding box
|
||||||
|
corner_triangles = create_corner_triangles(buffered_box_points)
|
||||||
|
|
||||||
|
# Convert the triangles to lists
|
||||||
|
corner_triangles = [np.array(triangle).tolist() for triangle in corner_triangles]
|
||||||
|
|
||||||
|
print("\nCorner Triangles:")
|
||||||
|
for i, triangle in enumerate(corner_triangles):
|
||||||
|
print(f"Triangle {i + 1}: {triangle}")
|
||||||
|
|
||||||
|
return corner_triangles
|
||||||
|
|
||||||
|
|
||||||
|
# diese funktion dient dazu ein JSON file zu schreiben um die meta informationen über die einzelnen objekte zuspeichern
|
||||||
|
def add_identifier_to_json(filename, tag, parentID, gmlID, json_file_path):
|
||||||
|
"""
|
||||||
|
Adds the identifier information for one .obj file to a JSON file.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- number (int): The number corresponding to the .obj file.
|
||||||
|
- tag (str): The tag corresponding to the .obj file.
|
||||||
|
- parentID (str): The parent ID corresponding to the .obj file.
|
||||||
|
- gmlID (str): The gml ID corresponding to the .obj file.
|
||||||
|
- json_file_path (str): Path to the JSON file where identifier information will be stored.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prüfen, ob die JSON-Datei existiert und laden
|
||||||
|
if os.path.exists(json_file_path):
|
||||||
|
with open(json_file_path, 'r') as json_file:
|
||||||
|
identifiers = json.load(json_file)
|
||||||
|
else:
|
||||||
|
identifiers = {}
|
||||||
|
|
||||||
|
# Neuen Identifier hinzufügen
|
||||||
|
identifiers[filename] = {
|
||||||
|
'tag': tag,
|
||||||
|
'parentID': parentID,
|
||||||
|
'gmlID': gmlID
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zuordnungen in JSON-Datei schreiben
|
||||||
|
with open(json_file_path, 'w') as json_file:
|
||||||
|
json.dump(identifiers, json_file, indent=4)
|
||||||
|
|
||||||
|
print(f"Zuordnung für {filename} wurde gespeichert.")
|
||||||
|
|
||||||
|
|
||||||
|
def perturb_points(points, perturbation_scale=1e-6):
|
||||||
|
"""
|
||||||
|
Perturb the points slightly to avoid degenerate cases.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
points (list of tuple of floats): A list where each element is a tuple (x, y, z) representing a 3D point.
|
||||||
|
perturbation_scale (float): The maximum magnitude of the perturbation applied to each coordinate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of list of floats: Perturbed list of points.
|
||||||
|
"""
|
||||||
|
points_array = np.array(points)
|
||||||
|
perturbation = np.random.uniform(-perturbation_scale, perturbation_scale, points_array.shape)
|
||||||
|
perturbed_points = points_array + perturbation
|
||||||
|
return perturbed_points.tolist()
|
||||||
|
|
||||||
|
|
||||||
|
def write_obj_file(surfaces, filename, tag, parentid, gmlid, counter, path, tr_1, translation_parameters):
|
||||||
|
for triangle in tr_1:
|
||||||
|
triangle_local, _ = addTranslationParameters(triangle, [], trans_param=translation_parameters)
|
||||||
|
surfaces.append(triangle_local)
|
||||||
|
with open(filename, 'w') as file:
|
||||||
|
vertex_index = 1
|
||||||
|
for triangle in surfaces:
|
||||||
|
for vertex in triangle:
|
||||||
|
file.write(f"v {vertex[0]} {vertex[1]} {vertex[2]}\n")
|
||||||
|
file.write(f"f {vertex_index} {vertex_index + 1} {vertex_index + 2}\n")
|
||||||
|
vertex_index += 3
|
||||||
|
add_identifier_to_json(filename, tag, parentid, gmlid, (path + "index.json"))
|
||||||
|
|
||||||
|
|
||||||
|
def remove_reccuring(list_vertices):
|
||||||
|
"""Removes recurring vertices, which messes up the triangulation.
|
||||||
|
Inspired by http://stackoverflow.com/a/1143432"""
|
||||||
|
# last_point = list_vertices[-1]
|
||||||
|
list_vertices_without_last = list_vertices[:-1]
|
||||||
|
found = set()
|
||||||
|
for item in list_vertices_without_last:
|
||||||
|
if str(item) not in found:
|
||||||
|
yield item
|
||||||
|
found.add(str(item))
|
||||||
|
|
||||||
|
|
||||||
|
def separate_string(s):
|
||||||
|
# Define the regex pattern
|
||||||
|
pattern = r'\{([^}]*)\}(.*)'
|
||||||
|
# Search for the pattern in the input string
|
||||||
|
match = re.search(pattern, s)
|
||||||
|
if match:
|
||||||
|
# Extract the parts
|
||||||
|
inside_braces = match.group(1)
|
||||||
|
outside_braces = match.group(2)
|
||||||
|
return inside_braces, outside_braces
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def specifyVersion():
|
||||||
|
global ns_citygml
|
||||||
|
global ns_gml
|
||||||
|
global ns_bldg
|
||||||
|
global ns_xsi
|
||||||
|
global ns_xAL
|
||||||
|
global ns_xlink
|
||||||
|
global ns_dem
|
||||||
|
global ns_con
|
||||||
|
global ns_app
|
||||||
|
global ns_pcl
|
||||||
|
global ns_gen
|
||||||
|
global ns_gss
|
||||||
|
global ns_pfx0
|
||||||
|
global ns_gsr
|
||||||
|
global ns_tran
|
||||||
|
global ns_gmd
|
||||||
|
global ns_gts
|
||||||
|
global ns_veg
|
||||||
|
global ns_frn
|
||||||
|
global ns_tun
|
||||||
|
global ns_wtr
|
||||||
|
global nsmap
|
||||||
|
|
||||||
|
if config.getVersion() == 1:
|
||||||
|
# -- Name spaces for CityGML 2.0
|
||||||
|
ns_citygml = "http://www.opengis.net/citygml/1.0"
|
||||||
|
ns_gml = "http://www.opengis.net/gml"
|
||||||
|
ns_bldg = "http://www.opengis.net/citygml/building/1.0"
|
||||||
|
ns_tran = "http://www.opengis.net/citygml/transportation/1.0"
|
||||||
|
ns_veg = "http://www.opengis.net/citygml/vegetation/1.0"
|
||||||
|
ns_gen = "http://www.opengis.net/citygml/generics/1.0"
|
||||||
|
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:1.0"
|
||||||
|
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||||
|
ns_dem = "http://www.opengis.net/citygml/relief/1.0"
|
||||||
|
ns_frn = "http://www.opengis.net/citygml/cityfurniture/1.0"
|
||||||
|
ns_tun = "http://www.opengis.net/citygml/tunnel/1.0"
|
||||||
|
ns_wtr = "http://www.opengis.net/citygml/waterbody/1.0"
|
||||||
|
ns_brid = "http://www.opengis.net/citygml/bridge/1.0"
|
||||||
|
ns_app = "http://www.opengis.net/citygml/appearance/1.0"
|
||||||
|
if config.getVersion() == 2:
|
||||||
|
# -- Name spaces for CityGML 2.0
|
||||||
|
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||||
|
ns_gml = "http://www.opengis.net/gml"
|
||||||
|
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||||
|
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||||
|
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||||
|
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||||
|
elif config.getVersion() == 3:
|
||||||
|
# -- Name spaces for CityGML 3.0
|
||||||
|
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||||
|
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||||
|
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||||
|
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||||
|
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||||
|
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||||
|
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||||
|
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||||
|
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||||
|
ns_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||||
|
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||||
|
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||||
|
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||||
|
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||||
|
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||||
|
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||||
|
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||||
|
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||||
|
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||||
|
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||||
|
|
||||||
|
nsmap = {
|
||||||
|
None: ns_citygml,
|
||||||
|
'gml': ns_gml,
|
||||||
|
'bldg': ns_bldg,
|
||||||
|
'xsi': ns_xsi,
|
||||||
|
'xAL': ns_xAL,
|
||||||
|
'xlink': ns_xlink,
|
||||||
|
'dem': ns_dem
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_convex_hull(points):
|
||||||
|
"""
|
||||||
|
Computes the convex hull of a set of 3D points using Open3D, including triangulation of the hull's faces.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
points (list of tuple of floats): A list where each element is a tuple (x, y, z) representing a 3D point.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of faces, where each face is a list of vertex coordinates forming that face.
|
||||||
|
"""
|
||||||
|
# Convert the list of points to a NumPy array
|
||||||
|
points_array = np.array(points)
|
||||||
|
perturbed_points = perturb_points(points_array)
|
||||||
|
|
||||||
|
# Create an Open3D PointCloud object
|
||||||
|
pcd = o3d.geometry.PointCloud()
|
||||||
|
pcd.points = o3d.utility.Vector3dVector(perturbed_points)
|
||||||
|
|
||||||
|
# Compute the convex hull
|
||||||
|
hull, triangles = pcd.compute_convex_hull()
|
||||||
|
|
||||||
|
# Extract vertices and faces (triangles)
|
||||||
|
hull_vertices = np.asarray(hull.vertices)
|
||||||
|
hull_triangles = np.asarray(hull.triangles)
|
||||||
|
|
||||||
|
# Prepare the faces in the desired format
|
||||||
|
faces = []
|
||||||
|
for triangle in hull_triangles:
|
||||||
|
face = [list(hull_vertices[vertex]) for vertex in triangle]
|
||||||
|
faces.append(face)
|
||||||
|
return faces
|
||||||
|
|
||||||
|
|
||||||
|
def process_polygon(p, trans_param):
|
||||||
|
e = p[0]
|
||||||
|
i = p[1]
|
||||||
|
e_trans, i_trans = addTranslationParameters(e, i, trans_param=trans_param)
|
||||||
|
t = p3dm.triangulation(e_trans, i_trans)
|
||||||
|
# print(f"t: {t}")
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def process_polygons_parallel(polys, trans_param):
|
||||||
|
data = []
|
||||||
|
results = []
|
||||||
|
for poly in polys:
|
||||||
|
e, i = m3dm.polydecomposer(poly)
|
||||||
|
epoints = m3dm.GMLpoints(e[0])
|
||||||
|
# -- Clean recurring points, except the last one
|
||||||
|
last_ep = epoints[-1]
|
||||||
|
epoints_clean = list(remove_reccuring(epoints))
|
||||||
|
epoints_clean.append(last_ep)
|
||||||
|
# -- LinearRing(s) forming the interior
|
||||||
|
irings = []
|
||||||
|
for iring in i:
|
||||||
|
ipoints = m3dm.GMLpoints(iring)
|
||||||
|
# -- Clean them in the same manner as the exterior ring
|
||||||
|
last_ip = ipoints[-1]
|
||||||
|
ipoints_clean = list(remove_reccuring(ipoints))
|
||||||
|
ipoints_clean.append(last_ip)
|
||||||
|
irings.append(ipoints_clean)
|
||||||
|
if len(epoints_clean) > 4:
|
||||||
|
t = process_polygon([epoints_clean, irings], trans_param=trans_param)
|
||||||
|
results.append(t)
|
||||||
|
# data.append(poly_components)
|
||||||
|
if len(epoints_clean) == 4:
|
||||||
|
epoints_clean_translated, _ = addTranslationParameters(epoints_clean, [], trans_param=trans_param)
|
||||||
|
results.append([epoints_clean_translated])
|
||||||
|
|
||||||
|
# t = process_polygon(poly)
|
||||||
|
# results.append(t)
|
||||||
|
|
||||||
|
# cpu_cores = os.cpu_count()
|
||||||
|
# print(f'Number of available CPU cores (using os): {cpu_cores}')
|
||||||
|
# with ThreadPoolExecutor(max_workers=cpu_cores) as executor:
|
||||||
|
# # Submitting all tasks
|
||||||
|
# futures = [executor.submit(process_polygon, p) for p in data]
|
||||||
|
# # Collecting results
|
||||||
|
# for future in futures:
|
||||||
|
# result = future.result() # This will re-raise any exception caught during the execution of the task
|
||||||
|
# results.append(result)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# this is an experimental method for parallelization
|
||||||
|
def processOpening(o, path, buildingid, overall_counter, tr_1, trans_param, b_counter):
|
||||||
|
for child in o.getiterator():
|
||||||
|
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||||
|
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||||
|
polys = m3dm.polygonFinder(o)
|
||||||
|
t = process_polygons_parallel(polys, trans_param=trans_param)
|
||||||
|
triangles = []
|
||||||
|
for poly in t:
|
||||||
|
for tr in poly:
|
||||||
|
triangles.append(tr)
|
||||||
|
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||||
|
write_obj_file(triangles, filename, str(child.tag), buildingid, unique_identifier, overall_counter, path,
|
||||||
|
tr_1, trans_param)
|
||||||
|
|
||||||
|
|
||||||
|
def getAllExteriorPoints(polys):
|
||||||
|
data = []
|
||||||
|
for poly in polys:
|
||||||
|
e, i = m3dm.polydecomposer(poly)
|
||||||
|
epoints = m3dm.GMLpoints(e[0])
|
||||||
|
# -- Clean recurring points, except the last one
|
||||||
|
last_ep = epoints[-1]
|
||||||
|
epoints_clean = list(remove_reccuring(epoints))
|
||||||
|
epoints_clean.append(last_ep)
|
||||||
|
for point in epoints_clean:
|
||||||
|
data.append(point)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def processWithApproximatedWindows(o, path, buildingid, overall_counter, tr_1, translation_parameters, b_counter):
|
||||||
|
for child in o.getiterator():
|
||||||
|
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||||
|
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||||
|
polys = m3dm.polygonFinder(o)
|
||||||
|
exterior_points = getAllExteriorPoints(polys)
|
||||||
|
t_global = compute_convex_hull(exterior_points)
|
||||||
|
_, t = addTranslationParameters(e=[], i=t_global, trans_param=translation_parameters)
|
||||||
|
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||||
|
write_obj_file(t, filename, str(child.tag), buildingid, unique_identifier, overall_counter, path, tr_1,
|
||||||
|
translation_parameters=translation_parameters)
|
||||||
|
|
||||||
|
|
||||||
|
# This function is used to im port a bounding box that is associated to the corresponding building component
|
||||||
|
# It still has to be tested if it works
|
||||||
|
def importBoundingBox(pathToBoundingBoxFile, trans_param):
|
||||||
|
print("Hier")
|
||||||
|
keys = ["_xmin", "_xmax", "_ymin", "_ymax", "_zmin", "_zmax"]
|
||||||
|
values = {}
|
||||||
|
|
||||||
|
with open(pathToBoundingBoxFile, 'r') as file:
|
||||||
|
for _ in range(15):
|
||||||
|
line = file.readline()
|
||||||
|
for key in keys:
|
||||||
|
match = re.search(f'"{key}"\s*:\s*([-0-9.]+)', line)
|
||||||
|
if match:
|
||||||
|
values[key] = float(match.group(1))
|
||||||
|
|
||||||
|
if not all(k in values for k in keys):
|
||||||
|
raise ValueError("Missing bounding box values in the first 15 lines")
|
||||||
|
|
||||||
|
min_x, max_x = values["_xmin"], values["_xmax"]
|
||||||
|
min_y, max_y = values["_ymin"], values["_ymax"]
|
||||||
|
min_z, max_z = values["_zmin"], values["_zmax"]
|
||||||
|
|
||||||
|
print(f"min x: {min_x} min y: {min_y} , min z: {min_z}")
|
||||||
|
print(f"max x: {max_x} max y: {max_y} , max z: {max_z}")
|
||||||
|
|
||||||
|
box_points = np.array([
|
||||||
|
[min_x, min_y, min_z],
|
||||||
|
[max_x, min_y, min_z],
|
||||||
|
[min_x, max_y, min_z],
|
||||||
|
[min_x, min_y, max_z],
|
||||||
|
[max_x, max_y, max_z],
|
||||||
|
[min_x, max_y, max_z],
|
||||||
|
[max_x, min_y, max_z],
|
||||||
|
[max_x, max_y, min_z]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Create triangles at the corners of the buffered bounding box
|
||||||
|
corner_triangles = create_corner_triangles(box_points)
|
||||||
|
|
||||||
|
# Convert the triangles to lists
|
||||||
|
corner_triangles = [np.array(triangle).tolist() for triangle in corner_triangles]
|
||||||
|
|
||||||
|
print("\nCorner Triangles:")
|
||||||
|
for i, triangle in enumerate(corner_triangles):
|
||||||
|
print(f"Triangle {i + 1}: {triangle}")
|
||||||
|
|
||||||
|
return corner_triangles
|
||||||
|
|
||||||
|
|
||||||
|
def separateComponents(b, path, APPROXIMATEWINDOWS, ADDBOUNDINGBOX, ADDBOUNDINGBOXJSON, TRANSLATEBUILDINGS,
|
||||||
|
IMPORTBOUNDINGBOX, b_counter):
|
||||||
|
if TRANSLATEBUILDINGS:
|
||||||
|
# Step 1: Obtain the axis oriented bounding box of the building
|
||||||
|
bounding_box_points = getBufferedBBoxPoints(b)
|
||||||
|
|
||||||
|
# Step 2 calculate the mean value of the points that the bbox points
|
||||||
|
translation_parameters = np.mean(bounding_box_points, axis=0)
|
||||||
|
|
||||||
|
if not TRANSLATEBUILDINGS: # todo: nocheinmal üerlegen ob man hier nicht vielleicht besser elif oder so nehmen sollte
|
||||||
|
translation_parameters = []
|
||||||
|
|
||||||
|
if IMPORTBOUNDINGBOX != None:
|
||||||
|
ADDBOUNDINGBOX = False # make sure that the bounding box is not calculated
|
||||||
|
|
||||||
|
# Option to include the small triangles to mark the buffered bounding box
|
||||||
|
if ADDBOUNDINGBOX:
|
||||||
|
tr_1 = claculateCornerTriangles(b, trans_param=translation_parameters)
|
||||||
|
elif not ADDBOUNDINGBOX:
|
||||||
|
tr_1 = []
|
||||||
|
global overall_counter
|
||||||
|
overall_counter = 0
|
||||||
|
output = {}
|
||||||
|
specifyVersion()
|
||||||
|
# comprehensive list of semantic surfaces
|
||||||
|
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface',
|
||||||
|
'InteriorWallSurface', 'FloorSurface', 'OuterCeilingSurface', 'OuterFloorSurface', 'Door',
|
||||||
|
"outerBuildingInstallation",
|
||||||
|
'Window', "BuildingInstallation", "BuildingConstructiveElement"]
|
||||||
|
|
||||||
|
for semanticSurface in semanticSurfaces:
|
||||||
|
output[semanticSurface] = []
|
||||||
|
|
||||||
|
# get the building id for the building
|
||||||
|
buildingid = b.xpath("@g:id", namespaces={'g': ns_gml})
|
||||||
|
|
||||||
|
# Handle the case when a building has no gml:id - This should however never occur...
|
||||||
|
if not buildingid:
|
||||||
|
buildingid = b_counter
|
||||||
|
|
||||||
|
# Todo: Muss noch implementiert werden
|
||||||
|
# Import the bounding box
|
||||||
|
if IMPORTBOUNDINGBOX != None:
|
||||||
|
pathToBoundingBoxFile = IMPORTBOUNDINGBOX + "/" + str(buildingid) + ".json"
|
||||||
|
tr_1 = importBoundingBox(pathToBoundingBoxFile=pathToBoundingBoxFile, trans_param=translation_parameters)
|
||||||
|
|
||||||
|
if config.getVersion() != 3:
|
||||||
|
openings = []
|
||||||
|
openingpolygons = []
|
||||||
|
for child in b.getiterator():
|
||||||
|
if child.tag == '{%s}opening' % ns_bldg:
|
||||||
|
openings.append(child)
|
||||||
|
for o in child.findall('.//{%s}Polygon' % ns_gml):
|
||||||
|
openingpolygons.append(o)
|
||||||
|
|
||||||
|
for o in openings:
|
||||||
|
# print("approximate windows: ", APPROXIMATEWINDOWS)
|
||||||
|
if APPROXIMATEWINDOWS:
|
||||||
|
processWithApproximatedWindows(o, path, buildingid, overall_counter, tr_1=tr_1,
|
||||||
|
translation_parameters=translation_parameters, b_counter=b_counter)
|
||||||
|
if not APPROXIMATEWINDOWS:
|
||||||
|
processOpening(o, path, buildingid, overall_counter, tr_1, trans_param=translation_parameters,
|
||||||
|
b_counter=b_counter)
|
||||||
|
if ADDBOUNDINGBOXJSON:
|
||||||
|
writeBBOXJSON(b, overall_counter=overall_counter, path=path, b_counter=b_counter,
|
||||||
|
trans_param=translation_parameters)
|
||||||
|
overall_counter += 1
|
||||||
|
|
||||||
|
if config.getVersion() == 3:
|
||||||
|
openingpolygons = []
|
||||||
|
print("Component separation for CityGML 3.0 is not implemented yet.")
|
||||||
|
# todo: muss noch implementiert werden
|
||||||
|
|
||||||
|
# -- Process other thematic boundaries
|
||||||
|
for cl in output:
|
||||||
|
cls = []
|
||||||
|
for child in b.getiterator():
|
||||||
|
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||||
|
cls.append(child)
|
||||||
|
|
||||||
|
for feature in cls:
|
||||||
|
# -- If it is the first feature, print the object identifier
|
||||||
|
unique_identifier = feature.xpath("@g:id", namespaces={
|
||||||
|
'g': ns_gml})
|
||||||
|
if str(unique_identifier) != "[]" or str(unique_identifier) == "[]":
|
||||||
|
cleaned_filename = str(unique_identifier)
|
||||||
|
# -- This is not supposed to happen, but just to be sure...
|
||||||
|
if feature.tag == '{%s}Window' % ns_bldg or feature.tag == '{%s}Door' % ns_bldg:
|
||||||
|
continue
|
||||||
|
tag = feature.tag
|
||||||
|
_, cleaned_tag = separate_string(tag)
|
||||||
|
# -- Find all polygons in this semantic boundary hierarchy
|
||||||
|
poly_t = []
|
||||||
|
t_ges = []
|
||||||
|
number_of_polygons = len(feature.findall('.//{%s}Polygon' % ns_gml))
|
||||||
|
pcounter = 0
|
||||||
|
print(f"there are {number_of_polygons} polygons there!")
|
||||||
|
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||||
|
found_opening = False
|
||||||
|
for optest in openingpolygons:
|
||||||
|
if p == optest:
|
||||||
|
found_opening = True
|
||||||
|
break
|
||||||
|
# -- If there is an opening skip it
|
||||||
|
if found_opening:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# -- Decompose the polygon into exterior and interior
|
||||||
|
e, i = m3dm.polydecomposer(p)
|
||||||
|
# -- Points forming the exterior LinearRing
|
||||||
|
epoints = m3dm.GMLpoints(e[0])
|
||||||
|
# -- Clean recurring points, except the last one
|
||||||
|
last_ep = epoints[-1]
|
||||||
|
epoints_clean = list(remove_reccuring(epoints))
|
||||||
|
epoints_clean.append(last_ep)
|
||||||
|
|
||||||
|
# -- LinearRing(s) forming the interior
|
||||||
|
irings = []
|
||||||
|
for iring in i:
|
||||||
|
ipoints = m3dm.GMLpoints(iring)
|
||||||
|
# -- Clean them in the same manner as the exterior ring
|
||||||
|
last_ip = ipoints[-1]
|
||||||
|
ipoints_clean = list(remove_reccuring(ipoints))
|
||||||
|
ipoints_clean.append(last_ip)
|
||||||
|
irings.append(ipoints_clean)
|
||||||
|
|
||||||
|
# Applying the translation parameters
|
||||||
|
e_trans, i_trans = addTranslationParameters(e=epoints_clean, i=irings,
|
||||||
|
trans_param=translation_parameters)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if len(epoints_clean) > 4:
|
||||||
|
t = p3dm.triangulation(e_trans, i_trans)
|
||||||
|
# print("Nur drei Punkte")
|
||||||
|
poly_t.append(t)
|
||||||
|
if len(epoints_clean) == 4:
|
||||||
|
t = e_trans[0:3]
|
||||||
|
poly_t.append([t])
|
||||||
|
if len(epoints_clean) < 3:
|
||||||
|
t = []
|
||||||
|
# print("Empty Surface!")
|
||||||
|
except:
|
||||||
|
t = []
|
||||||
|
|
||||||
|
for surfaces in poly_t:
|
||||||
|
t_ges = t_ges + surfaces
|
||||||
|
|
||||||
|
pcounter += 1
|
||||||
|
if pcounter % 100 == 0:
|
||||||
|
print(pcounter)
|
||||||
|
|
||||||
|
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||||
|
if ADDBOUNDINGBOXJSON:
|
||||||
|
writeBBOXJSON(b, overall_counter=overall_counter, path=path, b_counter=b_counter,
|
||||||
|
trans_param=translation_parameters)
|
||||||
|
write_obj_file(t_ges, filename, str(feature.tag), buildingid, cleaned_filename, overall_counter, path,
|
||||||
|
tr_1, translation_parameters=translation_parameters)
|
||||||
|
overall_counter += 1
|
||||||
|
|
||||||
|
print("Segmentation finished!")
|
||||||
|
return 0
|
||||||
7
scripts/CityGML2OBJv2/config.py
Normal file
7
scripts/CityGML2OBJv2/config.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
def setVersion(version):
|
||||||
|
global VERSION
|
||||||
|
VERSION = version
|
||||||
|
print(f"version: {VERSION}")
|
||||||
|
|
||||||
|
def getVersion():
|
||||||
|
return VERSION
|
||||||
52
scripts/CityGML2OBJv2/generateMTL.py
Normal file
52
scripts/CityGML2OBJv2/generateMTL.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
|
||||||
|
# This code is part of the CityGML2OBJs package
|
||||||
|
|
||||||
|
# Copyright (c) 2014
|
||||||
|
# Filip Biljecki
|
||||||
|
# Delft University of Technology
|
||||||
|
# fbiljecki@gmail.com
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.cm as cm
|
||||||
|
|
||||||
|
#-- Number of classes of the colormap
|
||||||
|
no_values = 101
|
||||||
|
|
||||||
|
#-- Select the colormap and get its RGB values for each of the classes
|
||||||
|
colormap = cm.get_cmap("afmhot", no_values) # http://matplotlib.org/examples/color/colormaps_reference.html
|
||||||
|
colormap_vals = colormap(np.arange(no_values)).tolist()
|
||||||
|
|
||||||
|
#-- This is the MTL file
|
||||||
|
mtlcontents = ""
|
||||||
|
|
||||||
|
#-- Class by class... MTL!
|
||||||
|
for i in range(0, no_values):
|
||||||
|
b = float(i)/100
|
||||||
|
mtlcontents += "newmtl " + str(b) + "\n"
|
||||||
|
mtlcontents += "Ka " + str(colormap_vals[i][0]) + " " + str(colormap_vals[i][1]) + " " + str(colormap_vals[i][2]) + "\n"
|
||||||
|
mtlcontents += "Kd " + str(colormap_vals[i][0]) + " " + str(colormap_vals[i][1]) + " " + str(colormap_vals[i][2]) + "\n"
|
||||||
|
#-- Write the MTL
|
||||||
|
with open("colormap.mtl", "w") as mtl_file:
|
||||||
|
mtl_file.write(mtlcontents)
|
||||||
148
scripts/CityGML2OBJv2/markup3dmodule.py
Normal file
148
scripts/CityGML2OBJv2/markup3dmodule.py
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import config
|
||||||
|
# The MIT License (MIT)
|
||||||
|
|
||||||
|
# This code is part of the CityGML2OBJs package
|
||||||
|
|
||||||
|
# Copyright (c) 2014
|
||||||
|
# Filip Biljecki
|
||||||
|
# Delft University of Technology
|
||||||
|
# fbiljecki@gmail.com
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
from config import setVersion
|
||||||
|
def specifyVersion():
|
||||||
|
global ns_citygml
|
||||||
|
global ns_gml
|
||||||
|
global ns_bldg
|
||||||
|
global ns_xsi
|
||||||
|
global ns_xAL
|
||||||
|
global ns_xlink
|
||||||
|
global ns_dem
|
||||||
|
global ns_con
|
||||||
|
global ns_app
|
||||||
|
global ns_pcl
|
||||||
|
global ns_gen
|
||||||
|
global ns_gss
|
||||||
|
global ns_pfx0
|
||||||
|
global ns_gsr
|
||||||
|
global ns_tran
|
||||||
|
global ns_gmd
|
||||||
|
global ns_gts
|
||||||
|
global ns_veg
|
||||||
|
global ns_frn
|
||||||
|
global ns_tun
|
||||||
|
global ns_wtr
|
||||||
|
global nsmap
|
||||||
|
|
||||||
|
#print("config.getVerision", config.getVersion())
|
||||||
|
if config.getVersion() == 2 or config.getVersion() == 1:
|
||||||
|
# -- Name spaces for CityGML 2.0
|
||||||
|
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||||
|
ns_gml = "http://www.opengis.net/gml"
|
||||||
|
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||||
|
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||||
|
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||||
|
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||||
|
elif config.getVersion() == 3:
|
||||||
|
# -- Name spaces for CityGML 3.0
|
||||||
|
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||||
|
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||||
|
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||||
|
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||||
|
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||||
|
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||||
|
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||||
|
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||||
|
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||||
|
ns_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||||
|
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||||
|
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||||
|
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||||
|
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||||
|
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||||
|
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||||
|
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||||
|
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||||
|
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||||
|
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||||
|
|
||||||
|
nsmap = {
|
||||||
|
None: ns_citygml,
|
||||||
|
'gml': ns_gml,
|
||||||
|
'bldg': ns_bldg,
|
||||||
|
'xsi': ns_xsi,
|
||||||
|
'xAL': ns_xAL,
|
||||||
|
'xlink': ns_xlink,
|
||||||
|
'dem': ns_dem
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def polydecomposer(polygon):
|
||||||
|
"""Extracts the <gml:exterior> and <gml:interior> of a <gml:Polygon>."""
|
||||||
|
specifyVersion()
|
||||||
|
exter = polygon.findall('.//{%s}exterior' % ns_gml)
|
||||||
|
inter = polygon.findall('.//{%s}interior' % ns_gml)
|
||||||
|
return exter, inter
|
||||||
|
|
||||||
|
|
||||||
|
def polygonFinder(GMLelement):
|
||||||
|
"""Find the <gml:polygon> element."""
|
||||||
|
specifyVersion()
|
||||||
|
#print("NSgml:", ns_gml)
|
||||||
|
polygonsLocal = GMLelement.findall('.//{%s}Polygon' % ns_gml)
|
||||||
|
#print(polygonsLocal)
|
||||||
|
|
||||||
|
#for polygon in polygonsLocal:
|
||||||
|
# gml_id = polygon.get('{%s}id' % ns_gml)
|
||||||
|
# print(gml_id)
|
||||||
|
return polygonsLocal
|
||||||
|
|
||||||
|
|
||||||
|
def GMLpoints(ring):
|
||||||
|
specifyVersion()
|
||||||
|
"Extract points from a <gml:LinearRing>."
|
||||||
|
# -- List containing points
|
||||||
|
listPoints = []
|
||||||
|
# -- Read the <gml:posList> value and convert to string
|
||||||
|
if len(ring.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||||
|
points = ring.findall('.//{%s}posList' % ns_gml)[0].text
|
||||||
|
# -- List of coordinates
|
||||||
|
coords = points.split()
|
||||||
|
assert (len(coords) % 3 == 0)
|
||||||
|
# -- Store the coordinate tuple
|
||||||
|
for i in range(0, len(coords), 3):
|
||||||
|
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||||
|
elif len(ring.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||||
|
points = ring.findall('.//{%s}pos' % ns_gml)
|
||||||
|
# -- Extract each point separately
|
||||||
|
for p in points:
|
||||||
|
coords = p.text.split()
|
||||||
|
assert (len(coords) % 3 == 0)
|
||||||
|
# -- Store the coordinate tuple
|
||||||
|
for i in range(0, len(coords), 3):
|
||||||
|
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return listPoints
|
||||||
77
scripts/CityGML2OBJv2/plotcolorbar.py
Normal file
77
scripts/CityGML2OBJv2/plotcolorbar.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
|
||||||
|
# This code is part of the CityGML2OBJs package
|
||||||
|
|
||||||
|
# Copyright (c) 2014
|
||||||
|
# Filip Biljecki
|
||||||
|
# Delft University of Technology
|
||||||
|
# fbiljecki@gmail.com
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
import matplotlib as mpl
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
#-- Resource: http://matplotlib.org/examples/api/colorbar_only.html
|
||||||
|
plt.rc('text', usetex=True)
|
||||||
|
plt.rc('font', family='serif')
|
||||||
|
#-- Make a figure and axes with dimensions as desired
|
||||||
|
fig = plt.figure(figsize=(8, 1))
|
||||||
|
ax1 = fig.add_axes([0.05, 0.80, 0.9, 0.15])
|
||||||
|
|
||||||
|
#-- Bounds
|
||||||
|
vmin = 350
|
||||||
|
vmax = 1300
|
||||||
|
|
||||||
|
#-- Colormap
|
||||||
|
cmap = mpl.cm.afmhot
|
||||||
|
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
||||||
|
|
||||||
|
cb1 = mpl.colorbar.ColorbarBase(ax1, cmap=cmap,
|
||||||
|
norm=norm,
|
||||||
|
orientation='horizontal')
|
||||||
|
|
||||||
|
#-- Label on the axis
|
||||||
|
cb1.set_label(r"Annual solar irradiation [kWh/m$^{2}$/year]")
|
||||||
|
|
||||||
|
|
||||||
|
cmap = mpl.colors.ListedColormap(['r', 'g', 'b', 'c'])
|
||||||
|
cmap.set_over('0.25')
|
||||||
|
cmap.set_under('0.75')
|
||||||
|
|
||||||
|
bounds = [1, 2, 4, 7, 8]
|
||||||
|
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
|
||||||
|
|
||||||
|
#-- Change the last tick label
|
||||||
|
labels = [item.get_text() for item in cb1.ax.get_xticklabels()]
|
||||||
|
li = 0
|
||||||
|
for l in labels:
|
||||||
|
# labels[li] = r"$"+str(l)+"$"
|
||||||
|
labels[li] = r""+str(l)
|
||||||
|
li += 1
|
||||||
|
labels[-1] = r"$\geq "+str(vmax) + "$"
|
||||||
|
cb1.ax.set_xticklabels(labels)
|
||||||
|
|
||||||
|
#-- Output
|
||||||
|
plt.savefig('colorbar.pdf', transparent=True)
|
||||||
|
plt.savefig('colorbar.png', dpi=600, transparent=True)
|
||||||
|
plt.show()
|
||||||
716
scripts/CityGML2OBJv2/polygon3dmodule.py
Normal file
716
scripts/CityGML2OBJv2/polygon3dmodule.py
Normal file
|
|
@ -0,0 +1,716 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
|
||||||
|
# This code is part of the CityGML2OBJs package
|
||||||
|
|
||||||
|
# Copyright (c) 2014
|
||||||
|
# Filip Biljecki
|
||||||
|
# Delft University of Technology
|
||||||
|
# fbiljecki@gmail.com
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
import math
|
||||||
|
import markup3dmodule
|
||||||
|
from lxml import etree
|
||||||
|
import copy
|
||||||
|
import triangle
|
||||||
|
import numpy as np
|
||||||
|
import shapely
|
||||||
|
from sklearn.decomposition import PCA
|
||||||
|
|
||||||
|
|
||||||
|
def getAreaOfGML(poly, height=True):
|
||||||
|
"""Function which reads <gml:Polygon> and returns its area.
|
||||||
|
The function also accounts for the interior and checks for the validity of the polygon."""
|
||||||
|
exteriorarea = 0.0
|
||||||
|
interiorarea = 0.0
|
||||||
|
# -- Decompose the exterior and interior boundary
|
||||||
|
e, i = markup3dmodule.polydecomposer(poly)
|
||||||
|
# -- Extract points in the <gml:LinearRing> of <gml:exterior>
|
||||||
|
epoints = markup3dmodule.GMLpoints(e[0])
|
||||||
|
if isPolyValid(epoints):
|
||||||
|
if height:
|
||||||
|
exteriorarea += get3DArea(epoints)
|
||||||
|
else:
|
||||||
|
exteriorarea += get2DArea(epoints)
|
||||||
|
for idx, iring in enumerate(i):
|
||||||
|
# -- Extract points in the <gml:LinearRing> of <gml:interior>
|
||||||
|
ipoints = markup3dmodule.GMLpoints(iring)
|
||||||
|
if isPolyValid(ipoints):
|
||||||
|
if height:
|
||||||
|
interiorarea += get3DArea(ipoints)
|
||||||
|
else:
|
||||||
|
interiorarea += get2DArea(ipoints)
|
||||||
|
# -- Account for the interior
|
||||||
|
area = exteriorarea - interiorarea
|
||||||
|
# -- Area in dimensionless units (coordinate units)
|
||||||
|
return area
|
||||||
|
|
||||||
|
|
||||||
|
# -- Validity of a polygon ---------
|
||||||
|
def isPolyValid(polypoints, output=True):
|
||||||
|
"""Checks if a polygon is valid. Second option is to supress output."""
|
||||||
|
# -- Number of points of the polygon (including the doubled first/last point)
|
||||||
|
npolypoints = len(polypoints)
|
||||||
|
# -- Assume that it is valid, and try to disprove the assumption
|
||||||
|
valid = True
|
||||||
|
# -- Check if last point equal
|
||||||
|
if polypoints[0] != polypoints[-1]:
|
||||||
|
if output:
|
||||||
|
print("\t\tA degenerate polygon. First and last points do not match.")
|
||||||
|
valid = False
|
||||||
|
# -- Check if it has at least three points
|
||||||
|
if npolypoints < 4: # -- Four because the first point is doubled as the last one in the ring
|
||||||
|
if output:
|
||||||
|
print("\t\tA degenerate polygon. The number of points is smaller than 3.")
|
||||||
|
valid = False
|
||||||
|
# -- Check if the points are planar
|
||||||
|
if not isPolyPlanar(polypoints):
|
||||||
|
if output:
|
||||||
|
print("\t\tA degenerate polygon. The points are not planar.")
|
||||||
|
valid = False
|
||||||
|
# -- Check if some of the points are repeating
|
||||||
|
for i in range(1, npolypoints):
|
||||||
|
if polypoints[i] == polypoints[i - 1]:
|
||||||
|
if output:
|
||||||
|
print("\t\tA degenerate polygon. There are identical points.")
|
||||||
|
valid = False
|
||||||
|
# -- Check if the polygon does not have self-intersections
|
||||||
|
# -- Disabled, something doesn't work here, will work on this later.
|
||||||
|
# if not isPolySimple(polypoints):
|
||||||
|
# print "A degenerate polygon. The edges are intersecting."
|
||||||
|
# valid = False
|
||||||
|
return valid
|
||||||
|
|
||||||
|
|
||||||
|
def isPolyPlanar(polypoints):
|
||||||
|
"""Checks if a polygon is planar."""
|
||||||
|
# -- Normal of the polygon from the first three points
|
||||||
|
try:
|
||||||
|
normal = unit_normal(polypoints[0], polypoints[1], polypoints[2])
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
# -- Number of points
|
||||||
|
npolypoints = len(polypoints)
|
||||||
|
# -- Tolerance
|
||||||
|
eps = 0.01
|
||||||
|
# -- Assumes planarity
|
||||||
|
planar = True
|
||||||
|
for i in range(3, npolypoints):
|
||||||
|
vector = [polypoints[i][0] - polypoints[0][0], polypoints[i][1] - polypoints[0][1],
|
||||||
|
polypoints[i][2] - polypoints[0][2]]
|
||||||
|
if math.fabs(dot(vector, normal)) > eps:
|
||||||
|
planar = False
|
||||||
|
return planar
|
||||||
|
|
||||||
|
|
||||||
|
def isPolySimple(polypoints): #todo: this function has to be adapted
|
||||||
|
"""Checks if the polygon is simple, i.e. it does not have any self-intersections.
|
||||||
|
Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf"""
|
||||||
|
npolypoints = len(polypoints)
|
||||||
|
# -- Check if the polygon is vertical, i.e. a projection cannot be made.
|
||||||
|
# -- First copy the list so the originals are not modified
|
||||||
|
temppolypoints = copy.deepcopy(polypoints)
|
||||||
|
newpolypoints = copy.deepcopy(temppolypoints)
|
||||||
|
# -- If the polygon is vertical
|
||||||
|
#if math.fabs(unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])[2]) < 10e-6:
|
||||||
|
# vertical = True
|
||||||
|
|
||||||
|
#else:
|
||||||
|
# vertical = False
|
||||||
|
|
||||||
|
normal = calculate_polygon_normal(temppolypoints)
|
||||||
|
if math.fabs(normal[2]) < 10e-6:
|
||||||
|
vertical = True
|
||||||
|
print("The polygon is vertical 2")
|
||||||
|
print("math.fabs(normal[2]): ", math.fabs(normal[2]))
|
||||||
|
else:
|
||||||
|
vertical = False
|
||||||
|
print("Not vertical 2")
|
||||||
|
|
||||||
|
# -- We want to project the vertical polygon to the XZ plane
|
||||||
|
# -- If a polygon is parallel with the YZ plane that will not be possible
|
||||||
|
YZ = True
|
||||||
|
for i in range(1, npolypoints):
|
||||||
|
if temppolypoints[i][0] != temppolypoints[0][0]:
|
||||||
|
YZ = False
|
||||||
|
continue
|
||||||
|
# -- Project the plane in the special case
|
||||||
|
if YZ:
|
||||||
|
for i in range(0, npolypoints):
|
||||||
|
newpolypoints[i][0] = temppolypoints[i][1]
|
||||||
|
newpolypoints[i][1] = temppolypoints[i][2]
|
||||||
|
# -- Project the plane
|
||||||
|
elif vertical:
|
||||||
|
for i in range(0, npolypoints):
|
||||||
|
newpolypoints[i][1] = temppolypoints[i][2]
|
||||||
|
else:
|
||||||
|
pass # -- No changes here
|
||||||
|
# -- Check for the self-intersection edge by edge
|
||||||
|
for i in range(0, npolypoints - 3):
|
||||||
|
if i == 0:
|
||||||
|
m = npolypoints - 3
|
||||||
|
else:
|
||||||
|
m = npolypoints - 2
|
||||||
|
for j in range(i + 2, m):
|
||||||
|
if intersection(newpolypoints[i], newpolypoints[i + 1], newpolypoints[j % npolypoints],
|
||||||
|
newpolypoints[(j + 1) % npolypoints]):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def intersection(p, q, r, s):
|
||||||
|
"""Check if two line segments (pq and rs) intersect. Computation is in 2D.
|
||||||
|
Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf"""
|
||||||
|
|
||||||
|
eps = 10e-6
|
||||||
|
|
||||||
|
V = [q[0] - p[0], q[1] - p[1]]
|
||||||
|
W = [r[0] - s[0], r[1] - s[1]]
|
||||||
|
|
||||||
|
d = V[0] * W[1] - W[0] * V[1]
|
||||||
|
|
||||||
|
if math.fabs(d) < eps:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
|
||||||
|
def collinear(p0, p1, p2):
|
||||||
|
# -- http://stackoverflow.com/a/9609069
|
||||||
|
x1, y1 = p1[0] - p0[0], p1[1] - p0[1]
|
||||||
|
x2, y2 = p2[0] - p0[0], p2[1] - p0[1]
|
||||||
|
return x1 * y2 - x2 * y1 < 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
# -- Area and other handy computations
|
||||||
|
def det(a):
|
||||||
|
"""Determinant of matrix a."""
|
||||||
|
return a[0][0] * a[1][1] * a[2][2] + a[0][1] * a[1][2] * a[2][0] + a[0][2] * a[1][0] * a[2][1] - a[0][2] * a[1][1] * \
|
||||||
|
a[2][0] - a[0][1] * a[1][0] * a[2][2] - a[0][0] * a[1][2] * a[2][1]
|
||||||
|
|
||||||
|
|
||||||
|
def unit_normal(a, b, c):
|
||||||
|
"""Unit normal vector of plane defined by points a, b, and c."""
|
||||||
|
x = det([[1, a[1], a[2]],
|
||||||
|
[1, b[1], b[2]],
|
||||||
|
[1, c[1], c[2]]])
|
||||||
|
y = det([[a[0], 1, a[2]],
|
||||||
|
[b[0], 1, b[2]],
|
||||||
|
[c[0], 1, c[2]]])
|
||||||
|
z = det([[a[0], a[1], 1],
|
||||||
|
[b[0], b[1], 1],
|
||||||
|
[c[0], c[1], 1]])
|
||||||
|
magnitude = (x ** 2 + y ** 2 + z ** 2) ** .5
|
||||||
|
if magnitude == 0.0:
|
||||||
|
raise ValueError(
|
||||||
|
"The normal of the polygon has no magnitude. Check the polygon. The most common cause for this are two identical sequential points or collinear points.")
|
||||||
|
return (x / magnitude, y / magnitude, z / magnitude)
|
||||||
|
|
||||||
|
|
||||||
|
def dot(a, b):
|
||||||
|
"""Dot product of vectors a and b."""
|
||||||
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||||
|
|
||||||
|
|
||||||
|
def cross(a, b):
|
||||||
|
"""Cross product of vectors a and b."""
|
||||||
|
x = a[1] * b[2] - a[2] * b[1]
|
||||||
|
y = a[2] * b[0] - a[0] * b[2]
|
||||||
|
z = a[0] * b[1] - a[1] * b[0]
|
||||||
|
return (x, y, z)
|
||||||
|
|
||||||
|
|
||||||
|
def get3DArea(polypoints):
|
||||||
|
"""Function which reads the list of coordinates and returns its area.
|
||||||
|
The code has been borrowed from http://stackoverflow.com/questions/12642256/python-find-area-of-polygon-from-xyz-coordinates"""
|
||||||
|
# -- Compute the area
|
||||||
|
total = [0, 0, 0]
|
||||||
|
for i in range(len(polypoints)):
|
||||||
|
vi1 = polypoints[i]
|
||||||
|
if i is len(polypoints) - 1:
|
||||||
|
vi2 = polypoints[0]
|
||||||
|
else:
|
||||||
|
vi2 = polypoints[i + 1]
|
||||||
|
prod = cross(vi1, vi2)
|
||||||
|
total[0] += prod[0]
|
||||||
|
total[1] += prod[1]
|
||||||
|
total[2] += prod[2]
|
||||||
|
result = dot(total, unit_normal(polypoints[0], polypoints[1], polypoints[2]))
|
||||||
|
return math.fabs(result * .5)
|
||||||
|
|
||||||
|
|
||||||
|
def get2DArea(polypoints):
|
||||||
|
"""Reads the list of coordinates and returns its projected area (disregards z coords)."""
|
||||||
|
flatpolypoints = copy.deepcopy(polypoints)
|
||||||
|
for p in flatpolypoints:
|
||||||
|
p[2] = 0.0
|
||||||
|
return get3DArea(flatpolypoints)
|
||||||
|
|
||||||
|
|
||||||
|
def getNormal(polypoints):
|
||||||
|
"""Get the normal of the first three points of a polygon. Assumes planarity."""
|
||||||
|
return unit_normal(polypoints[0], polypoints[1], polypoints[2])
|
||||||
|
|
||||||
|
|
||||||
|
def getAngles(normal):
|
||||||
|
"""Get the azimuth and altitude from the normal vector."""
|
||||||
|
# -- Convert from polar system to azimuth
|
||||||
|
azimuth = 90 - math.degrees(math.atan2(normal[1], normal[0]))
|
||||||
|
if azimuth >= 360.0:
|
||||||
|
azimuth -= 360.0
|
||||||
|
elif azimuth < 0.0:
|
||||||
|
azimuth += 360.0
|
||||||
|
t = math.sqrt(normal[0] ** 2 + normal[1] ** 2)
|
||||||
|
if t == 0:
|
||||||
|
tilt = 0.0
|
||||||
|
else:
|
||||||
|
tilt = 90 - math.degrees(math.atan(normal[2] / t)) # 0 for flat roof, 90 for wall
|
||||||
|
tilt = round(tilt, 3)
|
||||||
|
|
||||||
|
return azimuth, tilt
|
||||||
|
|
||||||
|
|
||||||
|
def GMLstring2points(pointstring):
|
||||||
|
"""Convert list of points in string to a list of points. Works for 3D points."""
|
||||||
|
listPoints = []
|
||||||
|
# -- List of coordinates
|
||||||
|
coords = pointstring.split()
|
||||||
|
# -- Store the coordinate tuple
|
||||||
|
assert (len(coords) % 3 == 0)
|
||||||
|
for i in range(0, len(coords), 3):
|
||||||
|
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||||
|
return listPoints
|
||||||
|
|
||||||
|
|
||||||
|
def smallestPoint(list_of_points):
|
||||||
|
"Finds the smallest point from a three-dimensional tuple list."
|
||||||
|
smallest = []
|
||||||
|
# -- Sort the points
|
||||||
|
sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2]))
|
||||||
|
# -- First one is the smallest one
|
||||||
|
smallest = sorted_points[0]
|
||||||
|
return smallest
|
||||||
|
|
||||||
|
|
||||||
|
def highestPoint(list_of_points, a=None):
|
||||||
|
"Finds the highest point from a three-dimensional tuple list."
|
||||||
|
highest = []
|
||||||
|
# -- Sort the points
|
||||||
|
sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2]))
|
||||||
|
# -- Last one is the highest one
|
||||||
|
if a is not None:
|
||||||
|
equalZ = True
|
||||||
|
for i in range(-1, -1 * len(list_of_points), -1):
|
||||||
|
if equalZ:
|
||||||
|
highest = sorted_points[i]
|
||||||
|
if highest[2] != a[2]:
|
||||||
|
equalZ = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
highest = sorted_points[-1]
|
||||||
|
return highest
|
||||||
|
|
||||||
|
|
||||||
|
def centroid(list_of_points):
|
||||||
|
"""Returns the centroid of the list of points."""
|
||||||
|
sum_x = 0
|
||||||
|
sum_y = 0
|
||||||
|
sum_z = 0
|
||||||
|
n = float(len(list_of_points))
|
||||||
|
for p in list_of_points:
|
||||||
|
sum_x += float(p[0])
|
||||||
|
sum_y += float(p[1])
|
||||||
|
sum_z += float(p[2])
|
||||||
|
return [sum_x / n, sum_y / n, sum_z / n]
|
||||||
|
|
||||||
|
# This function delivers very unsatisfying results for some reason.
|
||||||
|
# The returned point lies on the contour of the polygon sometimes, wich then messes up the triangulation
|
||||||
|
def point_inside(list_of_points):
|
||||||
|
"""Returns a point that is guaranteed to be inside the polygon, thanks to Shapely."""
|
||||||
|
# Th_Fr: new function that actually works
|
||||||
|
representative_point_tmp = centroid(list_of_points)
|
||||||
|
representative_point = shapely.geometry.Point(representative_point_tmp)
|
||||||
|
# End of the changes by Th_Fr
|
||||||
|
return representative_point.coords
|
||||||
|
|
||||||
|
|
||||||
|
def plane(a, b, c):
|
||||||
|
"""Returns the equation of a three-dimensional plane in space by entering the three coordinates of the plane."""
|
||||||
|
p_a = (b[1] - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (b[2] - a[2])
|
||||||
|
p_b = (b[2] - a[2]) * (c[0] - a[0]) - (c[2] - a[2]) * (b[0] - a[0])
|
||||||
|
p_c = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
|
||||||
|
p_d = -1 * (p_a * a[0] + p_b * a[1] + p_c * a[2])
|
||||||
|
return p_a, p_b, p_c, p_d
|
||||||
|
|
||||||
|
# added by Th_Fr
|
||||||
|
def planeAdjusted(points):
|
||||||
|
"""
|
||||||
|
Returns the equation of a plane in three dimensions using PCA.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
points: list of lists or numpy array of shape (n, 3)
|
||||||
|
List of points in 3D space [x, y, z] through which the plane should pass.
|
||||||
|
At least 3 points are required to define a plane uniquely.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
p_a, p_b, p_c, p_d: float
|
||||||
|
Parameters of the plane equation ax + by + cz + d = 0.
|
||||||
|
"""
|
||||||
|
# Convert points to numpy array for easier manipulation
|
||||||
|
points = np.array(points)
|
||||||
|
|
||||||
|
# Check if at least 3 points are provided
|
||||||
|
if points.shape[0] < 3:
|
||||||
|
raise ValueError("At least 3 points are required to define a plane.")
|
||||||
|
|
||||||
|
# Use PCA to fit the plane
|
||||||
|
pca = PCA(n_components=3)
|
||||||
|
pca.fit(points)
|
||||||
|
normal = pca.components_[2] # The normal vector to the plane
|
||||||
|
|
||||||
|
# Extract coefficients
|
||||||
|
p_a, p_b, p_c = normal
|
||||||
|
p_d = -np.dot(normal, pca.mean_) # Calculate d using the mean of points
|
||||||
|
|
||||||
|
return p_a, p_b, p_c, p_d
|
||||||
|
|
||||||
|
|
||||||
|
def get_height(plane, x, y):
|
||||||
|
"""Get the missing coordinate from the plane equation and the partial coordinates."""
|
||||||
|
p_a, p_b, p_c, p_d = plane
|
||||||
|
z = (-p_a * x - p_b * y - p_d) / p_c
|
||||||
|
return z
|
||||||
|
|
||||||
|
|
||||||
|
def get_y(plane, x, z):
|
||||||
|
"""Get the missing coordinate from the plane equation and the partial coordinates."""
|
||||||
|
p_a, p_b, p_c, p_d = plane
|
||||||
|
y = (-p_a * x - p_c * z - p_d) / (p_b)
|
||||||
|
return y
|
||||||
|
|
||||||
|
|
||||||
|
def compare_normals(n1, n2):
|
||||||
|
"""Compares if two normals are equal or opposite. Takes into account a small tolerance to overcome floating point errors."""
|
||||||
|
tolerance = 10e-2
|
||||||
|
# -- Assume equal and prove otherwise
|
||||||
|
equal = True
|
||||||
|
# -- i
|
||||||
|
if math.fabs(n1[0] - n2[0]) > tolerance:
|
||||||
|
equal = False
|
||||||
|
# -- j
|
||||||
|
elif math.fabs(n1[1] - n2[1]) > tolerance:
|
||||||
|
equal = False
|
||||||
|
# -- k
|
||||||
|
elif math.fabs(n1[2] - n2[2]) > tolerance:
|
||||||
|
equal = False
|
||||||
|
return equal
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_vertices(vertices):
|
||||||
|
"""Reverse vertices. Useful to reorient the normal of the polygon."""
|
||||||
|
reversed_vertices = []
|
||||||
|
nv = len(vertices)
|
||||||
|
for i in range(nv - 1, -1, -1):
|
||||||
|
reversed_vertices.append(vertices[i])
|
||||||
|
return reversed_vertices
|
||||||
|
|
||||||
|
# Added by Th_FR, inspirde by https://pythonseminar.de/prufen-ob-die-liste-doppelte-elemente-enthalt-in-python/
|
||||||
|
def has_duplicates(seq):
|
||||||
|
seen = []
|
||||||
|
unique_list = [x for x in seq if x not in seen and not seen.append(x)]
|
||||||
|
return len(seq) != len(unique_list)
|
||||||
|
# End of changes
|
||||||
|
|
||||||
|
# Added by Th_Fr
|
||||||
|
def weighted_centroid(vertices):
|
||||||
|
"""
|
||||||
|
Calculate the weighted centroid of a polygon defined by vertices.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
vertices (numpy array): Array of vertices of the polygon.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy array: Weighted centroid [x, y, z].
|
||||||
|
"""
|
||||||
|
total_area = 0.0
|
||||||
|
centroid = np.zeros(3)
|
||||||
|
num_vertices = len(vertices)
|
||||||
|
|
||||||
|
for i in range(num_vertices):
|
||||||
|
j = (i + 1) % num_vertices
|
||||||
|
cross = np.cross(vertices[i], vertices[j])
|
||||||
|
area = np.linalg.norm(cross)
|
||||||
|
centroid += (vertices[i] + vertices[j]) * area
|
||||||
|
total_area += area
|
||||||
|
|
||||||
|
|
||||||
|
return centroid / (3 * total_area)
|
||||||
|
|
||||||
|
def calculate_polygon_normal_old(poly):
|
||||||
|
"""
|
||||||
|
Calculate the normal vector of a polygon using a weighted centroid and cross product approach.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
poly (list of lists): List of vertices of the polygon, where each vertex is [x, y, z].
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy array: Normal vector (nx, ny, nz) of the polygon's plane.
|
||||||
|
"""
|
||||||
|
vertices = np.array(poly)
|
||||||
|
num_vertices = len(vertices)
|
||||||
|
|
||||||
|
# Calculate weighted centroid
|
||||||
|
#print("here b")
|
||||||
|
centroid1 = centroid(vertices)
|
||||||
|
|
||||||
|
# Compute the normal vector using cross product of edges
|
||||||
|
normal = np.zeros(3)
|
||||||
|
for i in range(num_vertices):
|
||||||
|
j = (i + 1) % num_vertices
|
||||||
|
vi = vertices[i]
|
||||||
|
vj = vertices[j]
|
||||||
|
normal[0] += (vi[1] - centroid1[1]) * (vj[2] - centroid1[2]) - (vi[2] - centroid1[2]) * (vj[1] - centroid1[1])
|
||||||
|
normal[1] += (vi[2] - centroid1[2]) * (vj[0] - centroid1[0]) - (vi[0] - centroid1[0]) * (vj[2] - centroid1[2])
|
||||||
|
normal[2] += (vi[0] - centroid1[0]) * (vj[1] - centroid1[1]) - (vi[1] - centroid1[1]) * (vj[0] - centroid1[0])
|
||||||
|
|
||||||
|
norm = np.linalg.norm(normal)
|
||||||
|
if norm != 0:
|
||||||
|
normal /= norm
|
||||||
|
else:
|
||||||
|
normal = np.array([0.0, 0.0, 0.0])
|
||||||
|
|
||||||
|
return normal
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_polygon_normal(polygon):
|
||||||
|
"""
|
||||||
|
Calculate the surface normal of a polygon.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
polygon (list): A list of vertices, where each vertex is a list of three coordinates [x, y, z].
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: A normalized vector representing the surface normal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
normal = np.array([0.0, 0.0, 0.0])
|
||||||
|
num_verts = len(polygon)
|
||||||
|
|
||||||
|
for i in range(num_verts):
|
||||||
|
current = np.array(polygon[i])
|
||||||
|
next_vert = np.array(polygon[(i + 1) % num_verts])
|
||||||
|
|
||||||
|
normal[0] += (current[1] - next_vert[1]) * (current[2] + next_vert[2])
|
||||||
|
normal[1] += (current[2] - next_vert[2]) * (current[0] + next_vert[0])
|
||||||
|
normal[2] += (current[0] - next_vert[0]) * (current[1] + next_vert[1])
|
||||||
|
|
||||||
|
normal = normalize(normal)
|
||||||
|
return normal
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(vector):
|
||||||
|
"""
|
||||||
|
Normalize a vector.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
vector (np.array): A vector to normalize.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: A normalized vector.
|
||||||
|
"""
|
||||||
|
norm = np.linalg.norm(vector)
|
||||||
|
if norm == 0:
|
||||||
|
return vector
|
||||||
|
return vector / norm
|
||||||
|
|
||||||
|
|
||||||
|
def triangulation(e, i):
|
||||||
|
"""Triangulate the polygon with the exterior and interior list of points. Works only for convex polygons.
|
||||||
|
Assumes planarity. Projects to a 2D plane and goes back to 3D."""
|
||||||
|
vertices = []
|
||||||
|
holes = []
|
||||||
|
segments = []
|
||||||
|
index_point = 0
|
||||||
|
# -- Slope computation points
|
||||||
|
a = [[], [], []]
|
||||||
|
b = [[], [], []]
|
||||||
|
for ip in range(len(e) - 1):
|
||||||
|
vertices.append(e[ip])
|
||||||
|
if a == [[], [], []] and index_point == 0:
|
||||||
|
a = [e[ip][0], e[ip][1], e[ip][2]]
|
||||||
|
if index_point > 0 and (e[ip] != e[ip - 1]):
|
||||||
|
if b == [[], [], []]:
|
||||||
|
b = [e[ip][0], e[ip][1], e[ip][2]]
|
||||||
|
if ip == len(e) - 2:
|
||||||
|
segments.append([index_point, 0])
|
||||||
|
else:
|
||||||
|
segments.append([index_point, index_point + 1])
|
||||||
|
index_point += 1
|
||||||
|
|
||||||
|
for hole in i:
|
||||||
|
first_point_in_hole = index_point
|
||||||
|
for p in range(len(hole) - 1):
|
||||||
|
if p == len(hole) - 2:
|
||||||
|
segments.append([index_point, first_point_in_hole])
|
||||||
|
else:
|
||||||
|
segments.append([index_point, index_point + 1])
|
||||||
|
index_point += 1
|
||||||
|
vertices.append(hole[p])
|
||||||
|
# -- A more robust way to get the point inside the hole, should work for non-convex interior polygons
|
||||||
|
# alt: holes.append(point_inside(hole[:-1]))
|
||||||
|
# -- Alternative, use centroid
|
||||||
|
holes.append(centroid(hole[:-1])) # This should be useful!
|
||||||
|
# -- Project to 2D since the triangulation cannot be done in 3D with the library that is used
|
||||||
|
npolypoints = len(vertices)
|
||||||
|
nholes = len(holes)
|
||||||
|
# -- Check if the polygon is vertical, i.e. a projection cannot be made.
|
||||||
|
# -- First copy the list so the originals are not modified
|
||||||
|
temppolypoints = copy.deepcopy(vertices)
|
||||||
|
newpolypoints = copy.deepcopy(vertices)
|
||||||
|
tempholes = copy.deepcopy(holes)
|
||||||
|
newholes = copy.deepcopy(holes)
|
||||||
|
|
||||||
|
# -- Compute the normal of the polygon for detecting vertical polygons and
|
||||||
|
# -- for the correct orientation of the new triangulated faces
|
||||||
|
# -- If the polygon is vertical
|
||||||
|
#normal = unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])
|
||||||
|
normal = calculate_polygon_normal(temppolypoints)
|
||||||
|
|
||||||
|
if math.fabs(normal[2]) < 10e-2:
|
||||||
|
vertical = True
|
||||||
|
#print("The polygon is vertical")
|
||||||
|
#print("math.fabs(normal[2]): ", math.fabs(normal[2]))
|
||||||
|
else:
|
||||||
|
vertical = False
|
||||||
|
#print("Not vertical")
|
||||||
|
# -- We want to project the vertical polygon to the XZ plane
|
||||||
|
# -- If a polygon is parallel with the YZ plane that will not be possible
|
||||||
|
|
||||||
|
|
||||||
|
YZ = True
|
||||||
|
for i in range(1, npolypoints):
|
||||||
|
if temppolypoints[i][0] != temppolypoints[0][0]:
|
||||||
|
YZ = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
# -- Project the plane in the special case
|
||||||
|
if YZ == True:
|
||||||
|
for i in range(0, npolypoints):
|
||||||
|
newpolypoints[i][0] = temppolypoints[i][1]
|
||||||
|
newpolypoints[i][1] = temppolypoints[i][2]
|
||||||
|
for i in range(0, nholes):
|
||||||
|
newholes[i][0] = tempholes[i][1]
|
||||||
|
newholes[i][1] = tempholes[i][2]
|
||||||
|
# -- Project the plane
|
||||||
|
elif vertical == True:
|
||||||
|
for i in range(0, npolypoints):
|
||||||
|
newpolypoints[i][1] = temppolypoints[i][2]
|
||||||
|
for i in range(0, nholes):
|
||||||
|
newholes[i][1] = tempholes[i][2]
|
||||||
|
else:
|
||||||
|
pass # -- No changes here
|
||||||
|
|
||||||
|
# -- Drop the last point (identical to first)
|
||||||
|
for p in newpolypoints:
|
||||||
|
# print("p: ", p)
|
||||||
|
p.pop(-1)
|
||||||
|
# print("p: ", p)
|
||||||
|
# -- If there are no holes
|
||||||
|
if len(newholes) == 0:
|
||||||
|
newholes = None
|
||||||
|
else:
|
||||||
|
counter = 0
|
||||||
|
for h in newholes:
|
||||||
|
counter = counter + 1
|
||||||
|
h = h.pop(-1)
|
||||||
|
|
||||||
|
# -- Plane information (assumes planarity) #todo: hier muss noch etwas angepasst werden; erledigt?
|
||||||
|
a = e[0]
|
||||||
|
b = e[1]
|
||||||
|
c = e[2]
|
||||||
|
# -- Construct the plane
|
||||||
|
pl = planeAdjusted(e)
|
||||||
|
|
||||||
|
# -- Prepare the polygon to be triangulated
|
||||||
|
# Change by Th_Fr: Distinguishing different cases!
|
||||||
|
# There are two cases distinguished here: 1. A Polygon without holes, 2. A polygon with holes
|
||||||
|
if newholes == None:
|
||||||
|
poly = {'vertices': np.array(newpolypoints), 'segments': np.array(segments)}
|
||||||
|
# For some reason this if.case sometimes fails, this is why there is a second version of the
|
||||||
|
# Trinangulation without the optional 'pQjz' parameter
|
||||||
|
if has_duplicates(newpolypoints) == False:
|
||||||
|
t = triangle.triangulate(poly, 'pQjz')
|
||||||
|
else:
|
||||||
|
t = triangle.triangulate(poly)
|
||||||
|
|
||||||
|
else:
|
||||||
|
poly = {'vertices': np.array(newpolypoints), 'segments': np.array(segments), 'holes': np.array(newholes)}
|
||||||
|
t = triangle.triangulate(poly, 'pQjz')
|
||||||
|
|
||||||
|
# End of changes by Th_Fr
|
||||||
|
|
||||||
|
# -- Get the triangles and their vertices
|
||||||
|
try:
|
||||||
|
tris = t['triangles']
|
||||||
|
except:
|
||||||
|
print("strange error")
|
||||||
|
tris = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
vert = t['vertices'].tolist()
|
||||||
|
except:
|
||||||
|
vert = []
|
||||||
|
# -- Store the vertices of each triangle in a list
|
||||||
|
tri_points = []
|
||||||
|
for tri in tris:
|
||||||
|
tri_points_tmp = []
|
||||||
|
for v in tri.tolist():
|
||||||
|
vert_adj = [[], [], []]
|
||||||
|
if YZ:
|
||||||
|
vert_adj[0] = temppolypoints[0][0]
|
||||||
|
vert_adj[1] = vert[v][0]
|
||||||
|
vert_adj[2] = vert[v][1]
|
||||||
|
elif vertical:
|
||||||
|
vert_adj[0] = vert[v][0]
|
||||||
|
vert_adj[2] = vert[v][1]
|
||||||
|
vert_adj[1] = get_y(pl, vert_adj[0], vert_adj[2])
|
||||||
|
else:
|
||||||
|
vert_adj[0] = vert[v][0]
|
||||||
|
vert_adj[1] = vert[v][1]
|
||||||
|
vert_adj[2] = get_height(pl, vert_adj[0], vert_adj[1])
|
||||||
|
tri_points_tmp.append(vert_adj)
|
||||||
|
try:
|
||||||
|
tri_normal = unit_normal(tri_points_tmp[0], tri_points_tmp[1], tri_points_tmp[2])
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if compare_normals(normal, tri_normal):
|
||||||
|
tri_points.append(tri_points_tmp)
|
||||||
|
else:
|
||||||
|
tri_points_tmp = reverse_vertices(tri_points_tmp)
|
||||||
|
tri_points.append(tri_points_tmp)
|
||||||
|
return tri_points
|
||||||
73
scripts/CityGML2OBJv2/requirements.txt
Normal file
73
scripts/CityGML2OBJv2/requirements.txt
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
asttokens==3.0.1
|
||||||
|
attrs==25.4.0
|
||||||
|
blinker==1.9.0
|
||||||
|
certifi==2026.1.4
|
||||||
|
charset-normalizer==3.4.4
|
||||||
|
click==8.3.1
|
||||||
|
colorama==0.4.6
|
||||||
|
comm==0.2.3
|
||||||
|
ConfigArgParse==1.7.1
|
||||||
|
contourpy==1.3.2
|
||||||
|
cycler==0.12.1
|
||||||
|
dash==4.0.0
|
||||||
|
dash-core-components==2.0.0
|
||||||
|
dash-html-components==2.0.0
|
||||||
|
dash-table==5.0.0
|
||||||
|
decorator==5.2.1
|
||||||
|
exceptiongroup==1.3.1
|
||||||
|
executing==2.2.1
|
||||||
|
fastjsonschema==2.21.2
|
||||||
|
Flask==3.1.3
|
||||||
|
fonttools==4.61.1
|
||||||
|
idna==3.15
|
||||||
|
importlib_metadata==8.7.1
|
||||||
|
ipython==8.38.0
|
||||||
|
ipywidgets==8.1.8
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
jedi==0.19.2
|
||||||
|
Jinja2==3.1.6
|
||||||
|
joblib==1.5.3
|
||||||
|
jsonschema==4.26.0
|
||||||
|
jsonschema-specifications==2025.9.1
|
||||||
|
jupyter_core==5.9.1
|
||||||
|
jupyterlab_widgets==3.0.16
|
||||||
|
kiwisolver==1.4.9
|
||||||
|
lxml==6.1.0
|
||||||
|
MarkupSafe==3.0.3
|
||||||
|
matplotlib==3.10.8
|
||||||
|
matplotlib-inline==0.2.1
|
||||||
|
narwhals==2.16.0
|
||||||
|
nbformat==5.10.4
|
||||||
|
nest-asyncio==1.6.0
|
||||||
|
numpy==2.2.6
|
||||||
|
open3d==0.19.0
|
||||||
|
packaging==26.0
|
||||||
|
parso==0.8.6
|
||||||
|
pillow==12.2.0
|
||||||
|
platformdirs==4.9.2
|
||||||
|
plotly==6.5.2
|
||||||
|
prompt_toolkit==3.0.52
|
||||||
|
pure_eval==0.2.3
|
||||||
|
Pygments==2.20.0
|
||||||
|
pyparsing==3.3.2
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
pywin32==311
|
||||||
|
referencing==0.37.0
|
||||||
|
requests==2.33.0
|
||||||
|
retrying==1.4.2
|
||||||
|
rpds-py==0.30.0
|
||||||
|
scikit-learn==1.7.2
|
||||||
|
scipy==1.15.3
|
||||||
|
shapely==2.1.2
|
||||||
|
six==1.17.0
|
||||||
|
stack-data==0.6.3
|
||||||
|
tenacity==9.1.4
|
||||||
|
threadpoolctl==3.6.0
|
||||||
|
traitlets==5.14.3
|
||||||
|
triangle==20250106
|
||||||
|
typing_extensions==4.15.0
|
||||||
|
urllib3==2.7.0
|
||||||
|
wcwidth==0.6.0
|
||||||
|
Werkzeug==3.1.6
|
||||||
|
widgetsnbextension==4.0.15
|
||||||
|
zipp==3.23.0
|
||||||
BIN
scripts/IfcConvert
Executable file
BIN
scripts/IfcConvert
Executable file
Binary file not shown.
36
scripts/backup/backup_daily.sh
Executable file
36
scripts/backup/backup_daily.sh
Executable file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# == func log ===================================================================================================================
|
||||||
|
warning() {
|
||||||
|
echo "Warning: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo "Error: $1" && exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
REPONAME="3drepository"
|
||||||
|
BPATH="/var/www/data/backups/daily"
|
||||||
|
BDPATH="/var/www/data/backups/daily/db"
|
||||||
|
WEBSITE="www.dfg-repository.wisski.cloud"
|
||||||
|
|
||||||
|
# == init script ================================================================================================================
|
||||||
|
CURRENT_DATE=$(date +%d%m%y%H%M)
|
||||||
|
TGZ_FILE="${BPATH}/backup_${WEBSITE}_${CURRENT_DATE}.tgz"
|
||||||
|
SQL_FILE="${BDPATH}/${WEBSITE}_databases_${CURRENT_DATE}.sql"
|
||||||
|
BSOURCE="/var/www/html/${REPONAME}"
|
||||||
|
|
||||||
|
mkdir -p "$BPATH"
|
||||||
|
|
||||||
|
# == main =======================================================================================================================
|
||||||
|
mkdir -p "${BDPATH}"
|
||||||
|
|
||||||
|
source read_settings.sh
|
||||||
|
#mysqldump -h ${DATABASE_HOST} -u ${DATABASE_USER} -p${DATABASE_PASS} ${DATABASE} --hex-blob --skip-lock-tables --single-transaction > "$SQL_FILE"
|
||||||
|
|
||||||
|
tar --exclude="${BPATH:1}" --warning=none -czpf "${TGZ_FILE}" -C / "$BSOURCE" var/www/data
|
||||||
|
|
||||||
|
#scp "$TGZ_FILE" "${USER}@example.work:/mnt/home/${USER}"
|
||||||
|
|
||||||
|
find ${BPATH}/*.tgz -mtime +7 -exec rm {} \;
|
||||||
|
find ${BDPATH}/*.tgz -mtime +7 -exec rm {} \;
|
||||||
63
scripts/backup/backup_monthly.sh
Executable file
63
scripts/backup/backup_monthly.sh
Executable file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "Error: $1\nExiting..." && exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
warning() {
|
||||||
|
echo "Warning: $1 Skipping..."
|
||||||
|
}
|
||||||
|
|
||||||
|
MONTHLY_BACKUPS_PATH="/var/www/data/backups/monthly"
|
||||||
|
WEEKLY_BACKUPS_PATH="/var/www/data/backups/weekly"
|
||||||
|
|
||||||
|
clean_old_monthly_backups() {
|
||||||
|
find ${MONTHLY_BACKUPS_PATH}/*.tgz -mtime +181 -exec rm {} \;
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_old_monthly_db_backups() {
|
||||||
|
find ${MONTHLY_BACKUPS_PATH}/db/*.tgz -mtime +181 -exec rm {} \;
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_from_weekly() {
|
||||||
|
# shellcheck disable=SC2010
|
||||||
|
ffd="$(ls -pt | grep -v / | head -1)"
|
||||||
|
if [[ -n "$ffd" ]]; then
|
||||||
|
rsync -dtz --ignore-existing "${ffd}" "$1"
|
||||||
|
else
|
||||||
|
warning "No files found to sync!"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
process_with_dbs() {
|
||||||
|
if [[ -d "${MONTHLY_BACKUPS_PATH}/" ]]; then
|
||||||
|
clean_old_monthly_backups
|
||||||
|
|
||||||
|
if ! cd "${WEEKLY_BACKUPS_PATH}/"; then
|
||||||
|
warning "Failed to cd into ${WEEKLY_BACKUPS_PATH}/"
|
||||||
|
else
|
||||||
|
# clean old weekly backups
|
||||||
|
find ${WEEKLY_BACKUPS_PATH}/*.tgz -mtime +28 -exec rm {} \;
|
||||||
|
|
||||||
|
sync_from_weekly "${MONTHLY_BACKUPS_PATH}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${MONTHLY_BACKUPS_PATH}/db/" ]]; then
|
||||||
|
clean_old_monthly_db_backups
|
||||||
|
|
||||||
|
if ! cd "${WEEKLY_BACKUPS_PATH}/db/"; then
|
||||||
|
warning "Failed to cd into ${WEEKLY_BACKUPS_PATH}/db/"
|
||||||
|
else
|
||||||
|
# clean old weekly db backups
|
||||||
|
find ${WEEKLY_BACKUPS_PATH}/db/*.tgz -mtime +28 -exec rm {} \;
|
||||||
|
|
||||||
|
sync_from_weekly "${MONTHLY_BACKUPS_PATH}/db/"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warning "Cannot locate ${MONTHLY_BACKUPS_PATH}/db/"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warning "Cannot locate ${MONTHLY_BACKUPS_PATH}/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
64
scripts/backup/backup_weekly.sh
Executable file
64
scripts/backup/backup_weekly.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# == func log ===================================================================================================================
|
||||||
|
warning() {
|
||||||
|
echo "Warning: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo "Error: $1" && exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
BDPATH="/var/www/data/backups/daily/db"
|
||||||
|
WEBSITE="www.dfg-repository.wisski.cloud"
|
||||||
|
|
||||||
|
DAILY_BACKUPS_PATH="/var/www/data/backups/daily"
|
||||||
|
WEEKLY_BACKUPS_PATH="/var/www/data/backups/weekly"
|
||||||
|
|
||||||
|
sync_from_daily() {
|
||||||
|
ffd="$(ls -pt | grep -v / | head -1)"
|
||||||
|
if [[ -n "$ffd" ]]; then
|
||||||
|
rsync -dtz --ignore-existing "${ffd}" "$1"
|
||||||
|
else
|
||||||
|
warning "No files found to sync!"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
process_with_dbs() {
|
||||||
|
path="${DAILY_BACKUPS_PATH}"
|
||||||
|
|
||||||
|
if [[ -d "${WEEKLY_BACKUPS_PATH}/" ]]; then
|
||||||
|
# clean old weekly backups
|
||||||
|
find ${WEEKLY_BACKUPS_PATH}/*.tgz -mtime +28 -exec rm {} \;
|
||||||
|
|
||||||
|
if ! cd "${path}"; then
|
||||||
|
warning "Failed to cd into ${path}"
|
||||||
|
else
|
||||||
|
# clean old daily backups
|
||||||
|
find "${path}"/*.tgz -mtime +6 -exec rm {} \;
|
||||||
|
|
||||||
|
sync_from_daily "${WEEKLY_BACKUPS_PATH}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
path+="/db"
|
||||||
|
if [[ -d "${WEEKLY_BACKUPS_PATH}/db/" ]]; then
|
||||||
|
# clean old weekly db backups
|
||||||
|
find ${WEEKLY_BACKUPS_PATH}/db/*.tgz -mtime +28 -exec rm {} \;
|
||||||
|
|
||||||
|
if ! cd "${path}"; then
|
||||||
|
warning "Failed to cd into ${path}"
|
||||||
|
else
|
||||||
|
# clean old daily db backups
|
||||||
|
find "${path}"/*.tgz -mtime +6 -exec rm {} \;
|
||||||
|
|
||||||
|
sync_from_daily "${WEEKLY_BACKUPS_PATH}/db/"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warning "Cannot locate ${WEEKLY_BACKUPS_PATH}/db/"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warning "Cannot locate ${WEEKLY_BACKUPS_PATH}/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
process_with_dbs
|
||||||
10
scripts/backup/read_settings.sh
Executable file
10
scripts/backup/read_settings.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
FILENAME="/var/www/html/3drepository/sites/default/settings.php"
|
||||||
|
|
||||||
|
test -f ../.env && source "$_"
|
||||||
|
|
||||||
|
DATABASE=$(php -r "error_reporting(0); \$filename = '$FILENAME'; error_reporting(0); (require(\$filename)); echo \$databases[\"default\"][\"default\"][\"database\"];")
|
||||||
|
DATABASE_HOST=$(php -r "error_reporting(0); \$filename = '$FILENAME'; error_reporting(0); (require(\$filename)); echo \$databases[\"default\"][\"default\"][\"host\"];")
|
||||||
|
DATABASE_PASS=$(php -r "error_reporting(0); \$filename = '$FILENAME'; error_reporting(0); (require(\$filename)); echo \$databases[\"default\"][\"default\"][\"password\"];")
|
||||||
|
DATABASE_USER=$(php -r "error_reporting(0); \$filename = '$FILENAME'; error_reporting(0); (require(\$filename)); echo \$databases[\"default\"][\"default\"][\"username\"];")
|
||||||
9
scripts/convert-blender-to-gltf.py
Normal file
9
scripts/convert-blender-to-gltf.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
print("Converting: '" + sys.argv[6] + "'")
|
||||||
|
print("Saving to : '" + sys.argv[7] + "'")
|
||||||
|
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=sys.argv[6])
|
||||||
|
bpy.ops.export_scene.gltf(filepath=sys.argv[7], export_format="GLB")
|
||||||
414
scripts/convert.sh
Executable file
414
scripts/convert.sh
Executable file
|
|
@ -0,0 +1,414 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Ubuntu way
|
||||||
|
#apt install xvfb
|
||||||
|
#apt install libxkbcommon0
|
||||||
|
#apt install blender python3-pip
|
||||||
|
#apt install python3-lxml python3-shapely python3-matplotlib (for CityGML converter)
|
||||||
|
#apt install libxi6 libgconf-2-4
|
||||||
|
#OR
|
||||||
|
# Debian way
|
||||||
|
#wget https://ftp.halifax.rwth-aachen.de/blender/release/Blender4.4/blender-4.4.3-linux-x64.tar.xz
|
||||||
|
#tar -xvf blender-4.4.3-linux-x64.tar.xz
|
||||||
|
#change .env BLENDER_PATH or make symlink to it `ln -s PATH_TO_YOUR_UNCOMPRESSED_BLENDER/blender-4.4.3-linux64/blender /usr/local/bin/blender`
|
||||||
|
#pip install numpy or apt install python3-numpy
|
||||||
|
#pip install triangle
|
||||||
|
#usage: ./convert.sh -c COMPRESS -cl COMPRESSION_LEVEL -i 'INPUT' -o 'OUTPUT' -b BINARY -f FORCE_OVERRIDE
|
||||||
|
|
||||||
|
|
||||||
|
#apt install -y libxi6 libxrender1 libxrandr2 libxinerama1 libxcursor1 libxcomposite1 libxdamage1 libxtst6 libglib2.0-0 libsm6 libice6 libgl1 libxkbcommon0
|
||||||
|
#TESTING:
|
||||||
|
# sudo blender -b -P ./scripts/2gltf2/2gltf2.py -- --input "/opt/drupal/web/sites/default/files/{NAME}" --ext "$GLTF" --compression "true" --compression_level "3" --output "/opt/drupal/web/sites/default/files/2025-05/test-TEST.glb"
|
||||||
|
# xvfb-run --auto-servernum --server-args="-screen 0 512x512x16" sudo blender -b -P ./scripts/render.py -- --input "/var/www/html/sites/default/files/{NAME}.glb" --ext "glb" --org_ext "glb" --output "/var/www/html/sites/default/files/views/" --is_archive false --resolution 512x512x16 --samples 20 -E BLENDER_EEVEE -f 1
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
MODULE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
if [[ -f "$SCRIPT_DIR/.env" ]]; then
|
||||||
|
source "$SCRIPT_DIR/.env"
|
||||||
|
fi
|
||||||
|
BLENDER_BIN="${BLENDER_BIN:-}"
|
||||||
|
if [[ -z "$BLENDER_BIN" ]]; then
|
||||||
|
BLENDER_BIN="blender"
|
||||||
|
fi
|
||||||
|
SPATH="${SPATH:-$MODULE_ROOT}"
|
||||||
|
if [[ ! -d "$SPATH/scripts" ]]; then
|
||||||
|
SPATH="$MODULE_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#Defaults:
|
||||||
|
COMPRESSION=false
|
||||||
|
COMPRESSION_LEVEL=3
|
||||||
|
GLTF="gltf"
|
||||||
|
FORCE=false
|
||||||
|
isOutput=false
|
||||||
|
IS_ARCHIVE=false
|
||||||
|
LIGHTWEIGHT=false
|
||||||
|
INPUT=""
|
||||||
|
OUTPUT=""
|
||||||
|
OUTPUTPATH=""
|
||||||
|
|
||||||
|
resolve_blender_bin () {
|
||||||
|
local candidate="$1"
|
||||||
|
|
||||||
|
if [[ -z "$candidate" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$candidate" == /* ]]; then
|
||||||
|
[[ -f "$candidate" && -x "$candidate" ]] || return 1
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$SCRIPT_DIR/$candidate" && -x "$SCRIPT_DIR/$candidate" ]]; then
|
||||||
|
printf '%s\n' "$SCRIPT_DIR/$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$SPATH/$candidate" && -x "$SPATH/$candidate" ]]; then
|
||||||
|
printf '%s\n' "$SPATH/$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v "$candidate" &> /dev/null; then
|
||||||
|
command -v "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_blender () {
|
||||||
|
if ! BLENDER_BIN="$(resolve_blender_bin "$BLENDER_BIN")"; then
|
||||||
|
echo "Blender doesn't exist, install it by 'apt install blender python3-pip' then 'pip install numpy' or set BLENDER_BIN in scripts/.env"
|
||||||
|
return 1
|
||||||
|
elif [[ ! -x "$BLENDER_BIN" ]]; then
|
||||||
|
echo "Configured BLENDER_BIN is not executable: $BLENDER_BIN"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${BLENDER_BIN:-}" ]]; then
|
||||||
|
echo "Blender exists and be used for next steps..."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_scripts () {
|
||||||
|
if [ ! -d "${SPATH}/scripts" ]; then
|
||||||
|
echo "Can't find dependencies directory. Resolved SPATH=${SPATH}. SCRIPT_DIR=${SCRIPT_DIR}. MODULE_ROOT=${MODULE_ROOT}. Did you change your SPATH value in scripts/.env?"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_blender
|
||||||
|
|
||||||
|
check_scripts
|
||||||
|
|
||||||
|
show_usage () {
|
||||||
|
echo "-c=compress -l=compression level -i=input path -o=output path -b=binary -f=force override existing file"
|
||||||
|
cat <<EOF
|
||||||
|
Usage: convert.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-c, --compression true|false
|
||||||
|
-l, --compression-level 0-9
|
||||||
|
-i, --input FILE
|
||||||
|
-o, --output FILE
|
||||||
|
-b, --binary true|false (true = glb, false = gltf)
|
||||||
|
-f, --force true|false
|
||||||
|
-t, --lightweight true|false
|
||||||
|
-a, --archive true|false
|
||||||
|
-h, --help
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Helpers
|
||||||
|
######################################
|
||||||
|
die() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
bool() {
|
||||||
|
[[ "$1" == "true" || "$1" == "false" ]] || die "Value must be true/false (is: $1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
file_exists() {
|
||||||
|
[[ -f "$1" ]] || die "File not found: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Parsing
|
||||||
|
######################################
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-c|--compression)
|
||||||
|
bool "$2"
|
||||||
|
COMPRESSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-l|--compression-level)
|
||||||
|
[[ "$2" =~ ^[0-9]$ ]] || die "compression-level must be 0–9"
|
||||||
|
COMPRESSION_LEVEL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-i|--input)
|
||||||
|
INPUT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-o|--output)
|
||||||
|
OUTPUT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-b|--binary)
|
||||||
|
bool "$2"
|
||||||
|
[[ "$2" == "true" ]] && GLTF="glb" || GLTF="gltf"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-f|--force)
|
||||||
|
bool "$2"
|
||||||
|
FORCE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-t|--lightweight)
|
||||||
|
bool "$2"
|
||||||
|
LIGHTWEIGHT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-a|--archive)
|
||||||
|
bool "$2"
|
||||||
|
IS_ARCHIVE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Error parsing arguments"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
check_status () {
|
||||||
|
if [ ! -f "$1.off" ]; then
|
||||||
|
touch "$1.off"
|
||||||
|
else
|
||||||
|
rm -rf "$1.off"
|
||||||
|
fi;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_main_lock() {
|
||||||
|
flock -u 200 2>/dev/null || true
|
||||||
|
exec 200>&- 2>/dev/null || true
|
||||||
|
if [[ -n "${MAIN_LOCKFILE:-}" ]]; then
|
||||||
|
rm -f "$MAIN_LOCKFILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dirs () {
|
||||||
|
mkdir -p "$INPATH"/gltf/
|
||||||
|
mkdir -p "$INPATH"/metadata/
|
||||||
|
}
|
||||||
|
|
||||||
|
create_flock () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
MAIN_LOCKFILE="$INPATH/${FILENAME}.lock"
|
||||||
|
exec 200>"$MAIN_LOCKFILE" || exit 1
|
||||||
|
flock -n 200 || {
|
||||||
|
echo "Process already running for $FILENAME"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap cleanup_main_lock EXIT
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_file () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
NAME=$3
|
||||||
|
EXT=$4
|
||||||
|
OUTPUT=$5
|
||||||
|
OUTPUTPATH=$6
|
||||||
|
|
||||||
|
create_flock "$INPATH" "$FILENAME"
|
||||||
|
|
||||||
|
if [[ "$isOutput" = false ]]; then
|
||||||
|
"$BLENDER_BIN" -b -P "${SPATH}/scripts/2gltf2/2gltf2.py" -- --input "$INPATH/$FILENAME" --ext "$GLTF" --compression "$COMPRESSION" --compression_level "$COMPRESSION_LEVEL" #> /dev/null 2>&1
|
||||||
|
else
|
||||||
|
"$BLENDER_BIN" -b -P "${SPATH}/scripts/2gltf2/2gltf2.py" -- --input "$INPATH/$FILENAME" --ext "$GLTF" --compression "$COMPRESSION" --compression_level "$COMPRESSION_LEVEL" --output "$OUTPUT$OUTPUTPATH" #> /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_unsupported_file () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
NAME=$3
|
||||||
|
EXT=$4
|
||||||
|
OUTPUT=$5
|
||||||
|
OUTPUTPATH=$6
|
||||||
|
|
||||||
|
#touch "${INPATH}/gltf/${NAME}.glb.off"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_ifc_file () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
NAME=$3
|
||||||
|
EXT=$4
|
||||||
|
OUTPUT=$5
|
||||||
|
OUTPUTPATH=$6
|
||||||
|
|
||||||
|
create_flock "$INPATH" "$FILENAME"
|
||||||
|
|
||||||
|
create_dirs
|
||||||
|
|
||||||
|
${SPATH}/scripts/IfcConvert "$INPATH/$FILENAME" "$INPATH/gltf/$NAME.glb" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_blend_file () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
NAME=$3
|
||||||
|
EXT=$4
|
||||||
|
|
||||||
|
create_flock "$INPATH" "$FILENAME"
|
||||||
|
|
||||||
|
create_dirs
|
||||||
|
|
||||||
|
"$BLENDER_BIN" -b -P "${SPATH}/scripts/convert-blender-to-gltf.py" -- "$INPATH/$FILENAME" "$INPATH/gltf/$NAME.glb" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_gml_file () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
NAME=$3
|
||||||
|
EXT=$4
|
||||||
|
OUTPUT=$5
|
||||||
|
OUTPUTPATH=$6
|
||||||
|
|
||||||
|
create_flock "$INPATH" "$FILENAME"
|
||||||
|
|
||||||
|
require_cmd python3
|
||||||
|
GLB_PATH="${INPATH}/${NAME}_GLB"
|
||||||
|
|
||||||
|
mkdir -p "$GLB_PATH"
|
||||||
|
cp -rf "$INPATH/$FILENAME" "$GLB_PATH/"
|
||||||
|
python3 "${SPATH}/scripts/CityGML2OBJv2/CityGML2OBJs.py" -i "$GLB_PATH" -o "$GLB_PATH" > /dev/null 2>&1
|
||||||
|
create_dirs
|
||||||
|
"$BLENDER_BIN" -b -P "${SPATH}/scripts/2gltf2/2gltf2.py" -- "$GLB_PATH/${NAME}.obj" "$GLTF" "$COMPRESSION" "$COMPRESSION_LEVEL" "$INPATH/gltf/$NAME.glb" > /dev/null 2>&1
|
||||||
|
rm -rf "$GLB_PATH"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
echo "======== Parameters ========"
|
||||||
|
echo " INPUT: $INPUT"
|
||||||
|
echo " OUTPUT: $OUTPUT"
|
||||||
|
echo " COMPRESSION: $COMPRESSION"
|
||||||
|
echo " LEVEL: $COMPRESSION_LEVEL"
|
||||||
|
echo " FORMAT: $GLTF"
|
||||||
|
echo " FORCE: $FORCE"
|
||||||
|
echo " LIGHTWEIGHT: $LIGHTWEIGHT"
|
||||||
|
echo " ARCHIVE: $IS_ARCHIVE"
|
||||||
|
echo "==========================="
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
if [[ ! -z "$INPUT" && -f $INPUT ]]; then
|
||||||
|
FILENAME=${INPUT##*/}
|
||||||
|
NAME="${FILENAME%.*}"
|
||||||
|
EXT=${FILENAME//*.}
|
||||||
|
INPATH=${INPUT%/*}
|
||||||
|
|
||||||
|
if [[ $FILENAME = $INPATH ]]; then
|
||||||
|
INPATH="."
|
||||||
|
fi
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
OUTPUT=`echo ${INPATH}/gltf`
|
||||||
|
else
|
||||||
|
#echo $OUTPUT
|
||||||
|
OUTFILENAME=${OUTPUT%/*} # trim everything past the last /
|
||||||
|
OUTFILENAME=${OUTFILENAME##*/}
|
||||||
|
OUTFILENAME=$(printf '%s' "$OUTFILENAME" | sed -E 's/_(ZIP|RAR|TAR|XZ|GZ)$//I')
|
||||||
|
OUTPUTPATH=`echo $OUTFILENAME.$GLTF`
|
||||||
|
OUTPUT=`echo ${OUTPUT}gltf/`
|
||||||
|
isOutput=true
|
||||||
|
fi
|
||||||
|
if [[ "$EXT" != "$FILENAME" ]]; then
|
||||||
|
EXT="${EXT,,}"
|
||||||
|
if [[ ! -d "$OUTPUT" ]]; then
|
||||||
|
mkdir "$OUTPUT"
|
||||||
|
fi
|
||||||
|
if [[ ! -f $OUTPUT/$NAME.$GLTF || $FORCE ]]; then
|
||||||
|
start=`date +%s`
|
||||||
|
case $EXT in
|
||||||
|
abc|dae|fbx|obj|ply|stl|wrl|x3d)
|
||||||
|
echo "Converting $EXT file..."
|
||||||
|
handle_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||||
|
end=`date +%s`
|
||||||
|
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
ifc)
|
||||||
|
echo "Converting $EXT file..."
|
||||||
|
handle_ifc_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||||
|
end=`date +%s`
|
||||||
|
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
blend)
|
||||||
|
echo "Converting $EXT file..."
|
||||||
|
handle_blend_file "$INPATH" "$FILENAME" "$NAME" $EXT
|
||||||
|
end=`date +%s`
|
||||||
|
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
gml)
|
||||||
|
echo "Converting $EXT file..."
|
||||||
|
handle_gml_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||||
|
end=`date +%s`
|
||||||
|
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
glb)
|
||||||
|
end=`date +%s`
|
||||||
|
echo "Given file was already compressed."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
handle_unsupported_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||||
|
#echo "File extension $EXT is not supported for conversion yet."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "Compressed file $OUTPUT/$NAME.$GLTF already exists."
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No extension found on $FILENAME";
|
||||||
|
exit 2;
|
||||||
|
fi
|
||||||
|
elif [[ -z "$INPUT" ]] && die "No --input argument provided"; then
|
||||||
|
file_exists "$INPUT"
|
||||||
|
elif [[ -n "$OUTPUT" && -f "$OUTPUT" && "$FORCE" != "true" ]]; then
|
||||||
|
die "Output file already exists (use --force true)"
|
||||||
|
else
|
||||||
|
echo "Given file '$INPUT' not found"
|
||||||
|
show_usage
|
||||||
|
fi
|
||||||
1
scripts/convert_files.txt
Normal file
1
scripts/convert_files.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/opt/drupal/web/sites/default/files/2024-12/MAINZ_3D_Impressao_3D.glb
|
||||||
76
scripts/convert_multiple.sh
Normal file
76
scripts/convert_multiple.sh
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Config
|
||||||
|
######################################
|
||||||
|
LOG_FILE="convert_$(date +%Y-%m-%d_%H-%M-%S).log"
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
FILES_LIST="$SCRIPT_DIR/convert_files.txt"
|
||||||
|
|
||||||
|
[[ -f $FILES_LIST ]] || { echo "Missing file: $FILES_LIST"; exit 1; }
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
FILES+=("$line")
|
||||||
|
done < "$FILES_LIST"
|
||||||
|
|
||||||
|
[[ ${#FILES[@]} -eq 0 ]] && {
|
||||||
|
echo "No files to process."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Logging helper
|
||||||
|
######################################
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%F %T')] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Start
|
||||||
|
######################################
|
||||||
|
echo "Conversion started: $(date)" | tee -a "$LOG_FILE"
|
||||||
|
echo "Log: $LOG_FILE" | tee -a "$LOG_FILE"
|
||||||
|
echo | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
FAILED=()
|
||||||
|
SUCCESS=()
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
START=$(date +%s)
|
||||||
|
log "Start: $file" | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
if "$SCRIPT_DIR/convert.sh" \
|
||||||
|
--input "$file" \
|
||||||
|
--compression true \
|
||||||
|
--compression-level 3 \
|
||||||
|
--binary true \
|
||||||
|
--force false \
|
||||||
|
2>&1 | tee -a "$LOG_FILE"
|
||||||
|
then
|
||||||
|
log "OK: $file" | tee -a "$LOG_FILE"
|
||||||
|
SUCCESS+=("$file")
|
||||||
|
else
|
||||||
|
log "FAIL: $file" | tee -a "$LOG_FILE"
|
||||||
|
FAILED+=("$file")
|
||||||
|
fi
|
||||||
|
END=$(date +%s)
|
||||||
|
log "$((END-START))s"
|
||||||
|
|
||||||
|
echo | tee -a "$LOG_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Summary
|
||||||
|
######################################
|
||||||
|
echo "================ SUMMARY ================" | tee -a "$LOG_FILE"
|
||||||
|
log "Success: ${#SUCCESS[@]}" | tee -a "$LOG_FILE"
|
||||||
|
log "Errors: ${#FAILED[@]}" | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
if (( ${#FAILED[@]} )); then
|
||||||
|
echo "Failed files:" | tee -a "$LOG_FILE"
|
||||||
|
printf ' - %s\n' "${FAILED[@]}" | tee -a "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "End: $(date)" | tee -a "$LOG_FILE"
|
||||||
157
scripts/convert_test.sh
Executable file
157
scripts/convert_test.sh
Executable file
|
|
@ -0,0 +1,157 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#apt install blender python3-pip
|
||||||
|
#pip install numpy
|
||||||
|
#usage: ./convert.sh -c COMPRESS -cl COMPRESSION_LEVEL -i INPUT -o OUTPUT -b BINARY -f FORCE_OVERRIDE
|
||||||
|
|
||||||
|
BLENDER_PATH=''
|
||||||
|
#BLENDER_PATH='/var/lib/snapd/snap/blender/current/'
|
||||||
|
|
||||||
|
#Defaults:
|
||||||
|
COMPRESSION=false
|
||||||
|
COMPRESSION_LEVEL=3
|
||||||
|
GLTF="gltf"
|
||||||
|
FORCE="false"
|
||||||
|
isOutput=false
|
||||||
|
IS_ARCHIVE=false
|
||||||
|
|
||||||
|
while getopts ":c:l:o:i:b:f:" flag; do
|
||||||
|
case "${flag}" in
|
||||||
|
c) COMPRESSION=${OPTARG};;
|
||||||
|
l) COMPRESSION_LEVEL=${OPTARG};;
|
||||||
|
i) INPUT="${OPTARG}";;
|
||||||
|
o) OUTPUT="${OPTARG}";;
|
||||||
|
f) FORCE="${OPTARG}";;
|
||||||
|
a) IS_ARCHIVE="${OPTARG}";;
|
||||||
|
b) if [[ "${OPTARG}" = "true" ]]; then GLTF="glb"; else GLTF="gltf"; fi;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
render_preview () {
|
||||||
|
if [[ ! -d "$INPATH/views" ]]; then
|
||||||
|
mkdir "$INPATH/views/"
|
||||||
|
fi
|
||||||
|
if [[ "$EXT" = "glb" ]]; then
|
||||||
|
xvfb-run --auto-servernum --server-args="-screen 0 512x512x16" sudo ${BLENDER_PATH}blender -b -P /var/www/html/3drepository/modules/dfg_3dviewer/scripts/render.py -- "$INPATH/$NAME.glb" "glb" $1 "$INPATH/views/" $IS_ARCHIVE -E BLENDER_EEVEE -f 1 > /dev/null 2>&1
|
||||||
|
else
|
||||||
|
xvfb-run --auto-servernum --server-args="-screen 0 512x512x16" sudo ${BLENDER_PATH}blender -b -P /var/www/html/3drepository/modules/dfg_3dviewer/scripts/render.py -- "$INPATH/gltf/$NAME.glb" "glb" $1 "$INPATH/views/" $IS_ARCHIVE -E BLENDER_EEVEE -f 1 > /dev/null 2>&1
|
||||||
|
fi;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_file () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
NAME=$3
|
||||||
|
EXT=$4
|
||||||
|
OUTPUT=$5
|
||||||
|
OUTPUTPATH=$6
|
||||||
|
|
||||||
|
if [[ "$isOutput" = false ]]; then
|
||||||
|
${BLENDER_PATH}blender -b -P /var/www/html/3drepository/modules/dfg_3dviewer/scripts/2gltf2/2gltf2.py -- "$INPATH/$FILENAME" "$GLTF" "$COMPRESSION" "$COMPRESSION_LEVEL" > /dev/null 2>&1
|
||||||
|
else
|
||||||
|
${BLENDER_PATH}blender -b -P /var/www/html/3drepository/modules/dfg_3dviewer/scripts/2gltf2/2gltf2.py -- "$INPATH/$FILENAME" "$GLTF" "$COMPRESSION" "$COMPRESSION_LEVEL" "$OUTPUT$OUTPUTPATH" > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
#if [[ -f "$INPATH/gltf/$NAME.glb" ]]; then
|
||||||
|
# render_preview $EXT
|
||||||
|
#else
|
||||||
|
# render_preview "$INPATH/$NAME.$EXT"
|
||||||
|
#fi;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_unsupported_file () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
NAME=$3
|
||||||
|
EXT=$4
|
||||||
|
OUTPUT=$5
|
||||||
|
OUTPUTPATH=$6
|
||||||
|
|
||||||
|
touch $INPATH/gltf/$NAME.glb.off
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_ifc_file () {
|
||||||
|
INPATH=$1
|
||||||
|
FILENAME=$2
|
||||||
|
NAME=$3
|
||||||
|
EXT=$4
|
||||||
|
OUTPUT=$5
|
||||||
|
OUTPUTPATH=$6
|
||||||
|
|
||||||
|
if [[ ! -d "$INPATH"/gltf/ ]]; then
|
||||||
|
mkdir "$INPATH"/gltf/
|
||||||
|
fi
|
||||||
|
/var/www/html/3drepository/modules/dfg_3dviewer/scripts/IfcConvert "$INPATH/$FILENAME" "$INPATH/gltf/$NAME.glb" > /dev/null 2>&1
|
||||||
|
render_preview $EXT
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -z "$INPUT" && -f $INPUT ]]; then
|
||||||
|
FILENAME=${INPUT##*/}
|
||||||
|
NAME="${FILENAME%.*}"
|
||||||
|
EXT=${FILENAME//*.}
|
||||||
|
INPATH=${INPUT%/*}
|
||||||
|
|
||||||
|
if [[ $FILENAME = $INPATH ]]; then
|
||||||
|
INPATH="."
|
||||||
|
fi
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
OUTPUT=`echo $INPATH/\gltf`
|
||||||
|
else
|
||||||
|
echo $OUTPUT
|
||||||
|
OUTFILENAME=${OUTPUT%/*} # trim everything past the last /
|
||||||
|
OUTFILENAME=${OUTFILENAME##*/}
|
||||||
|
OUTFILENAME=${OUTFILENAME/"_ZIP"/""}
|
||||||
|
OUTFILENAME=${OUTFILENAME/"_RAR"/""}
|
||||||
|
OUTFILENAME=${OUTFILENAME/"_TAR"/""}
|
||||||
|
OUTFILENAME=${OUTFILENAME/"_XZ"/""}
|
||||||
|
OUTFILENAME=${OUTFILENAME/"_GZ"/""}
|
||||||
|
OUTPUTPATH=`echo $OUTFILENAME.$GLTF`
|
||||||
|
OUTPUT=`echo ${OUTPUT}gltf/`
|
||||||
|
isOutput=true
|
||||||
|
fi
|
||||||
|
if [[ "$EXT" != "$filename" ]]; then
|
||||||
|
EXT="${EXT,,}"
|
||||||
|
if [[ ! -d "$OUTPUT" ]]; then
|
||||||
|
mkdir "$OUTPUT"
|
||||||
|
fi
|
||||||
|
if [[ ! -f $OUTPUT/$NAME.$GLTF || $FORCE ]]; then
|
||||||
|
start=`date +%s`
|
||||||
|
case $EXT in
|
||||||
|
abc|blend|dae|fbx|obj|ply|stl|wrl|x3d)
|
||||||
|
handle_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||||
|
end=`date +%s`
|
||||||
|
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
ifc)
|
||||||
|
handle_ifc_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||||
|
end=`date +%s`
|
||||||
|
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
glb)
|
||||||
|
render_preview $EXT
|
||||||
|
end=`date +%s`
|
||||||
|
echo "Given file was already compressed."
|
||||||
|
exit 0;
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
handle_unsupported_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||||
|
echo "Flie extension $EXT is not supported for conversion yet."
|
||||||
|
exit 1;
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "Compressed file $OUTPUT/$NAME.$GLTF already exists."
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No extension found on $FILENAME";
|
||||||
|
exit 2;
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No file $INPUT or 0 arguments given."
|
||||||
|
echo "Usage: ./convert.sh -c true/false -cl [0-6] -i INPUT -o OUTPUT -b true/false -f true/false"
|
||||||
|
echo "-c=compress -cl=compression level -i=input path -o=output path -b=binary -f=force override existing file"
|
||||||
|
fi
|
||||||
98
scripts/pack-drupal-module.js
Normal file
98
scripts/pack-drupal-module.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const stagingDir = path.join(root, '.pack-drupal-staging', 'dfg_3dviewer');
|
||||||
|
const outFile = path.join(root, 'dfg_3dviewer-drupal.zip');
|
||||||
|
|
||||||
|
const moduleFiles = [
|
||||||
|
'dfg_3dviewer.info.yml',
|
||||||
|
'dfg_3dviewer.libraries.yml',
|
||||||
|
'dfg_3dviewer.links.menu.yml',
|
||||||
|
'dfg_3dviewer.module',
|
||||||
|
'dfg_3dviewer.permissions.yml',
|
||||||
|
'dfg_3dviewer.routing.yml',
|
||||||
|
'dfg_3dviewer.services.yml',
|
||||||
|
'dfg_3dviewer.translation.yml',
|
||||||
|
'README.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
const moduleDirs = ['src', 'config'];
|
||||||
|
|
||||||
|
const scriptExclude = new Set([
|
||||||
|
'pack-drupal-module.js',
|
||||||
|
'.env',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function copyRecursive(src, dest) {
|
||||||
|
fs.cpSync(src, dest, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function stageScripts(targetScriptsDir) {
|
||||||
|
fs.mkdirSync(targetScriptsDir, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(path.join(root, 'scripts'), { withFileTypes: true })) {
|
||||||
|
if (entry.isFile() && scriptExclude.has(entry.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const src = path.join(root, 'scripts', entry.name);
|
||||||
|
const dest = path.join(targetScriptsDir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copyRecursive(src, dest);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stageModule() {
|
||||||
|
if (fs.existsSync(path.dirname(stagingDir))) {
|
||||||
|
fs.rmSync(path.dirname(stagingDir), { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(stagingDir, { recursive: true });
|
||||||
|
|
||||||
|
for (const file of moduleFiles) {
|
||||||
|
const src = path.join(root, file);
|
||||||
|
if (!fs.existsSync(src)) {
|
||||||
|
console.warn(`[pack-drupal] skip missing file: ${file}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fs.copyFileSync(src, path.join(stagingDir, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of moduleDirs) {
|
||||||
|
copyRecursive(path.join(root, dir), path.join(stagingDir, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
stageScripts(path.join(stagingDir, 'scripts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
stageModule();
|
||||||
|
|
||||||
|
if (fs.existsSync(outFile)) {
|
||||||
|
fs.rmSync(outFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipProcess = spawnSync('zip', ['-rq', outFile, 'dfg_3dviewer'], {
|
||||||
|
cwd: path.dirname(stagingDir),
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.rmSync(path.dirname(stagingDir), { recursive: true, force: true });
|
||||||
|
|
||||||
|
if (zipProcess.error) {
|
||||||
|
if (zipProcess.error.code === 'ENOENT') {
|
||||||
|
console.error('`zip` command not found. Install it before running pack.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw zipProcess.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zipProcess.status !== 0) {
|
||||||
|
process.exit(zipProcess.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Created ${outFile} (${fs.statSync(outFile).size} bytes)`);
|
||||||
33
scripts/progress.js
Normal file
33
scripts/progress.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
const progressWrapper = document.querySelector('[data-3d-progress]');
|
||||||
|
|
||||||
|
if (!progressWrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityId = progressWrapper.dataset.entityId;
|
||||||
|
|
||||||
|
function checkProgress() {
|
||||||
|
|
||||||
|
fetch('/dfg-3dviewer/progress/' + entityId)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
const bar = document.querySelector('#progress-bar');
|
||||||
|
const label = document.querySelector('#progress-label');
|
||||||
|
|
||||||
|
if (!bar || !label) return;
|
||||||
|
|
||||||
|
bar.style.width = data.progress + '%';
|
||||||
|
label.innerText = data.progress + '%';
|
||||||
|
|
||||||
|
if (data.status === 'ready') {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(checkProgress, 3000);
|
||||||
|
|
||||||
|
})();
|
||||||
439
scripts/render.py
Executable file
439
scripts/render.py
Executable file
|
|
@ -0,0 +1,439 @@
|
||||||
|
#
|
||||||
|
# The MIT License (MIT)
|
||||||
|
#
|
||||||
|
# Copyright (c) since 2017 UX3D GmbH
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Imports
|
||||||
|
#
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
import math
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
import itertools
|
||||||
|
from math import radians
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
if '--' in sys.argv:
|
||||||
|
argv = sys.argv[sys.argv.index('--') + 1:]
|
||||||
|
parser=argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--input", help="Input file path")
|
||||||
|
parser.add_argument("--ext", help="Extenstion of imported file")
|
||||||
|
parser.add_argument("--org_ext", help="Original extenstion of imported file")
|
||||||
|
parser.add_argument("--output", help="Output file path")
|
||||||
|
parser.add_argument("--is_archive", help="Importing archive flag")
|
||||||
|
parser.add_argument("--resolution", help="Resolution preview images")
|
||||||
|
parser.add_argument("--samples", help="Samples rendering quality")
|
||||||
|
args = parser.parse_known_args(argv)[0]
|
||||||
|
|
||||||
|
def rotation_matrix(axis, theta):
|
||||||
|
"""
|
||||||
|
Return the rotation matrix associated with counterclockwise rotation about
|
||||||
|
the given axis by theta radians.
|
||||||
|
"""
|
||||||
|
axis = np.asarray(axis)
|
||||||
|
axis = axis / math.sqrt(np.dot(axis, axis))
|
||||||
|
a = math.cos(theta / 2.0)
|
||||||
|
b, c, d = -axis * math.sin(theta / 2.0)
|
||||||
|
aa, bb, cc, dd = a * a, b * b, c * c, d * d
|
||||||
|
bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
|
||||||
|
return np.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
|
||||||
|
[2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
|
||||||
|
[2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]])
|
||||||
|
|
||||||
|
def rotate(point, angle_degrees, axis=(0,1,0)):
|
||||||
|
theta_degrees = angle_degrees
|
||||||
|
theta_radians = math.radians(theta_degrees)
|
||||||
|
|
||||||
|
rotated_point = np.dot(rotation_matrix(axis, theta_radians), point)
|
||||||
|
return rotated_point
|
||||||
|
|
||||||
|
""" get_min
|
||||||
|
- (bound_box) bound_box
|
||||||
|
utilized bound_box
|
||||||
|
>>> (Vector) (x,y,z)
|
||||||
|
get_min estimates the minimal x, y, z values
|
||||||
|
"""
|
||||||
|
def get_min(bound_box):
|
||||||
|
min_x = min([bound_box[i][0] for i in range(0, 8)])
|
||||||
|
min_y = min([bound_box[i][1] for i in range(0, 8)])
|
||||||
|
min_z = min([bound_box[i][2] for i in range(0, 8)])
|
||||||
|
return Vector((min_x, min_y, min_z))
|
||||||
|
|
||||||
|
|
||||||
|
""" get_max
|
||||||
|
- (bound_box) bound_box
|
||||||
|
utilized bound_box
|
||||||
|
>>> (Vector) (x,y,z)
|
||||||
|
get_max estimates the maximal x, y, z values
|
||||||
|
"""
|
||||||
|
def get_max(bound_box):
|
||||||
|
max_x = max([bound_box[i][0] for i in range(0, 8)])
|
||||||
|
max_y = max([bound_box[i][1] for i in range(0, 8)])
|
||||||
|
max_z = max([bound_box[i][2] for i in range(0, 8)])
|
||||||
|
return Vector((max_x, max_y, max_z))
|
||||||
|
|
||||||
|
|
||||||
|
def get_origin(v1, v2):
|
||||||
|
return v1 + 0.5 * (v2 - v1)
|
||||||
|
|
||||||
|
max_model_dim = 10
|
||||||
|
|
||||||
|
def scale_scene():
|
||||||
|
pmin = Vector((float("inf"), float("inf"), float("inf")))
|
||||||
|
pmax = Vector((float("-inf"), float("-inf"), float("-inf")))
|
||||||
|
for o in bpy.data.objects:
|
||||||
|
if o.type == 'MESH':
|
||||||
|
mat = o.matrix_world
|
||||||
|
for v in o.bound_box:
|
||||||
|
v = mat @ Vector(v)
|
||||||
|
if v[0] < pmin[0]: pmin[0] = v[0]
|
||||||
|
if v[1] < pmin[1]: pmin[1] = v[1]
|
||||||
|
if v[2] < pmin[2]: pmin[2] = v[2]
|
||||||
|
if v[0] > pmax[0]: pmax[0] = v[0]
|
||||||
|
if v[1] > pmax[1]: pmax[1] = v[1]
|
||||||
|
if v[2] > pmax[2]: pmax[2] = v[2]
|
||||||
|
|
||||||
|
root = bpy.data.objects.new("scaled_root", None)
|
||||||
|
for obj in bpy.context.scene.objects:
|
||||||
|
if not obj.parent:
|
||||||
|
obj.parent = root
|
||||||
|
bpy.context.scene.collection.objects.link(root)
|
||||||
|
|
||||||
|
center = (pmin + pmax) / 2
|
||||||
|
scale = max_model_dim / (pmax-pmin).length
|
||||||
|
root.matrix_world = Matrix.Diagonal((scale,) * 3).to_4x4() @ Matrix.Translation(-center)
|
||||||
|
|
||||||
|
pmin = root.matrix_world @ pmin
|
||||||
|
pmax = root.matrix_world @ pmax
|
||||||
|
bounds = [
|
||||||
|
pmin[0], pmin[1], pmin[2], # left front bottom
|
||||||
|
pmin[0], pmin[1], pmax[2], # left front top
|
||||||
|
pmin[0], pmax[1], pmax[2], # left back top
|
||||||
|
pmin[0], pmax[1], pmin[2], # left back bottom
|
||||||
|
pmax[0], pmin[1], pmin[2], # right front bottom
|
||||||
|
pmax[0], pmin[1], pmax[2], # right front top
|
||||||
|
pmax[0], pmax[1], pmax[2], # right back top
|
||||||
|
pmax[0], pmax[1], pmin[2] # right back bottom
|
||||||
|
]
|
||||||
|
return bounds
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Globals
|
||||||
|
#
|
||||||
|
bpy.context.scene.render.resolution_percentage = 70
|
||||||
|
bpy.context.scene.render.resolution_x = 1024
|
||||||
|
bpy.context.scene.render.resolution_y = 1024
|
||||||
|
bpy.context.scene.cycles.samples = 20
|
||||||
|
|
||||||
|
if args.resolution:
|
||||||
|
resolution = args.resolution.split('x', 2)
|
||||||
|
bpy.context.scene.render.resolution_x = int(resolution[0])
|
||||||
|
bpy.context.scene.render.resolution_y = int(resolution[1])
|
||||||
|
if args.samples:
|
||||||
|
bpy.context.scene.cycles.samples = int(args.samples)
|
||||||
|
#
|
||||||
|
# Functions
|
||||||
|
#
|
||||||
|
current_directory = os.getcwd()
|
||||||
|
|
||||||
|
extension = "glb"
|
||||||
|
original_extension = "glb"
|
||||||
|
|
||||||
|
if args.ext:
|
||||||
|
extension = args.ext
|
||||||
|
if extension == "gltf":
|
||||||
|
format = "GLTF_EMBEDDED"
|
||||||
|
else:
|
||||||
|
format = "GLB"
|
||||||
|
|
||||||
|
if args.org_ext:
|
||||||
|
original_extension = args.org_ext
|
||||||
|
|
||||||
|
is_archive = args.is_archive
|
||||||
|
|
||||||
|
print("Converting: '" + original_extension + "'")
|
||||||
|
|
||||||
|
root, current_extension = os.path.splitext(args.input)
|
||||||
|
current_basename = os.path.basename(root)
|
||||||
|
|
||||||
|
if current_extension == ".abc" or current_extension == ".blend" or current_extension == ".dae" or current_extension == ".fbx" or current_extension == ".gltf" or current_extension == ".glb" or current_extension == ".obj" or current_extension == ".ply" or current_extension == ".stl" or current_extension == ".wrl" or current_extension == ".x3d":
|
||||||
|
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
|
||||||
|
if current_extension == ".abc":
|
||||||
|
bpy.ops.wm.alembic_import(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".blend":
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".dae":
|
||||||
|
bpy.ops.wm.collada_import(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".fbx":
|
||||||
|
bpy.ops.import_scene.fbx(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".obj":
|
||||||
|
object=bpy.ops.import_scene.obj(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".ply":
|
||||||
|
bpy.ops.import_mesh.ply(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".stl":
|
||||||
|
bpy.ops.import_mesh.stl(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".wrl" or current_extension == ".x3d":
|
||||||
|
bpy.ops.import_scene.x3d(filepath=args.input)
|
||||||
|
|
||||||
|
if current_extension == ".gltf" or current_extension == ".glb":
|
||||||
|
bpy.ops.import_scene.gltf(filepath=args.input)
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
context = bpy.context
|
||||||
|
render = scene.render
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# UTILS
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
def np_matmul_coords(coords, matrix):
|
||||||
|
M = matrix.transposed()
|
||||||
|
ones = np.ones((coords.shape[0], 1))
|
||||||
|
coords4d = np.hstack((coords, ones))
|
||||||
|
return np.dot(coords4d, M)[:, :-1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_scene_bounds():
|
||||||
|
coords = np.vstack(
|
||||||
|
tuple(
|
||||||
|
np_matmul_coords(np.array(o.bound_box), o.matrix_world.copy())
|
||||||
|
for o in scene.objects if o.type == 'MESH'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bfl = coords.min(axis=0)
|
||||||
|
tbr = coords.max(axis=0)
|
||||||
|
size = Vector(tbr - bfl)
|
||||||
|
center = Vector((bfl + tbr) * 0.5)
|
||||||
|
return center, size
|
||||||
|
|
||||||
|
|
||||||
|
def fit_camera_to_bounds(cam, center, size, margin=1.2):
|
||||||
|
# aspect ratio of render
|
||||||
|
render = bpy.context.scene.render
|
||||||
|
aspect = render.resolution_x / render.resolution_y
|
||||||
|
|
||||||
|
ratio = size.x / size.z
|
||||||
|
print(f"Camera fit ratio: {ratio:.2f}")
|
||||||
|
|
||||||
|
if ratio > 6.0:
|
||||||
|
cam.data.type = 'ORTHO'
|
||||||
|
|
||||||
|
# ORTHO: skala = wysokość kadru
|
||||||
|
ortho_height = size.z * 1.2
|
||||||
|
ortho_width = size.x * 1.2 / aspect
|
||||||
|
|
||||||
|
cam.data.ortho_scale = max(ortho_height, ortho_width)
|
||||||
|
|
||||||
|
# clipping – MUST HAVE
|
||||||
|
cam.data.clip_start = 0.01
|
||||||
|
cam.data.clip_end = max(size) * 10
|
||||||
|
|
||||||
|
else:
|
||||||
|
cam.data.type = 'PERSP'
|
||||||
|
cam.data.clip_start = 0.01
|
||||||
|
cam.data.clip_end = max(size) * 10
|
||||||
|
|
||||||
|
# FOV vertical and horizontal
|
||||||
|
fov_x = cam_data.angle
|
||||||
|
fov_y = 2 * math.atan(math.tan(fov_x / 2) / aspect)
|
||||||
|
|
||||||
|
# required distance to fit bounds in view
|
||||||
|
dist_x = (size.x * 0.5) / math.tan(fov_x * 0.5)
|
||||||
|
dist_z = (size.z * 0.5) / math.tan(fov_y * 0.5)
|
||||||
|
|
||||||
|
distance = max(dist_x, dist_z) * margin
|
||||||
|
|
||||||
|
cam.location = center + Vector((0, -distance, 0))
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
export_file = args.output
|
||||||
|
else:
|
||||||
|
root = root[::-1].replace(current_basename[::-1], "", 1)[::-1]
|
||||||
|
export_file = root + "_" + extension
|
||||||
|
|
||||||
|
if is_archive:
|
||||||
|
mainfilepath=export_file+current_basename
|
||||||
|
else:
|
||||||
|
mainfilepath=export_file+current_basename+"."+original_extension
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# RENDER / CYCLES
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
render.engine = 'CYCLES'
|
||||||
|
render.film_transparent = True
|
||||||
|
render.resolution_x = int(resolution[0])
|
||||||
|
render.resolution_y = int(resolution[1])
|
||||||
|
render.resolution_percentage = 100
|
||||||
|
|
||||||
|
render.image_settings.file_format = 'PNG'
|
||||||
|
render.image_settings.color_mode = 'RGBA'
|
||||||
|
render.image_settings.color_depth = '16'
|
||||||
|
render.image_settings.color_management = 'FOLLOW_SCENE'
|
||||||
|
render.image_settings.view_settings.view_transform = 'Standard'
|
||||||
|
|
||||||
|
scene.render.use_compositing = True
|
||||||
|
|
||||||
|
scene.cycles.device = 'CPU'
|
||||||
|
scene.cycles.samples = 256
|
||||||
|
scene.cycles.use_adaptive_sampling = True
|
||||||
|
scene.cycles.adaptive_threshold = 0.03
|
||||||
|
scene.cycles.adaptive_min_samples = 16
|
||||||
|
|
||||||
|
scene.cycles.use_denoising = True
|
||||||
|
scene.cycles.denoiser = 'OPENIMAGEDENOISE'
|
||||||
|
scene.cycles.denoising_input_passes = 'RGB_ALBEDO_NORMAL'
|
||||||
|
scene.cycles.denoising_prefilter = 'ACCURATE'
|
||||||
|
|
||||||
|
scene.cycles.max_bounces = 6
|
||||||
|
scene.cycles.diffuse_bounces = 3
|
||||||
|
scene.cycles.glossy_bounces = 3
|
||||||
|
scene.cycles.transparent_max_bounces = 4
|
||||||
|
scene.cycles.transmission_bounces = 4
|
||||||
|
|
||||||
|
scene.cycles.sample_clamp_indirect = 20
|
||||||
|
scene.cycles.light_sampling_threshold = 0.03
|
||||||
|
|
||||||
|
scene.view_settings.exposure = 1.0
|
||||||
|
scene.view_settings.gamma = 1.0
|
||||||
|
|
||||||
|
# CUDA OFF (no warnings)
|
||||||
|
prefs = bpy.context.preferences
|
||||||
|
prefs.addons['cycles'].preferences.compute_device_type = 'NONE'
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# VIEW LAYER PASSES (CLI SAFE)
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
view_layer = scene.view_layers["ViewLayer"]
|
||||||
|
view_layer.use_pass_normal = True
|
||||||
|
view_layer.use_pass_diffuse_color = True
|
||||||
|
view_layer.use_pass_object_index = True
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# CAMERA
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
center, size = get_scene_bounds()
|
||||||
|
cam_data = bpy.data.cameras.new("Camera")
|
||||||
|
cam_data.lens = 50 # product look
|
||||||
|
cam_data.sensor_width = 36
|
||||||
|
|
||||||
|
cam = bpy.data.objects.new("Camera", cam_data)
|
||||||
|
scene.collection.objects.link(cam)
|
||||||
|
scene.camera = cam
|
||||||
|
|
||||||
|
cam_empty = bpy.data.objects.new("CamTarget", None)
|
||||||
|
cam_empty.location = center
|
||||||
|
scene.collection.objects.link(cam_empty)
|
||||||
|
|
||||||
|
cam.parent = cam_empty
|
||||||
|
|
||||||
|
constraint = cam.constraints.new(type='TRACK_TO')
|
||||||
|
constraint.target = cam_empty
|
||||||
|
constraint.track_axis = 'TRACK_NEGATIVE_Z'
|
||||||
|
constraint.up_axis = 'UP_Y'
|
||||||
|
constraint.owner_space = 'WORLD'
|
||||||
|
constraint.target_space = 'WORLD'
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# BASE CAMERA FIT
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
fit_camera_to_bounds(cam, center, size, margin=1.45)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# LIGHT
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
max_size = max(size)
|
||||||
|
|
||||||
|
sun_data = bpy.data.lights.new('SunMain', type='SUN')
|
||||||
|
sun_data.energy = 5.0 # 4.0–6.0 sweet spot
|
||||||
|
|
||||||
|
sun = bpy.data.objects.new('SunMain', sun_data)
|
||||||
|
scene.collection.objects.link(sun)
|
||||||
|
|
||||||
|
sun.rotation_euler = (
|
||||||
|
math.radians(50),
|
||||||
|
0.0,
|
||||||
|
math.radians(30)
|
||||||
|
)
|
||||||
|
|
||||||
|
fill_data = bpy.data.lights.new('SunFill', type='SUN')
|
||||||
|
fill_data.energy = 0.5 # 10% głównego
|
||||||
|
|
||||||
|
fill = bpy.data.objects.new('SunFill', fill_data)
|
||||||
|
scene.collection.objects.link(fill)
|
||||||
|
|
||||||
|
fill.rotation_euler = (
|
||||||
|
math.radians(75),
|
||||||
|
0.0,
|
||||||
|
math.radians(-120)
|
||||||
|
)
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# RENDERS
|
||||||
|
# --------------------------------------------------
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
|
print("Starting rendering...")
|
||||||
|
def render_angle(angle_deg, suffix):
|
||||||
|
print(f"Rendering angle {angle_deg}")
|
||||||
|
cam_empty.rotation_euler = (0, 0, math.radians(angle_deg))
|
||||||
|
scene.render.filepath = f"{mainfilepath}_{suffix}.png"
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
# sides
|
||||||
|
for a in [0, 90, 180, 270]:
|
||||||
|
render_angle(a, f"side{a}")
|
||||||
|
|
||||||
|
# hero angles
|
||||||
|
for a in [45, 135, 225, 315]:
|
||||||
|
render_angle(a, f"side{a}")
|
||||||
|
|
||||||
|
# top
|
||||||
|
cam.location = center + Vector((0, 0, max(size.x, size.y) * 1.3))
|
||||||
|
cam.rotation_euler = (0, 0, 0)
|
||||||
|
scene.render.filepath = f"{mainfilepath}_top.png"
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
|
||||||
|
print(f"Rendering done (took: {t1 - t0:.3f} s)")
|
||||||
|
# --------------------------------------------------
|
||||||
194
scripts/render.sh
Executable file
194
scripts/render.sh
Executable file
|
|
@ -0,0 +1,194 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [[ -f "${SCRIPT_DIR}/.env" ]]; then
|
||||||
|
source "${SCRIPT_DIR}/.env"
|
||||||
|
fi
|
||||||
|
BLENDER_BIN="${BLENDER_BIN:-}"
|
||||||
|
if [[ -z "$BLENDER_BIN" ]]; then
|
||||||
|
BLENDER_BIN="blender"
|
||||||
|
fi
|
||||||
|
SPATH="${SPATH:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||||
|
|
||||||
|
IS_ARCHIVE=false
|
||||||
|
INPUT=""
|
||||||
|
GLB_INPUT=""
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
bool() {
|
||||||
|
[[ "$1" == "true" || "$1" == "false" ]] || die "Value must be true/false (is: $1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_blender_bin() {
|
||||||
|
local candidate="$1"
|
||||||
|
|
||||||
|
if [[ -z "$candidate" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$candidate" == /* ]]; then
|
||||||
|
[[ -f "$candidate" && -x "$candidate" ]] || return 1
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$SCRIPT_DIR/$candidate" && -x "$SCRIPT_DIR/$candidate" ]]; then
|
||||||
|
printf '%s\n' "$SCRIPT_DIR/$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$SPATH/$candidate" && -x "$SPATH/$candidate" ]]; then
|
||||||
|
printf '%s\n' "$SPATH/$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v "$candidate" &> /dev/null; then
|
||||||
|
command -v "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! BLENDER_BIN="$(resolve_blender_bin "$BLENDER_BIN")"; then
|
||||||
|
BLENDER_BIN=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_blender() {
|
||||||
|
if ! BLENDER_BIN="$(resolve_blender_bin "$BLENDER_BIN")"; then
|
||||||
|
echo "Blender doesn't exist, install it by 'apt install blender python3-pip' then 'pip install numpy' or set BLENDER_BIN in scripts/.env"
|
||||||
|
return 1
|
||||||
|
elif [[ ! -f "$BLENDER_BIN" || ! -x "$BLENDER_BIN" ]]; then
|
||||||
|
echo "Configured BLENDER_BIN is not executable: $BLENDER_BIN"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${BLENDER_BIN:-}" ]]; then
|
||||||
|
echo "Blender exists and be used for next steps..."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_xvfb_run() {
|
||||||
|
if ! command -v xvfb-run &> /dev/null; then
|
||||||
|
echo "xvfb-run doesn't exist, install it by 'apt install xvfb'"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
echo "xvfb-run exists and be used for next steps..."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_scripts() {
|
||||||
|
if [ ! -d "${SPATH}/scripts" ]; then
|
||||||
|
echo "Can't find dependencies directory. Did you change your SPATH value in scripts/.env?"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: render.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-i, --input FILE Original input model path.
|
||||||
|
-g, --glb-input FILE Optional explicit GLB path.
|
||||||
|
-a, --archive true|false
|
||||||
|
-h, --help
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-i|--input)
|
||||||
|
INPUT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-g|--glb-input)
|
||||||
|
GLB_INPUT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-a|--archive)
|
||||||
|
bool "$2"
|
||||||
|
IS_ARCHIVE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Error parsing arguments"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$INPUT" ]] || die "No --input argument provided"
|
||||||
|
[[ -f "$INPUT" ]] || die "Input file not found: $INPUT"
|
||||||
|
|
||||||
|
FILENAME=${INPUT##*/}
|
||||||
|
NAME="${FILENAME%.*}"
|
||||||
|
EXT=${FILENAME//*.}
|
||||||
|
EXT="${EXT,,}"
|
||||||
|
INPATH=${INPUT%/*}
|
||||||
|
if [[ $FILENAME = $INPATH ]]; then
|
||||||
|
INPATH="."
|
||||||
|
fi
|
||||||
|
|
||||||
|
INPUT_GLTF_PATH="$INPATH/$NAME.glb"
|
||||||
|
ORG_EXT="$EXT"
|
||||||
|
|
||||||
|
if [[ "$EXT" != "glb" ]]; then
|
||||||
|
INPUT_GLTF_PATH="$INPATH/gltf/$NAME.glb"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$GLB_INPUT" ]]; then
|
||||||
|
INPUT_GLTF_PATH="$GLB_INPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$INPUT_GLTF_PATH" ]]; then
|
||||||
|
die "Render input not found: $INPUT_GLTF_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$INPATH/views"
|
||||||
|
RENDER_LOCKFILE="$INPATH/views/${NAME}.lock"
|
||||||
|
|
||||||
|
exec 201>"$RENDER_LOCKFILE" || exit 1
|
||||||
|
flock -n 201 || {
|
||||||
|
echo "Render already running for $NAME"
|
||||||
|
exec 201>&- 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap 'flock -u 201 2>/dev/null || true; exec 201>&- 2>/dev/null || true; rm -f "$RENDER_LOCKFILE"' EXIT
|
||||||
|
|
||||||
|
check_blender || die "Blender validation failed"
|
||||||
|
check_xvfb_run || die "xvfb-run validation failed"
|
||||||
|
check_scripts || die "Dependency scripts directory not found"
|
||||||
|
|
||||||
|
if [[ -z "$RENDER_RESOLUTION" ]]; then
|
||||||
|
RENDER_RESOLUTION='1024x1024x16'
|
||||||
|
fi
|
||||||
|
if [[ -z "$RENDER_SAMPLES" ]]; then
|
||||||
|
RENDER_SAMPLES='20'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Rendering thumbnails..."
|
||||||
|
xvfb-run --auto-servernum \
|
||||||
|
--server-args="-screen 0 ${RENDER_RESOLUTION}" \
|
||||||
|
"$BLENDER_BIN" -b -P "$SPATH/scripts/render.py" -- \
|
||||||
|
--input "$INPUT_GLTF_PATH" \
|
||||||
|
--ext glb \
|
||||||
|
--org_ext "$ORG_EXT" \
|
||||||
|
--output "$INPATH/views/" \
|
||||||
|
--is_archive "$IS_ARCHIVE" \
|
||||||
|
--resolution "$RENDER_RESOLUTION" \
|
||||||
|
--samples "$RENDER_SAMPLES" \
|
||||||
|
-E BLENDER_EEVEE -f 1
|
||||||
|
echo "Blender exit code: $?"
|
||||||
415
scripts/render2.py
Normal file
415
scripts/render2.py
Normal file
|
|
@ -0,0 +1,415 @@
|
||||||
|
#
|
||||||
|
# The MIT License (MIT)
|
||||||
|
#
|
||||||
|
# Copyright (c) since 2017 UX3D GmbH
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Imports
|
||||||
|
#
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
import math
|
||||||
|
from mathutils import Matrix, Vector
|
||||||
|
import itertools
|
||||||
|
from math import radians
|
||||||
|
|
||||||
|
bpy.context.scene.render.resolution_percentage = 50
|
||||||
|
bpy.context.scene.render.resolution_x = 1280
|
||||||
|
bpy.context.scene.render.resolution_y = 960
|
||||||
|
bpy.context.scene.cycles.samples = 20
|
||||||
|
|
||||||
|
def rotation_matrix(axis, theta):
|
||||||
|
"""
|
||||||
|
Return the rotation matrix associated with counterclockwise rotation about
|
||||||
|
the given axis by theta radians.
|
||||||
|
"""
|
||||||
|
axis = np.asarray(axis)
|
||||||
|
axis = axis / math.sqrt(np.dot(axis, axis))
|
||||||
|
a = math.cos(theta / 2.0)
|
||||||
|
b, c, d = -axis * math.sin(theta / 2.0)
|
||||||
|
aa, bb, cc, dd = a * a, b * b, c * c, d * d
|
||||||
|
bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
|
||||||
|
return np.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
|
||||||
|
[2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
|
||||||
|
[2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]])
|
||||||
|
|
||||||
|
def rotate(point, angle_degrees, axis=(0,1,0)):
|
||||||
|
theta_degrees = angle_degrees
|
||||||
|
theta_radians = math.radians(theta_degrees)
|
||||||
|
|
||||||
|
rotated_point = np.dot(rotation_matrix(axis, theta_radians), point)
|
||||||
|
return rotated_point
|
||||||
|
|
||||||
|
""" get_min
|
||||||
|
- (bound_box) bound_box
|
||||||
|
utilized bound_box
|
||||||
|
>>> (Vector) (x,y,z)
|
||||||
|
get_min estimates the minimal x, y, z values
|
||||||
|
"""
|
||||||
|
def get_min(bound_box):
|
||||||
|
min_x = min([bound_box[i][0] for i in range(0, 8)])
|
||||||
|
min_y = min([bound_box[i][1] for i in range(0, 8)])
|
||||||
|
min_z = min([bound_box[i][2] for i in range(0, 8)])
|
||||||
|
return Vector((min_x, min_y, min_z))
|
||||||
|
|
||||||
|
|
||||||
|
""" get_max
|
||||||
|
- (bound_box) bound_box
|
||||||
|
utilized bound_box
|
||||||
|
>>> (Vector) (x,y,z)
|
||||||
|
get_max estimates the maximal x, y, z values
|
||||||
|
"""
|
||||||
|
def get_max(bound_box):
|
||||||
|
max_x = max([bound_box[i][0] for i in range(0, 8)])
|
||||||
|
max_y = max([bound_box[i][1] for i in range(0, 8)])
|
||||||
|
max_z = max([bound_box[i][2] for i in range(0, 8)])
|
||||||
|
return Vector((max_x, max_y, max_z))
|
||||||
|
|
||||||
|
|
||||||
|
def get_origin(v1, v2):
|
||||||
|
return v1 + 0.5 * (v2 - v1)
|
||||||
|
|
||||||
|
max_model_dim = 10
|
||||||
|
|
||||||
|
def scale_scene():
|
||||||
|
pmin = Vector((float("inf"), float("inf"), float("inf")))
|
||||||
|
pmax = Vector((float("-inf"), float("-inf"), float("-inf")))
|
||||||
|
for o in bpy.data.objects:
|
||||||
|
if o.type == 'MESH':
|
||||||
|
mat = o.matrix_world
|
||||||
|
for v in o.bound_box:
|
||||||
|
v = mat @ Vector(v)
|
||||||
|
if v[0] < pmin[0]: pmin[0] = v[0]
|
||||||
|
if v[1] < pmin[1]: pmin[1] = v[1]
|
||||||
|
if v[2] < pmin[2]: pmin[2] = v[2]
|
||||||
|
if v[0] > pmax[0]: pmax[0] = v[0]
|
||||||
|
if v[1] > pmax[1]: pmax[1] = v[1]
|
||||||
|
if v[2] > pmax[2]: pmax[2] = v[2]
|
||||||
|
|
||||||
|
root = bpy.data.objects.new("scaled_root", None)
|
||||||
|
for obj in bpy.context.scene.objects:
|
||||||
|
if not obj.parent:
|
||||||
|
obj.parent = root
|
||||||
|
bpy.context.scene.collection.objects.link(root)
|
||||||
|
|
||||||
|
center = (pmin + pmax) / 2
|
||||||
|
scale = max_model_dim / (pmax-pmin).length
|
||||||
|
root.matrix_world = Matrix.Diagonal((scale,) * 3).to_4x4() @ Matrix.Translation(-center)
|
||||||
|
|
||||||
|
pmin = root.matrix_world @ pmin
|
||||||
|
pmax = root.matrix_world @ pmax
|
||||||
|
bounds = [
|
||||||
|
pmin[0], pmin[1], pmin[2], # left front bottom
|
||||||
|
pmin[0], pmin[1], pmax[2], # left front top
|
||||||
|
pmin[0], pmax[1], pmax[2], # left back top
|
||||||
|
pmin[0], pmax[1], pmin[2], # left back bottom
|
||||||
|
pmax[0], pmin[1], pmin[2], # right front bottom
|
||||||
|
pmax[0], pmin[1], pmax[2], # right front top
|
||||||
|
pmax[0], pmax[1], pmax[2], # right back top
|
||||||
|
pmax[0], pmax[1], pmin[2] # right back bottom
|
||||||
|
]
|
||||||
|
return bounds
|
||||||
|
|
||||||
|
#
|
||||||
|
# Globals
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Functions
|
||||||
|
#
|
||||||
|
current_directory = os.getcwd()
|
||||||
|
|
||||||
|
if sys.argv[6:]:
|
||||||
|
extension = sys.argv[6]
|
||||||
|
if extension == "gltf":
|
||||||
|
format = "GLTF_EMBEDDED"
|
||||||
|
else:
|
||||||
|
format = "GLB"
|
||||||
|
|
||||||
|
force_continue = True
|
||||||
|
for current_argument in sys.argv:
|
||||||
|
|
||||||
|
if force_continue:
|
||||||
|
if current_argument == '--':
|
||||||
|
force_continue = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
|
root, current_extension = os.path.splitext(current_argument)
|
||||||
|
current_basename = os.path.basename(root)
|
||||||
|
|
||||||
|
if current_extension != ".abc" and current_extension != ".blend" and current_extension != ".dae" and current_extension != ".fbx" and current_extension != ".gltf" and current_extension != ".glb" and current_extension != ".obj" and current_extension != ".ply" and current_extension != ".stl" and current_extension != ".wrl" and current_extension != ".x3d":
|
||||||
|
continue
|
||||||
|
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
#print("Converting: '" + current_argument + "'")
|
||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
|
if current_extension == ".abc":
|
||||||
|
bpy.ops.wm.alembic_import(filepath=current_argument)
|
||||||
|
|
||||||
|
if current_extension == ".blend":
|
||||||
|
bpy.ops.wm.open_mainfile(filepath=current_argument)
|
||||||
|
|
||||||
|
if current_extension == ".dae":
|
||||||
|
bpy.ops.wm.collada_import(filepath=current_argument)
|
||||||
|
|
||||||
|
if current_extension == ".fbx":
|
||||||
|
bpy.ops.import_scene.fbx(filepath=current_argument)
|
||||||
|
|
||||||
|
if current_extension == ".obj":
|
||||||
|
object=bpy.ops.import_scene.obj(filepath=current_argument)
|
||||||
|
|
||||||
|
if current_extension == ".ply":
|
||||||
|
bpy.ops.import_mesh.ply(filepath=current_argument)
|
||||||
|
|
||||||
|
if current_extension == ".stl":
|
||||||
|
bpy.ops.import_mesh.stl(filepath=current_argument)
|
||||||
|
|
||||||
|
if current_extension == ".wrl" or current_extension == ".x3d":
|
||||||
|
bpy.ops.import_scene.x3d(filepath=current_argument)
|
||||||
|
|
||||||
|
if current_extension == ".gltf" or current_extension == ".glb":
|
||||||
|
bpy.ops.import_scene.gltf(filepath=current_argument)
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
context = bpy.context
|
||||||
|
render = bpy.context.scene.render
|
||||||
|
bounds = scale_scene()
|
||||||
|
|
||||||
|
item='MESH'
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
bpy.ops.object.select_by_type(type=item)
|
||||||
|
|
||||||
|
# multiply 3d coord list by matrix
|
||||||
|
def np_matmul_coords(coords, matrix, space=None):
|
||||||
|
M = (space @ matrix @ space.inverted()
|
||||||
|
if space else matrix).transposed()
|
||||||
|
ones = np.ones((coords.shape[0], 1))
|
||||||
|
coords4d = np.hstack((coords, ones))
|
||||||
|
|
||||||
|
return np.dot(coords4d, M)[:,:-1]
|
||||||
|
return coords4d[:,:-1]
|
||||||
|
|
||||||
|
# get the global coordinates of all object bounding box corners
|
||||||
|
coords = np.vstack(
|
||||||
|
tuple(np_matmul_coords(np.array(o.bound_box), o.matrix_world.copy())
|
||||||
|
for o in
|
||||||
|
bpy.context.scene.objects
|
||||||
|
if o.type == 'MESH'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print("#" * 72)
|
||||||
|
# bottom front left (all the mins)
|
||||||
|
bfl = coords.min(axis=0)
|
||||||
|
# top back right
|
||||||
|
tbr = coords.max(axis=0)
|
||||||
|
G = np.array((bfl, tbr)).T
|
||||||
|
# bound box coords ie the 8 combinations of bfl tbr.
|
||||||
|
bbc = [i for i in itertools.product(*G)]
|
||||||
|
print(np.array(bbc))
|
||||||
|
bb_sides = get_min(bbc) - get_max(bbc)
|
||||||
|
bb_sides = (abs(bb_sides[0]), abs(bb_sides[1]), abs(bb_sides[2]))
|
||||||
|
print("#" * 72)
|
||||||
|
print('BBOX')
|
||||||
|
print(bb_sides)
|
||||||
|
|
||||||
|
group = bpy.data.collections.new("MainGroup")
|
||||||
|
bpy.context.scene.collection.children.link(group)
|
||||||
|
#for ob in context.selected_objects: # or whichever list of objects desired
|
||||||
|
# group.objects.link(ob)
|
||||||
|
#print("Moving objects into origin (0, 0, 0)")
|
||||||
|
#group.location = (0, 0, 0)
|
||||||
|
#for obj in context.selected_objects:
|
||||||
|
# obj.location = (0, 0, 0)
|
||||||
|
render.engine = "CYCLES"
|
||||||
|
render.film_transparent = True
|
||||||
|
scene.cycles.device = "CPU"
|
||||||
|
scene.cycles.samples = 128 # default 128
|
||||||
|
scene.cycles.use_adaptive_sampling = True
|
||||||
|
scene.cycles.adaptive_threshold = 0.1
|
||||||
|
scene.cycles.adaptive_min_samples = 1
|
||||||
|
scene.cycles.use_denoising = True
|
||||||
|
scene.cycles.seed = 0 # default
|
||||||
|
scene.cycles.use_animated_seed = True
|
||||||
|
scene.cycles.min_light_bounces = 0 # default
|
||||||
|
scene.cycles.min_transparent_bounces = 0 # default
|
||||||
|
scene.cycles.light_sampling_threshold = 0.01 # default
|
||||||
|
scene.cycles.max_bounces = 5
|
||||||
|
scene.cycles.sample_clamp_direct = 0 # default
|
||||||
|
scene.cycles.sample_clamp_indirect = 10 # default
|
||||||
|
scene.cycles.blur_glossy = 1 # default
|
||||||
|
scene.cycles.caustics_reflective = False
|
||||||
|
scene.cycles.caustics_refractive = False
|
||||||
|
|
||||||
|
#render.engine = 'BLENDER_EEVEE'
|
||||||
|
#render.engine = 'CYCLES'
|
||||||
|
#render.engine = 'BLENDER_WORKBENCH'
|
||||||
|
render.image_settings.color_mode = 'RGBA'
|
||||||
|
render.image_settings.color_depth = '16'
|
||||||
|
render.image_settings.file_format = 'PNG'
|
||||||
|
render.resolution_x = 512
|
||||||
|
render.resolution_y = 512
|
||||||
|
render.resolution_percentage = 100
|
||||||
|
render.film_transparent = True
|
||||||
|
#scene.render.engine = 'CYCLES'
|
||||||
|
scene.render.use_freestyle = False
|
||||||
|
scene.use_nodes = True
|
||||||
|
scene.view_layers["View Layer"].use_pass_normal = True
|
||||||
|
scene.view_layers["View Layer"].use_pass_diffuse_color = True
|
||||||
|
scene.view_layers["View Layer"].use_pass_object_index = True
|
||||||
|
|
||||||
|
#
|
||||||
|
#print("ROOT" + root)
|
||||||
|
if sys.argv[7:]:
|
||||||
|
export_file = str(sys.argv[7])
|
||||||
|
else:
|
||||||
|
root = root[::-1].replace(current_basename[::-1], "", 1)[::-1]
|
||||||
|
export_file = root + "_" + extension
|
||||||
|
|
||||||
|
multiplier=10
|
||||||
|
|
||||||
|
levels=3
|
||||||
|
density=5
|
||||||
|
r_offset=0.2
|
||||||
|
z_offset=0.2
|
||||||
|
|
||||||
|
#target_obj = bpy.context.selected_objects[0]
|
||||||
|
#target_origin = target_obj.location
|
||||||
|
# get bounding box side lengths
|
||||||
|
#bb_sides = get_min(target_obj.bound_box) - get_max(target_obj.bound_box)
|
||||||
|
(dist_x, dist_y, dist_z) = tuple([abs(c) for c in bb_sides])
|
||||||
|
originated_dist_y = .5 * dist_y
|
||||||
|
radius = 0.5 * max(dist_x, dist_z)
|
||||||
|
max_size = max(dist_x, dist_y, dist_z)
|
||||||
|
|
||||||
|
light_data = bpy.data.lights.new('light', type='AREA')
|
||||||
|
sun = bpy.data.objects.new('light', light_data)
|
||||||
|
sun.data.energy=max_size*5000.0
|
||||||
|
sun.data.size = max_size*2
|
||||||
|
#sun.location = (3, 4, -5)
|
||||||
|
#sun.location = (dist_x*1.4, dist_y*1.4, dist_z*1.4)
|
||||||
|
sun.location = (0,0,0)
|
||||||
|
bpy.context.collection.objects.link(sun)
|
||||||
|
sun_bottom = bpy.data.objects.new('light_bottom', light_data)
|
||||||
|
sun_bottom.data.energy=max_size*5000.0
|
||||||
|
sun_bottom.data.size = max_size*2
|
||||||
|
sun_bottom.location = (-dist_x*1.4, dist_y*1.4, dist_z*1.4)
|
||||||
|
#sun_bottom.location = (0, 0, dist_z/8)
|
||||||
|
sun_bottom.rotation_euler = (2, 0.3, 0.3)
|
||||||
|
bpy.context.collection.objects.link(sun_bottom)
|
||||||
|
|
||||||
|
scene.render.image_settings.file_format='PNG'
|
||||||
|
scene.render.filepath=export_file+current_basename+'.png'
|
||||||
|
print("Rendering: " + scene.render.filepath)
|
||||||
|
cam_data = bpy.data.cameras.new('camera')
|
||||||
|
cam = bpy.data.objects.new('camera', cam_data)
|
||||||
|
cam.data.lens = 35
|
||||||
|
cam.data.sensor_width = 32
|
||||||
|
cam_constraint = cam.constraints.new(type='TRACK_TO')
|
||||||
|
cam_constraint.track_axis = 'TRACK_NEGATIVE_Z'
|
||||||
|
cam_constraint.up_axis = 'UP_Y'
|
||||||
|
cam_empty = bpy.data.objects.new("Empty", None)
|
||||||
|
#cam_empty.location = (0, 0, 0)
|
||||||
|
cam_empty.location = (0, 0, 0)
|
||||||
|
cam.parent = cam_empty
|
||||||
|
|
||||||
|
scene.collection.objects.link(cam_empty)
|
||||||
|
context.view_layer.objects.active = cam_empty
|
||||||
|
cam_constraint.target = cam_empty
|
||||||
|
|
||||||
|
print(dist_x, dist_y, dist_z)
|
||||||
|
#cam.location=Vector((dist_x, dist_y, dist_z))
|
||||||
|
bpy.context.collection.objects.link(cam)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# get the current object
|
||||||
|
for _obj in scene.objects:
|
||||||
|
if (_obj.type == 'MESH'):
|
||||||
|
current_obj = _obj
|
||||||
|
# set geometry to origin
|
||||||
|
bpy.ops.object.origin_set(type="GEOMETRY_ORIGIN")
|
||||||
|
zverts = []
|
||||||
|
# get all z coordinates of the vertices
|
||||||
|
for face in current_obj.data.polygons:
|
||||||
|
verts_in_face = face.vertices[:]
|
||||||
|
for vert in verts_in_face:
|
||||||
|
local_point = current_obj.data.vertices[vert].co
|
||||||
|
world_point = current_obj.matrix_world @ local_point
|
||||||
|
zverts.append(world_point[2])
|
||||||
|
|
||||||
|
scene.cursor.location = (0, 0, min(zverts))
|
||||||
|
bpy.ops.object.origin_set(type="ORIGIN_CURSOR")
|
||||||
|
# set the object to (0,0,0)
|
||||||
|
current_obj.location = (0,0,0)
|
||||||
|
# reset the cursor
|
||||||
|
scene.cursor.location = (0,0,0)
|
||||||
|
"""
|
||||||
|
#bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
|
||||||
|
|
||||||
|
#bpy.ops.export_scene.fbx(filepath=export_file+current_basename+'.fbx')
|
||||||
|
|
||||||
|
scene.camera=cam
|
||||||
|
cam.location = (0, dist_y*2.5, dist_z*0.9)
|
||||||
|
sun.location = cam.location
|
||||||
|
sun_bottom.location = cam.location
|
||||||
|
#cam.location=Vector((dist_x/multiplier, dist_y/multiplier, dist_z/multiplier))
|
||||||
|
|
||||||
|
#scene.render.filepath=export_file+current_basename+'_org.png'
|
||||||
|
#bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
#cam.location = rotate(cam.location, 45, axis=(0, 0, 1))
|
||||||
|
|
||||||
|
for angle in range(0, 360, 90):
|
||||||
|
sun.location = rotate(sun.location, 80, axis=(0, 0, 1))
|
||||||
|
sun_bottom.location = rotate(sun_bottom.location, 80, axis=(0, 0, 1))
|
||||||
|
scene.render.filepath=export_file+current_basename+'_side'+str(angle)+'.png'
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
cam.location = rotate(cam.location, 90, axis=(0, 0, 1))
|
||||||
|
|
||||||
|
cam.location = (0, dist_y*2.9, dist_z*1.7)
|
||||||
|
cam.location = rotate(cam.location, 45, axis=(0, 0, 1))
|
||||||
|
for angle in range(45, 360, 90):
|
||||||
|
sun.location = rotate(sun.location, 80, axis=(0, 0, 1))
|
||||||
|
sun_bottom.location = rotate(sun_bottom.location, 80, axis=(0, 0, 1))
|
||||||
|
scene.render.filepath=export_file+current_basename+'_side'+str(angle)+'.png'
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
cam.location = rotate(cam.location, 90, axis=(0, 0, 1))
|
||||||
|
|
||||||
|
|
||||||
|
#top
|
||||||
|
cam.location=Vector((0, 0, dist_z*5))
|
||||||
|
sun.location = cam.location
|
||||||
|
scene.render.filepath=export_file+current_basename+'_top.png'
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
|
||||||
|
#bottom
|
||||||
|
#cam.location=Vector((0, 0, -dist_z*5))
|
||||||
|
#sun.location = cam.location
|
||||||
|
#scene.render.filepath=export_file+current_basename+'_bottom.png'
|
||||||
|
#bpy.ops.render.render(write_still=True)
|
||||||
|
print("Rendering done")
|
||||||
98
scripts/uncompress.sh
Executable file
98
scripts/uncompress.sh
Executable file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Ubuntu way
|
||||||
|
#apt install unrar-free
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TYPE=""
|
||||||
|
INPUT=""
|
||||||
|
OUTPUT=""
|
||||||
|
NAME=""
|
||||||
|
FORCE=""
|
||||||
|
|
||||||
|
while getopts ":t:o:i:f:n:" flag; do
|
||||||
|
case "${flag}" in
|
||||||
|
t) TYPE="${OPTARG}" ;;
|
||||||
|
i) INPUT="${OPTARG}" ;;
|
||||||
|
o) OUTPUT="${OPTARG}" ;;
|
||||||
|
n) NAME="${OPTARG}" ;;
|
||||||
|
f) FORCE="${OPTARG}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -f "$SCRIPT_DIR/.env" ]]; then
|
||||||
|
source "$SCRIPT_DIR/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -n "$TYPE" ]] || die "Missing archive type (-t)"
|
||||||
|
[[ -n "$INPUT" ]] || die "Missing input archive (-i)"
|
||||||
|
[[ -n "$OUTPUT" ]] || die "Missing output directory (-o)"
|
||||||
|
[[ -f "$INPUT" ]] || die "Archive file not found: $INPUT"
|
||||||
|
|
||||||
|
TYPE="${TYPE,,}"
|
||||||
|
|
||||||
|
mkdir -p "$OUTPUT"
|
||||||
|
|
||||||
|
case "$TYPE" in
|
||||||
|
zip)
|
||||||
|
if command -v unzip >/dev/null 2>&1; then
|
||||||
|
unzip -o "$INPUT" -d "$OUTPUT"
|
||||||
|
elif command -v 7z >/dev/null 2>&1; then
|
||||||
|
7z x -y "-o$OUTPUT" "$INPUT"
|
||||||
|
else
|
||||||
|
die "ZIP extraction requires 'unzip' or '7z'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
rar)
|
||||||
|
if command -v unrar >/dev/null 2>&1; then
|
||||||
|
unrar x -o+ "$INPUT" "$OUTPUT/"
|
||||||
|
elif command -v 7z >/dev/null 2>&1; then
|
||||||
|
7z x -y "-o$OUTPUT" "$INPUT"
|
||||||
|
else
|
||||||
|
die "RAR extraction requires 'unrar' or '7z'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
xz)
|
||||||
|
require_cmd tar
|
||||||
|
tar -xJf "$INPUT" -C "$OUTPUT/"
|
||||||
|
;;
|
||||||
|
tar)
|
||||||
|
require_cmd tar
|
||||||
|
tar -xf "$INPUT" -C "$OUTPUT/"
|
||||||
|
;;
|
||||||
|
gz)
|
||||||
|
require_cmd tar
|
||||||
|
tar -xzf "$INPUT" -C "$OUTPUT/"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unsupported archive type: $TYPE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Keep legacy behavior for archive layouts where the model is placed directly
|
||||||
|
# in the extraction root and convert.sh is expected to be called from here.
|
||||||
|
for filename in "$OUTPUT"*; do
|
||||||
|
[[ -e "$filename" ]] || continue
|
||||||
|
|
||||||
|
fname=${filename##*/}
|
||||||
|
name="${fname%.*}"
|
||||||
|
ext=${filename##*.}
|
||||||
|
ext="${ext,,}"
|
||||||
|
|
||||||
|
case "$ext" in
|
||||||
|
abc|blend|dae|fbx|obj|ply|stl|wrl|x3d|ifc)
|
||||||
|
"${SPATH}/scripts/convert.sh" -c 'true' -l '3' -b 'true' -i "${OUTPUT}${name}.${ext}" -o "${OUTPUT}" -f 'true' -a 'true'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
47
scripts/worker.sh
Normal file
47
scripts/worker.sh
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LOG="/opt/drupal/dfg3dworker.log"
|
||||||
|
PID_FILE="/opt/drupal/dfg3dworker.pid"
|
||||||
|
DRUPAL_SITE_URI="${DRUPAL_SITE_URI:-https://repository.covher.eu}"
|
||||||
|
DRUSH_BIN="${DRUSH_BIN:-/opt/drupal/vendor/bin/drush}"
|
||||||
|
|
||||||
|
is_pid_running() {
|
||||||
|
local pid="$1"
|
||||||
|
if [[ -z "$pid" || ! "$pid" =~ ^[0-9]+$ ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if [ -f "$LOG" ] && [ $(stat -c%s "$LOG") -gt 50000000 ]; then
|
||||||
|
mv "$LOG" "${LOG}.old"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
EXISTING_PID="$(tr -d '[:space:]' < "$PID_FILE" || true)"
|
||||||
|
if is_pid_running "$EXISTING_PID"; then
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') Worker already running with PID $EXISTING_PID, exiting" >> "$LOG"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') Removed stale PID file" >> "$LOG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$$" > "$PID_FILE"
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') Worker started" >> "$LOG"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') Run started" >> "$LOG"
|
||||||
|
"$DRUSH_BIN" --uri="$DRUPAL_SITE_URI" queue:run dfg_3dviewer_convert --time-limit=3600 2>&1 | awk '{ print strftime("%Y-%m-%d %H:%M:%S"), $0; fflush(); }' >> "$LOG"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
87
src/Controller/DFG3dController.php
Normal file
87
src/Controller/DFG3dController.php
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Controller;
|
||||||
|
|
||||||
|
use Drupal\wisski_core\Entity\WisskiEntity;
|
||||||
|
use Drupal\Core\Entity\ContentEntityStorageInterface;
|
||||||
|
use Drupal\Core\Cache\CacheableJsonResponse;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Drupal\image\Entity\ImageStyle;
|
||||||
|
use Drupal\wisski_salz\Entity\Adapter;
|
||||||
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
|
||||||
|
use Drupal\file\Entity\File;
|
||||||
|
use Drupal\Core\File\FileSystemInterface;
|
||||||
|
|
||||||
|
|
||||||
|
class DFG3dController extends ControllerBase {
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
dfg_3dviewer_init_constants();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create($container) {
|
||||||
|
return new static();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view() {
|
||||||
|
return [
|
||||||
|
'#markup' => DFG_3DVIEWER_MAIN_URL,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editEntity(?WisskiEntity $wisski_individual = NULL) {
|
||||||
|
|
||||||
|
$js_settings = dfg_3dviewer_build_js_settings();
|
||||||
|
$pathGeneration = $js_settings['viewer']['imageGeneration'] ?? NULL;
|
||||||
|
|
||||||
|
if ($pathGeneration) {
|
||||||
|
|
||||||
|
$url = \Drupal::request()->request->get('path');
|
||||||
|
|
||||||
|
// Change URL to public://
|
||||||
|
$parsed = parse_url($url, PHP_URL_PATH);
|
||||||
|
|
||||||
|
if (str_starts_with($parsed, '/sites/default/files/')) {
|
||||||
|
|
||||||
|
$relative = str_replace('/sites/default/files/', '', $parsed);
|
||||||
|
$uri = 'public://' . $relative;
|
||||||
|
|
||||||
|
$realpath = \Drupal::service('file_system')->realpath($uri);
|
||||||
|
|
||||||
|
if (file_exists($realpath)) {
|
||||||
|
|
||||||
|
// Check wheter file exists or not
|
||||||
|
$files = \Drupal::entityTypeManager()
|
||||||
|
->getStorage('file')
|
||||||
|
->loadByProperties(['uri' => $uri]);
|
||||||
|
|
||||||
|
if ($files) {
|
||||||
|
$file = reset($files);
|
||||||
|
} else {
|
||||||
|
$file = File::create(['uri' => $uri]);
|
||||||
|
$file->setPermanent();
|
||||||
|
$file->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$wisski_individual->set($pathGeneration, [
|
||||||
|
'target_id' => $file->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$wisski_individual->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new CacheableJsonResponse();
|
||||||
|
$response->setEncodingOptions(JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
|
$response->setData([
|
||||||
|
"id" => $wisski_individual->id(),
|
||||||
|
"path" => $url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/Controller/ModelController.php
Normal file
99
src/Controller/ModelController.php
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Controller;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Drupal\node\Entity\Node;
|
||||||
|
use Drupal\Core\Entity\EntityInterface;
|
||||||
|
|
||||||
|
class ModelController {
|
||||||
|
|
||||||
|
public function create(Request $request) {
|
||||||
|
|
||||||
|
$node = Node::create([
|
||||||
|
'type' => 'model',
|
||||||
|
'title' => 'Processing model',
|
||||||
|
'field_processing_progress' => 0,
|
||||||
|
'field_processing_status' => 'preparing',
|
||||||
|
'field_processing_message' => 'Preparing...'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$node->save();
|
||||||
|
|
||||||
|
$id = $node->id();
|
||||||
|
|
||||||
|
exec("/opt/drupal/scripts/worker.sh $id > /dev/null 2>&1 &");
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'entity_id' => $id,
|
||||||
|
'status' => 'started'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status($id) {
|
||||||
|
$entity = $this->loadProcessingEntity((string) $id);
|
||||||
|
if (!$entity) {
|
||||||
|
return new JsonResponse(['error' => 'not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$progress = 0;
|
||||||
|
$status = 'unknown';
|
||||||
|
$message = '';
|
||||||
|
|
||||||
|
if ($entity->hasField('field_processing_progress')) {
|
||||||
|
$value = $entity->get('field_processing_progress')->value;
|
||||||
|
$progress = is_numeric($value) ? (int) $value : 0;
|
||||||
|
}
|
||||||
|
if ($entity->hasField('field_processing_status')) {
|
||||||
|
$statusValue = $entity->get('field_processing_status')->value;
|
||||||
|
$status = $statusValue !== NULL && $statusValue !== '' ? (string) $statusValue : 'unknown';
|
||||||
|
}
|
||||||
|
if ($entity->hasField('field_processing_message')) {
|
||||||
|
$messageValue = $entity->get('field_processing_message')->value;
|
||||||
|
$message = $messageValue !== NULL ? (string) $messageValue : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new JsonResponse([
|
||||||
|
'progress' => $progress,
|
||||||
|
'status' => $status,
|
||||||
|
'message' => $message
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->headers->set('Cache-Control','no-store');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadProcessingEntity(string $id): ?EntityInterface {
|
||||||
|
if ($id === '') {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity_type_manager = \Drupal::entityTypeManager();
|
||||||
|
|
||||||
|
foreach (['wisski_individual', 'node'] as $entity_type) {
|
||||||
|
try {
|
||||||
|
if ($entity_type_manager->hasDefinition($entity_type)) {
|
||||||
|
$entity = $entity_type_manager->getStorage($entity_type)->load($id);
|
||||||
|
if ($entity instanceof EntityInterface) {
|
||||||
|
return $entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctype_digit($id)) {
|
||||||
|
$node = Node::load((int) $id);
|
||||||
|
if ($node instanceof EntityInterface) {
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
105
src/Controller/SaveMetadataController.php
Normal file
105
src/Controller/SaveMetadataController.php
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Controller;
|
||||||
|
|
||||||
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Drupal\Core\File\FileSystemInterface;
|
||||||
|
|
||||||
|
class SaveMetadataController extends ControllerBase {
|
||||||
|
|
||||||
|
private const ALLOWED_SUBDIR = 'viewer';
|
||||||
|
|
||||||
|
public function save(Request $request): JsonResponse {
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Auth (no $_SESSION)
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
if ($this->currentUser()->isAnonymous()) {
|
||||||
|
throw new AccessDeniedHttpException('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Input JSON
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$input = json_decode($request->getContent(), true);
|
||||||
|
if (!is_array($input)) {
|
||||||
|
throw new BadRequestHttpException('Invalid JSON');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Validate data
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$path = $input['path'] ?? '';
|
||||||
|
$filename = $input['filename'] ?? '';
|
||||||
|
$content = $input['content'] ?? '';
|
||||||
|
|
||||||
|
if (!is_string($path) || !is_string($filename)) {
|
||||||
|
throw new BadRequestHttpException('Invalid input');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('#^[a-zA-Z0-9._-]+(?:/[a-zA-Z0-9._-]+)*$#', $path)) {
|
||||||
|
\Drupal::logger('dfg_3dviewer')->error(
|
||||||
|
'Invalid path received: "@path"',
|
||||||
|
['@path' => $path]
|
||||||
|
);
|
||||||
|
throw new BadRequestHttpException('Invalid path');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('#^[a-zA-Z0-9._-]+$#', $filename)) {
|
||||||
|
\Drupal::logger('dfg_3dviewer')->error(
|
||||||
|
'Invalid filename received: "@filename"',
|
||||||
|
['@filename' => $filename]
|
||||||
|
);
|
||||||
|
throw new BadRequestHttpException('Invalid filename');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Paths (safe)
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$directory = 'public://' . $path . '/metadata';
|
||||||
|
|
||||||
|
$fileSystem = \Drupal::service('file_system');
|
||||||
|
|
||||||
|
$fileSystem->prepareDirectory(
|
||||||
|
$directory,
|
||||||
|
FileSystemInterface::CREATE_DIRECTORY |
|
||||||
|
FileSystemInterface::MODIFY_PERMISSIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
$realDir = $fileSystem->realpath($directory);
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Save file
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$content = is_array($content)
|
||||||
|
? json_encode($content, JSON_PRETTY_PRINT)
|
||||||
|
: (string) $content;
|
||||||
|
|
||||||
|
$filePath = $directory . '/' . $filename . '_viewer.json';
|
||||||
|
|
||||||
|
$fileSystem->saveData(
|
||||||
|
$content,
|
||||||
|
$filePath,
|
||||||
|
FileSystemInterface::EXISTS_REPLACE
|
||||||
|
);
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Response
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'status' => 'ok',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/Controller/ThumbnailUploadController.php
Normal file
169
src/Controller/ThumbnailUploadController.php
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Controller;
|
||||||
|
|
||||||
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
use Drupal\Core\File\FileSystemInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
|
class ThumbnailUploadController extends ControllerBase {
|
||||||
|
|
||||||
|
private function getConfiguredBaseUrl(): string {
|
||||||
|
$config = \Drupal::config('dfg_3dviewer.settings');
|
||||||
|
$base = (string) (
|
||||||
|
$config->get('dfg_3dviewer_main_url')
|
||||||
|
?? $config->get('main_url')
|
||||||
|
?? ''
|
||||||
|
);
|
||||||
|
$base = trim($base);
|
||||||
|
if ($base === '') {
|
||||||
|
$request = \Drupal::request();
|
||||||
|
return rtrim($request->getSchemeAndHttpHost(), '/');
|
||||||
|
}
|
||||||
|
if (!preg_match('#^https?://#i', $base)) {
|
||||||
|
$base = 'https://' . $base;
|
||||||
|
}
|
||||||
|
return rtrim($base, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload(Request $request): JsonResponse {
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Auth
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
if ($this->currentUser()->isAnonymous()) {
|
||||||
|
throw new AccessDeniedHttpException('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Input (POST fields)
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$filename = $request->request->get('filename', '');
|
||||||
|
$wisskiId = $request->request->get('wisski_individual', '');
|
||||||
|
|
||||||
|
if (!is_string($filename) || !preg_match('#^[a-zA-Z0-9_-]+$#', $filename)) {
|
||||||
|
throw new BadRequestHttpException('Invalid filename');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($wisskiId) || !preg_match('#^[a-zA-Z0-9_-]+$#', $wisskiId)) {
|
||||||
|
throw new BadRequestHttpException('Invalid WissKI id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Uploaded file
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$file = $request->files->get('data');
|
||||||
|
|
||||||
|
if (!$file || $file->getError() !== UPLOAD_ERR_OK) {
|
||||||
|
throw new BadRequestHttpException('Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed = ['image/png', 'image/jpeg'];
|
||||||
|
|
||||||
|
$mime = $file->getClientMimeType();
|
||||||
|
|
||||||
|
if (!in_array($mime, $allowed, true)) {
|
||||||
|
throw new HttpException(415, 'Invalid image type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!@getimagesize($file->getPathname())) {
|
||||||
|
throw new HttpException(415, 'File is not a valid image');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Target directory
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
|
||||||
|
$uri = (string) $request->request->get('path', '');
|
||||||
|
$parsed = parse_url($uri);
|
||||||
|
$path = $parsed['path'] ?? '';
|
||||||
|
|
||||||
|
$relative = preg_replace('#^/?sites/default/files/#', '', $path);
|
||||||
|
|
||||||
|
if (str_contains($relative, '..')) {
|
||||||
|
throw new \InvalidArgumentException('Invalid path');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileSystem = \Drupal::service('file_system');
|
||||||
|
|
||||||
|
$directory = 'public://' . $relative;
|
||||||
|
$views = $directory . '/views';
|
||||||
|
|
||||||
|
if (!is_dir($fileSystem->realpath($directory))) {
|
||||||
|
throw new \RuntimeException('Base directory missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileSystem->prepareDirectory(
|
||||||
|
$views,
|
||||||
|
FileSystemInterface::CREATE_DIRECTORY
|
||||||
|
| FileSystemInterface::MODIFY_PERMISSIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Filename
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$filename = preg_replace('/[^a-zA-Z0-9_-]/', '_', $filename);
|
||||||
|
|
||||||
|
$extension = match ($mime) {
|
||||||
|
'image/png' => 'png',
|
||||||
|
'image/jpeg' => 'jpg',
|
||||||
|
};
|
||||||
|
|
||||||
|
$targetName = $filename . '_side45.' . $extension;
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Save
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$file->move(
|
||||||
|
$fileSystem->realpath($views),
|
||||||
|
$targetName
|
||||||
|
);
|
||||||
|
|
||||||
|
$realPath = $fileSystem->realpath($views . '/' . $targetName);
|
||||||
|
/* =========================
|
||||||
|
WissKI call
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$wisskiBaseUrl = $this->getConfiguredBaseUrl();
|
||||||
|
$url = $wisskiBaseUrl . '/wisski/dfg_3dviewer/' . $wisskiId . '/savePreview';
|
||||||
|
|
||||||
|
$client = \Drupal::httpClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $client->post($url, [
|
||||||
|
'form_params' => [
|
||||||
|
'path' => $realPath,
|
||||||
|
],
|
||||||
|
'timeout' => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$wisskiStatus = $response->getStatusCode();
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
\Drupal::logger('dfg_3dviewer')->error($e->getMessage());
|
||||||
|
$wisskiStatus = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Response
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'status' => 'ok',
|
||||||
|
'bytes' => filesize($realPath),
|
||||||
|
'wisski_status' => $wisskiStatus
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
956
src/Controller/XmlExportController.php
Normal file
956
src/Controller/XmlExportController.php
Normal file
|
|
@ -0,0 +1,956 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Controller;
|
||||||
|
|
||||||
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
|
use Drupal\Core\File\FileSystemInterface;
|
||||||
|
use GuzzleHttp\ClientInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
|
||||||
|
class XmlExportController extends ControllerBase {
|
||||||
|
|
||||||
|
private const XSL_URL = 'https://raw.githubusercontent.com/slub/dfg-viewer/e54305a9fa58951d3f3d1dd7e64554cb2ee881eb/Resources/Public/XSLT/exportSingleToMetsMods.xsl';
|
||||||
|
private const JSON_EXPORT_PATH = '/api/digital_reconstruction/record/%d';
|
||||||
|
private const ADDITIONAL_MODEL_FIELD_CANDIDATES = [
|
||||||
|
'fdc6300213a0d25d4b68069564846363',
|
||||||
|
];
|
||||||
|
private const EXPORT_PATHS = [
|
||||||
|
'/wisski/navigate/%d/view',
|
||||||
|
'/export_xml_single/%d',
|
||||||
|
];
|
||||||
|
private const FILE_DIR = 'public://xml_structure';
|
||||||
|
|
||||||
|
protected ClientInterface $httpClient;
|
||||||
|
protected FileSystemInterface $fileSystem;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
ClientInterface $http_client,
|
||||||
|
FileSystemInterface $file_system
|
||||||
|
) {
|
||||||
|
$this->httpClient = $http_client;
|
||||||
|
$this->fileSystem = $file_system;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create(ContainerInterface $container): self {
|
||||||
|
return new static(
|
||||||
|
$container->get('http_client'),
|
||||||
|
$container->get('file_system')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route callback.
|
||||||
|
*/
|
||||||
|
public function export(Request $request, ?string $id = null): Response {
|
||||||
|
$content = trim((string) $request->getContent());
|
||||||
|
$domain = null;
|
||||||
|
|
||||||
|
if ($request->isMethod('GET')) {
|
||||||
|
$domain = trim((string) $request->query->get('domain', ''));
|
||||||
|
}
|
||||||
|
elseif ($content !== '' && $this->isJson($content)) {
|
||||||
|
$data = json_decode($content, true);
|
||||||
|
$id = $id ?? ($data['id'] ?? null);
|
||||||
|
$domain = trim((string) ($data['domain'] ?? ''));
|
||||||
|
if ($domain === '') {
|
||||||
|
return new Response('Missing domain', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
return new Response('Missing id', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isMethod('GET') && !$domain) {
|
||||||
|
return new Response('Missing domain', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->buildExportXml($request, (int) $id, (string) $domain);
|
||||||
|
$this->saveXml((string) $id, $result);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
$result,
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/xml; charset=UTF-8']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
\Drupal::logger('dfg_3dviewer')->error($e->getMessage());
|
||||||
|
return new Response('XML export failed', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected function isJson(string $content): bool {
|
||||||
|
json_decode($content);
|
||||||
|
return json_last_error() === JSON_ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds export XML from request body, legacy XML source or JSON API source.
|
||||||
|
*/
|
||||||
|
protected function buildExportXml(Request $request, int $id, string $domain): string {
|
||||||
|
$xmlString = trim($request->getContent() ?? '');
|
||||||
|
if ($xmlString !== '') {
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$xml = simplexml_load_string($xmlString);
|
||||||
|
if ($xml instanceof \SimpleXMLElement) {
|
||||||
|
return $this->transformXml($xml, $domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$xml = $this->fetchSourceXmlFromDomain($id, $domain);
|
||||||
|
return $this->transformXml($xml, $domain);
|
||||||
|
}
|
||||||
|
catch (\Throwable $legacyException) {
|
||||||
|
\Drupal::logger('dfg_3dviewer')->warning(
|
||||||
|
'Legacy XML source fetch failed for @id, trying JSON fallback: @msg',
|
||||||
|
[
|
||||||
|
'@id' => (string) $id,
|
||||||
|
'@msg' => $legacyException->getMessage(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$record = $this->fetchJsonRecordFromDomain($id, $domain);
|
||||||
|
$record = $this->enrichJsonRecordFromLocalEntity($record, $id);
|
||||||
|
return $this->buildXmlFromJsonRecord($record, $id, $domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchSourceXmlFromDomain(int $id, string $domain): \SimpleXMLElement {
|
||||||
|
$domain = $this->normalizeDomain($domain);
|
||||||
|
if ($domain === '') {
|
||||||
|
throw new \RuntimeException('Missing domain for XML source fetch');
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = http_build_query(['page' => 0, '_format' => 'xml']);
|
||||||
|
$attempts = [];
|
||||||
|
|
||||||
|
foreach (self::EXPORT_PATHS as $pattern) {
|
||||||
|
$url = $domain . sprintf($pattern, $id) . '?' . $query;
|
||||||
|
try {
|
||||||
|
$response = $this->httpClient->request('GET', $url, ['http_errors' => false]);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
$attempts[] = sprintf('%s => exception: %s', $url, $e->getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $response->getStatusCode();
|
||||||
|
$attempts[] = sprintf('%s => %d', $url, $status);
|
||||||
|
if ($status !== 200) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xmlString = (string) $response->getBody();
|
||||||
|
if ($xmlString === '') {
|
||||||
|
$attempts[] = sprintf('%s => empty body', $url);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$xml = simplexml_load_string($xmlString);
|
||||||
|
if ($xml instanceof \SimpleXMLElement) {
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
$attempts[] = sprintf('%s => invalid XML', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
\Drupal::logger('dfg_3dviewer')->error('XML source fetch failed; attempts: @attempts', ['@attempts' => implode('; ', $attempts)]);
|
||||||
|
throw new \RuntimeException('Cannot fetch source XML from domain');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeDomain(string $domain): string {
|
||||||
|
$domain = trim($domain);
|
||||||
|
if ($domain === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (!preg_match('#^https?://#i', $domain)) {
|
||||||
|
$domain = 'https://' . $domain;
|
||||||
|
}
|
||||||
|
return rtrim($domain, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchJsonRecordFromDomain(int $id, string $domain): array {
|
||||||
|
$json_base_url = trim((string) (
|
||||||
|
$this->config('dfg_3dviewer.settings')->get('dfg_3dviewer_json_export_base_url')
|
||||||
|
?? $this->config('dfg_3dviewer.settings')->get('json_export_base_url')
|
||||||
|
?? ''
|
||||||
|
));
|
||||||
|
$source = trim($json_base_url !== '' ? $json_base_url : $domain);
|
||||||
|
$url = $this->resolveJsonRecordUrl($source, $id);
|
||||||
|
if ($url === '') {
|
||||||
|
throw new \RuntimeException('Missing base URL for JSON source fetch');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->httpClient->request('GET', $url, ['http_errors' => false]);
|
||||||
|
if ($response->getStatusCode() !== 200) {
|
||||||
|
throw new \RuntimeException('JSON source fetch failed with status ' . $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_decode((string) $response->getBody(), true);
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
throw new \RuntimeException('JSON source returned invalid payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
$record = $payload[0] ?? $payload;
|
||||||
|
if (!is_array($record) || empty($record)) {
|
||||||
|
throw new \RuntimeException('JSON source returned an empty record');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveJsonRecordUrl(string $source, int $id): string {
|
||||||
|
$source = trim($source);
|
||||||
|
if ($source === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('#/api/digital_reconstruction/record/\d+/?$#', $source)) {
|
||||||
|
return rtrim($source, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('#/api/digital_reconstruction/record/?$#', $source)) {
|
||||||
|
return rtrim($source, '/') . '/' . $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$base_url = $this->normalizeDomain($source);
|
||||||
|
if ($base_url === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $base_url . sprintf(self::JSON_EXPORT_PATH, $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform XML using XSLT.
|
||||||
|
*/
|
||||||
|
protected function transformXml(\SimpleXMLElement $xml, string $domain): string {
|
||||||
|
$xsl = simplexml_load_string($this->fetchXsl());
|
||||||
|
if (!$xsl) {
|
||||||
|
throw new \RuntimeException('Cannot load XSL');
|
||||||
|
}
|
||||||
|
|
||||||
|
$xslt = new \XSLTProcessor();
|
||||||
|
$xslt->importStyleSheet($xsl);
|
||||||
|
|
||||||
|
$result = $xslt->transformToXML($xml);
|
||||||
|
if ($result === false) {
|
||||||
|
throw new \RuntimeException('XSLT transformation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->normalizeDefaultHostUrls($result, $domain);
|
||||||
|
return $this->formatXml($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildXmlFromJsonRecord(array $record, int $id, string $domain): string {
|
||||||
|
$domain = $this->normalizeDomain($domain);
|
||||||
|
$title = $this->stringValue($record, 'title', 'Digital reconstruction ' . $id);
|
||||||
|
$converted_file = $this->normalizeArchiveModelPath(
|
||||||
|
$this->extractModelUrlFromRecord($record)
|
||||||
|
);
|
||||||
|
if ($converted_file === '') {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'JSON record does not contain a 3D file URL. Available keys: ' . implode(', ', array_keys($record))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$preview = $this->firstNonEmptyValue($record, ['object_preview', 'preview', 'reconstruction_previews']);
|
||||||
|
$metadata_export = $this->firstNonEmptyValue($record, ['metadata_export']);
|
||||||
|
$viewer_metadata = $this->loadViewerMetadataFromModelUrl($converted_file);
|
||||||
|
$iiif_annotations_xml = trim((string) (
|
||||||
|
$viewer_metadata['iiifAnnotationsXml']
|
||||||
|
?? $viewer_metadata['iiif_annotations_xml']
|
||||||
|
?? $viewer_metadata['annotationsXml']
|
||||||
|
?? $viewer_metadata['annotations_xml']
|
||||||
|
?? ''
|
||||||
|
));
|
||||||
|
$annotation_entries = $this->extractAnnotationEntriesFromViewerMetadata($viewer_metadata);
|
||||||
|
$object_uri = $this->firstNonEmptyValue($record, ['object_URI', 'URI']);
|
||||||
|
$description = $this->firstNonEmptyValue($record, ['object_description']);
|
||||||
|
$authors = $this->firstNonEmptyValue($record, ['reconstruction_authors']);
|
||||||
|
$authors_affiliation = $this->firstNonEmptyValue($record, ['reconstruction_authors_affiliation']);
|
||||||
|
$license = $this->firstNonEmptyValue($record, ['reconstruction_license']);
|
||||||
|
$time_frame = $this->firstNonEmptyValue($record, ['reconstruction_time_frame']);
|
||||||
|
$edition_date = $this->firstNonEmptyValue($record, ['edition_date']);
|
||||||
|
$object_name = $this->firstNonEmptyValue($record, ['object_name']);
|
||||||
|
$object_type = $this->firstNonEmptyValue($record, ['object_type']);
|
||||||
|
$object_category = $this->firstNonEmptyValue($record, ['object_category']);
|
||||||
|
$project_name = $this->firstNonEmptyValue($record, ['project_name']);
|
||||||
|
$project_acronym = $this->firstNonEmptyValue($record, ['project_acronym']);
|
||||||
|
$custody = $this->firstNonEmptyValue($record, ['reconstruction_custody']);
|
||||||
|
$status = $this->firstNonEmptyValue($record, ['status']);
|
||||||
|
|
||||||
|
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||||
|
$dom->preserveWhiteSpace = false;
|
||||||
|
$dom->formatOutput = true;
|
||||||
|
|
||||||
|
$mets = $dom->createElementNS('http://www.loc.gov/METS/', 'mets:mets');
|
||||||
|
$mets->setAttribute('OBJID', (string) $id);
|
||||||
|
$mets->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:mods', 'http://www.loc.gov/mods/v3');
|
||||||
|
$mets->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||||||
|
$mets->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:iiif', 'http://iiif.io/api/presentation/3#');
|
||||||
|
$dom->appendChild($mets);
|
||||||
|
|
||||||
|
$metsHdr = $dom->createElement('mets:metsHdr');
|
||||||
|
if ($edition_date !== '') {
|
||||||
|
$metsHdr->setAttribute('LASTMODDATE', $edition_date);
|
||||||
|
}
|
||||||
|
$mets->appendChild($metsHdr);
|
||||||
|
|
||||||
|
$dmdSec = $dom->createElement('mets:dmdSec');
|
||||||
|
$dmdSec->setAttribute('ID', 'DMD1');
|
||||||
|
$mets->appendChild($dmdSec);
|
||||||
|
|
||||||
|
$mdWrap = $dom->createElement('mets:mdWrap');
|
||||||
|
$mdWrap->setAttribute('MDTYPE', 'MODS');
|
||||||
|
$dmdSec->appendChild($mdWrap);
|
||||||
|
|
||||||
|
$xmlData = $dom->createElement('mets:xmlData');
|
||||||
|
$mdWrap->appendChild($xmlData);
|
||||||
|
|
||||||
|
$mods = $dom->createElement('mods:mods');
|
||||||
|
$xmlData->appendChild($mods);
|
||||||
|
|
||||||
|
$titleInfo = $dom->createElement('mods:titleInfo');
|
||||||
|
$mods->appendChild($titleInfo);
|
||||||
|
$titleInfo->appendChild($dom->createElement('mods:title', $title));
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'reconstruction_authors' => $authors,
|
||||||
|
'reconstruction_authors_affiliation' => $authors_affiliation,
|
||||||
|
'object_name' => $object_name,
|
||||||
|
'object_type' => $object_type,
|
||||||
|
'object_category' => $object_category,
|
||||||
|
'project_name' => $project_name,
|
||||||
|
'project_acronym' => $project_acronym,
|
||||||
|
'reconstruction_custody' => $custody,
|
||||||
|
'status' => $status,
|
||||||
|
'reconstruction_time_frame' => $time_frame,
|
||||||
|
] as $label => $value) {
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$note = $dom->createElement('mods:note', $value);
|
||||||
|
$note->setAttribute('type', $label);
|
||||||
|
$mods->appendChild($note);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authors !== '') {
|
||||||
|
$name = $dom->createElement('mods:name');
|
||||||
|
$name->setAttribute('type', 'personal');
|
||||||
|
$mods->appendChild($name);
|
||||||
|
$name->appendChild($dom->createElement('mods:namePart', $authors));
|
||||||
|
$role = $dom->createElement('mods:role');
|
||||||
|
$name->appendChild($role);
|
||||||
|
$role->appendChild($dom->createElement('mods:roleTerm', 'creator'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($description !== '') {
|
||||||
|
$mods->appendChild($dom->createElement('mods:abstract', $description));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($license !== '') {
|
||||||
|
$mods->appendChild($dom->createElement('mods:accessCondition', $license));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($edition_date !== '') {
|
||||||
|
$originInfo = $dom->createElement('mods:originInfo');
|
||||||
|
$originInfo->appendChild($dom->createElement('mods:dateIssued', $edition_date));
|
||||||
|
$mods->appendChild($originInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($object_uri !== '' || $metadata_export !== '' || $domain !== '') {
|
||||||
|
$location = $dom->createElement('mods:location');
|
||||||
|
$mods->appendChild($location);
|
||||||
|
if ($object_uri !== '') {
|
||||||
|
$location->appendChild($dom->createElement('mods:url', $object_uri));
|
||||||
|
}
|
||||||
|
if ($metadata_export !== '') {
|
||||||
|
$url = $dom->createElement('mods:url', $metadata_export);
|
||||||
|
$url->setAttribute('usage', 'primary display');
|
||||||
|
$location->appendChild($url);
|
||||||
|
}
|
||||||
|
if ($domain !== '') {
|
||||||
|
$location->appendChild($dom->createElement('mods:physicalLocation', $domain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileSec = $dom->createElement('mets:fileSec');
|
||||||
|
$mets->appendChild($fileSec);
|
||||||
|
|
||||||
|
$modelGroup = $dom->createElement('mets:fileGrp');
|
||||||
|
$modelGroup->setAttribute('USE', 'MODEL');
|
||||||
|
$fileSec->appendChild($modelGroup);
|
||||||
|
|
||||||
|
$modelFile = $dom->createElement('mets:file');
|
||||||
|
$modelFile->setAttribute('ID', 'FILE_MODEL');
|
||||||
|
$modelFile->setAttribute('MIMETYPE', $this->guessMimeTypeFromUrl($converted_file));
|
||||||
|
$modelGroup->appendChild($modelFile);
|
||||||
|
|
||||||
|
$modelFLocat = $dom->createElement('mets:FLocat');
|
||||||
|
$modelFLocat->setAttribute('LOCTYPE', 'URL');
|
||||||
|
$modelFLocat->setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', $converted_file);
|
||||||
|
$modelFile->appendChild($modelFLocat);
|
||||||
|
|
||||||
|
if ($preview !== '') {
|
||||||
|
$thumbGroup = $dom->createElement('mets:fileGrp');
|
||||||
|
$thumbGroup->setAttribute('USE', 'THUMBNAIL');
|
||||||
|
$fileSec->appendChild($thumbGroup);
|
||||||
|
|
||||||
|
$thumbFile = $dom->createElement('mets:file');
|
||||||
|
$thumbFile->setAttribute('ID', 'FILE_PREVIEW');
|
||||||
|
$thumbFile->setAttribute('MIMETYPE', $this->guessMimeTypeFromUrl($preview));
|
||||||
|
$thumbGroup->appendChild($thumbFile);
|
||||||
|
|
||||||
|
$thumbFLocat = $dom->createElement('mets:FLocat');
|
||||||
|
$thumbFLocat->setAttribute('LOCTYPE', 'URL');
|
||||||
|
$thumbFLocat->setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', $preview);
|
||||||
|
$thumbFile->appendChild($thumbFLocat);
|
||||||
|
}
|
||||||
|
|
||||||
|
$structMap = $dom->createElement('mets:structMap');
|
||||||
|
$structMap->setAttribute('TYPE', 'LOGICAL');
|
||||||
|
$mets->appendChild($structMap);
|
||||||
|
|
||||||
|
$div = $dom->createElement('mets:div');
|
||||||
|
$div->setAttribute('TYPE', 'monograph');
|
||||||
|
$div->setAttribute('DMDID', 'DMD1');
|
||||||
|
$div->setAttribute('LABEL', $title);
|
||||||
|
$structMap->appendChild($div);
|
||||||
|
|
||||||
|
$fptr = $dom->createElement('mets:fptr');
|
||||||
|
$fptr->setAttribute('FILEID', 'FILE_MODEL');
|
||||||
|
$div->appendChild($fptr);
|
||||||
|
|
||||||
|
$amdSec = $dom->createElement('mets:amdSec');
|
||||||
|
$mets->appendChild($amdSec);
|
||||||
|
$techMD = $dom->createElement('mets:techMD');
|
||||||
|
$techMD->setAttribute('ID', 'TECH1');
|
||||||
|
$amdSec->appendChild($techMD);
|
||||||
|
$techWrap = $dom->createElement('mets:mdWrap');
|
||||||
|
$techWrap->setAttribute('MDTYPE', 'OTHER');
|
||||||
|
$techWrap->setAttribute('OTHERMDTYPE', 'DFG3D');
|
||||||
|
$techMD->appendChild($techWrap);
|
||||||
|
$techXmlData = $dom->createElement('mets:xmlData');
|
||||||
|
$techWrap->appendChild($techXmlData);
|
||||||
|
$techXmlData->appendChild($dom->createElement('converted_file', $converted_file));
|
||||||
|
if ($preview !== '') {
|
||||||
|
$techXmlData->appendChild($dom->createElement('object_preview', $preview));
|
||||||
|
}
|
||||||
|
if ($metadata_export !== '') {
|
||||||
|
$techXmlData->appendChild($dom->createElement('metadata_export', $metadata_export));
|
||||||
|
}
|
||||||
|
if ($iiif_annotations_xml !== '') {
|
||||||
|
$rawNode = $dom->createElement('iiif_annotations_xml');
|
||||||
|
$rawNode->appendChild($dom->createCDATASection($iiif_annotations_xml));
|
||||||
|
$techXmlData->appendChild($rawNode);
|
||||||
|
}
|
||||||
|
$iiifNode = $this->buildIiifAnnotationsNode($dom, $iiif_annotations_xml, $annotation_entries);
|
||||||
|
if ($iiifNode instanceof \DOMNode) {
|
||||||
|
$techXmlData->appendChild($iiifNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = $dom->saveXML();
|
||||||
|
if ($xml === false) {
|
||||||
|
throw new \RuntimeException('Cannot build XML from JSON record.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = $this->normalizeDefaultHostUrls($xml, $domain);
|
||||||
|
return $this->formatXml($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function enrichJsonRecordFromLocalEntity(array $record, int $id): array {
|
||||||
|
$current_model = $this->extractModelUrlFromRecord($record);
|
||||||
|
if ($current_model !== '') {
|
||||||
|
if ($this->stringValue($record, '3D_file') === '') {
|
||||||
|
$record['3D_file'] = $current_model;
|
||||||
|
$source = $this->detectModelSourceInRecord($record);
|
||||||
|
\Drupal::logger('dfg_3dviewer')->notice(
|
||||||
|
'Filled missing JSON 3D_file for entity @id from existing record source "@source": @value',
|
||||||
|
[
|
||||||
|
'@id' => (string) $id,
|
||||||
|
'@source' => $source,
|
||||||
|
'@value' => $current_model,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = $this->config('dfg_3dviewer.settings');
|
||||||
|
$field_candidates = array_values(array_filter(array_unique(array_merge([
|
||||||
|
trim((string) ($cfg->get('dfg_3dviewer_viewer_file_name') ?? $cfg->get('viewer_file_name') ?? '')),
|
||||||
|
trim((string) ($cfg->get('dfg_3dviewer_viewer_file_upload') ?? $cfg->get('viewer_file_upload') ?? '')),
|
||||||
|
trim((string) ($cfg->get('dfg_3dviewer_api_3d_file_field') ?? $cfg->get('api_3d_file_field') ?? '')),
|
||||||
|
], self::ADDITIONAL_MODEL_FIELD_CANDIDATES))));
|
||||||
|
|
||||||
|
foreach (['wisski_individual', 'node'] as $entity_type) {
|
||||||
|
try {
|
||||||
|
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($id);
|
||||||
|
if (!$entity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($field_candidates as $field_name) {
|
||||||
|
$resolved = $this->resolveEntityFieldToPublicUrl($entity, $field_name);
|
||||||
|
if ($resolved === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$record['3D_file'] = $resolved;
|
||||||
|
\Drupal::logger('dfg_3dviewer')->notice(
|
||||||
|
'Filled missing JSON 3D_file for entity @id from local field "@field": @value',
|
||||||
|
[
|
||||||
|
'@id' => (string) $id,
|
||||||
|
'@field' => $field_name,
|
||||||
|
'@value' => $resolved,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
// Try the next entity type.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveEntityFieldToPublicUrl($entity, string $field_name): string {
|
||||||
|
if ($field_name === '' || !method_exists($entity, 'hasField') || !$entity->hasField($field_name)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = $entity->get($field_name)->getValue();
|
||||||
|
$first = is_array($values[0] ?? null) ? $values[0] : [];
|
||||||
|
if (empty($first)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($first['target_id']) && ctype_digit((string) $first['target_id'])) {
|
||||||
|
$file = \Drupal\file\Entity\File::load((int) $first['target_id']);
|
||||||
|
if ($file) {
|
||||||
|
return $this->fileUriToPublicUrl((string) $file->getFileUri());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['value', 'uri'] as $key) {
|
||||||
|
$candidate = trim((string) ($first[$key] ?? ''));
|
||||||
|
if ($candidate === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preg_match('#^https?://#i', $candidate)) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $candidate)) {
|
||||||
|
return $this->fileUriToPublicUrl($candidate);
|
||||||
|
}
|
||||||
|
if (str_starts_with($candidate, '/sites/default/files/')) {
|
||||||
|
$base = $this->preferredPublicBaseUrl();
|
||||||
|
return $base !== '' ? rtrim($base, '/') . $candidate : $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fileUriToPublicUrl(string $uri): string {
|
||||||
|
$uri = trim($uri);
|
||||||
|
if ($uri === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('#^https?://#i', $uri)) {
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($uri, 'public://')) {
|
||||||
|
$relative = '/sites/default/files/' . ltrim(substr($uri, strlen('public://')), '/');
|
||||||
|
$base = $this->preferredPublicBaseUrl();
|
||||||
|
return $base !== '' ? rtrim($base, '/') . $relative : $relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$generated = (string) \Drupal::service('file_url_generator')->generateAbsoluteString($uri);
|
||||||
|
$host = (string) parse_url($generated, PHP_URL_HOST);
|
||||||
|
$path = (string) parse_url($generated, PHP_URL_PATH);
|
||||||
|
if ($host !== '' && (strpos($host, '_') !== false || strtolower($host) === 'default') && $path !== '') {
|
||||||
|
$base = $this->preferredPublicBaseUrl();
|
||||||
|
return $base !== '' ? rtrim($base, '/') . $path : $path;
|
||||||
|
}
|
||||||
|
return $generated;
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function preferredPublicBaseUrl(): string {
|
||||||
|
$cfg = $this->config('dfg_3dviewer.settings');
|
||||||
|
$candidates = [
|
||||||
|
trim((string) ($cfg->get('dfg_3dviewer_main_url') ?? $cfg->get('main_url') ?? '')),
|
||||||
|
trim((string) ($cfg->get('dfg_3dviewer_json_export_base_url') ?? $cfg->get('json_export_base_url') ?? '')),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
$parts = parse_url($candidate);
|
||||||
|
$host = is_array($parts) ? (string) ($parts['host'] ?? '') : '';
|
||||||
|
if (is_array($parts) && !empty($parts['scheme']) && $host !== '' && strpos($host, '_') === false && strtolower($host) !== 'default') {
|
||||||
|
return rtrim($candidate, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function firstNonEmptyValue(array $record, array $keys): string {
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$value = $this->stringValue($record, $key);
|
||||||
|
if ($value !== '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function stringValue(array $record, string $key, string $default = ''): string {
|
||||||
|
if (!array_key_exists($key, $record)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $record[$key];
|
||||||
|
if (is_array($value)) {
|
||||||
|
$value = implode(', ', array_filter(array_map('strval', $value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string) $value) ?: $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function guessMimeTypeFromUrl(string $url): string {
|
||||||
|
$path = parse_url($url, PHP_URL_PATH);
|
||||||
|
$extension = strtolower(pathinfo((string) $path, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
return match ($extension) {
|
||||||
|
'glb' => 'model/gltf-binary',
|
||||||
|
'gltf' => 'model/gltf+json',
|
||||||
|
'fbx' => 'application/octet-stream',
|
||||||
|
'obj' => 'text/plain',
|
||||||
|
'stl' => 'model/stl',
|
||||||
|
'ply' => 'application/octet-stream',
|
||||||
|
'dae' => 'model/vnd.collada+xml',
|
||||||
|
'jpg', 'jpeg' => 'image/jpeg',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'webp' => 'image/webp',
|
||||||
|
default => 'application/octet-stream',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function extractModelUrlFromRecord(array $record): string {
|
||||||
|
$candidate = $this->firstNonEmptyValue($record, [
|
||||||
|
'converted_file',
|
||||||
|
'3D_file',
|
||||||
|
'3d_file_original',
|
||||||
|
'3D_file_original',
|
||||||
|
'3d_file',
|
||||||
|
'model_file',
|
||||||
|
'model',
|
||||||
|
'file',
|
||||||
|
'viewer_file',
|
||||||
|
'viewer_file_name',
|
||||||
|
]);
|
||||||
|
if ($this->isModelUrl($candidate)) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->flattenRecordStrings($record) as $value) {
|
||||||
|
if ($this->isModelUrl($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function detectModelSourceInRecord(array $record): string {
|
||||||
|
$ordered_keys = [
|
||||||
|
'converted_file',
|
||||||
|
'3D_file',
|
||||||
|
'3d_file_original',
|
||||||
|
'3D_file_original',
|
||||||
|
'3d_file',
|
||||||
|
'model_file',
|
||||||
|
'model',
|
||||||
|
'file',
|
||||||
|
'viewer_file',
|
||||||
|
'viewer_file_name',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($ordered_keys as $key) {
|
||||||
|
$value = $this->stringValue($record, $key);
|
||||||
|
if ($value !== '' && $this->isModelUrl($value)) {
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'flattened_record_scan';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function flattenRecordStrings(array $record): array {
|
||||||
|
$values = [];
|
||||||
|
|
||||||
|
foreach ($record as $value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
$values = array_merge($values, $this->flattenRecordStrings($value));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
|
||||||
|
$values[] = trim((string) $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isModelUrl(string $value): bool {
|
||||||
|
if ($value === '' || !preg_match('#^https?://#i', $value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = (string) parse_url($value, PHP_URL_PATH);
|
||||||
|
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
return in_array($extension, ['glb', 'gltf', 'fbx', 'obj', 'stl', 'ply', 'dae', '3ds', 'ifc', 'xyz', 'pcd', 'abc'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadViewerMetadataFromModelUrl(string $model_url): array {
|
||||||
|
$path = (string) parse_url($model_url, PHP_URL_PATH);
|
||||||
|
if ($path === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = '/sites/default/files/';
|
||||||
|
$prefix_pos = strpos($path, $prefix);
|
||||||
|
if ($prefix_pos === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$relative = ltrim(substr($path, $prefix_pos + strlen($prefix)), '/');
|
||||||
|
if ($relative === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$relative = urldecode($relative);
|
||||||
|
$dirname = trim(dirname($relative), '/');
|
||||||
|
$basename = pathinfo($relative, PATHINFO_FILENAME);
|
||||||
|
if ($basename === '' || $basename === '.') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$viewer_uri = 'public://' . ($dirname !== '' ? $dirname . '/' : '') . 'metadata/' . $basename . '_viewer.json';
|
||||||
|
$real_path = $this->fileSystem->realpath($viewer_uri);
|
||||||
|
if ($real_path === false || !is_file($real_path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$content = file_get_contents($real_path);
|
||||||
|
if ($content === false || trim($content) === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$decoded = json_decode($content, true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function extractAnnotationEntriesFromViewerMetadata(array $viewer_metadata): array {
|
||||||
|
$entries = $viewer_metadata['annotationEntries'] ?? [];
|
||||||
|
if (!is_array($entries)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
if (!is_array($entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target_id = trim((string) (
|
||||||
|
$entry['targetId']
|
||||||
|
?? $entry['object']
|
||||||
|
?? ($entry['target']['id'] ?? '')
|
||||||
|
));
|
||||||
|
if ($target_id === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$faces = [];
|
||||||
|
$raw_faces = $entry['faceNumbers']
|
||||||
|
?? ($entry['target']['faces'] ?? [$entry['faceIndex'] ?? null]);
|
||||||
|
if (is_array($raw_faces)) {
|
||||||
|
foreach ($raw_faces as $face) {
|
||||||
|
if (is_numeric($face) && (int) $face >= 0) {
|
||||||
|
$faces[] = (int) $face;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($faces)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized[] = [
|
||||||
|
'id' => trim((string) ($entry['id'] ?? '')),
|
||||||
|
'targetId' => $target_id,
|
||||||
|
'faces' => array_values(array_unique($faces)),
|
||||||
|
'title' => trim((string) ($entry['title'] ?? '')),
|
||||||
|
'description' => trim((string) ($entry['description'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildIiifAnnotationsNode(
|
||||||
|
\DOMDocument $dom,
|
||||||
|
string $iiif_annotations_xml,
|
||||||
|
array $annotation_entries
|
||||||
|
): ?\DOMNode {
|
||||||
|
$iiif_annotations_xml = trim($iiif_annotations_xml);
|
||||||
|
if ($iiif_annotations_xml !== '') {
|
||||||
|
try {
|
||||||
|
$tmp = new \DOMDocument('1.0', 'UTF-8');
|
||||||
|
$tmp->loadXML($iiif_annotations_xml);
|
||||||
|
if ($tmp->documentElement instanceof \DOMElement) {
|
||||||
|
return $dom->importNode($tmp->documentElement, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
// Fallback to normalized entry export below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($annotation_entries)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = $dom->createElement('iiif:annotations');
|
||||||
|
$root->setAttribute('version', '3.0');
|
||||||
|
|
||||||
|
foreach ($annotation_entries as $entry) {
|
||||||
|
$target_id = trim((string) ($entry['targetId'] ?? ''));
|
||||||
|
$faces = is_array($entry['faces'] ?? null) ? $entry['faces'] : [];
|
||||||
|
if ($target_id === '' || empty($faces)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$annotation = $dom->createElement('iiif:annotation');
|
||||||
|
if (!empty($entry['id'])) {
|
||||||
|
$annotation->setAttribute('id', (string) $entry['id']);
|
||||||
|
}
|
||||||
|
$annotation->setAttribute('type', 'Annotation');
|
||||||
|
$annotation->setAttribute('motivation', 'commenting');
|
||||||
|
|
||||||
|
$body = $dom->createElement('iiif:body');
|
||||||
|
$body->setAttribute('type', 'TextualBody');
|
||||||
|
$body->setAttribute('format', 'text/plain');
|
||||||
|
$body->appendChild($dom->createElement('iiif:title', (string) ($entry['title'] ?? '')));
|
||||||
|
$body->appendChild($dom->createElement('iiif:description', (string) ($entry['description'] ?? '')));
|
||||||
|
$annotation->appendChild($body);
|
||||||
|
|
||||||
|
$target = $dom->createElement('iiif:target');
|
||||||
|
$target->setAttribute('id', $target_id);
|
||||||
|
$target->setAttribute('faces', implode(',', array_map('strval', $faces)));
|
||||||
|
$annotation->appendChild($target);
|
||||||
|
|
||||||
|
$root->appendChild($annotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$root->hasChildNodes()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $root;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeDefaultHostUrls(string $xml, string $domain): string {
|
||||||
|
$domain = $this->normalizeDomain($domain);
|
||||||
|
if ($domain === '') {
|
||||||
|
return $this->normalizeArchiveModelPath($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = preg_replace('#https?://(default|dfg_3dviewer)(?=/)#i', $domain, $xml);
|
||||||
|
$escaped = preg_quote($domain, '#');
|
||||||
|
|
||||||
|
$normalized = preg_replace(
|
||||||
|
"#{$escaped}/sites/default/files/wisski_original/{$escaped}#i",
|
||||||
|
$domain,
|
||||||
|
$normalized
|
||||||
|
);
|
||||||
|
$normalized = preg_replace(
|
||||||
|
"#https?://[^/]+/sites/default/files/wisski_original/{$escaped}#i",
|
||||||
|
$domain,
|
||||||
|
$normalized
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->normalizeArchiveModelPath($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeArchiveModelPath(string $value): string {
|
||||||
|
return preg_replace(
|
||||||
|
'#(/[^/"\'<>\s]+_(?:ZIP|RAR|TAR|XZ|GZ))/([^/"\'<>\s]+\.(?:glb|gltf))#i',
|
||||||
|
'$1/gltf/$2',
|
||||||
|
$value
|
||||||
|
) ?? $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchXsl(): string {
|
||||||
|
$response = $this->httpClient->request('GET', self::XSL_URL, ['http_errors' => false]);
|
||||||
|
if ($response->getStatusCode() !== 200) {
|
||||||
|
throw new \RuntimeException('Cannot fetch XSL: ' . $response->getStatusCode());
|
||||||
|
}
|
||||||
|
return (string) $response->getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty-print XML.
|
||||||
|
*/
|
||||||
|
protected function formatXml(string $xml): string {
|
||||||
|
$dom = new \DOMDocument('1.0');
|
||||||
|
$dom->preserveWhiteSpace = false;
|
||||||
|
$dom->formatOutput = true;
|
||||||
|
$dom->loadXML($xml);
|
||||||
|
|
||||||
|
return $dom->saveXML();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save XML to files directory.
|
||||||
|
*/
|
||||||
|
protected function saveXml(string $id, string $xml): string {
|
||||||
|
$directory = self::FILE_DIR;
|
||||||
|
$this->fileSystem->prepareDirectory(
|
||||||
|
$directory,
|
||||||
|
FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
$path = $directory . '/' . $id . '.xml';
|
||||||
|
$real_path = $this->fileSystem->realpath($directory);
|
||||||
|
|
||||||
|
file_put_contents($real_path . '/' . $id . '.xml', $xml);
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
360
src/Form/DFG3dViewerConfigForm.php
Normal file
360
src/Form/DFG3dViewerConfigForm.php
Normal file
|
|
@ -0,0 +1,360 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Form;
|
||||||
|
|
||||||
|
use Drupal\Core\Form\FormBase;
|
||||||
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
|
use Drupal\Core\File\FileSystemInterface;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class DFG3dViewerConfigForm extends FormBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the viewer module path to a path-first value.
|
||||||
|
*/
|
||||||
|
protected function normalizeBaseModulePath(string $value): string {
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
if ($value === '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('@^https?://@i', $value)) {
|
||||||
|
$parts = parse_url($value);
|
||||||
|
if (!empty($parts['path'])) {
|
||||||
|
$value = $parts['path'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (preg_match('@^/[^/]+\.[^/]+/.+@', $value)) {
|
||||||
|
$segments = array_values(array_filter(explode('/', $value), 'strlen'));
|
||||||
|
array_shift($segments);
|
||||||
|
$value = '/' . implode('/', $segments);
|
||||||
|
}
|
||||||
|
elseif (preg_match('@^[^/]+\.[^/]+/.+@', $value)) {
|
||||||
|
$segments = explode('/', $value);
|
||||||
|
array_shift($segments);
|
||||||
|
$value = '/' . implode('/', $segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = preg_replace('@/{2,}@', '/', $value);
|
||||||
|
|
||||||
|
if ($value !== '' && $value[0] !== '/') {
|
||||||
|
$value = '/' . $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim($value, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function getFormId() {
|
||||||
|
|
||||||
|
return 'dfg_3dviewer_settings_form';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||||
|
|
||||||
|
$settings = $this->configFactory()->getEditable('dfg_3dviewer.settings');
|
||||||
|
$default_config = \Drupal::config('dfg_3dviewer.settings');
|
||||||
|
$default_settings = [
|
||||||
|
'entity_bundle' => $default_config->get('dfg_3dviewer_entitybundle'),
|
||||||
|
'viewer_file_upload' => $default_config->get('dfg_3dviewer_viewer_file_upload'),
|
||||||
|
'image_generation' => $default_config->get('dfg_3dviewer_image_generation'),
|
||||||
|
'viewer_file_name' => $default_config->get('dfg_3dviewer_viewer_file_name'),
|
||||||
|
'api_3d_file_field' => $default_config->get('dfg_3dviewer_api_3d_file_field') ?? $default_config->get('api_3d_file_field'),
|
||||||
|
'field_df' => $default_config->get('dfg_3dviewer_field_df'),
|
||||||
|
'main_url' => $default_config->get('dfg_3dviewer_main_url') ?? $default_config->get('main_url'),
|
||||||
|
'metadata_url' => $default_config->get('dfg_3dviewer_metadata_url') ?? $default_config->get('metadata_url'),
|
||||||
|
'json_export_base_url' => $default_config->get('dfg_3dviewer_json_export_base_url') ?? $default_config->get('json_export_base_url'),
|
||||||
|
'basenamespace' => $default_config->get('dfg_3dviewer_basenamespace'),
|
||||||
|
'container' => $default_config->get('dfg_3dviewer_container'),
|
||||||
|
'lightweight' => $default_config->get('dfg_3dviewer_lightweight'),
|
||||||
|
'scale_container_x' => $default_config->get('dfg_3dviewer_scale_container_x'),
|
||||||
|
'scale_container_y' => $default_config->get('dfg_3dviewer_scale_container_y'),
|
||||||
|
'gallery_container' => $default_config->get('dfg_3dviewer_gallery_container'),
|
||||||
|
'gallery_image_class' => $default_config->get('dfg_3dviewer_gallery_image_class'),
|
||||||
|
'gallery_image_id' => $default_config->get('dfg_3dviewer_gallery_image_id'),
|
||||||
|
'base_module_path' => $default_config->get('dfg_3dviewer_base_module_path') ?: '/libraries/dfg-3dviewer/assets',
|
||||||
|
'entity_id_uri' => $default_config->get('dfg_3dviewer_entity_id_uri'),
|
||||||
|
'view_entity_path' => $default_config->get('dfg_3dviewer_view_entity_path'),
|
||||||
|
'attribute_id' => $default_config->get('dfg_3dviewer_attribute_id'),
|
||||||
|
'export_viewer' => $default_config->get('dfg_3dviewer_export_viewer'),
|
||||||
|
'export_viewer_url' => $default_config->get('dfg_3dviewer_export_viewer_url'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['#dfg_3dviewer_settings'] = $settings;
|
||||||
|
|
||||||
|
$form['#attached']['library'][] = dfg_3dviewer_get_library();
|
||||||
|
dfg_3dviewer_attach_settings($form);
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_main_url'] = [
|
||||||
|
'#default_value' => $default_settings['main_url'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Main URL'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => 'Change <b>main URL</b> for used repository',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_basenamespace'] = [
|
||||||
|
'#default_value' => $default_settings['basenamespace'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Default base namespace'),
|
||||||
|
'#description' => $this->t('(if different than Main URL)'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_metadata_url'] = [
|
||||||
|
'#default_value' => $default_settings['metadata_url'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Metadata URL'),
|
||||||
|
'#description' => '<b>URL</b> of the instance that serves metadata content'
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_json_export_base_url'] = [
|
||||||
|
'#default_value' => $default_settings['json_export_base_url'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('JSON Export Base URL'),
|
||||||
|
'#description' => '<b>Base URL</b> of the instance that serves JSON export, e.g. https://repository.covher.eu'
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_container'] = [
|
||||||
|
'#default_value' => $default_settings['container'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Container ID'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => '<b>ID</b> of the main container for the Viewer',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_entitybundle'] = [
|
||||||
|
'#default_value' => $default_settings['entity_bundle'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Entity Bundle ID'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => '<b>ID</b> of the bundle for the entity given in wisski pathbuilder for 3d_model field',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_viewer_file_upload'] = [
|
||||||
|
'#default_value' => $default_settings['viewer_file_upload'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Viewer File Upload ID'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => '<b>ID</b> of the bundle for the entity given in wisski pathbuilder for 3d_upload field',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_viewer_file_name'] = [
|
||||||
|
'#default_value' => $default_settings['viewer_file_name'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Viewer File Name ID'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => '<b>ID</b> of the bundle for the entity given in wisski pathbuilder for viewer_file_name field',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_api_3d_file_field'] = [
|
||||||
|
'#default_value' => $default_settings['api_3d_file_field'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('API 3D File Field'),
|
||||||
|
'#required' => false,
|
||||||
|
'#description' => '<b>ID or machine name</b> of the field that should populate API `3D_file`'
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_image_generation'] = [
|
||||||
|
'#default_value' => $default_settings['image_generation'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Image Generation'),
|
||||||
|
'#description' => '<b>ID</b> of the bundle for the entity given in wisski pathbuilder for image_generation field'
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_field_df'] = [
|
||||||
|
'#default_value' => $default_settings['field_df'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Field DF'),
|
||||||
|
'#description' => 'Name of the field given for <b>field_df</b>'
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_export_viewer'] = [
|
||||||
|
'#default_value' => $default_settings['export_viewer'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Export Viewer Field'),
|
||||||
|
'#description' => 'Name of the field given for <b>export_viewer</b>',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_export_viewer_url'] = [
|
||||||
|
'#default_value' => $default_settings['export_viewer_url'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Export Viewer URL'),
|
||||||
|
'#description' => 'URL for the export viewer',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['scale_wrapper'] = [
|
||||||
|
'#type' => 'container',
|
||||||
|
'#attributes' => [
|
||||||
|
'class' => ['scale-fields-wrapper', 'flex-container'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['scale_wrapper']['dfg_3dviewer_scale_container_x'] = [
|
||||||
|
'#default_value' => $default_settings['scale_container_x'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Scale container X'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => '<b>Width</b> scale of the container',
|
||||||
|
'#attributes' => [
|
||||||
|
'class' => ['half-width'],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['scale_wrapper']['dfg_3dviewer_scale_container_y'] = [
|
||||||
|
'#default_value' => $default_settings['scale_container_y'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Scale container Y'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => '<b>Height</b> scale of the container',
|
||||||
|
'#attributes' => [
|
||||||
|
'class' => ['half-width'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['gallery_wrapper'] = [
|
||||||
|
'#type' => 'container',
|
||||||
|
'#attributes' => [
|
||||||
|
'class' => ['gallery-fields-wrapper', 'gallery-container'],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['gallery_wrapper']['dfg_3dviewer_gallery_container'] = [
|
||||||
|
'#default_value' => $default_settings['gallery_container'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Gallery container element name'),
|
||||||
|
'#required' => false,
|
||||||
|
'#description' => '<b>Name</b> of the element with gallery URLs',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['gallery_wrapper']['dfg_3dviewer_gallery_image_class'] = [
|
||||||
|
'#default_value' => $default_settings['gallery_image_class'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Gallery class name for images'),
|
||||||
|
'#required' => false,
|
||||||
|
'#description' => '<b>Class</b> name for gallery images',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['gallery_wrapper']['dfg_3dviewer_gallery_image_id'] = [
|
||||||
|
'#default_value' => $default_settings['gallery_image_id'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Gallery ID name for images'),
|
||||||
|
'#required' => false,
|
||||||
|
'#description' => '<b>ID</b> name for gallery images',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_base_module_path'] = [
|
||||||
|
'#default_value' => $default_settings['base_module_path'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Path for the Viewer module'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => 'Real <b>path</b> for the Viewer module',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_entity_id_uri'] = [
|
||||||
|
'#default_value' => $default_settings['entity_id_uri'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Regex for entity ID'),
|
||||||
|
'#required' => false,
|
||||||
|
'#description' => '<b>Regex</b> that allows get ID of the entity',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_view_entity_path'] = [
|
||||||
|
'#default_value' => $default_settings['view_entity_path'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Path with navigate content'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => '<b>Path</b> that allows navigate to the entity',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_attribute_id'] = [
|
||||||
|
'#default_value' => $default_settings['attribute_id'],
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => $this->t('Attribute ID with WissKI content'),
|
||||||
|
'#required' => true,
|
||||||
|
'#description' => '<b>ID</b> that allows get more specific data from WissKI',
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['dfg_3dviewer_lightweight'] = [
|
||||||
|
'#default_value' => $default_settings['lightweight'],
|
||||||
|
'#type' => 'checkbox',
|
||||||
|
'#title' => $this->t('<b>Lightweight</b> version. If checked, 3D Viewer will provide only basic operations.'),
|
||||||
|
'#required' => false
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['submit'] = [
|
||||||
|
'#type' => 'submit',
|
||||||
|
'#value' => $this->t('Submit'),
|
||||||
|
];
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||||
|
if ($form_state->getValue('dfg_3dviewer_lightweight')) {
|
||||||
|
$optional_fields = [
|
||||||
|
'dfg_3dviewer_metadata_url',
|
||||||
|
'dfg_3dviewer_json_export_base_url',
|
||||||
|
'dfg_3dviewer_api_3d_file_field',
|
||||||
|
'dfg_3dviewer_image_generation',
|
||||||
|
'dfg_3dviewer_field_df',
|
||||||
|
'dfg_3dviewer_export_viewer',
|
||||||
|
'dfg_3dviewer_export_viewer_url',
|
||||||
|
'dfg_3dviewer_gallery_container',
|
||||||
|
'dfg_3dviewer_gallery_image_class',
|
||||||
|
'dfg_3dviewer_gallery_image_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($optional_fields as $field) {
|
||||||
|
$form_state->setValue($field, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||||
|
|
||||||
|
$settings = $form['#dfg_3dviewer_settings'];
|
||||||
|
$new_vals = $form_state->getValues();
|
||||||
|
$normalized_base_module_path = $this->normalizeBaseModulePath((string) $new_vals['dfg_3dviewer_base_module_path']);
|
||||||
|
|
||||||
|
$settings->set('dfg_3dviewer_basenamespace', $new_vals['dfg_3dviewer_basenamespace']);
|
||||||
|
$settings->set('dfg_3dviewer_main_url', $new_vals['dfg_3dviewer_main_url']);
|
||||||
|
$settings->set('dfg_3dviewer_metadata_url', $new_vals['dfg_3dviewer_metadata_url']);
|
||||||
|
$settings->set('dfg_3dviewer_json_export_base_url', $new_vals['dfg_3dviewer_json_export_base_url']);
|
||||||
|
$settings->set('dfg_3dviewer_entitybundle', $new_vals['dfg_3dviewer_entitybundle']);
|
||||||
|
$settings->set('dfg_3dviewer_container', $new_vals['dfg_3dviewer_container']);
|
||||||
|
$settings->set('dfg_3dviewer_viewer_file_upload', $new_vals['dfg_3dviewer_viewer_file_upload']);
|
||||||
|
$settings->set('dfg_3dviewer_viewer_file_name', $new_vals['dfg_3dviewer_viewer_file_name']);
|
||||||
|
$settings->set('dfg_3dviewer_api_3d_file_field', $new_vals['dfg_3dviewer_api_3d_file_field']);
|
||||||
|
$settings->set('dfg_3dviewer_image_generation', $new_vals['dfg_3dviewer_image_generation']);
|
||||||
|
$settings->set('dfg_3dviewer_field_df', $new_vals['dfg_3dviewer_field_df']);
|
||||||
|
$settings->set('dfg_3dviewer_lightweight', $new_vals['dfg_3dviewer_lightweight']);
|
||||||
|
$settings->set('dfg_3dviewer_scale_container_x', $new_vals['dfg_3dviewer_scale_container_x']);
|
||||||
|
$settings->set('dfg_3dviewer_scale_container_y', $new_vals['dfg_3dviewer_scale_container_y']);
|
||||||
|
$settings->set('dfg_3dviewer_gallery_container', $new_vals['dfg_3dviewer_gallery_container']);
|
||||||
|
$settings->set('dfg_3dviewer_gallery_image_class', $new_vals['dfg_3dviewer_gallery_image_class']);
|
||||||
|
$settings->set('dfg_3dviewer_gallery_image_id', $new_vals['dfg_3dviewer_gallery_image_id']);
|
||||||
|
$settings->set('dfg_3dviewer_base_module_path', $normalized_base_module_path);
|
||||||
|
$settings->set('dfg_3dviewer_entity_id_uri', $new_vals['dfg_3dviewer_entity_id_uri']);
|
||||||
|
$settings->set('dfg_3dviewer_view_entity_path', $new_vals['dfg_3dviewer_view_entity_path']);
|
||||||
|
$settings->set('dfg_3dviewer_attribute_id', $new_vals['dfg_3dviewer_attribute_id']);
|
||||||
|
$settings->set('dfg_3dviewer_export_viewer', $new_vals['dfg_3dviewer_export_viewer']);
|
||||||
|
$settings->set('dfg_3dviewer_export_viewer_url', $new_vals['dfg_3dviewer_export_viewer_url']);
|
||||||
|
|
||||||
|
$settings->save();
|
||||||
|
|
||||||
|
$this->messenger()->addStatus($this->t('Changed DFG 3D Viewer settings successfully'));
|
||||||
|
$form_state->setRedirect('system.admin_config');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
197
src/Plugin/Field/FieldFormatter/DFG3DDerivativeLinkFormatter.php
Executable file
197
src/Plugin/Field/FieldFormatter/DFG3DDerivativeLinkFormatter.php
Executable file
|
|
@ -0,0 +1,197 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Definition of Drupal\dfg_3dviewer\Plugin\field\formatter\DFG3DDerivativeLinkFormatter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Plugin\Field\FieldFormatter;
|
||||||
|
|
||||||
|
use Drupal\Core\Entity\EntityStorageInterface;
|
||||||
|
use Drupal\Core\Field\FieldItemListInterface;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
use Drupal\Core\Link;
|
||||||
|
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||||
|
use Drupal\Core\Session\AccountInterface;
|
||||||
|
use Drupal\Core\Url;
|
||||||
|
use Drupal\image\Entity\ImageStyle;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
|
use Drupal\Core\Cache\Cache;
|
||||||
|
use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatterBase;
|
||||||
|
use Drupal\colorbox\Plugin\Field\FieldFormatter\ColorboxFormatter;
|
||||||
|
use Drupal\Core\Template\Attribute;
|
||||||
|
use Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin implementation of the 'wisski_iip_image' formatter.
|
||||||
|
*
|
||||||
|
* @FieldFormatter(
|
||||||
|
* id = "dfg_3dderivativelink",
|
||||||
|
* module = "dfg_3dderivativelink",
|
||||||
|
* label = @Translation("DFG 3D Derivative Link"),
|
||||||
|
* field_types = {
|
||||||
|
* "file"
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
# class WisskiIIPImageFormatter extends ImageFormatterBase {
|
||||||
|
class DFG3DDerivativeLinkFormatter extends FileFormatterBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||||
|
|
||||||
|
|
||||||
|
// $elements = parent::viewElements($items, $langcode);
|
||||||
|
$elements = array();
|
||||||
|
|
||||||
|
$files = $this->getEntitiesToView($items, $langcode);
|
||||||
|
|
||||||
|
$elements['#attached']['library'][] = dfg_3dviewer_get_library();
|
||||||
|
dfg_3dviewer_attach_settings($elements);
|
||||||
|
|
||||||
|
// get the config
|
||||||
|
$cfg = dfg_3dviewer_config();
|
||||||
|
|
||||||
|
foreach ($files as $delta => $file) {
|
||||||
|
|
||||||
|
// get the filename
|
||||||
|
$filename = $file->getFilename();
|
||||||
|
|
||||||
|
// get pathinfo
|
||||||
|
$pathinfo = pathinfo($file->getFilename());
|
||||||
|
|
||||||
|
// pathinfo without the first extension so bla.tar.gz goes to bla.tar
|
||||||
|
$local_filename = $pathinfo['filename'];
|
||||||
|
|
||||||
|
// bla.tar.gz -> gz
|
||||||
|
$extension = $pathinfo['extension'];
|
||||||
|
|
||||||
|
// thats something like public://2022-01/bla.tar.gz
|
||||||
|
$local_fileuri = $file->getFileUri();
|
||||||
|
|
||||||
|
// we do a switch for compressed file formats because they are handled otherwise.
|
||||||
|
if($extension == "tar" || $extension == "zip" || $extension == "gz" || $extension == "xz" || $extension == "rar") {
|
||||||
|
$local_fileuri = str_replace("." . $extension, "_" . strtoupper($extension), $local_fileuri);
|
||||||
|
|
||||||
|
$local_fileuri = $local_fileuri . "/gltf/" . $local_filename . ".glb";
|
||||||
|
|
||||||
|
} if($extension == "glb" || $extension == "gltf") {
|
||||||
|
// do nothing - just party :D
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$local_fileuri = str_replace($filename, "gltf/" . $filename . ".glb", $local_fileuri);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_link = $this->uriToUrl($local_fileuri, (string) ($cfg['main_url'] ?? ''));
|
||||||
|
|
||||||
|
$url = Url::fromUri($file_link);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$elements[$delta] = array(
|
||||||
|
'#type' => 'link',
|
||||||
|
'#title' => $file_link, //$file->getFilename(),
|
||||||
|
'#url' => $url,
|
||||||
|
);
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $elements;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function uriToUrl(string $uri, string $public_base_url = ''): ?string {
|
||||||
|
if ($uri === '') {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$public_base_url = trim($public_base_url);
|
||||||
|
$base_parts = parse_url($public_base_url);
|
||||||
|
$base_host = is_array($base_parts) ? (string) ($base_parts['host'] ?? '') : '';
|
||||||
|
$has_safe_base = is_array($base_parts)
|
||||||
|
&& !empty($base_parts['scheme'])
|
||||||
|
&& $base_host !== ''
|
||||||
|
&& strpos($base_host, '_') === FALSE
|
||||||
|
&& strtolower($base_host) !== 'default';
|
||||||
|
|
||||||
|
// Keep storage deterministic in CLI contexts (e.g. drush) where request
|
||||||
|
// host may resolve to container aliases like "dfg_3dviewer".
|
||||||
|
if (str_starts_with($uri, 'public://')) {
|
||||||
|
$relative_public = '/sites/default/files/' . ltrim(substr($uri, strlen('public://')), '/');
|
||||||
|
return $has_safe_base ? rtrim($public_base_url, '/') . $relative_public : $relative_public;
|
||||||
|
}
|
||||||
|
|
||||||
|
$generator = \Drupal::service('file_url_generator');
|
||||||
|
$relative = (string) $generator->generateString($uri);
|
||||||
|
$relative_parts = parse_url($relative);
|
||||||
|
$relative_host = is_array($relative_parts) ? (string) ($relative_parts['host'] ?? '') : '';
|
||||||
|
$relative_path = is_array($relative_parts) ? (string) ($relative_parts['path'] ?? '') : '';
|
||||||
|
$relative_is_absolute = is_array($relative_parts) && !empty($relative_parts['scheme']) && $relative_host !== '';
|
||||||
|
$relative_has_bad_host = $relative_is_absolute && strpos($relative_host, '_') !== FALSE;
|
||||||
|
if (($relative_has_bad_host || strtolower($relative_host) === 'default')
|
||||||
|
&& str_starts_with($relative_path, '/sites/default/files/')) {
|
||||||
|
$relative = $relative_path;
|
||||||
|
}
|
||||||
|
if (!$relative_is_absolute && str_starts_with($relative, 'sites/default/files/')) {
|
||||||
|
$relative = '/' . ltrim($relative, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($public_base_url !== '' && $has_safe_base) {
|
||||||
|
if ($relative_is_absolute) {
|
||||||
|
if ($relative_path !== '' && str_starts_with($relative_path, '/sites/default/files/')) {
|
||||||
|
return rtrim($public_base_url, '/') . $relative_path;
|
||||||
|
}
|
||||||
|
return $relative;
|
||||||
|
}
|
||||||
|
return rtrim($public_base_url, '/') . '/' . ltrim($relative, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$absolute = $generator->generateAbsoluteString($uri);
|
||||||
|
$host = parse_url($absolute, PHP_URL_HOST);
|
||||||
|
if (is_string($host) && (strpos($host, '_') !== FALSE || strtolower($host) === 'default')) {
|
||||||
|
return $relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $absolute;
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
\Drupal::logger('dfg_3dviewer')->warning(
|
||||||
|
'Cannot build URL for URI "@uri": @msg',
|
||||||
|
[
|
||||||
|
'@uri' => $uri,
|
||||||
|
'@msg' => $e->getMessage(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function defaultSettings() {
|
||||||
|
return [
|
||||||
|
// 'wisski_inline' => 'FALSE',
|
||||||
|
] + parent::defaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||||
|
|
||||||
|
$element = parent::settingsForm($form, $form_state);
|
||||||
|
return $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function settingsSummary() {
|
||||||
|
return parent::settingsSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/Plugin/Field/FieldFormatter/DFG3DViewerFormatter.php
Executable file
269
src/Plugin/Field/FieldFormatter/DFG3DViewerFormatter.php
Executable file
|
|
@ -0,0 +1,269 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Definition of Drupal\dfg_3dviewer\Plugin\field\formatter\DFG3DViewerFormatter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Plugin\Field\FieldFormatter;
|
||||||
|
|
||||||
|
use Drupal\Core\Entity\EntityStorageInterface;
|
||||||
|
use Drupal\Core\Field\FieldItemListInterface;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
use Drupal\Core\Link;
|
||||||
|
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||||
|
use Drupal\Core\Session\AccountInterface;
|
||||||
|
use Drupal\Core\Url;
|
||||||
|
use Drupal\image\Entity\ImageStyle;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
|
use Drupal\Core\Cache\Cache;
|
||||||
|
use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatterBase;
|
||||||
|
use Drupal\colorbox\Plugin\Field\FieldFormatter\ColorboxFormatter;
|
||||||
|
use Drupal\Core\Template\Attribute;
|
||||||
|
use Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin implementation of the 'wisski_iip_image' formatter.
|
||||||
|
*
|
||||||
|
* @FieldFormatter(
|
||||||
|
* id = "dfg_3dviewer",
|
||||||
|
* module = "dfg_3dviewer",
|
||||||
|
* label = @Translation("DFG 3D Viewer"),
|
||||||
|
* field_types = {
|
||||||
|
* "file"
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
# class WisskiIIPImageFormatter extends ImageFormatterBase {
|
||||||
|
class DFG3DViewerFormatter extends FileFormatterBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||||
|
// $elements = parent::viewElements($items, $langcode);
|
||||||
|
$elements = array();
|
||||||
|
$entity = $items->getEntity();
|
||||||
|
|
||||||
|
// By Mark:
|
||||||
|
// get the derivative field id
|
||||||
|
// here must be some handling if this is empty.
|
||||||
|
$derivative_field_id = \Drupal::service('config.factory')->getEditable('dfg_3dviewer.settings')->get('dfg_3dviewer_viewer_file_name');
|
||||||
|
|
||||||
|
$derivative_paths = array();
|
||||||
|
if (!empty($derivative_field_id) && $entity->hasField($derivative_field_id)) {
|
||||||
|
$derivative_paths = $this->extractViewerPathsFromFieldValues($entity->get($derivative_field_id)->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have derivative values, act on that and not on the real values.
|
||||||
|
if(!empty($derivative_paths)) {
|
||||||
|
$elements = array();
|
||||||
|
|
||||||
|
$elements['#attached']['library'][] = dfg_3dviewer_get_library();
|
||||||
|
dfg_3dviewer_attach_settings($elements);
|
||||||
|
$container_id = \Drupal::config('dfg_3dviewer.settings')->get('dfg_3dviewer_container') ?: 'DFG_3DViewer';
|
||||||
|
\Drupal::logger('dfg_3dviewer')->notice(
|
||||||
|
'Viewer formatter uses converted model from field "@field" for entity type "@type" id "@id".',
|
||||||
|
[
|
||||||
|
'@field' => (string) $derivative_field_id,
|
||||||
|
'@type' => method_exists($entity, 'getEntityTypeId') ? (string) $entity->getEntityTypeId() : '',
|
||||||
|
'@id' => method_exists($entity, 'id') ? (string) $entity->id() : '',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach($derivative_paths as $delta => $resolved_path) {
|
||||||
|
$elements[$delta] = array(
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'p',
|
||||||
|
'#attributes' => array('id' => $container_id, '3d' => $resolved_path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$files = $this->getEntitiesToView($items, $langcode);
|
||||||
|
|
||||||
|
$elements['#attached']['library'][] = dfg_3dviewer_get_library();
|
||||||
|
dfg_3dviewer_attach_settings($elements);
|
||||||
|
$container_id = \Drupal::config('dfg_3dviewer.settings')->get('dfg_3dviewer_container') ?: 'DFG_3DViewer';
|
||||||
|
\Drupal::logger('dfg_3dviewer')->notice(
|
||||||
|
'Viewer formatter falls back to original upload field "@field" for entity type "@type" id "@id".',
|
||||||
|
[
|
||||||
|
'@field' => (string) $items->getName(),
|
||||||
|
'@type' => method_exists($entity, 'getEntityTypeId') ? (string) $entity->getEntityTypeId() : '',
|
||||||
|
'@id' => method_exists($entity, 'id') ? (string) $entity->id() : '',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($files as $delta => $file) {
|
||||||
|
$generator = \Drupal::service('file_url_generator');
|
||||||
|
$relative_path = (string) $generator->generateString($file->getFileUri());
|
||||||
|
$absolute_path = (string) $generator->generateAbsoluteString($file->getFileUri());
|
||||||
|
|
||||||
|
$base = trim((string) \Drupal::service('config.factory')
|
||||||
|
->getEditable('dfg_3dviewer.settings')
|
||||||
|
->get('dfg_3dviewer_basenamespace'));
|
||||||
|
$base_parts = parse_url($base);
|
||||||
|
$base_host = is_array($base_parts) ? (string) ($base_parts['host'] ?? '') : '';
|
||||||
|
$has_safe_base = is_array($base_parts)
|
||||||
|
&& !empty($base_parts['scheme'])
|
||||||
|
&& $base_host !== ''
|
||||||
|
&& strpos($base_host, '_') === FALSE
|
||||||
|
&& strtolower($base_host) !== 'default';
|
||||||
|
|
||||||
|
if ($has_safe_base) {
|
||||||
|
$override_basenamespace = rtrim($base, '/') . '/' . ltrim($relative_path, '/');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$absolute_host = (string) parse_url($absolute_path, PHP_URL_HOST);
|
||||||
|
$override_basenamespace = ((strpos($absolute_host, '_') !== FALSE) || strtolower($absolute_host) === 'default')
|
||||||
|
? $relative_path
|
||||||
|
: $absolute_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
$elements[$delta] = array(
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'p',
|
||||||
|
'#attributes' => array('id' => $container_id, '3d' => $override_basenamespace),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $elements;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function defaultSettings() {
|
||||||
|
return [
|
||||||
|
// 'wisski_inline' => 'FALSE',
|
||||||
|
] + parent::defaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||||
|
|
||||||
|
/* $element['wisski_inline'] = [
|
||||||
|
'#type' => 'checkbox',
|
||||||
|
'#title' => $this->t('Inline mode for IIP'),
|
||||||
|
'#default_value' => $this->getSetting('wisski_inline'),
|
||||||
|
];
|
||||||
|
*/
|
||||||
|
// $element = $element + parent::settingsForm($form, $form_state);
|
||||||
|
$element = parent::settingsForm($form, $form_state);
|
||||||
|
return $element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function settingsSummary() {
|
||||||
|
return parent::settingsSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveViewerPath(string $value): string {
|
||||||
|
$value = trim($value);
|
||||||
|
if ($value === '') {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('#^https?://#i', $value)) {
|
||||||
|
$host = (string) parse_url($value, PHP_URL_HOST);
|
||||||
|
$path = (string) parse_url($value, PHP_URL_PATH);
|
||||||
|
if (
|
||||||
|
$host !== ''
|
||||||
|
&& (strpos($host, '_') !== FALSE || strtolower($host) === 'default')
|
||||||
|
&& str_starts_with($path, '/sites/default/files/')
|
||||||
|
) {
|
||||||
|
$cfg = \Drupal::config('dfg_3dviewer.settings');
|
||||||
|
$main_url = trim((string) ($cfg->get('dfg_3dviewer_main_url') ?? $cfg->get('main_url') ?? ''));
|
||||||
|
$main_parts = parse_url($main_url);
|
||||||
|
$main_host = is_array($main_parts) ? (string) ($main_parts['host'] ?? '') : '';
|
||||||
|
$has_safe_main = is_array($main_parts)
|
||||||
|
&& !empty($main_parts['scheme'])
|
||||||
|
&& $main_host !== ''
|
||||||
|
&& strpos($main_host, '_') === FALSE;
|
||||||
|
return $has_safe_main ? rtrim($main_url, '/') . $path : $path;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($value, '/')) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $value)) {
|
||||||
|
try {
|
||||||
|
return \Drupal::service('file_url_generator')->generateString($value);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
\Drupal::logger('dfg_3dviewer')->warning(
|
||||||
|
'Could not resolve stream wrapper URI "@value" for viewer path: @msg',
|
||||||
|
[
|
||||||
|
'@value' => $value,
|
||||||
|
'@msg' => $e->getMessage(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($value, 'sites/default/files/')) {
|
||||||
|
return '/' . ltrim($value, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractViewerPathsFromFieldValues(array $values): array {
|
||||||
|
$paths = array();
|
||||||
|
|
||||||
|
foreach ($values as $delta => $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolved = $this->resolveViewerFieldRowPath($row);
|
||||||
|
if ($resolved === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$paths[$delta] = $resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveViewerFieldRowPath(array $row): string {
|
||||||
|
if (!empty($row['target_id']) && ctype_digit((string) $row['target_id'])) {
|
||||||
|
$file = \Drupal\file\Entity\File::load((int) $row['target_id']);
|
||||||
|
if ($file) {
|
||||||
|
try {
|
||||||
|
$generated = (string) \Drupal::service('file_url_generator')->generateString($file->getFileUri());
|
||||||
|
return $this->resolveViewerPath($generated);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
\Drupal::logger('dfg_3dviewer')->warning(
|
||||||
|
'Could not resolve target_id "@target_id" for viewer formatter: @msg',
|
||||||
|
[
|
||||||
|
'@target_id' => (string) $row['target_id'],
|
||||||
|
'@msg' => $e->getMessage(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['value', 'uri'] as $key) {
|
||||||
|
$candidate = trim((string) ($row[$key] ?? ''));
|
||||||
|
if ($candidate !== '') {
|
||||||
|
return $this->resolveViewerPath($candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
2394
src/Plugin/QueueWorker/ConvertWorker.php
Normal file
2394
src/Plugin/QueueWorker/ConvertWorker.php
Normal file
File diff suppressed because it is too large
Load diff
301
src/Service/ConvertProcessService.php
Normal file
301
src/Service/ConvertProcessService.php
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Service;
|
||||||
|
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
|
||||||
|
|
||||||
|
class ConvertProcessService {
|
||||||
|
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
|
public function __construct(LoggerChannelFactoryInterface $logger_factory) {
|
||||||
|
$this->logger = $logger_factory->get('dfg_3dviewer');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function boolToString($value): string {
|
||||||
|
return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePath(string $path): string {
|
||||||
|
return rtrim(str_replace('\\', '/', $path), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveConvertedOutputPath(string $inputPath, array $options): string {
|
||||||
|
$isBinary = filter_var($options['b'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||||
|
$outputExt = $isBinary ? 'glb' : 'gltf';
|
||||||
|
$inputExt = strtolower((string) pathinfo($inputPath, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if (!empty($options['o'])) {
|
||||||
|
$outputBase = $this->normalizePath((string) $options['o']);
|
||||||
|
$inputBase = (string) pathinfo($inputPath, PATHINFO_FILENAME);
|
||||||
|
return $outputBase . '/gltf/' . $inputBase . '.' . $outputExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inputExt === 'glb' && $outputExt === 'glb') {
|
||||||
|
return $inputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dirname = $this->normalizePath((string) pathinfo($inputPath, PATHINFO_DIRNAME));
|
||||||
|
$filename = (string) pathinfo($inputPath, PATHINFO_FILENAME);
|
||||||
|
return $dirname . '/gltf/' . $filename . '.' . $outputExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveThumbnailBasePath(string $inputPath): string {
|
||||||
|
$dirname = $this->normalizePath((string) pathinfo($inputPath, PATHINFO_DIRNAME));
|
||||||
|
$filename = (string) pathinfo($inputPath, PATHINFO_FILENAME);
|
||||||
|
$extension = strtolower((string) pathinfo($inputPath, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
return $dirname . '/views/' . $filename . '.' . $extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function thumbnailsAlreadyExist(string $inputPath): bool {
|
||||||
|
$basePath = $this->resolveThumbnailBasePath($inputPath);
|
||||||
|
$requiredFiles = [
|
||||||
|
$basePath . '_side45.png',
|
||||||
|
$basePath . '_side90.png',
|
||||||
|
$basePath . '_side135.png',
|
||||||
|
$basePath . '_side180.png',
|
||||||
|
$basePath . '_side225.png',
|
||||||
|
$basePath . '_side270.png',
|
||||||
|
$basePath . '_side315.png',
|
||||||
|
$basePath . '_top.png',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($requiredFiles as $file) {
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($basePath . '_side0.png') || file_exists($basePath . '_RENDER.png')) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function emitProgress(?callable $onProgress, int $percent, string $state, string $message): void {
|
||||||
|
if ($onProgress === NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$onProgress($percent, $state, $message);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
$this->logger->warning(
|
||||||
|
'Progress callback failed at @percent% (@state): @message',
|
||||||
|
[
|
||||||
|
'@percent' => $percent,
|
||||||
|
'@state' => $state,
|
||||||
|
'@message' => $e->getMessage(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run convert.sh process.
|
||||||
|
*
|
||||||
|
* @param string $spath
|
||||||
|
* @param string $inputPath
|
||||||
|
* @param int $lightweight
|
||||||
|
* @param array $options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function run(
|
||||||
|
string $spath,
|
||||||
|
string $inputPath,
|
||||||
|
int $lightweight = 0,
|
||||||
|
array $options = [],
|
||||||
|
?callable $onProgress = NULL
|
||||||
|
) : array {
|
||||||
|
|
||||||
|
$script = $spath . '/scripts/convert.sh';
|
||||||
|
|
||||||
|
if (!file_exists($script)) {
|
||||||
|
return [
|
||||||
|
'success' => FALSE,
|
||||||
|
'exit_code' => NULL,
|
||||||
|
'output' => '',
|
||||||
|
'error' => 'Script not found',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
$script,
|
||||||
|
'-t', $this->boolToString($lightweight),
|
||||||
|
'-c', $this->boolToString($options['c'] ?? true),
|
||||||
|
'-l', $options['l'] ?? '3',
|
||||||
|
'-b', $this->boolToString($options['b'] ?? true),
|
||||||
|
'-i', $inputPath,
|
||||||
|
];
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if (!empty($options['o'])) {
|
||||||
|
$args[] = '-o';
|
||||||
|
$args[] = $options['o'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$args[] = '-f';
|
||||||
|
$args[] = $this->boolToString($options['f'] ?? true);
|
||||||
|
|
||||||
|
if (isset($options['a'])) {
|
||||||
|
$args[] = '-a';
|
||||||
|
$args[] = $options['a'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$process = new \Symfony\Component\Process\Process($args);
|
||||||
|
$process->setTimeout($options['timeout'] ?? 600);
|
||||||
|
$process->setWorkingDirectory($spath);
|
||||||
|
|
||||||
|
$this->emitProgress($onProgress, 35, 'processing', 'Converting to GLTF...');
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
$success = $process->isSuccessful();
|
||||||
|
$exitCode = $process->getExitCode();
|
||||||
|
$output = $process->getOutput();
|
||||||
|
$error = $process->getErrorOutput();
|
||||||
|
$renderResult = NULL;
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$this->emitProgress($onProgress, 55, 'converted', 'GLTF conversion finished.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success && !filter_var($lightweight, FILTER_VALIDATE_BOOLEAN)) {
|
||||||
|
if ($this->thumbnailsAlreadyExist($inputPath)) {
|
||||||
|
$this->emitProgress($onProgress, 75, 'rendering', 'Skipping thumbnail generation, files already exist.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->emitProgress($onProgress, 65, 'rendering', 'Generating thumbnails...');
|
||||||
|
$renderResult = $this->render(
|
||||||
|
$spath,
|
||||||
|
$inputPath,
|
||||||
|
[
|
||||||
|
'a' => $this->boolToString($options['a'] ?? false),
|
||||||
|
'g' => $this->resolveConvertedOutputPath($inputPath, $options),
|
||||||
|
'timeout' => $options['render_timeout'] ?? $options['timeout'] ?? 600,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$output .= $renderResult['output'] ?? '';
|
||||||
|
$error .= $renderResult['error'] ?? '';
|
||||||
|
|
||||||
|
if (!($renderResult['success'] ?? FALSE)) {
|
||||||
|
$success = FALSE;
|
||||||
|
$exitCode = $renderResult['exit_code'] ?? 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->emitProgress($onProgress, 75, 'rendering', 'Thumbnails generated.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => $success,
|
||||||
|
'exit_code' => $exitCode,
|
||||||
|
'output' => $output,
|
||||||
|
'error' => $error,
|
||||||
|
'command' => $process->getCommandLine(),
|
||||||
|
'render' => $renderResult,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run render.sh process.
|
||||||
|
*
|
||||||
|
* @param string $spath
|
||||||
|
* @param string $inputPath
|
||||||
|
* @param array $options
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function render(
|
||||||
|
string $spath,
|
||||||
|
string $inputPath,
|
||||||
|
array $options = []
|
||||||
|
) : array {
|
||||||
|
$script = $spath . '/scripts/render.sh';
|
||||||
|
|
||||||
|
if (!file_exists($script)) {
|
||||||
|
return [
|
||||||
|
'success' => FALSE,
|
||||||
|
'exit_code' => NULL,
|
||||||
|
'output' => '',
|
||||||
|
'error' => 'Script not found',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
$script,
|
||||||
|
'-i', $inputPath,
|
||||||
|
'-a', $this->boolToString($options['a'] ?? false),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($options['g'])) {
|
||||||
|
$args[] = '-g';
|
||||||
|
$args[] = $options['g'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$process = new \Symfony\Component\Process\Process($args);
|
||||||
|
$process->setTimeout($options['timeout'] ?? 600);
|
||||||
|
$process->setWorkingDirectory($spath);
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => $process->isSuccessful(),
|
||||||
|
'exit_code' => $process->getExitCode(),
|
||||||
|
'output' => $process->getOutput(),
|
||||||
|
'error' => $process->getErrorOutput(),
|
||||||
|
'command' => $process->getCommandLine(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uncompress(
|
||||||
|
string $spath,
|
||||||
|
string $type,
|
||||||
|
string $inputPath,
|
||||||
|
string $outputPath,
|
||||||
|
string $name,
|
||||||
|
array $options = []
|
||||||
|
) : array {
|
||||||
|
|
||||||
|
$script = $spath . '/scripts/uncompress.sh';
|
||||||
|
|
||||||
|
if (!file_exists($script)) {
|
||||||
|
return [
|
||||||
|
'success' => FALSE,
|
||||||
|
'exit_code' => NULL,
|
||||||
|
'output' => '',
|
||||||
|
'error' => 'Script not found',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
$script,
|
||||||
|
'-t', $type,
|
||||||
|
'-i', $inputPath,
|
||||||
|
'-o', $outputPath,
|
||||||
|
'-n', $name,
|
||||||
|
];
|
||||||
|
|
||||||
|
$process = new \Symfony\Component\Process\Process($args);
|
||||||
|
$process->setTimeout($options['timeout'] ?? 600);
|
||||||
|
$process->setWorkingDirectory($spath);
|
||||||
|
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => $process->isSuccessful(),
|
||||||
|
'exit_code' => $process->getExitCode(),
|
||||||
|
'output' => $process->getOutput(),
|
||||||
|
'error' => $process->getErrorOutput(),
|
||||||
|
'command' => $process->getCommandLine(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
28
src/Service/ModelFormatManager.php
Normal file
28
src/Service/ModelFormatManager.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\dfg_3dviewer\Service;
|
||||||
|
|
||||||
|
class ModelFormatManager {
|
||||||
|
|
||||||
|
protected array $allowedModelFormats = [
|
||||||
|
'abc', 'obj', 'fbx', 'ply', 'dae', 'ifc',
|
||||||
|
'stl', 'xyz', 'pcd', 'json', '3ds',
|
||||||
|
'blend', 'gml', 'wrl', 'glb', 'gltf'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $zipFormats = [
|
||||||
|
'zip', 'rar', 'tar', 'xz', 'gz'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getAllowedModelFormats(): array {
|
||||||
|
return $this->allowedModelFormats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getZipFormats(): array {
|
||||||
|
return $this->zipFormats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllFormats(): array {
|
||||||
|
return array_merge($this->allowedModelFormats, $this->zipFormats);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue