mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05cd11ae12 | |||
| 307f9a2322 | |||
| 3d05dc234a | |||
| 1f87165601 | |||
| a513e1b56a | |||
| ec0c8f509e | |||
| a23bac6ecb | |||
| 569c19abca | |||
| dfeac601e4 | |||
| 53cecb3546 | |||
| cf87852d9c | |||
| 7da3d9ee70 | |||
| 9650bbb5ee | |||
| 45953c6b49 | |||
| b34caa1b5f | |||
| ea8433b187 | |||
| 390f23a30c | |||
| c8ba2b53c9 | |||
| dbbbb52e9e | |||
| 052d6e2254 | |||
| b05c711fb9 | |||
| eb0322759c | |||
| 25a7a95284 | |||
| 8ec1b7bb3e | |||
| 5f094f6a95 | |||
| f0b1935861 | |||
| 2540ba3467 | |||
| 88093728a7 | |||
| af6734d7b0 | |||
| 8a108f37c3 | |||
| d88510b919 | |||
| afe9f7602f | |||
| ff2b79a41f | |||
| 631722303f | |||
| 4b20a407e9 | |||
| b23f445946 | |||
| 09c8268494 | |||
| 0bc3543914 | |||
| 0412eb48a0 | |||
| e7d5add13d | |||
| 25948a608a | |||
| 11f772b5a3 | |||
| 412cf7c6cb | |||
| b2d5580c37 | |||
| 8386aa0e0a | |||
| ae0f2a9de6 | |||
| 4e0a19e013 | |||
| 9a840174d2 | |||
| 83528ba80a | |||
| eb23372fd0 | |||
| 286e9dfda9 | |||
| f0b9c9ad7b | |||
| afd79ee16d | |||
| caa4445eb0 | |||
| f850bd5741 | |||
| 8c5883ffaa | |||
| 8f8940de2a | |||
| bf59871db5 | |||
| 22eb0bf2e4 | |||
| 68838a37e5 | |||
| 22b4bfe07d | |||
| ff2714d991 | |||
| 308ee7f7a9 | |||
| c87924c4ba | |||
| 8f33c2ca9d | |||
| 9351038009 | |||
| 66281ec1f1 | |||
| ae843e6faf | |||
| 5ca0a254ff | |||
| c18458b3db | |||
| 3736a8b4b4 | |||
| 6558f68cd4 | |||
| 4fd5806f70 | |||
| b1c730c04d | |||
| bdec443410 | |||
| d9afb0f8a2 | |||
| 9dfd7e03f7 | |||
| 31b633b8b6 | |||
| 20f4737ffd | |||
| 4d4ab63404 | |||
| 056f7610c1 | |||
| bfb2f06e44 | |||
| a7febaa9b0 | |||
| ff984f62af | |||
| d005524c4c | |||
| a2362668a3 | |||
| 277317e6fd | |||
| b3f0b9b37b | |||
| 6de1e73e4f | |||
| c6cdf4ab77 | |||
| 21f8aca00f | |||
| 1d6c3b3fc2 | |||
| 4362163ea9 | |||
| 67808b8384 | |||
| 2cbc7a10ab | |||
| d97594df96 | |||
| c7eca135de | |||
| e63ec83325 | |||
| 1ddae8a6f5 | |||
| 03781bdf9b | |||
| 62e5c3684b | |||
| ff90728a22 | |||
| dadf23681b | |||
| 516bd37518 | |||
| 521ec1b124 | |||
| 4881e4c686 | |||
| a57cac5f9c | |||
| ca98e77b40 | |||
| cbd9c898c3 | |||
| f57731a10a | |||
| 0142bface5 | |||
| 5acbf4658f | |||
| 32e00f6d49 | |||
| 4e3ce192cc | |||
| 841bb59d18 | |||
| 8dd8a23af9 | |||
| adc59de2bf | |||
| 20198bccdb | |||
| c0909dace0 | |||
| bd054d4baf | |||
| 35a70a4ae3 | |||
| a1d30d157b | |||
| 621d89c80e | |||
| 2fa3793622 | |||
| 872c057c4f | |||
| f6a530e5e3 | |||
| 2043a629c7 | |||
| a7f9d548cf | |||
| 1391dd38ec | |||
| 38f6b7aca0 | |||
| f0e4397ef9 | |||
| 6b7ffc8ca6 | |||
| 5458f4f9dd | |||
| 0c3269d9e1 | |||
| 84dde4c5c0 | |||
| 7eefb3f62f | |||
| 90369bf7f7 | |||
| eff3318399 | |||
| ee59d366dc | |||
| c0b91848a3 | |||
| d05d430642 | |||
| e8a73e008e | |||
| 259bb4f36a | |||
| b87191a358 | |||
| a359230181 | |||
| a95e12c0b2 | |||
| 2ac27c6fdd | |||
| 757578a74b | |||
| 3466a38a44 | |||
| f07cf1d1ef | |||
| 7fb4cdaa16 | |||
| 427411e758 | |||
| cc056d5d53 | |||
| 31600033bf | |||
| 0338825175 | |||
| d9f8aa4e7b | |||
| 6803788676 | |||
| d421cc0bbb | |||
| bf57c8b1bf | |||
| 9a25db6584 | |||
| 17ccd0adf7 | |||
| 937f39c898 | |||
| 0b5ccd88a5 | |||
| ebf3cd8909 | |||
| d9b1c26bf0 | |||
| 490da99792 | |||
| 528df00027 | |||
| bc10f934d6 | |||
| 819db11db4 | |||
| 37a0854d40 | |||
| b984268b5d | |||
| e4d2ae185d | |||
| 345098b4af | |||
| 98b517954f | |||
| dba8d2df9d | |||
| 6756ba7022 | |||
| 08afec3461 | |||
| 4408a23873 | |||
| c33b5aa4b7 | |||
| 0ebbc62a06 | |||
| 0494373155 | |||
| c897cb3ea0 | |||
| f38e8f26f5 | |||
| 7164103a80 | |||
| 5fb7f33fb0 | |||
| e602f16ed3 | |||
| 17a872a8d8 | |||
| 4533b74d65 | |||
| 62229b032e | |||
| f2c2fa8dce | |||
| ae170bbfac | |||
| 706b925f4c | |||
| 65288eac04 | |||
| bf5a9bdac6 | |||
| e22ebfbf94 | |||
| fb2989c17f | |||
| ffbf6483b5 | |||
| 5c5488fa49 | |||
| 7ff1b8a34d | |||
| fb786bb90c | |||
| fad80eacaf | |||
| 84020c9832 | |||
| 59bce590cc | |||
| a7ae78ec2a | |||
| 523a97329e |
@@ -19,20 +19,19 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: '3.10'
|
||||
- name: Build package
|
||||
run: |
|
||||
cd src/bcf
|
||||
pip install build
|
||||
python -m build
|
||||
- name: Test
|
||||
run: |
|
||||
cd src/bcf
|
||||
make test
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
packages_dir: src/bcf/dist
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.7.7' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
python-version: '3.10.9' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
- run: echo ${{ env.DATE }}
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.7.7' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
- run: echo ${{ env.DATE }}
|
||||
- name: Get current date
|
||||
|
||||
@@ -26,16 +26,17 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [
|
||||
{ name: py39, distver: '3.9' },
|
||||
{ name: py310, distver: '3.10'}
|
||||
# { name: py39, distver: '3.9' },
|
||||
{ name: py310, distver: '3.10'},
|
||||
{ name: py311, distver: '3.11'}
|
||||
]
|
||||
platform: [
|
||||
{ name: Windows, distver: windows-2022, upload: 'false' },
|
||||
{ name: Windows, distver: windows-2019, upload: 'true' },
|
||||
{ name: Linux, distver: ubuntu-20.04, upload: 'false' },
|
||||
{ name: Linux, distver: ubuntu-18.04, upload: 'true' },
|
||||
{ name: Linux, distver: ubuntu-20.04, upload: 'true' },
|
||||
{ name: Linux, distver: ubuntu-22.04, upload: 'false' },
|
||||
{ name: macOS, distver: macos-11, upload: 'true' },
|
||||
{ name: macOS, distver: macos-10.15, upload: 'false' }
|
||||
{ name: macOS, distver: macos-12, upload: 'false' }
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
+14
@@ -73,3 +73,17 @@ _build/
|
||||
|
||||
# IDS Docs
|
||||
src/ifcopenshell-python/test/build
|
||||
|
||||
# tox cache
|
||||
.tox/
|
||||
*.egg-info/
|
||||
|
||||
# mypy cache
|
||||
.mypy_cache
|
||||
|
||||
# blenderbim libs
|
||||
src/blenderbim/blenderbim/libs
|
||||
|
||||
# ifcopenshell swig and compiled files
|
||||
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
|
||||
|
||||
@@ -1,325 +1,74 @@
|
||||
|
||||
IfcOpenShell
|
||||
============
|
||||
IfcOpenShell is an open source ([LGPL]) software library for working with the Industry Foundation Classes ([IFC])
|
||||
file format. Extensive geometric support is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1].
|
||||
Support for parsing is provided for IFC4x1, IFC4x2, and the IFC4x3 release candidates. Extending with support for
|
||||
arbitrary IFC schemas is possible at compile-time when using C++ and at run-time when using Python.
|
||||
|
||||
For more information, see
|
||||
* [http://ifcopenshell.org](http://ifcopenshell.org)
|
||||
* [http://academy.ifcopenshell.org](http://academy.ifcopenshell.org)
|
||||
IfcOpenShell is an open source ([LGPL]) software library for working with Industry Foundation Classes ([IFC]). Complete
|
||||
parsing support is provided for [IFC2x3 TC1], [IFC4 Add2 TC1], IFC4x1, IFC4x3, and IFC4x3. Extensive geometric support
|
||||
is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending with support for arbitrary IFC schemas
|
||||
is possible at compile-time when using C++ and at run-time when using Python.
|
||||
|
||||
In addition to a C++ and Python API, IfcOpenShell comes with an ecosystem of tools, notably including IfcConvert (an application to convert IFC models to
|
||||
other formats), the BlenderBIM Add-on (an add-on to Blender providing a graphical IFC authoring platform), and many
|
||||
other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF and IDS.
|
||||
|
||||
For more information, see:
|
||||
|
||||
* [IfcOpenShell Website](http://ifcopenshell.org)
|
||||
* [IfcOpenShell Documentation](http://blenderbim.org/docs-python)
|
||||
* [IfcOpenShell C++ Installation](https://blenderbim.org/docs-python/ifcopenshell/installation.html)
|
||||
* [IfcOpenShell Python Installation](https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
|
||||
* [IfcOpenShell Python Hello World Tutorial](https://blenderbim.org/docs-python/ifcopenshell-python/hello_world.html)
|
||||
* [BlenderBIM Add-on Website](https://blenderbim.org)
|
||||
* [BlenderBIM Add-on Documentation](http://blenderbim.org/docs)
|
||||
* [Add-on Installation](https://blenderbim.org/docs/users/installation.html)
|
||||
* [Exploring an IFC model](https://blenderbim.org/docs/users/exploring_an_ifc_model.html)
|
||||
|
||||
| Service | Status |
|
||||
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Anaconda Daily Build | [](https://anaconda.org/ifcopenshell/ifcopenshell) |
|
||||
| Anaconda v0.7.0 Stable | [](https://anaconda.org/conda-forge/ifcopenshell) |
|
||||
| PyPi Daily Build | [](https://pypi.org/project/ifcopenshell/) |
|
||||
| ArchLinux AUR Package | [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||
| ArchLinux AUR Package Stable | [](https://aur.archlinux.org/packages/ifcopenshell) |
|
||||
| ArchLinux AUR Package git | [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||
| BlenderBIM Add-on Chocolatey (under moderation) | [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| Sponsor development on OpenCollective | [](https://opencollective.com/opensourcebim/) |
|
||||
| Docker hub | [](https://hub.docker.com/r/aecgeeks/ifcopenshell) |
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
* Git
|
||||
* CMake (3.1.3 or newer)
|
||||
* Windows: [Visual Studio] 2008 to 2019 (2022 not yet supported by dependency CMake) with C++ toolset (or [Visual C++ Build Tools]) or [MSYS2] + MinGW
|
||||
* *nix: GCC 4.7 or newer, or Clang (any version)
|
||||
|
||||
Dependencies
|
||||
-------------
|
||||
* [Boost](http://www.boost.org/)
|
||||
* [Open Cascade](https://dev.opencascade.org/) - *optional*, but required for building IfcGeom
|
||||
([official](https://dev.opencascade.org/release), "OCCT", or [community edition](https://github.com/tpaviot/oce), "OCE")
|
||||
For converting IFC representation items into BRep solids and tessellated meshes
|
||||
* [OpenCOLLADA](https://github.com/khronosGroup/OpenCOLLADA/) - *optional*
|
||||
For IfcConvert to be able to write tessellated Collada (.dae) files
|
||||
* [SWIG](http://www.swig.org/) and [Python](https://www.python.org/) - *optional*
|
||||
For building the IfcOpenShell Python interface and the Blender add-on
|
||||
* [3ds Max SDK](http://www.autodesk.com/products/3ds-max/free-trial) - *optional*
|
||||
For building the 3ds Max plug-in.
|
||||
All recent versions of 3ds Max (2014 and newer) are 64-bit only, so a 64-bit installation is assumed.
|
||||
|
||||
Building IfcOpenShell
|
||||
---------------------
|
||||
|
||||
Note 1: The path where the source code is cloned to can contain spaces but non-ASCII characters are very likely to cause problems with the build.
|
||||
|
||||
Note 2: If you had not used `git clone --recursive https://github.com/IfcOpenShell/IfcOpenshell.git`, update the submodules by running `git submodule init & git submodule update`.
|
||||
|
||||
Note 3: Be careful with special characters is the path when using the nix or win build scripts, because the OpenCASCADE build will fail on paths containing ++ and likely other situations.
|
||||
|
||||
### Compiling on Windows
|
||||
The preferred way to fetch and build this project's dependencies is to use the build scripts
|
||||
in win/ folder. **See [win/readme.md] for more information**.
|
||||
|
||||
#### Using Visual Studio
|
||||
Instructions in a nutshell (**assuming Visual Studio 2015 x64 environment variables set**):
|
||||
|
||||
> cd IfcOpenShell\win
|
||||
> build-deps.cmd
|
||||
> run-cmake.bat
|
||||
|
||||
NB: `build-deps.cmd` need to be ran from the directory containing it, i.e. the `./win` folder.
|
||||
|
||||
You can now open and build the solution file in Visual Studio:
|
||||
|
||||
> ..\build-vs2015-x64\IfcOpenShell.sln
|
||||
|
||||
As the scripts default to using the `RelWithDebInfo` configuration, and a freshly created solution by CMake defaults
|
||||
to `Debug`, make sure to switch the used build configuration. Build the `INSTALL` project (right-click -> Project
|
||||
Only) to deploy the headers and binaries into a single location if wanted/needed.
|
||||
|
||||
Alternatively, one can use the utility batch file(s) to build and install the project easily from the command-line
|
||||
(installing a project will build it also, if required):
|
||||
|
||||
> install-ifcopenshell.bat
|
||||
|
||||
#### Using MSYS2 + MinGW
|
||||
|
||||
Start the MSYS2 Shell and then:
|
||||
|
||||
$ cd IfcOpenShell/win
|
||||
$ ./build-deps.sh
|
||||
$ ./run-cmake.sh
|
||||
$ ./install-ifcopenshell.sh
|
||||
|
||||
#### Using Bash on Ubuntu on Windows
|
||||
|
||||
Start Bash on Ubuntu on Windows and follow the instructions below. Compiling on Ubuntu 14.04.4 LTS using GCC 4.8.4
|
||||
or Clang 3.5 has been confirmed to work.
|
||||
|
||||
### Compiling on *nix
|
||||
|
||||
The following instructions are for Ubuntu, modify as required for other operating systems. [nix/build-all.py] script
|
||||
can be experimented with and studied for pointers for other operating systems, but note that this script is not currently
|
||||
meant to be used for a typical IfcOpenShell workspace setup.
|
||||
|
||||
Note 1: It is recommended to use OCCT for IfcOpenShell. You could use OCE as well, but sometimes it may lag behind OCCT and
|
||||
therefore not compile with the latest IfcOpenShell.
|
||||
|
||||
Note 2: where `make -j` is written, add a number roughly equal to the amount of CPU cores + 1.
|
||||
|
||||
**1)** Install most of the prerequisites and dependencies:
|
||||
|
||||
$ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev
|
||||
|
||||
**2a)** Either use an OCCT package from your operating system's software repository
|
||||
|
||||
$ sudo apt-get install libocct-data-exchange-dev libocct-draw-dev libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev
|
||||
|
||||
**2b)** or, if OCCT is not available or the latest code is wanted, obtain and compile OCCT from https://dev.opencascade.org/release
|
||||
|
||||
**2c)** or, if you'd like to try using OCE, use the package from your operating system's software repository
|
||||
|
||||
$ sudo apt-get install liboce-foundation-dev liboce-modeling-dev liboce-ocaf-dev liboce-visualization-dev liboce-ocaf-lite-dev
|
||||
|
||||
**2d)** or if OCE is not available, or the latest code is wanted, compile OCE yourself (note that the build takes a long time)
|
||||
|
||||
$ sudo apt-get install libftgl-dev libtbb2 libtbb-dev libgl1-mesa-dev libfreetype6-dev
|
||||
$ git clone https://github.com/tpaviot/oce.git
|
||||
$ cd oce
|
||||
$ mkdir build && cd build
|
||||
$ cmake ..
|
||||
$ make -j
|
||||
$ sudo make install
|
||||
|
||||
**3)** For building IfcConvert with COLLADA (.dae) support (on by default), OpenCOLLADA is needed:
|
||||
|
||||
$ sudo apt-get install libpcre3-dev libxml2-dev
|
||||
$ git clone https://github.com/KhronosGroup/OpenCOLLADA.git
|
||||
$ cd OpenCOLLADA
|
||||
Using a known good revision, but HEAD should work too:
|
||||
$ git checkout 064a60b65c2c31b94f013820856bc84fb1937cc6
|
||||
$ mkdir build && cd build
|
||||
$ cmake ..
|
||||
$ make -j
|
||||
$ sudo make install
|
||||
|
||||
**4)** For building the IfcPython wrapper (on by default), SWIG and Python development are needed, if not already available:
|
||||
|
||||
$ sudo apt-get install python-all-dev swig
|
||||
|
||||
**5)** To build IfcOpenShell please take the following steps. Alternatively use environment variables for setting the
|
||||
dependencies' paths. `OCC_INCLUDE_DIR` might be needed to set also. `OPENCOLLADA_INCLUDE_DIR` and `OPENCOLLADA_LIBRARY_DIR`
|
||||
(and potentially `PCRE_LIBRARY_DIR`) are needed if building with COLLADA support. (`-DCOLLADA_SUPPORT=0` disables it).
|
||||
|
||||
$ cd /path/to/IfcOpenShell
|
||||
$ mkdir build && cd build
|
||||
$ cmake ../cmake -DOCC_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu/ \
|
||||
-DOPENCOLLADA_INCLUDE_DIR="/usr/local/include/opencollada" \
|
||||
-DOPENCOLLADA_LIBRARY_DIR="/usr/local/lib/opencollada" \
|
||||
-DPCRE_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu/ \
|
||||
-DCGAL_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_INCLUDE_DIR=/usr/include \
|
||||
-DMPFR_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DHDF5_SUPPORT=Off
|
||||
$ make -j
|
||||
|
||||
If all worked out correctly you can now use IfcOpenShell. See the examples below.
|
||||
|
||||
**6)** Install the project if wanted:
|
||||
|
||||
$ sudo make install
|
||||
|
||||
### Installing on MacOS Using Homebrew
|
||||
|
||||
**1)** Install all dependencies using [Homebrew](https://brew.sh/)
|
||||
|
||||
```{shell}
|
||||
$ brew install boost cmake python3 cgal ftgl gmp libaec opencascade swig hdf5 zlib
|
||||
# homebrew automatically links most libraries, except some keg-only ones
|
||||
$ brew link zlib --force
|
||||
```
|
||||
|
||||
**2)** Clone the git repo and its submodules
|
||||
```{shell}
|
||||
$ git clone --recurse-submodules https://github.com/IfcOpenShell/IfcOpenShell.git
|
||||
```
|
||||
**3)** Build IfcOpenShell with flags for Homebrew dependencies: (``/usr/local/``) for Intel machines with x84_64 architecture,
|
||||
(``/opt/homebrew/``) for Apple Silicon processors with arm64 architecture.
|
||||
```{shell}
|
||||
$ cd /path/to/IfcOpenShell
|
||||
$ mkdir build && cd build
|
||||
# set library flags
|
||||
$ export LDFLAGS="$LDFLAGS -Wl,-flat_namespace,-undefined,suppress"
|
||||
$ cmake ../cmake
|
||||
-DPYTHON_EXECUTABLE=/opt/homebrew/bin/python3.10 \
|
||||
-DPYTHON_LIBRARY=/opt/homebrew/opt/python@3.10/Frameworks/Python.framework/Versions/3.10/lib/libpython3.10.dylib \
|
||||
-DPYTHON_INCLUDE_DIR=/opt/homebrew/opt/python@3.10/Frameworks/Python.framework/Versions/3.10/include/python3.10/ \
|
||||
-DOCC_LIBRARY_DIR=/opt/homebrew/lib/ \
|
||||
-DOCC_INCLUDE_DIR=/opt/homebrew/include/opencascade/ \
|
||||
-DCGAL_INCLUDE_DIR=/opt/homebrew/include/ \
|
||||
-DGMP_LIBRARY_DIR=/opt/homebrew/lib/ \
|
||||
-DMPFR_LIBRARY_DIR=/opt/homebrew/lib/ \
|
||||
-DHDF5_LIBRARY_DIR=/opt/homebrew/lib/ \
|
||||
-DHDF5_INCLUDE_DIR=/opt/homebrew/include/ \
|
||||
-DCOLLADA_SUPPORT=0
|
||||
# `sysctl -n hw.ncpu` returns the number of cpu cores on macOS
|
||||
$ make -j$(sysctl -n hw.ncpu)
|
||||
```
|
||||
|
||||
Note: Make sure to compile using the XCode provided Apple Clang compiler, installed with `xcode-select --install
|
||||
`, rather than a ```brew``` installed C/C++ compiler.
|
||||
|
||||
Installing IfcOpenShell with Conda
|
||||
----------------------------------
|
||||
Another option for building and installing IfcOpenShell is to use the popular
|
||||
[Anaconda Python Distribution](https://www.anaconda.com/download).
|
||||
The requirements are spread across a number of channels.
|
||||
You can add these channels to your configuration, or specify them all on the command line:
|
||||
|
||||
$ conda install -c conda-forge -c oce -c dlr-sc -c ifcopenshell ifcopenshell
|
||||
|
||||
Usage examples
|
||||
--------------
|
||||
|
||||
**Invoking IfcConvert from the command line**
|
||||
|
||||
$ wget -O duplex.zip https://portal.nibs.org/files/wl/\?id=4DsTgHFQAcOXzFetxbpRCECPbbfUqpgo
|
||||
$ unzip duplex.zip
|
||||
$ ./IfcConvert Duplex_A_20110907_optimized.ifc
|
||||
$ less Duplex_A_20110907_optimized.obj
|
||||
|
||||
**Using the IfcOpenShell Python interface**
|
||||
|
||||
$ wget -O duplex.zip https://portal.nibs.org/files/wl/\?id=4DsTgHFQAcOXzFetxbpRCECPbbfUqpgo
|
||||
$ unzip duplex.zip
|
||||
$ python
|
||||
>>> import ifcopenshell
|
||||
>>> f = ifcopenshell.open("Duplex_A_20110907_optimized.ifc")
|
||||
>>>
|
||||
>>> # Accessing entity instances by type:
|
||||
>>> f.by_type("ifcwall")[:2]
|
||||
[#91=IfcWallStandardCase('2O2Fr$t4X7Zf8NOew3FL9r',#1,'Basic Wall:Interior - Partition (92mm Stud):144586',$,'Basic Wall:Interior - Partition (92mm Stud):128360',#5198,#18806,'144586'), #92=IfcWallStandardCase('2O2Fr$t4X7Zf8NOew3FLIE',#1,'Basic Wall:Interior - Partition (92mm Stud):143921',$,'Basic Wall:Interior - Partition (92mm Stud):128360',#5206,#18805,'143921')]
|
||||
>>> wall = _[0]
|
||||
>>> len(wall) # number of EXPRESS attributes
|
||||
8
|
||||
>>>
|
||||
>>> # Accessing EXPRESS attributes by name:
|
||||
>>> wall.GlobalId
|
||||
'2O2Fr$t4X7Zf8NOew3FL9r'
|
||||
>>> wall.Name = "My wall"
|
||||
>>> wall.NonExistingAttr
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File ".\ifcopenshell.py", line 14, in __getattr__
|
||||
except: raise AttributeError("entity instance of type '%s' has no attribute'%s'"%(self.wrapped_data.is_a(), name)) from None
|
||||
AttributeError: entity instance of type 'IfcWallStandardCase' has no attribute 'NonExistingAttr'
|
||||
>>> wall.GlobalId = 3
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
File ".\ifcopenshell.py", line 26, in __setattr__
|
||||
self[self.wrapped_data.get_argument_index(key)] = value
|
||||
File ".\ifcopenshell.py", line 30, in __setitem__
|
||||
self.wrapped_data.set_argument(idx, entity_instance.map_value(value))
|
||||
File ".\ifc_wrapper.py", line 118, in <lambda>
|
||||
set_argument = lambda self,x,y: self._set_argument(x) if y is None else self
|
||||
._set_argument(x,y)
|
||||
File ".\ifc_wrapper.py", line 114, in _set_argument
|
||||
def _set_argument(self, *args): return _ifc_wrapper.entity_instance__set_argument(self, *args)
|
||||
RuntimeError: INT is not a valid type for 'GlobalId'
|
||||
>>> # Creating new entity instances
|
||||
>>> f.createIfcCartesianPoint(Coordinates=(1.0,1.5,2.0))
|
||||
#27530=IfcCartesianPoint((1.,1.5,2.))
|
||||
>>>
|
||||
>>> # Working with GlobalId attributes:
|
||||
>>> import uuid
|
||||
>>> ifcopenshell.guid.compress(uuid.uuid1().hex)
|
||||
'3x4C8Q_6qHuv$P$FYkANRX'
|
||||
>>> new_guid = _
|
||||
>>> owner_hist = f.by_type("IfcOwnerHistory")[0]
|
||||
>>> new_wall = f.createIfcWallStandardCase(new_guid, owner_hist, None, None, Tag='my_tag')
|
||||
>>> new_wall.ObjectType = ''
|
||||
>>> new_wall.ObjectPlacement = new_wall.Representation = None
|
||||
>>>
|
||||
>>> # Accessing entity instances by instance id or GlobalId:
|
||||
>>> f[92]
|
||||
#92=IfcWallStandardCase('2O2Fr$t4X7Zf8NOew3FLIE',#1,'Basic Wall:Interior - Partition (92mm Stud):143921',$,'Basic Wall:Interior - Partition (92mm Stud):128360',#5206,#18805,'143921')
|
||||
>>> f['2O2Fr$t4X7Zf8NOew3FLIE']
|
||||
#92=IfcWallStandardCase('2O2Fr$t4X7Zf8NOew3FLIE',#1,'Basic Wall:Interior - Partition (92mm Stud):143921',$,'Basic Wall:Interior - Partition (92mm Stud):128360',#5206,#18805,'143921')
|
||||
>>>
|
||||
>>> # Writing IFC-SPF files to disk:
|
||||
>>> f.write("out.ifc")
|
||||
|
||||
Extra tools
|
||||
-----------
|
||||
|
||||
Also available are a series of utilities that are based on or related to IfcOpenShell.
|
||||
Contents
|
||||
--------
|
||||
|
||||
Those marked with an asterisk are part of IfcOpenShell.
|
||||
|
||||
| Name | License |
|
||||
| ------------------------- | ------------------- |
|
||||
| bcf | LGPL-3.0-or-later |
|
||||
| blenderbim | GPL-3.0-or-later |
|
||||
| bsdd | LGPL-3.0-or-later |
|
||||
| ifc2ca | LGPL-3.0-or-later |
|
||||
| ifc4d | LGPL-3.0-or-later |
|
||||
| ifc5d | LGPL-3.0-or-later |
|
||||
| ifcbimtester | LGPL-3.0-or-later |
|
||||
| ifcblender | LGPL-3.0-or-later\* |
|
||||
| ifccityjson | LGPL-3.0-or-later |
|
||||
| ifcclash | LGPL-3.0-or-later |
|
||||
| ifccobie | LGPL-3.0-or-later |
|
||||
| ifcconvert | LGPL-3.0-or-later\* |
|
||||
| ifccsv | LGPL-3.0-or-later |
|
||||
| ifcdiff | LGPL-3.0-or-later |
|
||||
| ifcfm | LGPL-3.0-or-later |
|
||||
| ifcgeom | LGPL-3.0-or-later\* |
|
||||
| ifcgeom\_schema\_agnostic | LGPL-3.0-or-later\* |
|
||||
| ifcgeomserver | LGPL-3.0-or-later\* |
|
||||
| ifcjni | LGPL-3.0-or-later\* |
|
||||
| ifcmax | LGPL-3.0-or-later\* |
|
||||
| ifcopenshell-python | LGPL-3.0-or-later\* |
|
||||
| ifcparse | LGPL-3.0-or-later\* |
|
||||
| ifcpatch | LGPL-3.0-or-later |
|
||||
| ifcsverchok | GPL-3.0-or-later |
|
||||
| ifcwrap | LGPL-3.0-or-later\* |
|
||||
| qtviewer | LGPL-3.0-or-later\* |
|
||||
| serializers | LGPL-3.0-or-later\* |
|
||||
| Name | Description | License |
|
||||
| ------------------------- | ----------------------------------------------------------------------| ------------------- |
|
||||
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later |
|
||||
| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later |
|
||||
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later |
|
||||
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
|
||||
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later |
|
||||
| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later |
|
||||
| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
|
||||
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
|
||||
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later |
|
||||
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later |
|
||||
| ifccobie | Extract IFC data for COBie handover requirements | LGPL-3.0-or-later |
|
||||
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* |
|
||||
| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later |
|
||||
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later |
|
||||
| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later |
|
||||
| ifcgeom | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| ifcgeom\_schema\_agnostic | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| ifcgeomserver | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| ifcjni | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* |
|
||||
| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* |
|
||||
| ifcparse | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later |
|
||||
| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later |
|
||||
| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later |
|
||||
| ifcwrap | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| qtviewer | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| serializers | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
|
||||
[LGPL]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/COPYING.LESSER "LGPL-3.0-or-later"
|
||||
[IFC]: https://technical.buildingsmart.org/standards/ifc/ "IFC"
|
||||
|
||||
+64
-14
@@ -54,6 +54,7 @@ OPTION(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only."
|
||||
OPTION(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
|
||||
OPTION(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
|
||||
OPTION(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
|
||||
OPTION(WASM_BUILD OFF)
|
||||
if (${HAS_MAX})
|
||||
OPTION(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
|
||||
endif()
|
||||
@@ -149,6 +150,14 @@ UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
|
||||
endif()
|
||||
|
||||
if(WASM_BUILD)
|
||||
# when using the nix/build-all.py build script we should not
|
||||
# look into the sysroot for most of the dependencies but rather
|
||||
# in the designated build/ folder created by the script.
|
||||
set(CMAKE_FIND_ROOT_PATH_BACKUP "${CMAKE_FIND_ROOT_PATH}")
|
||||
set(CMAKE_FIND_ROOT_PATH "")
|
||||
endif()
|
||||
|
||||
if (NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
|
||||
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
|
||||
FIND_FILE(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}/nlohmann)
|
||||
@@ -199,7 +208,14 @@ ELSE()
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
|
||||
set(BOOST_COMPONENTS system program_options regex thread date_time)
|
||||
if (WASM_BUILD)
|
||||
set(BOOST_COMPONENTS)
|
||||
else()
|
||||
# @todo review this, shouldn't this be all possible header-only now?
|
||||
# ... or rewritten using C++17 features?
|
||||
set(BOOST_COMPONENTS system program_options regex thread date_time)
|
||||
endif()
|
||||
|
||||
if(USE_MMAP)
|
||||
if(MSVC)
|
||||
# filesystem is necessary for the utf-16 wpath
|
||||
@@ -210,6 +226,10 @@ if(USE_MMAP)
|
||||
add_definitions(-DUSE_MMAP)
|
||||
endif()
|
||||
|
||||
FIND_PACKAGE(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
|
||||
MESSAGE(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}")
|
||||
MESSAGE(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
|
||||
|
||||
if (NOT MINIMAL_BUILD)
|
||||
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
|
||||
find_package(LibXml2 REQUIRED)
|
||||
@@ -220,11 +240,6 @@ if (NOT MINIMAL_BUILD AND IFCXML_SUPPORT)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_IFCXML)
|
||||
endif()
|
||||
|
||||
FIND_PACKAGE(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
|
||||
MESSAGE(STATUS "Boost include files found in ${Boost_INCLUDE_DIRS}")
|
||||
MESSAGE(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
|
||||
|
||||
|
||||
# Usage:
|
||||
# set(SOME_LIRARIES foo bar)
|
||||
# add_debug_variants(SOME_LIRARIES "${SOME_LIRARIES}" d)
|
||||
@@ -343,10 +358,21 @@ if("${libTKernelExt}" STREQUAL ".a")
|
||||
set(OCCT_STATIC ON)
|
||||
endif()
|
||||
|
||||
if(WASM_BUILD)
|
||||
set(CMAKE_FIND_ROOT_PATH "${CMAKE_FIND_ROOT_PATH_BACKUP}")
|
||||
endif()
|
||||
|
||||
if(OCCT_STATIC)
|
||||
find_package(Threads)
|
||||
# OPENCASCADE_LIBRARIES repeated three times below in order to fix cyclic dependencies - use --start-group ... --end-group instead?
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
|
||||
if(WASM_BUILD)
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
# OPENCASCADE_LIBRARIES repeated N times below in order to fix cyclic dependencies - use --start-group ... --end-group instead?
|
||||
# tfk: --start-group ... --end-group didn't work on the apple linker when last tested
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${OPENCASCADE_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
|
||||
if (NOT APPLE AND NOT WIN32)
|
||||
set(OPENCASCADE_LIBRARIES ${OPENCASCADE_LIBRARIES} "rt")
|
||||
endif()
|
||||
@@ -620,7 +646,12 @@ function(files_for_ifc_version IFC_VERSION RESULT_NAME)
|
||||
endfunction()
|
||||
|
||||
if(NOT SCHEMA_VERSIONS)
|
||||
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4" "4x3")
|
||||
if(WASM_BUILD)
|
||||
# super arbitrarily try to keep size down at least a little bit
|
||||
set(SCHEMA_VERSIONS "2x3" "4")
|
||||
else()
|
||||
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4" "4x3" "4x3_tc1" "4x3_add1")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
foreach(s ${SCHEMA_VERSIONS})
|
||||
@@ -697,7 +728,11 @@ if (BUILD_IFCGEOM)
|
||||
foreach(s ${SCHEMA_VERSIONS})
|
||||
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom_ifc${s})
|
||||
endforeach()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
if (WASM_BUILD)
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
else()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
endif()
|
||||
endif()
|
||||
if (BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
foreach(s ${SCHEMA_VERSIONS})
|
||||
@@ -741,7 +776,11 @@ set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
|
||||
add_library(IfcParse ${IFCPARSE_FILES})
|
||||
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "0.6.0" SOVERSION "0.6")
|
||||
|
||||
TARGET_LINK_LIBRARIES(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
if (WASM_BUILD)
|
||||
TARGET_LINK_LIBRARIES(IfcParse ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
else()
|
||||
TARGET_LINK_LIBRARIES(IfcParse ${Boost_LIBRARIES} ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if (BUILD_IFCGEOM)
|
||||
|
||||
@@ -753,7 +792,9 @@ set(IFCGEOM_FILES ${IFCGEOM_CPP_FILES} ${IFCGEOM_H_FILES})
|
||||
foreach(s ${SCHEMA_VERSIONS})
|
||||
add_library(IfcGeom_ifc${s} STATIC ${IFCGEOM_FILES})
|
||||
set_target_properties(IfcGeom_ifc${s} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${s}")
|
||||
TARGET_LINK_LIBRARIES(IfcGeom_ifc${s} IfcParse ${OPENCASCADE_LIBRARIES})
|
||||
if (NOT WASM_BUILD)
|
||||
TARGET_LINK_LIBRARIES(IfcGeom_ifc${s} IfcParse ${OPENCASCADE_LIBRARIES})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# IfcGeom (schema agnostic)
|
||||
@@ -768,7 +809,11 @@ if (UNIX)
|
||||
find_package(Threads)
|
||||
endif()
|
||||
|
||||
TARGET_LINK_LIBRARIES(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
if (WASM_BUILD)
|
||||
TARGET_LINK_LIBRARIES(IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
else()
|
||||
TARGET_LINK_LIBRARIES(IfcGeom IfcParse ${IFCGEOM_SCHEMA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
@@ -785,7 +830,12 @@ set(SERIALIZERS_S_FILES ${SERIALIZERS_S_H_FILES} ${SERIALIZERS_S_CPP_FILES})
|
||||
foreach(s ${SCHEMA_VERSIONS})
|
||||
add_library(Serializers_ifc${s} STATIC ${SERIALIZERS_S_FILES})
|
||||
set_target_properties(Serializers_ifc${s} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${s} ${CONVERT_PRECISION}")
|
||||
TARGET_LINK_LIBRARIES(Serializers_ifc${s} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
|
||||
|
||||
if (WASM_BUILD)
|
||||
TARGET_LINK_LIBRARIES(Serializers_ifc${s} ${HDF5_LIBRARIES})
|
||||
else()
|
||||
TARGET_LINK_LIBRARIES(Serializers_ifc${s} IfcGeom ${OPENCASCADE_LIBRARIES} ${HDF5_LIBRARIES})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(Serializers ${SERIALIZERS_FILES})
|
||||
|
||||
+3
-2
@@ -13,6 +13,7 @@ source:
|
||||
|
||||
build:
|
||||
binary_relocation: false [osx]
|
||||
number: 1
|
||||
|
||||
requirements:
|
||||
build:
|
||||
@@ -24,7 +25,7 @@ requirements:
|
||||
|
||||
host:
|
||||
- python
|
||||
- boost-cpp <=1.74.0
|
||||
- boost-cpp
|
||||
- occt
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
@@ -36,7 +37,7 @@ requirements:
|
||||
|
||||
run:
|
||||
- python
|
||||
- boost-cpp <=1.74.0
|
||||
- boost-cpp
|
||||
- occt
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
|
||||
+165
-70
@@ -26,7 +26,7 @@
|
||||
# * cmake * git * bzip2 * tar * c(++) compilers * autoconf #
|
||||
# #
|
||||
# if building with USE_OCCT additionally: #
|
||||
# * freetype * glx.h #
|
||||
# * glx.h #
|
||||
# #
|
||||
# if building with OCCT 7.4.0 additionally: #
|
||||
# * libfontconfig1-dev #
|
||||
@@ -39,14 +39,14 @@
|
||||
# #
|
||||
# on debian 7.8 these can be obtained with: #
|
||||
# $ apt-get install git gcc g++ autoconf bison bzip2 cmake #
|
||||
# libfreetype6-dev mesa-common-dev libffi-dev libfontconfig1-dev #
|
||||
# mesa-common-dev libffi-dev libfontconfig1-dev #
|
||||
# #
|
||||
# on ubuntu 14.04: #
|
||||
# $ apt-get install git gcc g++ autoconf bison make cmake #
|
||||
# libfreetype6-dev mesa-common-dev libffi-dev libfontconfig1-dev #
|
||||
# mesa-common-dev libffi-dev libfontconfig1-dev #
|
||||
# #
|
||||
# on OS X El Capitan with homebrew: #
|
||||
# $ brew install git bison autoconf automake freetype libffi cmake #
|
||||
# $ brew install git bison autoconf automake libffi cmake #
|
||||
# #
|
||||
###############################################################################
|
||||
import logging
|
||||
@@ -72,11 +72,11 @@ PROJECT_NAME = "IfcOpenShell"
|
||||
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
|
||||
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
|
||||
|
||||
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3"]
|
||||
PYTHON_VERSIONS = ["3.6.14", "3.7.13", "3.8.13", "3.9.11", "3.10.3", "3.11.0"]
|
||||
JSON_VERSION = "v3.6.1"
|
||||
OCE_VERSION = "0.18.3"
|
||||
OCCT_VERSION = "7.5.3"
|
||||
BOOST_VERSION = "1.71.0"
|
||||
BOOST_VERSION = "1.80.0"
|
||||
PCRE_VERSION = "8.41"
|
||||
LIBXML2_VERSION = "2.9.11"
|
||||
SWIG_VERSION = "4.0.2"
|
||||
@@ -103,6 +103,9 @@ curl = "curl"
|
||||
wget = "wget"
|
||||
strip = "strip"
|
||||
|
||||
explicit_targets = [s for s in sys.argv[1:] if not s.startswith("-")]
|
||||
flags = set(s.lstrip('-') for s in sys.argv[1:] if s.startswith("-"))
|
||||
|
||||
# Helper function for coloured printing
|
||||
|
||||
NO_COLOR = "\033[0m" # <ref>http://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux</ref>
|
||||
@@ -135,9 +138,11 @@ if platform.system() == "Darwin":
|
||||
|
||||
IFCOS_NUM_BUILD_PROCS = os.getenv("IFCOS_NUM_BUILD_PROCS", multiprocessing.cpu_count() + 1)
|
||||
|
||||
CMAKE_DIR = os.path.realpath(os.path.join("..", "cmake"))
|
||||
CMAKE_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "cmake"))
|
||||
|
||||
path = ["..", "build", platform.system(), platform.machine()]
|
||||
build_dir = os.environ.get("BUILD_DIR", os.path.join(os.path.dirname(__file__), "..", "build"))
|
||||
|
||||
path = [build_dir, platform.system(), "wasm" if "wasm" in flags else platform.machine()]
|
||||
if TOOLSET:
|
||||
path.append(TOOLSET)
|
||||
DEFAULT_DEPS_DIR = os.path.realpath(os.path.join(*path))
|
||||
@@ -186,11 +191,12 @@ dependency_tree = {
|
||||
'boost': (),
|
||||
'libxml2': (),
|
||||
'python': (),
|
||||
'occ': (),
|
||||
'occ': ('freetype',),
|
||||
'pcre': (),
|
||||
'json': (),
|
||||
'hdf5': (),
|
||||
'cgal': ()
|
||||
'cgal': (),
|
||||
'freetype': (),
|
||||
}
|
||||
|
||||
def v(dep):
|
||||
@@ -199,10 +205,12 @@ def v(dep):
|
||||
for x in v(d):
|
||||
yield x
|
||||
|
||||
tgts = [s for s in sys.argv[1:] if not s.startswith("-")]
|
||||
flags = set(s for s in sys.argv[1:] if s.startswith("-"))
|
||||
if "v" in flags:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
BUILD_STATIC = not "-shared" in flags
|
||||
BUILD_STATIC = "shared" not in flags
|
||||
ENABLE_FLAG = "--enable-static" if BUILD_STATIC else "--enable-shared"
|
||||
DISABLE_FLAG = "--disable-shared" if BUILD_STATIC else "--disable-static"
|
||||
LINK_TYPE = "static" if BUILD_STATIC else "shared"
|
||||
@@ -210,10 +218,15 @@ LINK_TYPE_UCFIRST = LINK_TYPE[0].upper() + LINK_TYPE[1:]
|
||||
LIBRARY_EXT = "a" if BUILD_STATIC else "so"
|
||||
PIC = "-fPIC" if BUILD_STATIC else ""
|
||||
|
||||
if len(tgts):
|
||||
targets = set(sum((list(v(target)) for target in tgts), []))
|
||||
if any(f.startswith("py-") for f in flags):
|
||||
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if "py-%s" % "".join(pyv.split('.')[0:2]) in flags]
|
||||
|
||||
if len(explicit_targets):
|
||||
targets = set(sum((list(v(target)) for target in explicit_targets), []))
|
||||
else:
|
||||
targets = set(dependency_tree.keys())
|
||||
|
||||
targets = set(t for t in targets if 'without-%s' % t.lower() not in flags)
|
||||
|
||||
print("Building:", *sorted(targets, key=lambda t: len(list(v(t)))))
|
||||
|
||||
@@ -237,7 +250,7 @@ if not os.path.exists(LOG_FILE):
|
||||
open(LOG_FILE, "w").close()
|
||||
logger.info(f"using command log file '{LOG_FILE}'")
|
||||
|
||||
def run(cmds, cwd=None):
|
||||
def run(cmds, cwd=None, can_fail=False):
|
||||
|
||||
"""
|
||||
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
|
||||
@@ -254,7 +267,7 @@ def run(cmds, cwd=None):
|
||||
log_file_handle.close()
|
||||
logger.debug(f"command returned {proc.returncode}")
|
||||
|
||||
if proc.returncode != 0:
|
||||
if proc.returncode != 0 and not can_fail:
|
||||
print("-" * 70)
|
||||
print(stderr)
|
||||
print("-" * 70)
|
||||
@@ -266,6 +279,9 @@ if platform.system() == "Darwin":
|
||||
if run(["sw_vers", "-productVersion"]) >= "11.":
|
||||
# Apparently not supported
|
||||
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) >= (3, 7)]
|
||||
if run(["sw_vers", "-productVersion"]) < "10.16":
|
||||
# Apparently not supported
|
||||
PYTHON_VERSIONS = [pv for pv in PYTHON_VERSIONS if tuple(map(int, pv.split("."))) < (3, 11)]
|
||||
|
||||
BOOST_VERSION_UNDERSCORE = BOOST_VERSION.replace(".", "_")
|
||||
|
||||
@@ -281,7 +297,12 @@ def run_autoconf(arg1, configure_args, cwd):
|
||||
run([bash, "./autogen.sh"], cwd=os.path.realpath(os.path.join(cwd, ".."))) # only run autogen.sh in the directory it is located and use cwd to achieve that in order to not mess up things
|
||||
# Using `sh` over `bash` fixes issues with building swig
|
||||
prefix = os.path.realpath(f"{DEPS_DIR}/install/{arg1}")
|
||||
run(["/bin/sh", "../configure"] + configure_args + [f"--prefix={prefix}"], cwd=cwd)
|
||||
|
||||
wasm = []
|
||||
if "wasm" in flags:
|
||||
wasm.append("emconfigure")
|
||||
|
||||
run([*wasm, "/bin/sh", "../configure"] + configure_args + [f"--prefix={prefix}"], cwd=cwd)
|
||||
|
||||
|
||||
def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None):
|
||||
@@ -289,7 +310,12 @@ def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None):
|
||||
P = ".."
|
||||
else:
|
||||
P = cmake_dir
|
||||
run(["cmake", P] + cmake_args + [f"-DCMAKE_BUILD_TYPE={BUILD_CFG}"], cwd=cwd)
|
||||
|
||||
wasm = []
|
||||
if "wasm" in flags:
|
||||
wasm.append("emcmake")
|
||||
|
||||
run([*wasm, "cmake", P, *cmake_args, f"-DCMAKE_BUILD_TYPE={BUILD_CFG}"], cwd=cwd)
|
||||
|
||||
|
||||
def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
|
||||
@@ -371,12 +397,15 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
|
||||
urlretrieve(url, os.path.join(extract_dir, path))
|
||||
|
||||
if patch is not None:
|
||||
patch_abs = os.path.abspath(os.path.join(os.path.dirname(__file__), patch))
|
||||
if os.path.exists(patch_abs):
|
||||
try: run(["patch", "-p1", "--batch", "--forward", "-i", patch_abs], cwd=extract_dir)
|
||||
except Exception as e:
|
||||
# Assert that the patch has already been applied
|
||||
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
|
||||
if isinstance(patch, str):
|
||||
patch = [patch]
|
||||
for p in patch:
|
||||
patch_abs = os.path.abspath(os.path.join(os.path.dirname(__file__), p))
|
||||
if os.path.exists(patch_abs):
|
||||
try: run(["patch", "-p1", "--batch", "--forward", "-i", patch_abs], cwd=extract_dir)
|
||||
except Exception as e:
|
||||
# Assert that the patch has already been applied
|
||||
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
|
||||
|
||||
if mode == "ctest":
|
||||
run(["ctest", "-S", "HDF5config.cmake,BUILD_GENERATOR=Unix", "-C", BUILD_CFG, "-V", "-O", "hdf5.log"], cwd=extract_dir)
|
||||
@@ -407,7 +436,7 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d
|
||||
logger.info(f"\rConfiguring {name}...")
|
||||
run([bash, "./bootstrap.sh"], cwd=extract_dir)
|
||||
logger.info(f"\rBuilding {name}... ")
|
||||
run(["./b2", f"-j{IFCOS_NUM_BUILD_PROCS}"] + build_tool_args, cwd=extract_dir)
|
||||
run(["./b2", f"-j{IFCOS_NUM_BUILD_PROCS}"] + build_tool_args, cwd=extract_dir, can_fail="wasm" in flags)
|
||||
logger.info(f"\rInstalling {name}... ")
|
||||
shutil.copytree(os.path.join(extract_dir, "boost"), os.path.join(DEPS_DIR, "install", f"boost-{BOOST_VERSION}", "boost"))
|
||||
logger.info(f"\rInstalled {name} \n")
|
||||
@@ -418,11 +447,13 @@ cecho("Collecting dependencies:", GREEN)
|
||||
# TODO: This is untested
|
||||
|
||||
ADDITIONAL_ARGS = []
|
||||
BOOST_ADDRESS_MODEL = []
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
ADDITIONAL_ARGS = [f"-mmacosx-version-min={TOOLSET}"] + ADDITIONAL_ARGS
|
||||
|
||||
if "wasm" in flags:
|
||||
ADDITIONAL_ARGS.extend(("-sWASM_BIGINT", "-fexceptions"))
|
||||
|
||||
# If the linker supports GC sections, set it up to reduce binary file size
|
||||
# -fPIC is required for the shared libraries to work
|
||||
|
||||
@@ -431,7 +462,7 @@ CFLAGS = os.environ.get("CFLAGS", "")
|
||||
LDFLAGS = os.environ.get("LDFLAGS", "")
|
||||
|
||||
ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
|
||||
if sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
|
||||
if "wasm" not in flags and sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
if BUILD_STATIC:
|
||||
@@ -501,8 +532,27 @@ if "swig" in targets:
|
||||
download_tool=download_tool_git,
|
||||
revision=f"rel-{SWIG_VERSION}"
|
||||
)
|
||||
|
||||
if "freetype" in targets:
|
||||
build_dependency(
|
||||
name=f"freetype",
|
||||
mode="cmake",
|
||||
build_tool_args=[
|
||||
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/freetype"
|
||||
],
|
||||
download_url = "https://github.com/freetype/freetype",
|
||||
download_name = "freetype2",
|
||||
download_tool=download_tool_git,
|
||||
)
|
||||
|
||||
if USE_OCCT and "occ" in targets:
|
||||
patches = []
|
||||
if OCCT_VERSION < "7.4":
|
||||
patches.append("./patches/occt/enable-exception-handling.patch")
|
||||
|
||||
if "wasm" in flags:
|
||||
patches.append("./patches/occt/no_em_js.patch")
|
||||
|
||||
build_dependency(
|
||||
name=f"occt-{OCCT_VERSION}",
|
||||
mode="cmake",
|
||||
@@ -510,12 +560,13 @@ if USE_OCCT and "occ" in targets:
|
||||
f"-DINSTALL_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}",
|
||||
f"-DBUILD_LIBRARY_TYPE={LINK_TYPE_UCFIRST}",
|
||||
"-DBUILD_MODULE_Draw=0",
|
||||
"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off"
|
||||
"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off",
|
||||
f"-D3RDPARTY_FREETYPE_DIR={DEPS_DIR}/install/freetype"
|
||||
],
|
||||
download_url = "https://github.com/Open-Cascade-SAS/OCCT",
|
||||
download_name = "occt",
|
||||
download_tool=download_tool_git,
|
||||
patch=None if OCCT_VERSION >= "7.4" else "./patches/occt/enable-exception-handling.patch",
|
||||
patch=patches,
|
||||
revision="V" + OCCT_VERSION.replace('.', '_')
|
||||
)
|
||||
elif "occ" in targets:
|
||||
@@ -566,11 +617,11 @@ if "OpenCOLLADA" in targets:
|
||||
download_url="https://github.com/KhronosGroup/OpenCOLLADA.git",
|
||||
download_name="OpenCOLLADA",
|
||||
download_tool=download_tool_git,
|
||||
patch="./patches/opencollada/pr622_and_disable_subdirs.patch",
|
||||
patch=("./patches/opencollada/pr622_and_disable_subdirs.patch", "./patches/opencollada/remove_tr1.patch"),
|
||||
revision=OPENCOLLADA_VERSION
|
||||
)
|
||||
|
||||
if "python" in targets and not USE_CURRENT_PYTHON_VERSION:
|
||||
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flags:
|
||||
# Python should not be built with -fvisibility=hidden, from experience that introduces segfaults
|
||||
OLD_CXX_FLAGS = os.environ["CXXFLAGS"]
|
||||
OLD_C_FLAGS = os.environ["CFLAGS"]
|
||||
@@ -609,6 +660,9 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION:
|
||||
|
||||
if "boost" in targets:
|
||||
str_concat = lambda prefix: lambda postfix: "" if postfix.strip() == "" else "=".join((prefix, postfix.strip()))
|
||||
toolset = []
|
||||
if "wasm" in flags:
|
||||
toolset.append("toolset=emscripten")
|
||||
build_dependency(
|
||||
f"boost-{BOOST_VERSION}",
|
||||
mode="bjam",
|
||||
@@ -620,21 +674,30 @@ if "boost" in targets:
|
||||
"--with-thread",
|
||||
"--with-date_time",
|
||||
"--with-iostreams",
|
||||
f"link={LINK_TYPE}"
|
||||
] + \
|
||||
BOOST_ADDRESS_MODEL + \
|
||||
list(map(str_concat("cxxflags"), CXXFLAGS.strip().split(' '))) + \
|
||||
list(map(str_concat("linkflags"), LDFLAGS.strip().split(' '))) + \
|
||||
["stage", "-s", "NO_BZIP2=1"],
|
||||
f"link={LINK_TYPE}",
|
||||
*toolset,
|
||||
*map(str_concat("cxxflags"), CXXFLAGS.strip().split(' ')),
|
||||
*map(str_concat("linkflags"), LDFLAGS.strip().split(' ')),
|
||||
"stage", "-s", "NO_BZIP2=1"],
|
||||
download_url=BOOST_LOCATION,
|
||||
patch="./patches/boost/boostorg_regex_62.patch",
|
||||
download_name=f"boost_{BOOST_VERSION_UNDERSCORE}.tar.bz2"
|
||||
)
|
||||
if "wasm" in flags:
|
||||
# only supported on nix for now
|
||||
run(("find", ".", "-name", "*.bc", "-exec", "bash", "-c", "emar q ${1%.bc}.a $1", "bash", "{}", ";"), cwd=f"{DEPS_DIR}/install/boost-{BOOST_VERSION}/lib")
|
||||
|
||||
if "cgal" in targets:
|
||||
gmp_args = []
|
||||
mpfr_args = []
|
||||
if "wasm" in flags:
|
||||
gmp_args.extend(("--disable-assembly", "--host", "none", "--enable-cxx"))
|
||||
mpfr_args.extend(("--host", "none"))
|
||||
|
||||
build_dependency(
|
||||
name=f"gmp-{GMP_VERSION}",
|
||||
mode="autoconf",
|
||||
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, "--with-pic"],
|
||||
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, "--with-pic", *gmp_args],
|
||||
download_url="https://ftp.gnu.org/gnu/gmp/",
|
||||
download_name=f"gmp-{GMP_VERSION}.tar.bz2"
|
||||
)
|
||||
@@ -642,7 +705,7 @@ if "cgal" in targets:
|
||||
build_dependency(
|
||||
name=f"mpfr-{MPFR_VERSION}",
|
||||
mode="autoconf",
|
||||
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, f"--with-gmp={DEPS_DIR}/install/gmp-{GMP_VERSION}"],
|
||||
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, *mpfr_args, f"--with-gmp={DEPS_DIR}/install/gmp-{GMP_VERSION}"],
|
||||
download_url=f"http://www.mpfr.org/mpfr-{MPFR_VERSION}/",
|
||||
download_name=f"mpfr-{MPFR_VERSION}.tar.bz2"
|
||||
)
|
||||
@@ -676,18 +739,8 @@ os.makedirs(IFCOS_DIR, exist_ok=True)
|
||||
executables_dir = os.path.join(IFCOS_DIR, "executables")
|
||||
os.makedirs(executables_dir, exist_ok=True)
|
||||
|
||||
logger.info("\rConfiguring executables...")
|
||||
|
||||
OFF_ON = ["OFF", "ON"]
|
||||
|
||||
exec_args = [
|
||||
"-DBUILD_IFCGEOM=" +OFF_ON["IfcGeom" in targets],
|
||||
"-DBUILD_GEOMSERVER=" +OFF_ON["IfcGeomServer" in targets],
|
||||
"-DBUILD_CONVERT=" +OFF_ON["IfcConvert" in targets],
|
||||
"-DBUILD_IFCPYTHON=" "OFF",
|
||||
"-DCMAKE_INSTALL_PREFIX=" f"{DEPS_DIR}/install/ifcopenshell",
|
||||
]
|
||||
|
||||
cmake_args = [
|
||||
"-DUSE_MMAP=" "OFF",
|
||||
"-DBUILD_EXAMPLES=" "OFF",
|
||||
@@ -699,6 +752,11 @@ cmake_args = [
|
||||
"-DADD_COMMIT_SHA=" +("On" if ADD_COMMIT_SHA else "Off")
|
||||
]
|
||||
|
||||
if "wasm" in flags:
|
||||
# Boost is built by the build script so should not be found
|
||||
# inside of the sysroot set by the emscriptem toolchain
|
||||
cmake_args.append("-DWASM_BUILD=On")
|
||||
|
||||
if "cgal" in targets:
|
||||
cmake_args.extend([
|
||||
"-DCGAL_INCLUDE_DIR=" f"{DEPS_DIR}/install/cgal-{CGAL_VERSION}/include",
|
||||
@@ -750,20 +808,36 @@ if "hdf5" in targets:
|
||||
"-DHDF5_INCLUDE_DIR=" f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}/include",
|
||||
"-DHDF5_LIBRARY_DIR=" f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}/lib"
|
||||
])
|
||||
else:
|
||||
cmake_args.append("-DHDF5_SUPPORT=Off")
|
||||
|
||||
run_cmake("", exec_args + cmake_args, cmake_dir=CMAKE_DIR, cwd=executables_dir)
|
||||
if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets):
|
||||
logger.info("\rConfiguring executables...")
|
||||
|
||||
logger.info("\rBuilding executables... ")
|
||||
exec_args = [
|
||||
"-DBUILD_IFCGEOM=" +OFF_ON["IfcGeom" in targets],
|
||||
"-DBUILD_GEOMSERVER=" +OFF_ON["IfcGeomServer" in targets],
|
||||
"-DBUILD_CONVERT=" +OFF_ON["IfcConvert" in targets],
|
||||
"-DBUILD_IFCPYTHON=" "OFF",
|
||||
"-DCMAKE_INSTALL_PREFIX=" f"{DEPS_DIR}/install/ifcopenshell",
|
||||
]
|
||||
|
||||
run_cmake("", exec_args + cmake_args, cmake_dir=CMAKE_DIR, cwd=executables_dir)
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], cwd=executables_dir)
|
||||
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
|
||||
logger.info("\rBuilding executables... ")
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}"], cwd=executables_dir)
|
||||
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
|
||||
|
||||
if "IfcOpenShell-Python" in targets:
|
||||
# On OSX the actual Python library is not linked against.
|
||||
ADDITIONAL_ARGS = ""
|
||||
if platform.system() == "Darwin":
|
||||
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
|
||||
|
||||
|
||||
if "wasm" in flags:
|
||||
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
|
||||
|
||||
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
|
||||
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
|
||||
os.environ["LDFLAGS"] = f"{LDFLAGS} {ADDITIONAL_ARGS}"
|
||||
@@ -780,36 +854,57 @@ if "IfcOpenShell-Python" in targets:
|
||||
|
||||
os.environ["PYTHON_LIBRARY_BASENAME"] = os.path.basename(python_library)
|
||||
|
||||
swig_when_built = []
|
||||
if "swig" in targets:
|
||||
swig_when_built.append(f"-DSWIG_EXECUTABLE={DEPS_DIR}/install/swig/bin/swig")
|
||||
|
||||
run_cmake("",
|
||||
cmake_args + [
|
||||
"-DPYTHON_LIBRARY=" +python_library,
|
||||
"-DPYTHON_EXECUTABLE=" +python_executable,
|
||||
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
|
||||
# *([f"-DPYTHON_MODULE_INSTALL_DIR={os.environ['PYTHONPATH']}/ifcopenshell"] if "wasm" in flags else []),
|
||||
*(["-DPYTHON_MODULE_INSTALL_DIR="+os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "package"))] if "wasm" in flags else []),
|
||||
"-DPYTHON_INCLUDE_DIR=" +python_include,
|
||||
"-DSWIG_EXECUTABLE=" f"{DEPS_DIR}/install/swig/bin/swig",
|
||||
"-DCMAKE_INSTALL_PREFIX=" f"{DEPS_DIR}/install/ifcopenshell/tmp",
|
||||
"-DUSERSPACE_PYTHON_PREFIX=" +["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}]
|
||||
], cmake_dir=CMAKE_DIR, cwd=python_dir)
|
||||
"-DUSERSPACE_PYTHON_PREFIX=" +["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
|
||||
*swig_when_built],
|
||||
cmake_dir=CMAKE_DIR, cwd=python_dir)
|
||||
|
||||
logger.info(f"\rBuilding python {python_version} wrapper... ")
|
||||
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "_ifcopenshell_wrapper"], cwd=python_dir)
|
||||
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
|
||||
|
||||
module_dir = os.path.dirname(run([python_executable, "-c", "import inspect, ifcopenshell; print(inspect.getfile(ifcopenshell))"]))
|
||||
if python_executable:
|
||||
module_dir = os.path.dirname(run([python_executable, "-c", "import inspect, ifcopenshell; print(inspect.getfile(ifcopenshell))"]))
|
||||
|
||||
if platform.system() != "Darwin":
|
||||
# TODO: This symbol name depends on the Python version?
|
||||
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", "_ifcopenshell_wrapper.so"], cwd=module_dir)
|
||||
return module_dir
|
||||
if platform.system() != "Darwin":
|
||||
# TODO: This symbol name depends on the Python version?
|
||||
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", "_ifcopenshell_wrapper.so"], cwd=module_dir)
|
||||
|
||||
return module_dir
|
||||
|
||||
if USE_CURRENT_PYTHON_VERSION:
|
||||
python_info = sysconfig.get_paths()
|
||||
python_lib = os.path.join(
|
||||
sysconfig.get_config_var('LIBDIR'),
|
||||
sysconfig.get_config_var('multiarchsubdir').replace("/", ""),
|
||||
sysconfig.get_config_var("INSTSONAME")
|
||||
if "wasm" in flags:
|
||||
compile_python_wrapper(
|
||||
f"{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.{os.environ['PYMICRO']}",
|
||||
f"{os.environ['TARGETINSTALLDIR']}/lib/libpython{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.a",
|
||||
os.environ['PYTHONINCLUDE'],
|
||||
None
|
||||
)
|
||||
|
||||
elif USE_CURRENT_PYTHON_VERSION:
|
||||
python_info = sysconfig.get_paths()
|
||||
|
||||
py_path_components = [
|
||||
sysconfig.get_config_var('LIBDIR'),
|
||||
sysconfig.get_config_var("INSTSONAME")
|
||||
]
|
||||
|
||||
if sysconfig.get_config_var('multiarchsubdir'):
|
||||
py_path_components.insert(1, sysconfig.get_config_var('multiarchsubdir').replace("/", ""))
|
||||
|
||||
python_lib = os.path.join(*py_path_components)
|
||||
|
||||
compile_python_wrapper(platform.python_version(), python_lib, python_info["include"], sys.executable)
|
||||
else:
|
||||
for python_version in PYTHON_VERSIONS:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
--- a/tools/build/src/tools/emscripten.jam
|
||||
+++ b/tools/build/src/tools/emscripten.jam
|
||||
@@ -6,6 +6,7 @@
|
||||
import feature ;
|
||||
import os ;
|
||||
import toolset ;
|
||||
+import generators ;
|
||||
import common ;
|
||||
import gcc ;
|
||||
import type ;
|
||||
@@ -52,6 +53,8 @@
|
||||
<debug-symbols>off <debug-symbols>on
|
||||
<rtti>off <rtti>on
|
||||
;
|
||||
+generators.override builtin.lib-generator : emscripten.prebuilt ;
|
||||
+generators.override emscripten.searched-lib-generator : searched-lib-generator ;
|
||||
|
||||
type.set-generated-target-suffix EXE : <toolset>emscripten : "js" ;
|
||||
type.set-generated-target-suffix OBJ : <toolset>emscripten : "bc" ;
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
diff --git a/src/Message/Message_PrinterSystemLog.cxx b/src/Message/Message_PrinterSystemLog.cxx
|
||||
index 0c82c2167..a2e9e6d68 100644
|
||||
--- a/src/Message/Message_PrinterSystemLog.cxx
|
||||
+++ b/src/Message/Message_PrinterSystemLog.cxx
|
||||
@@ -55,27 +55,6 @@
|
||||
return ANDROID_LOG_DEBUG;
|
||||
}
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
- #include <emscripten/emscripten.h>
|
||||
-
|
||||
- //! Print message to console.debug().
|
||||
- EM_JS(void, occJSConsoleDebug, (const char* theStr), {
|
||||
- console.debug(UTF8ToString(theStr));
|
||||
- });
|
||||
-
|
||||
- //! Print message to console.info().
|
||||
- EM_JS(void, occJSConsoleInfo, (const char* theStr), {
|
||||
- console.info(UTF8ToString(theStr));
|
||||
- });
|
||||
-
|
||||
- //! Print message to console.warn().
|
||||
- EM_JS(void, occJSConsoleWarn, (const char* theStr), {
|
||||
- console.warn(UTF8ToString(theStr));
|
||||
- });
|
||||
-
|
||||
- //! Print message to console.error().
|
||||
- EM_JS(void, occJSConsoleError, (const char* theStr), {
|
||||
- console.error(UTF8ToString(theStr));
|
||||
- });
|
||||
#else
|
||||
#include <syslog.h>
|
||||
|
||||
@@ -169,16 +148,6 @@ void Message_PrinterSystemLog::send (const TCollection_AsciiString& theString,
|
||||
#elif defined(__ANDROID__)
|
||||
__android_log_write (getAndroidLogPriority (theGravity), myEventSourceName.ToCString(), theString.ToCString());
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
- // don't use bogus emscripten_log() corrupting UNICODE strings
|
||||
- switch (theGravity)
|
||||
- {
|
||||
- case Message_Trace: occJSConsoleDebug(theString.ToCString()); return;
|
||||
- case Message_Info: occJSConsoleInfo (theString.ToCString()); return;
|
||||
- case Message_Warning: occJSConsoleWarn (theString.ToCString()); return;
|
||||
- case Message_Alarm: occJSConsoleError(theString.ToCString()); return;
|
||||
- case Message_Fail: occJSConsoleError(theString.ToCString()); return;
|
||||
- }
|
||||
- occJSConsoleWarn (theString.ToCString());
|
||||
#else
|
||||
syslog (getSysLogPriority (theGravity), "%s", theString.ToCString());
|
||||
#endif
|
||||
diff --git a/src/OSD/OSD_MemInfo.cxx b/src/OSD/OSD_MemInfo.cxx
|
||||
index 08a939beb..7c1fc79b3 100644
|
||||
--- a/src/OSD/OSD_MemInfo.cxx
|
||||
+++ b/src/OSD/OSD_MemInfo.cxx
|
||||
@@ -37,15 +37,6 @@
|
||||
|
||||
#include <OSD_MemInfo.hxx>
|
||||
|
||||
-#if defined(__EMSCRIPTEN__)
|
||||
- #include <emscripten.h>
|
||||
-
|
||||
- //! Return WebAssembly heap size in bytes.
|
||||
- EM_JS(size_t, OSD_MemInfo_getModuleHeapLength, (), {
|
||||
- return Module.HEAP8.length;
|
||||
- });
|
||||
-#endif
|
||||
-
|
||||
// =======================================================================
|
||||
// function : OSD_MemInfo
|
||||
// purpose :
|
||||
@@ -156,29 +147,6 @@ void OSD_MemInfo::Update()
|
||||
}
|
||||
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
- if (IsActive (MemHeapUsage)
|
||||
- || IsActive (MemWorkingSet)
|
||||
- || IsActive (MemWorkingSetPeak))
|
||||
- {
|
||||
- // /proc/%d/status is not emulated - get more info from mallinfo()
|
||||
- const struct mallinfo aMI = mallinfo();
|
||||
- if (IsActive (MemHeapUsage))
|
||||
- {
|
||||
- myCounters[MemHeapUsage] = aMI.uordblks;
|
||||
- }
|
||||
- if (IsActive (MemWorkingSet))
|
||||
- {
|
||||
- myCounters[MemWorkingSet] = aMI.uordblks;
|
||||
- }
|
||||
- if (IsActive (MemWorkingSetPeak))
|
||||
- {
|
||||
- myCounters[MemWorkingSetPeak] = aMI.usmblks;
|
||||
- }
|
||||
- }
|
||||
- if (IsActive (MemVirtual))
|
||||
- {
|
||||
- myCounters[MemVirtual] = OSD_MemInfo_getModuleHeapLength();
|
||||
- }
|
||||
#elif (defined(__linux__) || defined(__linux))
|
||||
if (IsActive (MemHeapUsage))
|
||||
{
|
||||
@@ -0,0 +1,81 @@
|
||||
diff --git a/COLLADABaseUtils/include/COLLADABUhash_map.h b/COLLADABaseUtils/include/COLLADABUhash_map.h
|
||||
index 8ab0fb9b..12503bfb 100644
|
||||
--- a/COLLADABaseUtils/include/COLLADABUhash_map.h
|
||||
+++ b/COLLADABaseUtils/include/COLLADABUhash_map.h
|
||||
@@ -32,9 +32,9 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
|
||||
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
|
||||
- #define COLLADABU_HASH_SET std::tr1::unordered_set
|
||||
+ #define COLLADABU_HASH_MAP std::unordered_map
|
||||
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
|
||||
+ #define COLLADABU_HASH_SET std::unordered_set
|
||||
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
#define COLLADABU_HASH_FUN hash
|
||||
@@ -50,12 +50,12 @@
|
||||
#define COLLADABU_HASH_FUN hash
|
||||
#endif
|
||||
#elif defined(__MINGW32__) || defined(__MINGW64__)
|
||||
- #include <tr1/unordered_map>
|
||||
- #include <tr1/unordered_set>
|
||||
+ #include <unordered_map>
|
||||
+ #include <unordered_set>
|
||||
|
||||
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
|
||||
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
|
||||
- #define COLLADABU_HASH_SET std::tr1::unordered_set
|
||||
+ #define COLLADABU_HASH_MAP std::unordered_map
|
||||
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
|
||||
+ #define COLLADABU_HASH_SET std::unordered_set
|
||||
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
#define COLLADABU_HASH_FUN hash
|
||||
@@ -107,12 +107,12 @@
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE
|
||||
#define COLLADABU_HASH_FUN hash
|
||||
#else
|
||||
- #include <tr1/unordered_map>
|
||||
- #include <tr1/unordered_set>
|
||||
+ #include <unordered_map>
|
||||
+ #include <unordered_set>
|
||||
|
||||
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
|
||||
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
|
||||
- #define COLLADABU_HASH_SET std::tr1::unordered_set
|
||||
+ #define COLLADABU_HASH_MAP std::unordered_map
|
||||
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
|
||||
+ #define COLLADABU_HASH_SET std::unordered_set
|
||||
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
#define COLLADABU_HASH_FUN hash
|
||||
diff --git a/common/libBuffer/include/CommonFWriteBufferFlusher.h b/common/libBuffer/include/CommonFWriteBufferFlusher.h
|
||||
index c7af45b2..fac4f133 100644
|
||||
--- a/common/libBuffer/include/CommonFWriteBufferFlusher.h
|
||||
+++ b/common/libBuffer/include/CommonFWriteBufferFlusher.h
|
||||
@@ -15,12 +15,12 @@
|
||||
|
||||
#if (defined(WIN64) || defined(_WIN64) || defined(__WIN64__)) || (defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) || defined(__APPLE__))
|
||||
#if defined(__GNUC__) && !defined(_LIBCPP_VERSION)
|
||||
-# include <tr1/unordered_map>
|
||||
+# include <unordered_map>
|
||||
#else
|
||||
# include <unordered_map>
|
||||
#endif
|
||||
#else
|
||||
-# include <tr1/unordered_map>
|
||||
+# include <unordered_map>
|
||||
#endif
|
||||
|
||||
#ifdef _LIBCPP_VERSION
|
||||
@@ -58,7 +58,7 @@ namespace Common
|
||||
#else
|
||||
typedef __int64 FilePosType;
|
||||
#endif
|
||||
- typedef std::tr1::unordered_map<MarkId, FilePosType > MarkIdToFilePos;
|
||||
+ typedef std::unordered_map<MarkId, FilePosType > MarkIdToFilePos;
|
||||
|
||||
public:
|
||||
static const size_t DEFAUL_BUFFER_SIZE = 64*1024;
|
||||
@@ -0,0 +1,18 @@
|
||||
package:
|
||||
name: ifcopenshell
|
||||
version: 0.7.0
|
||||
|
||||
source:
|
||||
path: IfcOpenShell
|
||||
|
||||
build:
|
||||
script: |
|
||||
python nix/build-all.py --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py310 IfcOpenShell-Python
|
||||
cp pyodide/setup.py .
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
license: LGPL-3.0-or-later
|
||||
summary: |
|
||||
IfcOpenShell is an open source (LGPL) software library for
|
||||
working with the Industry Foundation Classes (IFC) file format.
|
||||
@@ -0,0 +1,11 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(name='IfcOpenShell',
|
||||
version='0.7.0',
|
||||
description='IfcOpenShell is an open source (LGPL) software library for working with the Industry Foundation Classes (IFC) file format.',
|
||||
author='Thomas Krijnen',
|
||||
author_email='thomas@aecgeeks.com',
|
||||
url='http://ifcopenshell.org',
|
||||
packages=find_packages(),
|
||||
package_data={'': ['*.so']},
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
.tox
|
||||
@@ -0,0 +1,37 @@
|
||||
.PHONY: test
|
||||
test:
|
||||
tox
|
||||
|
||||
.PHONY: qa
|
||||
qa:
|
||||
black .
|
||||
isort .
|
||||
pylint ./src --output-format=colorized
|
||||
|
||||
.PHONY: typecheck
|
||||
typecheck:
|
||||
mypy src/bcf
|
||||
|
||||
.PHONY: license
|
||||
license:
|
||||
copyright-header --license LGPL3 --copyright-holder "Andrea Ghensi <andrea.ghensi@gmail.com>" --copyright-year "2022" --copyright-software "IfcOpenShell" --copyright-software-description "BCF XML file handling" -a ./ -o ./
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
coverage run --source bcf -m pytest tests
|
||||
coverage html
|
||||
xdg-open htmlcov/index.html
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf htmlcov
|
||||
|
||||
# TODO: make this based on xsd file presence
|
||||
.PHONY: models
|
||||
models:
|
||||
cd src && xsdata generate -p bcf.v2.model --unnest-classes --kw-only --slots -ds Google bcf/v2/xsd
|
||||
cd src && xsdata generate -p bcf.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd
|
||||
|
||||
# .PHONY
|
||||
# api:
|
||||
# openapi-python-client generate --url https://api.swaggerhub.com/apis/buildingSMART/BCF/3.0
|
||||
+31
-48
@@ -1,64 +1,54 @@
|
||||
# bcf
|
||||
|
||||
A simple Python implementation of BCF. The data model is described in `data.py`.
|
||||
A simple Python implementation of BCF.
|
||||
Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API
|
||||
is available via `bcfapi.py`.
|
||||
|
||||
- BCF-XML version 2.1: Fully supported
|
||||
- BCF-API version 2.1: Not supported, will probably tackle this after BCF-API v3.0
|
||||
- BCF-XML version 3.0: Almost fully supported, except for the documents module
|
||||
- BCF-API version 3.0: Almost fully supported, except for two requests.
|
||||
It tries to support BCF-XML version 2.1 and 3.0, and BCF-API 3.0.
|
||||
|
||||
## bcfxml
|
||||
|
||||
The `bcfxml` module lets you interact with the BCF-XML standard.
|
||||
The `bcfxml.load` function lets you read a BCF-XML file.
|
||||
It takes care of using the right version based on the "bcf.version" file contained in the BCF package.
|
||||
|
||||
The BCF files are extracted and parsed on-demand, and edits are stored in memory until you call the `save` method.
|
||||
|
||||
```python
|
||||
from bcf import bcfxml
|
||||
|
||||
from bcf.bcfxml import load
|
||||
|
||||
# Load a project
|
||||
bcfxml = bcfxml.load("/path/to/file.bcf")
|
||||
with load("/path/to/file.bcf") as bcfxml:
|
||||
project = bcfxml.project
|
||||
print(project.name)
|
||||
|
||||
# To edit a project, just modify the object directly
|
||||
bcfxml.project.name = "New name"
|
||||
|
||||
# The project is also stored in the module
|
||||
# project == bcfxml.project
|
||||
project=bcfxml.get_project()
|
||||
print(project.name)
|
||||
# Get a dictionary of topics
|
||||
topics = bcfxml.topics
|
||||
|
||||
# To edit a project, just modify the object directly
|
||||
bcfxml.project.name = "New name"
|
||||
bcfxml.edit_project()
|
||||
for topic_handler in bcfxml.topics:
|
||||
topic = topic_handler.topic
|
||||
print("Topic guid is", topic.guid)
|
||||
print("Topic title is", topic.title)
|
||||
|
||||
# The BCF file is extracted to this temporary directory
|
||||
print(bcfxml.filepath)
|
||||
# Fetch extra data about a topic
|
||||
header = topic_handler.header
|
||||
comments = topic_handler.comments
|
||||
viewpoints = topic_handler.viewpoints
|
||||
|
||||
# Get a dictionary of topics
|
||||
topics = bcfxml.get_topics()
|
||||
for comment in comments:
|
||||
print(comment.guid)
|
||||
print(comment.comment)
|
||||
print(comment.author)
|
||||
|
||||
# Note: topics == bcfxml.topics
|
||||
for guid, topic in bcfxml.topics.items():
|
||||
print("Topic guid is", guid)
|
||||
print("Topic guid is", topic.guid)
|
||||
print("Topic title is", topic.title)
|
||||
# Get a particular topic
|
||||
topic = bcfxml.get_topic(guid)
|
||||
|
||||
# Fetch extra data about a topic
|
||||
header = bcfxml.get_header(guid)
|
||||
comments = bcfxml.get_comments(guid)
|
||||
viewpoints = bcfxml.get_viewpoints(guid)
|
||||
# Modify a topic
|
||||
topic.title = "New title"
|
||||
|
||||
# Note: comments == topic.comments, and so on
|
||||
for comment_guid, comment in comments.items():
|
||||
print(comment_guid)
|
||||
print(comment.comment)
|
||||
print(comment.author)
|
||||
|
||||
# Get a particular topic
|
||||
topic = bcfxml.get_topic(guid)
|
||||
|
||||
# Modify a topic
|
||||
topic.title = "New title"
|
||||
bcfxml.edit_topic(topic)
|
||||
bcfxml.save()
|
||||
```
|
||||
|
||||
## bcfapi
|
||||
@@ -92,10 +82,3 @@ print(data)
|
||||
data = bcf_client.get_extensions(project_id)
|
||||
print(data)
|
||||
```
|
||||
|
||||
## Todo List
|
||||
|
||||
The remaining work that needs to be completed in `bcfxml.py` and `bcfapi.py`.
|
||||
|
||||
- For `bcfxml.py` two xsds support is remaining namely 'documents.xsd`and`extensions.xsd`.
|
||||
- For `bcfapi.py` two requests that are `get_topics` and `get_comments` are remaining.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
name: bcf-client
|
||||
channels:
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- ifcopenshell
|
||||
- xsdata
|
||||
- numpy
|
||||
+116
-2
@@ -1,12 +1,126 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"setuptools>=61",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "bcf-client"
|
||||
# author = "IfcOpenShell"
|
||||
description = "BCF-XML file handler."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
keywords = ["IFC", "BCF", "BIM", "eingineering"]
|
||||
dependencies = [
|
||||
"xsdata",
|
||||
"numpy",
|
||||
]
|
||||
version = "0.0.1"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Scientific/Engineering",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Source = "https://github.com/IfcOpenShell/IfcOpenShell"
|
||||
Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
extend-exclude = "model"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
profile = "black"
|
||||
extend_skip_glob = ["src/bcf/*/model/*"]
|
||||
|
||||
[tool.coverage.paths]
|
||||
source = ["src"]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
source = ["bcf"]
|
||||
omit = ["*/model/*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
fail_under = 65
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if TYPE_CHECKING",
|
||||
"if __name__ == .__main__.:",
|
||||
"Protocol",
|
||||
]
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py310
|
||||
isolated_build = true
|
||||
skip_missing_interpreters = true
|
||||
requires = tox-conda
|
||||
|
||||
[testenv]
|
||||
conda_deps =
|
||||
pytest
|
||||
pytest-cov
|
||||
coverage
|
||||
conda_channels =
|
||||
conda-forge
|
||||
conda_env = environment.yml
|
||||
commands = pytest --cov --cov-report=term tests
|
||||
"""
|
||||
|
||||
[tool.mypy]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
#no_implicit_reexport = true
|
||||
show_column_numbers = true
|
||||
show_error_codes = true
|
||||
show_error_context = true
|
||||
strict_equality = true
|
||||
strict_optional = true
|
||||
warn_redundant_casts = true
|
||||
#warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
warn_unused_ignores = true
|
||||
exclude= "src/bcf/v(2|3)/model"
|
||||
plugins = "numpy.typing.mypy_plugin"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "tests"
|
||||
disallow_untyped_decorators = false
|
||||
disallow_untyped_defs = false
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"pytest",
|
||||
"pytest_mock",
|
||||
"ifcopenshell",
|
||||
"ifcopenshell.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pylint.main]
|
||||
ignore = ["model"]
|
||||
ignored-modules = ["bcf.v2.model", "bcf.v3.model", "xsdata"]
|
||||
jobs = 0
|
||||
disable="all"
|
||||
enable="E" # B,B9,BLK,C,D,E,F,I,N,S,W
|
||||
|
||||
[tool.pylint.design]
|
||||
max-args = 10
|
||||
max-attributes = 10
|
||||
|
||||
[tool.pylint.format]
|
||||
expected-line-ending-format = "LF"
|
||||
max-line-length = 120
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
black
|
||||
mypy
|
||||
pylint
|
||||
isort
|
||||
xsdata
|
||||
tox
|
||||
tox-conda
|
||||
@@ -1 +0,0 @@
|
||||
xmlschema
|
||||
@@ -1,37 +0,0 @@
|
||||
[metadata]
|
||||
name=bcf-client
|
||||
version=0.0.1
|
||||
author = Ifcopenshell
|
||||
description = A simple Python implementation of BCF
|
||||
url = https://github.com/IfcOpenShell/IfcOpenShell
|
||||
project_urls =
|
||||
Code=https://github.com/IfcOpenShell/IfcOpenShell
|
||||
Issues=https://github.com/IfcOpenShell/IfcOpenShell/issues
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
classifiers =
|
||||
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python :: 3
|
||||
Topic :: Scientific/Engineering
|
||||
Topic :: Utilities
|
||||
keywords =
|
||||
Python
|
||||
file formats
|
||||
engineering
|
||||
|
||||
[options]
|
||||
package_dir =
|
||||
= src
|
||||
packages = find:
|
||||
python_requires = >=3
|
||||
install_requires=
|
||||
xmlschema
|
||||
include_package_data = True
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
ignore = E24, E121, E123, E126, E203, E226, E704, E741, W503, W504
|
||||
@@ -0,0 +1,3 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
+58
-50
@@ -1,61 +1,69 @@
|
||||
# BCF - BCF Python library
|
||||
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
||||
#
|
||||
# This file is part of BCF.
|
||||
#
|
||||
# BCF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BCF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
BCF - BCF Python library
|
||||
Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
||||
Copyright (C) 2022 Andrea Ghensi <andrea.ghensi@gmail.com>
|
||||
|
||||
This file is part of BCF.
|
||||
|
||||
import os.path
|
||||
BCF is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
BCF is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import zipfile
|
||||
import tempfile
|
||||
from xml.dom import minidom
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
from bcf.v2.bcfxml import BcfXml as BcfXml2
|
||||
from bcf.v3.bcfxml import BcfXml as BcfXml3
|
||||
from bcf.v3.model import Version
|
||||
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||
|
||||
|
||||
def load(filepath):
|
||||
filepath = extract_project(filepath)
|
||||
if os.path.isfile(os.path.join(filepath, "bcf.version")):
|
||||
version_path = os.path.join(filepath, "bcf.version")
|
||||
version_id = get_version(version_path)
|
||||
# TODO: we actually coded it for 2.1, let's check the difference between 2.0 and 2.1
|
||||
if version_id == "2.1" or version_id == "2.0":
|
||||
from bcf.v2.bcfxml import BcfXml
|
||||
def load(
|
||||
filepath: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None
|
||||
) -> Optional[Union[BcfXml2, BcfXml3]]:
|
||||
"""
|
||||
Load a BCF file.
|
||||
|
||||
bcfxml = BcfXml()
|
||||
bcfxml.filepath = filepath
|
||||
return bcfxml
|
||||
elif version_id == "3.0":
|
||||
from bcf.v3.bcfxml import BcfXml
|
||||
Args:
|
||||
filepath: The path to the BCF file.
|
||||
|
||||
bcfxml = BcfXml()
|
||||
bcfxml.filepath = filepath
|
||||
return bcfxml
|
||||
else:
|
||||
raise Exception(f"Version {version_id} not supported.")
|
||||
Returns:
|
||||
The loaded BCF file.
|
||||
|
||||
Raises:
|
||||
ValueError: If the BCF version is not supported.
|
||||
"""
|
||||
xml_handler = xml_handler or XmlParserSerializer()
|
||||
version_id = _get_version(filepath, xml_handler)
|
||||
if version_id in {"2.1", "2.0"}:
|
||||
return BcfXml2.load(filepath, xml_handler)
|
||||
if version_id == "3.0":
|
||||
return BcfXml3.load(filepath, xml_handler)
|
||||
raise ValueError(f"Version {version_id} not supported.")
|
||||
|
||||
|
||||
def get_version(version_path):
|
||||
xmlparse = minidom.parse(version_path)
|
||||
version_el = xmlparse.getElementsByTagName("Version")[0]
|
||||
version = version_el.getAttribute("VersionId")
|
||||
return version
|
||||
def _get_version(filepath: Union[str, Path], xml_handler: Optional[AbstractXmlParserSerializer] = None) -> str:
|
||||
"""
|
||||
Returns the version of the BCF file.
|
||||
|
||||
Args:
|
||||
filepath: The path to the BCF file.
|
||||
xml_handler: The XML handler. If none is given, XmlParserSerializer is used.
|
||||
|
||||
def extract_project(filepath):
|
||||
if not filepath:
|
||||
return
|
||||
zip_file = zipfile.ZipFile(filepath)
|
||||
filepath = tempfile.mkdtemp()
|
||||
zip_file.extractall(filepath)
|
||||
return filepath
|
||||
Returns:
|
||||
The version of the BCF file.
|
||||
"""
|
||||
xml_handler = xml_handler or XmlParserSerializer()
|
||||
with zipfile.ZipFile(filepath) as bcf_zip:
|
||||
version = xml_handler.parse(bcf_zip.read("bcf.version"), Version)
|
||||
return version.version_id
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
|
||||
def calc_camera_vectors(
|
||||
elem_placement: NDArray[np.float_],
|
||||
) -> tuple[NDArray[np.float_], NDArray[np.float_], NDArray[np.float_]]:
|
||||
"""
|
||||
Calculate the vectors of a camera pointing to an element.
|
||||
|
||||
Args:
|
||||
elem_placement: Placement matrix of an element.
|
||||
|
||||
Returns:
|
||||
Camera position, direction and up vectors
|
||||
"""
|
||||
target_position = elem_placement[:3, 3]
|
||||
camera_position = target_position + np.array((5, 5, 5))
|
||||
camera_direction = unit_vector(camera_position - target_position)
|
||||
camera_right = unit_vector(np.cross(np.array([0.0, 0.0, 1.0]), camera_direction))
|
||||
camera_up = unit_vector(np.cross(camera_direction, camera_right))
|
||||
rotation_transform = np.eye(4)
|
||||
rotation_transform[0, :3] = camera_right
|
||||
rotation_transform[1, :3] = camera_up
|
||||
rotation_transform[2, :3] = camera_direction
|
||||
translation_transform = np.eye(4)
|
||||
translation_transform[:3, -1] = -camera_position
|
||||
look_at_transform = np.matmul(rotation_transform, translation_transform)
|
||||
mat = np.linalg.inv(look_at_transform)
|
||||
return camera_position, -mat[:3, 2], mat[:3, 1]
|
||||
|
||||
|
||||
def unit_vector(v: NDArray[np.float_]) -> NDArray[np.float_]:
|
||||
"""
|
||||
Return the unit vector of a vector.
|
||||
|
||||
Args:
|
||||
v: vector
|
||||
|
||||
Returns:
|
||||
unit vector.
|
||||
"""
|
||||
norm = np.linalg.norm(v)
|
||||
return v if norm == 0 else v / norm
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
In Memory Zip File management, taken from ruamel.std.zipfile
|
||||
|
||||
Copyright (c) 2017-2020 Anthon van der Neut, Ruamel bvba
|
||||
|
||||
original idea from https://stackoverflow.com/a/19722365/1307905
|
||||
"""
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
|
||||
class ZipFileInterface(Protocol):
|
||||
def writestr(self, filename_in_zip: str | zipfile.ZipInfo, file_contents: bytes | str) -> None:
|
||||
...
|
||||
|
||||
|
||||
class InMemoryZipFile:
|
||||
def __init__(
|
||||
self, file_name: Optional[str | Path] = None, compression: int = zipfile.ZIP_DEFLATED, debug: int = 0
|
||||
) -> None:
|
||||
# Create the in-memory file-like object
|
||||
self._file_name: Optional[str | Path] = str(file_name) if hasattr(file_name, "_from_parts") else file_name
|
||||
self.in_memory_data = BytesIO()
|
||||
# Create the in-memory zipfile
|
||||
self.in_memory_zip = zipfile.ZipFile(self.in_memory_data, "w", compression, False)
|
||||
self.in_memory_zip.debug = debug
|
||||
|
||||
def writestr(self, filename_in_zip: str | zipfile.ZipInfo, file_contents: bytes | str) -> None:
|
||||
"""Appends a file with name filename_in_zip and contents of
|
||||
file_contents to the in-memory zip."""
|
||||
self.in_memory_zip.writestr(filename_in_zip, file_contents)
|
||||
|
||||
def write_to_file(self, filename: str | bytes | PathLike[str] | PathLike[bytes] | int) -> None:
|
||||
"""Writes the in-memory zip to a file."""
|
||||
# Mark the files as having been created on Windows so that
|
||||
# Unix permissions are not inferred as 0000
|
||||
for zfile in self.in_memory_zip.filelist:
|
||||
zfile.create_system = 0
|
||||
self.in_memory_zip.close()
|
||||
with open(filename, "wb") as f:
|
||||
f.write(self.data)
|
||||
|
||||
@property
|
||||
def data(self) -> bytes:
|
||||
return self.in_memory_data.getvalue()
|
||||
|
||||
def __enter__(self) -> "InMemoryZipFile":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
||||
if self._file_name:
|
||||
self.write_to_file(self._file_name)
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# BCF - BCF Python library
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
@@ -16,4 +15,3 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
+251
-768
File diff suppressed because it is too large
Load Diff
@@ -1,198 +0,0 @@
|
||||
|
||||
# BCF - BCF Python library
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BCF.
|
||||
#
|
||||
# BCF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BCF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Project:
|
||||
def __init__(self):
|
||||
self.project_id = ""
|
||||
self.name = ""
|
||||
self.extension_schema = ""
|
||||
|
||||
|
||||
class BimSnippet:
|
||||
def __init__(self):
|
||||
self.snippet_type = None
|
||||
self.is_external = False
|
||||
self.reference = None
|
||||
self.reference_schema = None
|
||||
|
||||
|
||||
class DocumentReference:
|
||||
def __init__(self):
|
||||
self.referenced_document = None
|
||||
self.description = None
|
||||
self.guid = None
|
||||
self.is_external = False
|
||||
|
||||
|
||||
class RelatedTopic:
|
||||
def __init__(self):
|
||||
self.guid = None
|
||||
|
||||
|
||||
class HeaderFile:
|
||||
def __init__(self):
|
||||
self.filename = None
|
||||
self.date = None
|
||||
self.reference = None
|
||||
self.ifc_project = None
|
||||
self.ifc_spatial_structure_element = None
|
||||
self.is_external = True
|
||||
|
||||
|
||||
class Header:
|
||||
def __init__(self):
|
||||
self.files = []
|
||||
|
||||
|
||||
class Topic:
|
||||
def __init__(self):
|
||||
self.reference_links = []
|
||||
self.title = ""
|
||||
self.priority = None
|
||||
self.index = None # Deprecated, stored, but ignored
|
||||
self.labels = []
|
||||
self.creation_date = None
|
||||
self.creation_author = None
|
||||
self.modified_date = None
|
||||
self.modified_author = None
|
||||
self.due_date = None
|
||||
self.assigned_to = None
|
||||
self.stage = None
|
||||
self.description = None
|
||||
self.bim_snippet = None
|
||||
self.document_references = []
|
||||
self.related_topics = []
|
||||
self.topic_status = None
|
||||
self.topic_type = None
|
||||
self.guid = None
|
||||
|
||||
self.header = None
|
||||
self.comments = {}
|
||||
self.viewpoints = {}
|
||||
|
||||
|
||||
class Comment:
|
||||
def __init__(self):
|
||||
self.guid = None
|
||||
self.date = None
|
||||
self.author = None
|
||||
self.comment = None
|
||||
self.viewpoint = None
|
||||
self.modified_date = None
|
||||
self.modified_author = None
|
||||
self.topic_guid = None # Part of BCF-API
|
||||
|
||||
|
||||
class ViewSetupHints:
|
||||
def __init__(self):
|
||||
self.spaces_visible = False
|
||||
self.space_boundaries_visible = False
|
||||
self.openings_visible = False
|
||||
|
||||
|
||||
class Component:
|
||||
def __init__(self):
|
||||
self.originating_system = None
|
||||
self.authoring_tool_id = None
|
||||
self.ifc_guid = None
|
||||
|
||||
|
||||
class ComponentVisibility:
|
||||
def __init__(self):
|
||||
self.exceptions = []
|
||||
self.default_visibility = False
|
||||
|
||||
|
||||
class Color:
|
||||
def __init__(self):
|
||||
self.color = None
|
||||
self.components = []
|
||||
|
||||
|
||||
class Components:
|
||||
def __init__(self):
|
||||
self.view_setup_hints = None
|
||||
self.selection = []
|
||||
self.visibility = None
|
||||
self.coloring = []
|
||||
|
||||
|
||||
class Point:
|
||||
def __init__(self):
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.z = 0
|
||||
|
||||
|
||||
class Direction(Point):
|
||||
pass
|
||||
|
||||
|
||||
class OrthogonalCamera:
|
||||
def __init__(self):
|
||||
self.camera_view_point = Point()
|
||||
self.camera_direction = Direction()
|
||||
self.camera_up_vector = Direction()
|
||||
self.view_to_world_scale = 1.0
|
||||
|
||||
|
||||
class PerspectiveCamera:
|
||||
def __init__(self):
|
||||
self.camera_view_point = Point()
|
||||
self.camera_direction = Direction()
|
||||
self.camera_up_vector = Direction()
|
||||
self.field_of_view = 60.0
|
||||
|
||||
|
||||
class Line:
|
||||
def __init__(self):
|
||||
self.start_point = Point()
|
||||
self.end_point = Point()
|
||||
|
||||
|
||||
class ClippingPlane:
|
||||
def __init__(self):
|
||||
self.location = Point()
|
||||
self.direction = Direction()
|
||||
|
||||
|
||||
class Bitmap:
|
||||
def __init__(self):
|
||||
self.reference = "" # Only in BCF-XML
|
||||
self.bitmap_data = None # Only in BCF-API
|
||||
self.bitmap_format = "PNG" # Enum of png or jpg
|
||||
self.location = Point()
|
||||
self.normal = Direction()
|
||||
self.up = Direction()
|
||||
self.height = 1.0
|
||||
|
||||
|
||||
class Viewpoint:
|
||||
def __init__(self):
|
||||
self.guid = None
|
||||
self.viewpoint = None
|
||||
self.snapshot = None
|
||||
self.index = None
|
||||
|
||||
self.components = None # It's not a list, despite the plural name
|
||||
self.orthogonal_camera = None
|
||||
self.perspective_camera = None
|
||||
self.lines = []
|
||||
self.clipping_planes = []
|
||||
self.bitmaps = []
|
||||
@@ -0,0 +1,70 @@
|
||||
from bcf.v2.model.markup import (
|
||||
BimSnippet,
|
||||
Comment,
|
||||
CommentViewpoint,
|
||||
Header,
|
||||
HeaderFile,
|
||||
Markup,
|
||||
Topic,
|
||||
TopicDocumentReference,
|
||||
TopicRelatedTopic,
|
||||
ViewPoint,
|
||||
)
|
||||
from bcf.v2.model.project import Project, ProjectExtension
|
||||
from bcf.v2.model.version import Version
|
||||
from bcf.v2.model.visinfo import (
|
||||
BitmapFormat,
|
||||
ClippingPlane,
|
||||
Component,
|
||||
ComponentColoring,
|
||||
ComponentColoringColor,
|
||||
Components,
|
||||
ComponentSelection,
|
||||
ComponentVisibility,
|
||||
ComponentVisibilityExceptions,
|
||||
Direction,
|
||||
Line,
|
||||
OrthogonalCamera,
|
||||
PerspectiveCamera,
|
||||
Point,
|
||||
ViewSetupHints,
|
||||
VisualizationInfo,
|
||||
VisualizationInfoBitmap,
|
||||
VisualizationInfoClippingPlanes,
|
||||
VisualizationInfoLines,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BimSnippet",
|
||||
"Comment",
|
||||
"CommentViewpoint",
|
||||
"Header",
|
||||
"HeaderFile",
|
||||
"Markup",
|
||||
"Topic",
|
||||
"TopicDocumentReference",
|
||||
"TopicRelatedTopic",
|
||||
"ViewPoint",
|
||||
"Project",
|
||||
"ProjectExtension",
|
||||
"Version",
|
||||
"BitmapFormat",
|
||||
"ClippingPlane",
|
||||
"Component",
|
||||
"ComponentColoring",
|
||||
"ComponentColoringColor",
|
||||
"ComponentSelection",
|
||||
"ComponentVisibility",
|
||||
"ComponentVisibilityExceptions",
|
||||
"Components",
|
||||
"Direction",
|
||||
"Line",
|
||||
"OrthogonalCamera",
|
||||
"PerspectiveCamera",
|
||||
"Point",
|
||||
"ViewSetupHints",
|
||||
"VisualizationInfo",
|
||||
"VisualizationInfoBitmap",
|
||||
"VisualizationInfoClippingPlanes",
|
||||
"VisualizationInfoLines",
|
||||
]
|
||||
@@ -0,0 +1,461 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
from xsdata.models.datatype import XmlDateTime
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class BimSnippet:
|
||||
reference: str = field(
|
||||
metadata={
|
||||
"name": "Reference",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
reference_schema: str = field(
|
||||
metadata={
|
||||
"name": "ReferenceSchema",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
snippet_type: str = field(
|
||||
metadata={
|
||||
"name": "SnippetType",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
is_external: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"name": "isExternal",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class CommentViewpoint:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class HeaderFile:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
filename: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Filename",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
date: Optional[XmlDateTime] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Date",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
reference: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Reference",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
ifc_project: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "IfcProject",
|
||||
"type": "Attribute",
|
||||
"length": 22,
|
||||
"pattern": r"[0-9,A-Z,a-z,_$]*",
|
||||
}
|
||||
)
|
||||
ifc_spatial_structure_element: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "IfcSpatialStructureElement",
|
||||
"type": "Attribute",
|
||||
"length": 22,
|
||||
"pattern": r"[0-9,A-Z,a-z,_$]*",
|
||||
}
|
||||
)
|
||||
is_external: bool = field(
|
||||
default=True,
|
||||
metadata={
|
||||
"name": "isExternal",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicDocumentReference:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
referenced_document: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ReferencedDocument",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
description: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Description",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
guid: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||
}
|
||||
)
|
||||
is_external: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"name": "isExternal",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicRelatedTopic:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ViewPoint:
|
||||
viewpoint: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Viewpoint",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
snapshot: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Snapshot",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
index: Optional[int] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Index",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Comment:
|
||||
date: XmlDateTime = field(
|
||||
metadata={
|
||||
"name": "Date",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
author: str = field(
|
||||
metadata={
|
||||
"name": "Author",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
comment: str = field(
|
||||
metadata={
|
||||
"name": "Comment",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
viewpoint: Optional[CommentViewpoint] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Viewpoint",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
modified_date: Optional[XmlDateTime] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ModifiedDate",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
modified_author: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ModifiedAuthor",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Header:
|
||||
file: List[HeaderFile] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "File",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_occurs": 1,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Topic:
|
||||
reference_link: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "ReferenceLink",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
title: str = field(
|
||||
metadata={
|
||||
"name": "Title",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
priority: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Priority",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
index: Optional[int] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Index",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
labels: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Labels",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
creation_date: XmlDateTime = field(
|
||||
metadata={
|
||||
"name": "CreationDate",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
creation_author: str = field(
|
||||
metadata={
|
||||
"name": "CreationAuthor",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
modified_date: Optional[XmlDateTime] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ModifiedDate",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
modified_author: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ModifiedAuthor",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
due_date: Optional[XmlDateTime] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "DueDate",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
assigned_to: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "AssignedTo",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
stage: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Stage",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
description: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Description",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
bim_snippet: Optional[BimSnippet] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "BimSnippet",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
document_reference: List[TopicDocumentReference] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "DocumentReference",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
related_topic: List[TopicRelatedTopic] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "RelatedTopic",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||
}
|
||||
)
|
||||
topic_type: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "TopicType",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
topic_status: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "TopicStatus",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Markup:
|
||||
header: Optional[Header] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Header",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
topic: Topic = field(
|
||||
metadata={
|
||||
"name": "Topic",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
comment: List[Comment] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Comment",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
viewpoints: List[ViewPoint] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Viewpoints",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Project:
|
||||
name: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Name",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
project_id: str = field(
|
||||
metadata={
|
||||
"name": "ProjectId",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ProjectExtension:
|
||||
project: Optional[Project] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Project",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
extension_schema: str = field(
|
||||
metadata={
|
||||
"name": "ExtensionSchema",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Version:
|
||||
detailed_version: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "DetailedVersion",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
version_id: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "VersionId",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,476 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class BitmapFormat(Enum):
|
||||
PNG = "PNG"
|
||||
JPG = "JPG"
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Component:
|
||||
originating_system: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "OriginatingSystem",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
authoring_tool_id: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "AuthoringToolId",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
ifc_guid: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "IfcGuid",
|
||||
"type": "Attribute",
|
||||
"length": 22,
|
||||
"pattern": r"[0-9,A-Z,a-z,_$]*",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Direction:
|
||||
x: float = field(
|
||||
metadata={
|
||||
"name": "X",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
y: float = field(
|
||||
metadata={
|
||||
"name": "Y",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
z: float = field(
|
||||
metadata={
|
||||
"name": "Z",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Point:
|
||||
x: float = field(
|
||||
metadata={
|
||||
"name": "X",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
y: float = field(
|
||||
metadata={
|
||||
"name": "Y",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
z: float = field(
|
||||
metadata={
|
||||
"name": "Z",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ViewSetupHints:
|
||||
spaces_visible: Optional[bool] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "SpacesVisible",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
space_boundaries_visible: Optional[bool] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "SpaceBoundariesVisible",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
openings_visible: Optional[bool] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "OpeningsVisible",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ClippingPlane:
|
||||
location: Point = field(
|
||||
metadata={
|
||||
"name": "Location",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
direction: Direction = field(
|
||||
metadata={
|
||||
"name": "Direction",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentColoringColor:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
component: List[Component] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Component",
|
||||
"type": "Element",
|
||||
"min_occurs": 1,
|
||||
}
|
||||
)
|
||||
color: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Color",
|
||||
"type": "Attribute",
|
||||
"pattern": r"[0-9,a-f,A-F]{6}([0-9,a-f,A-F]{2})?",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentSelection:
|
||||
component: List[Component] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Component",
|
||||
"type": "Element",
|
||||
"min_occurs": 1,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentVisibilityExceptions:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
component: List[Component] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Component",
|
||||
"type": "Element",
|
||||
"min_occurs": 1,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Line:
|
||||
start_point: Point = field(
|
||||
metadata={
|
||||
"name": "StartPoint",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
end_point: Point = field(
|
||||
metadata={
|
||||
"name": "EndPoint",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class OrthogonalCamera:
|
||||
"""
|
||||
Attributes
|
||||
camera_view_point:
|
||||
camera_direction:
|
||||
camera_up_vector:
|
||||
view_to_world_scale: view's visible size in meters
|
||||
"""
|
||||
camera_view_point: Point = field(
|
||||
metadata={
|
||||
"name": "CameraViewPoint",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
camera_direction: Direction = field(
|
||||
metadata={
|
||||
"name": "CameraDirection",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
camera_up_vector: Direction = field(
|
||||
metadata={
|
||||
"name": "CameraUpVector",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
view_to_world_scale: float = field(
|
||||
metadata={
|
||||
"name": "ViewToWorldScale",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class PerspectiveCamera:
|
||||
"""
|
||||
Attributes
|
||||
camera_view_point:
|
||||
camera_direction:
|
||||
camera_up_vector:
|
||||
field_of_view: It is currently limited to a value between 45 and
|
||||
60 degrees. This limitation will be dropped in the next
|
||||
release and viewers should be expect values outside this
|
||||
range in current implementations.
|
||||
"""
|
||||
camera_view_point: Point = field(
|
||||
metadata={
|
||||
"name": "CameraViewPoint",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
camera_direction: Direction = field(
|
||||
metadata={
|
||||
"name": "CameraDirection",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
camera_up_vector: Direction = field(
|
||||
metadata={
|
||||
"name": "CameraUpVector",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
field_of_view: float = field(
|
||||
metadata={
|
||||
"name": "FieldOfView",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
"min_inclusive": 1.0,
|
||||
"max_inclusive": 170.0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class VisualizationInfoBitmap:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
bitmap: BitmapFormat = field(
|
||||
metadata={
|
||||
"name": "Bitmap",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
reference: str = field(
|
||||
metadata={
|
||||
"name": "Reference",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
location: Point = field(
|
||||
metadata={
|
||||
"name": "Location",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
normal: Direction = field(
|
||||
metadata={
|
||||
"name": "Normal",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
up: Direction = field(
|
||||
metadata={
|
||||
"name": "Up",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
height: float = field(
|
||||
metadata={
|
||||
"name": "Height",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentColoring:
|
||||
color: List[ComponentColoringColor] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Color",
|
||||
"type": "Element",
|
||||
"min_occurs": 1,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentVisibility:
|
||||
exceptions: Optional[ComponentVisibilityExceptions] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Exceptions",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
default_visibility: Optional[bool] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "DefaultVisibility",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class VisualizationInfoClippingPlanes:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
clipping_plane: List[ClippingPlane] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "ClippingPlane",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class VisualizationInfoLines:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
line: List[Line] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Line",
|
||||
"type": "Element",
|
||||
"min_occurs": 1,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Components:
|
||||
view_setup_hints: Optional[ViewSetupHints] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ViewSetupHints",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
selection: Optional[ComponentSelection] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Selection",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
visibility: ComponentVisibility = field(
|
||||
metadata={
|
||||
"name": "Visibility",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
coloring: Optional[ComponentColoring] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Coloring",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class VisualizationInfo:
|
||||
"""
|
||||
VisualizationInfo documentation.
|
||||
"""
|
||||
components: Optional[Components] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Components",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
orthogonal_camera: Optional[OrthogonalCamera] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "OrthogonalCamera",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
perspective_camera: Optional[PerspectiveCamera] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "PerspectiveCamera",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
lines: Optional[VisualizationInfoLines] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Lines",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
clipping_planes: Optional[VisualizationInfoClippingPlanes] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ClippingPlanes",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
bitmap: List[VisualizationInfoBitmap] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Bitmap",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,240 @@
|
||||
"""BCF XML V2 Topic handler."""
|
||||
import datetime
|
||||
import uuid
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from ifcopenshell import entity_instance
|
||||
from xsdata.models.datatype import XmlDateTime
|
||||
|
||||
import bcf.v2.model as mdl
|
||||
from bcf.inmemory_zipfile import ZipFileInterface
|
||||
from bcf.v2.visinfo import VisualizationInfoHandler
|
||||
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||
|
||||
|
||||
class TopicHandler:
|
||||
"""BCF Topic and related objects handler."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
topic_dir: Optional[zipfile.Path] = None,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> None:
|
||||
self._markup: Optional[mdl.Markup] = None
|
||||
self._viewpoints: dict[str, VisualizationInfoHandler] = {}
|
||||
self._reference_files: dict[str, bytes] = {}
|
||||
self._document_references: dict[str, bytes] = {}
|
||||
self._bim_snippet: Optional[bytes] = None
|
||||
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||
self._topic_dir = topic_dir
|
||||
|
||||
@property
|
||||
def markup(self) -> Optional[mdl.Markup]:
|
||||
if not self._markup:
|
||||
markup_path = self._topic_dir.joinpath("markup.bcf")
|
||||
if markup_path.exists():
|
||||
self._markup = self._xml_handler.parse(markup_path.read_bytes(), mdl.Markup)
|
||||
return self._markup
|
||||
|
||||
@markup.setter
|
||||
def markup(self, value: mdl.Markup) -> None:
|
||||
self._markup = value
|
||||
|
||||
@property
|
||||
def topic(self) -> mdl.Topic:
|
||||
"""Return the Topic object."""
|
||||
return self.markup.topic
|
||||
|
||||
@property
|
||||
def guid(self) -> str:
|
||||
"""Return the GUID of the topic."""
|
||||
if self._markup:
|
||||
return self.topic.guid
|
||||
return self._topic_dir.name if self._topic_dir else ""
|
||||
|
||||
@property
|
||||
def header(self) -> Optional[mdl.Header]:
|
||||
"""Return the header of the topic."""
|
||||
return self.markup.header if self.markup else None
|
||||
|
||||
@property
|
||||
def comments(self) -> list[mdl.Comment]:
|
||||
"""Return the comments of the topic."""
|
||||
return self.markup.comment if self.markup else []
|
||||
|
||||
@property
|
||||
def bim_snippet(self) -> Optional[bytes]:
|
||||
if not self._bim_snippet and self._topic_dir:
|
||||
self._bim_snippet = self._load_bim_snippet()
|
||||
return self._bim_snippet
|
||||
|
||||
@bim_snippet.setter
|
||||
def bim_snippet(self, value: bytes) -> None:
|
||||
self._bim_snippet = value
|
||||
|
||||
@property
|
||||
def viewpoints(self) -> dict[str, VisualizationInfoHandler]:
|
||||
if not self._viewpoints and self._topic_dir:
|
||||
self._viewpoints = self._load_viewpoints()
|
||||
return self._viewpoints
|
||||
|
||||
@property
|
||||
def reference_files(self) -> dict[str, bytes]:
|
||||
if self._reference_files or not self.header:
|
||||
return self._reference_files
|
||||
for ref in self.header.file:
|
||||
if ref.is_external:
|
||||
continue
|
||||
real_path = self._topic_dir
|
||||
for path_part in ref.reference.split("/"):
|
||||
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
|
||||
self._reference_files[ref.reference] = real_path.read_bytes()
|
||||
return self._reference_files
|
||||
|
||||
@property
|
||||
def document_references(self) -> dict[str, bytes]:
|
||||
if self._document_references or not self.topic:
|
||||
return self._document_references
|
||||
for doc in self.topic.document_reference:
|
||||
if doc.is_external or not doc.referenced_document:
|
||||
continue
|
||||
real_path = self._topic_dir
|
||||
for path_part in doc.referenced_document.split("/"):
|
||||
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
|
||||
self._document_references[doc.referenced_document] = real_path.read_bytes()
|
||||
return self._document_references
|
||||
|
||||
def _load_bim_snippet(self) -> Optional[bytes]:
|
||||
bim_snippet_obj = self.topic.bim_snippet
|
||||
if bim_snippet_obj and not bim_snippet_obj.is_external:
|
||||
bim_snippet_path = self._topic_dir.joinpath(bim_snippet_obj.reference)
|
||||
if bim_snippet_path.exists():
|
||||
return bim_snippet_path.read_bytes()
|
||||
return None
|
||||
|
||||
def _load_viewpoints(self) -> dict[str, VisualizationInfoHandler]:
|
||||
if self.markup and (viewpoints := self.markup.viewpoints):
|
||||
return VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints)
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def create_new(
|
||||
cls,
|
||||
title: str,
|
||||
description: str,
|
||||
author: str,
|
||||
topic_type: str = "",
|
||||
topic_status: str = "",
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> "TopicHandler":
|
||||
"""
|
||||
Create a new BCF topic.
|
||||
|
||||
Args:
|
||||
title: The title of the topic.
|
||||
description: The description of the topic.
|
||||
author: The author of the topic.
|
||||
topic_type: The type of the topic.
|
||||
topic_status: The status of the topic.
|
||||
xml_handler: The XML parser/serializer to use.
|
||||
|
||||
Returns:
|
||||
The BCF topic definition.
|
||||
"""
|
||||
creation_date = XmlDateTime.from_datetime(datetime.datetime.now())
|
||||
guid = str(uuid.uuid4())
|
||||
topic = mdl.Topic(
|
||||
title=title,
|
||||
description=description,
|
||||
creation_author=author,
|
||||
creation_date=creation_date,
|
||||
guid=guid,
|
||||
topic_type=topic_type,
|
||||
topic_status=topic_status,
|
||||
)
|
||||
markup = mdl.Markup(topic=topic)
|
||||
obj = cls(topic_dir=Path(guid), xml_handler=xml_handler or XmlParserSerializer())
|
||||
obj.markup = markup
|
||||
return obj
|
||||
|
||||
def save(self, destination_zip: ZipFileInterface) -> None:
|
||||
"""
|
||||
Save the topic to a BCF zip file.
|
||||
|
||||
Args:
|
||||
bcf_zip: The BCF zip file to save to.
|
||||
"""
|
||||
topic_dir = self.guid
|
||||
self._save_xml(destination_zip, self._markup, "markup.bcf")
|
||||
self._save_viewpoints(destination_zip, topic_dir)
|
||||
self._save_bim_snippet(destination_zip)
|
||||
self._save_reference_files(destination_zip)
|
||||
self._save_document_references(destination_zip)
|
||||
|
||||
def _save_viewpoints(self, destination_zip: ZipFileInterface, topic_dir: str) -> None:
|
||||
if not self.markup or not (viewpoints := self.markup.viewpoints):
|
||||
return
|
||||
for vpt in viewpoints:
|
||||
if vpt.viewpoint:
|
||||
self.viewpoints[vpt.viewpoint].save(destination_zip, topic_dir, vpt)
|
||||
|
||||
def _save_xml(self, destination_zip: ZipFileInterface, item: Any, target: str) -> None:
|
||||
if self._topic_dir is None:
|
||||
return
|
||||
to_write = self._xml_handler.serialize(item) if item else self._topic_dir.joinpath(target).read_bytes()
|
||||
destination_zip.writestr(f"{self._topic_dir.name}/{target}", to_write)
|
||||
|
||||
def _save_bim_snippet(self, destination_zip: ZipFileInterface) -> None:
|
||||
snippet = self.topic.bim_snippet
|
||||
if not snippet or snippet.is_external:
|
||||
return
|
||||
ref_filename = Path(snippet.reference).name
|
||||
if self.bim_snippet:
|
||||
destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet)
|
||||
|
||||
def _save_reference_files(self, destination_zip: ZipFileInterface) -> None:
|
||||
if not self.header:
|
||||
return
|
||||
for ref in self.header.file:
|
||||
if ref.is_external or not ref.reference:
|
||||
continue
|
||||
real_path = self._topic_dir
|
||||
for path_part in ref.reference.split("/"):
|
||||
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
|
||||
destination_zip.writestr(real_path.at, self.reference_files[ref.reference])
|
||||
|
||||
def _save_document_references(self, destination_zip: ZipFileInterface) -> None:
|
||||
if not self.topic:
|
||||
return
|
||||
for doc in self.topic.document_reference:
|
||||
if doc.is_external or not doc.referenced_document:
|
||||
continue
|
||||
real_path = self._topic_dir
|
||||
for path_part in doc.referenced_document.split("/"):
|
||||
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
|
||||
destination_zip.writestr(real_path.at, self.document_references[doc.referenced_document])
|
||||
|
||||
def add_viewpoint(self, element: entity_instance) -> None:
|
||||
"""
|
||||
Add a viewpoint tergeting an IFC element to the topic.
|
||||
|
||||
Args:
|
||||
element: The IFC element.
|
||||
"""
|
||||
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
|
||||
self.add_visinfo_handler(new_viewpoint)
|
||||
|
||||
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
|
||||
self.viewpoints[new_viewpoint.guid] = new_viewpoint
|
||||
self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, TopicHandler):
|
||||
raise TypeError("Equality needs a BcfXml object.")
|
||||
return (
|
||||
self.markup == other.markup
|
||||
and self.viewpoints == other.viewpoints
|
||||
and self.bim_snippet == other.bim_snippet
|
||||
)
|
||||
@@ -0,0 +1,246 @@
|
||||
import uuid
|
||||
import zipfile
|
||||
from functools import lru_cache
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
import numpy as np
|
||||
from ifcopenshell import entity_instance
|
||||
from ifcopenshell.util import placement
|
||||
from numpy.typing import NDArray
|
||||
|
||||
import bcf.v2.model as mdl
|
||||
from bcf.geometry import calc_camera_vectors
|
||||
from bcf.inmemory_zipfile import ZipFileInterface
|
||||
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||
|
||||
|
||||
class VisualizationInfoHandler:
|
||||
"""Handle the VisualizationInfo and related objects."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
visualization_info: mdl.VisualizationInfo,
|
||||
snapshot: Optional[bytes] = None,
|
||||
bitmaps: Optional[dict[str, bytes]] = None,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> None:
|
||||
self.visualization_info = visualization_info
|
||||
self.snapshot = snapshot
|
||||
self.bitmaps = bitmaps or {}
|
||||
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||
|
||||
@property
|
||||
def guid(self) -> str:
|
||||
"""Return the GUID of the visualization info."""
|
||||
return self.visualization_info.guid
|
||||
|
||||
@classmethod
|
||||
def from_topic_viewpoints(
|
||||
cls,
|
||||
topic_dir: zipfile.Path,
|
||||
vps: Iterable[mdl.ViewPoint],
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> dict[str, "VisualizationInfoHandler"]:
|
||||
"""Create VisualizationInfoHandler objects of a Topic's ViewPoints."""
|
||||
viewpoints = {}
|
||||
for vpt in vps:
|
||||
visinfo = cls.load(topic_dir, vpt, xml_handler)
|
||||
if visinfo and vpt.viewpoint:
|
||||
viewpoints[vpt.viewpoint] = visinfo
|
||||
return viewpoints
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
topic_dir: zipfile.Path,
|
||||
vpt: mdl.ViewPoint,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> Optional["VisualizationInfoHandler"]:
|
||||
"""
|
||||
Load the VisualizationInfo and related objects from a BCF zip file.
|
||||
|
||||
Args:
|
||||
topic_dir: The directory in the BCF zip file to load from.
|
||||
vpt: The ViewPoint to load.
|
||||
xml_handler: The XML handler to use to parse the VisualizationInfo.
|
||||
|
||||
Returns:
|
||||
The VisualizationInfoHandler object.
|
||||
"""
|
||||
visinfo = cls._load_visinfo(topic_dir, vpt.viewpoint, xml_handler)
|
||||
if not visinfo:
|
||||
return None
|
||||
snapshot = cls._load_snapshot(topic_dir, vpt.snapshot)
|
||||
bitmaps = cls._load_bitmaps(topic_dir, visinfo)
|
||||
return cls(visinfo, snapshot, bitmaps, xml_handler)
|
||||
|
||||
@staticmethod
|
||||
def _load_visinfo(
|
||||
topic_dir: zipfile.Path,
|
||||
vp_name: Optional[str],
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> Optional[mdl.VisualizationInfo]:
|
||||
if not vp_name:
|
||||
return None
|
||||
vp_path = topic_dir.joinpath(vp_name)
|
||||
if vp_path.exists():
|
||||
xml_handler = xml_handler or XmlParserSerializer()
|
||||
return xml_handler.parse(vp_path.read_bytes(), mdl.VisualizationInfo)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _load_snapshot(topic_dir: zipfile.Path, vp_snapshot: Optional[str]) -> Optional[bytes]:
|
||||
if vp_snapshot:
|
||||
snapshot_path = topic_dir.joinpath(vp_snapshot)
|
||||
if snapshot_path.exists():
|
||||
return snapshot_path.read_bytes()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _load_bitmaps(topic_dir: zipfile.Path, visinfo: Optional[mdl.VisualizationInfo]) -> dict[str, bytes]:
|
||||
if not visinfo or not (bitmaps := visinfo.bitmap):
|
||||
return {}
|
||||
bitmaps_dict = {}
|
||||
for bitmap in bitmaps:
|
||||
if not bitmap.reference:
|
||||
continue
|
||||
bitmap_path = topic_dir.joinpath(bitmap.reference)
|
||||
if bitmap_path.exists():
|
||||
bitmaps_dict[bitmap.reference] = bitmap_path.read_bytes()
|
||||
return bitmaps_dict
|
||||
|
||||
def save(
|
||||
self,
|
||||
bcf_zip: ZipFileInterface,
|
||||
topic_dir: str,
|
||||
vpt: mdl.ViewPoint,
|
||||
) -> None:
|
||||
"""
|
||||
Save the VisualizationInfo and related objects to a BCF zip file.
|
||||
|
||||
Args:
|
||||
bcf_zip: The BCF zip file to save to.
|
||||
topic_dir: The directory in the BCF zip file to save to.
|
||||
vpt: The ViewPoint to save.
|
||||
"""
|
||||
if not (vp_name := vpt.viewpoint):
|
||||
return
|
||||
self._save_visinfo(bcf_zip, topic_dir, vp_name)
|
||||
self._save_snapshot(bcf_zip, topic_dir, vpt.snapshot)
|
||||
self._save_bitmaps(bcf_zip, topic_dir)
|
||||
|
||||
def _save_snapshot(self, bcf_zip: ZipFileInterface, topic_dir: str, filename: Optional[str]) -> None:
|
||||
if self.snapshot and filename:
|
||||
bcf_zip.writestr(f"{topic_dir}/{filename}", self.snapshot)
|
||||
|
||||
def _save_visinfo(self, bcf_zip: ZipFileInterface, topic_dir: str, vp_name: str) -> None:
|
||||
bcf_zip.writestr(
|
||||
f"{topic_dir}/{vp_name}",
|
||||
self._xml_handler.serialize(self.visualization_info),
|
||||
)
|
||||
|
||||
def _save_bitmaps(self, bcf_zip: ZipFileInterface, topic_dir: str) -> None:
|
||||
if not self.bitmaps:
|
||||
return
|
||||
if not (bitmaps_defs := self.visualization_info.bitmap):
|
||||
return
|
||||
for bitmap_def in bitmaps_defs:
|
||||
if not (bitmap_name := bitmap_def.reference):
|
||||
continue
|
||||
if bitmap_name in self.bitmaps:
|
||||
bcf_zip.writestr(f"{topic_dir}/{bitmap_name}", self.bitmaps[bitmap_name])
|
||||
|
||||
@classmethod
|
||||
def create_new(
|
||||
cls,
|
||||
element: entity_instance,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> "VisualizationInfoHandler":
|
||||
"""
|
||||
Create a new VisualizationInfoHandler object from an IFC element.
|
||||
|
||||
Args:
|
||||
element: The IFC element to point at.
|
||||
xml_handler: The XML handler to use.
|
||||
|
||||
Returns:
|
||||
The VisualizationInfoHandler object.
|
||||
"""
|
||||
xml_handler = xml_handler or XmlParserSerializer()
|
||||
return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
|
||||
"""
|
||||
Return a BCF viewpoint of an IFC element.
|
||||
|
||||
This function is cached to speedudp the creation of multiple BCF topics regarding the same element.
|
||||
|
||||
Args:
|
||||
element: The IFC element to point at.
|
||||
|
||||
Returns:
|
||||
The BCF viewpoint definition.
|
||||
"""
|
||||
elem_placement = placement.get_local_placement(element.ObjectPlacement)
|
||||
|
||||
return mdl.VisualizationInfo(
|
||||
guid=str(uuid.uuid4()),
|
||||
components=build_components(element.GlobalId),
|
||||
perspective_camera=build_camera(elem_placement),
|
||||
)
|
||||
|
||||
|
||||
def build_components(guid: str) -> mdl.Components:
|
||||
"""
|
||||
Return the BCF components from an IFC element GUID.
|
||||
|
||||
Args:
|
||||
guid: The IFC element GUID.
|
||||
|
||||
Returns:
|
||||
The BCF components definition.
|
||||
"""
|
||||
return mdl.Components(
|
||||
selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]),
|
||||
visibility=mdl.ComponentVisibility(default_visibility=True),
|
||||
)
|
||||
|
||||
|
||||
def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera:
|
||||
"""
|
||||
Return a BCF camera for an IFC element placement matrix.
|
||||
|
||||
Args:
|
||||
elem_placement: The IFC element placement as a rototranslation matrix.
|
||||
|
||||
Returns:
|
||||
The BCF camera definition.
|
||||
"""
|
||||
return build_camera_from_vectors(*calc_camera_vectors(elem_placement))
|
||||
|
||||
|
||||
def build_camera_from_vectors(
|
||||
camera_position: NDArray[np.float_], camera_dir: NDArray[np.float_], camera_up: NDArray[np.float_]
|
||||
) -> mdl.PerspectiveCamera:
|
||||
"""
|
||||
Return a BCF camera for an IFC element placement matrix.
|
||||
|
||||
Args:
|
||||
camera_position: camera position array
|
||||
camera_dir: camera direction versor
|
||||
camera_up_vector: camera up versor
|
||||
|
||||
Returns:
|
||||
The BCF camera definition.
|
||||
"""
|
||||
camera_viewpoint = mdl.Point(x=camera_position[0], y=camera_position[1], z=camera_position[2])
|
||||
camera_direction = mdl.Direction(x=camera_dir[0], y=camera_dir[1], z=camera_dir[2])
|
||||
camera_up_vector = mdl.Direction(x=camera_up[0], y=camera_up[1], z=camera_up[2])
|
||||
return mdl.PerspectiveCamera(
|
||||
camera_view_point=camera_viewpoint,
|
||||
camera_direction=camera_direction,
|
||||
camera_up_vector=camera_up_vector,
|
||||
field_of_view=60.0,
|
||||
)
|
||||
@@ -1,19 +1 @@
|
||||
|
||||
# BCF - BCF Python library
|
||||
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
||||
#
|
||||
# This file is part of BCF.
|
||||
#
|
||||
# BCF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BCF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""BCF XML v3 handler."""
|
||||
|
||||
+172
-184
@@ -1,4 +1,3 @@
|
||||
|
||||
# BCF - BCF Python library
|
||||
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
||||
#
|
||||
@@ -17,25 +16,27 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
import time
|
||||
import json
|
||||
import urllib
|
||||
import requests
|
||||
import webbrowser
|
||||
import http.server
|
||||
import base64
|
||||
import tempfile
|
||||
import http.server
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import urllib
|
||||
import uuid
|
||||
import webbrowser
|
||||
from re import A
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
client_id, client_secret = "", ""
|
||||
|
||||
|
||||
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
def do_GET(self) -> None:
|
||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||
self.server.auth_code = query.get("code", [""])[0]
|
||||
self.server.auth_state = query.get("state", [""])[0]
|
||||
self.server.auth_code = query.get("code", [""])[0] # type:ignore
|
||||
self.server.auth_state = query.get("state", [""])[0] # type:ignore
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/plain")
|
||||
self.end_headers()
|
||||
@@ -43,20 +44,21 @@ class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
|
||||
class FoundationClient:
|
||||
def __init__(self, client_id, client_secret, base_url=None, redirect_subdir=None):
|
||||
def __init__(
|
||||
self, client_id: str, client_secret: str, base_url: Optional[str] = None, redirect_subdir: Optional[str] = None
|
||||
) -> None:
|
||||
self.baseurl = base_url
|
||||
self.access_token = ""
|
||||
self.refresh_token = ""
|
||||
self.access_token_expires_on = time.time()
|
||||
self.refresh_token_expires_on = float("inf")
|
||||
self.auth_endpoint = None
|
||||
self.token_endpoint = None
|
||||
self.token_endpoint = ""
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.auth_method = None
|
||||
self.auth_method: Optional[str] = None
|
||||
self.redirect_subdir = redirect_subdir
|
||||
|
||||
def get_access_token(self):
|
||||
def get_access_token(self) -> str:
|
||||
if self.access_token and self.access_token_expires_on > time.time():
|
||||
return self.access_token
|
||||
elif self.refresh_token and self.refresh_token_expires_on > time.time():
|
||||
@@ -65,18 +67,18 @@ class FoundationClient:
|
||||
self.login()
|
||||
return self.access_token
|
||||
|
||||
def get_auth_methods(self):
|
||||
def get_auth_methods(self) -> list[Any]:
|
||||
resp = requests.get(f"{self.baseurl}foundation/1.0/auth")
|
||||
return resp.json()["supported_oauth2_flows"]
|
||||
|
||||
def get_versions(self):
|
||||
def get_versions(self) -> list[Any]:
|
||||
resp = requests.get(f"{self.baseurl}foundation/versions")
|
||||
return resp.json()["versions"]
|
||||
|
||||
def login(self):
|
||||
def login(self) -> None:
|
||||
resp = requests.get(f"{self.baseurl}foundation/1.0/auth")
|
||||
values = resp.json()
|
||||
self.auth_endpoint = values["oauth2_auth_url"]
|
||||
auth_endpoint = values["oauth2_auth_url"]
|
||||
self.token_endpoint = values["oauth2_token_url"]
|
||||
|
||||
with http.server.HTTPServer(("", 8080), OAuthReceiver) as server:
|
||||
@@ -89,25 +91,22 @@ class FoundationClient:
|
||||
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
|
||||
}
|
||||
)
|
||||
if "?" in self.auth_endpoint:
|
||||
webbrowser.open(f"{self.auth_endpoint}&{query}")
|
||||
if "?" in auth_endpoint:
|
||||
webbrowser.open(f"{auth_endpoint}&{query}")
|
||||
else:
|
||||
webbrowser.open(f"{self.auth_endpoint}?{query}")
|
||||
webbrowser.open(f"{auth_endpoint}?{query}")
|
||||
server.timeout = 100
|
||||
server.state = state
|
||||
server.handle_request()
|
||||
if server.auth_code and server.auth_state == state:
|
||||
if server.auth_code and server.auth_state == state: # pylint: disable=E1101
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": server.auth_code,
|
||||
"code": server.auth_code, # pylint: disable=E1101 type:ignore
|
||||
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
|
||||
}
|
||||
auth_string = f"{self.client_id}:{self.client_secret}"
|
||||
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
||||
headers = {"Authorization": f"Basic {header_string}"}
|
||||
headers = self._get_access_token_headers()
|
||||
self.set_tokens_from_response(requests.post(self.token_endpoint, data=data, headers=headers))
|
||||
|
||||
def get_refresh_token(self):
|
||||
def get_refresh_token(self) -> None:
|
||||
self.set_tokens_from_response(
|
||||
requests.post(
|
||||
self.token_endpoint,
|
||||
@@ -118,10 +117,8 @@ class FoundationClient:
|
||||
).json()
|
||||
)
|
||||
|
||||
def get_new_access_token(self):
|
||||
auth_string = f"{self.client_id}:{self.client_secret}"
|
||||
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
||||
headers = {"Authorization": f"Basic {header_string}"}
|
||||
def get_new_access_token(self) -> None:
|
||||
headers = self._get_access_token_headers()
|
||||
self.set_tokens_from_response(
|
||||
requests.post(
|
||||
self.token_endpoint,
|
||||
@@ -133,35 +130,41 @@ class FoundationClient:
|
||||
).json()
|
||||
)
|
||||
|
||||
def set_auth_method(self, method="authorization_code_grant"):
|
||||
def _get_access_token_headers(self) -> dict[str, str]:
|
||||
auth_string = f"{self.client_id}:{self.client_secret}"
|
||||
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
||||
return {"Authorization": f"Basic {header_string}"}
|
||||
|
||||
def set_auth_method(self, method: str = "authorization_code_grant") -> None:
|
||||
if method != "authorization_code_grant":
|
||||
raise NotImplementedError(f"{method} not supported")
|
||||
else:
|
||||
self.auth_method = method
|
||||
|
||||
def set_tokens_from_response(self, response):
|
||||
response = response.json()
|
||||
self.access_token = response["access_token"]
|
||||
self.refresh_token = response["refresh_token"]
|
||||
self.access_token_expires_on = time.time() + response["expires_in"]
|
||||
if "refresh_token_expires_in" in response:
|
||||
self.refresh_token_expires_on = time.time() + response["refresh_token_expires_in"]
|
||||
def set_tokens_from_response(self, response: requests.Response) -> None:
|
||||
response_dict = response.json()
|
||||
self.access_token = response_dict["access_token"]
|
||||
self.refresh_token = response_dict["refresh_token"]
|
||||
self.access_token_expires_on = time.time() + response_dict["expires_in"]
|
||||
if "refresh_token_expires_in" in response_dict:
|
||||
self.refresh_token_expires_on = time.time() + response_dict["refresh_token_expires_in"]
|
||||
|
||||
|
||||
class BcfClient:
|
||||
def __init__(self, foundation_client):
|
||||
def __init__(self, foundation_client: FoundationClient) -> None:
|
||||
self.foundation_client = foundation_client
|
||||
self.version_id = None
|
||||
self.baseurl = None
|
||||
self.version_id: Optional[str] = None
|
||||
self.baseurl: Optional[str] = None
|
||||
self.filepath = tempfile.mkdtemp()
|
||||
|
||||
def set_version(self, version):
|
||||
def set_version(self, version: dict[str, str]) -> None:
|
||||
self.version_id = version["version_id"]
|
||||
self.baseurl = version["api_base_url"]
|
||||
|
||||
def get(self, endpoint, params=None, is_auth_required=False):
|
||||
def get(self, endpoint: str, params: Any = None, is_auth_required: bool = False) -> Any:
|
||||
# TODO: handle error http status codes and raise exception. Follow error.json standard.
|
||||
headers = {"Authorization": "Bearer " + self.foundation_client.get_access_token()}
|
||||
headers = {"Authorization": f"Bearer {self.foundation_client.get_access_token()}"}
|
||||
|
||||
response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
|
||||
try:
|
||||
response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
|
||||
@@ -171,68 +174,62 @@ class BcfClient:
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"message: {response.reason}' '{response.status_code}' '{ e }")
|
||||
|
||||
def post(self, endpoint, data=None, params=None):
|
||||
def post(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
|
||||
headers = {
|
||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
||||
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.baseurl}{endpoint}",
|
||||
headers=headers,
|
||||
params=params or None,
|
||||
data=data or None,
|
||||
f"{self.baseurl}{endpoint}", headers=headers, params=params or None, data=data or None
|
||||
)
|
||||
if response.status_code == 201:
|
||||
return response.status_code, response.text
|
||||
response.raise_for_status()
|
||||
|
||||
if response.status_code != 201:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
def put(self, endpoint, data=None, params=None):
|
||||
def put(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
|
||||
headers = {
|
||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
||||
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.put(
|
||||
f"{self.baseurl}{endpoint}",
|
||||
headers=headers,
|
||||
params=params or None,
|
||||
data=data or None,
|
||||
f"{self.baseurl}{endpoint}", headers=headers, params=params or None, data=data or None
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.status_code, response.text
|
||||
response.raise_for_status()
|
||||
|
||||
if response.status_code != 200:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
def delete(self, endpoint, params=None):
|
||||
def delete(self, endpoint: str, params: Any = None) -> Tuple[int, str]:
|
||||
headers = {
|
||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
||||
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.delete(
|
||||
f"{self.baseurl}{endpoint}",
|
||||
headers=headers,
|
||||
params=params or None,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.status_code, response.text
|
||||
response.raise_for_status()
|
||||
response = requests.delete(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
|
||||
|
||||
if response.status_code != 200:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
def get_projects(self) -> list:
|
||||
return self.get(
|
||||
f"/projects",
|
||||
)
|
||||
def get_projects(self) -> list[Any]:
|
||||
return self.get("/projects")
|
||||
|
||||
def get_project(
|
||||
self,
|
||||
project_id="",
|
||||
) -> dict:
|
||||
def get_project(self, project_id: str = "") -> dict[str, Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}",
|
||||
{
|
||||
@@ -240,16 +237,13 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def update_project(self, project_id="", data=None) -> dict:
|
||||
def update_project(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
|
||||
url = f"{self.baseurl}/projects/{project_id}"
|
||||
headers = {"Authorization": "Bearer " + self.foundation_client.get_access_token()}
|
||||
headers = {"Authorization": f"Bearer {self.foundation_client.get_access_token()}"}
|
||||
resp = requests.put(url, headers=headers, data=data)
|
||||
return resp.status_code, resp.text
|
||||
|
||||
def get_extensions(
|
||||
self,
|
||||
project_id="",
|
||||
) -> dict:
|
||||
def get_extensions(self, project_id: str = "") -> dict[str, Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/extensions",
|
||||
{
|
||||
@@ -259,10 +253,10 @@ class BcfClient:
|
||||
|
||||
def get_topics(
|
||||
self,
|
||||
project_id="",
|
||||
topics="",
|
||||
query_string=None,
|
||||
) -> list:
|
||||
project_id: str = "",
|
||||
topics: str = "",
|
||||
query_string: Optional[str] = None,
|
||||
) -> list[Any]:
|
||||
# return self.get(
|
||||
# f"/projects/{project_id}/topics",
|
||||
# {
|
||||
@@ -273,7 +267,7 @@ class BcfClient:
|
||||
# )
|
||||
pass
|
||||
|
||||
def get_topic(self, project_id="", topic_id="") -> dict:
|
||||
def get_topic(self, project_id: str = "", topic_id: str = "") -> dict[str, Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}",
|
||||
{
|
||||
@@ -282,42 +276,40 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def create_topic(self, project_id="", data=None):
|
||||
def create_topic(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
|
||||
return self.post(f"/projects/{project_id}/topics", data=data)
|
||||
|
||||
def update_topic(self, project_id="", topic_id="", data=None) -> dict:
|
||||
def update_topic(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
|
||||
return self.put(f"/projects/{project_id}/topics/{topic_id}", data=data)
|
||||
|
||||
def delete_topic(self, project_id="", topic_id=""):
|
||||
def delete_topic(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
|
||||
return self.delete(f"/projects/{project_id}/topics/{topic_id}")
|
||||
|
||||
def get_snippet(self, project_id="", topic_id="") -> str:
|
||||
def get_snippet(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
|
||||
headers = {
|
||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
||||
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||
"Content-type": "application/octet-stream",
|
||||
}
|
||||
response = requests.get(
|
||||
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet",
|
||||
headers=headers,
|
||||
)
|
||||
# TODO: write to tmpdir
|
||||
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_snippet.txt"), "wb") as f:
|
||||
f.write(response.content.decode("utf-8"))
|
||||
return response.status_code, response.content
|
||||
|
||||
def update_snippet(self, project_id="", topic_id="", files=None, data=None):
|
||||
response = requests.get(f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet", headers=headers)
|
||||
content = response.content.decode("utf-8")
|
||||
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_snippet.txt"), "w") as f:
|
||||
f.write(content)
|
||||
return response.status_code, content
|
||||
|
||||
def update_snippet(self, project_id: str = "", topic_id: str = "", files: Any = None, data: Any = None) -> int:
|
||||
headers = {
|
||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
||||
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||
"Content-type": "application/octet-stream",
|
||||
}
|
||||
|
||||
response = requests.put(
|
||||
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet",
|
||||
headers=headers,
|
||||
files=files,
|
||||
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet", headers=headers, files=files
|
||||
)
|
||||
|
||||
return response.status_code
|
||||
|
||||
def get_files_information(self, project_id="") -> list:
|
||||
def get_files_information(self, project_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/files_information",
|
||||
{
|
||||
@@ -325,7 +317,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_files(self, project_id="", topic_id="") -> list:
|
||||
def get_files(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/files",
|
||||
{
|
||||
@@ -336,32 +328,32 @@ class BcfClient:
|
||||
|
||||
def update_files(
|
||||
self,
|
||||
project_id="",
|
||||
topic_id="",
|
||||
data=None,
|
||||
params=None,
|
||||
):
|
||||
project_id: str = "",
|
||||
topic_id: str = "",
|
||||
data: Any = None,
|
||||
params: Any = None,
|
||||
) -> Tuple[int, str]:
|
||||
return self.put(
|
||||
f"/projects/{project_id}/topics/{topic_id}/files",
|
||||
data=data,
|
||||
)
|
||||
|
||||
def get_comments(self, project_id="", topic_id="") -> list:
|
||||
def get_comments(self, project_id: str = "", topic_id: str = "") -> None:
|
||||
pass
|
||||
|
||||
def create_comments(
|
||||
self,
|
||||
project_id="",
|
||||
topic_id="",
|
||||
data=None,
|
||||
params=None,
|
||||
):
|
||||
project_id: str = "",
|
||||
topic_id: str = "",
|
||||
data: Any = None,
|
||||
params: Any = None,
|
||||
) -> Tuple[int, str]:
|
||||
return self.post(
|
||||
f"/projects/{project_id}/topics/{topic_id}/comments",
|
||||
data=data,
|
||||
)
|
||||
|
||||
def get_comment(self, project_id="", topic_id="", comment_id="") -> dict:
|
||||
def get_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> dict[str, Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
|
||||
{
|
||||
@@ -371,22 +363,22 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def delete_comment(self, project_id="", topic_id="", comment_id=""):
|
||||
def delete_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> Tuple[int, str]:
|
||||
return self.delete(f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}")
|
||||
|
||||
def update_comment(
|
||||
self,
|
||||
project_id="",
|
||||
topic_id="",
|
||||
comment_id="",
|
||||
data=None,
|
||||
):
|
||||
project_id: str = "",
|
||||
topic_id: str = "",
|
||||
comment_id: str = "",
|
||||
data: Any = None,
|
||||
) -> Tuple[int, str]:
|
||||
return self.put(
|
||||
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
|
||||
data=data,
|
||||
)
|
||||
|
||||
def get_viewpoints(self, project_id="", topic_id="") -> list:
|
||||
def get_viewpoints(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
|
||||
{
|
||||
@@ -395,13 +387,13 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def create_viewpoints(self, project_id="", topic_id="", data=None):
|
||||
def create_viewpoints(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
|
||||
return self.post(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
|
||||
data=data,
|
||||
)
|
||||
|
||||
def get_viewpoint(self, project_id="", topic_id="", viewpoint_id="") -> dict:
|
||||
def get_viewpoint(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
|
||||
{
|
||||
@@ -413,15 +405,15 @@ class BcfClient:
|
||||
|
||||
def delete_viewpoint(
|
||||
self,
|
||||
project_id="",
|
||||
topic_id="",
|
||||
viewpoint_id="",
|
||||
):
|
||||
project_id: str = "",
|
||||
topic_id: str = "",
|
||||
viewpoint_id: str = "",
|
||||
) -> Tuple[int, str]:
|
||||
return self.delete(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
|
||||
)
|
||||
|
||||
def get_snapshot(self, project_id="", topic_id="", viewpoint_id="") -> str:
|
||||
def get_snapshot(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> str:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot",
|
||||
{
|
||||
@@ -431,7 +423,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_bitmap(self, project_id="", topic_id="", viewpoint_id="", bitmap_id="") -> str:
|
||||
def get_bitmap(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "", bitmap_id: str = "") -> str:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}",
|
||||
{
|
||||
@@ -442,7 +434,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_selection(self, project_id="", topic_id="", viewpoint_id="") -> dict:
|
||||
def get_selection(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection",
|
||||
{
|
||||
@@ -452,7 +444,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_coloring(self, project_id="", topic_id="", viewpoint_id="") -> dict:
|
||||
def get_coloring(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring",
|
||||
{
|
||||
@@ -462,7 +454,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_visibility(self, project_id="", topic_id="", viewpoint_id="") -> dict:
|
||||
def get_visibility(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility",
|
||||
{
|
||||
@@ -472,7 +464,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_related_topics(self, project_id="", topic_id="") -> list:
|
||||
def get_related_topics(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/related_topics",
|
||||
{
|
||||
@@ -483,16 +475,16 @@ class BcfClient:
|
||||
|
||||
def update_related_topics(
|
||||
self,
|
||||
project_id="",
|
||||
topic_id="",
|
||||
data=None,
|
||||
):
|
||||
project_id: str = "",
|
||||
topic_id: str = "",
|
||||
data: Any = None,
|
||||
) -> Tuple[int, str]:
|
||||
return self.put(
|
||||
f"/projects/{project_id}/topics/{topic_id}/related_topics",
|
||||
data=data,
|
||||
)
|
||||
|
||||
def get_document_references(self, project_id="", topic_id="") -> list:
|
||||
def get_document_references(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/document_references",
|
||||
{
|
||||
@@ -503,10 +495,10 @@ class BcfClient:
|
||||
|
||||
def create_document_reference(
|
||||
self,
|
||||
project_id="",
|
||||
topic_id="",
|
||||
data=None,
|
||||
):
|
||||
project_id: str = "",
|
||||
topic_id: str = "",
|
||||
data: Any = None,
|
||||
) -> Tuple[int, str]:
|
||||
return self.post(
|
||||
f"/projects/{project_id}/topics/{topic_id}/document_references",
|
||||
data=data,
|
||||
@@ -514,17 +506,17 @@ class BcfClient:
|
||||
|
||||
def update_document_references(
|
||||
self,
|
||||
project_id="",
|
||||
topic_id="",
|
||||
document_reference_id="",
|
||||
data=None,
|
||||
):
|
||||
project_id: str = "",
|
||||
topic_id: str = "",
|
||||
document_reference_id: str = "",
|
||||
data: Any = None,
|
||||
) -> Tuple[int, str]:
|
||||
return self.put(
|
||||
f"/projects/{project_id}/topics/{topic_id}/document_references/{document_reference_id}",
|
||||
data=data,
|
||||
)
|
||||
|
||||
def get_documents(self, project_id="", topic_id="") -> list:
|
||||
def get_documents(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/documents",
|
||||
{
|
||||
@@ -534,40 +526,36 @@ class BcfClient:
|
||||
)
|
||||
|
||||
def create_document(
|
||||
self,
|
||||
project_id="",
|
||||
topic_id="",
|
||||
guid=None,
|
||||
files=None,
|
||||
data=None,
|
||||
):
|
||||
self, project_id: str = "", topic_id: str = "", guid: Optional[str] = None, files: Any = None, data: Any = None
|
||||
) -> int:
|
||||
headers = {
|
||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
||||
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||
"Content-type": "application/octet-stream",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"/projects/{project_id}/topics/{topic_id}/documents",
|
||||
data=data,
|
||||
params={guid},
|
||||
params={"guid": guid},
|
||||
files=files,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
return response.status_code
|
||||
|
||||
def get_document(self, project_id="", topic_id="", document_id="") -> str:
|
||||
def get_document(self, project_id: str = "", topic_id: str = "", document_id: str = "") -> Tuple[int, str]:
|
||||
headers = {
|
||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
||||
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||
"Content-type": "application/octet-stream",
|
||||
}
|
||||
response = requests.get(
|
||||
f"{self.baseurl}/projects/{project_id}/topics/documents/{document_id}",
|
||||
headers=headers,
|
||||
)
|
||||
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_{document_id}_document.txt"), "wb") as f:
|
||||
f.write(response.content.decode("utf-8"))
|
||||
return response.status_code, response.content
|
||||
|
||||
def get_topics_events(self, project_id="") -> list:
|
||||
response = requests.get(f"{self.baseurl}/projects/{project_id}/topics/documents/{document_id}", headers=headers)
|
||||
content = response.content.decode("utf-8")
|
||||
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_{document_id}_document.txt"), "w") as f:
|
||||
f.write(content)
|
||||
return response.status_code, content
|
||||
|
||||
def get_topics_events(self, project_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/events",
|
||||
{
|
||||
@@ -575,7 +563,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_topic_events(self, project_id="", topic_id="") -> list:
|
||||
def get_topic_events(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/events",
|
||||
{
|
||||
@@ -584,7 +572,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_comments_events(self, project_id="") -> list:
|
||||
def get_comments_events(self, project_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/comments/events",
|
||||
{
|
||||
@@ -592,7 +580,7 @@ class BcfClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_comment_events(self, project_id="", topic_id="", comment_id="") -> list:
|
||||
def get_comment_events(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> list[Any]:
|
||||
return self.get(
|
||||
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events",
|
||||
{
|
||||
|
||||
+269
-825
File diff suppressed because it is too large
Load Diff
@@ -1,200 +0,0 @@
|
||||
|
||||
# BCF - BCF Python library
|
||||
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
||||
#
|
||||
# This file is part of BCF.
|
||||
#
|
||||
# BCF is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BCF is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Project:
|
||||
def __init__(self):
|
||||
self.project_id = ""
|
||||
self.name = ""
|
||||
|
||||
|
||||
class BimSnippet:
|
||||
def __init__(self):
|
||||
self.snippet_type = None
|
||||
self.is_external = False
|
||||
self.reference = None
|
||||
self.reference_schema = None
|
||||
|
||||
|
||||
class DocumentReference:
|
||||
def __init__(self):
|
||||
self.description = None
|
||||
self.document_guid = None
|
||||
self.url = None
|
||||
self.guid = None
|
||||
|
||||
|
||||
class RelatedTopic:
|
||||
def __init__(self):
|
||||
self.guid = None
|
||||
|
||||
|
||||
class HeaderFile:
|
||||
def __init__(self):
|
||||
self.filename = ""
|
||||
self.date = None
|
||||
self.reference = ""
|
||||
self.ifc_project = None
|
||||
self.ifc_spatial_structure_element = None
|
||||
self.is_external = True
|
||||
|
||||
|
||||
class Header:
|
||||
def __init__(self):
|
||||
self.files = []
|
||||
|
||||
|
||||
class Topic:
|
||||
def __init__(self):
|
||||
self.reference_links = []
|
||||
self.title = ""
|
||||
self.priority = None
|
||||
self.index = None # Deprecated, stored, but ignored
|
||||
self.labels = []
|
||||
self.creation_date = None
|
||||
self.creation_author = None
|
||||
self.modified_date = None
|
||||
self.modified_author = None
|
||||
self.due_date = None
|
||||
self.assigned_to = None
|
||||
self.stage = None
|
||||
self.description = None
|
||||
self.bim_snippet = None
|
||||
self.document_references = []
|
||||
self.related_topics = []
|
||||
self.topic_status = None
|
||||
self.topic_type = None
|
||||
self.guid = None
|
||||
|
||||
self.header = None
|
||||
self.comments = {}
|
||||
self.viewpoints = {}
|
||||
self.server_assigned_id = ""
|
||||
|
||||
|
||||
class Comment:
|
||||
def __init__(self):
|
||||
self.guid = None
|
||||
self.date = None
|
||||
self.author = ""
|
||||
self.comment = ""
|
||||
self.viewpoint = None
|
||||
self.modified_date = None
|
||||
self.modified_author = ""
|
||||
|
||||
|
||||
class ViewSetupHints:
|
||||
def __init__(self):
|
||||
self.spaces_visible = False
|
||||
self.space_boundaries_visible = False
|
||||
self.openings_visible = False
|
||||
|
||||
|
||||
class Component:
|
||||
def __init__(self):
|
||||
self.originating_system = None
|
||||
self.authoring_tool_id = None
|
||||
self.ifc_guid = None
|
||||
|
||||
|
||||
class ComponentVisibility:
|
||||
def __init__(self):
|
||||
self.exceptions = []
|
||||
self.default_visibility = False
|
||||
self.view_setup_hints = None
|
||||
|
||||
|
||||
class Color:
|
||||
def __init__(self):
|
||||
self.color = None
|
||||
self.components = []
|
||||
|
||||
|
||||
class Components:
|
||||
def __init__(self):
|
||||
|
||||
self.selection = []
|
||||
self.visibility = None
|
||||
self.coloring = []
|
||||
|
||||
|
||||
class Point:
|
||||
def __init__(self):
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.z = 0
|
||||
|
||||
|
||||
class Direction(Point):
|
||||
pass
|
||||
|
||||
|
||||
class OrthogonalCamera:
|
||||
def __init__(self):
|
||||
self.camera_view_point = Point()
|
||||
self.camera_direction = Direction()
|
||||
self.camera_up_vector = Direction()
|
||||
self.view_to_world_scale = 1.0
|
||||
self.aspect_ratio = 1.0
|
||||
|
||||
|
||||
class PerspectiveCamera:
|
||||
def __init__(self):
|
||||
self.camera_view_point = Point()
|
||||
self.camera_direction = Direction()
|
||||
self.camera_up_vector = Direction()
|
||||
self.field_of_view = 60.0
|
||||
self.aspect_ratio = 1.0
|
||||
|
||||
|
||||
class Line:
|
||||
def __init__(self):
|
||||
self.start_point = Point()
|
||||
self.end_point = Point()
|
||||
|
||||
|
||||
class ClippingPlane:
|
||||
def __init__(self):
|
||||
self.location = Point()
|
||||
self.direction = Direction()
|
||||
|
||||
|
||||
class Bitmap:
|
||||
def __init__(self):
|
||||
self.reference = "" # Only in BCF-XML
|
||||
self.bitmap_data = None # Only in BCF-API
|
||||
self.bitmap_format = "PNG" # Enum of png or jpg
|
||||
self.location = Point()
|
||||
self.normal = Direction()
|
||||
self.up = Direction()
|
||||
self.height = 1.0
|
||||
|
||||
|
||||
class Viewpoint:
|
||||
def __init__(self):
|
||||
self.guid = None
|
||||
self.viewpoint = None
|
||||
self.snapshot = None
|
||||
self.index = None
|
||||
|
||||
self.components = None # It's not a list, despite the plural name
|
||||
self.orthogonal_camera = None
|
||||
self.perspective_camera = None
|
||||
self.lines = []
|
||||
self.clipping_planes = []
|
||||
self.bitmaps = []
|
||||
@@ -0,0 +1,61 @@
|
||||
"""BCF XML V3 Documents handler."""
|
||||
import zipfile
|
||||
from typing import Any, Optional
|
||||
|
||||
import bcf.v3.model as mdl
|
||||
from bcf.inmemory_zipfile import ZipFileInterface
|
||||
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||
|
||||
|
||||
class DocumentsHandler:
|
||||
"""BCF documents handler."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
definition: mdl.DocumentInfo,
|
||||
documents: Optional[dict[str, bytes]] = None,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> None:
|
||||
self.definition = definition
|
||||
self.documents = documents or {}
|
||||
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
zip_file: zipfile.ZipFile,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> Optional["DocumentsHandler"]:
|
||||
"""
|
||||
Loads the documents from the given zip file directory.
|
||||
|
||||
Args:
|
||||
zip_path: The directory path inside the zip file.
|
||||
xml_handler: The xml parser/serializer to use.
|
||||
|
||||
Returns:
|
||||
The documents handler.
|
||||
"""
|
||||
xml_handler = xml_handler or XmlParserSerializer()
|
||||
file_to_open = zipfile.Path(zip_file, "documents.xml")
|
||||
if not file_to_open.exists():
|
||||
return None
|
||||
definition = xml_handler.parse(file_to_open.read_bytes(), mdl.DocumentInfo)
|
||||
documents = {}
|
||||
if def_docs := definition.documents:
|
||||
for document in def_docs.document:
|
||||
document_path = zipfile.Path(zip_file, f"documents/{document.guid}")
|
||||
if document_path.exists():
|
||||
documents[document.filename] = document_path.read_bytes()
|
||||
return cls(definition, documents=documents)
|
||||
|
||||
def save(self, bcf_zip: ZipFileInterface) -> None:
|
||||
"""Save the documents to the zip file."""
|
||||
bcf_zip.writestr("documents.xml", self._xml_handler.serialize(self.definition))
|
||||
if documents := self.definition.documents:
|
||||
for doc in documents.document:
|
||||
if doc.filename in self.documents:
|
||||
bcf_zip.writestr(
|
||||
f"documents/{doc.guid}",
|
||||
self.documents[doc.filename],
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
from bcf.v3.model.documents import Document, DocumentInfo, DocumentInfoDocuments
|
||||
from bcf.v3.model.extensions import (
|
||||
Extensions,
|
||||
ExtensionsPriorities,
|
||||
ExtensionsSnippetTypes,
|
||||
ExtensionsStages,
|
||||
ExtensionsTopicLabels,
|
||||
ExtensionsTopicStatuses,
|
||||
ExtensionsTopicTypes,
|
||||
ExtensionsUsers,
|
||||
)
|
||||
from bcf.v3.model.markup import (
|
||||
BimSnippet,
|
||||
Comment,
|
||||
CommentViewpoint,
|
||||
DocumentReference,
|
||||
File,
|
||||
Header,
|
||||
HeaderFiles,
|
||||
Markup,
|
||||
Topic,
|
||||
TopicComments,
|
||||
TopicDocumentReferences,
|
||||
TopicLabels,
|
||||
TopicReferenceLinks,
|
||||
TopicRelatedTopics,
|
||||
TopicRelatedTopicsRelatedTopic,
|
||||
TopicViewpoints,
|
||||
ViewPoint,
|
||||
)
|
||||
from bcf.v3.model.project import Project, ProjectInfo
|
||||
from bcf.v3.model.version import Version
|
||||
from bcf.v3.model.visinfo import (
|
||||
Bitmap,
|
||||
BitmapFormat,
|
||||
ClippingPlane,
|
||||
Component,
|
||||
ComponentColoring,
|
||||
ComponentColoringColor,
|
||||
ComponentColoringColorComponents,
|
||||
Components,
|
||||
ComponentSelection,
|
||||
ComponentVisibility,
|
||||
ComponentVisibilityExceptions,
|
||||
Direction,
|
||||
Line,
|
||||
OrthogonalCamera,
|
||||
PerspectiveCamera,
|
||||
Point,
|
||||
ViewSetupHints,
|
||||
VisualizationInfo,
|
||||
VisualizationInfoBitmaps,
|
||||
VisualizationInfoClippingPlanes,
|
||||
VisualizationInfoLines,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Document",
|
||||
"DocumentInfo",
|
||||
"DocumentInfoDocuments",
|
||||
"Extensions",
|
||||
"ExtensionsPriorities",
|
||||
"ExtensionsSnippetTypes",
|
||||
"ExtensionsStages",
|
||||
"ExtensionsTopicLabels",
|
||||
"ExtensionsTopicStatuses",
|
||||
"ExtensionsTopicTypes",
|
||||
"ExtensionsUsers",
|
||||
"BimSnippet",
|
||||
"Comment",
|
||||
"CommentViewpoint",
|
||||
"DocumentReference",
|
||||
"File",
|
||||
"Header",
|
||||
"HeaderFiles",
|
||||
"Markup",
|
||||
"Topic",
|
||||
"TopicComments",
|
||||
"TopicDocumentReferences",
|
||||
"TopicLabels",
|
||||
"TopicReferenceLinks",
|
||||
"TopicRelatedTopics",
|
||||
"TopicRelatedTopicsRelatedTopic",
|
||||
"TopicViewpoints",
|
||||
"ViewPoint",
|
||||
"Project",
|
||||
"ProjectInfo",
|
||||
"Version",
|
||||
"Bitmap",
|
||||
"BitmapFormat",
|
||||
"ClippingPlane",
|
||||
"Component",
|
||||
"ComponentColoring",
|
||||
"ComponentColoringColor",
|
||||
"ComponentColoringColorComponents",
|
||||
"ComponentSelection",
|
||||
"ComponentVisibility",
|
||||
"ComponentVisibilityExceptions",
|
||||
"Components",
|
||||
"Direction",
|
||||
"Line",
|
||||
"OrthogonalCamera",
|
||||
"PerspectiveCamera",
|
||||
"Point",
|
||||
"ViewSetupHints",
|
||||
"VisualizationInfo",
|
||||
"VisualizationInfoBitmaps",
|
||||
"VisualizationInfoClippingPlanes",
|
||||
"VisualizationInfoLines",
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Document:
|
||||
filename: str = field(
|
||||
metadata={
|
||||
"name": "Filename",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
description: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Description",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class DocumentInfoDocuments:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
document: List[Document] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Document",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class DocumentInfo:
|
||||
documents: Optional[DocumentInfoDocuments] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Documents",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,181 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ExtensionsPriorities:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
priority: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Priority",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ExtensionsSnippetTypes:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
snippet_type: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "SnippetType",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ExtensionsStages:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
stage: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Stage",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ExtensionsTopicLabels:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
topic_label: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "TopicLabel",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ExtensionsTopicStatuses:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
topic_status: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "TopicStatus",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ExtensionsTopicTypes:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
topic_type: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "TopicType",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ExtensionsUsers:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
user: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "User",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Extensions:
|
||||
topic_types: Optional[ExtensionsTopicTypes] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "TopicTypes",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
topic_statuses: Optional[ExtensionsTopicStatuses] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "TopicStatuses",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
priorities: Optional[ExtensionsPriorities] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Priorities",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
topic_labels: Optional[ExtensionsTopicLabels] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "TopicLabels",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
users: Optional[ExtensionsUsers] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Users",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
snippet_types: Optional[ExtensionsSnippetTypes] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "SnippetTypes",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
stages: Optional[ExtensionsStages] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Stages",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,616 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
from xsdata.models.datatype import XmlDateTime
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class BimSnippet:
|
||||
reference: str = field(
|
||||
metadata={
|
||||
"name": "Reference",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
reference_schema: str = field(
|
||||
metadata={
|
||||
"name": "ReferenceSchema",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
snippet_type: str = field(
|
||||
metadata={
|
||||
"name": "SnippetType",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
is_external: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"name": "IsExternal",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class CommentViewpoint:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class DocumentReference:
|
||||
document_guid: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "DocumentGuid",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
url: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Url",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
description: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Description",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class File:
|
||||
filename: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Filename",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
date: Optional[XmlDateTime] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Date",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
reference: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Reference",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
ifc_project: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "IfcProject",
|
||||
"type": "Attribute",
|
||||
"length": 22,
|
||||
"pattern": r"[0-9A-Za-z_$]*",
|
||||
}
|
||||
)
|
||||
ifc_spatial_structure_element: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "IfcSpatialStructureElement",
|
||||
"type": "Attribute",
|
||||
"length": 22,
|
||||
"pattern": r"[0-9A-Za-z_$]*",
|
||||
}
|
||||
)
|
||||
is_external: bool = field(
|
||||
default=True,
|
||||
metadata={
|
||||
"name": "IsExternal",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicLabels:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
label: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Label",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicReferenceLinks:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
reference_link: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "ReferenceLink",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicRelatedTopicsRelatedTopic:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ViewPoint:
|
||||
viewpoint: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Viewpoint",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
snapshot: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Snapshot",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
index: Optional[int] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Index",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Comment:
|
||||
date: XmlDateTime = field(
|
||||
metadata={
|
||||
"name": "Date",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
author: str = field(
|
||||
metadata={
|
||||
"name": "Author",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
comment: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Comment",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
viewpoint: Optional[CommentViewpoint] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Viewpoint",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
modified_date: Optional[XmlDateTime] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ModifiedDate",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
modified_author: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ModifiedAuthor",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class HeaderFiles:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
file: List[File] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "File",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicDocumentReferences:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
document_reference: List[DocumentReference] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "DocumentReference",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicRelatedTopics:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
related_topic: List[TopicRelatedTopicsRelatedTopic] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "RelatedTopic",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicViewpoints:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
view_point: List[ViewPoint] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "ViewPoint",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Header:
|
||||
files: Optional[HeaderFiles] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Files",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class TopicComments:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
comment: List[Comment] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Comment",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Topic:
|
||||
reference_links: Optional[TopicReferenceLinks] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ReferenceLinks",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
title: str = field(
|
||||
metadata={
|
||||
"name": "Title",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
priority: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Priority",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
index: Optional[int] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Index",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
labels: Optional[TopicLabels] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Labels",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
creation_date: XmlDateTime = field(
|
||||
metadata={
|
||||
"name": "CreationDate",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
creation_author: str = field(
|
||||
metadata={
|
||||
"name": "CreationAuthor",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
modified_date: Optional[XmlDateTime] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ModifiedDate",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
modified_author: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ModifiedAuthor",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
due_date: Optional[XmlDateTime] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "DueDate",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
assigned_to: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "AssignedTo",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
stage: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Stage",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
description: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Description",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
bim_snippet: Optional[BimSnippet] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "BimSnippet",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
document_references: Optional[TopicDocumentReferences] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "DocumentReferences",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
related_topics: Optional[TopicRelatedTopics] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "RelatedTopics",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
comments: Optional[TopicComments] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Comments",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
viewpoints: Optional[TopicViewpoints] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Viewpoints",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
server_assigned_id: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ServerAssignedId",
|
||||
"type": "Attribute",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
topic_type: str = field(
|
||||
metadata={
|
||||
"name": "TopicType",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
topic_status: str = field(
|
||||
metadata={
|
||||
"name": "TopicStatus",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Markup:
|
||||
header: Optional[Header] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Header",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
}
|
||||
)
|
||||
topic: Topic = field(
|
||||
metadata={
|
||||
"name": "Topic",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Project:
|
||||
name: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Name",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
project_id: str = field(
|
||||
metadata={
|
||||
"name": "ProjectId",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ProjectInfo:
|
||||
project: Project = field(
|
||||
metadata={
|
||||
"name": "Project",
|
||||
"type": "Element",
|
||||
"namespace": "",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Version:
|
||||
version_id: str = field(
|
||||
metadata={
|
||||
"name": "VersionId",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,524 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class BitmapFormat(Enum):
|
||||
PNG = "png"
|
||||
JPG = "jpg"
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Component:
|
||||
originating_system: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "OriginatingSystem",
|
||||
"type": "Element",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
authoring_tool_id: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "AuthoringToolId",
|
||||
"type": "Element",
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
ifc_guid: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "IfcGuid",
|
||||
"type": "Attribute",
|
||||
"length": 22,
|
||||
"pattern": r"[0-9A-Za-z_$]*",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Direction:
|
||||
x: float = field(
|
||||
metadata={
|
||||
"name": "X",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
y: float = field(
|
||||
metadata={
|
||||
"name": "Y",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
z: float = field(
|
||||
metadata={
|
||||
"name": "Z",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Point:
|
||||
x: float = field(
|
||||
metadata={
|
||||
"name": "X",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
y: float = field(
|
||||
metadata={
|
||||
"name": "Y",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
z: float = field(
|
||||
metadata={
|
||||
"name": "Z",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ViewSetupHints:
|
||||
spaces_visible: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"name": "SpacesVisible",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
space_boundaries_visible: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"name": "SpaceBoundariesVisible",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
openings_visible: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"name": "OpeningsVisible",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Bitmap:
|
||||
format: BitmapFormat = field(
|
||||
metadata={
|
||||
"name": "Format",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
reference: str = field(
|
||||
metadata={
|
||||
"name": "Reference",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
"min_length": 1,
|
||||
"white_space": "collapse",
|
||||
}
|
||||
)
|
||||
location: Point = field(
|
||||
metadata={
|
||||
"name": "Location",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
normal: Direction = field(
|
||||
metadata={
|
||||
"name": "Normal",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
up: Direction = field(
|
||||
metadata={
|
||||
"name": "Up",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
height: float = field(
|
||||
metadata={
|
||||
"name": "Height",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ClippingPlane:
|
||||
location: Point = field(
|
||||
metadata={
|
||||
"name": "Location",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
direction: Direction = field(
|
||||
metadata={
|
||||
"name": "Direction",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentColoringColorComponents:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
component: List[Component] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Component",
|
||||
"type": "Element",
|
||||
"min_occurs": 1,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentSelection:
|
||||
component: List[Component] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Component",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentVisibilityExceptions:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
component: List[Component] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Component",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Line:
|
||||
start_point: Point = field(
|
||||
metadata={
|
||||
"name": "StartPoint",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
end_point: Point = field(
|
||||
metadata={
|
||||
"name": "EndPoint",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class OrthogonalCamera:
|
||||
"""
|
||||
Attributes
|
||||
camera_view_point:
|
||||
camera_direction:
|
||||
camera_up_vector:
|
||||
view_to_world_scale: view's visible vertical size in meters
|
||||
aspect_ratio: Proportional relationship between the width and
|
||||
the height of the view (w/h).
|
||||
"""
|
||||
camera_view_point: Point = field(
|
||||
metadata={
|
||||
"name": "CameraViewPoint",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
camera_direction: Direction = field(
|
||||
metadata={
|
||||
"name": "CameraDirection",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
camera_up_vector: Direction = field(
|
||||
metadata={
|
||||
"name": "CameraUpVector",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
view_to_world_scale: float = field(
|
||||
metadata={
|
||||
"name": "ViewToWorldScale",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
aspect_ratio: float = field(
|
||||
metadata={
|
||||
"name": "AspectRatio",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
"min_exclusive": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class PerspectiveCamera:
|
||||
"""
|
||||
Attributes
|
||||
camera_view_point:
|
||||
camera_direction:
|
||||
camera_up_vector:
|
||||
field_of_view: Vertical field of view, in degrees. It is
|
||||
currently limited to a value between 45 and 60 degrees. This
|
||||
limitation will be dropped in the next release and viewers
|
||||
should be expect values outside this range in current
|
||||
implementations.
|
||||
aspect_ratio: Proportional relationship between the width and
|
||||
the height of the view (w/h).
|
||||
"""
|
||||
camera_view_point: Point = field(
|
||||
metadata={
|
||||
"name": "CameraViewPoint",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
camera_direction: Direction = field(
|
||||
metadata={
|
||||
"name": "CameraDirection",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
camera_up_vector: Direction = field(
|
||||
metadata={
|
||||
"name": "CameraUpVector",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
field_of_view: float = field(
|
||||
metadata={
|
||||
"name": "FieldOfView",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
"min_exclusive": 0.0,
|
||||
"max_exclusive": 180.0,
|
||||
}
|
||||
)
|
||||
aspect_ratio: float = field(
|
||||
metadata={
|
||||
"name": "AspectRatio",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
"min_exclusive": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentColoringColor:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
components: ComponentColoringColorComponents = field(
|
||||
metadata={
|
||||
"name": "Components",
|
||||
"type": "Element",
|
||||
"required": True,
|
||||
}
|
||||
)
|
||||
color: str = field(
|
||||
metadata={
|
||||
"name": "Color",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentVisibility:
|
||||
view_setup_hints: Optional[ViewSetupHints] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ViewSetupHints",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
exceptions: Optional[ComponentVisibilityExceptions] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Exceptions",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
default_visibility: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"name": "DefaultVisibility",
|
||||
"type": "Attribute",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class VisualizationInfoBitmaps:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
bitmap: List[Bitmap] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Bitmap",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class VisualizationInfoClippingPlanes:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
clipping_plane: List[ClippingPlane] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "ClippingPlane",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class VisualizationInfoLines:
|
||||
class Meta:
|
||||
global_type = False
|
||||
|
||||
line: List[Line] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Line",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ComponentColoring:
|
||||
color: List[ComponentColoringColor] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "Color",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Components:
|
||||
selection: Optional[ComponentSelection] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Selection",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
visibility: Optional[ComponentVisibility] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Visibility",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
coloring: Optional[ComponentColoring] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Coloring",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class VisualizationInfo:
|
||||
"""
|
||||
VisualizationInfo documentation.
|
||||
"""
|
||||
components: Optional[Components] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Components",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
orthogonal_camera: Optional[OrthogonalCamera] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "OrthogonalCamera",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
perspective_camera: Optional[PerspectiveCamera] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "PerspectiveCamera",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
lines: Optional[VisualizationInfoLines] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Lines",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
clipping_planes: Optional[VisualizationInfoClippingPlanes] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "ClippingPlanes",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
bitmaps: Optional[VisualizationInfoBitmaps] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"name": "Bitmaps",
|
||||
"type": "Element",
|
||||
}
|
||||
)
|
||||
guid: str = field(
|
||||
metadata={
|
||||
"name": "Guid",
|
||||
"type": "Attribute",
|
||||
"required": True,
|
||||
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,188 @@
|
||||
"""BCF XML V3 Topic handler."""
|
||||
import datetime
|
||||
import uuid
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from ifcopenshell import entity_instance
|
||||
from xsdata.models.datatype import XmlDateTime
|
||||
|
||||
import bcf.v3.model as mdl
|
||||
from bcf.inmemory_zipfile import ZipFileInterface
|
||||
from bcf.v3.visinfo import VisualizationInfoHandler
|
||||
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||
|
||||
|
||||
class TopicHandler:
|
||||
"""BCF Topic and related objects handler."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
topic_dir: Optional[zipfile.Path] = None,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> None:
|
||||
self._markup: Optional[mdl.Markup] = None
|
||||
self._viewpoints: dict[str, VisualizationInfoHandler] = {}
|
||||
self._bim_snippet: Optional[bytes] = None
|
||||
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||
self._topic_dir = topic_dir
|
||||
|
||||
@property
|
||||
def markup(self) -> Optional[mdl.Markup]:
|
||||
if not self._markup and self._topic_dir:
|
||||
markup_path = self._topic_dir.joinpath("markup.bcf")
|
||||
if markup_path.exists():
|
||||
self._markup = self._xml_handler.parse(markup_path.read_bytes(), mdl.Markup)
|
||||
return self._markup
|
||||
|
||||
@markup.setter
|
||||
def markup(self, value: mdl.Markup) -> None:
|
||||
self._markup = value
|
||||
|
||||
@property
|
||||
def topic(self) -> mdl.Topic:
|
||||
"""Return the Topic object."""
|
||||
return self.markup.topic
|
||||
|
||||
@property
|
||||
def guid(self) -> Optional[str]:
|
||||
"""Return the GUID of the topic."""
|
||||
if self._markup:
|
||||
return self.topic.guid
|
||||
return self._topic_dir.name if self._topic_dir else None
|
||||
|
||||
@property
|
||||
def header(self) -> Optional[mdl.Header]:
|
||||
"""Return the header of the topic."""
|
||||
return self.markup.header
|
||||
|
||||
@property
|
||||
def comments(self) -> list[mdl.Comment]:
|
||||
"""Return the comments of the topic."""
|
||||
return self.topic.comments.comment if self.topic.comments else []
|
||||
|
||||
@property
|
||||
def bim_snippet(self) -> Optional[bytes]:
|
||||
if not self._bim_snippet and self._topic_dir:
|
||||
self._bim_snippet = self._load_bim_snippet()
|
||||
return self._bim_snippet
|
||||
|
||||
@bim_snippet.setter
|
||||
def bim_snippet(self, value: bytes) -> None:
|
||||
self._bim_snippet = value
|
||||
|
||||
@property
|
||||
def viewpoints(self) -> Optional[VisualizationInfoHandler]:
|
||||
if (
|
||||
not self._viewpoints
|
||||
and self._topic_dir
|
||||
and self.topic.viewpoints
|
||||
and (viewpoints := self.topic.viewpoints.view_point)
|
||||
):
|
||||
self._viewpoints = VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints)
|
||||
return self._viewpoints
|
||||
|
||||
def _load_bim_snippet(self) -> Optional[bytes]:
|
||||
bim_snippet_obj = self.topic.bim_snippet
|
||||
if bim_snippet_obj and not bim_snippet_obj.is_external and self._topic_dir:
|
||||
bim_snippet_path = self._topic_dir.joinpath(bim_snippet_obj.reference)
|
||||
if bim_snippet_path.exists():
|
||||
return bim_snippet_path.read_bytes()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def create_new(
|
||||
cls,
|
||||
title: str,
|
||||
description: str,
|
||||
author: str,
|
||||
topic_type: str = "",
|
||||
topic_status: str = "",
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> "TopicHandler":
|
||||
"""
|
||||
Create a new BCF topic.
|
||||
|
||||
Args:
|
||||
title: The title of the topic.
|
||||
description: The description of the topic.
|
||||
author: The author of the topic.
|
||||
topic_type: The type of the topic.
|
||||
topic_status: The status of the topic.
|
||||
xml_handler: The XML parser/serializer to use.
|
||||
|
||||
Returns:
|
||||
The BCF topic definition.
|
||||
"""
|
||||
creation_date = XmlDateTime.from_datetime(datetime.datetime.now())
|
||||
guid = str(uuid.uuid4())
|
||||
topic = mdl.Topic(
|
||||
title=title,
|
||||
description=description,
|
||||
creation_author=author,
|
||||
creation_date=creation_date,
|
||||
guid=guid,
|
||||
topic_type=topic_type,
|
||||
topic_status=topic_status,
|
||||
)
|
||||
markup = mdl.Markup(topic=topic)
|
||||
obj = cls(topic_dir=Path(guid), xml_handler=xml_handler or XmlParserSerializer())
|
||||
obj.markup = markup
|
||||
return obj
|
||||
|
||||
def save(self, destination_zip: ZipFileInterface) -> None:
|
||||
"""
|
||||
Save the topic to a BCF zip file.
|
||||
|
||||
Args:
|
||||
bcf_zip: The BCF zip file to save to.
|
||||
"""
|
||||
topic_dir = self.guid
|
||||
self._save_xml(destination_zip, self._markup, "markup.bcf")
|
||||
self._save_viewpoints(destination_zip, topic_dir)
|
||||
self._save_bim_snippet(destination_zip)
|
||||
|
||||
def _save_viewpoints(self, destination_zip, topic_dir) -> None:
|
||||
if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point):
|
||||
return
|
||||
for vpt in viewpoints:
|
||||
if vpt.viewpoint:
|
||||
self.viewpoints[vpt.viewpoint].save(destination_zip, topic_dir, vpt)
|
||||
|
||||
def _save_xml(self, destination_zip: ZipFileInterface, item: Any, target: str) -> None:
|
||||
to_write = self._xml_handler.serialize(item) if item else self._topic_dir.joinpath(target).read_bytes()
|
||||
destination_zip.writestr(f"{self._topic_dir.name}/{target}", to_write)
|
||||
|
||||
def _save_bim_snippet(self, destination_zip: ZipFileInterface) -> None:
|
||||
snippet = self.topic.bim_snippet
|
||||
if not snippet or snippet.is_external:
|
||||
return
|
||||
ref_filename = Path(snippet.reference).name
|
||||
if self.bim_snippet:
|
||||
destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet)
|
||||
|
||||
def add_viewpoint(self, element: entity_instance) -> None:
|
||||
"""
|
||||
Add a viewpoint tergeting an IFC element to the topic.
|
||||
|
||||
Args:
|
||||
element: The IFC element.
|
||||
"""
|
||||
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
|
||||
self.add_visinfo_handler(new_viewpoint)
|
||||
|
||||
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
|
||||
self.viewpoints[new_viewpoint.guid] = new_viewpoint
|
||||
if self.topic.viewpoints is None:
|
||||
self.topic.viewpoints = mdl.TopicViewpoints()
|
||||
self.topic.viewpoints.view_point.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, TopicHandler):
|
||||
raise TypeError("Equality needs a BcfXml object.")
|
||||
return (
|
||||
self.markup == other.markup
|
||||
and self.viewpoints == other.viewpoints
|
||||
and self.bim_snippet == other.bim_snippet
|
||||
)
|
||||
@@ -0,0 +1,247 @@
|
||||
import uuid
|
||||
import zipfile
|
||||
from functools import lru_cache
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
import numpy as np
|
||||
from ifcopenshell import entity_instance
|
||||
from ifcopenshell.util import placement
|
||||
from numpy.typing import NDArray
|
||||
|
||||
import bcf.v3.model as mdl
|
||||
from bcf.geometry import calc_camera_vectors
|
||||
from bcf.inmemory_zipfile import ZipFileInterface
|
||||
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||
|
||||
|
||||
class VisualizationInfoHandler:
|
||||
"""Handle the VisualizationInfo and related objects."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
visualization_info: mdl.VisualizationInfo,
|
||||
snapshot: Optional[bytes] = None,
|
||||
bitmaps: Optional[dict[str, bytes]] = None,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> None:
|
||||
self.visualization_info = visualization_info
|
||||
self.snapshot = snapshot
|
||||
self.bitmaps = bitmaps or {}
|
||||
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||
|
||||
@property
|
||||
def guid(self) -> str:
|
||||
"""Return the GUID of the visualization info."""
|
||||
return self.visualization_info.guid
|
||||
|
||||
@classmethod
|
||||
def from_topic_viewpoints(
|
||||
cls,
|
||||
topic_dir: zipfile.Path,
|
||||
vps: Iterable[mdl.ViewPoint],
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> dict[str, "VisualizationInfoHandler"]:
|
||||
"""Create VisualizationInfoHandler objects of a Topic's ViewPoints."""
|
||||
viewpoints = {}
|
||||
for vpt in vps:
|
||||
visinfo = cls.load(topic_dir, vpt, xml_handler)
|
||||
if visinfo and vpt.viewpoint:
|
||||
viewpoints[vpt.viewpoint] = visinfo
|
||||
return viewpoints
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
topic_dir: zipfile.Path,
|
||||
vpt: mdl.ViewPoint,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> Optional["VisualizationInfoHandler"]:
|
||||
"""
|
||||
Load the VisualizationInfo and related objects from a BCF zip file.
|
||||
|
||||
Args:
|
||||
topic_dir: The directory in the BCF zip file to load from.
|
||||
vpt: The ViewPoint to load.
|
||||
xml_handler: The XML handler to use to parse the VisualizationInfo.
|
||||
|
||||
Returns:
|
||||
The VisualizationInfoHandler object.
|
||||
"""
|
||||
visinfo = cls._load_visinfo(topic_dir, vpt.viewpoint, xml_handler)
|
||||
if not visinfo:
|
||||
return None
|
||||
snapshot = cls._load_snapshot(topic_dir, vpt.snapshot)
|
||||
bitmaps = cls._load_bitmaps(topic_dir, visinfo)
|
||||
return cls(visinfo, snapshot, bitmaps, xml_handler)
|
||||
|
||||
@staticmethod
|
||||
def _load_visinfo(
|
||||
topic_dir: zipfile.Path,
|
||||
vp_name: Optional[str],
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> Optional[mdl.VisualizationInfo]:
|
||||
if not vp_name:
|
||||
return None
|
||||
vp_path = topic_dir.joinpath(vp_name)
|
||||
if vp_path.exists():
|
||||
xml_handler = xml_handler or XmlParserSerializer()
|
||||
return xml_handler.parse(vp_path.read_bytes(), mdl.VisualizationInfo)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _load_snapshot(topic_dir: zipfile.Path, vp_snapshot: Optional[str]) -> Optional[bytes]:
|
||||
if vp_snapshot:
|
||||
snapshot_path = topic_dir.joinpath(vp_snapshot)
|
||||
if snapshot_path.exists():
|
||||
return snapshot_path.read_bytes()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _load_bitmaps(topic_dir: zipfile.Path, visinfo: Optional[mdl.VisualizationInfo]) -> dict[str, bytes]:
|
||||
if not visinfo or not (bitmaps := visinfo.bitmaps):
|
||||
return {}
|
||||
bitmaps_dict = {}
|
||||
for bitmap in bitmaps.bitmap:
|
||||
if not bitmap.reference:
|
||||
continue
|
||||
bitmap_path = topic_dir.joinpath(bitmap.reference)
|
||||
if bitmap_path.exists():
|
||||
bitmaps_dict[bitmap.reference] = bitmap_path.read_bytes()
|
||||
return bitmaps_dict
|
||||
|
||||
def save(
|
||||
self,
|
||||
bcf_zip: ZipFileInterface,
|
||||
topic_dir: str,
|
||||
vpt: mdl.ViewPoint,
|
||||
) -> None:
|
||||
"""
|
||||
Save the VisualizationInfo and related objects to a BCF zip file.
|
||||
|
||||
Args:
|
||||
bcf_zip: The BCF zip file to save to.
|
||||
topic_dir: The directory in the BCF zip file to save to.
|
||||
vpt: The ViewPoint to save.
|
||||
"""
|
||||
if not (vp_name := vpt.viewpoint):
|
||||
return
|
||||
self._save_visinfo(bcf_zip, topic_dir, vp_name)
|
||||
self._save_snapshot(bcf_zip, topic_dir, vpt.snapshot)
|
||||
self._save_bitmaps(bcf_zip, topic_dir)
|
||||
|
||||
def _save_snapshot(self, bcf_zip: ZipFileInterface, topic_dir: str, filename: Optional[str]) -> None:
|
||||
if self.snapshot and filename:
|
||||
bcf_zip.writestr(f"{topic_dir}/{filename}", self.snapshot)
|
||||
|
||||
def _save_visinfo(self, bcf_zip: ZipFileInterface, topic_dir: str, vp_name: str) -> None:
|
||||
bcf_zip.writestr(
|
||||
f"{topic_dir}/{vp_name}",
|
||||
self._xml_handler.serialize(self.visualization_info),
|
||||
)
|
||||
|
||||
def _save_bitmaps(self, bcf_zip: ZipFileInterface, topic_dir: str) -> None:
|
||||
if not self.bitmaps:
|
||||
return
|
||||
if not (bitmaps_defs := self.visualization_info.bitmaps):
|
||||
return
|
||||
for bitmap_def in bitmaps_defs.bitmap:
|
||||
if not (bitmap_name := bitmap_def.reference):
|
||||
continue
|
||||
if bitmap_name in self.bitmaps:
|
||||
bcf_zip.writestr(f"{topic_dir}/{bitmap_name}", self.bitmaps[bitmap_name])
|
||||
|
||||
@classmethod
|
||||
def create_new(
|
||||
cls,
|
||||
element: entity_instance,
|
||||
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||
) -> "VisualizationInfoHandler":
|
||||
"""
|
||||
Create a new VisualizationInfoHandler object from an IFC element.
|
||||
|
||||
Args:
|
||||
element: The IFC element to point at.
|
||||
xml_handler: The XML handler to use.
|
||||
|
||||
Returns:
|
||||
The VisualizationInfoHandler object.
|
||||
"""
|
||||
xml_handler = xml_handler or XmlParserSerializer()
|
||||
return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
|
||||
"""
|
||||
Return a BCF viewpoint of an IFC element.
|
||||
|
||||
This function is cached to speedudp the creation of multiple BCF topics regarding the same element.
|
||||
|
||||
Args:
|
||||
element: The IFC element to point at.
|
||||
|
||||
Returns:
|
||||
The BCF viewpoint definition.
|
||||
"""
|
||||
elem_placement = placement.get_local_placement(element.ObjectPlacement)
|
||||
|
||||
return mdl.VisualizationInfo(
|
||||
guid=str(uuid.uuid4()),
|
||||
components=build_components(element.GlobalId),
|
||||
perspective_camera=build_camera(elem_placement),
|
||||
)
|
||||
|
||||
|
||||
def build_components(guid: str) -> mdl.Components:
|
||||
"""
|
||||
Return the BCF components from an IFC element GUID.
|
||||
|
||||
Args:
|
||||
guid: The IFC element GUID.
|
||||
|
||||
Returns:
|
||||
The BCF components definition.
|
||||
"""
|
||||
return mdl.Components(
|
||||
selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]),
|
||||
visibility=mdl.ComponentVisibility(default_visibility=True),
|
||||
)
|
||||
|
||||
|
||||
def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera:
|
||||
"""
|
||||
Return a BCF camera for an IFC element placement matrix.
|
||||
|
||||
Args:
|
||||
elem_placement: The IFC element placement as a rototranslation matrix.
|
||||
|
||||
Returns:
|
||||
The BCF camera definition.
|
||||
"""
|
||||
return build_camera_from_vectors(*calc_camera_vectors(elem_placement))
|
||||
|
||||
|
||||
def build_camera_from_vectors(
|
||||
camera_position: NDArray[np.float_], camera_dir: NDArray[np.float_], camera_up: NDArray[np.float_]
|
||||
) -> mdl.PerspectiveCamera:
|
||||
"""
|
||||
Return a BCF camera for an IFC element placement matrix.
|
||||
|
||||
Args:
|
||||
camera_position: camera position array
|
||||
camera_dir: camera direction versor
|
||||
camera_up_vector: camera up versor
|
||||
|
||||
Returns:
|
||||
The BCF camera definition.
|
||||
"""
|
||||
camera_viewpoint = mdl.Point(x=camera_position[0], y=camera_position[1], z=camera_position[2])
|
||||
camera_direction = mdl.Direction(x=camera_dir[0], y=camera_dir[1], z=camera_dir[2])
|
||||
camera_up_vector = mdl.Direction(x=camera_up[0], y=camera_up[1], z=camera_up[2])
|
||||
return mdl.PerspectiveCamera(
|
||||
camera_view_point=camera_viewpoint,
|
||||
camera_direction=camera_direction,
|
||||
camera_up_vector=camera_up_vector,
|
||||
aspect_ratio=1.0,
|
||||
field_of_view=60.0,
|
||||
)
|
||||
@@ -0,0 +1,83 @@
|
||||
"""XML Parser and Serializer factories."""
|
||||
from typing import Optional, Protocol, Type, TypeVar
|
||||
|
||||
from xsdata.formats.dataclass.context import XmlContext
|
||||
from xsdata.formats.dataclass.parsers import XmlParser
|
||||
from xsdata.formats.dataclass.serializers import XmlSerializer
|
||||
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||
|
||||
|
||||
def build_xml_parser(context: Optional[XmlContext] = None) -> XmlParser:
|
||||
"""Return a parser for an XML file."""
|
||||
parser = XmlParser(context=context or XmlContext())
|
||||
parser.register_namespace("xs", "http://www.w3.org/2001/XMLSchema")
|
||||
return parser
|
||||
|
||||
|
||||
def build_serializer(context: Optional[XmlContext] = None) -> XmlSerializer:
|
||||
"""Return a serializer for an XML file."""
|
||||
return XmlSerializer(
|
||||
config=SerializerConfig(pretty_print=True),
|
||||
context=context or XmlContext(),
|
||||
)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class AbstractXmlParserSerializer(Protocol):
|
||||
"""XML Parser and serializer wrapper."""
|
||||
|
||||
def parse(self, xml: bytes, clazz: Type[T]) -> T:
|
||||
"""
|
||||
Parse an XML file to an object.
|
||||
|
||||
Args:
|
||||
xml: The XML file as bytes.
|
||||
clazz: The class to parse to.
|
||||
"""
|
||||
|
||||
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
|
||||
"""
|
||||
Serialize an object to XML.
|
||||
|
||||
Args:
|
||||
obj: The object to serialize.
|
||||
ns_map: The namespace map to use.
|
||||
|
||||
Returns:
|
||||
The XML as string.
|
||||
"""
|
||||
|
||||
|
||||
class XmlParserSerializer:
|
||||
"""XML Parser and serializer wrapper."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.context = XmlContext()
|
||||
self.parser = build_xml_parser(self.context)
|
||||
self.serializer = build_serializer(self.context)
|
||||
|
||||
def parse(self, xml: bytes, clazz: Type[T]) -> T:
|
||||
"""
|
||||
Parse an XML file to an object.
|
||||
|
||||
Args:
|
||||
xml: The XML file as bytes.
|
||||
clazz: The class to parse to.
|
||||
"""
|
||||
return self.parser.from_bytes(xml, clazz)
|
||||
|
||||
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
|
||||
"""
|
||||
Serialize an object to XML.
|
||||
|
||||
Args:
|
||||
obj: The object to serialize.
|
||||
ns_map: The namespace map to use.
|
||||
|
||||
Returns:
|
||||
The XML as string.
|
||||
"""
|
||||
ns_map = ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
|
||||
return self.serializer.render(obj, ns_map)
|
||||
@@ -0,0 +1 @@
|
||||
"""BCF tests."""
|
||||
@@ -0,0 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from bcf.xml_parser import XmlParserSerializer
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def xml_handler() -> XmlParserSerializer:
|
||||
return XmlParserSerializer()
|
||||
Binary file not shown.
@@ -0,0 +1,122 @@
|
||||
"""BCF XML tests."""
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
|
||||
import bcf.v2.model as mdl
|
||||
from bcf.v2.bcfxml import BcfXml
|
||||
from bcf.v2.topic import TopicHandler
|
||||
from bcf.v2.visinfo import (
|
||||
VisualizationInfoHandler,
|
||||
build_camera_from_vectors,
|
||||
build_components,
|
||||
)
|
||||
from bcf.xml_parser import XmlParserSerializer
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def build_sample(xml_handler: XmlParserSerializer) -> tuple[BcfXml, TopicHandler]:
|
||||
bcf = BcfXml.create_new("Test project", xml_handler=xml_handler)
|
||||
orig_th = bcf.add_topic("Test topic", "Test message", "Test author", "Test type")
|
||||
return bcf, orig_th
|
||||
|
||||
|
||||
def test_bcf_roundtrip(xml_handler, build_sample) -> None:
|
||||
"""Saving and loading a bcf xml project returns the same objects."""
|
||||
bcf, orig_th = build_sample
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path)
|
||||
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||
assert parsed == bcf
|
||||
parsed_th = parsed.topics[orig_th.guid]
|
||||
assert parsed_th == orig_th
|
||||
|
||||
|
||||
def test_bcf_edit_saveas(xml_handler, build_sample) -> None:
|
||||
"""Saving and loading a bcf xml project returns the same objects."""
|
||||
bcf, orig_th = build_sample
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path)
|
||||
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||
for th in parsed.topics.values():
|
||||
th.topic.title = "New Topic Title"
|
||||
modified_path = Path(tmp_dir) / "edited.bcf"
|
||||
parsed.save(modified_path)
|
||||
with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed:
|
||||
assert modified_parsed == bcf
|
||||
parsed_th = modified_parsed.topics[orig_th.guid]
|
||||
assert parsed_th.markup != orig_th.markup
|
||||
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
|
||||
assert parsed_th.viewpoints == orig_th.viewpoints
|
||||
assert parsed_th.bim_snippet == orig_th.bim_snippet
|
||||
|
||||
|
||||
def test_bcf_edit(xml_handler, build_sample) -> None:
|
||||
"""Saving and loading a bcf xml project returns the same objects."""
|
||||
bcf, orig_th = build_sample
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path)
|
||||
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||
for th in parsed.topics.values():
|
||||
th.topic.title = "New Topic Title"
|
||||
parsed.save()
|
||||
with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed:
|
||||
assert modified_parsed == bcf
|
||||
assert len(modified_parsed.topics) == 1
|
||||
parsed_th = modified_parsed.topics[orig_th.guid]
|
||||
assert parsed_th.markup != orig_th.markup
|
||||
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
|
||||
assert parsed_th.viewpoints == orig_th.viewpoints
|
||||
assert parsed_th.bim_snippet == orig_th.bim_snippet
|
||||
|
||||
|
||||
def test_save_no_filename(build_sample) -> None:
|
||||
bcf, _ = build_sample
|
||||
with pytest.raises(ValueError):
|
||||
bcf.save()
|
||||
|
||||
|
||||
def test_load_no_filename() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
BcfXml.load("")
|
||||
|
||||
|
||||
def test_save_keep_open(build_sample) -> None:
|
||||
bcf, _ = build_sample
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path, keep_open=True)
|
||||
assert bcf._zip_file is not None
|
||||
bcf._zip_file.close()
|
||||
|
||||
|
||||
def test_massive_bcf(xml_handler) -> None:
|
||||
bcf = BcfXml.create_new("Test project", xml_handler=xml_handler)
|
||||
for i in range(100):
|
||||
th = bcf.add_topic(f"Topic {i:04}", f"Message {i:04}", "Test author", "Test type")
|
||||
vi = mdl.VisualizationInfo(
|
||||
guid=str(uuid.uuid4()),
|
||||
components=build_components(str(uuid.uuid4())),
|
||||
perspective_camera=build_camera_from_vectors([i, 0, 0], [0, 1, 0], [0, 0, 1]),
|
||||
)
|
||||
vh = VisualizationInfoHandler(visualization_info=vi, xml_handler=xml_handler)
|
||||
th.add_visinfo_handler(vh)
|
||||
assert len(bcf.topics) == 100
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path)
|
||||
|
||||
|
||||
def test_equality_with_wrong_object() -> None:
|
||||
with pytest.raises(TypeError):
|
||||
build_sample[0] == "Wrong object"
|
||||
|
||||
|
||||
def test_topic_equality_with_wrong_object() -> None:
|
||||
with pytest.raises(TypeError):
|
||||
build_sample[1] == "Wrong object"
|
||||
@@ -0,0 +1,365 @@
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from xsdata.models.datatype import XmlDateTime
|
||||
|
||||
import bcf.v2.model as mdl
|
||||
from bcf.v2.bcfxml import BcfXml
|
||||
from bcf.v2.topic import TopicHandler
|
||||
|
||||
|
||||
def test_maximum_information() -> None:
|
||||
"""
|
||||
All the info in a BCF is parsed correctly.
|
||||
|
||||
Uses sample file from https://github.com/buildingSMART/BCF-XML/
|
||||
"""
|
||||
bcf_path = Path(__file__).parent / "MaximumInformation.bcf"
|
||||
with BcfXml.load(bcf_path) as bcf:
|
||||
assert_everything_in_place(bcf)
|
||||
|
||||
|
||||
def test_save_maximum_information() -> None:
|
||||
base_dir = Path(__file__).parent
|
||||
bcf_path = base_dir / "MaximumInformation.bcf"
|
||||
target_path = base_dir / "MaximumInformationSaved.bcf"
|
||||
with BcfXml.load(bcf_path) as bcf:
|
||||
bcf.save(target_path)
|
||||
assert target_path.exists()
|
||||
with BcfXml.load(target_path) as bcf2:
|
||||
assert_everything_in_place(bcf2)
|
||||
|
||||
expected_files = [
|
||||
"bcf.version",
|
||||
"project.bcfp",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/markup.bcf",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/bitmap.png",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/tux.png",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/JsonElement.json",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_4ab7514b-b216-4d56-98d2-45cf8500ff5a.png",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_9a4a1878-ecbd-4916-83a8-dad82e560231.png",
|
||||
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.png",
|
||||
"d1068c81-af04-4546-b63c-348810f6c716/markup.bcf",
|
||||
"extensions.xsd",
|
||||
"IfcPile_01.ifc",
|
||||
"markup.xsd",
|
||||
]
|
||||
assert_files_present(target_path, expected_files)
|
||||
os.unlink(target_path)
|
||||
|
||||
|
||||
def assert_everything_in_place(bcf: BcfXml):
|
||||
assert bcf.version.version_id == "2.1"
|
||||
assert bcf.project.name == "BCF API Implementation"
|
||||
assert bcf.project_info.extension_schema == "extensions.xsd"
|
||||
|
||||
assert len(bcf.topics) == 2
|
||||
assert_first_topic_handler(bcf.topics["7ddc3ef0-0ab7-43f1-918a-45e38b42369c"])
|
||||
second_th = bcf.topics["d1068c81-af04-4546-b63c-348810f6c716"]
|
||||
assert second_th.topic == mdl.Topic(
|
||||
title="Referenced topic",
|
||||
creation_date=XmlDateTime(2017, 5, 22, 7, 51, 0, 42987900),
|
||||
creation_author="dangl@iabi.eu",
|
||||
description="This is just an empty topic that acts as a referenced topic.",
|
||||
guid="d1068c81-af04-4546-b63c-348810f6c716",
|
||||
)
|
||||
|
||||
|
||||
def assert_first_topic_handler(topic_handler: TopicHandler):
|
||||
assert topic_handler.guid == "7ddc3ef0-0ab7-43f1-918a-45e38b42369c"
|
||||
|
||||
expected_bs1 = mdl.BimSnippet(
|
||||
reference="JsonElement.json", reference_schema="http://json-schema.org", snippet_type="JSON"
|
||||
)
|
||||
expected_t1 = mdl.Topic(
|
||||
reference_link=["https://bim--it.net"],
|
||||
title="Maximum Content",
|
||||
priority="High",
|
||||
index=0,
|
||||
labels=["Structural", "IT Development"],
|
||||
creation_date=XmlDateTime(2015, 6, 21, 12, 0, 0),
|
||||
creation_author="dangl@iabi.eu",
|
||||
modified_date=XmlDateTime(2015, 6, 21, 14, 22, 47),
|
||||
modified_author="dangl@iabi.eu",
|
||||
due_date=XmlDateTime(2016, 10, 2, 14, 22, 47),
|
||||
assigned_to="linhard@iabi.eu",
|
||||
stage="Construction Start",
|
||||
description="This is a topic with all informations present.",
|
||||
bim_snippet=expected_bs1,
|
||||
document_reference=[
|
||||
mdl.TopicDocumentReference(
|
||||
referenced_document="https://github.com/BuildingSMART/BCF-XML",
|
||||
description="GitHub BCF Specification",
|
||||
is_external=True,
|
||||
),
|
||||
mdl.TopicDocumentReference(
|
||||
referenced_document="../markup.xsd",
|
||||
description="Markup.xsd Schema",
|
||||
is_external=False,
|
||||
),
|
||||
],
|
||||
related_topic=[mdl.TopicRelatedTopic(guid="d1068c81-af04-4546-b63c-348810f6c716")],
|
||||
guid="7ddc3ef0-0ab7-43f1-918a-45e38b42369c",
|
||||
topic_type="Structural",
|
||||
topic_status="Open",
|
||||
)
|
||||
assert topic_handler.topic == expected_t1
|
||||
expected_h1 = mdl.Header(
|
||||
file=[
|
||||
mdl.HeaderFile(
|
||||
filename="IfcPile_01.ifc",
|
||||
date=XmlDateTime(2014, 10, 27, 16, 27, 27),
|
||||
reference="../IfcPile_01.ifc",
|
||||
ifc_project="0M6o7Znnv7hxsbWgeu7oQq",
|
||||
ifc_spatial_structure_element="23B$bNeGHFQuMYJzvUX0FD",
|
||||
is_external=False,
|
||||
)
|
||||
]
|
||||
)
|
||||
assert topic_handler.header == expected_h1
|
||||
|
||||
expected_th1_comments = [
|
||||
mdl.Comment(
|
||||
date=XmlDateTime(2015, 8, 31, 12, 40, 17),
|
||||
author="dangl@iabi.eu",
|
||||
comment="This is an unmodified topic at the uppermost hierarchical level.\nAll times in the XML are marked as UTC times.",
|
||||
guid="07ccdba0-1736-47e1-807d-67dc6f3addaa",
|
||||
),
|
||||
mdl.Comment(
|
||||
date=XmlDateTime(2015, 8, 31, 14, 0, 1),
|
||||
author="dangl@iabi.eu",
|
||||
comment="This comment was a reply to the first comment in BCF v2.0. This is a no longer supported functionality and therefore is to be treated as a regular comment in v2.1.",
|
||||
guid="a12766c2-61bc-40b4-ab19-e9f45fd0b0bf",
|
||||
),
|
||||
mdl.Comment(
|
||||
date=XmlDateTime(2015, 8, 31, 13, 7, 11),
|
||||
author="dangl@iabi.eu",
|
||||
comment="This comment again is in the highest hierarchy level.\nIt references a viewpoint.",
|
||||
viewpoint=mdl.CommentViewpoint(guid="4ab7514b-b216-4d56-98d2-45cf8500ff5a"),
|
||||
guid="c2bb5bb0-773d-45dd-bdaa-19a216439ed3",
|
||||
),
|
||||
mdl.Comment(
|
||||
date=XmlDateTime(2015, 8, 31, 15, 42, 58),
|
||||
author="dangl@iabi.eu",
|
||||
comment="This comment contained some spllng errs.\nHopefully, the modifier did catch them all.",
|
||||
modified_date=XmlDateTime(2015, 8, 31, 16, 7, 11),
|
||||
modified_author="dangl@iabi.eu",
|
||||
guid="0b843a5c-c3bf-41ef-be98-52a9f7bd9790",
|
||||
),
|
||||
]
|
||||
assert topic_handler.comments == expected_th1_comments
|
||||
|
||||
expected_th1_viewpoints = [
|
||||
mdl.ViewPoint(
|
||||
viewpoint="Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv",
|
||||
snapshot="Snapshot_4ab7514b-b216-4d56-98d2-45cf8500ff5a.png",
|
||||
index=2,
|
||||
guid="4ab7514b-b216-4d56-98d2-45cf8500ff5a",
|
||||
),
|
||||
mdl.ViewPoint(
|
||||
viewpoint="Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv",
|
||||
snapshot="Snapshot_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.png",
|
||||
index=0,
|
||||
guid="fc4019d7-365e-47f3-b6d0-b39fc48f15fc",
|
||||
),
|
||||
mdl.ViewPoint(
|
||||
viewpoint="Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv",
|
||||
snapshot="Snapshot_9a4a1878-ecbd-4916-83a8-dad82e560231.png",
|
||||
index=1,
|
||||
guid="9a4a1878-ecbd-4916-83a8-dad82e560231",
|
||||
),
|
||||
]
|
||||
|
||||
expected_m1 = mdl.Markup(
|
||||
header=expected_h1,
|
||||
topic=expected_t1,
|
||||
comment=expected_th1_comments,
|
||||
viewpoints=expected_th1_viewpoints,
|
||||
)
|
||||
assert topic_handler.markup == expected_m1
|
||||
|
||||
assert json.loads(topic_handler.bim_snippet) == {"Material": "Concrete", "Temperatures": ["Cold", "Hot", "Hotter"]}
|
||||
|
||||
assert topic_handler.reference_files["../IfcPile_01.ifc"] is not None
|
||||
|
||||
assert_viewpoints(topic_handler.viewpoints)
|
||||
|
||||
|
||||
def assert_viewpoints(viewpoints):
|
||||
assert len(viewpoints) == 3
|
||||
|
||||
expected_selection = mdl.ComponentSelection(
|
||||
component=[
|
||||
mdl.Component(ifc_guid="0cSRUx$EX1NRjqiKcYQ$a0"),
|
||||
mdl.Component(ifc_guid="1jQQiGIAnFzxOUzrdmJYDS"),
|
||||
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
|
||||
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
|
||||
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
|
||||
]
|
||||
)
|
||||
|
||||
expected_exception = mdl.ComponentVisibilityExceptions(
|
||||
component=[
|
||||
mdl.Component(ifc_guid="0Gl71cVurFn8bxAOox6M4X"),
|
||||
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
|
||||
mdl.Component(ifc_guid="3DvyPxGIn8qR0KDwbL_9r1"),
|
||||
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
|
||||
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
|
||||
]
|
||||
)
|
||||
|
||||
expected_coloring = mdl.ComponentColoring(
|
||||
color=[
|
||||
mdl.ComponentColoringColor(
|
||||
component=[
|
||||
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
|
||||
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
|
||||
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
|
||||
mdl.Component(ifc_guid="0cSRUx$EX1NRjqiKcYQ$a0"),
|
||||
],
|
||||
color="3498DB",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert_first_viewpoint(
|
||||
viewpoints["Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv"],
|
||||
expected_selection,
|
||||
expected_exception,
|
||||
expected_coloring,
|
||||
)
|
||||
assert_second_viewpoint(
|
||||
viewpoints["Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv"],
|
||||
expected_selection,
|
||||
expected_exception,
|
||||
expected_coloring,
|
||||
)
|
||||
assert_third_viewpoint(
|
||||
viewpoints["Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv"],
|
||||
expected_selection,
|
||||
expected_exception,
|
||||
expected_coloring,
|
||||
)
|
||||
|
||||
|
||||
def assert_first_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=True,
|
||||
space_boundaries_visible=True,
|
||||
openings_visible=True,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
exceptions=expected_exception,
|
||||
default_visibility=True,
|
||||
),
|
||||
coloring=expected_coloring,
|
||||
),
|
||||
perspective_camera=mdl.PerspectiveCamera(
|
||||
camera_view_point=mdl.Point(x=0.43079984188079834, y=69.52057647705078, z=10.666350364685059),
|
||||
camera_direction=mdl.Direction(x=0.09159398823976517, y=-0.9375035166740417, z=-0.3357048034667969),
|
||||
camera_up_vector=mdl.Direction(x=0.01938679628074169, y=-0.3353792130947113, z=0.9418837428092957),
|
||||
field_of_view=60,
|
||||
),
|
||||
lines=mdl.VisualizationInfoLines(
|
||||
line=[
|
||||
mdl.Line(start_point=mdl.Point(x=0, y=0, z=0), end_point=mdl.Point(x=0, y=0, z=1)),
|
||||
mdl.Line(start_point=mdl.Point(x=0, y=0, z=1), end_point=mdl.Point(x=0, y=1, z=1)),
|
||||
mdl.Line(start_point=mdl.Point(x=0, y=1, z=1), end_point=mdl.Point(x=1, y=1, z=1)),
|
||||
]
|
||||
),
|
||||
clipping_planes=mdl.VisualizationInfoClippingPlanes(
|
||||
clipping_plane=[
|
||||
mdl.ClippingPlane(location=mdl.Point(x=0, y=0, z=0), direction=mdl.Direction(x=0, y=0, z=1)),
|
||||
mdl.ClippingPlane(location=mdl.Point(x=0, y=0, z=0), direction=mdl.Direction(x=0, y=1, z=0)),
|
||||
]
|
||||
),
|
||||
bitmap=[
|
||||
mdl.VisualizationInfoBitmap(
|
||||
bitmap=mdl.BitmapFormat.PNG,
|
||||
reference="bitmap.png",
|
||||
location=mdl.Point(x=10, y=-10, z=7),
|
||||
normal=mdl.Direction(x=0, y=1, z=0),
|
||||
up=mdl.Direction(x=0, y=0, z=1),
|
||||
height=5.0,
|
||||
),
|
||||
mdl.VisualizationInfoBitmap(
|
||||
bitmap=mdl.BitmapFormat.PNG,
|
||||
reference="tux.png",
|
||||
location=mdl.Point(x=20, y=-10, z=7),
|
||||
normal=mdl.Direction(x=0, y=1, z=0),
|
||||
up=mdl.Direction(x=0, y=0, z=1),
|
||||
height=5.0,
|
||||
),
|
||||
],
|
||||
guid="8dc86298-9737-40b4-a448-98a9e953293a",
|
||||
)
|
||||
assert viewpoint.visualization_info == expected_vp
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=False,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
exceptions=expected_exception,
|
||||
default_visibility=False,
|
||||
),
|
||||
coloring=expected_coloring,
|
||||
),
|
||||
perspective_camera=mdl.PerspectiveCamera(
|
||||
camera_view_point=mdl.Point(x=-47.18794250488281, y=43.829200744628906, z=10.666350364685059),
|
||||
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
|
||||
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
||||
field_of_view=60,
|
||||
),
|
||||
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
||||
)
|
||||
assert viewpoint.visualization_info == expected_vp
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=True,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
exceptions=expected_exception,
|
||||
default_visibility=True,
|
||||
),
|
||||
coloring=expected_coloring,
|
||||
),
|
||||
perspective_camera=mdl.PerspectiveCamera(
|
||||
camera_view_point=mdl.Point(x=-48.974571228027344, y=-64.20051574707031, z=10.666350364685059),
|
||||
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
|
||||
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
||||
field_of_view=60,
|
||||
),
|
||||
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
||||
)
|
||||
assert viewpoint.visualization_info == expected_vp
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
def assert_files_present(saved_bcf_path, expected_files):
|
||||
with zipfile.ZipFile(saved_bcf_path) as bcf_zip:
|
||||
for file_path in expected_files:
|
||||
assert zipfile.Path(bcf_zip, file_path).exists()
|
||||
Binary file not shown.
@@ -0,0 +1,129 @@
|
||||
"""BCF XML tests."""
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
|
||||
import bcf.v3.model as mdl
|
||||
from bcf.v3.bcfxml import BcfXml
|
||||
from bcf.v3.topic import TopicHandler
|
||||
from bcf.v3.visinfo import (
|
||||
VisualizationInfoHandler,
|
||||
build_camera_from_vectors,
|
||||
build_components,
|
||||
)
|
||||
from bcf.xml_parser import XmlParserSerializer
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def build_sample(xml_handler: XmlParserSerializer) -> tuple[BcfXml, TopicHandler]:
|
||||
ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"]))
|
||||
bcf = BcfXml.create_new("Test project", extensions=ext, xml_handler=xml_handler)
|
||||
orig_th = bcf.add_topic("Test topic", "Test message", "Test author", "Test type")
|
||||
return bcf, orig_th
|
||||
|
||||
|
||||
def test_bcf_roundtrip(xml_handler, build_sample) -> None:
|
||||
"""Saving and loading a bcf xml project returns the same objects."""
|
||||
bcf, orig_th = build_sample
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path)
|
||||
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||
assert parsed == bcf
|
||||
parsed_th = parsed.topics[orig_th.guid]
|
||||
assert parsed_th == orig_th
|
||||
|
||||
|
||||
def test_bcf_edit_saveas(xml_handler, build_sample) -> None:
|
||||
"""Saving and loading a bcf xml project returns the same objects."""
|
||||
bcf, orig_th = build_sample
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path)
|
||||
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||
for th in parsed.topics.values():
|
||||
th.topic.title = "New Topic Title"
|
||||
modified_path = Path(tmp_dir) / "edited.bcf"
|
||||
parsed.save(modified_path)
|
||||
with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed:
|
||||
assert modified_parsed == bcf
|
||||
parsed_th = modified_parsed.topics[orig_th.guid]
|
||||
assert parsed_th.markup != orig_th.markup
|
||||
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
|
||||
assert parsed_th.viewpoints == orig_th.viewpoints
|
||||
assert parsed_th.bim_snippet == orig_th.bim_snippet
|
||||
|
||||
|
||||
def test_bcf_edit(xml_handler, build_sample) -> None:
|
||||
"""Saving and loading a bcf xml project returns the same objects."""
|
||||
bcf, orig_th = build_sample
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path)
|
||||
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||
for th in parsed.topics.values():
|
||||
th.topic.title = "New Topic Title"
|
||||
parsed.save()
|
||||
with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed:
|
||||
assert modified_parsed == bcf
|
||||
assert len(modified_parsed.topics) == 1
|
||||
parsed_th = modified_parsed.topics[orig_th.guid]
|
||||
assert parsed_th.markup != orig_th.markup
|
||||
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
|
||||
assert parsed_th.viewpoints == orig_th.viewpoints
|
||||
assert parsed_th.bim_snippet == orig_th.bim_snippet
|
||||
|
||||
|
||||
def test_save_no_filename(build_sample) -> None:
|
||||
bcf, _ = build_sample
|
||||
with pytest.raises(ValueError):
|
||||
bcf.save()
|
||||
|
||||
|
||||
def test_load_no_filename() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
BcfXml.load("")
|
||||
|
||||
|
||||
def test_save_keep_open(build_sample) -> None:
|
||||
bcf, orig_th = build_sample
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path, keep_open=True)
|
||||
assert bcf._zip_file is not None
|
||||
bcf._zip_file.close()
|
||||
|
||||
|
||||
# image = PIL.Image.new('RGB', size=(100, 100))
|
||||
# file = BinaryIO()
|
||||
# image.save(file)
|
||||
|
||||
|
||||
def test_massive_bcf(xml_handler) -> None:
|
||||
ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"]))
|
||||
bcf = BcfXml.create_new("Test project", extensions=ext, xml_handler=xml_handler)
|
||||
for i in range(100):
|
||||
th = bcf.add_topic(f"Topic {i:04}", f"Message {i:04}", "Test author", "Test type")
|
||||
vi = mdl.VisualizationInfo(
|
||||
guid=str(uuid.uuid4()),
|
||||
components=build_components(str(uuid.uuid4())),
|
||||
perspective_camera=build_camera_from_vectors([i, 0, 0], [0, 1, 0], [0, 0, 1]),
|
||||
)
|
||||
vh = VisualizationInfoHandler(visualization_info=vi, xml_handler=xml_handler)
|
||||
th.add_visinfo_handler(vh)
|
||||
assert len(bcf.topics) == 100
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
file_path = Path(tmp_dir) / "test.bcf"
|
||||
bcf.save(file_path)
|
||||
|
||||
|
||||
def test_equality_with_wrong_object() -> None:
|
||||
with pytest.raises(TypeError):
|
||||
build_sample[0] == "Wrong object"
|
||||
|
||||
|
||||
def test_topic_equality_with_wrong_object() -> None:
|
||||
with pytest.raises(TypeError):
|
||||
build_sample[1] == "Wrong object"
|
||||
@@ -0,0 +1,230 @@
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from xsdata.models.datatype import XmlDateTime
|
||||
|
||||
import bcf.v3.model as mdl
|
||||
from bcf.v3.bcfxml import BcfXml
|
||||
from bcf.v3.topic import TopicHandler
|
||||
|
||||
|
||||
def test_doc_ref_internal() -> None:
|
||||
"""
|
||||
All the info in a BCF is parsed correctly.
|
||||
|
||||
Uses sample file from https://github.com/buildingSMART/BCF-XML/
|
||||
"""
|
||||
bcf_path = Path(__file__).parent / "Document reference internal.bcf"
|
||||
with BcfXml.load(bcf_path) as bcf:
|
||||
assert_everything_in_place(bcf)
|
||||
|
||||
|
||||
def test_doc_ref_internal_save() -> None:
|
||||
base_dir = Path(__file__).parent
|
||||
bcf_path = base_dir / "Document reference internal.bcf"
|
||||
target_path = base_dir / "Document reference internal Saved.bcf"
|
||||
with BcfXml.load(bcf_path) as bcf:
|
||||
bcf.save(target_path)
|
||||
assert target_path.exists()
|
||||
with BcfXml.load(target_path) as bcf2:
|
||||
assert_everything_in_place(bcf2)
|
||||
assert_files_present(target_path)
|
||||
os.unlink(target_path)
|
||||
|
||||
|
||||
def assert_everything_in_place(bcf: BcfXml):
|
||||
assert bcf.version.version_id == "3.0"
|
||||
assert bcf.project.name == "BCF 3.0 test cases"
|
||||
assert bcf.project.project_id == "de894a86-3a08-4ea0-b2d1-6c222b5602d1"
|
||||
|
||||
assert len(bcf.topics) == 1
|
||||
topic_handler = bcf.topics["8ac9822a-761a-4deb-9f39-f61286acbf6a"]
|
||||
|
||||
expected_h1 = mdl.Header(
|
||||
files=mdl.HeaderFiles(
|
||||
file=[
|
||||
mdl.File(
|
||||
filename="BCF-ARK",
|
||||
date=XmlDateTime(2021, 1, 4, 9, 37, 45),
|
||||
reference="https://bimsync.com/project/f5fbb3c695274d1890036bf64f77eb71/revisions/007afab57f264d2296aae0a452486ae1",
|
||||
is_external=True,
|
||||
),
|
||||
mdl.File(
|
||||
filename="BCF-MEP",
|
||||
date=XmlDateTime(2017, 8, 7, 9, 51, 34),
|
||||
reference="https://bimsync.com/project/f5fbb3c695274d1890036bf64f77eb71/revisions/a21ed391f9e046a2bb2bc879c48f1d48",
|
||||
is_external=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
assert topic_handler.header == expected_h1
|
||||
|
||||
expected_comments = [
|
||||
mdl.Comment(
|
||||
date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
|
||||
author="Architect@example.com",
|
||||
comment="A comment",
|
||||
viewpoint=mdl.CommentViewpoint(guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182"),
|
||||
guid="c045ef04-324d-4c36-9ac8-831f2a15c6d6",
|
||||
),
|
||||
]
|
||||
assert topic_handler.comments == expected_comments
|
||||
|
||||
expected_viewpoints = [
|
||||
mdl.ViewPoint(
|
||||
viewpoint="Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv",
|
||||
snapshot="Snapshot_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.png",
|
||||
guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182",
|
||||
),
|
||||
]
|
||||
|
||||
expected_topic = mdl.Topic(
|
||||
reference_links=mdl.TopicReferenceLinks(),
|
||||
title="Document Reference Internal",
|
||||
labels=mdl.TopicLabels(),
|
||||
creation_date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
|
||||
creation_author="Architect@example.com",
|
||||
modified_date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
|
||||
assigned_to="OtherUser@doe.com",
|
||||
document_references=mdl.TopicDocumentReferences(
|
||||
document_reference=[
|
||||
mdl.DocumentReference(
|
||||
document_guid="b1d1b7f0-60b9-457d-ad12-16e0fb997bc5",
|
||||
description="ThisIsADocument.txt",
|
||||
guid="048e898f-555f-47c3-a273-6c664fb2ef69",
|
||||
),
|
||||
],
|
||||
),
|
||||
related_topics=mdl.TopicRelatedTopics(),
|
||||
comments=mdl.TopicComments(comment=expected_comments),
|
||||
viewpoints=mdl.TopicViewpoints(view_point=expected_viewpoints),
|
||||
guid="8ac9822a-761a-4deb-9f39-f61286acbf6a",
|
||||
server_assigned_id="6",
|
||||
topic_type="ERROR",
|
||||
topic_status="OPEN",
|
||||
)
|
||||
assert topic_handler.topic == expected_topic
|
||||
|
||||
expected_m1 = mdl.Markup(
|
||||
header=expected_h1,
|
||||
topic=expected_topic,
|
||||
)
|
||||
assert topic_handler.markup == expected_m1
|
||||
|
||||
assert topic_handler.bim_snippet is None
|
||||
|
||||
assert_viewpoints(topic_handler.viewpoints)
|
||||
|
||||
|
||||
def assert_viewpoints(viewpoints):
|
||||
assert len(viewpoints) == 1
|
||||
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
selection=mdl.ComponentSelection(),
|
||||
visibility=mdl.ComponentVisibility(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=False,
|
||||
),
|
||||
exceptions=mdl.ComponentVisibilityExceptions(
|
||||
component=[
|
||||
mdl.Component(ifc_guid="1m5wAJelDFdhn6qBdOGjos", originating_system="https://bimsync.com"),
|
||||
mdl.Component(ifc_guid="1bbI761TbBCOoIa5Kt6PXt", originating_system="https://bimsync.com"),
|
||||
]
|
||||
),
|
||||
default_visibility=True,
|
||||
),
|
||||
coloring=mdl.ComponentColoring(),
|
||||
),
|
||||
perspective_camera=mdl.PerspectiveCamera(
|
||||
camera_view_point=mdl.Point(x=23.112083480037754, y=-25.45574897560043, z=18.52519828443092),
|
||||
camera_direction=mdl.Direction(x=-0.6289237666876837, y=0.5933487270610425, z=-0.5023864884632315),
|
||||
camera_up_vector=mdl.Direction(x=-0.36542566067664123, y=0.3447553774917179, z=0.8646431727652648),
|
||||
field_of_view=60,
|
||||
aspect_ratio=1,
|
||||
),
|
||||
lines=mdl.VisualizationInfoLines(),
|
||||
clipping_planes=mdl.VisualizationInfoClippingPlanes(),
|
||||
bitmaps=mdl.VisualizationInfoBitmaps(),
|
||||
guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182",
|
||||
)
|
||||
|
||||
viewpoint = viewpoints["Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv"]
|
||||
assert viewpoint.visualization_info == expected_vp
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=False,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
exceptions=expected_exception,
|
||||
default_visibility=False,
|
||||
),
|
||||
coloring=expected_coloring,
|
||||
),
|
||||
perspective_camera=mdl.PerspectiveCamera(
|
||||
camera_view_point=mdl.Point(x=-47.18794250488281, y=43.829200744628906, z=10.666350364685059),
|
||||
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
|
||||
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
||||
field_of_view=60,
|
||||
),
|
||||
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
||||
)
|
||||
assert viewpoint.visualization_info == expected_vp
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||
expected_vp = mdl.VisualizationInfo(
|
||||
components=mdl.Components(
|
||||
view_setup_hints=mdl.ViewSetupHints(
|
||||
spaces_visible=False,
|
||||
space_boundaries_visible=False,
|
||||
openings_visible=True,
|
||||
),
|
||||
selection=expected_selection,
|
||||
visibility=mdl.ComponentVisibility(
|
||||
exceptions=expected_exception,
|
||||
default_visibility=True,
|
||||
),
|
||||
coloring=expected_coloring,
|
||||
),
|
||||
perspective_camera=mdl.PerspectiveCamera(
|
||||
camera_view_point=mdl.Point(x=-48.974571228027344, y=-64.20051574707031, z=10.666350364685059),
|
||||
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
|
||||
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
||||
field_of_view=60,
|
||||
),
|
||||
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
||||
)
|
||||
assert viewpoint.visualization_info == expected_vp
|
||||
assert viewpoint.snapshot is not None
|
||||
|
||||
|
||||
def assert_files_present(saved_bcf_path):
|
||||
expected_files = [
|
||||
"bcf.version",
|
||||
"project.bcfp",
|
||||
"documents.xml",
|
||||
"extensions.xml",
|
||||
"documents/b1d1b7f0-60b9-457d-ad12-16e0fb997bc5",
|
||||
"8ac9822a-761a-4deb-9f39-f61286acbf6a/markup.bcf",
|
||||
"8ac9822a-761a-4deb-9f39-f61286acbf6a/Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv",
|
||||
"8ac9822a-761a-4deb-9f39-f61286acbf6a/Snapshot_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.png",
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(saved_bcf_path) as bcf_zip:
|
||||
for file_path in expected_files:
|
||||
assert zipfile.Path(bcf_zip, file_path).exists()
|
||||
@@ -182,7 +182,7 @@ endif
|
||||
cp -r blenderbim/* dist/blenderbim/
|
||||
|
||||
# Provides IfcOpenShell Python functionality
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-fa6bbf2-$(PLATFORM)64.zip
|
||||
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-cdde536-$(PLATFORM)64.zip
|
||||
cd dist/working && unzip ifcopenshell-python*
|
||||
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
|
||||
|
||||
@@ -312,6 +312,13 @@ endif
|
||||
cp -r dist/working/xmlschema-2.0.2/xmlschema dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by bcf
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/ad/c7/17c9d16320d8e2cfdb27d2fd298b5e8f7a3f211025b0c1bc7a39a85bd690/xsdata-22.11.tar.gz
|
||||
cd dist/working && tar -xzvf xsdata*
|
||||
cp -r dist/working/xsdata-22.11/xsdata dist/blenderbim/libs/site/packages/
|
||||
rm -rf dist/working
|
||||
|
||||
# Required by bcf
|
||||
mkdir dist/working
|
||||
cd dist/working && wget https://files.pythonhosted.org/packages/11/bc/5afb61dd5d863e5cf77cd952445c50c17e65953405986f19e97e4389692a/elementpath-3.0.2.tar.gz
|
||||
|
||||
@@ -91,10 +91,12 @@ classes = [
|
||||
operator.RemoveIfcFile,
|
||||
operator.SelectDataDir,
|
||||
operator.SelectIfcFile,
|
||||
operator.ReloadSelectedIfcFile,
|
||||
operator.SelectSchemaDir,
|
||||
operator.SelectURIAttribute,
|
||||
operator.EditBlenderCollection,
|
||||
operator.BIM_OT_open_webbrowser,
|
||||
operator.BIM_OT_show_description,
|
||||
prop.StrProperty,
|
||||
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
|
||||
prop.ObjProperty,
|
||||
@@ -159,6 +161,8 @@ def register():
|
||||
bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||
bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
|
||||
bpy.types.SCENE_PT_unit.append(ui.ifc_units)
|
||||
if hasattr(bpy.types, "UI_MT_button_context_menu"):
|
||||
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
|
||||
|
||||
for mod in modules.values():
|
||||
mod.register()
|
||||
@@ -179,6 +183,8 @@ def unregister():
|
||||
del bpy.types.Camera.BIMMeshProperties
|
||||
del bpy.types.PointLight.BIMMeshProperties
|
||||
bpy.types.SCENE_PT_unit.remove(ui.ifc_units)
|
||||
if hasattr(bpy.types, "UI_MT_button_context_menu"):
|
||||
bpy.types.UI_MT_button_context_menu.remove(ui.draw_custom_context_menu)
|
||||
|
||||
for mod in reversed(list(modules.values())):
|
||||
mod.unregister()
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -26,6 +26,7 @@ from blenderbim.bim.module.drawing.prop import RasterStyleProperty
|
||||
from bpy.app.handlers import persistent
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.owner.prop import get_user_person, get_user_organisation
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from ifcopenshell.api.material.data import Data as MaterialData
|
||||
from ifcopenshell.api.type.data import Data as TypeData
|
||||
|
||||
@@ -301,3 +302,4 @@ def setDefaultProperties(scene):
|
||||
drawing_style.name = "Blender Default"
|
||||
drawing_style.render_type = "DEFAULT"
|
||||
bpy.ops.bim.save_drawing_style(index="2")
|
||||
AuthoringData.type_thumbnails = {}
|
||||
|
||||
@@ -23,11 +23,11 @@ import math
|
||||
import zipfile
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.attribute
|
||||
from blenderbim.bim.prop import get_ifc_entity_doc_url
|
||||
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
|
||||
from mathutils import geometry
|
||||
from mathutils import Vector
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
|
||||
def draw_attributes(props, layout, copy_operator=None):
|
||||
@@ -85,6 +85,7 @@ def import_attribute(attribute, props, data, callback=None):
|
||||
new.is_null = data[attribute.name()] is None
|
||||
new.is_optional = attribute.optional()
|
||||
new.data_type = data_type if isinstance(data_type, str) else ""
|
||||
new.ifc_class = data["type"]
|
||||
is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
|
||||
|
||||
if is_handled_by_callback:
|
||||
@@ -92,19 +93,66 @@ def import_attribute(attribute, props, data, callback=None):
|
||||
elif is_handled_by_callback is False:
|
||||
props.remove(len(props) - 1)
|
||||
elif data_type == "string":
|
||||
new.string_value = "" if new.is_null else data[attribute.name()]
|
||||
new.string_value = "" if new.is_null else str(data[attribute.name()])
|
||||
if attribute.type_of_attribute().declared_type().name() == "IfcURIReference":
|
||||
new.is_uri = True
|
||||
elif data_type == "boolean":
|
||||
new.bool_value = False if new.is_null else data[attribute.name()]
|
||||
new.bool_value = False if new.is_null else bool(data[attribute.name()])
|
||||
elif data_type == "integer":
|
||||
new.int_value = 0 if new.is_null else data[attribute.name()]
|
||||
new.int_value = 0 if new.is_null else int(data[attribute.name()])
|
||||
elif data_type == "float":
|
||||
new.float_value = 0.0 if new.is_null else data[attribute.name()]
|
||||
new.float_value = 0.0 if new.is_null else float(data[attribute.name()])
|
||||
elif data_type == "enum":
|
||||
new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
|
||||
if data[attribute.name()]:
|
||||
new.enum_value = data[attribute.name()]
|
||||
enum_items = ifcopenshell.util.attribute.get_enum_items(attribute)
|
||||
new.enum_items = json.dumps(enum_items)
|
||||
add_attribute_enum_items_descriptions(new, enum_items)
|
||||
if data[new.name]:
|
||||
new.enum_value = data[new.name]
|
||||
add_attribute_description(new, data)
|
||||
add_attribute_min_max(new)
|
||||
|
||||
|
||||
ATTRIBUTE_MIN_MAX_CONSTRAINTS = {"IfcMaterialLayer": {"Priority": {"value_min": 0, "value_max": 100}}}
|
||||
|
||||
|
||||
def add_attribute_min_max(attribute_blender):
|
||||
if attribute_blender.ifc_class in ATTRIBUTE_MIN_MAX_CONSTRAINTS:
|
||||
constraints = ATTRIBUTE_MIN_MAX_CONSTRAINTS[attribute_blender.ifc_class].get(attribute_blender.name, {})
|
||||
for constraint, value in constraints.items():
|
||||
setattr(attribute_blender, constraint, value)
|
||||
setattr(attribute_blender, constraint + "_constraint", True)
|
||||
|
||||
|
||||
def add_attribute_enum_items_descriptions(attribute_blender, enum_items):
|
||||
attribute_blender.enum_descriptions.clear()
|
||||
if isinstance(enum_items, dict):
|
||||
enum_items = enum_items.keys()
|
||||
version = tool.Ifc.get_schema()
|
||||
for enum_item in enum_items:
|
||||
new_enum_description = attribute_blender.enum_descriptions.add()
|
||||
try:
|
||||
description = get_predefined_type_doc(version, attribute_blender.ifc_class, enum_item) or ""
|
||||
except KeyError: # TODO this only supports predefined type enums. Add support for other types of enums ?
|
||||
description = ""
|
||||
new_enum_description.name = description
|
||||
|
||||
|
||||
def add_attribute_description(attribute_blender, attribute_ifc=None):
|
||||
if not attribute_blender.name:
|
||||
return
|
||||
version = tool.Ifc.get_schema()
|
||||
description = ""
|
||||
try:
|
||||
description = get_attribute_doc(version, attribute_blender.ifc_class, attribute_blender.name)
|
||||
except RuntimeError: # It's not an Entity Attribute. Let's try a Property Set attribute.
|
||||
doc = get_property_doc(version, attribute_blender.ifc_class, attribute_blender.name)
|
||||
if doc:
|
||||
description = doc.get("description", "")
|
||||
else: # It's a custom property set. Check if this attribute has a description
|
||||
if attribute_ifc is not None:
|
||||
description = getattr(attribute_ifc, "Description", "")
|
||||
if description:
|
||||
attribute_blender.description = description
|
||||
|
||||
|
||||
def export_attributes(props, callback=None):
|
||||
@@ -120,29 +168,18 @@ def export_attributes(props, callback=None):
|
||||
def prop_with_search(layout, data, prop_name, **kwargs):
|
||||
# kwargs are layout.prop arguments (text, icon, etc.)
|
||||
row = layout.row(align=True)
|
||||
# Magick courtesy of https://blender.stackexchange.com/a/203443/86891
|
||||
row.context_pointer_set(name="data", data=data)
|
||||
row.prop(data, prop_name, **kwargs)
|
||||
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
|
||||
op.prop_name = prop_name
|
||||
|
||||
add_entity_url_button(row, getattr(data, prop_name))
|
||||
try:
|
||||
if len(get_enum_items(data, prop_name)) > 10:
|
||||
# Magick courtesy of https://blender.stackexchange.com/a/203443/86891
|
||||
row.context_pointer_set(name="data", data=data)
|
||||
op = row.operator("bim.enum_property_search", text="", icon="VIEWZOOM")
|
||||
op.prop_name = prop_name
|
||||
except TypeError: # Prop is not iterable
|
||||
pass
|
||||
|
||||
|
||||
def add_entity_url_button(layout, entity):
|
||||
if entity:
|
||||
try:
|
||||
url = get_ifc_entity_doc_url(entity)
|
||||
except KeyError:
|
||||
# TODO : support attributes, pset, etc.
|
||||
pass
|
||||
else:
|
||||
if url:
|
||||
url_op = layout.operator("bim.open_webbrowser", icon="INFO", text="")
|
||||
url_op.url = url
|
||||
|
||||
|
||||
def get_enum_items(data, prop_name, context):
|
||||
def get_enum_items(data, prop_name, context=None):
|
||||
# Retrieve items from a dynamic EnumProperty, which is otherwise not supported
|
||||
# Or throws an error in the console when the items callback returns an empty list
|
||||
# See https://blender.stackexchange.com/q/215781/86891
|
||||
@@ -152,7 +189,7 @@ def get_enum_items(data, prop_name, context):
|
||||
return
|
||||
if not isinstance(items, (list, tuple)):
|
||||
# items are retrieved through a callback, not a static list :
|
||||
items = items(data, context)
|
||||
items = items(data, context or bpy.context)
|
||||
return items
|
||||
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class MaterialCreator:
|
||||
|
||||
def parse_representation(self, representation):
|
||||
has_parsed = False
|
||||
representation_items = self.resolve_all_representation_items(representation)
|
||||
representation_items = self.resolve_all_stylable_representation_items(representation)
|
||||
for item in representation_items:
|
||||
if self.parse_representation_item(item):
|
||||
has_parsed = True
|
||||
@@ -153,11 +153,19 @@ class MaterialCreator:
|
||||
]
|
||||
self.mesh.polygons.foreach_set("material_index", material_index)
|
||||
|
||||
def resolve_all_representation_items(self, representation):
|
||||
def resolve_all_stylable_representation_items(self, representation):
|
||||
items = []
|
||||
for item in representation.Items:
|
||||
if item.is_a("IfcMappedItem"):
|
||||
items.extend(item.MappingSource.MappedRepresentation.Items)
|
||||
if item.is_a("IfcBooleanResult"):
|
||||
operand = item.FirstOperand
|
||||
while True:
|
||||
items.append(operand)
|
||||
if operand.is_a("IfcBooleanResult"):
|
||||
operand = operand.FirstOperand
|
||||
else:
|
||||
break
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
@@ -329,11 +337,14 @@ class IfcImporter:
|
||||
if self.ifc_import_settings.has_filter or offset or offset_limit < len(self.elements):
|
||||
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
|
||||
else:
|
||||
self.element_types = set(
|
||||
self.file.by_type("IfcElementType")
|
||||
+ self.file.by_type("IfcDoorStyle")
|
||||
+ self.file.by_type("IfcWindowStyle")
|
||||
)
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.element_types = set(
|
||||
self.file.by_type("IfcElementType")
|
||||
+ self.file.by_type("IfcDoorStyle")
|
||||
+ self.file.by_type("IfcWindowStyle")
|
||||
)
|
||||
else:
|
||||
self.element_types = set(self.file.by_type("IfcElementType"))
|
||||
|
||||
if self.ifc_import_settings.has_filter and self.ifc_import_settings.should_filter_spatial_elements:
|
||||
self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements)
|
||||
@@ -517,14 +528,21 @@ class IfcImporter:
|
||||
|
||||
def does_element_likely_have_geometry_far_away(self, element):
|
||||
for representation in element.Representation.Representations:
|
||||
for subelement in self.file.traverse(representation):
|
||||
if subelement.is_a("IfcCartesianPointList3D"):
|
||||
for point in subelement.CoordList:
|
||||
if len(point) == 3 and self.is_point_far_away(point, is_meters=False):
|
||||
items = []
|
||||
for item in representation.Items:
|
||||
if item.is_a("IfcMappedItem"):
|
||||
items.extend(item.MappingSource.MappedRepresentation.Items)
|
||||
else:
|
||||
items.append(item)
|
||||
for item in items:
|
||||
for subelement in self.file.traverse(item):
|
||||
if subelement.is_a("IfcCartesianPointList3D"):
|
||||
for point in subelement.CoordList:
|
||||
if len(point) == 3 and self.is_point_far_away(point, is_meters=False):
|
||||
return True
|
||||
if subelement.is_a("IfcCartesianPoint"):
|
||||
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
|
||||
return True
|
||||
if subelement.is_a("IfcCartesianPoint"):
|
||||
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
|
||||
return True
|
||||
|
||||
def apply_blender_offset_to_matrix_world(self, obj, matrix):
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
|
||||
@@ -28,3 +28,7 @@ class BcfStore:
|
||||
if not BcfStore.bcfxml:
|
||||
BcfStore.bcfxml = bcf.v2.bcfxml.BcfXml()
|
||||
return BcfStore.bcfxml
|
||||
|
||||
@classmethod
|
||||
def set(cls, bcfxml):
|
||||
cls.bcfxml = bcfxml
|
||||
|
||||
@@ -20,7 +20,6 @@ import os
|
||||
import bpy
|
||||
import bcf
|
||||
import bcf.bcfxml
|
||||
import bcf.v2.data
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
@@ -38,8 +37,8 @@ class NewBcfProject(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
context.scene.BCFProperties.is_loaded = False
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
bcfxml.new_project()
|
||||
bcfxml = bcf.v2.bcfxml.BcfXml.create_new("New Project")
|
||||
bcfstore.BcfStore.set(bcfxml)
|
||||
bpy.ops.bim.load_bcf_project()
|
||||
context.scene.BCFProperties.is_loaded = True
|
||||
return {"FINISHED"}
|
||||
@@ -97,34 +96,34 @@ class LoadBcfTopic(bpy.types.Operator):
|
||||
new = context.scene.BCFProperties.topics[self.topic_index]
|
||||
data_map = {
|
||||
"name": topic.guid,
|
||||
"title": topic.title,
|
||||
"type": topic.topic_type,
|
||||
"status": topic.topic_status,
|
||||
"priority": topic.priority,
|
||||
"stage": topic.stage,
|
||||
"creation_date": topic.creation_date,
|
||||
"creation_author": topic.creation_author,
|
||||
"modified_date": topic.modified_date,
|
||||
"modified_author": topic.modified_author,
|
||||
"assigned_to": topic.assigned_to,
|
||||
"due_date": topic.due_date,
|
||||
"description": topic.description,
|
||||
"title": topic.topic.title,
|
||||
"type": topic.topic.topic_type,
|
||||
"status": topic.topic.topic_status,
|
||||
"priority": topic.topic.priority,
|
||||
"stage": topic.topic.stage,
|
||||
"creation_date": topic.topic.creation_date,
|
||||
"creation_author": topic.topic.creation_author,
|
||||
"modified_date": topic.topic.modified_date,
|
||||
"modified_author": topic.topic.modified_author,
|
||||
"assigned_to": topic.topic.assigned_to,
|
||||
"due_date": topic.topic.due_date,
|
||||
"description": topic.topic.description,
|
||||
}
|
||||
for key, value in data_map.items():
|
||||
if value is not None:
|
||||
setattr(new, key, str(value))
|
||||
|
||||
new.reference_links.clear()
|
||||
for reference_link in topic.reference_links:
|
||||
for reference_link in topic.topic.reference_link:
|
||||
new_reference_link = new.reference_links.add()
|
||||
new_reference_link.name = reference_link
|
||||
|
||||
new.labels.clear()
|
||||
for label in topic.labels:
|
||||
for label in topic.topic.labels:
|
||||
new_label = new.labels.add()
|
||||
new_label.name = label
|
||||
|
||||
if topic.bim_snippet:
|
||||
if topic.topic.bim_snippet:
|
||||
data_map = {
|
||||
"type": topic.bim_snippet.snippet_type,
|
||||
"is_external": topic.bim_snippet.is_external,
|
||||
@@ -136,7 +135,7 @@ class LoadBcfTopic(bpy.types.Operator):
|
||||
setattr(new.bim_snippet, key, value)
|
||||
|
||||
new.document_references.clear()
|
||||
for doc in topic.document_references:
|
||||
for doc in topic.topic.document_reference:
|
||||
new_document_references = new.document_references.add()
|
||||
data_map = {
|
||||
"reference": doc.referenced_document,
|
||||
@@ -149,7 +148,7 @@ class LoadBcfTopic(bpy.types.Operator):
|
||||
setattr(new_document_references, key, value)
|
||||
|
||||
new.related_topics.clear()
|
||||
for related_topic in topic.related_topics:
|
||||
for related_topic in topic.topic.related_topic:
|
||||
new_related_topic = new.related_topics.add()
|
||||
new_related_topic.name = related_topic.guid
|
||||
|
||||
@@ -276,7 +275,7 @@ class AddBcfTopic(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
bcfxml = bcfstore.BcfStore.get_bcfxml()
|
||||
bcfxml.add_topic()
|
||||
bcfxml.add_topic("New Topic", "", context.scene.BCFProperties.author)
|
||||
bpy.ops.bim.load_bcf_topics()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
tool.Boundary.move_origin_to_space_origin(context.active_object)
|
||||
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
|
||||
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -67,7 +67,6 @@ class BIM_PT_Boundary(Panel):
|
||||
def draw(self, context):
|
||||
props = context.active_object.BIMObjectProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
element = ifc_file.by_id(props.ifc_definition_id)
|
||||
boundary = ifc_file.by_id(props.ifc_definition_id)
|
||||
self.bprops = context.active_object.bim_boundary_properties
|
||||
if self.bprops.is_editing:
|
||||
|
||||
@@ -51,7 +51,6 @@ class BIM_PT_brickschema(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, self.props, "namespace", text="")
|
||||
prop_with_search(row, self.props, "brick_equipment_class", text="")
|
||||
row.prop(self.props, "brick_equipment_class", text="")
|
||||
row.operator("bim.add_brick", text="", icon="ADD")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.module.type.prop as type_prop
|
||||
from bpy.types import WorkSpaceTool
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
@@ -51,9 +52,14 @@ class CadTool(WorkSpaceTool):
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="", icon="EVENT_Q")
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
if obj.BIMObjectProperties.ifc_definition_id:
|
||||
row.operator("bim.edit_extrusion_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
|
||||
else:
|
||||
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
|
||||
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
|
||||
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -180,7 +186,10 @@ class CadHotkey(bpy.types.Operator):
|
||||
bpy.ops.bim.cad_offset(distance=self.props.distance)
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
if tool.Ifc.get_entity(bpy.context.active_object):
|
||||
bpy.ops.bim.edit_extrusion_profile()
|
||||
else:
|
||||
bpy.ops.bim.edit_arbitrary_profile()
|
||||
|
||||
def hotkey_S_R(self):
|
||||
if self.is_profile():
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from ifcopenshell.util.doc import get_entity_doc
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.prop import Attribute
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -31,6 +33,11 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def get_available_constraint_types(self, context):
|
||||
version = tool.Ifc.get_schema()
|
||||
return [(c, c, get_entity_doc(version, c).get("description", "")) for c in ["IfcObjective"]]
|
||||
|
||||
|
||||
class Constraint(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
@@ -46,6 +53,4 @@ class BIMConstraintProperties(PropertyGroup):
|
||||
|
||||
class BIMObjectConstraintProperties(PropertyGroup):
|
||||
is_adding: StringProperty(name="Is Adding")
|
||||
available_constraint_types: EnumProperty(
|
||||
items=[(c, c, "") for c in ["IfcObjective"]], name="Available Constraint Types"
|
||||
)
|
||||
available_constraint_types: EnumProperty(items=get_available_constraint_types, name="Available Constraint Types")
|
||||
|
||||
@@ -37,6 +37,7 @@ class BIM_PT_debug(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(context.scene.BIMProperties, "ifc_file", text="")
|
||||
row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="")
|
||||
row.operator("bim.reload_selected_ifc_file", icon="FILE_REFRESH", text="")
|
||||
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -32,6 +32,12 @@ from mathutils import Vector, Matrix
|
||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from math import acos, pi
|
||||
|
||||
|
||||
def ccw(A, B, C):
|
||||
"""whether a-b-c located in counter-clockwise order in 2d space"""
|
||||
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x)
|
||||
|
||||
|
||||
class BaseDecorator:
|
||||
@@ -66,6 +72,42 @@ class BaseDecorator:
|
||||
}
|
||||
}
|
||||
|
||||
bool check_counterclockwise(in vec4 A, in vec4 B, in vec4 C) {
|
||||
return (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x);
|
||||
}
|
||||
|
||||
void angle_circle_head(
|
||||
in vec4 circle_start, in float circle_angle,
|
||||
in bool counterclockwise,
|
||||
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
|
||||
|
||||
// 1 added to CIRCLE_SEGS because we're number of vertices
|
||||
// for n segments is n+1
|
||||
|
||||
float angle_d;
|
||||
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
|
||||
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
|
||||
angle_segs = max(1, ceil(circle_angle / angle_d));
|
||||
angle_d = circle_angle / angle_segs;
|
||||
|
||||
for(int i = 0; i < (angle_segs + 1); i++) {
|
||||
float angle = angle_d * i;
|
||||
if (counterclockwise) {
|
||||
head[i] = vec4(
|
||||
circle_start.x * cos(-angle) + circle_start.y * sin(-angle),
|
||||
circle_start.x * -sin(-angle) + circle_start.y * cos(-angle),
|
||||
0, 0
|
||||
);
|
||||
} else {
|
||||
head[i] = vec4(
|
||||
circle_start.x * cos(angle) + circle_start.y * sin(angle),
|
||||
circle_start.x * -sin(angle) + circle_start.y * cos(angle),
|
||||
0, 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
|
||||
vec4 nose = dir * size;
|
||||
float c = cos(PI/2), s = sin(PI/2);
|
||||
@@ -79,12 +121,15 @@ class BaseDecorator:
|
||||
uniform mat4 viewMatrix;
|
||||
in vec3 pos;
|
||||
in uint topo;
|
||||
in vec3 next_vert;
|
||||
out vec4 gl_Position;
|
||||
out uint type;
|
||||
out vec4 v_next_vert;
|
||||
|
||||
void main() {
|
||||
gl_Position = viewMatrix * vec4(pos, 1.0);
|
||||
type = topo;
|
||||
v_next_vert = viewMatrix * vec4(next_vert, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -267,7 +312,13 @@ class BaseDecorator:
|
||||
"""perform actual drawing stuff"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def draw_lines(self, context, obj, vertices, indices, topology=None, is_scale_dependant=True):
|
||||
def draw_lines(
|
||||
self, context, obj, vertices, indices, topology=None, is_scale_dependant=True, fill_next_vertices=False
|
||||
):
|
||||
# use is_scale_dependant = False if shader is not using uniform viewportDrawingScale
|
||||
# otherwise uniform will be discarded during the optimization process
|
||||
# and you will get "ValueError: GPUShader.uniform_float: uniform viewportDrawingScale not found"
|
||||
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
color = context.scene.DocProperties.decorations_colour
|
||||
@@ -276,12 +327,18 @@ class BaseDecorator:
|
||||
fmt.attr_add(id="pos", comp_type="F32", len=3, fetch_mode="FLOAT")
|
||||
if topology:
|
||||
fmt.attr_add(id="topo", comp_type="U8", len=1, fetch_mode="INT")
|
||||
if fill_next_vertices:
|
||||
fmt.attr_add(id="next_vert", comp_type="F32", len=3, fetch_mode="FLOAT")
|
||||
|
||||
vbo = GPUVertBuf(len=len(vertices), format=fmt)
|
||||
vbo.attr_fill(id="pos", data=vertices)
|
||||
if topology:
|
||||
vbo.attr_fill(id="topo", data=topology)
|
||||
|
||||
if fill_next_vertices:
|
||||
shifted_vertices = vertices[1:] + [vertices[0]]
|
||||
vbo.attr_fill(id="next_vert", data=shifted_vertices)
|
||||
|
||||
ibo = GPUIndexBuf(type="LINES", seq=indices)
|
||||
|
||||
batch = GPUBatch(type="LINES", buf=vbo, elem=ibo)
|
||||
@@ -480,6 +537,132 @@ class DimensionDecorator(BaseDecorator):
|
||||
self.draw_label(context, text, p0 + (dir) * 0.5, dir)
|
||||
|
||||
|
||||
class AngleDecorator(BaseDecorator):
|
||||
"""Decorator for angle objects
|
||||
- each edge of a segment with arrow
|
||||
- every non-last edge has angle circle
|
||||
- every circle is labeled with angle in degrees
|
||||
"""
|
||||
|
||||
objecttype = "ANGLE"
|
||||
|
||||
DEF_GLSL = (
|
||||
BaseDecorator.DEF_GLSL
|
||||
+ """
|
||||
#define ARROW_ANGLE PI / 12.0
|
||||
#define ARROW_SIZE 8.0
|
||||
#define CIRCLE_SIZE 6.0
|
||||
"""
|
||||
)
|
||||
|
||||
GEOM_GLSL = """
|
||||
uniform vec2 winsize;
|
||||
uniform float viewportDrawingScale;
|
||||
|
||||
layout(lines) in;
|
||||
layout(line_strip, max_vertices=MAX_POINTS) out;
|
||||
in uint type[];
|
||||
in vec4 v_next_vert[];
|
||||
|
||||
// per edge shader
|
||||
void main() {
|
||||
vec4 clip2win = matCLIP2WIN();
|
||||
vec4 win2clip = matWIN2CLIP();
|
||||
|
||||
vec4 p0 = gl_in[0].gl_Position, p1 = gl_in[1].gl_Position;
|
||||
vec4 p2 = v_next_vert[1];
|
||||
uint t0 = type[0], t1 = type[1];
|
||||
|
||||
vec4 p0w = CLIP2WIN(p0), p1w = CLIP2WIN(p1);
|
||||
vec4 p2w = CLIP2WIN(p2);
|
||||
vec4 edge0 = p1w - p0w, dir = normalize(edge0);
|
||||
|
||||
vec4 p;
|
||||
// draw a segment line
|
||||
p = p0w;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
p = p1w;
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
EndPrimitive();
|
||||
|
||||
// end edge with angle circle for the non-last segment
|
||||
if (t1 == 0u) { // draws only on internal vertex
|
||||
edge0 = p0w - p1w;
|
||||
vec4 dir0 = normalize(edge0);
|
||||
vec4 edge1 = p2w - p1w;
|
||||
vec4 dir1 = normalize(edge1);
|
||||
|
||||
float angle_circle_size = min( length(edge0), length(edge1) );
|
||||
vec4 circle_start = dir0 * angle_circle_size;
|
||||
vec4 circle_end = dir1 * angle_circle_size;
|
||||
|
||||
float cos_a = dot( edge0, edge1 ) / ( length(edge0) * length(edge1) );
|
||||
float circle_angle = acos(cos_a);
|
||||
|
||||
vec4 circle_head_data[CIRCLE_SEGS+1];
|
||||
float angle_segs;
|
||||
bool counterclockwise = check_counterclockwise(p2w, p1w, p0w);
|
||||
angle_circle_head(
|
||||
circle_start,
|
||||
circle_angle,
|
||||
counterclockwise,
|
||||
circle_head_data,
|
||||
angle_segs);
|
||||
|
||||
for(int i=0; i<angle_segs+1; i++) {
|
||||
p = p1w + circle_head_data[i];
|
||||
gl_Position = WIN2CLIP(p);
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def decorate(self, context, obj):
|
||||
verts, idxs, topo = self.get_path_geom(obj)
|
||||
self.draw_lines(context, obj, verts, idxs, topo, fill_next_vertices=True, is_scale_dependant=False)
|
||||
self.draw_labels(context, obj, verts, idxs)
|
||||
|
||||
def draw_labels(self, context, obj, vertices, indices):
|
||||
region = context.region
|
||||
region3d = context.region_data
|
||||
|
||||
last_segment_i = len(indices) - 1
|
||||
for edge_i, edge_vertices in enumerate(indices):
|
||||
if edge_i == last_segment_i:
|
||||
continue
|
||||
|
||||
# draw angle label
|
||||
i0, i1 = edge_vertices
|
||||
v0 = Vector(vertices[i0])
|
||||
v1 = Vector(vertices[i1])
|
||||
v2 = Vector(vertices[i1 + 1])
|
||||
p0 = location_3d_to_region_2d(region, region3d, v0)
|
||||
p1 = location_3d_to_region_2d(region, region3d, v1)
|
||||
p2 = location_3d_to_region_2d(region, region3d, v2)
|
||||
|
||||
edge0 = p0 - p1
|
||||
edge1 = p2 - p1
|
||||
cos_a = edge0.dot(edge1) / (edge0.length * edge1.length)
|
||||
circle_angle = acos(cos_a) / pi * 180
|
||||
|
||||
text = f"{int(circle_angle)}d"
|
||||
|
||||
# TODO: set label position pased on p1
|
||||
# + y relative to p0p1 if p0p1p2 is clockwise
|
||||
# - y relative to p0p1 if p0p1p2 is counter-clockwise
|
||||
# counter_clockwise = ccw(p0, p1, p2)
|
||||
# label_position = (p1 + Vector( (0, 10) ) * (1 if counter_clockwise else -1)) + edge1 * 0.1
|
||||
label_position = p1 + edge1 * 0.1
|
||||
|
||||
# TODO: set label direction based on the first edge (p0, p1)
|
||||
label_dir = Vector((1, 0))
|
||||
self.draw_label(context, text, label_position, label_dir)
|
||||
|
||||
|
||||
class DiameterDecorator(DimensionDecorator):
|
||||
objecttype = "DIAMETER"
|
||||
|
||||
@@ -895,7 +1078,6 @@ class LevelDecorator(BaseDecorator):
|
||||
def decorate(self, context, obj):
|
||||
verts, idxs, topo = self.get_path_geom(obj)
|
||||
self.draw_lines(context, obj, verts, idxs, topo)
|
||||
splines = self.get_splines(obj)
|
||||
self.draw_labels(context, obj, splines)
|
||||
|
||||
|
||||
@@ -1585,6 +1767,7 @@ class TextDecorator(BaseDecorator):
|
||||
class DecorationsHandler:
|
||||
decorators_classes = [
|
||||
DimensionDecorator,
|
||||
AngleDecorator,
|
||||
GridDecorator,
|
||||
HiddenDecorator,
|
||||
LeaderDecorator,
|
||||
|
||||
@@ -34,6 +34,7 @@ import blenderbim.bim.module.drawing.annotation as annotation
|
||||
from mathutils import Vector
|
||||
from mathutils import geometry
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
class External(svgwrite.container.Group):
|
||||
@@ -140,6 +141,8 @@ class SvgWriter:
|
||||
self.draw_dimension_annotations(obj)
|
||||
elif element.ObjectType == "ANGLE":
|
||||
self.draw_angle_annotations(obj)
|
||||
elif element.ObjectType == "ANGLE_OLD":
|
||||
self.draw_angle_old_annotations(obj)
|
||||
elif element.ObjectType == "RADIUS":
|
||||
self.draw_radius_annotations(obj)
|
||||
elif element.ObjectType == "DIAMETER":
|
||||
@@ -619,19 +622,52 @@ class SvgWriter:
|
||||
)
|
||||
|
||||
def draw_angle_annotations(self, obj):
|
||||
# calculate p3 which is the center of the arc
|
||||
# to use draw_svg_3point_arc()
|
||||
points = obj.data.splines[0].points
|
||||
region = bpy.context.region
|
||||
region_3d = bpy.context.area.spaces.active.region_3d
|
||||
points_2d = [view3d_utils.location_3d_to_region_2d(region, region_3d, p.co.xyz) for p in points]
|
||||
|
||||
edge0 = points_2d[0] - points_2d[1]
|
||||
edge1 = points_2d[2] - points_2d[1]
|
||||
angle_radius = min(edge0.length, edge1.length)
|
||||
dir0 = edge0.normalized()
|
||||
dir1 = edge1.normalized()
|
||||
dir2 = ((dir0 + dir1) / 2).normalized()
|
||||
|
||||
p3 = points_2d[1] + dir2 * angle_radius
|
||||
|
||||
# make all edges the same radius
|
||||
p0 = points_2d[1] + dir0 * angle_radius
|
||||
p2 = points_2d[1] + dir1 * angle_radius
|
||||
points = [view3d_utils.region_2d_to_origin_3d(region, region_3d, p) for p in [p0, p3, p2]]
|
||||
# points = [p.co.xyz for p in bpy.context.object.data.splines[0].points[:3]]
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
new_verts = [bm.verts.new(p) for p in points]
|
||||
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in ((0, 1), (1, 2))]
|
||||
self.draw_svg_3point_arc(obj, bm)
|
||||
|
||||
def draw_angle_old_annotations(self, obj):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bm.verts.ensure_lookup_table()
|
||||
self.draw_svg_3point_arc(obj, bm)
|
||||
|
||||
def draw_svg_3point_arc(self, obj, bm):
|
||||
# This implementation uses an SVG arc, which means that it can only draw
|
||||
# arcs that are orthogonal to the view (e.g. not arcs in 3D).
|
||||
# Gosh this is bad code :(
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
|
||||
points = [v.co for v in bm.verts][:3]
|
||||
center = tool.Cad.get_center_of_arc(points, obj)
|
||||
classes = self.get_attribute_classes(obj)
|
||||
matrix_world = obj.matrix_world
|
||||
|
||||
points = [v.co for v in obj.data.vertices][:3]
|
||||
center = tool.Cad.get_center_of_arc(points, obj)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
bm.verts.ensure_lookup_table()
|
||||
arc_end_verts = [v for v in bm.verts if len(v.link_edges) == 1]
|
||||
arc_end_pts = [matrix_world @ v.co for v in arc_end_verts]
|
||||
@@ -680,11 +716,11 @@ class SvgWriter:
|
||||
|
||||
# Center of gravity of all vertices, used to help position the text
|
||||
cog = Vector((0, 0, 0))
|
||||
for vert in obj.data.vertices:
|
||||
cog += vert.co
|
||||
cog = matrix_world @ (cog / len(obj.data.vertices))
|
||||
for point in points:
|
||||
cog += point
|
||||
cog = matrix_world @ (cog / len(points))
|
||||
|
||||
radius = ((matrix_world @ obj.data.vertices[0].co) - center).length
|
||||
radius = ((matrix_world @ points[0]) - center).length
|
||||
|
||||
arc_midpoint = center + ((cog - center).normalized() * radius)
|
||||
|
||||
|
||||
@@ -171,10 +171,10 @@ class BIM_PT_drawings(Panel):
|
||||
active_drawing = self.props.drawings[self.props.active_drawing_index]
|
||||
row = self.layout.row(align=True)
|
||||
col = row.column()
|
||||
col.alignment = 'LEFT'
|
||||
col.alignment = "LEFT"
|
||||
col.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id
|
||||
col = row.column()
|
||||
col.alignment = 'RIGHT'
|
||||
col.alignment = "RIGHT"
|
||||
op = row.operator("bim.open_view", icon="URL", text="")
|
||||
op.view = active_drawing.name
|
||||
op = row.operator("bim.activate_view", icon="OUTLINER_OB_CAMERA", text="")
|
||||
@@ -376,6 +376,9 @@ class BIM_PT_annotation_utilities(Panel):
|
||||
op.data_type = "curve"
|
||||
op = row.operator("bim.add_annotation", text="Angle", icon="DRIVER_ROTATIONAL_DIFFERENCE")
|
||||
op.object_type = "ANGLE"
|
||||
op.data_type = "curve"
|
||||
op = row.operator("bim.add_annotation", text="Angle (old)", icon="DRIVER_ROTATIONAL_DIFFERENCE")
|
||||
op.object_type = "ANGLE_OLD"
|
||||
op.data_type = "mesh"
|
||||
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -481,7 +481,7 @@ class Helper:
|
||||
unshared_vert.co, profile_face.verts[0].co, profile_face.normal
|
||||
)
|
||||
)
|
||||
> 0.001
|
||||
> 1e-6
|
||||
):
|
||||
if unshared_vert == edge.verts[1]:
|
||||
return [edge.verts[0].index, edge.verts[1].index]
|
||||
|
||||
@@ -367,10 +367,12 @@ class OverrideDeleteTrait:
|
||||
if getattr(element, "FillsVoids", None):
|
||||
self.remove_filling(element)
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
for rel in element.HasFillings:
|
||||
self.remove_filling(rel.RelatedBuildingElement)
|
||||
if element.VoidsElements:
|
||||
self.delete_opening_element(element)
|
||||
if element.HasFillings:
|
||||
for rel in element.HasFillings:
|
||||
self.remove_filling(rel.RelatedBuildingElement)
|
||||
else:
|
||||
if element.VoidsElements:
|
||||
self.delete_opening_element(element)
|
||||
else:
|
||||
if getattr(element, "HasOpenings", None):
|
||||
for rel in element.HasOpenings:
|
||||
@@ -545,7 +547,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
# Copy the actual class
|
||||
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
if new:
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = new
|
||||
old_to_new[tool.Ifc.get_entity(obj)] = [new]
|
||||
# Recreate decompositions
|
||||
tool.Root.recreate_decompositions(relationships, old_to_new)
|
||||
bpy.ops.transform.translate("INVOKE_DEFAULT")
|
||||
|
||||
@@ -32,11 +32,14 @@ classes = (
|
||||
operator.CopyMaterial,
|
||||
operator.DisableEditingAssignedMaterial,
|
||||
operator.DisableEditingMaterialSetItem,
|
||||
operator.DisableEditingMaterialSetItemProfile,
|
||||
operator.DisableEditingMaterials,
|
||||
operator.EditAssignedMaterial,
|
||||
operator.EditMaterialSetItem,
|
||||
operator.EditMaterialSetItemProfile,
|
||||
operator.EnableEditingAssignedMaterial,
|
||||
operator.EnableEditingMaterialSetItem,
|
||||
operator.EnableEditingMaterialSetItemProfile,
|
||||
operator.ExpandMaterialCategory,
|
||||
operator.LoadMaterials,
|
||||
operator.RemoveConstituent,
|
||||
|
||||
@@ -37,6 +37,7 @@ class MaterialsData:
|
||||
cls.data = {
|
||||
"total_materials": cls.total_materials(),
|
||||
"material_types": cls.material_types(),
|
||||
"profiles": cls.profiles(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@@ -65,9 +66,14 @@ class MaterialsData:
|
||||
"IfcMaterialProfileSet",
|
||||
"IfcMaterialList",
|
||||
]
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
version = tool.Ifc.get_schema()
|
||||
if version == "IFC2X3":
|
||||
material_types = ["IfcMaterial", "IfcMaterialLayerSet", "IfcMaterialList"]
|
||||
return [(m, m, "") for m in material_types]
|
||||
return [(m, m, ifcopenshell.util.doc.get_entity_doc(version, m).get("description", "")) for m in material_types]
|
||||
|
||||
@classmethod
|
||||
def profiles(cls):
|
||||
return [(str(p.id()), p.ProfileName or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcProfileDef")]
|
||||
|
||||
|
||||
class ObjectMaterialData:
|
||||
@@ -76,12 +82,142 @@ class ObjectMaterialData:
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"materials": cls.materials(),
|
||||
"type_material": cls.type_material(),
|
||||
}
|
||||
cls.data["material_class"] = cls.material_class()
|
||||
cls.data["material_id"] = cls.material_id()
|
||||
cls.data["material_name"] = cls.material_name()
|
||||
cls.data["set"] = cls.set()
|
||||
cls.data["set_usage"] = cls.set_usage()
|
||||
cls.data["set_items"] = cls.set_items()
|
||||
cls.data["set_item_name"] = cls.set_item_name()
|
||||
cls.data["total_thickness"] = cls.total_thickness()
|
||||
cls.data["materials"] = cls.materials()
|
||||
cls.data["type_material"] = cls.type_material()
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def material_class(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
cls.material = ifcopenshell.util.element.get_material(element)
|
||||
if cls.material:
|
||||
return cls.material.is_a()
|
||||
|
||||
@classmethod
|
||||
def material_id(cls):
|
||||
if cls.material:
|
||||
return cls.material.id()
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def set(cls):
|
||||
mset = None
|
||||
if cls.material:
|
||||
mset = cls.material
|
||||
if cls.material.is_a("IfcMaterialLayerSetUsage"):
|
||||
mset = cls.material.ForLayerSet
|
||||
elif cls.material.is_a("IfcMaterialProfileSetUsage"):
|
||||
mset = cls.material.ForProfileSet
|
||||
return {
|
||||
"id": mset.id(),
|
||||
"name": getattr(mset, "LayerSetName", getattr(mset, "Name", None)) or "Unnamed",
|
||||
"description": getattr(mset, "Description", None),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def set_usage(cls):
|
||||
# TODO: complain to buildingSMART
|
||||
cardinal_point_map = {
|
||||
1: "bottom left",
|
||||
2: "bottom centre",
|
||||
3: "bottom right",
|
||||
4: "mid-depth left",
|
||||
5: "mid-depth centre",
|
||||
6: "mid-depth right",
|
||||
7: "top left",
|
||||
8: "top centre",
|
||||
9: "top right",
|
||||
10: "geometric centroid",
|
||||
11: "bottom in line with the geometric centroid",
|
||||
12: "left in line with the geometric centroid",
|
||||
13: "right in line with the geometric centroid",
|
||||
14: "top in line with the geometric centroid",
|
||||
15: "shear centre",
|
||||
16: "bottom in line with the shear centre",
|
||||
17: "left in line with the shear centre",
|
||||
18: "right in line with the shear centre",
|
||||
19: "top in line with the shear centre",
|
||||
}
|
||||
results = {}
|
||||
if cls.material:
|
||||
if cls.material.is_a("IfcMaterialProfileSetUsage"):
|
||||
if cls.material.CardinalPoint:
|
||||
results["cardinal_point"] = cardinal_point_map[cls.material.CardinalPoint]
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def set_items(cls):
|
||||
results = []
|
||||
if cls.material:
|
||||
items = []
|
||||
if cls.material.is_a("IfcMaterialLayerSetUsage"):
|
||||
items = cls.material.ForLayerSet.MaterialLayers
|
||||
elif cls.material.is_a("IfcMaterialProfileSetUsage"):
|
||||
items = cls.material.ForProfileSet.MaterialProfiles
|
||||
elif cls.material.is_a("IfcMaterialLayerSet"):
|
||||
items = cls.material.MaterialLayers
|
||||
elif cls.material.is_a("IfcMaterialProfileSet"):
|
||||
items = cls.material.MaterialProfiles
|
||||
elif cls.material.is_a("IfcMaterialConstituentSet"):
|
||||
items = cls.material.MaterialConstituents
|
||||
|
||||
icon = "LAYER_ACTIVE"
|
||||
if "Layer" in cls.material.is_a():
|
||||
icon = "ALIGN_CENTER"
|
||||
elif "Profile" in cls.material.is_a():
|
||||
icon = "ITALIC"
|
||||
elif "Constituent" in cls.material.is_a():
|
||||
icon = "POINTCLOUD_DATA"
|
||||
|
||||
for item in items or []:
|
||||
data = {"id": item.id(), "name": item.Name or "Unnamed", "icon": icon}
|
||||
if item.is_a("IfcMaterialProfile") and not item.Name:
|
||||
data["name"] = item.Profile.ProfileName or "Unnamed"
|
||||
if item.is_a("IfcMaterialLayer"):
|
||||
data["name"] += f" ({item.LayerThickness})"
|
||||
if not item.is_a("IfcMaterialList"):
|
||||
data["material"] = item.Material.Name or "Unnamed"
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def total_thickness(cls):
|
||||
if cls.material:
|
||||
layers = []
|
||||
if cls.material.is_a("IfcMaterialLayerSetUsage"):
|
||||
layers = cls.material.ForLayerSet.MaterialLayers
|
||||
elif cls.material.is_a("IfcMaterialLayerSet"):
|
||||
layers = cls.material.MaterialLayers
|
||||
return sum([l.LayerThickness for l in layers or []])
|
||||
|
||||
@classmethod
|
||||
def set_item_name(cls):
|
||||
results = []
|
||||
if cls.material:
|
||||
if "Constituent" in cls.material.is_a():
|
||||
return "constituent"
|
||||
elif "Layer" in cls.material.is_a():
|
||||
return "layer"
|
||||
elif "Profile" in cls.material.is_a():
|
||||
return "profile"
|
||||
elif "List" in cls.material.is_a():
|
||||
return "list_item"
|
||||
|
||||
@classmethod
|
||||
def material_name(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
if material:
|
||||
return getattr(material, "Name", None) or "Unnamed"
|
||||
|
||||
@classmethod
|
||||
def materials(cls):
|
||||
return sorted(
|
||||
|
||||
@@ -25,10 +25,10 @@ import ifcopenshell.util.representation
|
||||
import blenderbim.bim.helper
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.material as core
|
||||
import blenderbim.bim.module.model.profile as model_profile
|
||||
from blenderbim.bim.module.material.prop import purge as material_prop_purge
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.material.data import Data
|
||||
from ifcopenshell.api.profile.data import Data as ProfileData
|
||||
|
||||
|
||||
class LoadMaterials(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -59,7 +59,7 @@ class SelectByMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.select_by_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
|
||||
|
||||
|
||||
class AssignParameterizedProfile(bpy.types.Operator):
|
||||
class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_parameterized_profile"
|
||||
bl_label = "Assign Parameterized Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -67,9 +67,6 @@ class AssignParameterizedProfile(bpy.types.Operator):
|
||||
material_profile: bpy.props.IntProperty()
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -84,9 +81,7 @@ class AssignParameterizedProfile(bpy.types.Operator):
|
||||
**{"material_profile": self.file.by_id(self.material_profile), "profile": profile},
|
||||
)
|
||||
Data.load_profiles()
|
||||
ProfileData.load(self.file)
|
||||
bpy.ops.bim.enable_editing_material_set_item(obj=obj.name, material_set_item=self.material_profile)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -145,16 +140,13 @@ class UnlinkMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.unlink_material(tool.Ifc, obj=context.active_object.active_material)
|
||||
|
||||
|
||||
class AssignMaterial(bpy.types.Operator):
|
||||
class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.assign_material"
|
||||
bl_label = "Assign Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
material_type: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
material_type = self.material_type or obj.BIMObjectMaterialProperties.material_type
|
||||
@@ -171,18 +163,14 @@ class AssignMaterial(bpy.types.Operator):
|
||||
)
|
||||
Data.load(IfcStore.get_file())
|
||||
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class UnassignMaterial(bpy.types.Operator):
|
||||
class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.unassign_material"
|
||||
bl_label = "Unassign Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -192,19 +180,15 @@ class UnassignMaterial(bpy.types.Operator):
|
||||
**{"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)},
|
||||
)
|
||||
Data.purge()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddConstituent(bpy.types.Operator):
|
||||
class AddConstituent(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_constituent"
|
||||
bl_label = "Add Constituent"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
constituent_set: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -217,19 +201,15 @@ class AddConstituent(bpy.types.Operator):
|
||||
},
|
||||
)
|
||||
Data.load_constituents()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveConstituent(bpy.types.Operator):
|
||||
class RemoveConstituent(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_constituent"
|
||||
bl_label = "Remove Constituent"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
constituent: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -237,64 +217,49 @@ class RemoveConstituent(bpy.types.Operator):
|
||||
"material.remove_constituent", self.file, **{"constituent": self.file.by_id(self.constituent)}
|
||||
)
|
||||
Data.load_constituents()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddProfile(bpy.types.Operator):
|
||||
class AddProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_profile"
|
||||
bl_label = "Add Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
profile_set: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run(
|
||||
"material.add_profile",
|
||||
self.file,
|
||||
**{
|
||||
"profile_set": self.file.by_id(self.profile_set),
|
||||
"material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
|
||||
},
|
||||
profile_set=self.file.by_id(self.profile_set),
|
||||
material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
|
||||
profile=self.file.by_id(int(context.scene.BIMMaterialProperties.profiles)),
|
||||
)
|
||||
Data.load_profiles()
|
||||
ProfileData.load(self.file)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveProfile(bpy.types.Operator):
|
||||
class RemoveProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_profile"
|
||||
bl_label = "Remove Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
profile: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run("material.remove_profile", self.file, **{"profile": self.file.by_id(self.profile)})
|
||||
Data.load_profiles()
|
||||
ProfileData.load(self.file)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddLayer(bpy.types.Operator):
|
||||
class AddLayer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_layer"
|
||||
bl_label = "Add Layer"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
layer_set: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -307,10 +272,9 @@ class AddLayer(bpy.types.Operator):
|
||||
},
|
||||
)
|
||||
Data.load_layers()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ReorderMaterialSetItem(bpy.types.Operator):
|
||||
class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.reorder_material_set_item"
|
||||
bl_label = "Reorder Material Set Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -319,9 +283,6 @@ class ReorderMaterialSetItem(bpy.types.Operator):
|
||||
new_index: bpy.props.IntProperty()
|
||||
material_set: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -341,40 +302,31 @@ class ReorderMaterialSetItem(bpy.types.Operator):
|
||||
Data.load_layers()
|
||||
elif material_set.is_a("IfcMaterialProfileSet"):
|
||||
Data.load_profiles()
|
||||
ProfileData.load(self.file)
|
||||
elif material_set.is_a("IfcMaterialList"):
|
||||
Data.load_lists()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveLayer(bpy.types.Operator):
|
||||
class RemoveLayer(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_layer"
|
||||
bl_label = "Remove Layer"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
layer: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
ifcopenshell.api.run("material.remove_layer", self.file, **{"layer": self.file.by_id(self.layer)})
|
||||
Data.load_layers()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddListItem(bpy.types.Operator):
|
||||
class AddListItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_list_item"
|
||||
bl_label = "Add List Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
list_item_set: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -387,10 +339,9 @@ class AddListItem(bpy.types.Operator):
|
||||
},
|
||||
)
|
||||
Data.load_lists()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveListItem(bpy.types.Operator):
|
||||
class RemoveListItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_list_item"
|
||||
bl_label = "Remove List Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -399,9 +350,6 @@ class RemoveListItem(bpy.types.Operator):
|
||||
list_item: bpy.props.IntProperty()
|
||||
list_item_index: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
@@ -414,7 +362,6 @@ class RemoveListItem(bpy.types.Operator):
|
||||
},
|
||||
)
|
||||
Data.load_lists()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingAssignedMaterial(bpy.types.Operator):
|
||||
@@ -503,13 +450,14 @@ class DisableEditingAssignedMaterial(bpy.types.Operator):
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.bim.disable_editing_material_set_item()
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
props = obj.BIMObjectMaterialProperties
|
||||
props.is_editing = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditAssignedMaterial(bpy.types.Operator):
|
||||
class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_assigned_material"
|
||||
bl_label = "Edit Assigned Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -517,15 +465,15 @@ class EditAssignedMaterial(bpy.types.Operator):
|
||||
material_set: bpy.props.IntProperty()
|
||||
material_set_usage: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
props = obj.BIMObjectMaterialProperties
|
||||
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
|
||||
|
||||
if props.active_material_set_item_id != 0: # We were editing a material layer set item
|
||||
bpy.ops.bim.edit_material_set_item(material_set_item=props.active_material_set_item_id)
|
||||
|
||||
if product_data["type"] == "IfcMaterial":
|
||||
bpy.ops.bim.unassign_material(obj=obj.name)
|
||||
bpy.ops.bim.assign_material(obj=obj.name, material_type="IfcMaterial")
|
||||
@@ -569,9 +517,54 @@ class EditAssignedMaterial(bpy.types.Operator):
|
||||
elif material_set.is_a("IfcMaterialProfileSet"):
|
||||
Data.load_profiles()
|
||||
bpy.ops.bim.disable_editing_assigned_material(obj=obj.name)
|
||||
|
||||
|
||||
class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_material_set_item_profile"
|
||||
bl_label = "Enable Editing Material Set Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
material_set_item: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.props = obj.BIMObjectMaterialProperties
|
||||
self.props.material_set_item_profile_attributes.clear()
|
||||
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
|
||||
blenderbim.bim.helper.import_attributes2(profile, self.props.material_set_item_profile_attributes)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingMaterialSetItemProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_material_set_item_profile"
|
||||
bl_label = "Disable Editing Material Set Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.props = obj.BIMObjectMaterialProperties
|
||||
self.props.material_set_item_profile_attributes.clear()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditMaterialSetItemProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_material_set_item_profile"
|
||||
bl_label = "Edit Material Set Item Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
material_set_item: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.props = obj.BIMObjectMaterialProperties
|
||||
attributes = blenderbim.bim.helper.export_attributes(self.props.material_set_item_profile_attributes)
|
||||
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
|
||||
ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes)
|
||||
self.props.material_set_item_profile_attributes.clear()
|
||||
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile)
|
||||
|
||||
|
||||
class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_material_set_item"
|
||||
bl_label = "Enable Editing Material Set Item"
|
||||
@@ -582,6 +575,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.mprops = context.scene.BIMMaterialProperties
|
||||
self.props = obj.BIMObjectMaterialProperties
|
||||
self.props.active_material_set_item_id = self.material_set_item
|
||||
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
|
||||
@@ -600,7 +594,8 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
|
||||
self.load_set_item_attributes(material_set_item, material_set_item_data)
|
||||
if material_set_item.is_a("IfcMaterialProfile"):
|
||||
self.load_profile_attributes(material_set_item, material_set_item_data)
|
||||
if material_set_item.Profile:
|
||||
self.mprops.profiles = str(material_set_item.Profile.id())
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -613,55 +608,20 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
|
||||
continue
|
||||
if attribute.name() in material_set_item_data:
|
||||
new = self.props.material_set_item_attributes.add()
|
||||
new.ifc_class = material_set_item.is_a()
|
||||
new.name = attribute.name()
|
||||
new.is_null = material_set_item_data[attribute.name()] is None
|
||||
new.data_type = data_type
|
||||
if data_type == "string":
|
||||
new.string_value = "" if new.is_null else material_set_item_data[attribute.name()]
|
||||
new.string_value = "" if new.is_null else str(material_set_item_data[attribute.name()])
|
||||
elif data_type == "float":
|
||||
new.float_value = 0.0 if new.is_null else material_set_item_data[attribute.name()]
|
||||
new.float_value = 0.0 if new.is_null else float(material_set_item_data[attribute.name()])
|
||||
elif data_type == "integer":
|
||||
new.int_value = 0 if new.is_null else material_set_item_data[attribute.name()]
|
||||
new.int_value = 0 if new.is_null else int(material_set_item_data[attribute.name()])
|
||||
elif data_type == "boolean":
|
||||
new.bool_value = False if new.is_null else material_set_item_data[attribute.name()]
|
||||
|
||||
def load_profile_attributes(self, material_set_item, material_set_item_data):
|
||||
self.props.material_set_item_profile_attributes.clear()
|
||||
|
||||
if not material_set_item_data["Profile"]:
|
||||
return
|
||||
|
||||
profile = self.file.by_id(material_set_item_data["Profile"])
|
||||
profile_data = ProfileData.profiles[material_set_item_data["Profile"]]
|
||||
|
||||
for attribute in IfcStore.get_schema().declaration_by_name(profile.is_a()).all_attributes():
|
||||
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
|
||||
if data_type == "entity":
|
||||
continue
|
||||
if attribute.name() in profile_data:
|
||||
new = self.props.material_set_item_profile_attributes.add()
|
||||
new.name = attribute.name()
|
||||
new.is_null = profile_data[attribute.name()] is None
|
||||
new.is_optional = attribute.optional()
|
||||
new.data_type = data_type
|
||||
if data_type == "string":
|
||||
new.string_value = "" if new.is_null else profile_data[attribute.name()]
|
||||
elif data_type == "float":
|
||||
new.float_value = 0.0 if new.is_null else profile_data[attribute.name()]
|
||||
elif data_type == "integer":
|
||||
new.int_value = 0 if new.is_null else profile_data[attribute.name()]
|
||||
elif data_type == "boolean":
|
||||
new.bool_value = False if new.is_null else profile_data[attribute.name()]
|
||||
elif data_type == "enum":
|
||||
new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
|
||||
if profile_data[attribute.name()]:
|
||||
new.enum_value = profile_data[attribute.name()]
|
||||
|
||||
# Force null to be false if the attribute is mandatory because when we first assign a profile, all of
|
||||
# its fields are null (which is illegal).
|
||||
# TODO: find a better solution.
|
||||
if not new.is_optional:
|
||||
new.is_null = False
|
||||
new.bool_value = False if new.is_null else bool(material_set_item_data[attribute.name()])
|
||||
blenderbim.bim.helper.add_attribute_description(new, material_set_item_data)
|
||||
blenderbim.bim.helper.add_attribute_min_max(new)
|
||||
|
||||
|
||||
class DisableEditingMaterialSetItem(bpy.types.Operator):
|
||||
@@ -677,20 +637,18 @@ class DisableEditingMaterialSetItem(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditMaterialSetItem(bpy.types.Operator):
|
||||
class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_material_set_item"
|
||||
bl_label = "Edit Material Set Item"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
material_set_item: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
props = obj.BIMObjectMaterialProperties
|
||||
mprops = context.scene.BIMMaterialProperties
|
||||
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
|
||||
|
||||
attributes = blenderbim.bim.helper.export_attributes(props.material_set_item_attributes)
|
||||
@@ -718,34 +676,30 @@ class EditMaterialSetItem(bpy.types.Operator):
|
||||
)
|
||||
Data.load_layers()
|
||||
elif product_data["type"] == "IfcMaterialProfileSet" or product_data["type"] == "IfcMaterialProfileSetUsage":
|
||||
profile_attributes = blenderbim.bim.helper.export_attributes(props.material_set_item_profile_attributes)
|
||||
profile_def = None
|
||||
if mprops.profiles:
|
||||
profile_def = tool.Ifc.get().by_id(int(mprops.profiles))
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"material.edit_profile",
|
||||
self.file,
|
||||
**{
|
||||
"profile": self.file.by_id(self.material_set_item),
|
||||
"attributes": attributes,
|
||||
"profile_attributes": profile_attributes,
|
||||
"material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)),
|
||||
},
|
||||
profile=self.file.by_id(self.material_set_item),
|
||||
attributes=attributes,
|
||||
profile_def=profile_def,
|
||||
material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)),
|
||||
)
|
||||
Data.load_profiles()
|
||||
ProfileData.load(self.file)
|
||||
else:
|
||||
pass
|
||||
|
||||
bpy.ops.bim.disable_editing_material_set_item(obj=obj.name)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CopyMaterial(bpy.types.Operator):
|
||||
class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.copy_material"
|
||||
bl_label = "Copy Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = IfcStore.get_file()
|
||||
material = ifcopenshell.util.element.get_material(
|
||||
@@ -766,7 +720,6 @@ class CopyMaterial(bpy.types.Operator):
|
||||
)
|
||||
Data.load(self.file, obj.BIMObjectProperties.ifc_definition_id)
|
||||
self.set_default_material(obj, material)
|
||||
return {"FINISHED"}
|
||||
|
||||
def set_default_material(self, obj, material):
|
||||
object_material_ids = [
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from ifcopenshell.util.doc import get_entity_doc
|
||||
from ifcopenshell.api.material.data import Data
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.module.material.data import MaterialsData, ObjectMaterialData
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
@@ -47,28 +49,31 @@ def purge():
|
||||
parameterizedprofileclasses_enum = []
|
||||
|
||||
|
||||
def getProfileClasses(self, context):
|
||||
def get_profile_classes(self, context):
|
||||
global profileclasses_enum
|
||||
if len(profileclasses_enum) == 0 and IfcStore.get_schema():
|
||||
version = tool.Ifc.get_schema()
|
||||
profileclasses_enum.clear()
|
||||
profileclasses_enum = [
|
||||
(t.name(), t.name(), "") for t in IfcStore.get_schema().declaration_by_name("IfcProfileDef").subtypes()
|
||||
(t.name(), t.name(), get_entity_doc(version, t.name()).get("description", ""))
|
||||
for t in IfcStore.get_schema().declaration_by_name("IfcProfileDef").subtypes()
|
||||
]
|
||||
return profileclasses_enum
|
||||
|
||||
|
||||
def getParameterizedProfileClasses(self, context):
|
||||
def get_parameterized_profile_classes(self, context):
|
||||
global parameterizedprofileclasses_enum
|
||||
if len(parameterizedprofileclasses_enum) == 0 and IfcStore.get_schema():
|
||||
version = tool.Ifc.get_schema()
|
||||
parameterizedprofileclasses_enum.clear()
|
||||
parameterizedprofileclasses_enum = [
|
||||
(t.name(), t.name(), "")
|
||||
(t.name(), t.name(), get_entity_doc(version, t.name()).get("description", ""))
|
||||
for t in IfcStore.get_schema().declaration_by_name("IfcParameterizedProfileDef").subtypes()
|
||||
]
|
||||
for ifc_class in parameterizedprofileclasses_enum:
|
||||
parameterizedprofileclasses_enum.extend(
|
||||
[
|
||||
(t.name(), t.name(), "")
|
||||
(t.name(), t.name(), get_entity_doc(version, t.name()).get("description", ""))
|
||||
for t in IfcStore.get_schema().declaration_by_name(ifc_class[0]).subtypes() or []
|
||||
]
|
||||
)
|
||||
@@ -81,7 +86,7 @@ def get_materials(self, context):
|
||||
return ObjectMaterialData.data["materials"]
|
||||
|
||||
|
||||
def getMaterialTypes(self, context):
|
||||
def get_object_material_types(self, context):
|
||||
global materialtypes_enum
|
||||
if len(materialtypes_enum) == 0 and IfcStore.get_file():
|
||||
material_types = [
|
||||
@@ -93,10 +98,11 @@ def getMaterialTypes(self, context):
|
||||
"IfcMaterialProfileSetUsage",
|
||||
"IfcMaterialList",
|
||||
]
|
||||
if IfcStore.get_file().schema == "IFC2X3":
|
||||
version = tool.Ifc.get_schema()
|
||||
if version == "IFC2X3":
|
||||
material_types = ["IfcMaterial", "IfcMaterialLayerSet", "IfcMaterialLayerSetUsage", "IfcMaterialList"]
|
||||
materialtypes_enum.clear()
|
||||
materialtypes_enum = [(m, m, "") for m in material_types]
|
||||
materialtypes_enum = [(m, m, get_entity_doc(version, m).get("description", "")) for m in material_types]
|
||||
return materialtypes_enum
|
||||
|
||||
|
||||
@@ -106,6 +112,12 @@ def get_material_types(self, context):
|
||||
return MaterialsData.data["material_types"]
|
||||
|
||||
|
||||
def get_profiles(self, context):
|
||||
if not MaterialsData.is_loaded:
|
||||
MaterialsData.load()
|
||||
return MaterialsData.data["profiles"]
|
||||
|
||||
|
||||
class Material(PropertyGroup):
|
||||
name: StringProperty(name="Name")
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
@@ -119,10 +131,11 @@ class BIMMaterialProperties(PropertyGroup):
|
||||
material_type: EnumProperty(items=get_material_types, name="Material Type")
|
||||
materials: CollectionProperty(name="Materials", type=Material)
|
||||
active_material_index: IntProperty(name="Active Material Index")
|
||||
profiles: EnumProperty(items=get_profiles, name="Profiles")
|
||||
|
||||
|
||||
class BIMObjectMaterialProperties(PropertyGroup):
|
||||
material_type: EnumProperty(items=getMaterialTypes, name="Material Type")
|
||||
material_type: EnumProperty(items=get_object_material_types, name="Material Type")
|
||||
material: EnumProperty(items=get_materials, name="Material")
|
||||
is_editing: BoolProperty(name="Is Editing", default=False)
|
||||
material_set_usage_attributes: CollectionProperty(name="Material Set Usage Attributes", type=Attribute)
|
||||
@@ -133,7 +146,7 @@ class BIMObjectMaterialProperties(PropertyGroup):
|
||||
name="Material Set Item Profile Attributes", type=Attribute
|
||||
)
|
||||
material_set_item_material: EnumProperty(items=get_materials, name="Material")
|
||||
profile_classes: EnumProperty(items=getProfileClasses, name="Profile Classes")
|
||||
profile_classes: EnumProperty(items=get_profile_classes, name="Profile Classes")
|
||||
parameterized_profile_classes: EnumProperty(
|
||||
items=getParameterizedProfileClasses, name="Parameterized Profile Classes"
|
||||
items=get_parameterized_profile_classes, name="Parameterized Profile Classes"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import blenderbim.bim.helper
|
||||
from bpy.types import Panel, UIList
|
||||
from ifcopenshell.api.material.data import Data
|
||||
from ifcopenshell.api.profile.data import Data as ProfileData
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.helper import draw_attributes
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
@@ -128,13 +127,11 @@ class BIM_PT_object_material(Panel):
|
||||
self.file = IfcStore.get_file()
|
||||
self.oprops = context.active_object.BIMObjectProperties
|
||||
self.props = context.active_object.BIMObjectMaterialProperties
|
||||
self.mprops = context.scene.BIMMaterialProperties
|
||||
if not Data.is_loaded:
|
||||
Data.load(IfcStore.get_file())
|
||||
if self.oprops.ifc_definition_id not in Data.products:
|
||||
Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id)
|
||||
if not ProfileData.is_loaded:
|
||||
ProfileData.load(self.file)
|
||||
self.product_data = Data.products[self.oprops.ifc_definition_id]
|
||||
|
||||
if not ObjectMaterialData.data["materials"]:
|
||||
row = self.layout.row(align=True)
|
||||
@@ -146,46 +143,7 @@ class BIM_PT_object_material(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT")
|
||||
|
||||
if self.product_data:
|
||||
if self.product_data["type"] == "IfcMaterialConstituentSet":
|
||||
self.material_set_id = self.product_data["id"]
|
||||
self.material_set_data = Data.constituent_sets[self.material_set_id]
|
||||
self.set_items = self.material_set_data["MaterialConstituents"] or []
|
||||
self.set_data = Data.constituents
|
||||
self.set_item_name = "constituent"
|
||||
elif self.product_data["type"] == "IfcMaterialLayerSet":
|
||||
self.material_set_id = self.product_data["id"]
|
||||
self.material_set_data = Data.layer_sets[self.material_set_id]
|
||||
self.set_items = self.material_set_data["MaterialLayers"] or []
|
||||
self.set_data = Data.layers
|
||||
self.set_item_name = "layer"
|
||||
elif self.product_data["type"] == "IfcMaterialLayerSetUsage":
|
||||
self.material_set_usage = Data.layer_set_usages[self.product_data["id"]]
|
||||
self.material_set_id = self.material_set_usage["ForLayerSet"]
|
||||
self.material_set_data = Data.layer_sets[self.material_set_id]
|
||||
self.set_items = self.material_set_data["MaterialLayers"] or []
|
||||
self.set_data = Data.layers
|
||||
self.set_item_name = "layer"
|
||||
elif self.product_data["type"] == "IfcMaterialProfileSet":
|
||||
self.material_set_id = self.product_data["id"]
|
||||
self.material_set_data = Data.profile_sets[self.material_set_id]
|
||||
self.set_items = self.material_set_data["MaterialProfiles"] or []
|
||||
self.set_data = Data.profiles
|
||||
self.set_item_name = "profile"
|
||||
elif self.product_data["type"] == "IfcMaterialProfileSetUsage":
|
||||
self.material_set_usage = Data.profile_set_usages[self.product_data["id"]]
|
||||
self.material_set_id = self.material_set_usage["ForProfileSet"]
|
||||
self.material_set_data = Data.profile_sets[self.material_set_id]
|
||||
self.set_items = self.material_set_data["MaterialProfiles"] or []
|
||||
self.set_data = Data.profiles
|
||||
self.set_item_name = "profile"
|
||||
elif self.product_data["type"] == "IfcMaterialList":
|
||||
self.material_set_id = self.product_data["id"]
|
||||
self.material_set_data = Data.lists[self.material_set_id]
|
||||
self.set_items = self.material_set_data["Materials"] or []
|
||||
self.set_item_name = "list_item"
|
||||
else:
|
||||
self.material_set_id = 0
|
||||
if ObjectMaterialData.data["material_class"]:
|
||||
return self.draw_material_ui()
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
@@ -196,21 +154,21 @@ class BIM_PT_object_material(Panel):
|
||||
|
||||
def draw_material_ui(self):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=self.product_data["type"])
|
||||
row.label(text=ObjectMaterialData.data["material_class"])
|
||||
|
||||
if self.props.is_editing:
|
||||
op = row.operator("bim.edit_assigned_material", icon="CHECKMARK", text="")
|
||||
op.material_set = self.material_set_id
|
||||
if "Usage" in self.product_data["type"]:
|
||||
op.material_set_usage = self.product_data["id"]
|
||||
op.material_set = ObjectMaterialData.data["set"]["id"]
|
||||
if "Usage" in ObjectMaterialData.data["material_class"]:
|
||||
op.material_set_usage = ObjectMaterialData.data["material_id"]
|
||||
row.operator("bim.disable_editing_assigned_material", icon="CANCEL", text="")
|
||||
else:
|
||||
if self.product_data["type"] == "IfcMaterial":
|
||||
if ObjectMaterialData.data["material_class"] == "IfcMaterial":
|
||||
row.operator("bim.copy_material", icon="COPYDOWN", text="")
|
||||
row.operator("bim.enable_editing_assigned_material", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.unassign_material", icon="X", text="")
|
||||
|
||||
if self.product_data["type"] == "IfcMaterial":
|
||||
if ObjectMaterialData.data["material_class"] == "IfcMaterial":
|
||||
self.draw_single_ui()
|
||||
else:
|
||||
self.draw_set_ui()
|
||||
@@ -224,10 +182,9 @@ class BIM_PT_object_material(Panel):
|
||||
prop_with_search(self.layout, self.props, "material", text="")
|
||||
|
||||
def draw_read_only_single_ui(self):
|
||||
material = Data.materials[self.product_data["id"]]
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Name")
|
||||
row.label(text=material["Name"])
|
||||
row.label(text=ObjectMaterialData.data["material_name"])
|
||||
|
||||
def draw_set_ui(self):
|
||||
if self.props.is_editing:
|
||||
@@ -235,166 +192,117 @@ class BIM_PT_object_material(Panel):
|
||||
self.draw_read_only_set_ui()
|
||||
|
||||
def draw_editable_set_ui(self):
|
||||
blenderbim.bim.helper.draw_attributes(self.props.material_set_attributes, self.layout)
|
||||
blenderbim.bim.helper.draw_attributes(self.props.material_set_usage_attributes, self.layout)
|
||||
|
||||
for attribute in self.props.material_set_attributes:
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile" and not self.mprops.profiles:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(attribute, "string_value", text=attribute.name)
|
||||
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
|
||||
row = self.layout.row(align=True)
|
||||
prop_with_search(row, self.props, "material", text="")
|
||||
row.label(text="No Profiles Available")
|
||||
row.operator("bim.add_profile_def", icon="ADD", text="")
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile":
|
||||
row.prop(self.mprops, "profiles", icon="ITALIC", text="")
|
||||
prop_with_search(row, self.props, "material", icon="MATERIAL", text="")
|
||||
op = row.operator(f"bim.add_{ObjectMaterialData.data['set_item_name']}", icon="ADD", text="")
|
||||
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_set", ObjectMaterialData.data["set"]["id"])
|
||||
|
||||
op = row.operator(f"bim.add_{self.set_item_name}", icon="ADD", text="")
|
||||
setattr(op, f"{self.set_item_name}_set", self.material_set_id)
|
||||
|
||||
total_items = len(self.set_items)
|
||||
for index, set_item_id in enumerate(self.set_items):
|
||||
if self.props.active_material_set_item_id == set_item_id:
|
||||
self.draw_editable_set_item_ui(set_item_id)
|
||||
total_items = len(ObjectMaterialData.data["set_items"])
|
||||
for index, set_item in enumerate(ObjectMaterialData.data["set_items"]):
|
||||
if len(self.props.material_set_item_profile_attributes):
|
||||
self.draw_editable_set_item_profile_ui(set_item)
|
||||
elif self.props.active_material_set_item_id == set_item["id"]:
|
||||
self.draw_editable_set_item_ui(set_item)
|
||||
else:
|
||||
self.draw_read_only_set_item_ui(
|
||||
set_item_id, index, is_first=index == 0, is_last=index == total_items - 1
|
||||
set_item, index, is_first=index == 0, is_last=index == total_items - 1
|
||||
)
|
||||
|
||||
def draw_editable_set_item_ui(self, set_item_id):
|
||||
item = self.set_data[set_item_id]
|
||||
material = Data.materials[item["Material"]]
|
||||
|
||||
def draw_editable_set_item_profile_ui(self, set_item):
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
row.prop(self.props, "material_set_item_material", icon="MATERIAL")
|
||||
op = row.operator("bim.edit_material_set_item", icon="CHECKMARK", text="")
|
||||
op.material_set_item = set_item_id
|
||||
op = row.operator("bim.edit_material_set_item_profile", icon="CHECKMARK", text="Save Changes")
|
||||
op.material_set_item = set_item["id"]
|
||||
row.operator("bim.disable_editing_material_set_item_profile", icon="CANCEL", text="")
|
||||
draw_attributes(self.props.material_set_item_profile_attributes, box)
|
||||
|
||||
def draw_editable_set_item_ui(self, set_item):
|
||||
box = self.layout.box()
|
||||
row = box.row(align=True)
|
||||
op = row.operator("bim.edit_material_set_item", icon="CHECKMARK", text="Save Changes")
|
||||
op.material_set_item = set_item["id"]
|
||||
row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="")
|
||||
|
||||
draw_attributes(self.props.material_set_item_attributes, self.layout)
|
||||
draw_attributes(self.props.material_set_item_attributes, box)
|
||||
|
||||
if self.set_item_name == "profile":
|
||||
self.draw_assign_profile_ui(box, item)
|
||||
self.draw_editable_profile_ui(box, item)
|
||||
row = box.row()
|
||||
row.prop(self.props, "material_set_item_material", icon="MATERIAL", text="Material")
|
||||
|
||||
def draw_assign_profile_ui(self, layout, item):
|
||||
row = layout.row(align=True)
|
||||
row.prop(self.props, "profile_classes", text="")
|
||||
if self.props.profile_classes == "IfcParameterizedProfileDef":
|
||||
row.prop(self.props, "parameterized_profile_classes", text="")
|
||||
op = row.operator(
|
||||
"bim.assign_parameterized_profile", icon="GREASEPENCIL" if item["Profile"] else "ADD", text=""
|
||||
)
|
||||
op.ifc_class = self.props.parameterized_profile_classes
|
||||
op.material_profile = item["id"]
|
||||
else:
|
||||
# TODO: support non parametric profiles by showing a list of named profiles to select from, or an
|
||||
# eyedropper to pick profile geometry from the scene
|
||||
row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="")
|
||||
if ObjectMaterialData.data["set_item_name"] == "profile":
|
||||
row = box.row()
|
||||
row.prop(self.mprops, "profiles", icon="ITALIC", text="Profile")
|
||||
|
||||
def draw_editable_profile_ui(self, layout, item):
|
||||
draw_attributes(self.props.material_set_item_profile_attributes, self.layout)
|
||||
|
||||
def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False):
|
||||
if self.product_data["type"] == "IfcMaterialList":
|
||||
item = Data.materials[set_item_id]
|
||||
def draw_read_only_set_item_ui(self, set_item, index, is_first=False, is_last=False):
|
||||
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="IfcMaterial", icon="ALIGN_CENTER")
|
||||
row.label(text=item["Name"], icon="MATERIAL")
|
||||
row.label(text="IfcMaterial", icon="LAYER_ACTIVE")
|
||||
row.label(text=set_item["name"], icon="MATERIAL")
|
||||
else:
|
||||
item = self.set_data[set_item_id]
|
||||
row = self.layout.row(align=True)
|
||||
item_name = item.get("Name", "Unnamed") or "Unnamed"
|
||||
thickness = item.get("LayerThickness")
|
||||
if thickness:
|
||||
item_name += f" ({thickness})"
|
||||
row.label(text=item_name, icon="ALIGN_CENTER")
|
||||
row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL")
|
||||
row.label(text=set_item["name"], icon=set_item["icon"])
|
||||
row.label(text=set_item["material"], icon="MATERIAL")
|
||||
|
||||
if not is_first:
|
||||
op = row.operator(f"bim.reorder_material_set_item", icon="TRIA_UP", text="")
|
||||
op.old_index = index
|
||||
op.new_index = index - 1
|
||||
setattr(op, "material_set", self.material_set_id)
|
||||
setattr(op, "material_set", ObjectMaterialData.data["set"]["id"])
|
||||
if not is_last:
|
||||
op = row.operator(f"bim.reorder_material_set_item", icon="TRIA_DOWN", text="")
|
||||
op.old_index = index
|
||||
op.new_index = index + 1
|
||||
setattr(op, "material_set", self.material_set_id)
|
||||
if not self.props.active_material_set_item_id and self.product_data["type"] != "IfcMaterialList":
|
||||
setattr(op, "material_set", ObjectMaterialData.data["set"]["id"])
|
||||
if not self.props.active_material_set_item_id and ObjectMaterialData.data["material_class"] != "IfcMaterialList":
|
||||
if "Profile" in ObjectMaterialData.data["material_class"]:
|
||||
op = row.operator("bim.enable_editing_material_set_item_profile", icon="ITALIC", text="")
|
||||
op.material_set_item = set_item["id"]
|
||||
op = row.operator("bim.enable_editing_material_set_item", icon="GREASEPENCIL", text="")
|
||||
op.material_set_item = set_item_id
|
||||
op = row.operator(f"bim.remove_{self.set_item_name}", icon="X", text="")
|
||||
if self.product_data["type"] == "IfcMaterialList":
|
||||
setattr(op, "list_item_set", self.material_set_id)
|
||||
setattr(op, self.set_item_name, item["id"])
|
||||
if hasattr(op, f"{self.set_item_name}_index"):
|
||||
setattr(op, f"{self.set_item_name}_index", index)
|
||||
op.material_set_item = set_item["id"]
|
||||
op = row.operator(f"bim.remove_{ObjectMaterialData.data['set_item_name']}", icon="X", text="")
|
||||
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
|
||||
setattr(op, "list_item_set", ObjectMaterialData.data["set"]["id"])
|
||||
setattr(op, ObjectMaterialData.data["set_item_name"], set_item["id"])
|
||||
if hasattr(op, f"{ObjectMaterialData.data['set_item_name']}_index"):
|
||||
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_index", index)
|
||||
|
||||
def draw_read_only_set_ui(self):
|
||||
if (
|
||||
self.product_data["type"] == "IfcMaterialLayerSetUsage"
|
||||
or self.product_data["type"] == "IfcMaterialLayerSet"
|
||||
):
|
||||
name_attr = "LayerSetName"
|
||||
else:
|
||||
name_attr = "Name"
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=name_attr)
|
||||
row.label(text=self.material_set_data.get(name_attr, "Unnamed") or "Unnamed")
|
||||
if hasattr(self.material_set_data, "Description") and self.material_set_data["Description"]:
|
||||
row.label(text="Name")
|
||||
row.label(text=ObjectMaterialData.data["set"]["name"])
|
||||
if ObjectMaterialData.data["set"]["description"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Description")
|
||||
row.label(text=str(self.material_set_data["Description"]))
|
||||
row.label(text=ObjectMaterialData.data["set"]["description"])
|
||||
|
||||
if self.product_data["type"] == "IfcMaterialProfileSetUsage":
|
||||
# TODO: complain to buildingSMART
|
||||
cardinal_point_map = {
|
||||
1: "bottom left",
|
||||
2: "bottom centre",
|
||||
3: "bottom right",
|
||||
4: "mid-depth left",
|
||||
5: "mid-depth centre",
|
||||
6: "mid-depth right",
|
||||
7: "top left",
|
||||
8: "top centre",
|
||||
9: "top right",
|
||||
10: "geometric centroid",
|
||||
11: "bottom in line with the geometric centroid",
|
||||
12: "left in line with the geometric centroid",
|
||||
13: "right in line with the geometric centroid",
|
||||
14: "top in line with the geometric centroid",
|
||||
15: "shear centre",
|
||||
16: "bottom in line with the shear centre",
|
||||
17: "left in line with the shear centre",
|
||||
18: "right in line with the shear centre",
|
||||
19: "top in line with the shear centre",
|
||||
}
|
||||
if self.material_set_usage["CardinalPoint"]:
|
||||
if ObjectMaterialData.data["material_class"] == "IfcMaterialProfileSetUsage":
|
||||
if ObjectMaterialData.data["set_usage"].get("cardinal_point"):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="CardinalPoint")
|
||||
row.label(text=cardinal_point_map[self.material_set_usage["CardinalPoint"]])
|
||||
if self.material_set_usage["ReferenceExtent"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="ReferenceExtent")
|
||||
row.label(text=str(self.material_set_usage["ReferenceExtent"]))
|
||||
row.label(text=ObjectMaterialData.data["set_usage"]["cardinal_point"])
|
||||
|
||||
total_thickness = 0
|
||||
for item_id in self.set_items:
|
||||
if self.product_data["type"] == "IfcMaterialList":
|
||||
for set_item in ObjectMaterialData.data["set_items"]:
|
||||
if ObjectMaterialData.data["material_class"] == "IfcMaterialList":
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="IfcMaterial", icon="ALIGN_CENTER")
|
||||
row.label(text=Data.materials[item_id]["Name"], icon="MATERIAL")
|
||||
row.label(text="IfcMaterial", icon="LAYER_ACTIVE")
|
||||
row.label(text=set_item["name"], icon="MATERIAL")
|
||||
else:
|
||||
item = self.set_data[item_id]
|
||||
row = self.layout.row(align=True)
|
||||
item_name = item.get("Name", "Unnamed") or "Unnamed"
|
||||
thickness = item.get("LayerThickness")
|
||||
if thickness:
|
||||
item_name += f" ({thickness:.3f})"
|
||||
total_thickness += thickness
|
||||
row.label(text=item_name, icon="ALIGN_CENTER")
|
||||
row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL")
|
||||
row.label(text=set_item["name"], icon=set_item["icon"])
|
||||
row.label(text=set_item["material"], icon="MATERIAL")
|
||||
|
||||
if total_thickness:
|
||||
if ObjectMaterialData.data["total_thickness"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"Total Thickness: {total_thickness:.3f}")
|
||||
row.label(text=f"Total Thickness: {ObjectMaterialData.data['total_thickness']:.3f}")
|
||||
|
||||
|
||||
class BIM_UL_materials(UIList):
|
||||
|
||||
@@ -17,9 +17,15 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import handler, prop, ui, grid, product, wall, slab, stair, opening, pie, workspace, profile
|
||||
from . import handler, prop, ui, grid, array, product, wall, slab, stair, opening, pie, workspace, profile
|
||||
|
||||
classes = (
|
||||
array.AddArray,
|
||||
array.DisableEditingArray,
|
||||
array.EditArray,
|
||||
array.EnableEditingArray,
|
||||
array.RemoveArray,
|
||||
array.SelectArrayParent,
|
||||
product.AddConstrTypeInstance,
|
||||
product.AddEmptyType,
|
||||
product.AlignProduct,
|
||||
@@ -66,13 +72,23 @@ classes = (
|
||||
prop.ConstrClassInfo,
|
||||
prop.ConstrBrowserState,
|
||||
prop.BIMModelProperties,
|
||||
prop.BIMArrayProperties,
|
||||
prop.BIMStairProperties,
|
||||
ui.BIM_PT_authoring,
|
||||
ui.BIM_PT_array,
|
||||
ui.BIM_PT_stair,
|
||||
ui.DisplayConstrTypesUI,
|
||||
ui.LaunchTypeManager,
|
||||
ui.HelpConstrTypes,
|
||||
ui.BIM_MT_model,
|
||||
grid.BIM_OT_add_object,
|
||||
stair.BIM_OT_add_object,
|
||||
stair.BIM_OT_add_clever_stair,
|
||||
stair.AddStair,
|
||||
stair.CancelEditingStair,
|
||||
stair.FinishEditingStair,
|
||||
stair.EnableEditingStair,
|
||||
stair.RemoveStair,
|
||||
pie.OpenPieClass,
|
||||
pie.PieUpdateContainer,
|
||||
pie.PieAddOpening,
|
||||
@@ -87,6 +103,8 @@ def register():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.register_tool(workspace.BimTool, after={"builtin.scale_cage"}, separator=True, group=True)
|
||||
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
|
||||
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
|
||||
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button)
|
||||
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
|
||||
@@ -103,6 +121,8 @@ def unregister():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.unregister_tool(workspace.BimTool)
|
||||
del bpy.types.Scene.BIMModelProperties
|
||||
del bpy.types.Object.BIMArrayProperties
|
||||
del bpy.types.Object.BIMStairProperties
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button)
|
||||
bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button)
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
class AddArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_array"
|
||||
bl_label = "Add Array"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
array = {"children": [], "count": 1, "x": 0.0, "y": 0.0, "z": 0.0}
|
||||
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
pset = psets.get("BBIM_Array", None)
|
||||
|
||||
if pset:
|
||||
data = json.loads(pset["Data"])
|
||||
data.append(array)
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Array")
|
||||
data = [array]
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Parent": element.GlobalId, "Data": json.dumps(data)},
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_editing_array"
|
||||
bl_label = "Disable Editing Array"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.active_object.BIMArrayProperties.is_editing = -1
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EditArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_array"
|
||||
bl_label = "Edit Array"
|
||||
bl_options = {"REGISTER"}
|
||||
item: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMArrayProperties
|
||||
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
pset = psets["BBIM_Array"]
|
||||
data = json.loads(pset["Data"])
|
||||
data[self.item] = {
|
||||
"children": data[self.item]["children"],
|
||||
"count": props.count,
|
||||
"x": props.x,
|
||||
"y": props.y,
|
||||
"z": props.z,
|
||||
}
|
||||
|
||||
props.is_editing = -1
|
||||
|
||||
try:
|
||||
parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"]))
|
||||
except:
|
||||
return {"FINISHED"}
|
||||
|
||||
tool.Model.regenerate_array(parent, data)
|
||||
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
data = json.dumps(data)
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_array"
|
||||
bl_label = "Enable Editing Array"
|
||||
bl_options = {"REGISTER"}
|
||||
item: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
data = json.loads(psets["BBIM_Array"]["Data"])[self.item]
|
||||
props = obj.BIMArrayProperties
|
||||
props.count = data["count"]
|
||||
props.x = data["x"]
|
||||
props.y = data["y"]
|
||||
props.z = data["z"]
|
||||
props.is_editing = self.item
|
||||
|
||||
|
||||
class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_array"
|
||||
bl_label = "Remove Array"
|
||||
bl_options = {"REGISTER"}
|
||||
item: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMArrayProperties
|
||||
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
pset = psets["BBIM_Array"]
|
||||
data = json.loads(pset["Data"])
|
||||
data[self.item]["count"] = 1
|
||||
|
||||
props.is_editing = -1
|
||||
|
||||
try:
|
||||
parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"]))
|
||||
except:
|
||||
return {"FINISHED"}
|
||||
|
||||
tool.Model.regenerate_array(parent, data)
|
||||
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
if len(data) == 1:
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
else:
|
||||
del data[self.item]
|
||||
data = json.dumps(data)
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectArrayParent(bpy.types.Operator):
|
||||
bl_idname = "bim.select_array_parent"
|
||||
bl_label = "Select Array Parent"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
element = tool.Ifc.get().by_guid(self.parent)
|
||||
except:
|
||||
return {"FINISHED"}
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
return {"FINISHED"}
|
||||
@@ -18,8 +18,11 @@
|
||||
|
||||
import bpy
|
||||
import math
|
||||
import json
|
||||
import functools
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||
@@ -27,6 +30,8 @@ from blenderbim.bim.module.model.root import ConstrTypeEntityNotFound
|
||||
|
||||
def refresh():
|
||||
AuthoringData.is_loaded = False
|
||||
ArrayData.is_loaded = False
|
||||
StairData.is_loaded = False
|
||||
|
||||
|
||||
class AuthoringData:
|
||||
@@ -60,15 +65,22 @@ class AuthoringData:
|
||||
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
|
||||
names = [d.name() for d in declarations]
|
||||
names.extend(("IfcDoorStyle", "IfcWindowStyle"))
|
||||
return [(c, c, "") for c in sorted(names)]
|
||||
version = tool.Ifc.get_schema()
|
||||
return [(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted(names)]
|
||||
|
||||
@classmethod
|
||||
def type_predefined_type(cls):
|
||||
results = []
|
||||
declaration = tool.Ifc().schema().declaration_by_name(cls.props.type_class)
|
||||
version = tool.Ifc.get_schema()
|
||||
for attribute in declaration.attributes():
|
||||
if attribute.name() == "PredefinedType":
|
||||
results.extend([(e, e, "") for e in attribute.type_of_attribute().declared_type().enumeration_items()])
|
||||
results.extend(
|
||||
[
|
||||
(e, e, get_predefined_type_doc(version, cls.props.type_class, e))
|
||||
for e in attribute.type_of_attribute().declared_type().enumeration_items()
|
||||
]
|
||||
)
|
||||
break
|
||||
return results
|
||||
|
||||
@@ -179,14 +191,17 @@ class AuthoringData:
|
||||
if not ifc_class and ifc_classes:
|
||||
ifc_class = ifc_classes[0][0]
|
||||
if ifc_class:
|
||||
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name)
|
||||
elements = sorted(tool.Ifc.get().by_type(ifc_class), key=lambda s: s.Name or "Unnamed")
|
||||
results.extend(elements)
|
||||
return results
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def relating_types(cls, ifc_class=None):
|
||||
return [(str(e.id()), e.Name, e.Description or "") for e in cls.constr_class_entities(ifc_class=ifc_class)]
|
||||
return [
|
||||
(str(e.id()), e.Name or "Unnamed", e.Description or "")
|
||||
for e in cls.constr_class_entities(ifc_class=ifc_class)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def relating_types_browser(cls):
|
||||
@@ -313,3 +328,49 @@ class AuthoringData:
|
||||
def relating_type_id_by_name(cls, ifc_class, relating_type):
|
||||
relating_types = [ct[0] for ct in cls.relating_types(ifc_class=ifc_class) if ct[1] == relating_type]
|
||||
return None if len(relating_types) == 0 else relating_types[0]
|
||||
|
||||
|
||||
class ArrayData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data = {"parameters": cls.parameters()}
|
||||
|
||||
@classmethod
|
||||
def parameters(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
parameters = psets.get("BBIM_Array", None)
|
||||
if parameters:
|
||||
try:
|
||||
parent = tool.Ifc.get().by_guid(parameters["Parent"])
|
||||
parameters["has_parent"] = True
|
||||
parameters["parent_name"] = parent.Name or "Unnamed"
|
||||
parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]")
|
||||
except:
|
||||
parameters["has_parent"] = False
|
||||
return parameters
|
||||
|
||||
|
||||
class StairData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.is_loaded = True
|
||||
cls.data = {"parameters": cls.parameters()}
|
||||
|
||||
@classmethod
|
||||
def parameters(cls):
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if element:
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
parameters = psets.get("BBIM_Stair", None)
|
||||
if parameters:
|
||||
parameters["data"] = json.loads(parameters.get("Data", "[]") or "[]")
|
||||
return parameters
|
||||
|
||||
@@ -71,12 +71,12 @@ class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
new_matrix = voided_obj.matrix_world.copy()
|
||||
new_matrix.col[3] = tool.Cad.point_on_edge(target, axis).to_4d()
|
||||
if not filling.is_a("IfcDoor"):
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
if container_obj:
|
||||
new_matrix[2][3] = container_obj.matrix_world[2][3] + (props.rl2 * unit_scale)
|
||||
|
||||
if filling.is_a("IfcDoor"):
|
||||
new_matrix[2][3] = voided_obj.matrix_world[2][3]
|
||||
else:
|
||||
new_matrix[2][3] = voided_obj.matrix_world[2][3] + (props.rl2 * unit_scale)
|
||||
|
||||
filling_obj.matrix_world = new_matrix
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -201,7 +201,9 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
|
||||
page: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
context.scene.BIMModelProperties.type_page = self.page
|
||||
props = context.scene.BIMModelProperties
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=props.type_class, offset=9 * (self.page - 1), limit=9)
|
||||
props.type_page = self.page
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -316,14 +318,27 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Load Type Thumbnails"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
ifc_class: bpy.props.StringProperty()
|
||||
limit: bpy.props.IntProperty()
|
||||
offset: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
if bpy.app.background:
|
||||
return
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
processing = set()
|
||||
# Only process at most one class at a time.
|
||||
# Only process at most one paginated class at a time.
|
||||
# Large projects have hundreds of types which can lead to unnecessary lag.
|
||||
queue = tool.Ifc.get().by_type(self.ifc_class)
|
||||
queue = sorted(tool.Ifc.get().by_type(self.ifc_class), key=lambda e: e.Name or "Unnamed")
|
||||
if self.limit:
|
||||
queue = queue[self.offset:self.offset + self.limit]
|
||||
else:
|
||||
offset = 9 * (props.type_page - 1)
|
||||
if offset < 0:
|
||||
offset = 0
|
||||
queue = queue[offset:offset + 9]
|
||||
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
|
||||
@@ -199,6 +199,17 @@ class DumbProfileGenerator:
|
||||
|
||||
|
||||
class DumbProfileRegenerator:
|
||||
def regenerate_from_profile_def(self, profile):
|
||||
self.file = tool.Ifc.get()
|
||||
objs = []
|
||||
if not profile:
|
||||
return
|
||||
for element in self.get_elements_using_profile(profile):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
objs.append(obj)
|
||||
DumbProfileRecalculator().recalculate(objs)
|
||||
|
||||
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
|
||||
self.file = ifc_file
|
||||
objs = []
|
||||
@@ -833,7 +844,7 @@ class DumbProfileRecalculator:
|
||||
queue.add((rel.RelatingElement, tool.Ifc.get_object(rel.RelatingElement)))
|
||||
joiner = DumbProfileJoiner()
|
||||
for element, profile in queue:
|
||||
if element.is_a() in ("IfcColumn", "IfcBeam", "IfcMember") and profile:
|
||||
if profile:
|
||||
joiner.recreate_profile(element, profile)
|
||||
|
||||
|
||||
|
||||
@@ -228,8 +228,8 @@ class BIMModelProperties(PropertyGroup):
|
||||
x: bpy.props.FloatProperty(name="X", default=0.5)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0.5)
|
||||
z: bpy.props.FloatProperty(name="Z", default=0.5)
|
||||
rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc
|
||||
rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture
|
||||
rl1: bpy.props.FloatProperty(name="RL", default=1) # Used for things like walls, doors, flooring, skirting, etc
|
||||
rl2: bpy.props.FloatProperty(name="RL", default=1) # Used for things like windows, other hosted furniture
|
||||
x_angle: bpy.props.FloatProperty(name="X Angle", default=0)
|
||||
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
|
||||
type_template: bpy.props.EnumProperty(
|
||||
@@ -245,3 +245,52 @@ class BIMModelProperties(PropertyGroup):
|
||||
)
|
||||
type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class)
|
||||
type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None)
|
||||
|
||||
|
||||
class BIMArrayProperties(PropertyGroup):
|
||||
is_editing: bpy.props.IntProperty(default=-1)
|
||||
count: bpy.props.IntProperty(name="Count", default=0)
|
||||
x: bpy.props.FloatProperty(name="X", default=0)
|
||||
y: bpy.props.FloatProperty(name="Y", default=0)
|
||||
z: bpy.props.FloatProperty(name="Z", default=0)
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
stair_types = (
|
||||
("CONCRETE", "Concrete", ""),
|
||||
("WOOD/STEEL", "Wood / Steel", ""),
|
||||
)
|
||||
|
||||
is_editing: bpy.props.IntProperty(default=-1)
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01)
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01)
|
||||
number_of_treads: bpy.props.IntProperty(name="Number of treads", default=6, soft_min=1)
|
||||
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01)
|
||||
tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01)
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base slab depth", default=0.25, soft_min=0)
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top slab depth", default=0.25, soft_min=0)
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has top nib", default=True)
|
||||
stair_type: bpy.props.EnumProperty(name="Stair type", items=stair_types, default="CONCRETE")
|
||||
|
||||
def get_props_kwargs(self):
|
||||
if self.stair_type == "CONCRETE":
|
||||
return {
|
||||
"stair_type": self.stair_type,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"number_of_treads": self.number_of_treads,
|
||||
"tread_depth": self.tread_depth,
|
||||
"tread_run": self.tread_run,
|
||||
"base_slab_depth": self.base_slab_depth,
|
||||
"top_slab_depth": self.top_slab_depth,
|
||||
"has_top_nib": self.has_top_nib,
|
||||
}
|
||||
elif self.stair_type == "WOOD/STEEL":
|
||||
return {
|
||||
"stair_type": self.stair_type,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"number_of_treads": self.number_of_treads,
|
||||
"tread_depth": self.tread_depth,
|
||||
"tread_run": self.tread_run,
|
||||
}
|
||||
|
||||
@@ -173,11 +173,9 @@ class DumbSlabGenerator:
|
||||
matrix_world[2][3] = self.collection_obj.location[2] - self.depth
|
||||
else:
|
||||
matrix_world[2][3] -= self.depth
|
||||
obj.matrix_world = matrix_world
|
||||
obj.matrix_world = Matrix.Rotation(self.x_angle, 4, "X") @ matrix_world
|
||||
bpy.context.view_layer.update()
|
||||
self.collection.objects.link(obj)
|
||||
rotation = mathutils.Matrix.Rotation(self.x_angle, 4, 'X')
|
||||
obj.matrix_world = rotation
|
||||
|
||||
element = blenderbim.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
@@ -619,122 +617,13 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
self.vertices = []
|
||||
self.edges = []
|
||||
self.arcs = []
|
||||
self.circles = []
|
||||
|
||||
profile = extrusion.SweptArea
|
||||
if profile.is_a("IfcArbitraryClosedProfileDef"):
|
||||
self.process_curve(obj, position, profile.OuterCurve)
|
||||
if profile.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
for inner_curve in profile.InnerCurves:
|
||||
self.process_curve(obj, position, inner_curve)
|
||||
elif profile.is_a() == "IfcRectangleProfileDef":
|
||||
self.process_rectangle(obj, position, profile)
|
||||
|
||||
mesh = bpy.data.meshes.new("Profile")
|
||||
mesh.from_pydata(self.vertices, self.edges, [])
|
||||
mesh.BIMMeshProperties.is_profile = True
|
||||
obj.data = mesh
|
||||
|
||||
for arc in self.arcs:
|
||||
group = obj.vertex_groups.new(name="IFCARCINDEX")
|
||||
group.add(arc, 1, "REPLACE")
|
||||
|
||||
for circle in self.circles:
|
||||
group = obj.vertex_groups.new(name="IFCCIRCLE")
|
||||
group.add(circle, 1, "REPLACE")
|
||||
tool.Model.import_profile(extrusion.SweptArea, obj=obj, position=position)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
DecorationsHandler.install(context)
|
||||
bpy.ops.wm.tool_set_by_id(name="bim.cad_tool")
|
||||
return {"FINISHED"}
|
||||
|
||||
def process_curve(self, obj, position, curve):
|
||||
offset = len(self.vertices)
|
||||
|
||||
if curve.is_a("IfcPolyline"):
|
||||
total_points = len(curve.Points)
|
||||
last_index = len(curve.Points) - 1
|
||||
for i, point in enumerate(curve.Points):
|
||||
if i == last_index:
|
||||
continue
|
||||
global_point = position @ Vector(self.convert_unit_to_si(point.Coordinates)).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
|
||||
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
|
||||
elif curve.is_a("IfcIndexedPolyCurve"):
|
||||
is_arc = False
|
||||
if curve.Segments:
|
||||
for segment in curve.Segments:
|
||||
if len(segment[0]) == 3: # IfcArcIndex
|
||||
is_arc = True
|
||||
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
|
||||
global_point = position @ Vector(local_point).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][1] - 1])
|
||||
global_point = position @ Vector(local_point).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
self.arcs.append([len(self.vertices) - 2, len(self.vertices) - 1])
|
||||
else:
|
||||
local_point = self.convert_unit_to_si(curve.Points.CoordList[segment[0][0] - 1])
|
||||
global_point = position @ Vector(local_point).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
if is_arc:
|
||||
self.arcs[-1].append(len(self.vertices) - 1)
|
||||
is_arc = False
|
||||
else:
|
||||
for local_point in curve.Points.CoordList:
|
||||
global_point = position @ Vector(self.convert_unit_to_si(local_point)).to_3d()
|
||||
self.vertices.append(global_point)
|
||||
# Curves without segments are self closing
|
||||
del self.vertices[-1]
|
||||
|
||||
self.edges.extend([(i, i + 1) for i in range(offset, len(self.vertices))])
|
||||
self.edges[-1] = (len(self.vertices) - 1, offset) # Close the loop
|
||||
elif curve.is_a("IfcCircle"):
|
||||
center = self.convert_unit_to_si(
|
||||
Matrix(ifcopenshell.util.placement.get_axis2placement(curve.Position).tolist()).col[3].to_3d()
|
||||
)
|
||||
radius = self.convert_unit_to_si(curve.Radius)
|
||||
self.vertices.extend(
|
||||
[
|
||||
position @ Vector((center[0], center[1] - curve.Radius, 0.0)),
|
||||
position @ Vector((center[0], center[1] + curve.Radius, 0.0)),
|
||||
]
|
||||
)
|
||||
self.circles.append([offset, offset + 1])
|
||||
self.edges.append((offset, offset + 1))
|
||||
|
||||
def process_rectangle(self, obj, position, profile):
|
||||
if profile.Position:
|
||||
p_position = Matrix(ifcopenshell.util.placement.get_axis2placement(profile.Position).tolist())
|
||||
p_position[0][3] *= self.unit_scale
|
||||
p_position[1][3] *= self.unit_scale
|
||||
p_position[2][3] *= self.unit_scale
|
||||
else:
|
||||
p_position = Matrix()
|
||||
|
||||
x = self.convert_unit_to_si(profile.XDim)
|
||||
y = self.convert_unit_to_si(profile.YDim)
|
||||
|
||||
self.vertices.extend(
|
||||
[
|
||||
position @ p_position @ Vector((-x / 2, -y / 2, 0.0)),
|
||||
position @ p_position @ Vector((x / 2, -y / 2, 0.0)),
|
||||
position @ p_position @ Vector((x / 2, y / 2, 0.0)),
|
||||
position @ p_position @ Vector((-x / 2, y / 2, 0.0)),
|
||||
]
|
||||
)
|
||||
self.edges.extend([(i, i + 1) for i in range(0, len(self.vertices))])
|
||||
self.edges[-1] = (len(self.vertices) - 1, 0) # Close the loop
|
||||
|
||||
def convert_unit_to_si(self, value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [v * self.unit_scale for v in value]
|
||||
return value * self.unit_scale
|
||||
|
||||
|
||||
class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.edit_extrusion_profile"
|
||||
@@ -759,10 +648,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
else:
|
||||
position = Matrix()
|
||||
|
||||
helper = Helper(tool.Ifc.get())
|
||||
indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data)
|
||||
profile = tool.Model.export_profile(obj, position=position)
|
||||
|
||||
if isinstance(indices, tuple) and indices[0] is False: # Ugly
|
||||
if not profile:
|
||||
|
||||
def msg(self, context):
|
||||
self.layout.label(text="INVALID PROFILE: " + indices[1])
|
||||
@@ -770,32 +658,11 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
|
||||
DecorationsHandler.install(context)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
return {"FINISHED"}
|
||||
|
||||
self.bm = bmesh.new()
|
||||
self.bm.from_mesh(obj.data)
|
||||
self.bm.verts.ensure_lookup_table()
|
||||
self.bm.edges.ensure_lookup_table()
|
||||
|
||||
if indices["inner_curves"]:
|
||||
profile = tool.Ifc.get().createIfcArbitraryProfileDefWithVoids("AREA")
|
||||
else:
|
||||
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
self.points = self.create_points(position, indices["points"])
|
||||
|
||||
profile.OuterCurve = self.create_curve(position, indices["profile"])
|
||||
if indices["inner_curves"]:
|
||||
results = []
|
||||
for inner_curve in indices["inner_curves"]:
|
||||
results.append(self.create_curve(position, inner_curve))
|
||||
profile.InnerCurves = results
|
||||
|
||||
self.bm.free()
|
||||
return
|
||||
|
||||
old_profile = extrusion.SweptArea
|
||||
extrusion.SweptArea = profile
|
||||
for inverse in tool.Ifc.get().get_inverse(old_profile):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
|
||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
|
||||
|
||||
blenderbim.core.geometry.switch_representation(
|
||||
@@ -832,45 +699,6 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def create_points(self, position, indices):
|
||||
position_i = position.inverted()
|
||||
points = []
|
||||
for point in indices:
|
||||
local_point = (position_i @ point).to_2d()
|
||||
points.append(self.convert_si_to_unit(list(local_point)))
|
||||
return tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
|
||||
def create_curve(self, position, edge_indices, points=None):
|
||||
position_i = position.inverted()
|
||||
if len(edge_indices) == 2:
|
||||
diameter = edge_indices[0]
|
||||
p1 = self.bm.verts[diameter[0]].co
|
||||
p2 = self.bm.verts[diameter[1]].co
|
||||
center = self.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
|
||||
radius = self.convert_si_to_unit((p1 - p2).length / 2)
|
||||
return tool.Ifc.get().createIfcCircle(
|
||||
tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius
|
||||
)
|
||||
if tool.Ifc.get().schema == "IFC2X3":
|
||||
points = []
|
||||
for edge in edge_indices:
|
||||
local_point = (position_i @ Vector(self.bm.verts[edge[0]].co)).to_2d()
|
||||
points.append(tool.Ifc.get().createIfcCartesianPoint(self.convert_si_to_unit(local_point)))
|
||||
points.append(points[0])
|
||||
return tool.Ifc.get().createIfcPolyline(points)
|
||||
segments = []
|
||||
for segment in edge_indices:
|
||||
if len(segment) == 2:
|
||||
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
|
||||
elif len(segment) == 3:
|
||||
segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment]))
|
||||
return tool.Ifc.get().createIfcIndexedPolyCurve(self.points, segments, False)
|
||||
|
||||
def convert_si_to_unit(self, value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
return [v / self.unit_scale for v in value]
|
||||
return value / self.unit_scale
|
||||
|
||||
|
||||
class ResetVertex(bpy.types.Operator):
|
||||
bl_idname = "bim.reset_vertex"
|
||||
@@ -957,7 +785,7 @@ class DecorationsHandler:
|
||||
special_vertex_indices = {}
|
||||
selected_edges = []
|
||||
unselected_edges = []
|
||||
special_edges = []
|
||||
special_edges = [] # edges that have a circle or an arc associated with them
|
||||
|
||||
arc_groups = []
|
||||
circle_groups = []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @Andrej730
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -18,14 +18,24 @@
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty
|
||||
from bpy.props import FloatProperty, IntProperty, BoolProperty
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
|
||||
import bmesh
|
||||
from bmesh.types import BMVert
|
||||
|
||||
import ifcopenshell
|
||||
import blenderbim
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.module.model.prop import BIMStairProperties
|
||||
|
||||
|
||||
from mathutils import Vector
|
||||
from pprint import pprint
|
||||
import json
|
||||
|
||||
|
||||
def add_object(self, context):
|
||||
if self.number_of_treads <= 0:
|
||||
self.number_of_treads = 1
|
||||
def add_dumb_stair_object(self, context):
|
||||
verts = [
|
||||
Vector((0, 0, 0)),
|
||||
Vector((0, self.tread_length, 0)),
|
||||
@@ -63,15 +73,313 @@ class BIM_OT_add_object(Operator, AddObjectHelper):
|
||||
width: FloatProperty(name="Width", default=1.1)
|
||||
height: FloatProperty(name="Height", default=1)
|
||||
tread_depth: FloatProperty(name="Tread Depth", default=0.2)
|
||||
number_of_treads: IntProperty(name="Number of Treads (Goings)", default=6)
|
||||
number_of_treads: IntProperty(name="Number of Treads (Goings)", default=6, soft_min=1)
|
||||
tread_length: FloatProperty(name="Tread Length (Going)", default=0.25)
|
||||
riser_height: FloatProperty(name="*Calculated* Riser Height")
|
||||
length: FloatProperty(name="*Calculated* Length")
|
||||
|
||||
def execute(self, context):
|
||||
add_object(self, context)
|
||||
add_dumb_stair_object(self, context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def generate_stair_2d_profile(
|
||||
number_of_treads,
|
||||
height,
|
||||
width,
|
||||
tread_run,
|
||||
tread_depth,
|
||||
stair_type,
|
||||
# CONCRETE STAIR ARGUMENTS
|
||||
has_top_nib=None,
|
||||
top_slab_depth=None,
|
||||
base_slab_depth=None,
|
||||
):
|
||||
vertices = []
|
||||
edges = []
|
||||
faces = []
|
||||
|
||||
number_of_risers = number_of_treads + 1
|
||||
tread_rise = height / number_of_risers
|
||||
length = tread_run * number_of_risers
|
||||
|
||||
if stair_type == "WOOD/STEEL":
|
||||
for i in range(number_of_risers):
|
||||
v1 = Vector(((i + 1) * tread_run, 0, (i + 1) * tread_rise))
|
||||
v0 = v1 - Vector((tread_run, 0, 0))
|
||||
v2 = v1 - Vector((0, 0, tread_depth))
|
||||
v3 = v0 - Vector((0, 0, tread_depth))
|
||||
vertices.extend([v0, v1, v2, v3])
|
||||
|
||||
cur_vertex = i * 4
|
||||
edges.extend([
|
||||
(cur_vertex, cur_vertex+1),
|
||||
(cur_vertex+1, cur_vertex+2),
|
||||
(cur_vertex+2, cur_vertex+3),
|
||||
(cur_vertex+3, cur_vertex),
|
||||
])
|
||||
faces.append(list(range(4 * i, 4 * i + 1)))
|
||||
|
||||
return (vertices, edges, faces)
|
||||
elif stair_type == "CONCRETE":
|
||||
for i in range(number_of_risers):
|
||||
vertices.extend([
|
||||
Vector((tread_run*i, 0, tread_rise*i)),
|
||||
Vector((tread_run*i, 0, tread_rise*(i+1)))
|
||||
])
|
||||
cur_vertex = i * 2
|
||||
if i != 0:
|
||||
edges.append((cur_vertex - 1, cur_vertex))
|
||||
edges.append((cur_vertex, cur_vertex + 1))
|
||||
|
||||
vertices.append(Vector((tread_run * number_of_risers, 0, tread_rise * number_of_risers)))
|
||||
edges.append((number_of_risers * 2, number_of_risers * 2 - 1))
|
||||
|
||||
td_vector = Vector((vertices[2][2], 0, -vertices[2][0])).normalized() * tread_depth
|
||||
|
||||
k = tread_rise / tread_run
|
||||
s0 = vertices[0] + td_vector
|
||||
b = s0.z - k * s0.x # comes from y = kx + b
|
||||
# you could use td_vector as depth_vector
|
||||
# but then stair won't be perpendicular to the X+
|
||||
# b is kind of vertical tread_depth (along Z+)
|
||||
depth_vector = Vector((0, 0, b))
|
||||
|
||||
# top nib
|
||||
if has_top_nib:
|
||||
vertices.append( vertices[number_of_risers * 2] + Vector((0, 0, -top_slab_depth)) )
|
||||
vertices.append( vertices[number_of_risers * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth)) )
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append( (number_of_risers * 2, last_vertex_i - 1) )
|
||||
edges.append( (last_vertex_i - 1, last_vertex_i) )
|
||||
else:
|
||||
vertices.append(vertices[number_of_risers * 2] + depth_vector)
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append((number_of_risers * 2, last_vertex_i))
|
||||
|
||||
top_nib_end = len(vertices) - 1
|
||||
|
||||
# bottom nib
|
||||
if abs(b) <= base_slab_depth:
|
||||
vertices.append(vertices[0] + depth_vector)
|
||||
edges.append((0, len(vertices) - 1))
|
||||
bottom_nib_end = len(vertices) - 1
|
||||
else:
|
||||
vertices.append(vertices[0] + Vector(((-base_slab_depth - b) / k, 0, -base_slab_depth)))
|
||||
vertices.append(vertices[0] + Vector((0, 0, -base_slab_depth)))
|
||||
last_vertex_i = len(vertices) - 1
|
||||
edges.append( (0, last_vertex_i) )
|
||||
edges.append( (last_vertex_i - 1, last_vertex_i) )
|
||||
bottom_nib_end = len(vertices) - 2
|
||||
|
||||
edges.append( (bottom_nib_end, top_nib_end) )
|
||||
faces = [list(range(len(vertices)))]
|
||||
|
||||
return (vertices, edges, faces)
|
||||
|
||||
|
||||
def update_stair_modifier(context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMStairProperties
|
||||
|
||||
props_kwargs = props.get_props_kwargs()
|
||||
vertices, edges, faces = generate_stair_2d_profile(**props_kwargs)
|
||||
|
||||
obj = context.object
|
||||
bm = bmesh.new()
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in vertices]
|
||||
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in edges]
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
bmesh.ops.contextual_create(bm, geom=new_edges)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
faces = bm.faces
|
||||
extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
|
||||
extrusion_vector = Vector((0, 1, 0)) * props.width
|
||||
translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)]
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts)
|
||||
|
||||
if context.object.mode == "EDIT":
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
else:
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
obj.data.update()
|
||||
|
||||
|
||||
class BIM_OT_add_clever_stair(Operator):
|
||||
bl_idname = "mesh.add_clever_stair"
|
||||
bl_label = "Clever Stair"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
ifcfile = tool.Ifc.get()
|
||||
if not ifcfile:
|
||||
self.report({"ERROR"}, "You need to start IFC project first to create a stair.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if context.object is not None:
|
||||
spawn_location = context.object.location.copy()
|
||||
context.object.select_set(False)
|
||||
else:
|
||||
spawn_location = bpy.context.scene.cursor.location.copy()
|
||||
|
||||
mesh = bpy.data.meshes.new("StairFlight")
|
||||
obj = bpy.data.objects.new("StairFlight", mesh)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
obj.location = spawn_location
|
||||
body_context = ifcopenshell.util.representation.get_context(ifcfile, "Model", "Body", "MODEL_VIEW")
|
||||
element = blenderbim.core.root.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Root,
|
||||
obj=obj,
|
||||
ifc_class="IfcStairFlight",
|
||||
should_add_representation=True,
|
||||
context=body_context,
|
||||
)
|
||||
bpy.context.view_layer.objects.active = None
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
bpy.ops.bim.add_stair()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# UI operators
|
||||
class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_stair"
|
||||
bl_label = "Add Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
if not element.is_a("IfcStairFlight"):
|
||||
self.report({"ERROR"}, "Object has to be IfcStairFlight type to add a stair.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = obj.BIMStairProperties
|
||||
|
||||
stair_data = props.get_props_kwargs()
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
pset = psets.get("BBIM_Stair", None)
|
||||
|
||||
if pset:
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
else:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Stair")
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"pset.edit_pset",
|
||||
tool.Ifc.get(),
|
||||
pset=pset,
|
||||
properties={"Data": json.dumps(stair_data)},
|
||||
)
|
||||
update_stair_modifier(context)
|
||||
|
||||
# update IfcStairFlight properties
|
||||
element.NumberOfRisers = props.number_of_treads + 1
|
||||
element.NumberOfTreads = props.number_of_treads
|
||||
element.RiserHeight = props.height / element.NumberOfRisers
|
||||
element.TreadLength = props.tread_depth
|
||||
element.PredefinedType = "STRAIGHT"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_stair"
|
||||
bl_label = "Cancel editing Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
data = json.loads(psets["BBIM_Stair"]["Data"])
|
||||
props = obj.BIMStairProperties
|
||||
# restore previous settings since editing was canceled
|
||||
for prop_name in data:
|
||||
setattr(props, prop_name, data[prop_name])
|
||||
update_stair_modifier(context)
|
||||
|
||||
props.is_editing = -1
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_stair"
|
||||
bl_label = "Finish editing stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMStairProperties
|
||||
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
pset = psets["BBIM_Stair"]
|
||||
data = props.get_props_kwargs()
|
||||
|
||||
props.is_editing = -1
|
||||
|
||||
update_stair_modifier(context)
|
||||
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
data = json.dumps(data)
|
||||
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
|
||||
|
||||
# update IfcStairFlight properties
|
||||
if element.is_a("IfcStairFlight"):
|
||||
element.NumberOfRisers = props.number_of_treads + 1
|
||||
element.NumberOfTreads = props.number_of_treads
|
||||
element.RiserHeight = props.height / element.NumberOfRisers
|
||||
element.TreadLength = props.tread_depth
|
||||
element.PredefinedType = "STRAIGHT"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_stair"
|
||||
bl_label = "Enable Editing Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
props = obj.BIMStairProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
pset = ifcopenshell.util.element.get_psets(element)
|
||||
data = json.loads(pset["BBIM_Stair"]["Data"])
|
||||
# required since we could load pset from .ifc and BIMStairProperties won't be set
|
||||
for prop_name in data:
|
||||
setattr(props, prop_name, data[prop_name])
|
||||
|
||||
props.is_editing = 1
|
||||
|
||||
|
||||
class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_stair"
|
||||
bl_label = "Remove Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
obj.BIMStairProperties.is_editing = -1
|
||||
|
||||
pset = ifcopenshell.util.element.get_psets(element)
|
||||
pset = tool.Ifc.get().by_id(pset["BBIM_Stair"]["id"])
|
||||
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN")
|
||||
self.layout.operator(BIM_OT_add_clever_stair.bl_idname, icon="PLUGIN")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
@@ -17,10 +17,13 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel, Operator, Menu
|
||||
from blenderbim.bim.module.model.data import AuthoringData
|
||||
from blenderbim.bim.module.model.data import AuthoringData, ArrayData, StairData
|
||||
from blenderbim.bim.module.model.prop import store_cursor_position
|
||||
from blenderbim.bim.helper import prop_with_search, close_operator_panel
|
||||
from blenderbim.bim.module.model.stair import update_stair_modifier
|
||||
|
||||
from blenderbim.bim.helper import prop_with_search
|
||||
|
||||
|
||||
class LaunchTypeManager(bpy.types.Operator):
|
||||
@@ -37,7 +40,7 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
props.type_page = 1
|
||||
if props.ifc_class:
|
||||
props.type_class = props.ifc_class
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class)
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class, offset=0, limit=9)
|
||||
if not AuthoringData.is_loaded:
|
||||
AuthoringData.load()
|
||||
return context.window_manager.invoke_popup(self, width=550)
|
||||
@@ -46,7 +49,7 @@ class LaunchTypeManager(bpy.types.Operator):
|
||||
props = context.scene.BIMModelProperties
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "type_class", text="")
|
||||
prop_with_search(row, props, "type_class", text="")
|
||||
|
||||
columns = self.layout.column_flow(columns=3)
|
||||
row = columns.row()
|
||||
@@ -254,6 +257,117 @@ class HelpConstrTypes(Operator):
|
||||
]
|
||||
|
||||
|
||||
class BIM_PT_array(bpy.types.Panel):
|
||||
bl_label = "IFC Array"
|
||||
bl_idname = "BIM_PT_array"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "modifier"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object)
|
||||
|
||||
def draw(self, context):
|
||||
if not ArrayData.is_loaded:
|
||||
ArrayData.load()
|
||||
|
||||
props = context.active_object.BIMArrayProperties
|
||||
|
||||
if ArrayData.data["parameters"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF")
|
||||
op = row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="")
|
||||
op.parent = ArrayData.data["parameters"]["Parent"]
|
||||
if ArrayData.data["parameters"]["data"]:
|
||||
row.operator("bim.add_array", icon="ADD", text="")
|
||||
|
||||
for i, array in enumerate(ArrayData.data["parameters"]["data"]):
|
||||
box = self.layout.box()
|
||||
if props.is_editing == i:
|
||||
row = box.row(align=True)
|
||||
row.prop(props, "count", icon="MOD_ARRAY")
|
||||
row.operator("bim.edit_array", icon="CHECKMARK", text="").item = i
|
||||
row.operator("bim.disable_editing_array", icon="CANCEL", text="")
|
||||
row = box.row(align=True)
|
||||
row.prop(props, "x")
|
||||
row.prop(props, "y")
|
||||
row.prop(props, "z")
|
||||
|
||||
else:
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"{array['count']} Items", icon="MOD_ARRAY")
|
||||
row.operator("bim.enable_editing_array", icon="GREASEPENCIL", text="").item = i
|
||||
row.operator("bim.remove_array", icon="X", text="").item = i
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"X: {array['x']}")
|
||||
row.label(text=f"Y: {array['y']}")
|
||||
row.label(text=f"Z: {array['z']}")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Array Found")
|
||||
row.operator("bim.add_array", icon="ADD", text="")
|
||||
|
||||
|
||||
class BIM_PT_stair(bpy.types.Panel):
|
||||
bl_label = "IFC Stair"
|
||||
bl_idname = "BIM_PT_stair"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "modifier"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# always display modifier if it's IFC object
|
||||
return tool.Ifc.get() and tool.Ifc.get_entity(context.active_object)
|
||||
|
||||
def draw(self, context):
|
||||
if not StairData.is_loaded:
|
||||
StairData.load()
|
||||
|
||||
props = context.active_object.BIMStairProperties
|
||||
|
||||
if StairData.data["parameters"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Stair parameters", icon="IPO_CONSTANT")
|
||||
|
||||
stair_data = StairData.data["parameters"]["data"]
|
||||
if props.is_editing != -1:
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish editing")
|
||||
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_name in props.get_props_kwargs():
|
||||
self.layout.prop(props, prop_name)
|
||||
update_stair_modifier(context)
|
||||
else:
|
||||
row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_stair", icon="X", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop in props.get_props_kwargs():
|
||||
prop_value = stair_data[prop]
|
||||
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"{props.bl_rna.properties[prop].name}")
|
||||
row.label(text=str(prop_value))
|
||||
|
||||
# calculated properties
|
||||
number_of_rises = props.number_of_treads + 1
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Number of risers")
|
||||
row.label(text=str(number_of_rises))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Tread rise")
|
||||
row.label(text=str(round(props.height / number_of_rises, 5)))
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="Length")
|
||||
row.label(text=str(round(props.tread_run * number_of_rises, 5)))
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.label(text="No Stair Found")
|
||||
row.operator("bim.add_stair", icon="ADD", text="")
|
||||
|
||||
|
||||
class BIM_MT_model(Menu):
|
||||
bl_idname = "BIM_MT_model"
|
||||
bl_label = "IFC Objects"
|
||||
|
||||
@@ -76,7 +76,7 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Join Wall"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = """ Trim/Extend the selected walls to the last selected wall:
|
||||
'T' mode: Trim/Extend to a selected wall or 3D target
|
||||
'T' mode: Trim/Extend to a selected wall, slab, or 3D target
|
||||
'L' mode: Butt join two selected walls
|
||||
'V' mode: Mitre join two selected wall
|
||||
'' (empty) mode: Unjoin selected walls
|
||||
@@ -107,10 +107,18 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if len(selected_objs) < 2:
|
||||
return {"FINISHED"}
|
||||
if self.join_type == "T":
|
||||
for obj in selected_objs:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
joiner.join_T(obj, context.active_object)
|
||||
elements = [tool.Ifc.get_entity(o) for o in context.selected_objects]
|
||||
targets = [e for e in elements if e and e.is_a() in ("IfcSlab", "IfcSlabStandardCase", "IfcRoof")]
|
||||
if targets:
|
||||
target = tool.Ifc.get_object(targets[0])
|
||||
for element in elements:
|
||||
if element.is_a("IfcWall"):
|
||||
joiner.join_Z(tool.Ifc.get_object(element), target)
|
||||
else:
|
||||
for obj in selected_objs:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
joiner.join_T(obj, context.active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -234,8 +242,18 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
extrusion.Depth = self.depth
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
extrusion.Depth = self.depth * (1 / cos(x_angle))
|
||||
if element.is_a("IfcWall"):
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements":
|
||||
ifcopenshell.api.run(
|
||||
"geometry.disconnect_element",
|
||||
tool.Ifc.get(),
|
||||
relating_element=rel.RelatingElement,
|
||||
related_element=element,
|
||||
)
|
||||
wall_objs.append(obj)
|
||||
if wall_objs:
|
||||
DumbWallRecalculator().recalculate(wall_objs)
|
||||
@@ -256,6 +274,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
wall_objs = []
|
||||
other_objs = []
|
||||
x_angle = radians(self.x_angle)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
@@ -266,6 +285,10 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
extrusion = tool.Model.get_extrusion(representation)
|
||||
if not extrusion:
|
||||
return
|
||||
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
||||
existing_x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
perpendicular_depth = extrusion.Depth / (1 / cos(existing_x_angle))
|
||||
extrusion.Depth = perpendicular_depth * (1 / cos(x_angle))
|
||||
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
|
||||
if element.is_a("IfcWall"):
|
||||
wall_objs.append(obj)
|
||||
@@ -278,8 +301,12 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
is_global=True,
|
||||
should_sync_changes_first=False,
|
||||
)
|
||||
rotation = mathutils.Matrix.Rotation(x_angle, 4, 'X')
|
||||
obj.matrix_world = rotation
|
||||
|
||||
euler = obj.matrix_world.to_euler()
|
||||
euler.x = x_angle
|
||||
new_matrix = euler.to_matrix().to_4x4()
|
||||
new_matrix.col[3] = obj.matrix_world.col[3]
|
||||
obj.matrix_world = new_matrix
|
||||
if wall_objs:
|
||||
DumbWallRecalculator().recalculate(wall_objs)
|
||||
return {"FINISHED"}
|
||||
@@ -611,7 +638,7 @@ class DumbWallGenerator:
|
||||
"geometry.add_wall_representation",
|
||||
tool.Ifc.get(),
|
||||
context=self.body_context,
|
||||
thickness=self.layers["thickness"] * 1/cos(self.x_angle), #1/cos(self.x_angle) maintains the correct MaterialLayer thickness upon rotation
|
||||
thickness=self.layers["thickness"],
|
||||
offset=self.layers["offset"],
|
||||
length=self.length,
|
||||
height=self.height,
|
||||
@@ -920,6 +947,20 @@ class DumbWallJoiner:
|
||||
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
|
||||
return wall2
|
||||
|
||||
def join_Z(self, wall1, slab2):
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(slab2)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"geometry.connect_element",
|
||||
tool.Ifc.get(),
|
||||
relating_element=element2,
|
||||
related_element=element1,
|
||||
description="TOP",
|
||||
)
|
||||
|
||||
self.recreate_wall(element1, wall1)
|
||||
|
||||
def join_L(self, wall1, wall2):
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(wall2)
|
||||
@@ -1043,22 +1084,37 @@ class DumbWallJoiner:
|
||||
layers = tool.Model.get_material_layer_parameters(element)
|
||||
|
||||
for rel in element.ConnectedTo:
|
||||
connection = rel.RelatingConnectionType
|
||||
other = tool.Ifc.get_object(rel.RelatedElement)
|
||||
if connection not in ["ATPATH", "NOTDEFINED"]:
|
||||
self.join(
|
||||
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
|
||||
)
|
||||
if rel.is_a("IfcRelConnectsPathElements"):
|
||||
connection = rel.RelatingConnectionType
|
||||
other = tool.Ifc.get_object(rel.RelatedElement)
|
||||
if connection not in ["ATPATH", "NOTDEFINED"]:
|
||||
self.join(
|
||||
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
|
||||
)
|
||||
for rel in element.ConnectedFrom:
|
||||
connection = rel.RelatedConnectionType
|
||||
other = tool.Ifc.get_object(rel.RelatingElement)
|
||||
if connection not in ["ATPATH", "NOTDEFINED"]:
|
||||
self.join(
|
||||
obj, other, connection, rel.RelatingConnectionType, is_relating=False, description=rel.Description
|
||||
)
|
||||
if rel.is_a("IfcRelConnectsPathElements"):
|
||||
connection = rel.RelatedConnectionType
|
||||
other = tool.Ifc.get_object(rel.RelatingElement)
|
||||
if connection not in ["ATPATH", "NOTDEFINED"]:
|
||||
self.join(
|
||||
obj,
|
||||
other,
|
||||
connection,
|
||||
rel.RelatingConnectionType,
|
||||
is_relating=False,
|
||||
description=rel.Description,
|
||||
)
|
||||
|
||||
previous_matrix = obj.matrix_world.copy()
|
||||
previous_origin = previous_matrix.col[3].to_2d()
|
||||
obj.matrix_world[0][3], obj.matrix_world[1][3] = self.body[0]
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
for rel in element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements":
|
||||
height = self.clip(obj, tool.Ifc.get_object(rel.RelatingElement))
|
||||
|
||||
new_matrix = copy.deepcopy(obj.matrix_world)
|
||||
new_matrix.col[3] = self.body[0].to_4d().copy()
|
||||
new_matrix.invert()
|
||||
|
||||
for clipping in self.clippings:
|
||||
@@ -1092,7 +1148,7 @@ class DumbWallJoiner:
|
||||
height=height,
|
||||
x_angle=x_angle,
|
||||
offset=layers["offset"],
|
||||
thickness=layers["thickness"] * 1/cos(x_angle), #1/cos(x_angle) maintains the correct MaterialLayer thickness upon rotation
|
||||
thickness=layers["thickness"],
|
||||
clippings=self.clippings,
|
||||
booleans=tool.Model.get_manual_booleans(element),
|
||||
)
|
||||
@@ -1109,10 +1165,6 @@ class DumbWallJoiner:
|
||||
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_body
|
||||
)
|
||||
|
||||
previous_matrix = obj.matrix_world.copy()
|
||||
previous_origin = previous_matrix.col[3].to_2d()
|
||||
obj.matrix_world[0][3], obj.matrix_world[1][3] = self.body[0]
|
||||
bpy.context.view_layer.update()
|
||||
if tool.Ifc.is_moved(obj):
|
||||
# Openings should move with the host overall ...
|
||||
# ... except their position should stay the same along the local X axis of the wall
|
||||
@@ -1152,16 +1204,17 @@ class DumbWallJoiner:
|
||||
)
|
||||
|
||||
def get_extrusion_data(self, representation):
|
||||
results = {"height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
|
||||
results = {"item": None, "height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
|
||||
item = representation.Items[0]
|
||||
while True:
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
results["height"] = item.Depth * self.unit_scale
|
||||
results["item"] = item
|
||||
x, y, z = item.ExtrudedDirection.DirectionRatios
|
||||
if not tool.Cad.is_x(x, 0) or not tool.Cad.is_x(y, 0) or not tool.Cad.is_x(z, 1):
|
||||
results["direction"] = Vector(item.ExtrudedDirection.DirectionRatios)
|
||||
results["x_angle"] = Vector((0, 1)).angle_signed(Vector((y, z)))
|
||||
results["is_sloped"] = True
|
||||
results["height"] = (item.Depth * self.unit_scale) / (1 / cos(results["x_angle"]))
|
||||
break
|
||||
elif item.is_a("IfcBooleanClippingResult"):
|
||||
item = item.FirstOperand
|
||||
@@ -1215,7 +1268,8 @@ class DumbWallJoiner:
|
||||
bp2 = wall2.matrix_world @ Vector(wall2.bound_box[0])
|
||||
tp1 = wall1.matrix_world @ Vector(wall1.bound_box[1])
|
||||
|
||||
# Axis lines on bottom, for base and side axes
|
||||
# Axis lines on bottom, for reference, base, and side axes
|
||||
bra1 = (Vector((*axis1["reference"][0], bp1[2])), Vector((*axis1["reference"][1], bp1[2])))
|
||||
bba1 = (Vector((*axis1["base"][0], bp1[2])), Vector((*axis1["base"][1], bp1[2])))
|
||||
bsa1 = (Vector((*axis1["side"][0], bp1[2])), Vector((*axis1["side"][1], bp1[2])))
|
||||
bba2 = (Vector((*axis2["base"][0], bp2[2])), Vector((*axis2["base"][1], bp2[2])))
|
||||
@@ -1260,7 +1314,7 @@ class DumbWallJoiner:
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)).copy()
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)).copy()
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy()
|
||||
self.body[j] = new_body.to_2d()
|
||||
self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d()
|
||||
|
||||
if connection1 == connection2:
|
||||
if (connection1 == "ATEND" and angle > 0) or (connection1 != "ATEND" and angle < 0):
|
||||
@@ -1301,9 +1355,9 @@ class DumbWallJoiner:
|
||||
and not extrusion2["is_sloped"]
|
||||
):
|
||||
if is_relating:
|
||||
self.body[j] = bbf.to_2d()
|
||||
self.body[j] = tool.Cad.point_on_edge(bbf, bra1).to_2d()
|
||||
else:
|
||||
self.body[j] = bbn.to_2d()
|
||||
self.body[j] = tool.Cad.point_on_edge(bbn, bra1).to_2d()
|
||||
return True
|
||||
|
||||
bsf_ = tool.Cad.point_on_edge(bsf, bba1)
|
||||
@@ -1312,7 +1366,7 @@ class DumbWallJoiner:
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)).copy()
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)).copy()
|
||||
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy()
|
||||
self.body[j] = new_body.to_2d()
|
||||
self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d()
|
||||
|
||||
if is_relating:
|
||||
pt = bbf.to_2d().to_3d()
|
||||
@@ -1336,3 +1390,41 @@ class DumbWallJoiner:
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def clip(self, wall1, slab2):
|
||||
element1 = tool.Ifc.get_entity(wall1)
|
||||
element2 = tool.Ifc.get_entity(slab2)
|
||||
|
||||
layers1 = tool.Model.get_material_layer_parameters(element1)
|
||||
axis1 = tool.Model.get_wall_axis(wall1, layers1)
|
||||
|
||||
bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()]
|
||||
|
||||
extrusion = self.get_extrusion_data(tool.Ifc.get().by_id(wall1.data.BIMMeshProperties.ifc_definition_id))
|
||||
d = wall1.matrix_world.to_quaternion() @ extrusion["direction"]
|
||||
|
||||
slab_pt = slab2.matrix_world @ Vector((0, 0, 0))
|
||||
slab_dir = slab2.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
|
||||
tops = [mathutils.geometry.intersect_line_plane(b, b + d, slab_pt, slab_dir) for b in bases]
|
||||
|
||||
i_bottom = None
|
||||
i_top = None
|
||||
for i, co in enumerate(tops):
|
||||
if i_top is None or co[2] > i_top[2]:
|
||||
i_top = co
|
||||
i_bottom = bases[i]
|
||||
|
||||
quaternion = slab2.matrix_world.to_quaternion()
|
||||
x_axis = quaternion @ Vector((1, 0, 0))
|
||||
y_axis = quaternion @ Vector((0, 1, 0))
|
||||
z_axis = quaternion @ Vector((0, 0, 1))
|
||||
self.clippings.append(
|
||||
{
|
||||
"type": "IfcBooleanClippingResult",
|
||||
"operand_type": "IfcHalfSpaceSolid",
|
||||
"matrix": self.create_matrix(i_top, x_axis, y_axis, z_axis),
|
||||
}
|
||||
)
|
||||
|
||||
return (i_top - i_bottom).length
|
||||
|
||||
@@ -413,7 +413,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.align_product(align_type="CENTERLINE")
|
||||
|
||||
def hotkey_S_E(self):
|
||||
if self.active_class in ("IfcWall", "IfcWallStandardCase"):
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
elif self.active_class in ("IfcWall", "IfcWallStandardCase"):
|
||||
bpy.ops.bim.join_wall(join_type="T")
|
||||
elif self.active_class in ("IfcSlab", "IfcSlabStandardCase", "IfcRamp", "IfcRoof"):
|
||||
if not bpy.context.active_object:
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from ifcopenshell.util.doc import get_entity_doc
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from blenderbim.bim.module.owner.data import OwnerData, ActorData, ObjectActorData
|
||||
from bpy.types import PropertyGroup
|
||||
@@ -64,6 +66,23 @@ def update_actor_class(self, context):
|
||||
ActorData.data["actors"] = ActorData.actors()
|
||||
|
||||
|
||||
def get_actor_class_enum(self, context):
|
||||
version = tool.Ifc.get_schema()
|
||||
return [
|
||||
("IfcActor", "Actor", get_entity_doc(version, "IfcActor").get("description", "")),
|
||||
("IfcOccupant", "Occupant", get_entity_doc(version, "IfcOccupant").get("description", "")),
|
||||
]
|
||||
|
||||
|
||||
def get_actor_type_enum(self, context):
|
||||
version = tool.Ifc.get_schema()
|
||||
return [
|
||||
("IfcPerson", "Person", get_entity_doc(version, "IfcPerson").get("description", "")),
|
||||
("IfcOrganization", "Organisation", get_entity_doc(version, "IfcOrganization").get("description", "")),
|
||||
("IfcPersonAndOrganization", "User", get_entity_doc(version, "IfcPersonAndOrganization").get("description", "")),
|
||||
]
|
||||
|
||||
|
||||
class BIMOwnerProperties(PropertyGroup):
|
||||
active_person_id: IntProperty(name="Active Person Id")
|
||||
person_attributes: CollectionProperty(name="Person Attributes", type=Attribute)
|
||||
@@ -81,24 +100,28 @@ class BIMOwnerProperties(PropertyGroup):
|
||||
facsimile_numbers: CollectionProperty(type=StrProperty, name="Facsimile Numbers")
|
||||
electronic_mail_addresses: CollectionProperty(type=StrProperty, name="Emails")
|
||||
messaging_ids: CollectionProperty(type=StrProperty, name="IMs")
|
||||
user_person: EnumProperty(items=get_user_person, name="Person")
|
||||
user_organisation: EnumProperty(items=get_user_organisation, name="Organisation")
|
||||
user_person: EnumProperty(
|
||||
items=get_user_person, name="Person", description="This entity represents an individual human being."
|
||||
)
|
||||
user_organisation: EnumProperty(
|
||||
items=get_user_organisation,
|
||||
name="Organisation",
|
||||
description="A named and structured grouping with a corporate identity.",
|
||||
)
|
||||
active_user_id: IntProperty(name="Active User Id")
|
||||
active_actor_id: IntProperty(name="Active Actor Id")
|
||||
actor_attributes: CollectionProperty(name="Actor Attributes", type=Attribute)
|
||||
actor_class: EnumProperty(
|
||||
items=(("IfcActor", "Actor", ""), ("IfcOccupant", "Occupant", "")),
|
||||
items=get_actor_class_enum,
|
||||
name="Actor Type",
|
||||
update=update_actor_class,
|
||||
)
|
||||
actor_type: EnumProperty(
|
||||
items=(
|
||||
("IfcPerson", "Person", ""),
|
||||
("IfcOrganization", "Organisation", ""),
|
||||
("IfcPersonAndOrganization", "User", ""),
|
||||
),
|
||||
items=get_actor_type_enum,
|
||||
name="Actor Type",
|
||||
update=update_actor_type,
|
||||
)
|
||||
the_actor: EnumProperty(items=get_the_actor, name="Actor")
|
||||
the_actor: EnumProperty(
|
||||
items=get_the_actor, name="Actor", description="This entity represents an individual human being."
|
||||
)
|
||||
actor: EnumProperty(items=get_actor, name="Actor")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user