Compare commits

...

95 Commits

Author SHA1 Message Date
Dion Moult 0ee07fb8cc See #4240. Fix bug where 2D arbitrary profiles were invalid IFC (used 3D point lists not 2D) 2024-01-21 16:07:00 +11:00
Dirk Olbrich 36cf458498 docs: include doxygen-awesome as git submodule 2024-01-20 14:05:50 +01:00
Dirk Olbrich e7c25bf99f docs: add README with instructions 2024-01-20 14:05:50 +01:00
Dirk Olbrich 50a3c8c2f3 docs: modify doxygen config to new folder 2024-01-20 14:05:50 +01:00
Dirk Olbrich f18298d1fd git: clean up gitignore 2024-01-20 14:05:50 +01:00
Dirk Olbrich d4770bdee3 docs: move C++ API docs to subfolder 2024-01-20 14:05:50 +01:00
Dirk Olbrich 1fb107c361 docs: include doxygen-awesome theme 2024-01-20 14:05:50 +01:00
Dirk Olbrich 0fa1e2d585 docs: remove duplicate cmake for Sphinx setup 2024-01-20 14:05:50 +01:00
Dirk Olbrich dc13f4c5cc docs: remove geom setting docs and images
- these are duplicated in /src/ifcopenshell-python/docs/ifcopenshell/
2024-01-20 14:05:50 +01:00
Dirk Olbrich d197c3f49a docs: update doxygen file to 1.9.8 2024-01-20 14:05:50 +01:00
Dirk Olbrich c05200a5c6 docs: set LOOKUP_CACHE_SIZE 2024-01-20 14:05:50 +01:00
Dirk Olbrich 235bb198da docs: update Doxyfile to v1.9.4
- specify ./output/doxygen as main path for output
- use relative paths for subfolder output
2024-01-20 14:05:50 +01:00
Dion Moult 40f332d674 Fix #3947. Do slight 2mm offset to account for Z-fighting on 2D plan views and RCPs. 2024-01-20 21:53:34 +11:00
Dion Moult e8a272dc97 Fix #4213. Allow having separate windows to trigger drawing operators. 2024-01-20 15:06:47 +11:00
Dion Moult 0b308770a6 See #4216. Document using OpenCASCADE geometry serialisation to create geometry. 2024-01-20 14:35:52 +11:00
Dion Moult 96c8323a06 Fix #4221. Only load leaf rows with cost values. Createing a monetary measure with None results in a segfault on Windows. 2024-01-20 14:17:19 +11:00
Dion Moult 25573cd0e0 Fix #4236. Consolidate revert, reload IFC, and reload selected IFC operations.
Reload IFC incrementally (via diff) is moved to the debug panel since it is a dangerous advanced user command.
2024-01-20 13:15:59 +11:00
Dion Moult ff3c7786f6 Fix #4109. Write docs about geometry serialiser. 2024-01-19 23:18:38 +11:00
Dion Moult 5f4dfcf215 Fix #4231. Bug where you couldn't add a door to an element assembly. 2024-01-19 22:47:53 +11:00
Andrej730 9a89555a53 bbim_translations.py - remove operator context from operator tooltips
noticed editing translations from ui that operator tooltip doesn't appear in ui if tooltips has `msgctxt "Operator"`
2024-01-19 16:31:34 +05:00
Dion Moult fc69a2acbe Fix #4127. Bug where IfcTester couldn't save ODS report. 2024-01-19 22:25:03 +11:00
Andrej730 85a2a5ad12 fix build error 2024-01-19 15:11:59 +05:00
Andrej730 49c3d28a0b Merge branch 'localization' into v0.7.0 2024-01-19 15:02:01 +05:00
Andrej730 a8f20b2c6b drop bpy related translations.py<->.po stuff as we won't be using it 2024-01-19 15:00:41 +05:00
Andrej730 66fa1cedd9 run bbim_translations.py without bpy 2024-01-19 15:00:41 +05:00
Andrej730 eb971d3d73 save just a final dictionary to translations.py instead of tuples
We can do that since In our workflow we won't be converting translations.py to .po files, that way it will be much shorter as it's containing only translated strings and doesn't have strings sources.
2024-01-19 13:58:49 +05:00
Andrej730 a30a796c07 generate translations.py without bpy 2024-01-19 13:58:49 +05:00
Andrej730 9e46bacc12 gettext bug fix 2024-01-19 12:06:11 +05:00
Ioannis P. Christovasilis b33378e135 ifc2ca major update - todo: update readme file 2024-01-18 12:53:31 +01:00
Vukas Pajic d42e19f491 Fix #4226 2024-01-18 08:54:24 +01:00
Eduardo Schilling 2597827ab3 Roof slope as percentage property 2024-01-18 09:40:33 +11:00
Dion Moult 3f71efea6d Fix #4227. Add shapely as an optional dependency for IOS-Python 2024-01-18 09:22:35 +11:00
Andrej730 2d279b7a7f update blenderbim build to include translation data 2024-01-17 17:18:04 +05:00
Andrej730 e5fe039fd0 expose current blender locale in bbim translations ui 2024-01-17 17:18:04 +05:00
Andrej730 9e14829cc1 bbim messages parser to also parse gettext 2024-01-17 17:10:26 +05:00
Andrej730 e867602d9d keep order of strings in .pot as they were parsed and store source filepaths 2024-01-17 17:10:26 +05:00
Andrej730 aecc3d56b8 switch to our own string parser instead of blender's 2024-01-17 17:10:26 +05:00
Andrej730 e3f5d3d2b7 rename BlenderBIM translation script/addon 2024-01-17 17:10:26 +05:00
Thomas Krijnen 65f8b1a532 #4222 template kw 2024-01-17 11:10:06 +01:00
Thomas Krijnen 96b4df10ba #4222 typename keywords 2024-01-17 11:02:02 +01:00
Andrej730 98b56aea42 allow missing translations.py as not everyone will need translations for BBIM 2024-01-17 12:07:02 +05:00
Andrej730 ba6ba29da8 auto enable "Manage UI Translations" addon if it's not enabled 2024-01-16 17:34:24 +05:00
Andrej730 1112b102f7 add without-blender-way to generate translations module from po files
though it does require `bpy` module which is available only for python 3.10.
Actually, `bpy` module by itself is not necessary - we just need `bl_i18n_utils` that becomes available after `import `bpy`. So alternatively we can just download that module from Blender https://projects.blender.org/blender/blender/src/branch/main/scripts/modules/bl_i18n_utils
2024-01-16 17:06:43 +05:00
Andrej730 04b8172ea9 black format 2024-01-16 15:28:55 +05:00
Andrej730 f5f3edbc79 check if translation ui is loaded by current Blender session instead of current Blender file
as it's more reliable and thing could change after Blender restart
2024-01-16 15:27:43 +05:00
Andrej730 30029e3dc7 expose i18n path in translator UI
removed bim.open_po_directory operator as it's now available just from alt-clicking on the path browsing button
2024-01-16 15:24:03 +05:00
Andrej730 ed836f5b4a Update bbim_setup_translations.py 2024-01-16 15:18:05 +05:00
Andrej730 a88b708442 fix issues with empty paths accepted as valid paths 2024-01-16 15:15:11 +05:00
Andrej730 764c3bbb85 separate developer/translator ui 2024-01-16 15:08:55 +05:00
Andrej730 382d3f1f9f generate pot file too converting translations.py 2024-01-16 15:04:02 +05:00
Thomas Krijnen c92f607a15 #4222 sprinkle with template keywords 2024-01-16 09:56:50 +01:00
Thomas Krijnen b57b7080fd #4220 weakref to file 2024-01-16 09:44:04 +01:00
Thomas Krijnen 860ff4e513 #4220 Update file.py 2024-01-16 09:31:35 +01:00
Andrej730 3546f13593 change test addon name from localization_test 2024-01-16 11:42:34 +05:00
Dion Moult 663966e08d Fix #4089. Document support for Linux AArch64 on Conda. 2024-01-16 11:19:17 +11:00
Dion Moult f077ebdc81 Fix #4211. Minor documentation fix that clarifies upstream bug about whether qto can be assigned to types. 2024-01-16 10:56:35 +11:00
Dion Moult eb888fd116 Fix #4223. Basic lint error. 2024-01-16 10:50:43 +11:00
Dion Moult 40b6b3bf17 Fix #4224. Fix sequencing lint bugs. 2024-01-16 10:47:50 +11:00
Dion Moult 2ef2f923d0 Fix #4219. Update documentation for creating a new model. 2024-01-16 10:45:13 +11:00
Bruno Postle 75fab58831 IfcGit fixes suggested by ruff 2024-01-15 22:16:19 +00:00
Andrej730 ce26c6e19b BBIM - upgrade to IFC4X3 on loading projects
Added "Upgrate to IFC4X3" button on loading new projects to migrate them to IFC4x3 as Ifc4x3 got approved now will be used more widely.

https://i.imgur.com/mHSIV7T.png
2024-01-15 17:39:49 +05:00
Andrej730 8728fa38b9 Migrator - basic support for migrating attributes ifc4 -> ifc4x3
And added one of the IfcProperty renamed attribute as an example
2024-01-15 17:39:49 +05:00
Dion Moult 3037795199 Fix #4139. Bulk purge orphaned classes and move advanced operators to debug panel. 2024-01-15 23:16:42 +11:00
Andrej730 39ee9cd8b1 Migrator - display some error message for unhandled schemas
Noticed it was resulting in error below migrating to IFC4X3 in some cases as we don't have a mapping for IFC4X3 yet.
```
    if value is None and not attribute.optional():
UnboundLocalError: local variable 'value' referenced before assignment
```
2024-01-15 16:16:29 +05:00
Andrej730 b375bbf558 black format 2024-01-15 16:16:29 +05:00
Thomas Krijnen f2cae79dea #4220 Update entity_instance.py 2024-01-15 12:01:18 +01:00
Andrej730 aadc77f6b6 minor fix for bim.create_all_shapes printed messages 2024-01-15 14:44:40 +05:00
Andrej730 35223d968c semi-automatic setup for necessary directories 2024-01-15 14:43:23 +05:00
Andrej730 e106dc4502 register translations dict only if addon is already loaded 2024-01-15 13:39:27 +05:00
Andrej730 2995b73c2d open .po directory operator 2024-01-15 13:39:27 +05:00
Andrej730 770bc6ea61 Operator to disable BBIM and restart Blender
as it's needed to reload the translations

Update bbim_setup_translations.py

Update bbim_setup_translations.py
2024-01-15 13:38:34 +05:00
Andrej730 f5736a7144 convert setup_translations to blender addon for UI 2024-01-15 12:18:53 +05:00
Dion Moult b4120b1cab Fix #4173. Don't recreate boundaries of building elements that already exist.
Inline opening processing. Minor improvement to detecting attributes for int/ext and physical/virtual.
2024-01-15 15:28:52 +11:00
Dirk Olbrich f44ca67ab9 readme: fix listed ifc version, add link to IFC4x3 docs 2024-01-15 09:20:15 +11:00
Dion Moult 77eae1c8e8 Fix #4195. Forgot to add UI for contains selector. 2024-01-14 23:20:06 +11:00
Dion Moult 02d79f0687 See #4173. Prototype new approach to generating 1st level space boundaries. 2024-01-14 23:12:14 +11:00
mohamadalbaaj 83d364574a Fixing Dimension Mismatch in Matrix Multiplication 2024-01-14 15:08:08 +11:00
Dion Moult 688d2b7bd1 Minor test fix 2024-01-13 16:49:27 +11:00
Dion Moult bf35239979 Fix bug where UI didn't show applicable psets to specific predefined types 2024-01-13 16:49:02 +11:00
Dion Moult ca8e014647 Fix #4195. Fix regressions from f840da3 and add support for "contains string" comparisons. 2024-01-13 15:58:57 +11:00
Dion Moult b5f63ec4f3 Fix #4196. Settings are now saved when you save Spreadsheet export JSON files. Also a new setting to generate an SVG alongside the spreadsheet (intended to be used as a schedule placed on a sheet). 2024-01-13 15:03:51 +11:00
Dion Moult aec2141128 Minor fix 2024-01-13 00:00:58 +11:00
Dion Moult 778344c804 Fix #4197. URLs in SVG schedules are now clickable. 2024-01-12 23:44:45 +11:00
Dion Moult 3ede9b254f Fix #4199. Bug where external references on sheets weren't properly referenced leading to no names. 2024-01-12 19:48:32 +11:00
Dion Moult 8df40dc4a6 Fix #4203. Fix bug where removing nested cost items left orphaned relationships. 2024-01-12 18:14:49 +11:00
Andrej730 6569d0031f monkey patch blender's dump_addon_messages
The way blender gather messages to translate is:
1) disable the addon
2) parse all available strings
3) enable the addon
4) parse all strings available strings again and subtract the ones gathered at step 2.

But due Blender bug (https://projects.blender.org/blender/blender/issues/116579) not all addon parts unregistered at step 1, so some addon's strings left out and gathered at step 2 and then subtracted at step 4. At the end, we loose them and they will be missing from resulting translations files.

I've monkey patched dump_addon_messages so it now works a bit more safe:
0) It expects addon to be disabled and Blender restarted after
1) Gather all Blender strings
2) Enable addon
3) Gather all Blender strings again and subtract strings from step 1.
2024-01-09 16:54:58 +05:00
Andrej730 bfaa61e592 setup_translations - original dump_addon_messages
same as with dump_py_messages - this commit is needed to track monkey patch changes later on
2024-01-09 16:46:22 +05:00
Andrej730 1a09ce40e9 fix error converting translations to .po if .po files already exist 2024-01-09 16:00:21 +05:00
Andrej730 ff251b7a6a monkey patch dump_py_messages to ignore blenderbim dependencies 2023-12-29 17:36:12 +05:00
Andrej730 dbce57ffc5 setup_translations - original dump_py_messages
as we're going to monkey patch it and this commit is needed to keep the history of changes
2023-12-29 17:36:12 +05:00
Andrej730 8ff9bcef32 update translations in current blender session 2023-12-29 17:13:43 +05:00
Andrej730 82b2006686 setup_translations.py - use just 1 directory to store .po files
and use temp directory to store .po files for ui_translate.export/import
2023-12-29 17:13:43 +05:00
Andrej730 ca4181c714 fixed typo 2023-12-29 16:58:31 +05:00
Andrej730 3928dd7c9c BBIM - register translation strings #889 2023-12-29 15:33:38 +05:00
Andrej730 925964ee2a BBIM script to reload/export/import translation strings #889 2023-12-29 15:33:30 +05:00
102 changed files with 4801 additions and 3170 deletions
+4 -12
View File
@@ -7,34 +7,26 @@
# output directories
/cmake/out/
/docs/out/
/src/examples/out/
/src/ifcmax/out/
/src/ifcwrap/out/
/src/qtviewer/out/
/win/BuildDepsCache*.txt
# IfcExpressParser residue
/src/ifcexpressparser/express_parser.py
# General Python residue
__pycache__
*.py.bak
# Visual Studio Code files
.vscode
.vs
# PyCharm files
.idea
#Virtual Env Files
Pipfile
Pipfile.lock
# Docs
/docs/output
/docs/rst_files
/docs/doxygen
/src/ifcblenderexport/docs/_build
/docs/cpp-api/output
# gettext binary translation files
*.mo
+4 -1
View File
@@ -10,4 +10,7 @@
url = https://github.com/IfcOpenShell/svgfill
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
path = src/ifcopenshell-python/test/Sample-BIM-Files
url = https://github.com/IfcOpenShell/ids-test-files
url = https://github.com/IfcOpenShell/ids-test-files
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
path = docs/cpp-api/assets/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git
+8 -7
View File
@@ -7,13 +7,13 @@ IfcOpenShell
</p>
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, IFC4x2, and IFC4x3. Extensive geometric support
parsing support is provided for [IFC2x3 TC1], [IFC4 Add2 TC1], IFC4x1, IFC4x2, and [IFC4x3 Add2]. 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.
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:
@@ -32,8 +32,8 @@ For more information, see:
| Anaconda Daily Build | [![Anaconda-Server Badge](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell)](https://anaconda.org/ifcopenshell/ifcopenshell) |
| Anaconda v0.7.0 Stable | [![Anaconda-Server Badge](https://img.shields.io/conda/vn/conda-forge/ifcopenshell)](https://anaconda.org/conda-forge/ifcopenshell) |
| PyPi Daily Build | [![PyPi Badge](https://img.shields.io/pypi/v/ifcopenshell)](https://pypi.org/project/ifcopenshell/) |
| ArchLinux AUR Package Stable | [![AUR Badge](https://img.shields.io/aur/version/ifcopenshell)](https://aur.archlinux.org/packages/ifcopenshell) |
| ArchLinux AUR Package git | [![AUR Badge](https://img.shields.io/aur/version/ifcopenshell-git)](https://aur.archlinux.org/packages/ifcopenshell-git) |
| ArchLinux AUR Package Stable | [![AUR Badge](https://img.shields.io/aur/version/ifcopenshell)](https://aur.archlinux.org/packages/ifcopenshell) |
| ArchLinux AUR Package git | [![AUR Badge](https://img.shields.io/aur/version/ifcopenshell-git)](https://aur.archlinux.org/packages/ifcopenshell-git) |
| BlenderBIM Add-on Chocolatey (under moderation) | [![Chocolatey Badge](https://img.shields.io/chocolatey/v/blenderbim-nightly)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| Sponsor development on OpenCollective | [![Financial Contributors](https://opencollective.com/opensourcebim/tiers/badge.svg)](https://opencollective.com/opensourcebim/) |
| Docker hub | [![Docker Pulls](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell)](https://hub.docker.com/r/aecgeeks/ifcopenshell) |
@@ -44,7 +44,7 @@ Contents
Those marked with an asterisk are part of IfcOpenShell.
| 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 |
@@ -77,6 +77,7 @@ Those marked with an asterisk are part of IfcOpenShell.
[IFC]: https://technical.buildingsmart.org/standards/ifc/ "IFC"
[IFC2x3 TC1]: https://standards.buildingsmart.org/IFC/RELEASE/IFC2x3/TC1/HTML/ "IFC2x3 TC1"
[IFC4 Add2 TC1]: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/ "IFC4 Add2 TC1"
[IFC4x3 Add2]: https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/ "IFC4x3 Add2"
[Visual Studio]: https://www.visualstudio.com/ "Visual Studio"
[Visual C++ Build Tools]: http://landinghub.visualstudio.com/visual-cpp-build-tools "Visual C++ Build Tools"
[MSYS2]: https://msys2.github.io/ "MSYS2"
-11
View File
@@ -1,11 +0,0 @@
#Look for an executable called sphinx-build
find_program(SPHINX_EXECUTABLE
NAMES sphinx-build
DOC "Path to sphinx-build executable")
include(FindPackageHandleStandardArgs)
#Handle standard arguments to find_package like REQUIRED and QUIET
find_package_handle_standard_args(Sphinx
"Failed to find sphinx-build executable"
SPHINX_EXECUTABLE)
-11
View File
@@ -1,11 +0,0 @@
#Look for an executable called sphinx-build
find_program(SPHINX_EXECUTABLE
NAMES sphinx-build
DOC "Path to sphinx-build executable")
include(FindPackageHandleStandardArgs)
#Handle standard arguments to find_package like REQUIRED and QUIET
find_package_handle_standard_args(Sphinx
"Failed to find sphinx-build executable"
SPHINX_EXECUTABLE)
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
# IfcOpenShell C++ API documentation
This folder contains the setup to build the IfcOpenShell C++ API documentation from the source code.
## Generating the documentation
> Prerequisites:
>
> Make sure to have [Doxygen](https://www.doxygen.nl) and [Graphviz](https://graphviz.org) installed into your `$PATH` variable.
>
> The documentation also use the [doxygen-awesome](https://jothepro.github.io/doxygen-awesome-css) theme as a git submodule.
Build with the command (from within the `/docs/cpp-api` folder):
```shell
$ doxygen
```
To include the current git commit hash into the build documentation, use the following command:
```shell
$ PROJECT_NUMBER=$(git rev-parse --short HEAD) doxygen
```
This will extract the current commit hash in short version and sets the propper ENV variable used by doxygen.
The generation of the documentation might take a while depending on your systems hardware, as it is configured to generate the Class graphs using .
The resulting documentation is located unter `/cpp-api/output/html` and can be directly accessed with your browser:
```shell
$ open ./output/html/index.html
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

-240
View File
@@ -1,240 +0,0 @@
APPLY_DEFAULT_MATERIALS
-----------------------
Given the command invocation:
Duplex_A_20110907_optimized.ifc d.dae -yv --include attribute GlobalId 3bXiCStxP6Fgxdej$yc50U
You will find log messages along the lines of
[Warning] {3bXiCStxP6Fgxdej$yc50U} No material and surface styles for:
#333=IfcCovering('3bXiCStxP6Fgxdej$yc50U',#1,'Compound Ceiling:Gypsum Board:187483',$,'Compound Ceiling:Gypsum Board',#17840,#17052,'187483',.CEILING.)
This means that there is no IfcStyledItem associated to the representation items and that the element does not have an IfcMaterial association with IfcMaterialRepresentation from which we can derive a style (colour) for the element.
The interactive session below shows how with this setting enabled you will get a default generated material from the IFC element entity type and material indices of 0 pointing to that. With this setting disabled the material index would be -1 to indicate a missing style. Note that there is one material index for every triangle in the list of `shp.geometry.faces`.
>>> import ifcopenshell, ifcopenshell.geom
>>> f = ifcopenshell.open("Duplex_A_20110907_optimized.ifc")
>>> s = ifcopenshell.geom.settings()
>>> c = f["3bXiCStxP6Fgxdej$yc50U"]
>>>
>>> shp = ifcopenshell.geom.create_shape(s, c)
>>> shp.geometry.material_ids
(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)
>>> [(m.name, m.diffuse) for m in shp.geometry.materials]
[]
>>>
>>> s.set(s.APPLY_DEFAULT_MATERIALS, True)
>>> shp = ifcopenshell.geom.create_shape(s, c)
>>> shp.geometry.material_ids
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
>>> [(m.name, m.diffuse) for m in shp.geometry.materials]
[('IfcCovering', (0.7, 0.7, 0.7))]
This is enabled by default for the IfcConvert serializers as they will not gracefully handle -1 material indices and allows users to quickly assign colours based on entity types in their modelling applications.
APPLY_LAYERSETS
---------------
This setting is available in IfcConvert as `--enable-layerset-slicing`.
For IfcWall and IfcSlab elements, takes the associated IfcMaterialLayerSet and builds a set of surfaces to segment the building element geometry.
Note that enabling this settings is computationally intensive as it involves 3D Boolean operations.
Duplex_A_20110907_optimized.ifc d1.dae -yv --include attribute GlobalId 2O2Fr$t4X7Zf8NOew3FNr2
![Image of d1.dae](images/settings-1.png)
Duplex_A_20110907_optimized.ifc d2.dae --enable-layerset-slicing -yv --include attribute GlobalId 2O2Fr$t4X7Zf8NOew3FNr2
![Image of d2.dae](images/settings-2.png)
BUILDING_LOCAL_PLACEMENT
------------------------
This setting is available in IfcConvert using `--building-local-placement`.
In the typical IfcSite > IfcBuilding > IfcBuildingStorey > ... hierarchy of elements, don't incorporate the ObjectPlacement of the IfcBuilding and above in the placement of elements in the output. This is useful when there is a large offset in this placement that reduces precision in further processing.
CONVERT_BACK_UNITS
------------------
This setting is available in IfcConvert using `--convert-back-units`.
Internally IfcOpenShell uses meters as the global length unit to do calculations. This setting restores the coordinate positions after conversion by multiplying the factor of the IfcUnit with UnitType=LENGTHUNIT into the output geometry coordinate values.
DISABLE_OPENING_SUBTRACTIONS
----------------------------
This setting is available in IfcConvert using `--disable-opening-subtraction`.
As in most viewer applications, IfcOpeningElement geometry is subtracted from their host elements. This setting disables this behavior.
Duplex_A_20110907_optimized.ifc d1.dae -yv --include attribute GlobalId 2O2Fr$t4X7Zf8NOew3FNr2
![Image of d1.dae](images/settings-1.png)
Duplex_A_20110907_optimized.ifc d3.dae --disable-opening-subtraction -yv --include attribute GlobalId 2O2Fr$t4X7Zf8NOew3FNr2
![Image of d3.dae](images/settings-3.png)
Note that disabling this settings will reduce processing time and improve robustness as it involves 3D Boolean operations.
DISABLE_TRIANGULATION
---------------------
To be used in conjunction with `USE_BREP_DATA`. Do not apply the triangulation and - when `USE_BREP_DATA` is set - return a OpenCASCADE serialized TopoDS_Shape from `create_shape()` and `iterator`.
>>> import ifcopenshell, ifcopenshell.geom
>>> s = ifcopenshell.geom.settings()
>>> s.set(s.DISABLE_TRIANGULATION, True)
>>> s.set(s.USE_BREP_DATA, True)
>>> f = ifcopenshell.open("Duplex_A_20110907_optimized.ifc")
>>> c = f["3bXiCStxP6Fgxdej$yc50U"]
>>> shp = ifcopenshell.geom.create_shape(s, c)
>>> print(shp.geometry.brep_data)
CASCADE Topology V1, (c) Matra-Datavision
Locations 0
Curve2ds 0
Curves 12
1 4.6750000000000034 -8.0749999999999904 2.657 -2.0455514041918775e-15 -1 0
1 4.6750000000000034 -8.0749999999999904 2.657 1 -3.435893306383461e-15 0
1 6.2260000000000044 -8.0749999999999957 2.657 -2.0455514041918724e-15 -1 0
1 6.226 -10.246000000000031 2.657 -1 6.8717866127669219e-15 0
...
EDGE_ARROWS
-----------
When `INCLUDE_CURVES` is true and geometric elements include curves (such as the wall axis), add arrow heads to the edges to indicate direction of the curve.
Duplex_A_20110907_optimized.ifc d4.dae --model --plan --edge-arrows -yv --include attribute GlobalId 2O2Fr$t4X7Zf8NOew3FNr2
![Image of d4.dae](images/settings-4.png)
EXCLUDE_SOLIDS_AND_SURFACES
---------------------------
Exclude faces, shells and solids from geometrical output. Implied when using `--plan` without `--model` in IfcConvert.
FASTER_BOOLEANS
---------------
NOTE: Only applicable when using OCCT 6.9 and earlier.
This setting is available in IfcConvert using `--merge-boolean-operands`.
Fuse the collection of all boolean operands into a single union before applying the boolean subtraction, as opposed to doing individual subtractions. This likely improves performance. From OCCT 7.0 onwards the boolean operations with multiple arguments is used.
GENERATE_UVS
------------
This setting is available in IfcConvert using `--generate-uvs`.
Applies a box projection on the generated geometry for the element to obtain UV coordinates. This is purely generated, it does not involve texture coordinates stored in the IFC model.
Duplex_A_20110907_optimized.ifc d5.dae --generate-uvs -yv --include attribute GlobalId 2O2Fr$t4X7Zf8NOew3FNr2
![Image of d5.dae with a UV grid applied in Blender](images/settings-5.png)
INCLUDE_CURVES
--------------
This setting is available in IfcConvert using `--plan`.
Include edge and wire geometries in the geometric output.
LAYERSET_FIRST
--------------
This setting is available in IfcConvert using `--layerset-first`.
When not using APPLY_LAYERSETS, take the first material layer from the set to use as the material for the overall element.
NO_NORMALS
----------
This setting is available in IfcConvert using `--no-normals`.
Do not emit normals on geometric output
SEARCH_FLOOR
------------
Note: Only applicable to Collada .DAE output when used from IfcConvert.
This setting is available in IfcConvert using `--use-element-hierarchy`.
Include the spatial hierarchy in the elements.
SEW_SHELLS
----------
This setting is available in IfcConvert using `--orient-shells`.
Re-orient or sew connected face sets to have a consistent outwards orientation.
SITE_LOCAL_PLACEMENT
--------------------
This setting is available in IfcConvert using `--site-local-placement`.
See `BUILDING_LOCAL_PLACEMENT`, but exclude also the ObjectPlacement of the IfcSite.
USE_BREP_DATA
-------------
See `DISABLE_TRIANGULATION`.
USE_PYTHON_OPENCASCADE
----------------------
Note: Only available when an import of `OCC.Core.BRepTools` or `OCC.BRepTools` succeeds.
This implies `USE_WORLD_COORDS` `USE_BREP_DATA` and `DISABLE_TRIANGULATION`. The serialized TopoDS_Shape of `USE_BREP_DATA` is deserialized by Python OpenCASCADE.
USE_WORLD_COORDS
----------------
Apply the ObjectPlacement of the building elements to the geometric output. This is implied when using the Wavefront .OBJ output in IfcConvert. Note that this also eliminates the possibility for geometric elements to point to the same interpreted geometry result.
VALIDATE_QUANTITIES
-------------------
This setting is available in IfcConvert using `--validate`.
Running IfcConvert with `--validate` will set a non-zero exit code when ever a log message with severity equal or greater than ERROR has been emitted.
Currently for internal use only. For every building element geometry converted, looks for an associated quantity set where the OwnerHistory's organization name is IfcOpenShell. And looks for the quantities "Total Surface Area", "Volume", "Shape Validation Properties.Surface Genus" and validates these according to the interpreted geometry definition. Emit Logger::Error when calculated values are outside of the tolerance range for the value stored in the model.
WELD_VERTICES
-------------
Note: In Python, this setting is *on* by default.
Note: This setting only affects triangulated output.
This setting is available in IfcConvert using `--weld-vertices`.
Discards normals and joins vertices solely based on position. This is useful when output is to be modified in a modeling application.
>>> import ifcopenshell, ifcopenshell.geom
>>> s = ifcopenshell.geom.settings()
>>> s.set(s.WELD_VERTICES, False)
>>> f = ifcopenshell.open("Duplex_A_20110907_optimized.ifc")
>>> c = f["3bXiCStxP6Fgxdej$yc50U"]
>>> shp = ifcopenshell.geom.create_shape(s, c)
>>> shp.geometry.verts
(4.675000000000003, -8.07499999999999, 2.657, 4.674999999999999, -10.24600000000002, 2.657, 6.226000000000004, -8.074999999999996, 2.657, 6.226, -10.24600000000003, 2.657, 4.675000000000003, -8.07499999999999, 2.6, 4.674999999999999, -10.24600000000002, 2.6, 6.226000000000004, -8.074999999999996, 2.6, 6.226, -10.24600000000003, 2.6, 4.674999999999999, -10.24600000000002, 2.657, 4.674999999999999, -10.24600000000002, 2.6, 4.675000000000003, -8.07499999999999, 2.657, 4.675000000000003, -8.07499999999999, 2.6, 6.226, -10.24600000000003, 2.657, 4.674999999999999, -10.24600000000002, 2.657, 6.226, -10.24600000000003, 2.6, 4.674999999999999, -10.24600000000002, 2.6, 6.226000000000004, -8.074999999999996, 2.657, 6.226, -10.24600000000003, 2.657, 6.226000000000004, -8.074999999999996, 2.6, 6.226, -10.24600000000003, 2.6, 4.675000000000003, -8.07499999999999, 2.657, 4.675000000000003, -8.07499999999999, 2.6, 6.226000000000004, -8.074999999999996, 2.657, 6.226000000000004, -8.074999999999996, 2.6)
>>> shp.geometry.normals
(3.059754518198021e-17, 0.0, -1.0, 3.059754518198021e-17, 0.0, -1.0, 3.059754518198021e-17, 0.0, -1.0, 3.059754518198021e-17, 0.0, -1.0, 2.110175529791737e-16, 0.0, -1.0, 2.110175529791737e-16, 0.0, -1.0, 2.110175529791737e-16, 0.0, -1.0, 2.110175529791737e-16, 0.0, -1.0, -1.0, 1.79434333701042e-15, 0.0, -1.0, 1.79434333701042e-15, 0.0, -1.0, 1.79434333701042e-15, 0.0, -1.0, 1.79434333701042e-15, 0.0, 6.8717866127669046e-15, 1.0, 0.0, 6.8717866127669046e-15, 1.0, 0.0, 6.8717866127669046e-15, 1.0, 0.0, 6.8717866127669046e-15, 1.0, 0.0, -1.0, 1.79434333701042e-15, 0.0, -1.0, 1.79434333701042e-15, 0.0, -1.0, 1.79434333701042e-15, 0.0, -1.0, 1.79434333701042e-15, 0.0, 3.4358933063834523e-15, 1.0, 0.0, 3.4358933063834523e-15, 1.0, 0.0, 3.4358933063834523e-15, 1.0, 0.0, 3.4358933063834523e-15, 1.0, 0.0)
>>>
>>> s.set(s.WELD_VERTICES, True)
>>> shp = ifcopenshell.geom.create_shape(s, c)
>>> shp.geometry.verts
(4.675000000000003, -8.07499999999999, 2.657, 4.674999999999999, -10.24600000000002, 2.657, 6.226000000000004, -8.074999999999996, 2.657, 6.226, -10.24600000000003, 2.657, 4.675000000000003, -8.07499999999999, 2.6, 4.674999999999999, -10.24600000000002, 2.6, 6.226000000000004, -8.074999999999996, 2.6, 6.226, -10.24600000000003, 2.6)
>>> shp.geometry.normals
()
+5
View File
@@ -696,6 +696,11 @@ endif
# Required for Desktop icon and file association
cp -r blenderbim/libs/desktop dist/blenderbim/libs/
# generate translations module for BBIM build
git clone https://github.com/IfcOpenShell/blenderbim-translations.git dist/working
python scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim"
rm -rf dist/working
# Remove dependencies also bundled with Blender
rm -rf dist/blenderbim/libs/site/packages/numpy
rm -rf dist/blenderbim/libs/site/packages/numpy.libs
+15 -8
View File
@@ -24,6 +24,10 @@ import blenderbim
import importlib
from . import handler, ui, prop, operator, helper
try:
from blenderbim.translations import translations_dict
except ImportError:
translations_dict = {}
cwd = os.path.dirname(os.path.realpath(__file__))
modules = {
@@ -92,23 +96,22 @@ for name in modules.keys():
classes = [
operator.AddIfcFile,
operator.BIM_OT_add_section_plane,
operator.BIM_OT_open_webbrowser,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_show_description,
operator.EditBlenderCollection,
operator.FileAssociate,
operator.FileUnassociate,
operator.OpenUpstream,
operator.OpenUri,
operator.SwitchTab,
operator.SetTab,
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.SelectDataDir,
operator.SelectIfcFile,
operator.ReloadSelectedIfcFile,
operator.SelectSchemaDir,
operator.FileAssociate,
operator.FileUnassociate,
operator.SelectURIAttribute,
operator.EditBlenderCollection,
operator.BIM_OT_open_webbrowser,
operator.BIM_OT_show_description,
operator.SetTab,
operator.SwitchTab,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
prop.ObjProperty,
@@ -250,6 +253,8 @@ def register():
except:
pass
bpy.app.translations.register("blenderbim", translations_dict)
def unregister():
global icons
@@ -292,3 +297,5 @@ def unregister():
bpy.utils.unregister_class(override_panel)
bpy.utils.register_class(original_panel)
del overridden_scene_panels[panel]
bpy.app.translations.unregister("blenderbim")
@@ -22,6 +22,7 @@ import logging
import shapely
import mathutils
import numpy as np
import multiprocessing
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcopenshell.util.shape
@@ -30,7 +31,7 @@ import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.bim.import_ifc as import_ifc
from math import pi, inf
from math import pi, inf, degrees, acos, radians
from mathutils import Vector, Matrix
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.decorator import ProfileDecorator
@@ -586,6 +587,11 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
)
if parent_boundary:
parent_boundaries.append(parent_boundary)
elif len(objs) == 1:
# New prototype, old code not yet removed. Still testing.
space = tool.Ifc.get_entity(objs[0])
if space.is_a("IfcSpace"):
self.auto_generate_boundaries(space, objs[0])
elif len(objs) == 1:
# Optionally the user may select just the space, and the building element shall be auto-detected
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
@@ -632,6 +638,227 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
obj = tool.Ifc.get_object(parent_boundary)
obj.select_set(True)
def auto_generate_boundaries(self, space, space_obj):
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = (
tool.Ifc.get().by_type("IfcWall")
+ tool.Ifc.get().by_type("IfcSlab")
+ tool.Ifc.get().by_type("IfcVirtualElement")
)
# Don't generate boundaries of building elements that we've already got bounaries for.
for boundary in space.BoundedBy:
if boundary.RelatedBuildingElement in building_elements:
building_elements.remove(boundary.RelatedBuildingElement)
# Create tree of gross shapes of all potential related building elements
include = building_elements + [space]
tree = ifcopenshell.geom.tree()
shapes = {}
settings = ifcopenshell.geom.settings()
settings.set(settings.STRICT_TOLERANCE, True)
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
if iterator.initialize():
while True:
tree.add_element(iterator.get_native())
shape = iterator.get()
shapes[shape.id] = shape
if not iterator.next():
break
# Spatially query all potential boundary elements via a 100mm extension of the space
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
if not building_elements:
return
# Create a dissolved bmesh for the space
space_bm = bmesh.new()
space_bm.from_mesh(space_obj.data)
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts, edges=space_bm.edges)
# Create dissolved bmeshes for all boundary elements
building_element_bms = {}
for building_element in building_elements:
bm = bmesh.new()
shape = shapes[building_element.id()]
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
for vert in verts:
bm.verts.new(Vector(vert))
bm.verts.ensure_lookup_table()
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
for face in faces:
bm.faces.new([bm.verts[i] for i in face])
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
bm.normal_update() # Needed so that dissolve_limit will work.
bmesh.ops.dissolve_limit(bm, angle_limit=radians(1), verts=bm.verts[:], edges=bm.edges[:])
bm.verts.ensure_lookup_table()
bm.faces.ensure_lookup_table()
building_element_bms[building_element.id()] = bm
# Compare space faces and building element faces to see if they relate to one another
for space_face in space_bm.faces:
space_face_normal = space_obj.matrix_world.to_3x3() @ space_face.normal
space_face_vert = space_obj.matrix_world @ space_face.verts[0].co
for building_element in building_elements:
for face in building_element_bms[building_element.id()].faces:
building_obj = tool.Ifc.get_object(building_element)
face_normal = building_obj.matrix_world.to_3x3() @ face.normal
angle = degrees(acos(max(min(space_face_normal.dot(face_normal), 1), -1)))
if tool.Cad.is_x(angle, 180, tolerance=2):
pass # Faces need to be parallel and have opposite normals to be related.
elif building_element.is_a("IfcVirtualElement") and tool.Cad.is_x(angle, 0, tolerance=2):
pass # Virtual elements only need to be parallel to be related, since they are planes.
else:
continue
# Both faces should be close to one another. Say within 50mm.
space_vert = building_obj.matrix_world.inverted() @ space_face_vert
dist = mathutils.geometry.distance_point_to_plane(space_vert, face.verts[0].co, face.normal)
if abs(dist) > 0.05:
continue
# Project the building element face onto the space face
space_face_verts = [v.co.copy() for v in space_face.verts]
space_face_matrix = self.get_face_matrix(*[v.copy() for v in space_face_verts[0:3]])
space_face_matrix_i = space_face_matrix.inverted()
space_face_polygon = shapely.Polygon(
[tuple((space_face_matrix_i @ v).xy) for v in space_face_verts]
)
space_matrix_world_i = space_obj.matrix_world.inverted()
face_verts = [space_matrix_world_i @ building_obj.matrix_world @ v.co.copy() for v in face.verts]
face_polygon = shapely.Polygon([tuple((space_face_matrix_i @ v).xy) for v in face_verts])
gross_boundary_polygon = space_face_polygon.intersection(face_polygon)
if type(gross_boundary_polygon) == shapely.GeometryCollection:
for geom in gross_boundary_polygon.geoms:
if type(geom) == shapely.Polygon:
gross_boundary_polygon = geom
break
if (
not (isinstance(gross_boundary_polygon, shapely.Polygon) and gross_boundary_polygon.is_valid)
or gross_boundary_polygon.is_empty
):
continue
# The gross boundary polygon may not be a true gross boundary since it
# may have openings already removed, such as in IFC4 Reference View. So
# we cheat by using the exterior boundary to mean "gross".
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
parent_boundary = tool.Ifc.run(
"root.create_entity", ifc_class=bpy.context.scene.BIMModelProperties.boundary_class
)
if building_element.is_a("IfcVirtualElement"):
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
else:
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
parent_boundary.InternalOrExternalBoundary = "NOTDEFINED"
if building_element.is_a("IfcWall"):
is_external = ifcopenshell.util.element.get_pset(
building_element, "Pset_WallCommon", "IsExternal"
)
if is_external is True:
parent_boundary.InternalOrExternalBoundary = "EXTERNAL"
elif is_external is False:
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
elif building_element.is_a("IfcSlab"):
predefined_type = ifcopenshell.util.element.get_predefined_type(building_element)
if predefined_type == "BASESLAB":
parent_boundary.InternalOrExternalBoundary = "EXTERNAL_EARTH"
else:
is_external = ifcopenshell.util.element.get_pset(
building_element, "Pset_SlabCommon", "IsExternal"
)
if is_external is True:
parent_boundary.InternalOrExternalBoundary = "EXTERNAL"
elif is_external is False:
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
parent_boundary.RelatingSpace = space
parent_boundary.RelatedBuildingElement = building_element
parent_boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
exterior_boundary_polygon, space_face_matrix
)
self.set_boundary_name(parent_boundary)
for rel in getattr(building_element, "HasOpenings", []):
opening = rel.RelatedOpeningElement
filling = opening.HasFillings[0].RelatedBuildingElement if opening.HasFillings else None
# Create shape of opening as a dissolved BMesh
settings = ifcopenshell.geom.settings()
settings.set(settings.STRICT_TOLERANCE, True)
shape = ifcopenshell.geom.create_shape(settings, opening)
m = shape.transformation.matrix.data
mat = Matrix(
(
[m[0], m[3], m[6], m[9]],
[m[1], m[4], m[7], m[10]],
[m[2], m[5], m[8], m[11]],
[0, 0, 0, 1],
)
)
opening_bm = bmesh.new()
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
for vert in verts:
opening_bm.verts.new(Vector(vert))
opening_bm.verts.ensure_lookup_table()
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
for face in faces:
opening_bm.faces.new([opening_bm.verts[i] for i in face])
opening_bm.verts.ensure_lookup_table()
opening_bm.faces.ensure_lookup_table()
opening_bm.normal_update() # Needed so that dissolve_limit will work.
bmesh.ops.dissolve_limit(
opening_bm, angle_limit=radians(1), verts=opening_bm.verts[:], edges=opening_bm.edges[:]
)
opening_bm.verts.ensure_lookup_table()
opening_bm.faces.ensure_lookup_table()
# Get relevant faces of BMesh that can turn into boundaries
opening_polygons = []
for opening_face in opening_bm.faces:
opening_face_normal = mat.to_3x3() @ opening_face.normal
angle = degrees(acos(max(min(opening_face_normal.dot(face_normal), 1), -1)))
if not tool.Cad.is_x(angle, 180, tolerance=2):
continue # Any non-parallel faces are not relevant
opening_face_verts = [space_matrix_world_i @ mat @ v.co.copy() for v in opening_face.verts]
polygon = shapely.Polygon([tuple((space_face_matrix_i @ v).xy) for v in opening_face_verts])
opening_polygons.append(polygon)
# Merge them all into a single opening polygon for our boundary
opening_polygon = shapely.ops.unary_union(opening_polygons)
# Only openings that are projected onto our exterior boundary are relevant.
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
continue
boundary = tool.Ifc.run(
"root.create_entity", ifc_class=bpy.context.scene.BIMModelProperties.boundary_class
)
boundary.RelatingSpace = space
boundary.RelatedBuildingElement = filling or opening
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
opening_polygon, space_face_matrix
)
if filling:
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
else:
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
boundary.InternalOrExternalBoundary = parent_boundary.InternalOrExternalBoundary
if boundary.is_a() != "IfcRelSpaceBoundary":
boundary.ParentBoundary = parent_boundary
self.set_boundary_name(boundary)
def create_element_boundary(
self, context, relating_space, relating_space_obj, related_building_element, related_building_element_obj
):
@@ -37,12 +37,10 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
object_type: bpy.props.StringProperty()
def _execute(self, context):
core.add_cost_schedule(
tool.Ifc,
name=self.name,
predefined_type=context.scene.BIMCostProperties.cost_schedule_predefined_types,
object_type=self.object_type,
)
predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types
if predefined_type == "USERDEFINED":
predefined_type = self.object_type
core.add_cost_schedule(tool.Ifc, name=self.name, predefined_type=predefined_type)
def draw(self, context):
layout = self.layout
@@ -24,6 +24,7 @@ import logging
import tempfile
import ifcopenshell
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.scheduler as scheduler
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.handler import refresh_ui_data
@@ -92,15 +93,26 @@ class ImportCsvAttributes(bpy.types.Operator):
data = json.load(open(self.filepath))
tool.Search.import_filter_query(data["query"], props.filter_groups)
for prop in [
"should_generate_svg",
"should_preserve_existing",
"include_global_id",
"null_value",
"empty_value",
"true_value",
"false_value",
"concat_value",
"csv_delimiter",
"format",
"csv_custom_delimiter",
]:
setattr(props, prop, data["settings"][prop])
props.csv_attributes.clear()
for attribute in data["attributes"]:
new = props.csv_attributes.add()
new.name = attribute["name"]
new.header = attribute["header"]
new.sort = attribute["sort"]
new.group = attribute["group"]
new.summary = attribute["summary"]
new.formatting = attribute["formatting"]
for prop in ["name", "header", "sort", "group", "summary", "formatting"]:
setattr(new, prop, attribute[prop])
return {"FINISHED"}
def invoke(self, context, event):
@@ -120,6 +132,22 @@ class ExportCsvAttributes(bpy.types.Operator):
def execute(self, context):
props = context.scene.CsvProperties
settings = {}
for prop in [
"should_generate_svg",
"should_preserve_existing",
"include_global_id",
"null_value",
"empty_value",
"true_value",
"false_value",
"concat_value",
"csv_delimiter",
"format",
"csv_custom_delimiter",
]:
settings[prop] = getattr(props, prop)
data = {
"query": tool.Search.export_filter_query(props.filter_groups),
"attributes": [
@@ -133,6 +161,7 @@ class ExportCsvAttributes(bpy.types.Operator):
}
for a in props.csv_attributes
],
"settings": settings,
}
with open(self.filepath, "w") as outfile:
@@ -213,6 +242,10 @@ class ExportIfcCsv(bpy.types.Operator):
summaries=summaries,
formatting=formatting,
)
if props.format != "csv" and props.should_generate_svg:
schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
return {"FINISHED"}
@@ -65,6 +65,7 @@ class CsvProperties(PropertyGroup):
ifc_selector: StringProperty(default="", name="IFC Selector")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
csv_attributes: CollectionProperty(name="CSV Attributes", type=CsvAttribute)
should_generate_svg: BoolProperty(default=False, name="Generate SVG")
should_preserve_existing: BoolProperty(default=False, name="Preserve Existing")
include_global_id: BoolProperty(default=True, name="Include GlobalId")
null_value: StringProperty(default="N/A", name="Null Value")
@@ -67,6 +67,8 @@ class BIM_PT_ifccsv(Panel):
row = layout.row(align=True)
row.prop(props, "csv_custom_delimiter")
row = layout.row()
row.prop(props, "should_generate_svg")
row = layout.row()
row.prop(props, "should_preserve_existing")
row = layout.row()
@@ -213,7 +213,7 @@ class CreateAllShapes(bpy.types.Operator):
settings_2d.set(settings_2d.INCLUDE_CURVES, True)
failures = []
excludes = () # For the developer to debug with
for i, element in enumerate(elements):
for i, element in enumerate(elements, 1):
if element.GlobalId in excludes:
continue
print(f"{i}/{total}:", element)
@@ -510,8 +510,116 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
"Will find all elements of class that have no inverse refernces and will remove them, use very carefully"
)
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def _execute(self, context):
props = context.scene.BIMDebugProperties
purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge)
self.report({"INFO"}, f"{purged_elements} unused elements found and removed.")
if props.ifc_class_purge:
purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge)
self.report({"INFO"}, f"{purged_elements} unused elements found and removed.")
tool.Ifc.get().write(self.filepath)
return
# A whitelisted class is a class that only contains simple data that
# has no meaning by itself (only has meaning when combined with other
# data). E.g. a colour by itself has no meaning so is whitelisted, but
# a material by itself may be part of your materials library.
# There are classes I don't know enough about to decide. Not whitelisting in case.
# IfcAlignmentParameterSegment
# IfcBoundaryCondition
# IfcStructuralConnectionCondition
# IfcStructuralLoad
whitelisted_classes = [
"IfcActorRole",
"IfcAddress",
"IfcApplication",
"IfcAppliedValue",
"IfcConnectionGeometry",
"IfcCoordinateReferenceSystem",
"IfcDerivedUnit",
"IfcDerivedUnitElement",
"IfcDimensionalExponents",
"IfcExternalReference",
"IfcGridAxis",
"IfcIrregularTimeSeriesValue",
"IfcLightDistributionData",
"IfcLightIntensityDistribution",
"IfcMeasureWithUnit",
"IfcMonetaryUnit",
"IfcNamedUnit",
"IfcObjectPlacement",
"IfcOrganization", # Should be referenced as part of an actor
"IfcOwnerHistory",
"IfcPerson", # Should be referenced as part of an actor
"IfcPersonAndOrganization", # Should be referenced as part of an actor
"IfcPhysicalQuantity",
"IfcPresentationItem",
"IfcProductDefinitionShape",
"IfcPropertyAbstraction",
"IfcRecurrencePattern",
"IfcReference",
"IfcRepresentation",
"IfcRepresentationContext",
"IfcRepresentationItem",
"IfcRepresentationMap",
"IfcPropertyDefinition", # A bit of a questionable one, and the odd one out from IfcRoot.
"IfcSchedulingTime",
"IfcShapeAspect",
"IfcTable",
"IfcTableColumn",
"IfcTableRow",
"IfcTextureCoordinateIndices",
"IfcTimePeriod",
"IfcTimeSeries",
"IfcTimeSeriesValue",
"IfcUnitAssignment",
"IfcVirtualGridIntersection",
]
total_purged = 0
while True:
total_batches = 0
print("*" * 100)
total_batch_purged = 0
for ifc_class in whitelisted_classes:
total_class_purged = 0
try:
elements = tool.Ifc.get().by_type(ifc_class)
except:
continue # Probably not in this schema?
to_purge = set()
for element in elements:
try:
if ifc_class == "IfcRepresentationItem" and element.is_a("IfcStyledItem") and element.Item:
continue
except:
to_purge.add(element.id()) # It's invalid, definitely purge it.
if tool.Ifc.get().get_total_inverses(element) == 0:
to_purge.add(element.id())
for element_id in to_purge:
try:
element = tool.Ifc.get().by_id(element_id)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), element)
total_class_purged += 1
except:
continue
if total_class_purged > 0:
total_batch_purged += total_class_purged
print(f"Auto purged {total_class_purged} {ifc_class}")
if total_batch_purged > 0:
total_purged += total_batch_purged
print(f"Auto purged in batch: {total_batch_purged}")
total_batches += 1
if total_batch_purged == 0:
break
elif total_batches > 20:
print("Finished 20 batches. Manually stopping in case of infinite loop.")
self.report({"INFO"}, f"Auto purged {total_purged} orphaned elements")
tool.Ifc.get().write(self.filepath)
@@ -37,7 +37,6 @@ 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)
@@ -45,6 +44,9 @@ class BIM_PT_debug(Panel):
row.operator("bim.parse_express", icon="IMPORT", text="")
row.operator("bim.select_express_file", icon="FILE_FOLDER", text="")
row = layout.row()
row.operator("bim.reload_ifc_file", text="Incrementally Reload Changes")
row = layout.row()
row.operator("bim.print_ifc_file")
@@ -87,6 +89,13 @@ class BIM_PT_debug(Panel):
row.prop(props, "display_type", text="")
row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
if context.active_object and context.active_object.data:
mprops = context.active_object.data.BIMMeshProperties
row = layout.row()
@@ -18,6 +18,7 @@
import os
import bpy
import json
import ifcopenshell.util.element
import ifcopenshell.util.representation
import blenderbim.tool as tool
@@ -494,6 +494,11 @@ class CreateDrawing(bpy.types.Operator):
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
if ifc.by_id(body_context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
geom_settings.set_context_ids(body_context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=elements
@@ -513,6 +518,11 @@ class CreateDrawing(bpy.types.Operator):
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
)
if ifc.by_id(annotation_context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
geom_settings.set_context_ids(annotation_context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=elements
@@ -1996,11 +2006,11 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
props = context.scene.DocProperties
active_reference = props.references[props.active_reference_index]
active_sheet = tool.Drawing.get_active_sheet(context)
reference = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
if tool.Ifc.get_schema() == "IFC2X3":
reference_location = tool.Drawing.get_path_with_ext(reference.DocumentReferences[0].Location, "svg")
extref_location = tool.Drawing.get_path_with_ext(extref.DocumentReferences[0].Location, "svg")
else:
reference_location = tool.Drawing.get_path_with_ext(reference.HasDocumentReferences[0].Location, "svg")
extref_location = tool.Drawing.get_path_with_ext(extref.HasDocumentReferences[0].Location, "svg")
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
if not sheet.is_a("IfcDocumentInformation"):
@@ -2008,30 +2018,30 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
references = tool.Drawing.get_document_references(sheet)
has_reference = False
has_extref = False
for reference in references:
if reference.Location == reference_location:
has_reference = True
if reference.Location == extref_location:
has_extref = True
break
if has_reference:
if has_extref:
return
if not tool.Drawing.does_file_exist(tool.Ifc.resolve_uri(reference_location)):
self.report({"ERROR"}, f"Cannot find reference svg by path {reference_location}.")
if not tool.Drawing.does_file_exist(tool.Ifc.resolve_uri(extref_location)):
self.report({"ERROR"}, f"Cannot find reference svg by path {extref_location}.")
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "REFERENCE")]) + 1),
"Location": reference_location,
"Location": extref_location,
"Description": "REFERENCE",
}
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
sheet_builder.add_document(reference, reference, sheet)
sheet_builder.add_document(reference, extref, sheet)
tool.Drawing.import_sheets()
@@ -74,7 +74,6 @@ class Scheduler:
column_dimensions = {k: v.width for k, v in sheet.column_dimensions.items()}
row_dimensions = {k: v.height for k, v in sheet.row_dimensions.items()}
print(row_dimensions)
default_width = 8.43 # 8.43 characters wide
default_height = 15 # 15pt seems to be the default height
@@ -83,7 +82,13 @@ class Scheduler:
pass
y = self.margin
for row in sheet.iter_rows():
rows = list(sheet.iter_rows())
total_rows = len(rows)
for i, row in enumerate(rows):
# The last row may contain only null values
if i == (total_rows - 1) and not [c for c in row if c.value is not None]:
continue
x = self.margin
for cell in row:
if isinstance(cell, openpyxl.cell.cell.MergedCell):
@@ -135,6 +140,10 @@ class Scheduler:
)
)
if not cell.value:
x += unmerged_width
continue
font_size = cell.font.size or 11 # 11pt default
font_size = font_size / FONT_SIZE_PT * FONT_SIZE # Magic?
@@ -528,6 +537,10 @@ class Scheduler:
if len(text_lines) == 1 and not wrap_text:
text_params.update(box_alignment_params)
text_tag = self.svg.text(text_lines[0], insert=(x, y), **(text_params))
if self.is_url(text_lines[0]):
anchor = self.svg.a(text_lines[0].strip())
anchor.add(text_tag)
text_tag = anchor
self.svg.add(text_tag)
return
@@ -562,11 +575,18 @@ class Scheduler:
tspan = self.svg.tspan(text_line, insert=(x, y), **text_params)
# doing it here and not in tspan constructor because constructor adds unnecessary spaces
tspan.update({"dy": f"{line_number}em"})
if self.is_url(text_line):
anchor = self.svg.a(text_line.strip())
anchor.add(tspan)
tspan = anchor
text_tag.add(tspan)
line_number += dy_dir
self.svg.add(text_tag)
def is_url(self, value):
return value.strip().startswith("http") and " " not in value.strip() and "://" in value # Wrong but fast
def convert_character_width_to_mm(self, value):
# Excel measures widths in terms of "number of characters of the
# default font", which is most commonly 11pt Calibri. The width of the
@@ -187,11 +187,11 @@ class SheetBuilder:
layout_tree.write(layout_path)
def add_document(self, reference, schedule, sheet):
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(schedule), "svg")
def add_document(self, reference, document, sheet):
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(document), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_schedule(schedule)
schedule_name = os.path.splitext(os.path.basename(view_path))[0]
tool.Drawing.create_svg_document(document)
document_name = os.path.splitext(os.path.basename(view_path))[0]
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
@@ -207,12 +207,12 @@ class SheetBuilder:
view_height = self.convert_to_mm(view_root.attrib.get("height"))
view = ET.SubElement(layout_root, "g")
view.attrib["data-type"] = "schedule"
view.attrib["data-id"] = str(reference.id())
view.attrib["data-schedule"] = str(schedule.id())
view.attrib["data-type"] = document.Scope.lower()
view.attrib["data-document"] = str(document.id())
foreground = ET.SubElement(view, "image")
foreground.attrib["data-type"] = "table"
foreground.attrib["data-type"] = "content"
foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir)
foreground.attrib["x"] = str(DEFAULT_POSITION.x)
foreground.attrib["y"] = str(DEFAULT_POSITION.y)
@@ -263,7 +263,7 @@ class SheetBuilder:
self.build_titleblock(root, sheet)
self.build_drawings(root, sheet)
self.build_schedules(root, sheet)
self.build_documents(root, sheet)
with open(sheet_path, "wb") as output:
tree.write(output)
@@ -390,11 +390,14 @@ class SheetBuilder:
for image in images:
view.remove(image)
def build_schedules(self, root, sheet):
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'):
def build_documents(self, root, sheet):
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
documents = schedules + references
for view in documents:
try:
reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"]))
document = tool.Ifc.get().by_id(int(view.attrib["data-document"]))
except:
# Perhaps the SVG has outdated content or is edited externally which we cannot control.
continue
@@ -405,7 +408,7 @@ class SheetBuilder:
view_title = None
for image in images:
if image.attrib["data-type"] == "table":
if image.attrib["data-type"] == "content":
table = image
elif image.attrib["data-type"] == "view-title":
view_title = image
@@ -418,7 +421,7 @@ class SheetBuilder:
data = reference.get_info()
data.update({"Sheet" + k: v for k, v in sheet.get_info().items()})
if not data["Name"]:
data["Name"] = schedule.Name or "Unnamed"
data["Name"] = document.Name or "Unnamed"
view.append(self.parse_embedded_svg(view_title, data))
for image in images:
@@ -88,7 +88,7 @@ class IfcGitData:
try:
if tool.IfcGitRepo.repo.branches:
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
except:
except AttributeError:
return {}
@classmethod
@@ -204,7 +204,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
if (
building_obj
and building_element
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering"]
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering", "IfcElementAssembly"]
and instance_class in ["IfcWindow", "IfcDoor"]
):
# Fills should be a sibling to the building element
@@ -234,7 +234,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
if (
building_obj
and building_element
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering"]
and building_element.is_a() in ["IfcWall", "IfcWallStandardCase", "IfcCovering", "IfcElementAssembly"]
):
if instance_class in ["IfcWindow", "IfcDoor"]:
# TODO For now we are hardcoding windows and doors as a prototype
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell
import blenderbim.tool as tool
import math
from blenderbim.bim.prop import ObjProperty
from blenderbim.bim.module.model.data import AuthoringData
from bpy.types import PropertyGroup, NodeTree
@@ -692,6 +693,7 @@ class BIMRoofProperties(PropertyGroup):
"roof_type",
"generation_method",
"angle",
"percentage",
"rafter_edge_angle",
)
roof_types = (("HIP/GABLE ROOF", "HIP/GABLE ROOF", ""),)
@@ -710,7 +712,8 @@ class BIMRoofProperties(PropertyGroup):
height: bpy.props.FloatProperty(
name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE"
)
angle: bpy.props.FloatProperty(name="Slope Angle", default=pi / 18, subtype="ANGLE")
angle: bpy.props.FloatProperty(name="Slope Angle", default=pi / 18, subtype="ANGLE", update=lambda self, context: self.update_percentage())
percentage: bpy.props.FloatProperty(name="Slope %", default=17.63269754733197, subtype="PERCENTAGE", update=lambda self, context: self.update_angle())
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE")
rafter_edge_angle: bpy.props.FloatProperty(name="Rafter Edge Angle", min=0, max=pi, default=pi / 2, subtype="ANGLE")
@@ -727,6 +730,7 @@ class BIMRoofProperties(PropertyGroup):
kwargs["height"] = self.height
else:
kwargs["angle"] = self.angle
kwargs["percentage"] = self.percentage
if not convert_to_project_units:
return kwargs
@@ -736,3 +740,9 @@ class BIMRoofProperties(PropertyGroup):
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name])
def update_angle(self):
self.angle = math.atan(self.percentage / 100)
def update_percentage(self):
self.percentage = math.tan(self.angle)*100
@@ -683,7 +683,7 @@ class RevertProject(bpy.types.Operator, IFCFileSelector):
bl_idname = "bim.revert_project"
bl_label = "Revert IFC Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Reload currently opened IFC project discarding all unsaved changes"
bl_description = "Revert to a fresh session discarding all unsaved changes"
@classmethod
def poll(cls, context):
@@ -211,7 +211,6 @@ class BIM_PT_project(Panel):
row = self.layout.row(align=True)
row.prop(props, "ifc_file", text="")
row.operator("bim.reload_ifc_file", icon="FILE_REFRESH", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
row.operator("bim.unload_project", text="", icon="CANCEL")
else:
@@ -399,11 +398,3 @@ class BIM_PT_purge(Panel):
layout = self.layout
layout.operator("bim.purge_unused_profiles")
layout.operator("bim.purge_unused_types")
layout.operator("bim.purge_unused_representations")
layout.separator()
layout.label(text="Purge unused elements by class:")
row = layout.row(align=True)
row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
@@ -91,7 +91,9 @@ class ObjectPsetsData(Data):
element = tool.Ifc.get_entity(obj)
if not element:
return []
psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(element.is_a(), pset_only=True)
psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(
element.is_a(), ifcopenshell.util.element.get_predefined_type(element), pset_only=True
)
psetnames = cls.format_pset_enum(psets)
assigned_names = ifcopenshell.util.element.get_psets(element, psets_only=True, should_inherit=False).keys()
return [p for p in psetnames if p[0] not in assigned_names]
@@ -102,7 +104,9 @@ class ObjectPsetsData(Data):
element = tool.Ifc.get_entity(obj)
if not element:
return []
qtos = blenderbim.bim.schema.ifc.psetqto.get_applicable(element.is_a(), qto_only=True)
qtos = blenderbim.bim.schema.ifc.psetqto.get_applicable(
element.is_a(), ifcopenshell.util.element.get_predefined_type(element), qto_only=True
)
return cls.format_pset_enum(qtos)
@classmethod
@@ -32,6 +32,10 @@ from bpy.props import (
CollectionProperty,
)
import gettext
_ = gettext.gettext
def get_style_types(self, context):
if not StylesData.is_loaded:
@@ -88,9 +92,9 @@ def update_shader_graph(self, context):
UV_MODES = [
("UV", "UV", "Actual UV data presented on the geometry"),
("Generated", "Generated", "Automatically-generated UV from the vertex positions of the mesh"),
("Camera", "Camera", "UV from position coordinate in camera space"),
("UV", "UV", _("Actual UV data presented on the geometry")),
("Generated", "Generated", _("Automatically-generated UV from the vertex positions of the mesh")),
("Camera", "Camera", _("UV from position coordinate in camera space")),
]
-17
View File
@@ -142,23 +142,6 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
return {"RUNNING_MODAL"}
class ReloadSelectedIfcFile(bpy.types.Operator):
bl_idname = "bim.reload_selected_ifc_file"
bl_label = "Reload selected IFC File"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Reload currently selected IFC file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
filepath = context.scene.BIMProperties.ifc_file
valid_file = os.path.exists(filepath) and "ifc" in os.path.splitext(filepath)[1].lower()
if not valid_file:
self.report({"ERROR"}, f"Couldn't find .ifc file by the path '{filepath}'")
return {"CANCELLED"}
context.scene.BIMProperties.ifc_file = context.scene.BIMProperties.ifc_file
return {"FINISHED"}
class SelectDataDir(bpy.types.Operator):
bl_idname = "bim.select_data_dir"
bl_label = "Select Data Directory"
+2
View File
@@ -489,6 +489,8 @@ class BIMFacet(PropertyGroup):
("<=", "lesser than or equal to", ""),
(">", "greater than", ""),
("<", "less than", ""),
("*=", "contains", ""),
("!*=", "does not contain", ""),
],
)
+14 -10
View File
@@ -65,16 +65,20 @@ class IFCFileSelector:
split = box.split()
split.label(text=key.title().replace("_", " "))
split.label(text=str(value))
if (
key.lower() == "schema_name"
and str(value).lower() == "ifc2x3"
and filepath[-4:].lower() == ".ifc"
):
row = box.row()
op = row.operator("bim.run_migrate_patch", text="Upgrade to IFC4")
op.infile = filepath
op.outfile = filepath[0:-4] + "-IFC4.ifc"
op.schema = "IFC4"
if key.lower() == "schema_name" and filepath[-4:].lower() == ".ifc":
schema_lower = str(value).lower()
if schema_lower == "ifc2x3":
row = box.row()
op = row.operator("bim.run_migrate_patch", text="Upgrade to IFC4")
op.infile = filepath
op.outfile = filepath[0:-4] + "-IFC4.ifc"
op.schema = "IFC4"
elif schema_lower == "ifc4":
row = box.row()
op = row.operator("bim.run_migrate_patch", text="Upgrade to IFC4X3")
op.infile = filepath
op.outfile = filepath[0:-4] + "-IFC4X3.ifc"
op.schema = "IFC4X3"
if bpy.data.is_saved:
layout.prop(self, "use_relative_path")
+2 -2
View File
@@ -1,5 +1,5 @@
def add_cost_schedule(ifc, name, predefined_type, object_type):
ifc.run("cost.add_cost_schedule", name=name, predefined_type=predefined_type, object_type=object_type)
def add_cost_schedule(ifc, name, predefined_type):
ifc.run("cost.add_cost_schedule", name=name, predefined_type=predefined_type)
def edit_cost_schedule(ifc, cost, cost_schedule):
+8 -1
View File
@@ -166,6 +166,13 @@ class Blender(blenderbim.core.tool.Blender):
bpy.context.window_manager.popup_menu(message_ui, title=message_type.capitalize(), icon=message_type)
@classmethod
def get_view3d_area(cls):
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == "VIEW_3D":
return area
@classmethod
def get_blender_prop_default_value(cls, props, prop_name):
prop_bl_rna = props.bl_rna.properties[prop_name]
@@ -184,7 +191,7 @@ class Blender(blenderbim.core.tool.Blender):
It's a bit naive since it's just taking the first available `VIEW_3D` area
when in real life you can have a couple of those but should work for the most cases.
"""
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
area = cls.get_view3d_area()
region = next(region for region in area.regions if region.type == "WINDOW")
space = next(space for space in area.spaces if space.type == "VIEW_3D")
context_override = {"area": area, "region": region, "space_data": space}
+5 -9
View File
@@ -1592,12 +1592,8 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def is_drawing_active(cls):
camera = bpy.context.scene.camera
return (
camera
and camera.type == "CAMERA"
and camera.BIMObjectProperties.ifc_definition_id
and any(a.type == "VIEW_3D" for a in bpy.context.screen.areas)
)
area = tool.Blender.get_view3d_area()
return camera and camera.type == "CAMERA" and camera.BIMObjectProperties.ifc_definition_id and area
@classmethod
def is_camera_orthographic(cls):
@@ -1614,7 +1610,7 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def activate_drawing(cls, camera):
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
area = tool.Blender.get_view3d_area()
is_local_view = area.spaces[0].local_view is not None
if is_local_view:
bpy.ops.view3d.localview()
@@ -1742,8 +1738,8 @@ class Drawing(blenderbim.core.tool.Drawing):
local_x = [v.x for v in local_bbox]
local_y = [v.y for v in local_bbox]
local_z = [v.z for v in local_bbox]
aabb1_min = (-x/2, -y/2, -clip_end)
aabb1_max = (x/2, y/2, -clip_start)
aabb1_min = (-x / 2, -y / 2, -clip_end)
aabb1_max = (x / 2, y / 2, -clip_start)
aabb2_min = (min(local_x), min(local_y), min(local_z))
aabb2_max = (max(local_x), max(local_y), max(local_z))
for i in range(3):
+14 -14
View File
@@ -1,18 +1,17 @@
import os
import re
# allows git import even if git executable isn't found
os.environ["GIT_PYTHON_REFRESH"] = "quiet"
try:
import git
except:
print("Warning: GitPython not available.")
import bpy
import logging
from blenderbim.bim import import_ifc
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
# allows git import even if git executable isn't found
os.environ["GIT_PYTHON_REFRESH"] = "quiet"
try:
import git
except ImportError:
print("Warning: GitPython not available.")
class IfcGit:
@classmethod
@@ -54,17 +53,19 @@ class IfcGit:
else:
return None
if IfcGitRepo.repo != None and IfcGitRepo.repo.working_dir == path_dir:
if IfcGitRepo.repo is not None and IfcGitRepo.repo.working_dir == path_dir:
return IfcGitRepo.repo
try:
repo = git.Repo(path_dir)
except:
except git.exc.InvalidGitRepositoryError:
parentdir_path = os.path.abspath(os.path.join(path_dir, os.pardir))
if parentdir_path == path_dir:
# root folder
return None
return cls.repo_from_path(parentdir_path)
except git.exc.NoSuchPathError:
return None
if repo:
IfcGitRepo.repo = repo
return repo
@@ -181,9 +182,9 @@ class IfcGit:
for commit in commits:
if props.ifcgit_filter == "tagged" and not commit.hexsha in lookup:
if props.ifcgit_filter == "tagged" and commit.hexsha not in lookup:
continue
elif props.ifcgit_filter == "relevant" and not commit in commits_relevant:
elif props.ifcgit_filter == "relevant" and commit not in commits_relevant:
continue
props.ifcgit_commits.add()
@@ -485,8 +486,7 @@ class IfcGit:
cls.dos2unix(path_ifc)
repo.index.add(path_ifc)
repo.git.commit("--no-edit")
except:
except git.exc.GitError:
operator.report({"ERROR"}, "Unknown IFC Merge failure")
return False
@@ -506,7 +506,7 @@ class IfcGit:
query = "/^#" + str(step_id) + "[ =]/,/;/:" + relpath_ifc
try:
logtext = repo.git.log("-L", query, "-s")
except:
except git.exc.CommandError:
logtext = "No Git history found :("
return logtext
+8 -3
View File
@@ -56,7 +56,12 @@ class Pset(blenderbim.core.tool.Pset):
@classmethod
def is_pset_applicable(cls, element, pset_name):
return bool(pset_name in blenderbim.bim.schema.ifc.psetqto.get_applicable_names(element.is_a(), pset_only=True))
return bool(
pset_name
in blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
element.is_a(), ifcopenshell.util.element.get_predefined_type(element), pset_only=True
)
)
@classmethod
def is_pset_empty(cls, pset):
@@ -69,7 +74,7 @@ class Pset(blenderbim.core.tool.Pset):
@classmethod
def enable_pset_editing(cls, pset_id=None, pset_name=None, pset_type=None, obj=None, obj_type=None):
#TODO REFACTOR ONCE toll/CORE functions are available
# TODO REFACTOR ONCE toll/CORE functions are available
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=tool.Pset.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
)
)
+9 -7
View File
@@ -70,7 +70,9 @@ class Qto(blenderbim.core.tool.Qto):
def get_applicable_base_quantity_name(cls, product=None):
if not product:
return
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(product.is_a(), qto_only=True)
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
product.is_a(), ifcopenshell.util.element.get_predefined_type(product), qto_only=True
)
return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None)
@classmethod
@@ -205,12 +207,12 @@ class Qto(blenderbim.core.tool.Qto):
if quantity == cost_item_quantity:
result.append(
{
"cost_item_id" : cost_item.id(),
"cost_item_name" : cost_item.Name,
"quantity_id" : quantity.id(),
"quantity_name" : quantity.Name,
"quantity_value" : quantity[3],
"quantity_type" : quantity.is_a(),
"cost_item_id": cost_item.id(),
"cost_item_name": cost_item.Name,
"quantity_id": quantity.id(),
"quantity_name": quantity.Name,
"quantity_value": quantity[3],
"quantity_type": quantity.is_a(),
}
)
return result
+19 -15
View File
@@ -144,7 +144,7 @@ class ImportFilterQueryTransformer(lark.Transformer):
new2.pset = arg["pset"]
if "comparison" in arg:
new2.comparison = arg["comparison"]
######################################################################
def facet(self, args):
return args[0]
@@ -169,8 +169,7 @@ class ImportFilterQueryTransformer(lark.Transformer):
def property(self, args):
pset, prop, comparison, value = args
#return {"type": "property", "pset": pset, "name": prop, "value": f"{comparison}{value}"}
return {"type": "property", "pset": pset, "name": prop, "comparison" : comparison, "value": f"{value}"}
return {"type": "property", "pset": pset, "name": prop, "comparison": comparison, "value": f"{value}"}
def classification(self, args):
comparison, value = args
@@ -189,19 +188,24 @@ class ImportFilterQueryTransformer(lark.Transformer):
return {"type": "query", "name": keys, "value": f"{comparison}{value}"}
def comparison(self, args):
if args[0].data == "equals":
return "="
elif args[0].data == "morethanequalto":
return ">="
elif args[0].data == "lessthanequalto":
return "<="
elif args[0].data == "morethan":
return ">"
elif args[0].data == "lessthan":
return "<"
if args[0].data == "not":
comparison = args[1].data
is_not = "!"
else:
return "!="
#return "" if args[0].data == "equals" else "!="
comparison = args[0].data
is_not = ""
return (
is_not
+ {
"equals": "=",
"morethanequalto": ">=",
"lessthanequalto": "<=",
"morethan": ">",
"lessthan": "<",
"contains": "*=",
}[comparison]
)
def keys(self, args):
return self.value(args)
@@ -16,19 +16,24 @@ you will need to categorise your 3D elements (such as "Wall", "Column",
Creating a single object
------------------------
In the **Properties** panel in the bottom right, activate the icon for the
**Scene Properties** tab, and find the **IFC Project** subpanel. Click on
**Create Project** to create a blank IFC project.
In the **Properties** panel on the right, ensure the icon for the **Scene
Properties** tab is active showing the **Project Overview**. Click on **Create
Project** to create a blank IFC project.
.. image:: images/create-project.png
In the top right **Outliner** panel, you will see a hierarchy of spaces that has
been automatically created for you. This hierarchy is known is the **Spatial
Tree**. The Camera, Cube, and Light come with a fresh new Blender session by
default, and are not part of your IFC project.
In the left **Outliner** panel, you will see a hierarchy of spaces that has
been automatically created for you. This hierarchy is known is the **Spatial
Tree**.
.. image:: images/default-spatial-tree.png
There are many ways to create objects. In practice, you should use an element
type library, and we will see how to do this later. For now, we will only add a
single element. In the **Add** menu, add a cube.
.. image:: images/add-cube.png
Any Blender object that you want to be part of IFC project must be converted
into a IFC object by assigning a category. This category is known as the **IFC
Class**.
@@ -39,11 +44,11 @@ Class**.
<https://blenderbim.org/search-ifc-class.html>`__ to help choose an **IFC
Class**!
Select only the default Blender Cube (selected objects are highlighted in
orange, careful not to select anything else!), switch to the **Object
Properties** tab, and find the **IFC Class** panel. Let's pretend our Cube is a
column, so select **IfcColumn** from the **Class** drop-down, and press **Assign
IFC Class**.
Select the cube (selected objects are highlighted in orange, careful not to
select anything else!) and switch to the **Object Information** tab. Let's
pretend our Cube is a column, so select **IfcElement** from the **Products**
dropdown, **IfcColumn** from the **Class** drop-down, and press **Assign IFC
Class**.
.. image:: images/assign-class.png
@@ -53,13 +58,12 @@ IFC Class**.
the shape of your object. You can have a monkey-shaped wall if you want!
All IFC objects must also belong inside the **Spatial Tree**. In the
**Outliner** panel, you can drag and drop your newly created **IfcColumn/Cube**
inside the **IfcBuildingStorey/Ground Floor**. Wasn't that simple?
**Outliner** panel, you will see that your newly created **IfcColumn/Cube** has
been automatically placed in **IfcBuildingStorey/My Storey**.
.. image:: images/drag-drop-outliner-tree.png
.. image:: images/outliner-cube.png
Go back to the **Scene Properties** tab and to the **IFC Project** panel and
press **Save Project**. Save your new IFC model somewhere on your computer.
In the top left **File** menu, Save your new IFC model on your computer.
.. image:: images/save-project.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+375
View File
@@ -0,0 +1,375 @@
try:
import bpy
import bl_i18n_utils
import addon_utils
BPY_IS_LOADED = True
except ModuleNotFoundError:
BPY_IS_LOADED = False
import shutil
import tempfile
import importlib
import os
import re
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, List, Optional
bl_info = {
"name": "BlenderBIM Translations",
"description": "",
"author": "IfcOpenShell Contributors",
"blender": (2, 80, 0),
"version": (0, 0, 999999),
"location": "Properties -> Render -> BBIM Update Translation",
"tracker_url": "https://github.com/IfcOpenShell/IfcOpenShell/issues",
"category": "System",
}
# NOTE: list of available languages in Blender can be retrieved
# by the command below:
# bpy.context.preferences.view.language = "test"
ADDON_NAME = "blenderbim"
TRANSLATION_UI_IS_LOADED = False
def is_addon_loaded(addon_name) -> bool:
loaded_default, loaded_state = addon_utils.check(addon_name)
return loaded_state
def get_branches_directory():
return Path(bpy.context.preferences.filepaths.i18n_branches_directory)
def get_addon_directory() -> Path:
addon_module = importlib.import_module(ADDON_NAME)
return Path(addon_module.__file__).parent
def rearrange_files_for_po_import(po_dir_path: Path, temp_directory: tempfile.TemporaryDirectory):
"""I18n directory also has a bit different format then `ui_translate.export/import`,
every .po file has a parent folder with the same name.
So we rearrange the data that way"""
# NOTE: TemporaryDirectory is cleared automatically as variable goes out of scope
# so we expect it as an argument so it won't get cleared right away
temp_po_dir_path = Path(temp_directory.name)
for file in po_dir_path.glob("**/*"):
if file.suffix != ".po":
continue
shutil.copy(file, temp_po_dir_path / file.name)
@dataclass
class Message:
msgid: str
msgctxt: str | None
sources: Optional[List[str]] = field(default_factory=list)
# mapping languages to translated strings
translations: Optional[Dict[str, str]] = field(default_factory=dict)
def blenderbim_strings_parse(addon_directory=None, po_directory=None):
# NOTE: we decided to use our own parser due bug in Blender parser
# as it tends to pick up strings from other addons and other Blender parts
# and this bug probably would be to low of a priority for Blender to fix
# ref: https://projects.blender.org/blender/blender/issues/116579
if addon_directory is None:
addon_directory = get_addon_directory()
directory = addon_directory / "bim"
if po_directory is None:
po_directory = get_branches_directory()
patterns = [
r'bl_label\s*=\s*"(.*)"', # operator labels
r'bl_description\s*=\s*"(.*)"', # operator descriptions
r'text\s*=\s*"(.*?)"', # UI labels
r'Property\(.*?name\s*=\s*"(.*?)"', # property names
r'report\(\{.*?\}\s*,\s*"(.*?)"', # operator reports
r'info\(\{.*?\}\s*,\s*"(.*?)"', # operator info
r'\b_\("(.*?)"\)', # gettext called with `_`
]
regexes = [re.compile(pattern) for pattern in patterns]
matched_dict: Dict[str, Message] = dict()
# NOTE: currently there is no special handling same message with different contexts
for root, dirs, files in os.walk(directory):
root = Path(root)
for file in files:
if not file.endswith(".py"):
continue
filepath = root / file
source_rel_path = filepath.relative_to(addon_directory).as_posix()
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
for i, regex in enumerate(regexes):
# NOTE: operator tooltips doesnt seem to need Operator context
is_operator = i < 1
for regex_match in regex.finditer(content):
string = regex_match.group(1)
if string == "":
continue
elif "{" in string:
# Includes a formatting string. To fix.
# print(match.group(1))
continue
elif is_operator:
ctx = "Operator"
else:
ctx = None
message = matched_dict.get(string)
if message is None:
message = Message(string, ctx)
matched_dict[string] = message
elif ctx != message.msgctxt and False:
print(
f'WARNING. Message "{string}" was already registered with different context {message.context}. '
f"Current context: {ctx}. File: {source_rel_path}"
)
message.sources.append(source_rel_path)
pot_filepath = po_directory / "blenderbim.pot"
with open(pot_filepath, "w", encoding="utf-8") as fo:
for msg in matched_dict.values():
# make sure sources are unique but keep the order
sources = list(dict.fromkeys(msg.sources))
for source in sources:
fo.write(f"#: {source}\n")
if msg.msgctxt != None:
fo.write(f'msgctxt "{msg.msgctxt}"\n')
fo.write(f'msgid "{msg.msgid}"\n')
fo.write('msgstr ""\n')
fo.write("\n")
def update_translations_from_po(po_directory: Path, translations_module: Path):
translation_data: Dict[str, Message] = dict()
def process_po_entry(language, current_chunk: list[str]):
sources = []
msgid = None
msgstr = None
msgctxt = None
parse_message_attr = lambda line, attr_name: line.removeprefix(f'{attr_name} "').removesuffix('"')
for line in current_chunk:
line = line.strip()
if line.startswith('msgid "'):
msgid = parse_message_attr(line, "msgid")
elif line.startswith('msgstr "'):
msgstr = parse_message_attr(line, "msgstr")
elif line.startswith('msgctxt "'):
msgctxt = parse_message_attr(line, "msgctxt")
elif line.startswith("#:"):
sources.append(line.removeprefix("# ").strip())
msg = translation_data.get(msgid)
if msg is None:
msg = Message(msgid, msgctxt, sources, {language: msgstr})
translation_data[msgid] = msg
else:
msg.sources.extend(sources)
msg.translations[language] = msgstr
# load data from .po files
langs = set()
for po_file_path in po_directory.glob("**/*.po"):
lang = po_file_path.stem
langs.add(lang)
with open(po_file_path, "r") as po_file:
current_chunk = []
for line in po_file:
current_chunk.append(line)
if line.startswith("msgstr"):
process_po_entry(lang, current_chunk)
current_chunk = []
# generate translations.py file
tab = " "
default_context = "*"
ret = ["translations_dict = {"]
for lang in langs:
ret.append(f'{tab}"{lang}": {{')
for msgid, msg in translation_data.items():
if (msgstr := msg.translations[lang]) in (None, ""):
continue
msgctxt = msg.msgctxt
if not msgctxt:
msgctxt = default_context
ret.append(f"{tab*2}({msgctxt!r}, {msgid!r}): {msgstr!r},")
ret.append(f"{tab}}},")
ret.append("}")
with open(translations_module / "translations.py", "w") as fo:
fo.write("\n".join(ret))
if BPY_IS_LOADED:
class SetupTranslationUI(bpy.types.Operator):
bl_idname = "bim.setup_translation_ui"
bl_label = "Setup Translation UI"
bl_options = set()
def execute(self, context):
if not is_addon_loaded("ui_translate"):
addon_utils.enable("ui_translate", default_set=True)
self.report({"INFO"}, '"Manage UI translations" addon was not enabled, it\'s enabled now.')
addon_prefs = context.preferences.addons["ui_translate"].preferences
def is_valid_path(path: Path):
return path.is_dir() and path.is_absolute()
# check branches directory
branches_dir = get_branches_directory()
if not is_valid_path(branches_dir):
raise Exception(
f"I18n Branches directory is not set up or doesn't exist ({branches_dir}).\n"
"Setup I18n branches directory below (or in Preferences > File Paths > Development > I18n Branches) "
"to a folder containing (or that will contain) .po files."
)
# check translations directory
i18n_dir = Path(addon_prefs.I18N_DIR)
# we won't really use the translations directory, we just need addon to stop complaining about it
if not is_valid_path(i18n_dir):
addon_prefs.I18N_DIR = str(branches_dir)
self.report(
{"INFO"},
f'Translations directory ({i18n_dir}) doesn\'t exist. It was reset to I18n branches directory: "{branches_dir}"',
)
# check source directory
source_dir = Path(addon_prefs.SOURCE_DIR)
default_source_path = Path(bpy.app.binary_path).parent / ".".join([str(i) for i in bpy.app.version[:2]])
if not is_valid_path(source_dir):
addon_prefs.SOURCE_DIR = str(default_source_path)
self.report(
{"INFO"},
f'Source directory ({source_dir}) doesn\'t exist. It was reset to default directory: "{default_source_path}"',
)
source_dir = default_source_path
source_locale_path = source_dir / "locale/po"
# Blender UI translations also expect "scripts/presets/keyconfig" to be present in SOURCE_DIR
# but default directory has it by default
if not source_locale_path.is_dir():
source_locale_path.mkdir(parents=True, exist_ok=True)
self.report(
{"INFO"},
f"Couldn't find locale path in the source directory, creating dummy directory: {source_locale_path}.",
)
from ui_translate.settings import settings as ui_translate_settings
from ui_translate.update_ui import UI_OT_i18n_updatetranslation_init_settings
i18n_settings = context.window_manager.i18n_update_settings
if not i18n_settings.is_init:
# if it's not loaded yet, we'll try to reload it one more time
# since we default values we set up during the current operator might helped
UI_OT_i18n_updatetranslation_init_settings.execute_static(context, ui_translate_settings)
if not i18n_settings.is_init:
raise Exception(
"UI Translation settings are not initalized. Make sure the following directories exist:\n"
f" - {ui_translate_settings.WORK_DIR}\n"
f" - {ui_translate_settings.BLENDER_I18N_PO_DIR}\n"
)
global TRANSLATION_UI_IS_LOADED
TRANSLATION_UI_IS_LOADED = True
return {"FINISHED"}
class ParseBlenderBIMStrings(bpy.types.Operator):
bl_idname = "bim.parse_blenderbim_strings"
bl_label = "Parse BlenderBIM strings To .pot"
bl_description = "Parse strings from BlenderBIM and save to .pot"
bl_options = set()
def execute(self, context):
blenderbim_strings_parse()
self.report({"INFO"}, "String were parsed and saved to .pot file.")
return {"FINISHED"}
class UpdateTranslationsFromPo(bpy.types.Operator):
bl_idname = "bim.update_translations_from_po"
bl_label = "Update Translations From .po"
bl_description = (
"Load translation strings from po files at I18n Branches back to translations.py\n"
"Also updates current addon translations in UI"
)
bl_options = set()
def execute(self, context):
branches_dir = get_branches_directory()
update_translations_from_po(branches_dir, get_addon_directory())
if is_addon_loaded(ADDON_NAME):
bpy.app.translations.unregister(ADDON_NAME)
addon_module = importlib.import_module(ADDON_NAME)
translations_module = getattr(addon_module, "translations")
importlib.reload(translations_module)
bpy.app.translations.register(ADDON_NAME, translations_module.translations_dict)
self.report({"INFO"}, f"Addon's translation updated from .po in {branches_dir}")
return {"FINISHED"}
class BBIM_PT_translations(bpy.types.Panel):
bl_label = "BlenderBIM Translations"
bl_idname = "BBIM_PT_translations"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "render"
def draw(self, context):
layout = self.layout
layout.prop(context.preferences.view, "language")
layout.prop(context.preferences.filepaths, "i18n_branches_directory")
if not TRANSLATION_UI_IS_LOADED:
layout.operator("bim.setup_translation_ui")
return
layout.label(text="Developer UI:")
layout.operator("bim.parse_blenderbim_strings", icon="FILE_REFRESH")
layout.separator()
layout.label(text="Translator UI:")
layout.operator("bim.update_translations_from_po", icon="IMPORT")
classes = (
ParseBlenderBIMStrings,
UpdateTranslationsFromPo,
SetupTranslationUI,
BBIM_PT_translations,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
# Example:
# py src/blenderbim/scripts/bbim_translations.py -i "C:/blenderbim-translations" -o "C:/Blender/4.0/scripts/addons/blenderbim/translations.py"
import argparse
parser = argparse.ArgumentParser(description="Converts .po files to translations.py")
parser.add_argument("-i", "--input", type=str, required=True, help="Directory with .po files")
parser.add_argument(
"-o", "--output", type=str, required=True, help="translations.py module location (file may not exist yet)"
)
args = parser.parse_args()
update_translations_from_po(po_directory=Path(args.input), translations_module=Path(args.output))
+12 -12
View File
@@ -114,14 +114,14 @@ std::string format_string(const Argument* argument) {
template <typename Schema, typename T>
void process_pset(element_properties& props, const T* inst) {
// Process an individual Property or Quantity set.
if (auto pset = inst->as<Schema::IfcPropertySet>()) {
if (auto pset = inst->template as<typename Schema::IfcPropertySet>()) {
if (!pset->Name()) {
return;
}
auto ps = pset->HasProperties();
for (auto it = ps->begin(); it != ps->end(); ++it) {
auto& p = *it;
if (auto singleval = p->as<typename Schema::IfcPropertySingleValue>()) {
if (auto singleval = p->template as<typename Schema::IfcPropertySingleValue>()) {
std::string propname, propvalue;
if constexpr (is_ifc4_or_higher<Schema>::value) {
if (!singleval->Name()) {
@@ -135,26 +135,26 @@ void process_pset(element_properties& props, const T* inst) {
if (!singleval->NominalValue()) {
propvalue = "-";
} else {
props[*pset->Name()][propname] = format_string(singleval->NominalValue()->as<IfcUtil::IfcBaseClass>()->data().getArgument(0));
props[*pset->Name()][propname] = format_string(singleval->NominalValue()->template as<IfcUtil::IfcBaseClass>()->data().getArgument(0));
}
}
}
}
if (auto qset = inst->as<Schema::IfcElementQuantity>()) {
if (auto qset = inst->template as<typename Schema::IfcElementQuantity>()) {
if (!qset->Name()) {
return;
}
auto qs = qset->Quantities();
for (auto it = qs->begin(); it != qs->end(); ++it) {
auto& q = *it;
if (q->as<typename Schema::IfcPhysicalSimpleQuantity>() && q->data().getArgument(3)->type() == IfcUtil::Argument_DOUBLE) {
if (q->template as<typename Schema::IfcPhysicalSimpleQuantity>() && q->data().getArgument(3)->type() == IfcUtil::Argument_DOUBLE) {
double v = *q->data().getArgument(3);
props[*qset->Name()][q->Name()] = std::to_string(v);
}
}
}
if constexpr (is_ifc4_or_higher<Schema>::value) {
if (auto extprops = inst->as<Schema::IfcExtendedProperties>()) {
if (auto extprops = inst->template as<Schema::IfcExtendedProperties>()) {
// @todo
}
}
@@ -163,7 +163,7 @@ void process_pset(element_properties& props, const T* inst) {
template <typename Schema>
void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefinition* inst) {
// Extracts the property definitions for an IFC instance.
if (auto tyob = inst->as<typename Schema::IfcTypeObject>()) {
if (auto tyob = inst->template as<typename Schema::IfcTypeObject>()) {
if (tyob->HasPropertySets()) {
auto defs = *tyob->HasPropertySets();
for (auto it = defs->begin(); it != defs->end(); ++it) {
@@ -173,14 +173,14 @@ void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefi
}
}
if constexpr (is_ifc4_or_higher<Schema>::value) {
if (auto mdef = inst->as<typename Schema::IfcMaterialDefinition>()) {
if (auto mdef = inst->template as<typename Schema::IfcMaterialDefinition>()) {
auto defs = mdef->HasProperties();
for (auto it = defs->begin(); it != defs->end(); ++it) {
auto& def = *it;
process_pset<Schema>(props, def);
}
}
if (auto pdef = inst->as<typename Schema::IfcProfileDef>()) {
if (auto pdef = inst->template as<typename Schema::IfcProfileDef>()) {
auto defs = pdef->HasProperties();
for (auto it = defs->begin(); it != defs->end(); ++it) {
auto& def = *it;
@@ -188,7 +188,7 @@ void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefi
}
}
}
if (auto ob = inst->as<typename Schema::IfcObject>()) {
if (auto ob = inst->template as<typename Schema::IfcObject>()) {
if constexpr (is_ifc4_or_higher<Schema>::value) {
auto rels = ob->IsTypedBy();
for (auto it = rels->begin(); it != rels->end(); ++it) {
@@ -200,9 +200,9 @@ void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefi
auto rels = ob->IsDefinedBy();
for (auto it = rels->begin(); it != rels->end(); ++it) {
auto& rel = *it;
if (auto bytype = rel->as<typename Schema::IfcRelDefinesByType>()) {
if (auto bytype = rel->template as<typename Schema::IfcRelDefinesByType>()) {
get_psets_s<Schema>(props, bytype->RelatingType());
} else if (auto byprops = rel->as<typename Schema::IfcRelDefinesByProperties>()) {
} else if (auto byprops = rel->template as<typename Schema::IfcRelDefinesByProperties>()) {
process_pset<Schema>(props, byprops->RelatingPropertyDefinition());
}
}
+10
View File
@@ -0,0 +1,10 @@
# Use Miniconda base image
FROM continuumio/miniconda3:4.10.3
# Update Conda, install necessary libraries, and then install Mamba
RUN conda update -n base -c defaults conda && \
conda install libarchive -c conda-forge -y && \
conda install mamba -c conda-forge -y
# Install Code_Aster and Python dependencies with Conda
RUN mamba install -c conda-forge code-aster python=3.10 -y
+19
View File
@@ -0,0 +1,19 @@
# Ifc2CA - IFC Code_Aster utility
# Copyright (C) 2020, 2021, 2023, 2024 Ioannis P. Christovasilis <ipc@aethereng.com>
#
# This file is part of Ifc2CA.
#
# Ifc2CA 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.
#
# Ifc2CA 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 Ifc2CA. If not, see <http://www.gnu.org/licenses/>.
from .ifc2ca import Ifc2CA
+525
View File
@@ -0,0 +1,525 @@
# Ifc2CA - IFC Code_Aster utility
# Copyright (C) 2020, 2021 Ioannis P. Christovasilis <ipc@aethereng.com>
#
# This file is part of Ifc2CA.
#
# Ifc2CA 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.
#
# Ifc2CA 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 Ifc2CA. If not, see <http://www.gnu.org/licenses/>.
import json
import ifcopenshell
import os
from datetime import datetime
class CA2IFC:
def __init__(self, inputFilename, outputFilename):
self.inputFilename = inputFilename
self.outputFilename = outputFilename
self.data = None
self.f = None
self.reps = {}
self.origin = None
self.xAxis = None
self.yAxis = None
self.zAxis = None
def convert(self):
# load json file
with open(self.inputFilename) as dataFile:
self.data = json.load(dataFile)
# initiate ifc file
self.f = ifcopenshell.file()
# create header
self.create_header()
# create global axes
globalAxes = self.create_global_axes()
localPlacement = self.f.createIfcLocalPlacement(None, globalAxes)
# TODO: create units
lengthUnit = self.f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")
unitAssignment = self.f.createIfcUnitAssignment((lengthUnit,))
# create owner history
ownerHistory = self.create_owner_history()
# create representations and subrepresentations
self.reps = self.create_reference_subrep(globalAxes)
# create project and model
project = self.f.createIfcProject(
self.guid(), ownerHistory, "A Project", None, None, None, None, (self.reps["model"],), unitAssignment
)
model = self.f.createIfcStructuralAnalysisModel(
self.guid(),
ownerHistory,
self.data["name"],
None,
None,
"NOTDEFINED",
globalAxes,
None,
None,
localPlacement,
)
self.f.createIfcRelDeclares(self.guid(), ownerHistory, None, None, project, (model,))
# create materials
ifcMaterials = [None for _ in range(len(self.data["db"]["materials"]))]
for i, material in enumerate(self.data["db"]["materials"]):
ifcMaterials[i] = self.create_material(material)
# create profiles
ifcProfiles = [None for _ in range(len(self.data["db"]["profiles"]))]
for i, profile in enumerate(self.data["db"]["profiles"]):
ifcProfiles[i] = self.create_profile(profile)
# create material-profile sets
mpSets = list(
set([el["material"] + "-" + el["profile"] for el in self.data["elements"] if el["geometryType"] == "line"])
)
ifcMaterialProfileSets = [None for _ in range(len(mpSets))]
for i, mpSet in enumerate(mpSets):
materialIndex = [mat["referenceName"] for mat in self.data["db"]["materials"]].index(mpSet.split("-")[0])
profileIndex = [prof["referenceName"] for prof in self.data["db"]["profiles"]].index(mpSet.split("-")[1])
material = ifcMaterials[materialIndex]
profile = ifcProfiles[profileIndex]
matProf = self.f.createIfcMaterialProfile(
self.data["db"]["materials"][materialIndex]["name"]
+ " | "
+ self.data["db"]["profiles"][profileIndex]["profileName"],
None,
material,
profile,
)
ifcMaterialProfileSets[i] = self.f.createIfcMaterialProfileSet(None, None, (matProf,))
# create structural elements
ifcElements = [None for _ in range(len(self.data["elements"]))]
for i, el in enumerate(self.data["elements"]):
# geometry - product definition shape
prodDefShape = self.create_geometry(el)
if el["geometryType"] == "line":
# z axis TODO: group by elements
localZAxis = self.f.createIfcDirection(tuple(el["orientation"][2]))
# element
ifcElements[i] = self.f.createIfcStructuralCurveMember(
self.guid(),
ownerHistory,
el["name"],
None,
None,
localPlacement,
prodDefShape,
el["predefinedType"],
localZAxis,
)
if el["geometryType"] == "surface":
ifcElements[i] = self.f.createIfcStructuralSurfaceMember(
self.guid(),
ownerHistory,
el["name"],
None,
None,
localPlacement,
prodDefShape,
el["predefinedType"],
el["thickness"],
)
# create structural point connections
ifcConnections = [None for _ in range(len(self.data["connections"]))]
for i, conn in enumerate(self.data["connections"]):
# geometry - product definition shape
prodDefShape = self.create_geometry(conn)
# boundary conditions
if conn["appliedCondition"]:
bc = self.create_applied_conditions(conn["appliedCondition"], conn["geometryType"])
if conn["geometryType"] == "point":
appliedCondition = self.f.createIfcBoundaryNodeCondition(
None, bc["dx"], bc["dy"], bc["dz"], bc["drx"], bc["dry"], bc["drz"]
)
if conn["geometryType"] == "line":
appliedCondition = self.f.createIfcBoundaryEdgeCondition(
None, bc["dx"], bc["dy"], bc["dz"], bc["drx"], bc["dry"], bc["drz"]
)
if conn["geometryType"] == "surface":
appliedCondition = self.f.createIfcBoundaryFaceCondition(None, bc["dx"], bc["dy"], bc["dz"])
else:
appliedCondition = None
if conn["geometryType"] == "point":
# local axes
localAxes = self.create_orientation(conn["orientation"])
# connection
ifcConnections[i] = self.f.createIfcStructuralPointConnection(
self.guid(),
ownerHistory,
conn["name"],
None,
None,
localPlacement,
prodDefShape,
appliedCondition,
localAxes,
)
if conn["geometryType"] == "line":
# z axis TODO: group by elements
localZAxis = self.f.createIfcDirection(tuple(conn["orientation"][2]))
# connection
ifcConnections[i] = self.f.createIfcStructuralCurveConnection(
self.guid(),
ownerHistory,
conn["name"],
None,
None,
localPlacement,
prodDefShape,
appliedCondition,
localZAxis,
)
if conn["geometryType"] == "surface":
ifcConnections[i] = self.f.createIfcStructuralSurfaceConnection(
self.guid(), ownerHistory, conn["name"], None, None, localPlacement, prodDefShape, appliedCondition
)
# assign material-profile-sets
for i, mpSet in enumerate(mpSets):
groupOfElements = []
for j, el in enumerate(self.data["elements"]):
if el["geometryType"] == "line" and el["material"] + "-" + el["profile"] == mpSet:
groupOfElements.append(ifcElements[j])
if groupOfElements:
self.f.createIfcRelAssociatesMaterial(
self.guid(), ownerHistory, None, None, tuple(groupOfElements), ifcMaterialProfileSets[i]
)
# assign materials
for i, mat in enumerate(self.data["db"]["materials"]):
groupOfElements = []
for j, el in enumerate(self.data["elements"]):
if el["geometryType"] == "surface" and el["material"] == mat["referenceName"]:
groupOfElements.append(ifcElements[j])
if groupOfElements:
self.f.createIfcRelAssociatesMaterial(
self.guid(), ownerHistory, None, None, tuple(groupOfElements), ifcMaterials[i]
)
# create connections with elements
for i, el in enumerate(self.data["elements"]):
for conn in el["connections"]:
j = [c["referenceName"] for c in self.data["connections"]].index(conn["relatedConnection"])
geometryType = self.data["connections"][j]["geometryType"]
if conn["appliedCondition"]:
bc = self.create_applied_conditions(conn["appliedCondition"], geometryType)
if geometryType == "point":
appliedCondition = self.f.createIfcBoundaryNodeCondition(
None, bc["dx"], bc["dy"], bc["dz"], bc["drx"], bc["dry"], bc["drz"]
)
if geometryType == "line":
appliedCondition = self.f.createIfcBoundaryEdgeCondition(
None, bc["dx"], bc["dy"], bc["dz"], bc["drx"], bc["dry"], bc["drz"]
)
if geometryType == "surface":
appliedCondition = self.f.createIfcBoundaryFaceCondition(None, bc["dx"], bc["dy"], bc["dz"])
else:
appliedCondition = None
# local axes
localAxes = self.create_orientation(conn["orientation"])
if geometryType == "point":
if not conn["eccentricity"]:
self.f.createIfcRelConnectsStructuralMember(
self.guid(),
ownerHistory,
None,
None,
ifcElements[i],
ifcConnections[j],
appliedCondition,
None,
None,
localAxes,
)
else:
pointOnElement = self.f.createIfcCartesianPoint(tuple(conn["eccentricity"]["pointOnElement"]))
vector = conn["eccentricity"]["vector"]
connPointEcc = self.f.createIfcConnectionPointEccentricity(
pointOnElement, None, vector[0], vector[1], vector[2]
)
self.f.createIfcRelConnectsWithEccentricity(
self.guid(),
ownerHistory,
None,
None,
ifcElements[i],
ifcConnections[j],
appliedCondition,
None,
None,
localAxes,
connPointEcc,
)
if geometryType in ["line", "surface"]:
self.f.createIfcRelConnectsStructuralMember(
self.guid(),
ownerHistory,
None,
None,
ifcElements[i],
ifcConnections[j],
appliedCondition,
None,
None,
localAxes,
)
# assign elements and connections to group
self.f.createIfcRelAssignsToGroup(
self.guid(), ownerHistory, None, None, tuple(ifcElements + ifcConnections), None, model
)
# finalize ifc file
self.f.write(self.outputFilename)
def guid(self):
return ifcopenshell.guid.new()
def create_header(self):
self.f.wrapped_data.header.file_name.name = os.path.basename(self.outputFilename)
def create_global_axes(self):
self.xAxis = self.f.createIfcDirection((1.0, 0.0, 0.0))
self.yAxis = self.f.createIfcDirection((0.0, 1.0, 0.0))
self.zAxis = self.f.createIfcDirection((0.0, 0.0, 1.0))
self.origin = self.f.createIfcCartesianPoint((0.0, 0.0, 0.0))
axes = self.f.createIfcAxis2Placement3D(self.origin, self.zAxis, self.xAxis)
return axes
def create_orientation(self, orientation):
xAxis = self.f.createIfcDirection(tuple(orientation[0]))
zAxis = self.f.createIfcDirection(tuple(orientation[2]))
axes = self.f.createIfcAxis2Placement3D(self.origin, zAxis, xAxis)
return axes
def create_owner_history(self):
actor = self.f.createIfcActorRole("ENGINEER", None, None)
person = self.f.createIfcPerson("Christovasilis", None, "Ioannis", None, None, None, (actor,))
organization = self.f.createIfcOrganization(
None,
"IfcOpenShell",
"IfcOpenShell, an open source (LGPL) software library that helps users and software developers to work with the IFC file format.",
)
p_o = self.f.createIfcPersonAndOrganization(person, organization)
application = self.f.createIfcApplication(organization, "v0.0.x", "IFC2CA", "IFC2CA")
timestamp = int(datetime.now().timestamp())
ownerHistory = self.f.createIfcOwnerHistory(p_o, application, "READWRITE", None, None, None, None, timestamp)
return ownerHistory
def create_reference_subrep(self, globalAxes):
modelRep = self.f.createIfcGeometricRepresentationContext(None, "Model", 3, 1.0e-05, globalAxes, None)
bodySubRep = self.f.createIfcGeometricRepresentationSubContext(
"Body", "Model", None, None, None, None, modelRep, None, "MODEL_VIEW", None
)
refSubRep = self.f.createIfcGeometricRepresentationSubContext(
"Reference", "Model", None, None, None, None, modelRep, None, "GRAPH_VIEW", None
)
return {"model": modelRep, "body": bodySubRep, "reference": refSubRep}
def create_material(self, material):
ifcMaterial = self.f.createIfcMaterial(material["name"], None, material["category"])
mechProps = []
if "youngModulus" in material["mechProps"]:
youngModulus = self.f.createIfcPropertySingleValue(
"YoungModulus", None, self.f.createIfcModulusOfElasticityMeasure(material["mechProps"]["youngModulus"])
)
mechProps.append(youngModulus)
if "shearModulus" in material["mechProps"]:
shearModulus = self.f.createIfcPropertySingleValue(
"ShearModulus", None, self.f.createIfcModulusOfElasticityMeasure(material["mechProps"]["shearModulus"])
)
mechProps.append(shearModulus)
if "poissonRatio" in material["mechProps"]:
poissonRatio = self.f.createIfcPropertySingleValue(
"PoissonRatio", None, self.f.createIfcPositiveRatioMeasure(material["mechProps"]["poissonRatio"])
)
mechProps.append(poissonRatio)
if mechProps:
self.f.createIfcMaterialProperties(
"Pset_MaterialMechanical", material["name"], tuple(mechProps), ifcMaterial
)
commonProps = []
if "massDensity" in material["commonProps"]:
massDensity = self.f.createIfcPropertySingleValue(
"MassDensity", None, self.f.createIfcMassDensityMeasure(material["commonProps"]["massDensity"])
)
commonProps.append(massDensity)
if commonProps:
self.f.createIfcMaterialProperties("Pset_MaterialCommon", material["name"], tuple(commonProps), ifcMaterial)
return ifcMaterial
def create_profile(self, profile):
if profile["profileShape"] == "rectangular":
ifcProfile = self.f.createIfcRectangleProfileDef(
profile["profileType"], profile["profileName"], None, profile["xDim"], profile["yDim"]
)
if profile["profileShape"] == "iSymmetrical":
ifcProfile = self.f.createIfcIShapeProfileDef(
profile["profileType"],
profile["profileName"],
None,
profile["commonProps"]["overallWidth"],
profile["commonProps"]["overallDepth"],
profile["commonProps"]["webThickness"],
profile["commonProps"]["flangeThickness"],
profile["commonProps"]["filletRadius"],
)
mechProps = []
if "massPerLength" in profile["mechProps"]:
massPerLength = self.f.createIfcPropertySingleValue(
"MassPerLength", None, self.f.createIfcMassPerLengthMeasure(profile["mechProps"]["massPerLength"])
)
mechProps.append(massPerLength)
if "crossSectionArea" in profile["mechProps"]:
crossSectionArea = self.f.createIfcPropertySingleValue(
"CrossSectionArea", None, self.f.createIfcAreaMeasure(profile["mechProps"]["crossSectionArea"])
)
mechProps.append(crossSectionArea)
if "momentOfInertiaY" in profile["mechProps"]:
momentOfInertiaY = self.f.createIfcPropertySingleValue(
"MomentOfInertiaY",
None,
self.f.createIfcMomentOfInertiaMeasure(profile["mechProps"]["momentOfInertiaY"]),
)
mechProps.append(momentOfInertiaY)
if "momentOfInertiaZ" in profile["mechProps"]:
momentOfInertiaZ = self.f.createIfcPropertySingleValue(
"MomentOfInertiaZ",
None,
self.f.createIfcMomentOfInertiaMeasure(profile["mechProps"]["momentOfInertiaZ"]),
)
mechProps.append(momentOfInertiaZ)
if "torsionalConstantX" in profile["mechProps"]:
torsionalConstantX = self.f.createIfcPropertySingleValue(
"TorsionalConstantX",
None,
self.f.createIfcMomentOfInertiaMeasure(profile["mechProps"]["torsionalConstantX"]),
)
mechProps.append(torsionalConstantX)
if mechProps:
self.f.createIfcProfileProperties(
"Pset_ProfileMechanical", profile["profileName"], tuple(mechProps), ifcProfile
)
return ifcProfile
def create_geometry(self, object):
if object["geometryType"] == "point":
point = self.f.createIfcCartesianPoint(tuple(object["geometry"]))
vertex = self.f.createIfcVertexPoint(point)
vertexTopologyRep = self.f.createIfcTopologyRepresentation(
self.reps["reference"], "Reference", "Vertex", (vertex,)
)
vertexProdDefShape = self.f.createIfcProductDefinitionShape(None, None, (vertexTopologyRep,))
return vertexProdDefShape
if object["geometryType"] == "line":
startPoint = self.f.createIfcCartesianPoint(tuple(object["geometry"][0]))
startVertex = self.f.createIfcVertexPoint(startPoint)
endPoint = self.f.createIfcCartesianPoint(tuple(object["geometry"][1]))
endVertex = self.f.createIfcVertexPoint(endPoint)
edge = self.f.createIfcEdge(startVertex, endVertex)
edgeTopologyRep = self.f.createIfcTopologyRepresentation(
self.reps["reference"], "Reference", "Edge", (edge,)
)
edgeProdDefShape = self.f.createIfcProductDefinitionShape(None, None, (edgeTopologyRep,))
return edgeProdDefShape
if object["geometryType"] == "surface":
verts = [None for _ in range(len(object["geometry"]))]
for i, p in enumerate(object["geometry"]):
point = self.f.createIfcCartesianPoint(tuple(p))
verts[i] = self.f.createIfcVertexPoint(point)
orientedEdges = [None for _ in range(len(object["geometry"]))]
for i, v in enumerate(verts):
v2Index = (i + 1) if i < len(verts) - 1 else 0
edge = self.f.createIfcEdge(v, verts[v2Index])
orientedEdges[i] = self.f.createIfcOrientedEdge(None, None, edge, True)
edgeLoop = self.f.createIfcEdgeLoop(tuple(orientedEdges))
localAxes = self.create_orientation(object["orientation"])
plane = self.f.createIfcPlane(localAxes)
faceBound = self.f.createIfcFaceBound(edgeLoop, True)
face = self.f.createIfcFaceSurface((faceBound,), plane, True)
faceTopologyRep = self.f.createIfcTopologyRepresentation(
self.reps["reference"], "Reference", "Face", (face,)
)
faceProdDefShape = self.f.createIfcProductDefinitionShape(None, None, (faceTopologyRep,))
return faceProdDefShape
def create_applied_conditions(self, bc, geometryType):
for dof in ["dx", "dy", "dz"]:
if isinstance(bc[dof], bool):
bc[dof] = self.f.createIfcBoolean(bc[dof])
else:
if geometryType == "point":
bc[dof] = self.f.createIfcLinearStiffnessMeasure(bc[dof])
if geometryType == "line":
bc[dof] = self.f.createIfcModulusOfLinearSubgradeReactionMeasure(bc[dof])
if geometryType == "surface":
bc[dof] = self.f.createIfcModulusOfSubgradeReactionMeasure(bc[dof])
for dof in ["drx", "dry", "drz"]:
if isinstance(bc[dof], bool):
bc[dof] = self.f.createIfcBoolean(bc[dof])
else:
if geometryType == "point":
bc[dof] = self.f.createIfcRotationalStiffnessMeasure(bc[dof])
if geometryType == "line":
bc[dof] = self.f.createIfcModulusOfRotationalSubgradeReactionMeasure(bc[dof])
return bc
if __name__ == "__main__":
inputFilename = "grid_of_beams.json"
outputFilename = "grid_of_beams.ifc"
ca2ifc = CA2IFC(inputFilename, outputFilename)
ca2ifc.convert()
+393 -467
View File
@@ -1,6 +1,6 @@
# Ifc2CA - IFC Code_Aster utility
# Copyright (C) 2020, 2021 Ioannis P. Christovasilis <ipc@aethereng.com>
# Copyright (C) 2020, 2021, 2023, 2024 Ioannis P. Christovasilis <ipc@aethereng.com>
#
# This file is part of Ifc2CA.
#
@@ -17,509 +17,435 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Ifc2CA. If not, see <http://www.gnu.org/licenses/>.
import json
import ifcopenshell
import os
from datetime import datetime
import itertools
import ifcopenshell as ios
import meshio
import numpy as np
flatten = itertools.chain.from_iterable
class CA2IFC:
def __init__(self, inputFilename, outputFilename):
self.inputFilename = inputFilename
self.outputFilename = outputFilename
self.data = None
self.f = None
self.reps = {}
self.origin = None
self.xAxis = None
self.yAxis = None
self.zAxis = None
def get_element_data(model, name, element):
if element["geometry_type"] == "Edge":
for i, cell_block in enumerate(model.cells):
if cell_block.type == "line":
cell_tags = model.cell_data["cell_tags"][i]
break
rows = []
for i_row, i in enumerate(cell_tags):
if i == 0:
continue
tags = model.cell_tags[i]
for tag in tags:
if tag == name:
# print(i_row, i)
rows.append(i_row)
break
def convert(self):
# load json file
with open(self.inputFilename) as dataFile:
self.data = json.load(dataFile)
points = list(set(flatten([cell_block.data[c] for c in rows])))
points.sort(key=lambda p: np.linalg.norm(model.points[p] - np.array(element["origin"])))
coords = [np.round(model.points[p], 4).tolist() for p in points]
local_coords = [
[float(round(np.linalg.norm(model.points[p] - np.array(element["origin"])), 4))] for p in points
]
# initiate ifc file
self.f = ifcopenshell.file()
return {
"name": name,
"points": points,
"coords": coords,
"local_coords": local_coords,
}
# create header
self.create_header()
elif element["geometry_type"] == "Face":
triangle_cell_tags = None
quad_cell_tags = None
for i, cell_block in enumerate(model.cells):
if cell_block.type == "triangle":
triangle_cell_tags = model.cell_data["cell_tags"][i]
break
# create global axes
globalAxes = self.create_global_axes()
localPlacement = self.f.createIfcLocalPlacement(None, globalAxes)
# TODO: create units
lengthUnit = self.f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")
unitAssignment = self.f.createIfcUnitAssignment((lengthUnit,))
# create owner history
ownerHistory = self.create_owner_history()
# create representations and subrepresentations
self.reps = self.create_reference_subrep(globalAxes)
# create project and model
project = self.f.createIfcProject(
self.guid(), ownerHistory, "A Project", None, None, None, None, (self.reps["model"],), unitAssignment
)
model = self.f.createIfcStructuralAnalysisModel(
self.guid(),
ownerHistory,
self.data["name"],
None,
None,
"NOTDEFINED",
globalAxes,
None,
None,
localPlacement,
)
self.f.createIfcRelDeclares(self.guid(), ownerHistory, None, None, project, (model,))
# create materials
ifcMaterials = [None for _ in range(len(self.data["db"]["materials"]))]
for i, material in enumerate(self.data["db"]["materials"]):
ifcMaterials[i] = self.create_material(material)
# create profiles
ifcProfiles = [None for _ in range(len(self.data["db"]["profiles"]))]
for i, profile in enumerate(self.data["db"]["profiles"]):
ifcProfiles[i] = self.create_profile(profile)
# create material-profile sets
mpSets = list(
set([el["material"] + "-" + el["profile"] for el in self.data["elements"] if el["geometryType"] == "line"])
)
ifcMaterialProfileSets = [None for _ in range(len(mpSets))]
for i, mpSet in enumerate(mpSets):
materialIndex = [mat["referenceName"] for mat in self.data["db"]["materials"]].index(mpSet.split("-")[0])
profileIndex = [prof["referenceName"] for prof in self.data["db"]["profiles"]].index(mpSet.split("-")[1])
material = ifcMaterials[materialIndex]
profile = ifcProfiles[profileIndex]
matProf = self.f.createIfcMaterialProfile(
self.data["db"]["materials"][materialIndex]["name"]
+ " | "
+ self.data["db"]["profiles"][profileIndex]["profileName"],
None,
material,
profile,
)
ifcMaterialProfileSets[i] = self.f.createIfcMaterialProfileSet(None, None, (matProf,))
# create structural elements
ifcElements = [None for _ in range(len(self.data["elements"]))]
for i, el in enumerate(self.data["elements"]):
# geometry - product definition shape
prodDefShape = self.create_geometry(el)
if el["geometryType"] == "line":
# z axis TODO: group by elements
localZAxis = self.f.createIfcDirection(tuple(el["orientation"][2]))
# element
ifcElements[i] = self.f.createIfcStructuralCurveMember(
self.guid(),
ownerHistory,
el["name"],
None,
None,
localPlacement,
prodDefShape,
el["predefinedType"],
localZAxis,
)
if el["geometryType"] == "surface":
ifcElements[i] = self.f.createIfcStructuralSurfaceMember(
self.guid(),
ownerHistory,
el["name"],
None,
None,
localPlacement,
prodDefShape,
el["predefinedType"],
el["thickness"],
)
# create structural point connections
ifcConnections = [None for _ in range(len(self.data["connections"]))]
for i, conn in enumerate(self.data["connections"]):
# geometry - product definition shape
prodDefShape = self.create_geometry(conn)
# boundary conditions
if conn["appliedCondition"]:
bc = self.create_applied_conditions(conn["appliedCondition"], conn["geometryType"])
if conn["geometryType"] == "point":
appliedCondition = self.f.createIfcBoundaryNodeCondition(
None, bc["dx"], bc["dy"], bc["dz"], bc["drx"], bc["dry"], bc["drz"]
)
if conn["geometryType"] == "line":
appliedCondition = self.f.createIfcBoundaryEdgeCondition(
None, bc["dx"], bc["dy"], bc["dz"], bc["drx"], bc["dry"], bc["drz"]
)
if conn["geometryType"] == "surface":
appliedCondition = self.f.createIfcBoundaryFaceCondition(None, bc["dx"], bc["dy"], bc["dz"])
if triangle_cell_tags is not None:
rows = []
for i_row, i in enumerate(triangle_cell_tags):
if i == 0:
continue
tags = model.cell_tags[i]
for tag in tags:
if tag == name:
# print(i_row, i)
rows.append(i_row)
break
if not len(rows):
points = []
else:
appliedCondition = None
points = list(flatten([cell_block.data[c] for c in rows]))
if conn["geometryType"] == "point":
# local axes
localAxes = self.create_orientation(conn["orientation"])
# connection
ifcConnections[i] = self.f.createIfcStructuralPointConnection(
self.guid(),
ownerHistory,
conn["name"],
None,
None,
localPlacement,
prodDefShape,
appliedCondition,
localAxes,
)
for i, cell_block in enumerate(model.cells):
if cell_block.type == "quad":
quad_cell_tags = model.cell_data["cell_tags"][i]
break
if conn["geometryType"] == "line":
# z axis TODO: group by elements
localZAxis = self.f.createIfcDirection(tuple(conn["orientation"][2]))
# connection
ifcConnections[i] = self.f.createIfcStructuralCurveConnection(
self.guid(),
ownerHistory,
conn["name"],
None,
None,
localPlacement,
prodDefShape,
appliedCondition,
localZAxis,
)
if quad_cell_tags is not None:
rows = []
for i_row, i in enumerate(quad_cell_tags):
if i == 0:
continue
tags = model.cell_tags[i]
for tag in tags:
if tag == name:
# print(i_row, i)
rows.append(i_row)
break
if len(rows):
points.extend(list(flatten([cell_block.data[c] for c in rows])))
if conn["geometryType"] == "surface":
ifcConnections[i] = self.f.createIfcStructuralSurfaceConnection(
self.guid(), ownerHistory, conn["name"], None, None, localPlacement, prodDefShape, appliedCondition
)
points = list(set(points))
points.sort()
coords = [model.points[p].tolist() for p in points]
local_coords = [
np.round(np.array(element["orientation"]).dot(model.points[p] - np.array(element["origin"])), 4).tolist()[
:2
]
for p in points
]
# assign material-profile-sets
for i, mpSet in enumerate(mpSets):
groupOfElements = []
for j, el in enumerate(self.data["elements"]):
if el["geometryType"] == "line" and el["material"] + "-" + el["profile"] == mpSet:
groupOfElements.append(ifcElements[j])
return {
"name": name,
"points": points,
"coords": coords,
"local_coords": local_coords,
}
if groupOfElements:
self.f.createIfcRelAssociatesMaterial(
self.guid(), ownerHistory, None, None, tuple(groupOfElements), ifcMaterialProfileSets[i]
)
# assign materials
for i, mat in enumerate(self.data["db"]["materials"]):
groupOfElements = []
for j, el in enumerate(self.data["elements"]):
if el["geometryType"] == "surface" and el["material"] == mat["referenceName"]:
groupOfElements.append(ifcElements[j])
if groupOfElements:
self.f.createIfcRelAssociatesMaterial(
self.guid(), ownerHistory, None, None, tuple(groupOfElements), ifcMaterials[i]
)
def get_element_result_data(model, field_label, name, element, field_type):
points = get_element_data(model, name, element)["points"]
if field_type == "InternalForces":
if element["geometry_type"] == "Edge":
return {
"N": [round(model.point_data[field_label][p][0], 4) for p in points],
"VY": [round(model.point_data[field_label][p][1], 4) for p in points],
"VZ": [round(model.point_data[field_label][p][2], 4) for p in points],
"MT": [round(model.point_data[field_label][p][3], 4) for p in points],
"MFY": [round(model.point_data[field_label][p][4], 4) for p in points],
"MFZ": [round(model.point_data[field_label][p][5], 4) for p in points],
}
# create connections with elements
for i, el in enumerate(self.data["elements"]):
for conn in el["connections"]:
j = [c["referenceName"] for c in self.data["connections"]].index(conn["relatedConnection"])
geometryType = self.data["connections"][j]["geometryType"]
elif element["geometry_type"] == "Face":
if len(model.point_data[field_label][points[0]]) == 8:
offset = 0
elif len(model.point_data[field_label][points[0]]) == 14:
offset = 6
else:
assert (
False
), f"Internal force field with {len(model.point_data[field_label][points[0]])} field values for {field_label} and {element['Name']} "
if conn["appliedCondition"]:
bc = self.create_applied_conditions(conn["appliedCondition"], geometryType)
if geometryType == "point":
appliedCondition = self.f.createIfcBoundaryNodeCondition(
None, bc["dx"], bc["dy"], bc["dz"], bc["drx"], bc["dry"], bc["drz"]
)
if geometryType == "line":
appliedCondition = self.f.createIfcBoundaryEdgeCondition(
None, bc["dx"], bc["dy"], bc["dz"], bc["drx"], bc["dry"], bc["drz"]
)
if geometryType == "surface":
appliedCondition = self.f.createIfcBoundaryFaceCondition(None, bc["dx"], bc["dy"], bc["dz"])
else:
appliedCondition = None
return {
"NXX": [round(model.point_data[field_label][p][offset + 0], 4) for p in points],
"NYY": [round(model.point_data[field_label][p][offset + 1], 4) for p in points],
"NXY": [round(model.point_data[field_label][p][offset + 2], 4) for p in points],
"MXX": [round(model.point_data[field_label][p][offset + 3], 4) for p in points],
"MYY": [round(model.point_data[field_label][p][offset + 4], 4) for p in points],
"MXY": [round(model.point_data[field_label][p][offset + 5], 4) for p in points],
"QX": [round(model.point_data[field_label][p][offset + 6], 4) for p in points],
"QY": [round(model.point_data[field_label][p][offset + 7], 4) for p in points],
}
# local axes
localAxes = self.create_orientation(conn["orientation"])
if field_type == "Displacements":
return {
"DX": [round(model.point_data[field_label][p][0], 4) for p in points],
"DY": [round(model.point_data[field_label][p][1], 4) for p in points],
"DZ": [round(model.point_data[field_label][p][2], 4) for p in points],
"DRX": [round(model.point_data[field_label][p][3], 4) for p in points],
"DRY": [round(model.point_data[field_label][p][4], 4) for p in points],
"DRZ": [round(model.point_data[field_label][p][5], 4) for p in points],
}
if geometryType == "point":
if not conn["eccentricity"]:
self.f.createIfcRelConnectsStructuralMember(
self.guid(),
ownerHistory,
None,
None,
ifcElements[i],
ifcConnections[j],
appliedCondition,
None,
None,
localAxes,
)
else:
pointOnElement = self.f.createIfcCartesianPoint(tuple(conn["eccentricity"]["pointOnElement"]))
vector = conn["eccentricity"]["vector"]
connPointEcc = self.f.createIfcConnectionPointEccentricity(
pointOnElement, None, vector[0], vector[1], vector[2]
)
self.f.createIfcRelConnectsWithEccentricity(
self.guid(),
ownerHistory,
None,
None,
ifcElements[i],
ifcConnections[j],
appliedCondition,
None,
None,
localAxes,
connPointEcc,
)
if geometryType in ["line", "surface"]:
self.f.createIfcRelConnectsStructuralMember(
self.guid(),
ownerHistory,
None,
None,
ifcElements[i],
ifcConnections[j],
appliedCondition,
None,
None,
localAxes,
def results_to_ifc(ifc_file, ifc_model, rmed_path, global_case, field_types, data):
if not rmed_path.exists():
print(f"Med file with results not found for case_instant: {global_case}")
return
result = meshio.read(rmed_path, "med")
if global_case == "LC":
model_cases = data["load_cases"]
elif global_case == "COMB":
model_cases = data["load_combinations"]
for field in field_types:
if field == "InternalForces":
_parsed_data = internal_forces_to_ifc(ifc_file, ifc_model, result, model_cases, data["elements"])
elif field == "Displacements":
_parsed_data = displacements_to_ifc(ifc_file, ifc_model, result, model_cases, data["elements"])
def internal_forces_to_ifc(ifc_file, ifc_model, result, model_cases, elements):
result_cases = [dict() for _ in model_cases]
field_cases = [f"ELEMENT_FORCE[{i}] - {i + 1}" for i in range(len(result_cases))]
# Create Result Groups for load case_instance combinations
for iCase, case_instance in enumerate(model_cases):
result_cases[iCase]["case_instance"] = ifc_file.create_entity(
"IfcStructuralResultGroup",
**{
"GlobalId": ios.guid.new(),
"Name": "Internal Forces for " + case_instance["Name"],
"TheoryType": "FIRST_ORDER_THEORY",
"ResultForLoadGroup": ifc_file.by_id(case_instance["id"]),
"IsLinear": True,
},
)
result_cases[iCase]["assignment"] = ifc_file.create_entity(
"IfcRelAssignsToGroup",
**{
"GlobalId": ios.guid.new(),
"RelatedObjects": [],
"RelatingGroup": result_cases[iCase]["case_instance"],
},
)
if ifc_model.HasResults:
ifc_model.HasResults += tuple([result["case_instance"] for result in result_cases])
else:
ifc_model.HasResults = tuple([result["case_instance"] for result in result_cases])
data = []
for _, element in enumerate(elements):
group_name = getGroupName(element["ref_id"])
name = element["Name"]
info = get_element_data(result, group_name, element)
assert len(info["coords"]) >= 2
for iCase, field_case in enumerate(field_cases):
forces = get_element_result_data(result, field_case, group_name, element, field_type="InternalForces")
reaction = ifc_file.create_entity(
"IfcStructuralCurveReaction" if element["geometry_type"] == "Edge" else "IfcStructuralSurfaceReaction",
**{
"GlobalId": ios.guid.new(),
"Name": "Internal Forces for " + model_cases[iCase]["Name"] + f" on {name}",
# "AppliedLoad": load["ifcLoad"],
"GlobalOrLocal": "LOCAL_COORDS",
"PredefinedType": "DISCRETE",
},
)
result_cases[iCase]["assignment"].RelatedObjects += (reaction,)
ifc_file.create_entity(
"IfcRelConnectsStructuralActivity",
**{
"GlobalId": ios.guid.new(),
"RelatingElement": ifc_file.by_id(element["id"]),
"RelatedStructuralActivity": reaction,
},
)
reaction.AppliedLoad = ifc_file.create_entity(
"IfcStructuralLoadConfiguration",
**{
"Name": "Internal Forces for " + model_cases[iCase]["Name"] + f" on {name}",
"Values": [],
"Locations": tuple([tuple(node) for node in info["local_coords"]]),
},
)
if element["geometry_type"] == "Edge":
for iNode, node in enumerate(info["coords"]):
location = f"({node[0]}, {node[1]}, {node[2]})"
distance = info["local_coords"][iNode][0]
N = forces["N"][iNode]
VY = forces["VY"][iNode]
VZ = forces["VZ"][iNode]
MT = forces["MT"][iNode]
MFY = forces["MFY"][iNode]
MFZ = forces["MFZ"][iNode]
data.append([name, f"LCC-{iCase + 1} @ {distance}", location, N, VY, VZ, MT, MFY, MFZ])
pointValue = ifc_file.create_entity(
"IfcStructuralLoadSingleForce",
**{
"Name": "Internal Forces for " + model_cases[iCase]["Name"] + f" @ {distance} on {name}",
"ForceX": N,
"ForceY": VY,
"ForceZ": VZ,
"MomentX": MT,
"MomentY": MFY,
"MomentZ": MFZ,
},
)
reaction.AppliedLoad.Values += (pointValue,)
# assign elements and connections to group
self.f.createIfcRelAssignsToGroup(
self.guid(), ownerHistory, None, None, tuple(ifcElements + ifcConnections), None, model
elif element["geometry_type"] == "Face":
for iNode, node in enumerate(info["coords"]):
location = f"({node[0]}, {node[1]}, {node[2]})"
distance = tuple(info["local_coords"][iNode])
NXX = forces["NXX"][iNode]
NYY = forces["NYY"][iNode]
NXY = forces["NXY"][iNode]
MXX = forces["MXX"][iNode]
MYY = forces["MYY"][iNode]
MXY = forces["MXY"][iNode]
data.append([name, f"LCC-{iCase + 1} @ {distance}", location, NXX, NYY, NXY, MXX, MYY, MXY])
pointValue = ifc_file.create_entity(
"IfcStructuralLoadSingleForce",
**{
"Name": "Internal Forces for " + model_cases[iCase]["Name"] + f" @ {distance} on {name}",
"ForceX": NXX,
"ForceY": NYY,
"ForceZ": NXY,
"MomentX": MXX,
"MomentY": MYY,
"MomentZ": MXY,
},
)
reaction.AppliedLoad.Values += (pointValue,)
return data
def displacements_to_ifc(ifc_file, ifc_model, result, model_cases, elements):
result_cases = [dict() for _ in model_cases]
field_cases = [f"MODEL_DISP[{i}] - {i + 1}" for i in range(len(result_cases))]
# Create Result Groups for load case_instance combinations
for iCase, case_instance in enumerate(model_cases):
result_cases[iCase]["case_instance"] = ifc_file.create_entity(
"IfcStructuralResultGroup",
**{
"GlobalId": ios.guid.new(),
"Name": "Global Displacements for " + case_instance["Name"],
"TheoryType": "FIRST_ORDER_THEORY",
"ResultForLoadGroup": ifc_file.by_id(case_instance["id"]),
"IsLinear": True,
},
)
# finalize ifc file
self.f.write(self.outputFilename)
def guid(self):
return ifcopenshell.guid.new()
def create_header(self):
self.f.wrapped_data.header.file_name.name = os.path.basename(self.outputFilename)
def create_global_axes(self):
self.xAxis = self.f.createIfcDirection((1.0, 0.0, 0.0))
self.yAxis = self.f.createIfcDirection((0.0, 1.0, 0.0))
self.zAxis = self.f.createIfcDirection((0.0, 0.0, 1.0))
self.origin = self.f.createIfcCartesianPoint((0.0, 0.0, 0.0))
axes = self.f.createIfcAxis2Placement3D(self.origin, self.zAxis, self.xAxis)
return axes
def create_orientation(self, orientation):
xAxis = self.f.createIfcDirection(tuple(orientation[0]))
zAxis = self.f.createIfcDirection(tuple(orientation[2]))
axes = self.f.createIfcAxis2Placement3D(self.origin, zAxis, xAxis)
return axes
def create_owner_history(self):
actor = self.f.createIfcActorRole("ENGINEER", None, None)
person = self.f.createIfcPerson("Christovasilis", None, "Ioannis", None, None, None, (actor,))
organization = self.f.createIfcOrganization(
None,
"IfcOpenShell",
"IfcOpenShell, an open source (LGPL) software library that helps users and software developers to work with the IFC file format.",
)
p_o = self.f.createIfcPersonAndOrganization(person, organization)
application = self.f.createIfcApplication(organization, "v0.0.x", "IFC2CA", "IFC2CA")
timestamp = int(datetime.now().timestamp())
ownerHistory = self.f.createIfcOwnerHistory(p_o, application, "READWRITE", None, None, None, None, timestamp)
return ownerHistory
def create_reference_subrep(self, globalAxes):
modelRep = self.f.createIfcGeometricRepresentationContext(None, "Model", 3, 1.0e-05, globalAxes, None)
bodySubRep = self.f.createIfcGeometricRepresentationSubContext(
"Body", "Model", None, None, None, None, modelRep, None, "MODEL_VIEW", None
)
refSubRep = self.f.createIfcGeometricRepresentationSubContext(
"Reference", "Model", None, None, None, None, modelRep, None, "GRAPH_VIEW", None
result_cases[iCase]["assignment"] = ifc_file.create_entity(
"IfcRelAssignsToGroup",
**{
"GlobalId": ios.guid.new(),
"RelatedObjects": [],
"RelatingGroup": result_cases[iCase]["case_instance"],
},
)
return {"model": modelRep, "body": bodySubRep, "reference": refSubRep}
if ifc_model.HasResults:
ifc_model.HasResults += tuple([result["case_instance"] for result in result_cases])
else:
ifc_model.HasResults = tuple([result["case_instance"] for result in result_cases])
def create_material(self, material):
ifcMaterial = self.f.createIfcMaterial(material["name"], None, material["category"])
data = []
for _, element in enumerate(elements):
group_name = getGroupName(element["ref_id"])
name = element["Name"]
info = get_element_data(result, group_name, element)
assert len(info["coords"]) >= 2
for iCase, case_instance in enumerate(field_cases):
displacements = get_element_result_data(
result, case_instance, group_name, element, field_type="Displacements"
)
reaction = ifc_file.create_entity(
"IfcStructuralCurveReaction" if element["geometry_type"] == "Edge" else "IfcStructuralSurfaceReaction",
**{
"GlobalId": ios.guid.new(),
"Name": "Global Displacements for " + model_cases[iCase]["Name"] + f" on {name}",
# "AppliedLoad": load["ifcLoad"],
"GlobalOrLocal": "LOCAL_COORDS",
"PredefinedType": "DISCRETE",
},
)
result_cases[iCase]["assignment"].RelatedObjects += (reaction,)
mechProps = []
if "youngModulus" in material["mechProps"]:
youngModulus = self.f.createIfcPropertySingleValue(
"YoungModulus", None, self.f.createIfcModulusOfElasticityMeasure(material["mechProps"]["youngModulus"])
)
mechProps.append(youngModulus)
if "shearModulus" in material["mechProps"]:
shearModulus = self.f.createIfcPropertySingleValue(
"ShearModulus", None, self.f.createIfcModulusOfElasticityMeasure(material["mechProps"]["shearModulus"])
)
mechProps.append(shearModulus)
if "poissonRatio" in material["mechProps"]:
poissonRatio = self.f.createIfcPropertySingleValue(
"PoissonRatio", None, self.f.createIfcPositiveRatioMeasure(material["mechProps"]["poissonRatio"])
)
mechProps.append(poissonRatio)
if mechProps:
self.f.createIfcMaterialProperties(
"Pset_MaterialMechanical", material["name"], tuple(mechProps), ifcMaterial
ifc_file.create_entity(
"IfcRelConnectsStructuralActivity",
**{
"GlobalId": ios.guid.new(),
"RelatingElement": ifc_file.by_id(element["id"]),
"RelatedStructuralActivity": reaction,
},
)
commonProps = []
if "massDensity" in material["commonProps"]:
massDensity = self.f.createIfcPropertySingleValue(
"MassDensity", None, self.f.createIfcMassDensityMeasure(material["commonProps"]["massDensity"])
)
commonProps.append(massDensity)
if commonProps:
self.f.createIfcMaterialProperties("Pset_MaterialCommon", material["name"], tuple(commonProps), ifcMaterial)
return ifcMaterial
def create_profile(self, profile):
if profile["profileShape"] == "rectangular":
ifcProfile = self.f.createIfcRectangleProfileDef(
profile["profileType"], profile["profileName"], None, profile["xDim"], profile["yDim"]
reaction.AppliedLoad = ifc_file.create_entity(
"IfcStructuralLoadConfiguration",
**{
"Name": "Global Displacements for " + model_cases[iCase]["Name"] + f" on {name}",
"Values": [],
"Locations": tuple([tuple(node) for node in info["local_coords"]]),
},
)
if profile["profileShape"] == "iSymmetrical":
ifcProfile = self.f.createIfcIShapeProfileDef(
profile["profileType"],
profile["profileName"],
None,
profile["commonProps"]["overallWidth"],
profile["commonProps"]["overallDepth"],
profile["commonProps"]["webThickness"],
profile["commonProps"]["flangeThickness"],
profile["commonProps"]["filletRadius"],
)
if element["geometry_type"] == "Edge":
for iNode, node in enumerate(info["coords"]):
location = f"({node[0]}, {node[1]}, {node[2]})"
distance = info["local_coords"][iNode][0]
mechProps = []
if "massPerLength" in profile["mechProps"]:
massPerLength = self.f.createIfcPropertySingleValue(
"MassPerLength", None, self.f.createIfcMassPerLengthMeasure(profile["mechProps"]["massPerLength"])
)
mechProps.append(massPerLength)
if "crossSectionArea" in profile["mechProps"]:
crossSectionArea = self.f.createIfcPropertySingleValue(
"CrossSectionArea", None, self.f.createIfcAreaMeasure(profile["mechProps"]["crossSectionArea"])
)
mechProps.append(crossSectionArea)
if "momentOfInertiaY" in profile["mechProps"]:
momentOfInertiaY = self.f.createIfcPropertySingleValue(
"MomentOfInertiaY",
None,
self.f.createIfcMomentOfInertiaMeasure(profile["mechProps"]["momentOfInertiaY"]),
)
mechProps.append(momentOfInertiaY)
if "momentOfInertiaZ" in profile["mechProps"]:
momentOfInertiaZ = self.f.createIfcPropertySingleValue(
"MomentOfInertiaZ",
None,
self.f.createIfcMomentOfInertiaMeasure(profile["mechProps"]["momentOfInertiaZ"]),
)
mechProps.append(momentOfInertiaZ)
if "torsionalConstantX" in profile["mechProps"]:
torsionalConstantX = self.f.createIfcPropertySingleValue(
"TorsionalConstantX",
None,
self.f.createIfcMomentOfInertiaMeasure(profile["mechProps"]["torsionalConstantX"]),
)
mechProps.append(torsionalConstantX)
if mechProps:
self.f.createIfcProfileProperties(
"Pset_ProfileMechanical", profile["profileName"], tuple(mechProps), ifcProfile
)
DX = displacements["DX"][iNode]
DY = displacements["DY"][iNode]
DZ = displacements["DZ"][iNode]
DRX = displacements["DRX"][iNode]
DRY = displacements["DRY"][iNode]
DRZ = displacements["DRZ"][iNode]
return ifcProfile
data.append([name, f"LCC-{iCase + 1} @ {distance}", location, DX, DY, DZ, DRX, DRY, DRZ])
def create_geometry(self, object):
if object["geometryType"] == "point":
point = self.f.createIfcCartesianPoint(tuple(object["geometry"]))
vertex = self.f.createIfcVertexPoint(point)
vertexTopologyRep = self.f.createIfcTopologyRepresentation(
self.reps["reference"], "Reference", "Vertex", (vertex,)
)
vertexProdDefShape = self.f.createIfcProductDefinitionShape(None, None, (vertexTopologyRep,))
pointValue = ifc_file.create_entity(
"IfcStructuralLoadSingleDisplacement",
**{
"Name": "Global Displacements for "
+ model_cases[iCase]["Name"]
+ f" @ {distance} on {name}",
"DisplacementX": DX,
"DisplacementY": DY,
"DisplacementZ": DZ,
"RotationalDisplacementRX": DRX,
"RotationalDisplacementRY": DRY,
"RotationalDisplacementRZ": DRZ,
},
)
reaction.AppliedLoad.Values += (pointValue,)
return vertexProdDefShape
elif element["geometry_type"] == "Face":
for iNode, node in enumerate(info["coords"]):
location = f"({node[0]}, {node[1]}, {node[2]})"
distance = tuple(info["local_coords"][iNode])
if object["geometryType"] == "line":
startPoint = self.f.createIfcCartesianPoint(tuple(object["geometry"][0]))
startVertex = self.f.createIfcVertexPoint(startPoint)
endPoint = self.f.createIfcCartesianPoint(tuple(object["geometry"][1]))
endVertex = self.f.createIfcVertexPoint(endPoint)
edge = self.f.createIfcEdge(startVertex, endVertex)
edgeTopologyRep = self.f.createIfcTopologyRepresentation(
self.reps["reference"], "Reference", "Edge", (edge,)
)
edgeProdDefShape = self.f.createIfcProductDefinitionShape(None, None, (edgeTopologyRep,))
DX = displacements["DX"][iNode]
DY = displacements["DY"][iNode]
DZ = displacements["DZ"][iNode]
DRX = displacements["DRX"][iNode]
DRY = displacements["DRY"][iNode]
DRZ = displacements["DRZ"][iNode]
return edgeProdDefShape
data.append([name, f"LCC-{iCase + 1} @ {distance}", location, DX, DY, DZ, DRX, DRY, DRZ])
if object["geometryType"] == "surface":
verts = [None for _ in range(len(object["geometry"]))]
for i, p in enumerate(object["geometry"]):
point = self.f.createIfcCartesianPoint(tuple(p))
verts[i] = self.f.createIfcVertexPoint(point)
pointValue = ifc_file.create_entity(
"IfcStructuralLoadSingleDisplacement",
**{
"Name": "Global Displacements for "
+ model_cases[iCase]["Name"]
+ f" @ {distance} on {name}",
"DisplacementX": DX,
"DisplacementY": DY,
"DisplacementZ": DZ,
"RotationalDisplacementRX": DRX,
"RotationalDisplacementRY": DRY,
"RotationalDisplacementRZ": DRZ,
},
)
reaction.AppliedLoad.Values += (pointValue,)
orientedEdges = [None for _ in range(len(object["geometry"]))]
for i, v in enumerate(verts):
v2Index = (i + 1) if i < len(verts) - 1 else 0
edge = self.f.createIfcEdge(v, verts[v2Index])
orientedEdges[i] = self.f.createIfcOrientedEdge(None, None, edge, True)
edgeLoop = self.f.createIfcEdgeLoop(tuple(orientedEdges))
localAxes = self.create_orientation(object["orientation"])
plane = self.f.createIfcPlane(localAxes)
faceBound = self.f.createIfcFaceBound(edgeLoop, True)
face = self.f.createIfcFaceSurface((faceBound,), plane, True)
faceTopologyRep = self.f.createIfcTopologyRepresentation(
self.reps["reference"], "Reference", "Face", (face,)
)
faceProdDefShape = self.f.createIfcProductDefinitionShape(None, None, (faceTopologyRep,))
return faceProdDefShape
def create_applied_conditions(self, bc, geometryType):
for dof in ["dx", "dy", "dz"]:
if isinstance(bc[dof], bool):
bc[dof] = self.f.createIfcBoolean(bc[dof])
else:
if geometryType == "point":
bc[dof] = self.f.createIfcLinearStiffnessMeasure(bc[dof])
if geometryType == "line":
bc[dof] = self.f.createIfcModulusOfLinearSubgradeReactionMeasure(bc[dof])
if geometryType == "surface":
bc[dof] = self.f.createIfcModulusOfSubgradeReactionMeasure(bc[dof])
for dof in ["drx", "dry", "drz"]:
if isinstance(bc[dof], bool):
bc[dof] = self.f.createIfcBoolean(bc[dof])
else:
if geometryType == "point":
bc[dof] = self.f.createIfcRotationalStiffnessMeasure(bc[dof])
if geometryType == "line":
bc[dof] = self.f.createIfcModulusOfRotationalSubgradeReactionMeasure(bc[dof])
return bc
return data
if __name__ == "__main__":
inputFilename = "grid_of_beams.json"
outputFilename = "grid_of_beams.ifc"
ca2ifc = CA2IFC(inputFilename, outputFilename)
ca2ifc.convert()
def getGroupName(name):
if "|" in name:
info = name.split("|")
sortName = "".join(c for c in info[0] if c.isupper())
return f"{sortName[2:]}_{info[1]}"
else:
return name
+898 -572
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
# STEP: DEFINE SUPPORTS AND CONSTRAINTS
connection = AFFE_CHAR_MECA(
MODELE = model,
{%- if vertexConnections %}
LIAISON_DDL = (
{%- for conn in vertexConnections %}
{%- if conn.appliedCondition %}
{%- for i in range(len(conn.liaisons.coeffs)) %}
_F(
GROUP_NO = {{ conn.liaisons.groupNames }},
DDL = {{ conn.liaisons.dofs[i] }},
COEF_MULT = {{ conn.liaisons.coeffs[i] }},
COEF_IMPO = 0.0
),
{%- endfor %}
{%- endif %}
{%- for rel in conn.related_elements %}
{%- for i in range(len(rel.liaisons.coeffs)) %}
_F(
GROUP_NO = {{ rel.liaisons.groupNames }},
DDL = {{ rel.liaisons.dofs[i] }},
COEF_MULT = {{ rel.liaisons.coeffs[i] }},
COEF_IMPO = 0.0
),
{%- endfor %}
{%- endfor %}
{%- endfor %}
),
{%- endif %}
{%- if edgeConnections %}
LIAISON_GROUP = (
{%- for conn in edgeConnections %}
{%- if conn.appliedCondition %}
{%- for i in range(len(conn.liaisons.coeffs)) %}
_F(
GROUP_NO_1 = {{ tuple([conn.liaisons.groupNames[0]]) }},
GROUP_NO_2 = {{ tuple([conn.liaisons.groupNames[0]]) }},
DDL_1 = {{ conn.liaisons.dofs[i] }},
DDL_2 = {{ conn.liaisons.dofs[i] }},
COEF_MULT_1 = {{ conn.liaisons.coeffs[i] }},
COEF_MULT_2 = (0.0, 0.0, 0.0),
COEF_IMPO = 0.0
),
{%- endfor %}
{%- endif %}
{%- for rel in conn.related_elements %}
{%- for i in range(len(rel.liaisons.coeffs)) %}
_F(
GROUP_NO_1 = {{ tuple([rel.liaisons.groupNames[0]]) }},
GROUP_NO_2 = {{ tuple([rel.liaisons.groupNames[3]]) }},
DDL_1 = {{ tuple(rel.liaisons.dofs[i][:3]) }},
DDL_2 = {{ tuple(rel.liaisons.dofs[i][:3]) }},
COEF_MULT_1 = {{ tuple(rel.liaisons.coeffs[i][:3]) }},
COEF_MULT_2 = {{ tuple(rel.liaisons.coeffs[i][3:]) }},
COEF_IMPO = 0.0
),
{%- endfor %}
{%- endfor %}
{%- endfor %}
),
{%- endif %}
{%- if unifiedConnections %}
LIAISON_UNIF = (
{%- for conn in unifiedConnections %}
_F(
GROUP_NO = {{ conn.unifiedGroupNames }},
DDL = ('DX', 'DY', 'DZ', 'DRX', 'DRY', 'DRZ')
),
{%- endfor %}
),
{%- endif %}
{%- if rigidLinkGroupNames %}
LIAISON_SOLIDE = (
{%- for groupName in rigidLinkGroupNames %}
_F(GROUP_MA = {{ tuple([groupName]) }}),
{%- endfor %}
),
{%- endif %}
)
{{ "\n" }}
@@ -0,0 +1,115 @@
# STEP: DEFINE ELEMENTS
element = AFFE_CARA_ELEM(
MODELE = model,
POUTRE = (
{%- for _, profile in profiles.items() %}
{%- if profile.properties %}
_F(
GROUP_MA = {{ profile.groupNames }},
SECTION = 'GENERALE',
CARA = ('A', 'IY', 'IZ', 'JX'),
VALE = ({{ profile.properties.CrossSectionArea }}, {{ profile.properties.MomentOfInertiaY }}, {{ profile.properties.MomentOfInertiaZ }}, {{ profile.properties.TorsionalConstantX }})
),
{%- elif profile.type == "IfcRectangleProfileDef" and profile.ProfileType == "AREA" %}
_F(
GROUP_MA = {{ profile.groupNames }},
SECTION = 'RECTANGLE',
CARA = ('HY', 'HZ'),
VALE = ({{ profile.XDim }}, {{ profile.YDim }})
),
{%- elif profile.type == "IfcRectangleHollowProfileDef" and profile.ProfileType == "AREA" %}
_F(
GROUP_MA = {{ profile.groupNames }},
SECTION = 'RECTANGLE',
CARA = ('HY', 'HZ', 'EPY', 'EPZ'),
VALE = ({{ profile.XDim }}, {{ profile.YDim }}, {{ profile.WallThickness }}, {{ profile.WallThickness }})
),
{%- else %}
_F(
GROUP_MA = {{ profile.groupNames }},
SECTION = 'GENERALE',
CARA = ('A', 'IY', 'IZ', 'JX'),
VALE = ({{ profile.properties.CrossSectionArea }}, {{ profile.properties.MomentOfInertiaY }}, {{ profile.properties.MomentOfInertiaZ }}, {{ profile.properties.TorsionalConstantX }})
),
{%- endif %}
{%- endfor %}
{%- if rigidLinkGroupNames %}
_F(
GROUP_MA = {{ rigidLinkGroupNames }},
SECTION = 'RECTANGLE',
CARA = ('HY', 'HZ'),
VALE = (1.0, 1.0)
),
{%- endif %}
),
COQUE = (
{%- for el in shellElements %}
_F(
GROUP_MA = {{ tuple([getGroupName(el.ref_id)]) }},
EPAIS = {{ el.Thickness }},
VECTEUR = {{ tuple(el.orientation[0]) }}
),
{%- endfor %}
),
DISCRET = (
{%- for conn in vertexConnections %}
_F(
GROUP_MA = {{ tuple([getGroupName(conn.ref_id) + "_0D"]) }},
CARA = 'K_TR_D_N',
VALE = {{ conn.stiffnesses }},
REPERE = 'LOCAL'
),
{%- if includeZeroLength1DSprings %}
{%- for rel in conn.related_elements %}
_F(
GROUP_MA = {{ tuple([rel.springGroupName]) }},
CARA = 'K_TR_D_L',
VALE = {{ rel.stiffnesses }},
REPERE = 'LOCAL'
),
{%- endfor %}
{%- endif %}
{%- endfor %}
{%- for conn in edgeConnections %}
_F(
GROUP_MA = {{ tuple([getGroupName(conn.ref_id) + "_0D"]) }},
CARA = 'K_TR_D_N',
VALE = {{ conn.stiffnesses }},
REPERE = 'LOCAL'
),
{%- endfor %}
),
ORIENTATION = (
{%- for el in beamElements %}
_F(
GROUP_MA = {{ tuple([getGroupName(el.ref_id)]) }},
CARA = 'VECT_Y',
VALE = {{ tuple(el.orientation[1]) }}
),
{%- endfor %}
{%- for conn in vertexConnections %}
_F(
GROUP_MA = {{ tuple([getGroupName(conn.ref_id) + "_0D"]) }},
CARA = 'VECT_X_Y',
VALE = {{ tuple(conn.orientation[0] + conn.orientation[1]) }}
),
{%- if includeZeroLength1DSprings %}
{%- for rel in conn.related_elements %}
_F(
GROUP_MA = {{ tuple([rel.springGroupName]) }},
CARA = 'VECT_X_Y',
VALE = {{ tuple(rel.orientation[0] + rel.orientation[1]) }},
),
{%- endfor %}
{%- endif %}
{%- endfor %}
{%- for conn in edgeConnections %}
_F(
GROUP_MA = {{ tuple([getGroupName(conn.ref_id) + "_0D"]) }},
CARA = 'VECT_X_Y',
VALE = {{ tuple(conn.orientation[0] + conn.orientation[1]) }}
),
{%- endfor %}
),
)
{{ "\n" }}
@@ -0,0 +1,44 @@
# STEP: DEFINE TIME
{{ analysis_time }} = DEFI_LIST_REEL(
DEBUT = {{ start }},
INTERVALLE = _F(
JUSQU_A = {{ end }},
NOMBRE = {{ steps }}
)
)
# STEP: DEFINE LOADS
{{ load }} = AFFE_CHAR_MECA_F(
MODELE = model,
FORCE_NODALE = (
{%- for el in vertexLoadElements %}
_F(
GROUP_NO = {{ tuple([getGroupName(el.ref_id)]) }},
{%- for key, load in el.loads[load_key].items() %}
{{ key }} = DEFI_FONCTION(NOM_PARA='INST', ABSCISSE={{ time }}, ORDONNEE={{ tuple(load) }}),
{%- endfor %}
),
{%- endfor %}
),
FORCE_POUTRE = (
{%- for el in edgeLoadElements %}
_F(
GROUP_MA = {{ tuple([getGroupName(el.ref_id)]) }},
{%- for key, load in el.loads[load_key].items() %}
{{ key }} = DEFI_FONCTION(NOM_PARA='INST', ABSCISSE={{ time }}, ORDONNEE={{ tuple(load) }}),
{%- endfor %}
),
{%- endfor %}
),
FORCE_COQUE = (
{%- for el in faceLoadElements %}
_F(
GROUP_MA = {{ tuple([getGroupName(el.ref_id)]) }},
{%- for key, load in el.loads[load_key].items() %}
{{ key }} = DEFI_FONCTION(NOM_PARA='INST', ABSCISSE={{ time }}, ORDONNEE={{ tuple(load) }}),
{%- endfor %}
),
{%- endfor %}
),
)
{{ "\n" }}
@@ -0,0 +1,28 @@
# STEP: DEFINE MATERIALS
{%- for i, (_, material) in enumerate(materials.items()) %}
{{ "mat" + "_%s" % i }} = DEFI_MATERIAU(
ELAS = _F(
E = {{ material.properties.YoungModulus }},
NU = {{ material.properties.PoissonRatio }},
RHO = {{ material.properties.MassDensity }}
)
)
{% endfor %}
material = AFFE_MATERIAU(
MAILLAGE = mesh,
AFFE = (
{%- for i, (_, material) in enumerate(materials.items()) %}
_F(
GROUP_MA = {{ material.groupNames }},
MATER = {{ "mat" + "_%s" % i }},
),
{%- endfor %}
{%- if rigidLinkGroupNames %}
_F(
GROUP_MA = {{ rigidLinkGroupNames }},
MATER = {{ "mat_0" }},
),
{%- endif %}
)
)
{{ "\n" }}
@@ -0,0 +1,47 @@
# STEP: DEFINE MODEL
model = AFFE_MODELE(
MAILLAGE = mesh,
AFFE = (
_F(
TOUT = 'OUI',
PHENOMENE = 'MECANIQUE',
MODELISATION = '3D'
),
{%- if faceGroupNames %}
_F(
GROUP_MA = {{ faceGroupNames }},
PHENOMENE = 'MECANIQUE',
MODELISATION = 'DKT'
),
{%- endif %}
{%- if edgeGroupNames %}
_F(
GROUP_MA = {{ edgeGroupNames }},
PHENOMENE = 'MECANIQUE',
MODELISATION = 'POU_D_E'
),
{%- endif %}
{%- if point0DGroupNames %}
_F(
GROUP_MA = {{ point0DGroupNamesPlus }},
PHENOMENE = 'MECANIQUE',
MODELISATION = 'DIS_TR'
),
{%- endif %}
{%- if point1DGroupNames %}
_F(
GROUP_MA = {{ point1DGroupNames }},
PHENOMENE = 'MECANIQUE',
MODELISATION = 'DIS_TR'
),
{%- endif %}
{%- if rigidLinkGroupNames %}
_F(
GROUP_MA = {{ rigidLinkGroupNames }},
PHENOMENE = 'MECANIQUE',
MODELISATION = 'POU_D_E'
),
{%- endif %}
)
)
{{ "\n" }}
+13
View File
@@ -0,0 +1,13 @@
P actions make_etude
P memory_limit {{ allocated_memory }}
P time_limit {{ time_limit }}
P version stable
F comm {{ model_name }}_{{ run_label }}.comm D 1
F libr {{ model_name }}.med D 20
F mess {{ model_name }}_{{ run_label }}.mess R 6
{%- if "LC" in cases %}
F rmed {{ model_name + "_LC" }}.rmed R 80
{%- endif %}
{%- if "COMB" in cases %}
F rmed {{ model_name + "_COMB" }}.rmed R 81
{%- endif %}
@@ -0,0 +1,11 @@
# STEP: RESULT EXTRACTION
IMPR_RESU(
FORMAT="MED",
UNITE={{unit_number}},
RESU=_F(
RESULTAT={{res_Bld}},
NOM_CHAM=("DEPL", "EFGE_NOEU"),
NOM_CHAM_MED=("MODEL_DISP", "ELEMENT_FORCE"),
),
)
{{"\n"}}
@@ -0,0 +1,4 @@
# STEP: CONCLUDE STUDY
# code_aster.close()
FIN()
{{"\n"}}
@@ -0,0 +1,3 @@
# STEP: READ MED FILE
mesh = LIRE_MAILLAGE(FORMAT="MED", UNITE=20)
{{"\n"}}
@@ -0,0 +1,27 @@
# STEP: RUN ANALYSIS
{{ res_Bld }} = MECA_STATIQUE(
MODELE = model,
CHAM_MATER = material,
CARA_ELEM = element,
LIST_INST = {{ analysis_time }},
EXCIT = (
_F(
CHARGE = connection
),
_F(
CHARGE = {{ load }}
)
),
SOLVEUR=_F(
NPREC=12,
RESI_RELA=1e-1,
STOP_SINGULIER='NON',
)
)
{{ res_Bld }} = CALC_CHAMP(
reuse = {{ res_Bld }},
RESULTAT = {{ res_Bld }},
CONTRAINTE=('EFGE_NOEU', ),
)
{{ "\n" }}
@@ -0,0 +1,24 @@
# Ifc2CA - IFC Code_Aster utility
# Copyright (C) 2020, 2021, 2023, 2024 Ioannis P. Christovasilis <ipc@aethereng.com>
#
# This file is part of Ifc2CA.
#
# Ifc2CA 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.
#
# Ifc2CA 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 Ifc2CA. If not, see <http://www.gnu.org/licenses/>.
# STEP: INITIALIZE STUDY
# import code_aster
# code_aster.init()
DEBUT()
{{"\n"}}
@@ -1,5 +1,5 @@
# Ifc2CA - IFC Code_Aster utility
# Copyright (C) 2020, 2021 Ioannis P. Christovasilis <ipc@aethereng.com>
# Copyright (C) 2020, 2021, 2023, 2024 Ioannis P. Christovasilis <ipc@aethereng.com>
#
# This file is part of Ifc2CA.
#
@@ -16,26 +16,31 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Ifc2CA. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
from __future__ import print_function
import itertools
import json
import os
import time
import json
from pathlib import Path
import numpy as np
import salome
import salome_notebook
import salome_version
import numpy as np
import itertools
from pathlib import Path
flatten = itertools.chain.from_iterable
mesh_size = {{ mesh_size }}
med_path = r"{{ med_path }}"
json_path = r"{{ json_path }}"
with open(json_path, "r") as f:
data = json.load(f)
class MODEL:
def __init__(self, dataFilename, medFilename, meshSize):
self.dataFilename = dataFilename
self.medFilename = medFilename
self.meshSize = meshSize
def __init__(self):
self.medFilename = med_path
self.mesh_size = mesh_size
self.tolLoc = 0
self.mesh = None
self.meshNodes = None
@@ -82,29 +87,22 @@ class MODEL:
return self.geompy.MakeFaceWires(LineList, 1)
def makeObject(self, geometry, geometryType):
if geometryType == "point":
def makeObject(self, geometry, geometry_type):
if geometry_type == "Vertex":
return self.makePoint(geometry)
if geometryType == "line":
if geometry_type == "Edge":
return self.makeLine(geometry)
if geometryType == "surface":
if geometry_type == "Face":
return self.makeFace(geometry)
def makePartition(self, objects, geometryType):
if geometryType == "point":
def makePartition(self, objects, geometry_type):
if geometry_type == "Vertex":
shapeType = "VERTEX"
if geometryType == "line":
if geometry_type == "Edge":
shapeType = "EDGE"
if geometryType == "surface":
if geometry_type == "Face":
shapeType = "FACE"
return self.geompy.MakePartition(
objects, [], [], [], self.geompy.ShapeType[shapeType], 0, [], 1
)
def getLinkGeometry(self, ecc, orientation, finalPoint):
vector = np.array(orientation).transpose().dot(ecc["vector"])
initialPoint = (np.array(finalPoint) - vector).tolist()
return [initialPoint, finalPoint]
return self.geompy.MakePartition(objects, [], [], [], self.geompy.ShapeType[shapeType], 0, [], 1)
def length(self, geometry):
return (
@@ -115,18 +113,17 @@ class MODEL:
def create(self):
# Read data from input file
with open(self.dataFilename) as dataFile:
data = json.load(dataFile)
# data = data
elements = data["elements"]
connections = data["connections"]
self.elements = elements = data["elements"]
self.connections = connections = data["connections"]
# --> Delete this reference data and repopulate it with the objects
# while going through elements
for conn in connections:
conn["relatedElements"] = []
conn["related_elements"] = []
# End <--
meshSize = self.meshSize
mesh_size = self.mesh_size
dec = 7 # 4 decimals for length in mm
tol = 10 ** (-dec - 3 + 1)
@@ -134,7 +131,7 @@ class MODEL:
self.tolLoc = tol * 10 * 2
tolLoc = self.tolLoc
NEW_SALOME = int(salome_version.getVersion()[0]) >= 9
self.NEW_SALOME = NEW_SALOME = int(salome_version.getVersion()[0]) >= 9
salome.salome_init()
theStudy = salome.myStudy
notebook = salome_notebook.NoteBook(theStudy)
@@ -142,10 +139,11 @@ class MODEL:
###
### GEOM component
###
import GEOM
from salome.geom import geomBuilder
import math
import GEOM
import SALOMEDS
from salome.geom import geomBuilder
gg = salome.ImportComponentGUI("GEOM")
if NEW_SALOME:
@@ -163,9 +161,9 @@ class MODEL:
geompy.addToStudy(OY, "OY")
geompy.addToStudy(OZ, "OZ")
if len([e for e in elements if e["geometryType"] == "line"]) > 0:
if len([e for e in elements if e["geometry_type"] == "Edge"]) > 0:
buildingShapeType = "EDGE"
if len([e for e in elements if e["geometryType"] == "surface"]) > 0:
if len([e for e in elements if e["geometry_type"] == "Face"]) > 0:
buildingShapeType = "FACE"
### Define entities ###
@@ -175,31 +173,23 @@ class MODEL:
# Loop 1
for el in elements:
el["elemObj"] = self.makeObject(el["geometry"], el["geometryType"])
el["elemObj"] = self.makeObject(el["geometry"], el["geometry_type"])
el["connObjs"] = [None for _ in el["connections"]]
el["linkObjs"] = [None for _ in el["connections"]]
el["linkPointObjs"] = [[None, None] for _ in el["connections"]]
for j, rel in enumerate(el["connections"]):
conn = [
c for c in connections if c["referenceName"] == rel["relatedConnection"]
][0]
conn = [c for c in connections if c["ref_id"] == rel["related_connection"]][0]
if rel["eccentricity"]:
rel["index"] = len(conn["relatedElements"]) + 1
conn["relatedElements"].append(rel)
rel["index"] = len(conn["related_elements"]) + 1
conn["related_elements"].append(rel)
if not rel["eccentricity"]:
el["connObjs"][j] = self.makeObject(
conn["geometry"], conn["geometryType"]
)
el["connObjs"][j] = self.makeObject(conn["geometry"], conn["geometry_type"])
else:
if conn["geometryType"] == "point":
geometry = self.getLinkGeometry(
rel["eccentricity"], el["orientation"], conn["geometry"]
)
el["connObjs"][j] = self.makeObject(
geometry[0], conn["geometryType"]
)
if conn["geometry_type"] == "Vertex":
geometry = rel["eccentricity"]["point_on_element"], conn["geometry"]
el["connObjs"][j] = self.makeObject(geometry[0], conn["geometry_type"])
el["linkPointObjs"][j][0] = self.geompy.MakeVertex(
geometry[0][0], geometry[0][1], geometry[0][2]
@@ -211,27 +201,18 @@ class MODEL:
el["linkPointObjs"][j][0], el["linkPointObjs"][j][1]
)
else:
print(
"Eccentricity defined for a %s geometryType"
% conn["geometryType"]
)
el["partObj"] = self.makePartition(
[el["elemObj"]] + el["connObjs"], el["geometryType"]
)
print("Eccentricity defined for a %s geometry_type" % conn["geometry_type"])
el["partObj"] = self.makePartition([el["elemObj"]] + el["connObjs"], el["geometry_type"])
el["elemObj"] = geompy.GetInPlace(el["partObj"], el["elemObj"], True)
for j, rel in enumerate(el["connections"]):
el["connObjs"][j] = geompy.GetInPlace(
el["partObj"], el["connObjs"][j], True
)
el["connObjs"][j] = geompy.GetInPlace(el["partObj"], el["connObjs"][j], True)
for conn in connections:
conn["connObj"] = self.makeObject(conn["geometry"], conn["geometryType"])
conn["connObj"] = self.makeObject(conn["geometry"], conn["geometry_type"])
# Make assemble of Building Object
bldObjs = []
bldObjs.extend([el["partObj"] for el in elements])
bldObjs.extend(
flatten([[link for link in el["linkObjs"] if link] for el in elements])
)
bldObjs.extend(flatten([[link for link in el["linkObjs"] if link] for el in elements]))
bldObjs.extend([conn["connObj"] for conn in connections])
bldComp = geompy.MakeCompound(bldObjs)
@@ -240,59 +221,55 @@ class MODEL:
# Loop 2
for el in elements:
# geompy.addToStudy(el['partObj'], self.getGroupName(el['referenceName']))
geompy.addToStudyInFather(
el["partObj"], el["elemObj"], self.getGroupName(el["referenceName"])
)
# geompy.addToStudy(el['partObj'], self.getGroupName(el['ref_id']))
geompy.addToStudyInFather(el["partObj"], el["elemObj"], self.getGroupName(el["ref_id"]))
for j, rel in enumerate(el["connections"]):
conn = [
c for c in connections if c["referenceName"] == rel["relatedConnection"]
][0]
conn = [c for c in connections if c["ref_id"] == rel["related_connection"]][0]
rel["conn_string"] = None
if conn["geometryType"] == "point":
if conn["geometry_type"] == "Vertex":
rel["conn_string"] = "_0DC_"
if conn["geometryType"] == "line":
if conn["geometry_type"] == "Edge":
rel["conn_string"] = "_1DC_"
if conn["geometryType"] == "surface":
if conn["geometry_type"] == "Face":
rel["conn_string"] = "_2DC_"
geompy.addToStudyInFather(
el["partObj"],
el["connObjs"][j],
self.getGroupName(el["referenceName"])
+ rel["conn_string"]
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + rel["conn_string"] + self.getGroupName(rel["related_connection"]),
)
if rel["eccentricity"]:
pass
# geompy.addToStudy(el['linkObjs'][j], self.getGroupName(el['referenceName']) + '_1DR_' + self.getGroupName(rel['relatedConnection']))
# geompy.addToStudyInFather(el['linkObjs'][j], el['linkPointObjs'][j][0], self.getGroupName(rel['relatedConnection']) + '_0DC_' + self.getGroupName(el['referenceName']))
# geompy.addToStudyInFather(el['linkObjs'][j], el['linkPointObjs'][j][0], self.getGroupName(rel['relatedConnection']) + '_0DC_%g' % rel['index'])
# geompy.addToStudy(el['linkObjs'][j], self.getGroupName(el['ref_id']) + '_1DR_' + self.getGroupName(rel['related_connection']))
# geompy.addToStudyInFather(el['linkObjs'][j], el['linkPointObjs'][j][0], self.getGroupName(rel['related_connection']) + '_0DC_' + self.getGroupName(el['ref_id']))
# geompy.addToStudyInFather(el['linkObjs'][j], el['linkPointObjs'][j][0], self.getGroupName(rel['related_connection']) + '_0DC_%g' % rel['index'])
for conn in connections:
# geompy.addToStudy(conn['connObj'], self.getGroupName(conn['referenceName']))
geompy.addToStudyInFather(
conn["connObj"], conn["connObj"], self.getGroupName(conn["referenceName"])
)
# geompy.addToStudy(conn['connObj'], self.getGroupName(conn['ref_id']))
geompy.addToStudyInFather(conn["connObj"], conn["connObj"], self.getGroupName(conn["ref_id"]))
elapsed_time = time.time() - init_time
init_time += elapsed_time
print("Building Geometry Defined in %g sec" % (elapsed_time))
# Define and add groups for all curve and surface members
if len([e for e in elements if e["geometryType"] == "line"]) > 0:
if len([e for e in elements if e["geometry_type"] == "Edge"]) > 0:
# Make compound of requested group
compoundTemp = geompy.MakeCompound(
[e["elemObj"] for e in elements if e["geometryType"] == "line"]
)
compoundTemp = geompy.MakeCompound([e["elemObj"] for e in elements if e["geometry_type"] == "Edge"])
# Define group object and add to study
curveCompound = geompy.GetInPlace(bldComp, compoundTemp, True)
geompy.addToStudyInFather(bldComp, curveCompound, "CurveMembers")
if len([e for e in elements if e["geometryType"] == "surface"]) > 0:
rigid_links = list(flatten([[link for link in el["linkObjs"] if link] for el in elements]))
if len(rigid_links) > 0:
# Make compound of requested group
compoundTemp = geompy.MakeCompound(
[e["elemObj"] for e in elements if e["geometryType"] == "surface"]
)
compoundTemp = geompy.MakeCompound(rigid_links)
# Define group object and add to study
rigidLinkCompound = geompy.GetInPlace(bldComp, compoundTemp, True)
geompy.addToStudyInFather(bldComp, rigidLinkCompound, "RigidLinks")
if len([e for e in elements if e["geometry_type"] == "Face"]) > 0:
# Make compound of requested group
compoundTemp = geompy.MakeCompound([e["elemObj"] for e in elements if e["geometry_type"] == "Face"])
# Define group object and add to study
surfaceCompound = geompy.GetInPlace(bldComp, compoundTemp, True)
geompy.addToStudyInFather(bldComp, surfaceCompound, "SurfaceMembers")
@@ -300,45 +277,34 @@ class MODEL:
# Loop 3
for el in elements:
# el['partObj'] = geompy.RestoreGivenSubShapes(bldComp, [el['partObj']], GEOM.FSM_GetInPlace, False, False)[0]
geompy.addToStudyInFather(
bldComp, el["elemObj"], self.getGroupName(el["referenceName"])
)
geompy.addToStudyInFather(bldComp, el["elemObj"], self.getGroupName(el["ref_id"]))
for j, rel in enumerate(el["connections"]):
geompy.addToStudyInFather(
bldComp,
el["connObjs"][j],
self.getGroupName(el["referenceName"])
+ rel["conn_string"]
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + rel["conn_string"] + self.getGroupName(rel["related_connection"]),
)
if rel["eccentricity"]: # point geometry
geompy.addToStudyInFather(
bldComp,
el["linkObjs"][j],
self.getGroupName(el["referenceName"])
+ "_1DR_"
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + "_1DR_" + self.getGroupName(rel["related_connection"]),
)
geompy.addToStudyInFather(
bldComp,
el["linkPointObjs"][j][0],
self.getGroupName(rel["relatedConnection"])
+ "_0DC_"
+ self.getGroupName(el["referenceName"]),
self.getGroupName(rel["related_connection"]) + "_0DC_" + self.getGroupName(el["ref_id"]),
)
geompy.addToStudyInFather(
bldComp,
el["linkPointObjs"][j][1],
self.getGroupName(rel["relatedConnection"])
+ "_0DC_%g" % rel["index"],
self.getGroupName(rel["related_connection"]) + "_0DC_%g" % rel["index"],
)
for conn in connections:
# conn['connObj'] = geompy.RestoreGivenSubShapes(bldComp, [conn['connObj']], GEOM.FSM_GetInPlace, False, False)[0]
geompy.addToStudyInFather(
bldComp, conn["connObj"], self.getGroupName(conn["referenceName"])
)
geompy.addToStudyInFather(bldComp, conn["connObj"], self.getGroupName(conn["ref_id"]))
elapsed_time = time.time() - init_time
init_time += elapsed_time
@@ -359,15 +325,15 @@ class MODEL:
smesh = smeshBuilder.New(theStudy)
bldMesh = smesh.Mesh(bldComp)
Regular_1D = bldMesh.Segment()
Local_Length_1 = Regular_1D.LocalLength(meshSize, None, tolLoc)
Local_Length_1 = Regular_1D.LocalLength(mesh_size, None, tolLoc)
if buildingShapeType == "FACE":
NETGEN2D_ONLY = bldMesh.Triangle(algo=smeshBuilder.NETGEN_2D)
NETGEN2D_Pars = NETGEN2D_ONLY.Parameters()
NETGEN2D_Pars.SetMaxSize(meshSize)
NETGEN2D_Pars.SetMaxSize(mesh_size)
NETGEN2D_Pars.SetOptimize(1)
NETGEN2D_Pars.SetFineness(2)
NETGEN2D_Pars.SetMinSize(meshSize / 5.0)
NETGEN2D_Pars.SetMinSize(mesh_size / 5.0)
NETGEN2D_Pars.SetUseSurfaceCurvature(1)
NETGEN2D_Pars.SetQuadAllowed(1)
NETGEN2D_Pars.SetSecondOrder(0)
@@ -383,142 +349,111 @@ class MODEL:
smesh.SetName(NETGEN2D_ONLY.GetAlgorithm(), "NETGEN2D_ONLY")
smesh.SetName(NETGEN2D_Pars, "NETGEN2D_Pars")
smesh.SetName(bldMesh.GetMesh(), "bldMesh")
smesh.SetName(bldMesh.GetMesh(), "{{ mesh_name }}")
elapsed_time = time.time() - init_time
init_time += elapsed_time
print("Meshing Operations Completed in %g sec" % (elapsed_time))
# Define and add groups for all curve and surface members
if len([e for e in elements if e["geometryType"] == "line"]) > 0:
if len([e for e in elements if e["geometry_type"] == "Edge"]) > 0:
tempgroup = bldMesh.GroupOnGeom(curveCompound, "CurveMembers", SMESH.EDGE)
smesh.SetName(tempgroup, "CurveMembers")
if len([e for e in elements if e["geometryType"] == "surface"]) > 0:
if len(rigid_links) > 0:
tempgroup = bldMesh.GroupOnGeom(rigidLinkCompound, "RigidLinks", SMESH.EDGE)
smesh.SetName(tempgroup, "RigidLinks")
if len([e for e in elements if e["geometry_type"] == "Face"]) > 0:
tempgroup = bldMesh.GroupOnGeom(surfaceCompound, "SurfaceMembers", SMESH.FACE)
smesh.SetName(tempgroup, "SurfaceMembers")
# Define groups in Mesh
for el in elements:
if el["geometryType"] == "line":
if el["geometry_type"] == "Edge":
shapeType = SMESH.EDGE
if el["geometryType"] == "surface":
if el["geometry_type"] == "Face":
shapeType = SMESH.FACE
tempgroup = bldMesh.GroupOnGeom(
el["elemObj"], self.getGroupName(el["referenceName"]), shapeType
)
smesh.SetName(tempgroup, self.getGroupName(el["referenceName"]))
tempgroup = bldMesh.GroupOnGeom(el["elemObj"], self.getGroupName(el["ref_id"]), shapeType)
smesh.SetName(tempgroup, self.getGroupName(el["ref_id"]))
# tempgroup = bldMesh.GroupOnGeom(el["elemObj"], self.getGroupName(el["ref_id"]), SMESH.NODE)
# smesh.SetName(tempgroup, self.getGroupName(el["ref_id"]))
for j, rel in enumerate(el["connections"]):
tempgroup = bldMesh.GroupOnGeom(
el["connObjs"][j],
self.getGroupName(el["referenceName"])
+ rel["conn_string"]
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + rel["conn_string"] + self.getGroupName(rel["related_connection"]),
SMESH.NODE,
)
smesh.SetName(
tempgroup,
self.getGroupName(el["referenceName"])
+ rel["conn_string"]
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + rel["conn_string"] + self.getGroupName(rel["related_connection"]),
)
rel["node"] = (
bldMesh.GetIDSource(tempgroup.GetNodeIDs(), SMESH.NODE)
).GetIDs()[0]
rel["node"] = (bldMesh.GetIDSource(tempgroup.GetNodeIDs(), SMESH.NODE)).GetIDs()[0]
if rel["eccentricity"]:
tempgroup = bldMesh.GroupOnGeom(
el["linkObjs"][j],
self.getGroupName(el["referenceName"])
+ "_1DR_"
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + "_1DR_" + self.getGroupName(rel["related_connection"]),
SMESH.EDGE,
)
smesh.SetName(
tempgroup,
self.getGroupName(el["referenceName"])
+ "_1DR_"
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + "_1DR_" + self.getGroupName(rel["related_connection"]),
)
tempgroup = bldMesh.GroupOnGeom(
el["linkPointObjs"][j][0],
self.getGroupName(rel["relatedConnection"])
+ "_0DC_"
+ self.getGroupName(el["referenceName"]),
self.getGroupName(rel["related_connection"]) + "_0DC_" + self.getGroupName(el["ref_id"]),
SMESH.NODE,
)
smesh.SetName(
tempgroup,
self.getGroupName(rel["relatedConnection"])
+ "_0DC_"
+ self.getGroupName(el["referenceName"]),
self.getGroupName(rel["related_connection"]) + "_0DC_" + self.getGroupName(el["ref_id"]),
)
rel["eccNode"] = (
bldMesh.GetIDSource(tempgroup.GetNodeIDs(), SMESH.NODE)
).GetIDs()[0]
rel["eccNode"] = (bldMesh.GetIDSource(tempgroup.GetNodeIDs(), SMESH.NODE)).GetIDs()[0]
tempgroup = bldMesh.GroupOnGeom(
el["linkPointObjs"][j][1],
self.getGroupName(rel["relatedConnection"])
self.getGroupName(rel["related_connection"])
+ "_0DC_"
+ self.getGroupName(rel["relatedConnection"]),
+ self.getGroupName(rel["related_connection"]),
SMESH.NODE,
)
smesh.SetName(
tempgroup,
self.getGroupName(rel["relatedConnection"])
+ "_0DC_%g" % rel["index"],
self.getGroupName(rel["related_connection"]) + "_0DC_%g" % rel["index"],
)
for conn in connections:
tempgroup = bldMesh.GroupOnGeom(
conn["connObj"], self.getGroupName(conn["referenceName"]), SMESH.NODE
)
smesh.SetName(tempgroup, self.getGroupName(conn["referenceName"]))
tempgroup = bldMesh.GroupOnGeom(conn["connObj"], self.getGroupName(conn["ref_id"]), SMESH.NODE)
smesh.SetName(tempgroup, self.getGroupName(conn["ref_id"]))
nodesId = bldMesh.GetIDSource(tempgroup.GetNodeIDs(), SMESH.NODE)
tempgroup = bldMesh.Add0DElementsToAllNodes(
nodesId, self.getGroupName(conn["referenceName"])
)
smesh.SetName(tempgroup, self.getGroupName(conn["referenceName"] + "_0D"))
if conn["geometryType"] == "point":
tempgroup = bldMesh.Add0DElementsToAllNodes(nodesId, self.getGroupName(conn["ref_id"]))
smesh.SetName(tempgroup, self.getGroupName(conn["ref_id"] + "_0D"))
if conn["geometry_type"] == "Vertex":
conn["node"] = nodesId.GetIDs()[0]
if conn["geometryType"] == "line":
tempgroup = bldMesh.GroupOnGeom(
conn["connObj"], self.getGroupName(conn["referenceName"]), SMESH.EDGE
)
smesh.SetName(tempgroup, self.getGroupName(conn["referenceName"]))
if conn["geometryType"] == "surface":
tempgroup = bldMesh.GroupOnGeom(
conn["connObj"], self.getGroupName(conn["referenceName"]), SMESH.FACE
)
smesh.SetName(tempgroup, self.getGroupName(conn["referenceName"]))
if conn["geometry_type"] == "Edge":
tempgroup = bldMesh.GroupOnGeom(conn["connObj"], self.getGroupName(conn["ref_id"]), SMESH.EDGE)
smesh.SetName(tempgroup, self.getGroupName(conn["ref_id"]))
if conn["geometry_type"] == "Face":
tempgroup = bldMesh.GroupOnGeom(conn["connObj"], self.getGroupName(conn["ref_id"]), SMESH.FACE)
smesh.SetName(tempgroup, self.getGroupName(conn["ref_id"]))
# create 1D SEG2 spring elements
for el in elements:
for j, rel in enumerate(el["connections"]):
conn = [
c for c in connections if c["referenceName"] == rel["relatedConnection"]
][0]
if conn["geometryType"] == "point":
conn = [c for c in connections if c["ref_id"] == rel["related_connection"]][0]
if conn["geometry_type"] == "Vertex":
grpName = bldMesh.CreateEmptyGroup(
SMESH.EDGE,
self.getGroupName(el["referenceName"])
+ "_1DS_"
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + "_1DS_" + self.getGroupName(rel["related_connection"]),
)
smesh.SetName(
grpName,
self.getGroupName(el["referenceName"])
+ "_1DS_"
+ self.getGroupName(rel["relatedConnection"]),
self.getGroupName(el["ref_id"]) + "_1DS_" + self.getGroupName(rel["related_connection"]),
)
if not rel["eccentricity"]:
conn = [
conn
for conn in connections
if conn["referenceName"] == rel["relatedConnection"]
][0]
conn = [conn for conn in connections if conn["ref_id"] == rel["related_connection"]][0]
grpName.Add([bldMesh.AddEdge([conn["node"], rel["node"]])])
else:
grpName.Add([bldMesh.AddEdge([rel["eccNode"], rel["node"]])])
@@ -545,26 +480,30 @@ class MODEL:
except:
print("ExportMED() failed. Invalid file name?")
if salome.sg.hasDesktop():
if NEW_SALOME:
salome.sg.updateObjBrowser()
else:
salome.sg.updateObjBrowser(1)
# if salome.sg.hasDesktop():
# if NEW_SALOME:
# salome.sg.updateObjBrowser()
# else:
# salome.sg.updateObjBrowser(1)
elapsed_time = init_time - start_time
print("ALL Operations Completed in %g sec" % (elapsed_time))
if __name__ == "__main__":
fileNames = ["structure_01"]
files = fileNames
model = MODEL()
meshSize = 0.1
for el in model.elements:
for j, conn in enumerate(el["connections"]):
d = model.geompy.MinDistance(el["elemObj"], el["connObjs"][j])
if d > 0:
print(f'NOTE: Element {el["ref_id"]} and connection {conn["ref_id"]} have a distance of {d}')
# elif d == 0:
# print(
# f'SUCCESS: Element {el["ref_id"]} and connection {conn["ref_id"]} have a distance of {d}'
# )
for fileName in files:
BASE_PATH = Path(
"/home/jesusbill/Dev-Projects/github.com/IfcOpenShell/analysis-models/models/"
)
DATAFILENAME = BASE_PATH / fileName / f"{fileName}.json"
MEDFILENAME = BASE_PATH / fileName / f"{fileName}.med"
model = MODEL(DATAFILENAME, str(MEDFILENAME), meshSize)
if salome.sg.hasDesktop():
if model.NEW_SALOME:
salome.sg.updateObjBrowser()
else:
salome.sg.updateObjBrowser(1)
+1 -1
View File
@@ -134,7 +134,7 @@ class Csv2Ifc:
category = category.replace("Rate", "")
category = category.strip()
cost_value.Category = category
else:
elif cost_item["CostValues"]:
cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"])
cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(cost_item["CostValues"])
if self.is_schedule_of_rates:
@@ -557,6 +557,45 @@ bar.
For more information, consult the :doc:`shape builder documentation
<autoapi/ifcopenshell/util/shape_builder/index>`.
OpenCASCADE representations
---------------------------
If you are familiar with OpenCASCADE's Python bindings, you may also use
OpenCASCADE directly to create geometry. Since both OpenCASCADE and IFC are
inspired by STEP, the majority of OpenCASCADE shapes will be able to be
converted into IFC. For example:
.. code-block:: python
import ifcopenshell
import ifcopenshell.geom
from OCC.Core.gp import gp_Pnt
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
outer = BRepPrimAPI_MakeBox(gp_Pnt(-5000., -180., -2000.), gp_Pnt(5000., 5180., 3000.)).Shape()
inner = BRepPrimAPI_MakeBox(gp_Pnt(-4640., 180., 0.), gp_Pnt(4640., 4820., 3000.)).Shape()
window1 = BRepPrimAPI_MakeBox(gp_Pnt(-5000., -180., 400.), gp_Pnt( 500., 1180., 2000.)).Shape()
window2 = BRepPrimAPI_MakeBox(gp_Pnt( 2070., -180., 400.), gp_Pnt(3930., 180., 2000.)).Shape()
building_shell = BRepAlgoAPI_Cut(
BRepAlgoAPI_Cut(
BRepAlgoAPI_Cut(outer, inner).Shape(),
window1
).Shape(),
window2
).Shape()
model = ifcopenshell.file(schema="IFC2X3")
product_definition = ifcopenshell.geom.serialise("IFC2X3", building_shell, False)
product_definition = model.add(product_definition)
.. warning::
The schema version is significant. IFC2X3 is very limited with regard to
curved surfaces, so generally non-planar surfaces will fail to serialise in
IFC2X3. Newer versions such as IFC4 can support more serialisations. If a
serialisation fails, ``ifcopenshell.geom.serialise`` will return ``None``.
Manual representations
----------------------
@@ -12,6 +12,14 @@ The simplest way to process any geometry in a standardised fashion is to use the
IfcOpenShell ``create_shape()`` function. This will provide a list of vertices,
edges, and faces, or alternatively an OpenCASCADE BRep.
.. warning::
This section describes individual processing only. This is useful for
learning how geometry processing works, but is not recommended for practical
applications. See the `Geometry iterator`_ section below after reading this
to see how to process geometry with multiple threads.
Here is a simple example of processing a single wall into a list of vertices and
faces. In this example, a ``shape`` variable is returned, which holds geometry
related information in ``shape.geometry``:
@@ -158,39 +166,34 @@ In these scenarios, a ``geometry`` is returned directly, equivalent to
geometry = geom.create_shape(settings, ifc_file.by_type("IfcProfileDef")[0])
Process individual element with multiple shape representations
---------------------
When an element contains multiple shape representations with the same identifier or when you want more explicit control over which representation is processed (e.g `Body` or `Tesselation`), you can use the third parameter of ``create_shape()`` to nominate a specific shape representation to be processed in the context of a product.
The element in your ifc file might look like this.
When an element contains multiple shape representations with the same
identifier or when you want more explicit control over which representation is
processed (e.g ``Body`` or ``Tessellation``), you can use the third parameter of
``create_shape()`` to nominate a specific shape representation to be processed
in the context of a product. The element in your ifc file might look like
this.
.. code-block:: ifc
#1618937=IFCSHAPEREPRESENTATION(#4,'Body','BRep',(#1617476));
#1618938=IFCSHAPEREPRESENTATION(#4,'Body','BRep',(#1617583));
#1618939=IFCSHAPEREPRESENTATION(#4,'Body','BRep',(#1617630));
#1618957=IFCPRODUCTDEFINITIONSHAPE($,$,(#1618937,#1618938,#1618939));
#1618958=IFCWINDOW('0Rrp2csNr07QrVCrEBJezu',#9,'test','test',$,#1618936,#1618957,'\X2\5EFA7B517A97\X0\',$,$,$,$,$);
#1=IFCSHAPEREPRESENTATION(#4,'Body','BRep',(#1617476));
#2=IFCSHAPEREPRESENTATION(#4,'Body','BRep',(#1617583));
#3=IFCSHAPEREPRESENTATION(#4,'Body','BRep',(#1617630));
#5=IFCPRODUCTDEFINITIONSHAPE($,$,(#1,#2,#3));
#6=IFCWINDOW('0Rrp2csNr07QrVCrEBJezu',#9,'test','test',$,#7,#5,'test',$,$,$,$,$);
In order to get the geometry data (e.g. vertices) for this ``IFCWINDOW``, we can use the Python code below:
In order to get the geometry data (e.g. vertices) for this ``IfcWindow``, we can use the Python code below:
.. code-block:: python
import ifcopenshell
import ifcopenshell.geom
settings = ifcopenshell.geom.settings()
ifc_file = ifcopenshell.open('window.ifc')
window = ifc_file.by_type('IfcWindow')[0] # Get the IFCWINDOW that contains multiple Representations
representations = window.Representation.Representations
for rep in representations:
shape = ifcopenshell.geom.create_shape(settings, window, rep) # The third parameter specifies which representation of the window is handled
vertices = shape.geometry.verts
print(vertices)
for representation in representations:
# ... code that filters which representation you want ...
shape = ifcopenshell.geom.create_shape(settings, window, representation)
.. seealso::
.. note::
You may still need to determine which representation don't contain geometry data or some type like Box need to be discarded at render time.
You may find the ``ifcopenshell.util.representation`` module useful to
filter out specific representations.
Geometry iterator
@@ -210,11 +213,6 @@ By default, the geometry iterator processes all 3D geometry in a model from all
elements, and returns a list of X Y Z vertex ordinates in a flattened list, as
well as a flattened list of triangulated faces denoted by vertex indices.
There are a variety of configuration settings to get different output. For
example, you may filter elements from processing, extract 2D data, or return
non-triangulated OpenCASCADE BReps. For more information on the various
settings, see :doc:`Geometry Settings<../ifcopenshell/geometry_settings>`.
Here is a simple example in Python:
.. code-block:: python
@@ -241,28 +239,19 @@ Here is a simple example in Python:
break
The geometry iterator can process specific elements' geometry by using parameter ``include`` and ``exclude``, ``include`` and ``exclude`` cannot be specified simultaneously. Code below shows how to get specific wall's geometry (e.g. walls[1]):
There are a variety of configuration settings to get different output. For
example, you may filter elements from processing, extract 2D data, or return
non-triangulated OpenCASCADE BReps. For more information on the various
settings, see :doc:`Geometry Settings<../ifcopenshell/geometry_settings>`.
One of the more common settings used is the ``include`` setting, which
specifies only to process certain geometry. For example, this iterator will
only process wall elements.
.. code-block:: python
import ifcopenshell
import ifcopenshell.geom
import multiprocessing
import ifcopenshell.util.shape
ifc = ifcopenshell.open('model.ifc')
walls = ifc.by_type('ifcwall')
settings = ifcopenshell.geom.settings()
iterator = ifcopenshell.geom.iterator(settings, ifc, multiprocessing.cpu_count(), include=[walls[1]])
valid_file = iterator.initialize()
while True:
shape = iterator.get()
element = ifc.by_id(shape.id)
geometry = shape.geometry
verts = geometry.verts
print(verts)
if not iterator.next():
break
walls = ifc.by_type('IfcWall')
iterator = ifcopenshell.geom.iterator(settings, ifc, multiprocessing.cpu_count(), include=walls)
.. note::
@@ -297,3 +286,43 @@ specifically pinpoint the Radius parameter.
Given the advanced nature of manual processing, it is generally not recommended
except in specific tasks.
Geometry serialisation
----------------------
Geometry may be serialised into many different formats using
:doc:`IfcConvert<../ifcconvert>`. Alternatively, you may also access the
serialiser with Python to customise the conversion, such as by writing a script
the modifies the IFC on the fly before converting it, or writing complex
include and exclude filters.
Here is a typical example to serialising to glTF / glb.
.. code-block:: python
import ifcopenshell
import ifcopenshell.geom
import multiprocessing
settings = ifcopenshell.geom.settings()
settings.set(settings.STRICT_TOLERANCE, True)
settings.set(settings.INCLUDE_CURVES, True)
# Setting element GUIDs is optional, but useful to uniquely identify objects in non-semantic formats.
settings.set(settings.USE_ELEMENT_GUIDS, True)
# Note that applying default materials is required in glTF serialisation.
settings.set(settings.APPLY_DEFAULT_MATERIALS, True)
serialiser = ifcopenshell.geom.serializers.gltf("output.glb", settings)
# Alternatively, this is an example for OBJ
# serialiser = ifcopenshell.geom.serializers.obj('output.obj', 'output.mtl', settings)
serialiser.setFile(self.file)
serialiser.setUnitNameAndMagnitude("METER", 1.0)
serialiser.writeHeader()
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count())
if iterator.initialize():
while True:
serialiser.write(iterator.get())
if not iterator.next():
break
serialiser.finalize()
@@ -27,21 +27,21 @@ the API.
1. Choose which version to download based on your operating system, Python
version, and computer architecture.
+-------------+----------------+----------------+----------------+----------------+------------------+
| | Linux 64bit | Windows 32bit | Windows 64bit | MacOS 64bit | MacOS M1 64bit |
+=============+================+================+================+================+==================+
| Python 3.6 | py36-linux64_ | py36-win32_ | py36-win64_ | py36-macos64_ | N/A |
+-------------+----------------+----------------+----------------+----------------+------------------+
| Python 3.7 | py37-linux64_ | py37-win32_ | py37-win64_ | py37-macos64_ | py37-macosm164_ |
+-------------+----------------+----------------+----------------+----------------+------------------+
| Python 3.8 | py38-linux64_ | py38-win32_ | py38-win64_ | py38-macos64_ | py38-macosm164_ |
+-------------+----------------+----------------+----------------+----------------+------------------+
| Python 3.9 | py39-linux64_ | py39-win32_ | py39-win64_ | py39-macos64_ | py39-macosm164_ |
+-------------+----------------+----------------+----------------+----------------+------------------+
| Python 3.10 | py310-linux64_ | py310-win32_ | py310-win64_ | py310-macos64_ | py310-macosm164_ |
+-------------+----------------+----------------+----------------+----------------+------------------+
| Python 3.11 | py311-linux64_ | py311-win32_ | py311-win64_ | N/A | py311-macosm164_ |
+-------------+----------------+----------------+----------------+----------------+------------------+
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| | Linux 64bit | Windows 32bit | Windows 64bit | MacOS Intel 64bit | MacOS Silicon 64bit |
+=============+================+================+================+===================+=====================+
| Python 3.6 | py36-linux64_ | py36-win32_ | py36-win64_ | py36-macos64_ | N/A |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| Python 3.7 | py37-linux64_ | py37-win32_ | py37-win64_ | py37-macos64_ | py37-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| Python 3.8 | py38-linux64_ | py38-win32_ | py38-win64_ | py38-macos64_ | py38-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| Python 3.9 | py39-linux64_ | py39-win32_ | py39-win64_ | py39-macos64_ | py39-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| Python 3.10 | py310-linux64_ | py310-win32_ | py310-win64_ | py310-macos64_ | py310-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| Python 3.11 | py311-linux64_ | py311-win32_ | py311-win64_ | N/A | py311-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
.. _py36-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-6c9e130-linux64.zip
.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-6c9e130-linux64.zip
@@ -121,7 +121,8 @@ Conda
.. note::
Installing IfcOpenShell from Conda will also install IfcConvert.
Installing IfcOpenShell from Conda will also install IfcConvert. Conda also
supports Linux AArch64.
.. note::
@@ -22,7 +22,7 @@ from datetime import datetime
class Usecase:
def __init__(self, file, name=None, predefined_type="NOTDEFINED", object_type=None):
def __init__(self, file, name=None, predefined_type="NOTDEFINED"):
"""Add a new cost schedule
A cost schedule is a group of cost items which typically represent a
@@ -54,7 +54,7 @@ class Usecase:
item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule)
"""
self.file = file
self.settings = {"name": name, "predefined_type": predefined_type, "object_type": object_type}
self.settings = {"name": name, "predefined_type": predefined_type}
def execute(self):
cost_schedule = ifcopenshell.api.run(
@@ -65,6 +65,4 @@ class Usecase:
name=self.settings["name"],
)
cost_schedule.UpdateDate = ifcopenshell.util.date.datetime2ifc(datetime.now(), "IfcDateTime")
if self.settings["object_type"]:
cost_schedule.ObjectType = self.settings["object_type"]
return cost_schedule
@@ -52,7 +52,7 @@ class Usecase:
if inverse.RelatingObject == self.settings["cost_item"]:
for related_object in inverse.RelatedObjects:
ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=related_object)
elif inverse.RelatedObjects == tuple(self.settings["cost_item"]):
elif inverse.RelatedObjects == (self.settings["cost_item"],):
history = inverse.OwnerHistory
self.file.remove(inverse)
if history:
@@ -58,7 +58,12 @@ class Usecase:
if self.file.schema == "IFC2X3":
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
else:
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(points))
dimensions = len(points[0])
if dimensions == 2:
ifc_points = self.file.createIfcCartesianPointList2D(points)
elif dimensions == 3:
ifc_points = self.file.createIfcCartesianPointList3D(points)
curve = self.file.createIfcIndexedPolyCurve(ifc_points)
return self.file.createIfcArbitraryClosedProfileDef("AREA", self.settings["name"], curve)
def convert_si_to_unit(self, co):
@@ -74,7 +74,12 @@ class Usecase:
outer_curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(outer_points))
inner_curves = []
for inner_point in inner_points:
inner_curves.append(self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(inner_point)))
dimensions = len(inner_point[0])
if dimensions == 2:
ifc_points = self.file.createIfcCartesianPointList2D(inner_point)
elif dimensions == 3:
ifc_points = self.file.createIfcCartesianPointList3D(inner_point)
inner_curves.append(self.file.createIfcIndexedPolyCurve(ifc_points))
return self.file.createIfcArbitraryProfileDefWithVoids("AREA", self.settings["name"], outer_curve, inner_curves)
def convert_si_to_unit(self, co):
@@ -30,8 +30,6 @@ class Usecase:
called NetSideArea with a area value of "4.2". Quantities are grouped
into quantity sets, so that related quantities are grouped together.
Quantities can only be assigned to occurrences, not types.
Quantities are similar to, but different from properties in that they
may store a method of measurement or formula. Quantities may also have
parametric relationships to other calculated values, such as cost
@@ -70,7 +70,7 @@ class Usecase:
elif self.settings["relating_product"]:
for reference in self.settings["relating_product"].ReferencedBy:
if reference.is_a("IfcRelAssignsToProduct"):
related_object = referenced_by.RelatedObjects[0]
related_object = reference.RelatedObjects[0]
if related_object:
assignments = self.settings["related_object"].HasAssignments
for assignment in assignments:
@@ -139,7 +139,7 @@ class entity_instance(object):
# ugh circular imports, name collisions
from . import file
return file.file.from_pointer(self.wrapped_data.file_pointer())
return file.from_pointer(self.wrapped_data.file_pointer())
def __getattr__(self, name):
INVALID, FORWARD, INVERSE = range(3)
+6 -3
View File
@@ -252,8 +252,11 @@ class file(object):
self.future = []
self.transaction = None
# Temporarily commented out until bot builds are available and tested to prevent user bugs.
# file_dict[self.file_pointer()] = self
import weakref
file_dict[self.file_pointer()] = weakref.ref(self)
def __del__(self):
del file_dict[self.file_pointer()]
def set_history_size(self, size):
self.history_size = size
@@ -575,4 +578,4 @@ class file(object):
@staticmethod
def from_pointer(v):
return file_dict.get(v)
return file_dict.get(v)()
@@ -0,0 +1,23 @@
{
"IfcPropertyBoundedValue": {
"Specification": "Description"
},
"IfcPropertyEnumeratedValue": {
"Specification": "Description"
},
"IfcPropertyListValue": {
"Specification": "Description"
},
"IfcPropertyReferenceValue": {
"Specification": "Description"
},
"IfcPropertyTableValue": {
"Specification": "Description"
},
"IfcPropertySingleValue": {
"Specification": "Description"
},
"IfcComplexProperty": {
"Specification": "Description"
}
}
@@ -54,16 +54,16 @@ def reassign_class(ifc_file, element, new_class):
Attempts to change the class (entity name) of `element` to `new_class` by
removing element and recreating a similar instance of type `new_class`
with the same id.
In certain cases it may affect the structure of inversely related instances:
- Multiple occurrences of reassigned instance within the same aggregate
(such as start and end-point of polyline)
- Occurrences of reassigned instance within an ordered aggregate
(such as IfcRelNests)
It's unlikely that this affects real-world usage of this function.
"""
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema)
try:
declaration = schema.declaration_by_name(new_class)
@@ -132,7 +132,7 @@ class BatchReassignClass:
for inverse, replacements in self.replacements.items():
for index, element_map in replacements.items():
value = inverse[index]
new = inverse.walk(lambda x : True, lambda v: element_map.get(v, v), value)
new = inverse.walk(lambda x: True, lambda v: element_map.get(v, v), value)
if value != new:
inverse[index] = new
@@ -151,8 +151,11 @@ class Migrator:
self.class_4_to_2x3 = json.load(open(os.path.join(cwd, "class_4_to_2x3.json"), "r"))
self.class_2x3_to_4 = json.load(open(os.path.join(cwd, "class_2x3_to_4.json"), "r"))
# IFC4 classes, and their IFC4 attribute : IFC2X3 attributes
self.attribute_4_to_2x3 = json.load(open(os.path.join(cwd, "attribute_4_to_2x3.json"), "r"))
# IFC classes, and their IFC attributes mapping
self.attributes_mapping = {
("IFC4", "IFC2X3"): json.load(open(os.path.join(cwd, "attribute_4_to_2x3.json"), "r")),
("IFC4X3", "IFC4"): json.load(open(os.path.join(cwd, "attribute_4x3_to_4.json"), "r")),
}
self.default_values = {
"ChangeAction": "NOCHANGE",
@@ -241,46 +244,72 @@ class Migrator:
self.migrate_attribute(attribute, element, new_file, new_element, new_element_schema)
return new_element
def migrate_attribute(self, attribute, element, new_file, new_element, new_element_schema):
def find_equivalent_attribute(self, new_element, attribute, element, attributes_mapping, reverse_mapping=False):
# print("Searching for an equivalent", element, new_element, attribute.name())
try:
if reverse_mapping:
equivalent_map = attributes_mapping[new_element.is_a()]
equivalent = list(equivalent_map.keys())[list(equivalent_map.values()).index(attribute.name())]
else:
equivalent = attributes_mapping[new_element.is_a()][attribute.name()]
if hasattr(element, equivalent):
# print("Equivalent found", equivalent)
value = getattr(element, equivalent)
else:
return
except Exception as e:
print(
"Unable to find equivalent attribute of {} to migrate from {} to {}".format(
attribute.name(), element, new_element
)
)
raise e
def migrate_attribute(
self, attribute, element, new_file: ifcopenshell.file, new_element, new_element_schema
):
# NOTE: `attribute` is an attribute in new file schema
# print("Migrating attribute", element, new_element, attribute.name())
old_file = element.wrapped_data.file
if hasattr(element, attribute.name()):
value = getattr(element, attribute.name())
# print("Attribute names matched", value)
elif new_file.schema == "IFC2X3":
elif new_file.schema == "IFC2X3" and old_file.schema == "IFC4":
# IFC4 to IFC2X3: We know the IFC2X3 attribute name, but not its IFC4 equivalent
# print("Searching for an equivalent", new_element, attribute.name())
try:
equivalent_map = self.attribute_4_to_2x3[new_element.is_a()]
equivalent = list(equivalent_map.keys())[list(equivalent_map.values()).index(attribute.name())]
if hasattr(element, equivalent):
# print("Equivalent found", equivalent)
value = getattr(element, equivalent)
else:
return
except:
print(
"Unable to find equivalent attribute of {} to migrate from {} to {}".format(
attribute.name(), element, new_element
)
value = self.find_equivalent_attribute(
new_element, attribute, element, self.attributes_mapping[("IFC4", "IFC2X3")], reverse_mapping=True
)
return # We tried our best
elif new_file.schema == "IFC4":
except: # We tried our best
return
elif new_file.schema == "IFC4" and old_file.schema == "IFC2X3":
# IFC2X3 to IFC4: We know the IFC4 attribute name, but not its IFC2X3 equivalent
# print("Searching for an equivalent", element, new_element, attribute.name())
try:
equivalent = self.attribute_4_to_2x3[new_element.is_a()][attribute.name()]
# print("Searching for equivalent", equivalent)
if hasattr(element, equivalent):
value = getattr(element, equivalent)
else:
return
except:
print(
"Unable to find equivalent attribute of {} to migrate from {} to {}".format(
attribute.name(), element, new_element
)
value = self.find_equivalent_attribute(
new_element, attribute, element, self.attributes_mapping[("IFC4", "IFC2X3")]
)
return # We tried our best
except: # We tried our best
return
elif new_file.schema == "IFC4X3" and old_file.schema == "IFC4":
try:
value = self.find_equivalent_attribute(
new_element, attribute, element, self.attributes_mapping[("IFC4X3", "IFC4")]
)
except: # We tried our best
return
try:
value
except UnboundLocalError:
print(
f"Couldn't match attribute {attribute.name()} by name to migrate from {element} "
f"to {new_element} and there is no special mapping to handle migration "
f"from {old_file.schema} -> {new_file.schema}"
)
return
# print("Continuing migration of {} to migrate from {} to {}".format(attribute.name(), element, new_element))
if value is None and not attribute.optional():
@@ -55,19 +55,20 @@ filter_elements_grammar = lark.Lark(
ifc_class: /Ifc\\w+/
value: special | quoted_string | regex_string | unquoted_string
unquoted_string: /[^,.=\\s]+/
unquoted_string: /[^,.=><*!\\s]+/
regex_string: "/" /[^\\/]+/ "/"
quoted_string: ESCAPED_STRING
special: null | true | false
comparison: not? equals | morethanequalto | lessthanequalto | morethan | lessthan
comparison: not? equals | morethanequalto | lessthanequalto | morethan | lessthan | not? contains
not: "!"
equals: "="
morethanequalto: ">="
lessthanequalto: "<="
morethan: ">"
lessthan: "<"
contains: "*="
null: "NULL"
true: "TRUE"
false: "FALSE"
@@ -557,18 +558,24 @@ class FacetTransformer(lark.Transformer):
return tree
def comparison(self, args):
if args[0].data == "equals":
return "="
elif args[0].data == "morethanequalto":
return ">="
elif args[0].data == "lessthanequalto":
return "<="
elif args[0].data == "morethan":
return ">"
elif args[0].data == "lessthan":
return "<"
if args[0].data == "not":
comparison = args[1].data
is_not = "!"
else:
return "!="
comparison = args[0].data
is_not = ""
return (
is_not
+ {
"equals": "=",
"morethanequalto": ">=",
"lessthanequalto": "<=",
"morethan": ">",
"lessthan": "<",
"contains": "*=",
}[comparison]
)
def keys(self, args):
return self.value(args)
@@ -595,35 +602,42 @@ class FacetTransformer(lark.Transformer):
return False
def compare(self, element_value, comparison, value):
if element_value:
if isinstance(element_value, (list, tuple)):
return any(self.compare(ev, comparison, value) for ev in element_value)
elif isinstance(value, re.Pattern):
result = bool(value.match(element_value)) if element_value is not None else False
elif isinstance(value, str):
if isinstance(element_value, int):
value = int(value)
elif isinstance(element_value, float):
value = float(value)
if comparison == "=":
result = element_value == value
elif comparison == ">":
result = element_value > value
elif comparison == "<":
result = element_value < value
elif comparison == ">=":
result = element_value >= value
elif comparison == "<= ":
result = element_value <= value
else:
result = element_value != value
if isinstance(element_value, (list, tuple)):
return any(self.compare(ev, comparison, value) for ev in element_value)
elif isinstance(value, str):
if isinstance(element_value, int):
value = int(value)
elif isinstance(element_value, float):
value = float(value)
elif value in (None, True, False):
result = element_value is value
else:
result = False
return result
if isinstance(element_value, (int, float)):
operator = comparison.lstrip("!")
if operator == ">=":
result = element_value >= value
elif operator == "<=":
result = element_value <= value
elif operator == ">":
result = element_value > value
elif operator == "<":
result = element_value < value
else:
result = element_value == value # Tolerance?
elif isinstance(element_value, str):
operator = comparison.lstrip("!")
if operator == "*=":
result = value in element_value
else:
result = element_value == value
else:
result = element_value == value
elif isinstance(value, re.Pattern):
result = bool(value.match(element_value)) if element_value is not None else False
elif value in (None, True, False):
result = element_value is value
if comparison.startswith("!"):
return not result
return result
class Selector:
@@ -18,6 +18,7 @@
import datetime
import ifcopenshell.util.date
from math import floor
from functools import lru_cache
@@ -221,7 +222,7 @@ def is_work_time_applicable_to_day(work_time, day):
return False # TODO
elif recurrence.RecurrenceType == "MONTHLY_BY_POSITION":
if not recurrence.Interval and not recurrence.Occurrences:
return (day.weekday() + 1) in recurrence.WeekdayComponent and math.floor(
return (day.weekday() + 1) in recurrence.WeekdayComponent and floor(
day.day / 7
) + 1 == recurrence["Position"]
return False # TODO
@@ -237,7 +238,7 @@ def is_work_time_applicable_to_day(work_time, day):
return (
day.month in recurrence.MonthComponent
and (day.weekday() + 1) in recurrence.WeekdayComponent
and math.floor(day.day / 7) + 1 == recurrence.Position
and floor(day.day / 7) + 1 == recurrence.Position
)
return False # TODO
@@ -217,7 +217,8 @@ def get_edges(geometry):
def get_faces(geometry):
"""Get all the faces as a numpy array
Faces are always triangulated.
Faces are always triangulated. If the shape is a BRep and you want to get
the original untriangulated output, refer to ``get_edges``.
Results are a nested numpy array e.g. [[f1v1, f1v2, f1v3], [f2v1, f2v2, f2v3], ...]
@@ -247,7 +248,7 @@ def get_shape_vertices(shape, geometry):
"""
verts = get_vertices(geometry)
mat = get_shape_matrix(shape)
return np.array([mat @ np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)])
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
def get_element_vertices(element, geometry):
@@ -269,7 +270,7 @@ def get_element_vertices(element, geometry):
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return verts
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
return np.array([mat @ np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)])
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
def get_bottom_elevation(geometry):
+1 -1
View File
@@ -15,7 +15,7 @@ classifiers = [
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
]
[project.optional-dependencies]
geometry = ["mathutils"]
geometry = ["mathutils", "shapely"]
date = ["isodate"]
[project.urls]
"Homepage" = "http://ifcopenshell.org"
@@ -0,0 +1,17 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell 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.
#
# IfcOpenShell 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 IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,17 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell 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.
#
# IfcOpenShell 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 IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,36 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell 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.
#
# IfcOpenShell 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 IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api
class TestAddCostItem(test.bootstrap.IFC4):
def test_add_a_cost_item(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo")
item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule)
assert item1.is_a("IfcCostItem")
assert item1.HasAssignments[0].is_a("IfcRelAssignsToControl")
assert item1.HasAssignments[0].RelatingControl == schedule
def test_add_a_sub_cost_item(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo")
item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule)
item2 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=item1)
assert item2.is_a("IfcCostItem")
assert item2.Nests[0].RelatingObject == item1
@@ -0,0 +1,35 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell 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.
#
# IfcOpenShell 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 IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api
class TestAddCostSchedule(test.bootstrap.IFC4):
def test_add_a_cost_schedule(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET")
assert schedule.is_a("IfcCostSchedule")
assert schedule.Name == "Foo"
assert schedule.PredefinedType == "BUDGET"
def test_adding_a_userdefined_type(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="FOO")
assert schedule.is_a("IfcCostSchedule")
assert schedule.Name == "Foo"
assert schedule.PredefinedType == "USERDEFINED"
assert schedule.ObjectType == "FOO"
@@ -0,0 +1,47 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell 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.
#
# IfcOpenShell 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 IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api
class TestRemoveCostItem(test.bootstrap.IFC4):
def test_remove_a_cost_item(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET")
item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule)
ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=item1)
assert not self.file.by_type("IfcCostItem")
assert not self.file.by_type("IfcRelAssignsToControl")
def test_remove_a_sub_cost_item(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET")
item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule)
item2 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=item1)
ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=item2)
assert self.file.by_type("IfcCostItem") == [item1]
assert self.file.by_type("IfcRelAssignsToControl")
assert not self.file.by_type("IfcRelNests")
def test_remove_a_parent_cost_item(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET")
item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule)
item2 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=item1)
ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=item1)
assert not self.file.by_type("IfcCostItem")
assert not self.file.by_type("IfcRelAssignsToControl")
assert not self.file.by_type("IfcRelNests")
@@ -0,0 +1,37 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell 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.
#
# IfcOpenShell 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 IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api
class TestRemoveCostSchedule(test.bootstrap.IFC4):
def test_remove_a_cost_schedule(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET")
ifcopenshell.api.run("cost.remove_cost_schedule", self.file, cost_schedule=schedule)
assert not self.file.by_type("IfcCostSchedule")
def test_remove_a_schedule_with_items(self):
schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET")
item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule)
item2 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=item1)
ifcopenshell.api.run("cost.remove_cost_schedule", self.file, cost_schedule=schedule)
assert not self.file.by_type("IfcCostSchedule")
assert not self.file.by_type("IfcCostItem")
assert not self.file.by_type("IfcRelNests")
assert not self.file.by_type("IfcRelAssignsToControl")
+12 -1
View File
@@ -46,6 +46,17 @@ class TestPsetQto:
names = self.pset_qto.get_applicable_names("IfcWall")
assert "Pset_WallCommon" in names
names = self.pset_qto.get_applicable_names("IfcWallType")
assert len(names) == 6
assert len(names) == 12
assert "Pset_WallCommon" in names
assert "Qto_WallBaseQuantities" in names # Backported fix for IFC4
def test_getting_applicable_names_by_predefined_type(self):
names = self.pset_qto.get_applicable_names("IfcFurniture")
assert "Pset_FurnitureTypeTable" not in names
names = self.pset_qto.get_applicable_names("IfcFurniture", "TABLE")
assert "Pset_FurnitureTypeTable" in names
names = self.pset_qto.get_applicable_names("IfcFurnitureType", "TABLE")
assert "Pset_FurnitureTypeTable" in names
names = self.pset_qto.get_applicable_names("IfcFurnitureType" )
names2 = self.pset_qto.get_applicable_names("IfcFurnitureType", "CUSTOM")
assert names == names2
@@ -192,6 +192,20 @@ class TestFilterElements(test.bootstrap.IFC4):
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Status": ["New"]})
assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status=New") == {element}
def test_selecting_by_property_with_comparisons(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foobar")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Baz": 123})
assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>100") == {element}
assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz<100") == set()
assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz>=100") == {element}
assert subject.filter_elements(self.file, "IfcWall, Foobar.Baz<=100") == set()
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"})
assert subject.filter_elements(self.file, "IfcWall, Foobar.Foo*=ar") == {element}
assert subject.filter_elements(self.file, "IfcWall, Foobar.Foo!*=ar") == {element2}
assert subject.filter_elements(self.file, "IfcWall, Foobar.Foo*=Foo") == set()
def test_selecting_by_classification(self):
project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")

Some files were not shown because too many files have changed in this diff Show More