diff --git a/.github/workflows/ci-black-formatting.yml b/.github/workflows/ci-black-formatting.yml new file mode 100644 index 0000000000..fb293e36c8 --- /dev/null +++ b/.github/workflows/ci-black-formatting.yml @@ -0,0 +1,12 @@ +name: ci-black-formatting + +on: + push: + pull_request: + +jobs: + lint-formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: psf/black@stable diff --git a/.github/workflows/ci-ifcconvert.yml b/.github/workflows/ci-ifcconvert.yml new file mode 100644 index 0000000000..d47dad04ba --- /dev/null +++ b/.github/workflows/ci-ifcconvert.yml @@ -0,0 +1,65 @@ +name: ci-ifcconvert + +on: + workflow_dispatch: + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + name: ${{ matrix.config.name }}-${{ matrix.pyver }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + config: + - { + name: "Windows 64bit", + short_name: win64, + } + - { + name: "Windows 32bit", + short_name: win32, + } + - { + name: "Linux 64bit", + short_name: linux64 + } + - { + name: "MacOS Intel 64bit", + short_name: macos64 + } + - { + name: "MacOS Silicon 64bit", + short_name: macosm164 + } + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax + architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified + - run: echo ${{ env.DATE }} + - name: Get current version + id: version + run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT + - name: Compile + run: | + cd src/ifcopenshell-python && + make zip-ifcconvert PLATFORM=${{ matrix.config.short_name }} + - name: Upload zip file to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: src/ifcopenshell-python/dist/ifcconvert-${{ steps.version.outputs.version }}-${{ matrix.config.short_name }}.zip + asset_name: ifcconvert-${{ steps.version.outputs.version }}-${{ matrix.config.short_name }}.zip + release_name: "ifcconvert-${{steps.version.outputs.version}}" + tag: "ifcconvert-${{steps.version.outputs.version}}" + overwrite: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5d8f641b4..2f671da1a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: run: | python -m pip install --upgrade pip pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely + pip install mathutils pip install src/bcf --no-deps pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip @@ -125,4 +126,9 @@ jobs: python tests.py cd ../src/ifcopenshell-python mv ifcopenshell ifcopenshell-local # Force testing on installed module - make test-safe + make test + cd ../bcf && make test + pip install requests + cd ../bsdd && make test + cd ../ifcpatch && make test + cd ../ifctester && make test diff --git a/README.md b/README.md index a7076b7d6d..21ba13c948 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,12 @@ Contents | ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* | | ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccityjson?label=PyPI&color=006dad)](https://pypi.org/project/ifccityjson/) | | ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcclash?label=PyPI&color=006dad)](https://pypi.org/project/ifcclash/) | -| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) +| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true) | ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) | | ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) | | ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) | | ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html) -| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) | +| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) | | ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) | | ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true) | ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) | diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 382e7396b4..60294c2256 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -221,10 +221,11 @@ endif() if(GLTF_SUPPORT OR CITYJSON_SUPPORT) UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR) - find_file(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}/nlohmann) + find_path(json_header_path "json.hpp" ${JSON_INCLUDE_DIR} PATH_SUFFIXES "nlohmann") + set(JSON_INCLUDE_DIR ${json_header_path}) - if(json_hpp) - message(STATUS "JSON for Modern C++ header file found") + if(json_header_path) + message(STATUS "JSON for Modern C++ header file found in ${JSON_INCLUDE_DIR}") else() message(FATAL_ERROR "Unable to find JSON for Modern C++ header file, aborting") endif() diff --git a/pyproject.toml b/pyproject.toml index 518164504f..8e329ff191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,34 @@ [tool.black] line-length = 120 +include = ''' + src/( + bcf + |bcfserver + |blenderbim + |bsdd + |foundationserver + |ifc2ca + |ifc4d + |ifc5d + |ifcbimtester + |ifcblender + |ifccityjson + |ifcclash + |ifccsv + |ifcdiff + |ifcfm + |ifcpatch + |ifctester + |ifcopenshell-python + |ifcsverchok + |opencdeserver + )/.*.py$ +''' +extend-exclude = ''' + src/ifcopenshell-python/ifcopenshell/express/* + |src/ifcopenshell-python/ifcopenshell/mvd/* + |src/ifc2ca/templates/* +''' [tool.pyright] reportInvalidTypeForm = false diff --git a/src/bcf/Makefile b/src/bcf/Makefile index daf7696155..ef03ba0d43 100644 --- a/src/bcf/Makefile +++ b/src/bcf/Makefile @@ -33,6 +33,10 @@ models: cd src && xsdata generate -p bcf.v2.model --unnest-classes --kw-only --slots -ds Google bcf/v2/xsd cd src && xsdata generate -p bcf.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd +.PHONY: test +test: + pytest -p no:pytest-blender tests + # .PHONY # api: # openapi-python-client generate --url https://api.swaggerhub.com/apis/buildingSMART/BCF/3.0 diff --git a/src/bcf/bcf/bcfxml.py b/src/bcf/bcf/bcfxml.py index d54e7a1482..9aeb726c51 100644 --- a/src/bcf/bcf/bcfxml.py +++ b/src/bcf/bcf/bcfxml.py @@ -18,6 +18,7 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with BCF. If not, see . """ + import zipfile from pathlib import Path from typing import Optional, Union diff --git a/src/bcf/bcf/geometry.py b/src/bcf/bcf/geometry.py index 92759f9b99..c32ae7619a 100644 --- a/src/bcf/bcf/geometry.py +++ b/src/bcf/bcf/geometry.py @@ -5,8 +5,8 @@ from numpy.typing import NDArray def camera_vectors_from_element_placement( - elem_placement: NDArray[np.float_], -) -> tuple[NDArray[np.float_], NDArray[np.float_], NDArray[np.float_]]: + elem_placement: NDArray[np.float64], +) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: """ Calculate the vectors of a camera pointing to an element. @@ -21,8 +21,8 @@ def camera_vectors_from_element_placement( def camera_vectors_from_target_position( - target_position: NDArray[np.float_], offset: Optional[NDArray[np.float_]] = None -) -> tuple[NDArray[np.float_], NDArray[np.float_], NDArray[np.float_]]: + target_position: NDArray[np.float64], offset: Optional[NDArray[np.float64]] = None +) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: """ Calculate the vectors of a camera pointing to a target point. @@ -50,7 +50,7 @@ def camera_vectors_from_target_position( # return camera_position, -mat[:3, 2], mat[:3, 1] -def unit_vector(v: NDArray[np.float_]) -> NDArray[np.float_]: +def unit_vector(v: NDArray[np.float64]) -> NDArray[np.float64]: """ Return the unit vector of a vector. diff --git a/src/bcf/bcf/inmemory_zipfile.py b/src/bcf/bcf/inmemory_zipfile.py index c78e9f9a74..2a4d4117ce 100644 --- a/src/bcf/bcf/inmemory_zipfile.py +++ b/src/bcf/bcf/inmemory_zipfile.py @@ -5,6 +5,7 @@ Copyright (c) 2017-2020 Anthon van der Neut, Ruamel bvba original idea from https://stackoverflow.com/a/19722365/1307905 """ + import zipfile from io import BytesIO from os import PathLike @@ -13,8 +14,7 @@ from typing import Any, Optional, Protocol class ZipFileInterface(Protocol): - def writestr(self, filename_in_zip: str | zipfile.ZipInfo, file_contents: bytes | str) -> None: - ... + def writestr(self, filename_in_zip: str | zipfile.ZipInfo, file_contents: bytes | str) -> None: ... class InMemoryZipFile: diff --git a/src/bcf/bcf/v2/bcfxml.py b/src/bcf/bcf/v2/bcfxml.py index de71f65937..fe1d482d61 100644 --- a/src/bcf/bcf/v2/bcfxml.py +++ b/src/bcf/bcf/v2/bcfxml.py @@ -1,4 +1,5 @@ """BCF XML V2 handler.""" + import uuid import warnings import zipfile diff --git a/src/bcf/bcf/v2/model/markup.py b/src/bcf/bcf/v2/model/markup.py index 88ddd3e3a4..0b8fd987aa 100644 --- a/src/bcf/bcf/v2/model/markup.py +++ b/src/bcf/bcf/v2/model/markup.py @@ -34,7 +34,7 @@ class BimSnippet: metadata={ "name": "isExternal", "type": "Attribute", - } + }, ) @@ -64,7 +64,7 @@ class HeaderFile: "name": "Filename", "type": "Element", "namespace": "", - } + }, ) date: Optional[XmlDateTime] = field( default=None, @@ -72,7 +72,7 @@ class HeaderFile: "name": "Date", "type": "Element", "namespace": "", - } + }, ) reference: Optional[str] = field( default=None, @@ -80,7 +80,7 @@ class HeaderFile: "name": "Reference", "type": "Element", "namespace": "", - } + }, ) ifc_project: Optional[str] = field( default=None, @@ -89,7 +89,7 @@ class HeaderFile: "type": "Attribute", "length": 22, "pattern": r"[0-9,A-Z,a-z,_$]*", - } + }, ) ifc_spatial_structure_element: Optional[str] = field( default=None, @@ -98,14 +98,14 @@ class HeaderFile: "type": "Attribute", "length": 22, "pattern": r"[0-9,A-Z,a-z,_$]*", - } + }, ) is_external: bool = field( default=True, metadata={ "name": "isExternal", "type": "Attribute", - } + }, ) @@ -120,7 +120,7 @@ class TopicDocumentReference: "name": "ReferencedDocument", "type": "Element", "namespace": "", - } + }, ) description: Optional[str] = field( default=None, @@ -128,7 +128,7 @@ class TopicDocumentReference: "name": "Description", "type": "Element", "namespace": "", - } + }, ) guid: Optional[str] = field( default=None, @@ -136,14 +136,14 @@ class TopicDocumentReference: "name": "Guid", "type": "Attribute", "pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", - } + }, ) is_external: bool = field( default=False, metadata={ "name": "isExternal", "type": "Attribute", - } + }, ) @@ -170,7 +170,7 @@ class ViewPoint: "name": "Viewpoint", "type": "Element", "namespace": "", - } + }, ) snapshot: Optional[str] = field( default=None, @@ -178,7 +178,7 @@ class ViewPoint: "name": "Snapshot", "type": "Element", "namespace": "", - } + }, ) index: Optional[int] = field( default=None, @@ -186,7 +186,7 @@ class ViewPoint: "name": "Index", "type": "Element", "namespace": "", - } + }, ) guid: str = field( metadata={ @@ -230,7 +230,7 @@ class Comment: "name": "Viewpoint", "type": "Element", "namespace": "", - } + }, ) modified_date: Optional[XmlDateTime] = field( default=None, @@ -238,7 +238,7 @@ class Comment: "name": "ModifiedDate", "type": "Element", "namespace": "", - } + }, ) modified_author: Optional[str] = field( default=None, @@ -246,7 +246,7 @@ class Comment: "name": "ModifiedAuthor", "type": "Element", "namespace": "", - } + }, ) guid: str = field( metadata={ @@ -267,7 +267,7 @@ class Header: "type": "Element", "namespace": "", "min_occurs": 1, - } + }, ) @@ -279,7 +279,7 @@ class Topic: "name": "ReferenceLink", "type": "Element", "namespace": "", - } + }, ) title: str = field( metadata={ @@ -295,7 +295,7 @@ class Topic: "name": "Priority", "type": "Element", "namespace": "", - } + }, ) index: Optional[int] = field( default=None, @@ -303,7 +303,7 @@ class Topic: "name": "Index", "type": "Element", "namespace": "", - } + }, ) labels: List[str] = field( default_factory=list, @@ -311,7 +311,7 @@ class Topic: "name": "Labels", "type": "Element", "namespace": "", - } + }, ) creation_date: XmlDateTime = field( metadata={ @@ -335,7 +335,7 @@ class Topic: "name": "ModifiedDate", "type": "Element", "namespace": "", - } + }, ) modified_author: Optional[str] = field( default=None, @@ -343,7 +343,7 @@ class Topic: "name": "ModifiedAuthor", "type": "Element", "namespace": "", - } + }, ) due_date: Optional[XmlDateTime] = field( default=None, @@ -351,7 +351,7 @@ class Topic: "name": "DueDate", "type": "Element", "namespace": "", - } + }, ) assigned_to: Optional[str] = field( default=None, @@ -359,7 +359,7 @@ class Topic: "name": "AssignedTo", "type": "Element", "namespace": "", - } + }, ) stage: Optional[str] = field( default=None, @@ -367,7 +367,7 @@ class Topic: "name": "Stage", "type": "Element", "namespace": "", - } + }, ) description: Optional[str] = field( default=None, @@ -375,7 +375,7 @@ class Topic: "name": "Description", "type": "Element", "namespace": "", - } + }, ) bim_snippet: Optional[BimSnippet] = field( default=None, @@ -383,7 +383,7 @@ class Topic: "name": "BimSnippet", "type": "Element", "namespace": "", - } + }, ) document_reference: List[TopicDocumentReference] = field( default_factory=list, @@ -391,7 +391,7 @@ class Topic: "name": "DocumentReference", "type": "Element", "namespace": "", - } + }, ) related_topic: List[TopicRelatedTopic] = field( default_factory=list, @@ -399,7 +399,7 @@ class Topic: "name": "RelatedTopic", "type": "Element", "namespace": "", - } + }, ) guid: str = field( metadata={ @@ -414,14 +414,14 @@ class Topic: metadata={ "name": "TopicType", "type": "Attribute", - } + }, ) topic_status: Optional[str] = field( default=None, metadata={ "name": "TopicStatus", "type": "Attribute", - } + }, ) @@ -433,7 +433,7 @@ class Markup: "name": "Header", "type": "Element", "namespace": "", - } + }, ) topic: Topic = field( metadata={ @@ -449,7 +449,7 @@ class Markup: "name": "Comment", "type": "Element", "namespace": "", - } + }, ) viewpoints: List[ViewPoint] = field( default_factory=list, @@ -457,5 +457,5 @@ class Markup: "name": "Viewpoints", "type": "Element", "namespace": "", - } + }, ) diff --git a/src/bcf/bcf/v2/model/project.py b/src/bcf/bcf/v2/model/project.py index fea44c6f50..e86fb9b009 100644 --- a/src/bcf/bcf/v2/model/project.py +++ b/src/bcf/bcf/v2/model/project.py @@ -10,7 +10,7 @@ class Project: "name": "Name", "type": "Element", "namespace": "", - } + }, ) project_id: str = field( metadata={ @@ -29,7 +29,7 @@ class ProjectExtension: "name": "Project", "type": "Element", "namespace": "", - } + }, ) extension_schema: str = field( metadata={ diff --git a/src/bcf/bcf/v2/model/version.py b/src/bcf/bcf/v2/model/version.py index 777572c6ae..9f55ad5bf9 100644 --- a/src/bcf/bcf/v2/model/version.py +++ b/src/bcf/bcf/v2/model/version.py @@ -10,12 +10,12 @@ class Version: "name": "DetailedVersion", "type": "Element", "namespace": "", - } + }, ) version_id: Optional[str] = field( default=None, metadata={ "name": "VersionId", "type": "Attribute", - } + }, ) diff --git a/src/bcf/bcf/v2/model/visinfo.py b/src/bcf/bcf/v2/model/visinfo.py index f9976e1c78..e1f13d3a37 100644 --- a/src/bcf/bcf/v2/model/visinfo.py +++ b/src/bcf/bcf/v2/model/visinfo.py @@ -15,14 +15,14 @@ class Component: metadata={ "name": "OriginatingSystem", "type": "Element", - } + }, ) authoring_tool_id: Optional[str] = field( default=None, metadata={ "name": "AuthoringToolId", "type": "Element", - } + }, ) ifc_guid: Optional[str] = field( default=None, @@ -31,7 +31,7 @@ class Component: "type": "Attribute", "length": 22, "pattern": r"[0-9,A-Z,a-z,_$]*", - } + }, ) @@ -92,21 +92,21 @@ class ViewSetupHints: metadata={ "name": "SpacesVisible", "type": "Attribute", - } + }, ) space_boundaries_visible: Optional[bool] = field( default=None, metadata={ "name": "SpaceBoundariesVisible", "type": "Attribute", - } + }, ) openings_visible: Optional[bool] = field( default=None, metadata={ "name": "OpeningsVisible", "type": "Attribute", - } + }, ) @@ -139,7 +139,7 @@ class ComponentColoringColor: "name": "Component", "type": "Element", "min_occurs": 1, - } + }, ) color: Optional[str] = field( default=None, @@ -147,7 +147,7 @@ class ComponentColoringColor: "name": "Color", "type": "Attribute", "pattern": r"[0-9,a-f,A-F]{6}([0-9,a-f,A-F]{2})?", - } + }, ) @@ -159,7 +159,7 @@ class ComponentSelection: "name": "Component", "type": "Element", "min_occurs": 1, - } + }, ) @@ -174,7 +174,7 @@ class ComponentVisibilityExceptions: "name": "Component", "type": "Element", "min_occurs": 1, - } + }, ) @@ -205,6 +205,7 @@ class OrthogonalCamera: camera_up_vector: view_to_world_scale: view's visible size in meters """ + camera_view_point: Point = field( metadata={ "name": "CameraViewPoint", @@ -247,6 +248,7 @@ class PerspectiveCamera: release and viewers should be expect values outside this range in current implementations. """ + camera_view_point: Point = field( metadata={ "name": "CameraViewPoint", @@ -336,7 +338,7 @@ class ComponentColoring: "name": "Color", "type": "Element", "min_occurs": 1, - } + }, ) @@ -347,14 +349,14 @@ class ComponentVisibility: metadata={ "name": "Exceptions", "type": "Element", - } + }, ) default_visibility: Optional[bool] = field( default=None, metadata={ "name": "DefaultVisibility", "type": "Attribute", - } + }, ) @@ -368,7 +370,7 @@ class VisualizationInfoClippingPlanes: metadata={ "name": "ClippingPlane", "type": "Element", - } + }, ) @@ -383,7 +385,7 @@ class VisualizationInfoLines: "name": "Line", "type": "Element", "min_occurs": 1, - } + }, ) @@ -394,14 +396,14 @@ class Components: metadata={ "name": "ViewSetupHints", "type": "Element", - } + }, ) selection: Optional[ComponentSelection] = field( default=None, metadata={ "name": "Selection", "type": "Element", - } + }, ) visibility: ComponentVisibility = field( metadata={ @@ -415,7 +417,7 @@ class Components: metadata={ "name": "Coloring", "type": "Element", - } + }, ) @@ -424,47 +426,48 @@ class VisualizationInfo: """ VisualizationInfo documentation. """ + components: Optional[Components] = field( default=None, metadata={ "name": "Components", "type": "Element", - } + }, ) orthogonal_camera: Optional[OrthogonalCamera] = field( default=None, metadata={ "name": "OrthogonalCamera", "type": "Element", - } + }, ) perspective_camera: Optional[PerspectiveCamera] = field( default=None, metadata={ "name": "PerspectiveCamera", "type": "Element", - } + }, ) lines: Optional[VisualizationInfoLines] = field( default=None, metadata={ "name": "Lines", "type": "Element", - } + }, ) clipping_planes: Optional[VisualizationInfoClippingPlanes] = field( default=None, metadata={ "name": "ClippingPlanes", "type": "Element", - } + }, ) bitmap: List[VisualizationInfoBitmap] = field( default_factory=list, metadata={ "name": "Bitmap", "type": "Element", - } + }, ) guid: str = field( metadata={ diff --git a/src/bcf/bcf/v2/topic.py b/src/bcf/bcf/v2/topic.py index c1188c9a06..82f09c54a4 100644 --- a/src/bcf/bcf/v2/topic.py +++ b/src/bcf/bcf/v2/topic.py @@ -1,4 +1,5 @@ """BCF XML V2 Topic handler.""" + import datetime import tempfile import uuid @@ -277,7 +278,7 @@ class TopicHandler: self.add_visinfo_handler(new_viewpoint) return new_viewpoint - def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float_], *guids: str) -> None: + def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float64], *guids: str) -> None: """Add a viewpoint pointing at an XYZ point in space Args: diff --git a/src/bcf/bcf/v2/visinfo.py b/src/bcf/bcf/v2/visinfo.py index 7ac1a750d8..08694b8111 100644 --- a/src/bcf/bcf/v2/visinfo.py +++ b/src/bcf/bcf/v2/visinfo.py @@ -176,7 +176,7 @@ class VisualizationInfoHandler: @classmethod def create_from_point_and_guids( cls, - position: NDArray[np.float_], + position: NDArray[np.float64], *guids: str, xml_handler: Optional[AbstractXmlParserSerializer] = None, ) -> "VisualizationInfoHandler": @@ -224,7 +224,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: ) -def build_viewpoint_from_position_and_guids(position: NDArray[np.float_], *guids: str) -> mdl.VisualizationInfo: +def build_viewpoint_from_position_and_guids(position: NDArray[np.float64], *guids: str) -> mdl.VisualizationInfo: """ Return a BCF viewpoint of an IFC element. @@ -261,7 +261,7 @@ def build_components(*guids: str) -> mdl.Components: ) -def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera: +def build_camera(elem_placement: NDArray[np.float64]) -> mdl.PerspectiveCamera: """ Return a BCF camera for an IFC element placement matrix. @@ -275,7 +275,7 @@ def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera: def build_camera_from_vectors( - camera_position: NDArray[np.float_], camera_dir: NDArray[np.float_], camera_up: NDArray[np.float_] + camera_position: NDArray[np.float64], camera_dir: NDArray[np.float64], camera_up: NDArray[np.float64] ) -> mdl.PerspectiveCamera: """ Return a BCF camera for an IFC element placement matrix. diff --git a/src/bcf/bcf/v3/bcfxml.py b/src/bcf/bcf/v3/bcfxml.py index 481a7557fa..3318264670 100644 --- a/src/bcf/bcf/v3/bcfxml.py +++ b/src/bcf/bcf/v3/bcfxml.py @@ -1,4 +1,5 @@ """BCF XML V3 handlers.""" + import uuid import warnings import zipfile diff --git a/src/bcf/bcf/v3/document.py b/src/bcf/bcf/v3/document.py index 56a15da42d..afc6be50b9 100644 --- a/src/bcf/bcf/v3/document.py +++ b/src/bcf/bcf/v3/document.py @@ -1,4 +1,5 @@ """BCF XML V3 Documents handler.""" + import zipfile from typing import Any, Optional diff --git a/src/bcf/bcf/v3/model/documents.py b/src/bcf/bcf/v3/model/documents.py index c9a1b43c9c..1b18758d0e 100644 --- a/src/bcf/bcf/v3/model/documents.py +++ b/src/bcf/bcf/v3/model/documents.py @@ -22,7 +22,7 @@ class Document: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) guid: str = field( metadata={ @@ -45,7 +45,7 @@ class DocumentInfoDocuments: "name": "Document", "type": "Element", "namespace": "", - } + }, ) @@ -57,5 +57,5 @@ class DocumentInfo: "name": "Documents", "type": "Element", "namespace": "", - } + }, ) diff --git a/src/bcf/bcf/v3/model/extensions.py b/src/bcf/bcf/v3/model/extensions.py index cb0af500f6..c9c47495ce 100644 --- a/src/bcf/bcf/v3/model/extensions.py +++ b/src/bcf/bcf/v3/model/extensions.py @@ -15,7 +15,7 @@ class ExtensionsPriorities: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -32,7 +32,7 @@ class ExtensionsSnippetTypes: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -49,7 +49,7 @@ class ExtensionsStages: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -66,7 +66,7 @@ class ExtensionsTopicLabels: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -83,7 +83,7 @@ class ExtensionsTopicStatuses: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -100,7 +100,7 @@ class ExtensionsTopicTypes: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -117,7 +117,7 @@ class ExtensionsUsers: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -129,7 +129,7 @@ class Extensions: "name": "TopicTypes", "type": "Element", "namespace": "", - } + }, ) topic_statuses: Optional[ExtensionsTopicStatuses] = field( default=None, @@ -137,7 +137,7 @@ class Extensions: "name": "TopicStatuses", "type": "Element", "namespace": "", - } + }, ) priorities: Optional[ExtensionsPriorities] = field( default=None, @@ -145,7 +145,7 @@ class Extensions: "name": "Priorities", "type": "Element", "namespace": "", - } + }, ) topic_labels: Optional[ExtensionsTopicLabels] = field( default=None, @@ -153,7 +153,7 @@ class Extensions: "name": "TopicLabels", "type": "Element", "namespace": "", - } + }, ) users: Optional[ExtensionsUsers] = field( default=None, @@ -161,7 +161,7 @@ class Extensions: "name": "Users", "type": "Element", "namespace": "", - } + }, ) snippet_types: Optional[ExtensionsSnippetTypes] = field( default=None, @@ -169,7 +169,7 @@ class Extensions: "name": "SnippetTypes", "type": "Element", "namespace": "", - } + }, ) stages: Optional[ExtensionsStages] = field( default=None, @@ -177,5 +177,5 @@ class Extensions: "name": "Stages", "type": "Element", "namespace": "", - } + }, ) diff --git a/src/bcf/bcf/v3/model/markup.py b/src/bcf/bcf/v3/model/markup.py index f212b9fc7e..4121b04abe 100644 --- a/src/bcf/bcf/v3/model/markup.py +++ b/src/bcf/bcf/v3/model/markup.py @@ -40,7 +40,7 @@ class BimSnippet: metadata={ "name": "IsExternal", "type": "Attribute", - } + }, ) @@ -68,7 +68,7 @@ class DocumentReference: "type": "Element", "namespace": "", "pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}", - } + }, ) url: Optional[str] = field( default=None, @@ -78,7 +78,7 @@ class DocumentReference: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) description: Optional[str] = field( default=None, @@ -88,7 +88,7 @@ class DocumentReference: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) guid: str = field( metadata={ @@ -110,7 +110,7 @@ class File: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) date: Optional[XmlDateTime] = field( default=None, @@ -118,7 +118,7 @@ class File: "name": "Date", "type": "Element", "namespace": "", - } + }, ) reference: Optional[str] = field( default=None, @@ -128,7 +128,7 @@ class File: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) ifc_project: Optional[str] = field( default=None, @@ -137,7 +137,7 @@ class File: "type": "Attribute", "length": 22, "pattern": r"[0-9A-Za-z_$]*", - } + }, ) ifc_spatial_structure_element: Optional[str] = field( default=None, @@ -146,14 +146,14 @@ class File: "type": "Attribute", "length": 22, "pattern": r"[0-9A-Za-z_$]*", - } + }, ) is_external: bool = field( default=True, metadata={ "name": "IsExternal", "type": "Attribute", - } + }, ) @@ -170,7 +170,7 @@ class TopicLabels: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -187,7 +187,7 @@ class TopicReferenceLinks: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) @@ -216,7 +216,7 @@ class ViewPoint: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) snapshot: Optional[str] = field( default=None, @@ -226,7 +226,7 @@ class ViewPoint: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) index: Optional[int] = field( default=None, @@ -234,7 +234,7 @@ class ViewPoint: "name": "Index", "type": "Element", "namespace": "", - } + }, ) guid: str = field( metadata={ @@ -274,7 +274,7 @@ class Comment: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) viewpoint: Optional[CommentViewpoint] = field( default=None, @@ -282,7 +282,7 @@ class Comment: "name": "Viewpoint", "type": "Element", "namespace": "", - } + }, ) modified_date: Optional[XmlDateTime] = field( default=None, @@ -290,7 +290,7 @@ class Comment: "name": "ModifiedDate", "type": "Element", "namespace": "", - } + }, ) modified_author: Optional[str] = field( default=None, @@ -300,7 +300,7 @@ class Comment: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) guid: str = field( metadata={ @@ -323,7 +323,7 @@ class HeaderFiles: "name": "File", "type": "Element", "namespace": "", - } + }, ) @@ -338,7 +338,7 @@ class TopicDocumentReferences: "name": "DocumentReference", "type": "Element", "namespace": "", - } + }, ) @@ -353,7 +353,7 @@ class TopicRelatedTopics: "name": "RelatedTopic", "type": "Element", "namespace": "", - } + }, ) @@ -368,7 +368,7 @@ class TopicViewpoints: "name": "ViewPoint", "type": "Element", "namespace": "", - } + }, ) @@ -380,7 +380,7 @@ class Header: "name": "Files", "type": "Element", "namespace": "", - } + }, ) @@ -395,7 +395,7 @@ class TopicComments: "name": "Comment", "type": "Element", "namespace": "", - } + }, ) @@ -407,7 +407,7 @@ class Topic: "name": "ReferenceLinks", "type": "Element", "namespace": "", - } + }, ) title: str = field( metadata={ @@ -427,7 +427,7 @@ class Topic: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) index: Optional[int] = field( default=None, @@ -435,7 +435,7 @@ class Topic: "name": "Index", "type": "Element", "namespace": "", - } + }, ) labels: Optional[TopicLabels] = field( default=None, @@ -443,7 +443,7 @@ class Topic: "name": "Labels", "type": "Element", "namespace": "", - } + }, ) creation_date: XmlDateTime = field( metadata={ @@ -469,7 +469,7 @@ class Topic: "name": "ModifiedDate", "type": "Element", "namespace": "", - } + }, ) modified_author: Optional[str] = field( default=None, @@ -479,7 +479,7 @@ class Topic: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) due_date: Optional[XmlDateTime] = field( default=None, @@ -487,7 +487,7 @@ class Topic: "name": "DueDate", "type": "Element", "namespace": "", - } + }, ) assigned_to: Optional[str] = field( default=None, @@ -497,7 +497,7 @@ class Topic: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) stage: Optional[str] = field( default=None, @@ -507,7 +507,7 @@ class Topic: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) description: Optional[str] = field( default=None, @@ -517,7 +517,7 @@ class Topic: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) bim_snippet: Optional[BimSnippet] = field( default=None, @@ -525,7 +525,7 @@ class Topic: "name": "BimSnippet", "type": "Element", "namespace": "", - } + }, ) document_references: Optional[TopicDocumentReferences] = field( default=None, @@ -533,7 +533,7 @@ class Topic: "name": "DocumentReferences", "type": "Element", "namespace": "", - } + }, ) related_topics: Optional[TopicRelatedTopics] = field( default=None, @@ -541,7 +541,7 @@ class Topic: "name": "RelatedTopics", "type": "Element", "namespace": "", - } + }, ) comments: Optional[TopicComments] = field( default=None, @@ -549,7 +549,7 @@ class Topic: "name": "Comments", "type": "Element", "namespace": "", - } + }, ) viewpoints: Optional[TopicViewpoints] = field( default=None, @@ -557,7 +557,7 @@ class Topic: "name": "Viewpoints", "type": "Element", "namespace": "", - } + }, ) guid: str = field( metadata={ @@ -574,7 +574,7 @@ class Topic: "type": "Attribute", "min_length": 1, "white_space": "collapse", - } + }, ) topic_type: str = field( metadata={ @@ -604,7 +604,7 @@ class Markup: "name": "Header", "type": "Element", "namespace": "", - } + }, ) topic: Topic = field( metadata={ diff --git a/src/bcf/bcf/v3/model/project.py b/src/bcf/bcf/v3/model/project.py index b6962213d6..76a6922925 100644 --- a/src/bcf/bcf/v3/model/project.py +++ b/src/bcf/bcf/v3/model/project.py @@ -12,7 +12,7 @@ class Project: "namespace": "", "min_length": 1, "white_space": "collapse", - } + }, ) project_id: str = field( metadata={ diff --git a/src/bcf/bcf/v3/model/visinfo.py b/src/bcf/bcf/v3/model/visinfo.py index fa541945ea..1a1dcb72c5 100644 --- a/src/bcf/bcf/v3/model/visinfo.py +++ b/src/bcf/bcf/v3/model/visinfo.py @@ -17,7 +17,7 @@ class Component: "type": "Element", "min_length": 1, "white_space": "collapse", - } + }, ) authoring_tool_id: Optional[str] = field( default=None, @@ -26,7 +26,7 @@ class Component: "type": "Element", "min_length": 1, "white_space": "collapse", - } + }, ) ifc_guid: Optional[str] = field( default=None, @@ -35,7 +35,7 @@ class Component: "type": "Attribute", "length": 22, "pattern": r"[0-9A-Za-z_$]*", - } + }, ) @@ -96,21 +96,21 @@ class ViewSetupHints: metadata={ "name": "SpacesVisible", "type": "Attribute", - } + }, ) space_boundaries_visible: bool = field( default=False, metadata={ "name": "SpaceBoundariesVisible", "type": "Attribute", - } + }, ) openings_visible: bool = field( default=False, metadata={ "name": "OpeningsVisible", "type": "Attribute", - } + }, ) @@ -191,7 +191,7 @@ class ComponentColoringColorComponents: "name": "Component", "type": "Element", "min_occurs": 1, - } + }, ) @@ -202,7 +202,7 @@ class ComponentSelection: metadata={ "name": "Component", "type": "Element", - } + }, ) @@ -216,7 +216,7 @@ class ComponentVisibilityExceptions: metadata={ "name": "Component", "type": "Element", - } + }, ) @@ -249,6 +249,7 @@ class OrthogonalCamera: aspect_ratio: Proportional relationship between the width and the height of the view (w/h). """ + camera_view_point: Point = field( metadata={ "name": "CameraViewPoint", @@ -302,6 +303,7 @@ class PerspectiveCamera: aspect_ratio: Proportional relationship between the width and the height of the view (w/h). """ + camera_view_point: Point = field( metadata={ "name": "CameraViewPoint", @@ -371,21 +373,21 @@ class ComponentVisibility: metadata={ "name": "ViewSetupHints", "type": "Element", - } + }, ) exceptions: Optional[ComponentVisibilityExceptions] = field( default=None, metadata={ "name": "Exceptions", "type": "Element", - } + }, ) default_visibility: bool = field( default=False, metadata={ "name": "DefaultVisibility", "type": "Attribute", - } + }, ) @@ -399,7 +401,7 @@ class VisualizationInfoBitmaps: metadata={ "name": "Bitmap", "type": "Element", - } + }, ) @@ -413,7 +415,7 @@ class VisualizationInfoClippingPlanes: metadata={ "name": "ClippingPlane", "type": "Element", - } + }, ) @@ -427,7 +429,7 @@ class VisualizationInfoLines: metadata={ "name": "Line", "type": "Element", - } + }, ) @@ -438,7 +440,7 @@ class ComponentColoring: metadata={ "name": "Color", "type": "Element", - } + }, ) @@ -449,21 +451,21 @@ class Components: metadata={ "name": "Selection", "type": "Element", - } + }, ) visibility: Optional[ComponentVisibility] = field( default=None, metadata={ "name": "Visibility", "type": "Element", - } + }, ) coloring: Optional[ComponentColoring] = field( default=None, metadata={ "name": "Coloring", "type": "Element", - } + }, ) @@ -472,47 +474,48 @@ class VisualizationInfo: """ VisualizationInfo documentation. """ + components: Optional[Components] = field( default=None, metadata={ "name": "Components", "type": "Element", - } + }, ) orthogonal_camera: Optional[OrthogonalCamera] = field( default=None, metadata={ "name": "OrthogonalCamera", "type": "Element", - } + }, ) perspective_camera: Optional[PerspectiveCamera] = field( default=None, metadata={ "name": "PerspectiveCamera", "type": "Element", - } + }, ) lines: Optional[VisualizationInfoLines] = field( default=None, metadata={ "name": "Lines", "type": "Element", - } + }, ) clipping_planes: Optional[VisualizationInfoClippingPlanes] = field( default=None, metadata={ "name": "ClippingPlanes", "type": "Element", - } + }, ) bitmaps: Optional[VisualizationInfoBitmaps] = field( default=None, metadata={ "name": "Bitmaps", "type": "Element", - } + }, ) guid: str = field( metadata={ diff --git a/src/bcf/bcf/v3/topic.py b/src/bcf/bcf/v3/topic.py index 71550505ca..44109a21db 100644 --- a/src/bcf/bcf/v3/topic.py +++ b/src/bcf/bcf/v3/topic.py @@ -1,4 +1,5 @@ """BCF XML V3 Topic handler.""" + import datetime import uuid import zipfile @@ -174,7 +175,7 @@ class TopicHandler: new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler) self.add_visinfo_handler(new_viewpoint) - def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float_], *guids: str) -> None: + def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float64], *guids: str) -> None: """ Add a viewpoint tergeting an IFC element to the topic. diff --git a/src/bcf/bcf/v3/visinfo.py b/src/bcf/bcf/v3/visinfo.py index 56c1692206..f2666288d9 100644 --- a/src/bcf/bcf/v3/visinfo.py +++ b/src/bcf/bcf/v3/visinfo.py @@ -175,7 +175,7 @@ class VisualizationInfoHandler: @classmethod def create_from_point_and_guids( cls, - position: NDArray[np.float_], + position: NDArray[np.float64], *guids: str, xml_handler: Optional[AbstractXmlParserSerializer] = None, ) -> "VisualizationInfoHandler": @@ -218,7 +218,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: ) -def build_viewpoint_from_position_and_guids(position: NDArray[np.float_], *guids: str) -> mdl.VisualizationInfo: +def build_viewpoint_from_position_and_guids(position: NDArray[np.float64], *guids: str) -> mdl.VisualizationInfo: """ Return a BCF viewpoint of an IFC element. @@ -255,7 +255,7 @@ def build_components(*guids: str) -> mdl.Components: ) -def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera: +def build_camera(elem_placement: NDArray[np.float64]) -> mdl.PerspectiveCamera: """ Return a BCF camera for an IFC element placement matrix. @@ -269,7 +269,7 @@ def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera: def build_camera_from_vectors( - camera_position: NDArray[np.float_], camera_dir: NDArray[np.float_], camera_up: NDArray[np.float_] + camera_position: NDArray[np.float64], camera_dir: NDArray[np.float64], camera_up: NDArray[np.float64] ) -> mdl.PerspectiveCamera: """ Return a BCF camera for an IFC element placement matrix. diff --git a/src/bcf/bcf/xml_parser.py b/src/bcf/bcf/xml_parser.py index b3b51367e0..a5dbd3b71c 100644 --- a/src/bcf/bcf/xml_parser.py +++ b/src/bcf/bcf/xml_parser.py @@ -1,4 +1,5 @@ """XML Parser and Serializer factories.""" + from typing import Optional, Protocol, Type, TypeVar from xsdata.formats.dataclass.context import XmlContext diff --git a/src/bcf/tests/v2/test_bcf_xml.py b/src/bcf/tests/v2/test_bcf_xml.py index 26446ee0c3..fed1428e73 100644 --- a/src/bcf/tests/v2/test_bcf_xml.py +++ b/src/bcf/tests/v2/test_bcf_xml.py @@ -1,4 +1,5 @@ """BCF XML tests.""" + import uuid from pathlib import Path from tempfile import TemporaryDirectory diff --git a/src/bcf/tests/v3/test_bcf_xml.py b/src/bcf/tests/v3/test_bcf_xml.py index 133dd66419..50664b98c7 100644 --- a/src/bcf/tests/v3/test_bcf_xml.py +++ b/src/bcf/tests/v3/test_bcf_xml.py @@ -1,4 +1,5 @@ """BCF XML tests.""" + import uuid from pathlib import Path from tempfile import TemporaryDirectory diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index f1e4f1a01f..1603e887b5 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -91,12 +91,11 @@ BLENDER_PLATFORM:=windows-x64 endif # Current build commit hash. -OLD:=c18e4ea +OLD:=d51fa2c .PHONY: bump bump: cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile cd ../ifcopenshell-python/ && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile - cd ../ifcopenshell-python/docs/ifcconvert/ && $(SED) -b "s/$(OLD)/$(NEW)/" installation.rst .PHONY: dist dist: @@ -196,19 +195,20 @@ endif cd build/blenderbim/bim/data/gantt/ && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.css # Provides IFCJSON functionality - cd build && wget https://github.com/IFCJSON-Team/IFC2JSON_python/archive/master.zip - cd build && unzip master.zip && rm master.zip + # TODO: replace with main repo if https://github.com/IFCJSON-Team/IFC2JSON_python/pull/3 is merged. + cd build && wget -O ifc2json.zip https://github.com/Moult/IFC2JSON_python/archive/refs/heads/feature-ios-v0.8.0.zip + cd build && unzip ifc2json.zip && rm ifc2json.zip # IFCJSON doesn't have pyproject.toml, so we use python command. - cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-master/file_converters && \ + cd build && . env/$(VENV_ACTIVATE) && cd IFC2JSON_python-*/file_converters && \ $(PYTHON) -c "from setuptools import setup; \ setup( \ name='ifcjson', \ - version='0.0.0', \ + version='0.0.1', \ author='Jan Brouwer', \ author_email='jan@brewsky.nl', \ packages=['ifcjson'], \ )" bdist_wheel - cp -r build/IFC2JSON_python-master/file_converters/dist/*.whl build/wheels/ + cp -r build/IFC2JSON_python-*/file_converters/dist/*.whl build/wheels/ # Brickschema requires pkg_resources which is provided by Blender. # Provides Brickschema functionality diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 51f2c02614..15e7c8c47e 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -98,10 +98,10 @@ classes = [ operator.AddIfcFile, operator.BIM_OT_add_section_plane, operator.BIM_OT_delete_object, - operator.BIM_OT_open_webbrowser, operator.BIM_OT_remove_section_plane, operator.BIM_OT_select_object, operator.BIM_OT_show_description, + operator.BIM_OT_multiple_file_selector, operator.ClippingPlaneCutWithCappings, operator.CloseError, operator.EditBlenderCollection, @@ -121,6 +121,7 @@ classes = [ prop.StrProperty, operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty prop.ObjProperty, + prop.MultipleFileSelect, prop.Attribute, prop.BIMAreaProperties, prop.BIMTabProperties, diff --git a/src/blenderbim/blenderbim/bim/data/webui/sioserver.py b/src/blenderbim/blenderbim/bim/data/webui/sioserver.py index fdbef0b50c..6d81c03665 100644 --- a/src/blenderbim/blenderbim/bim/data/webui/sioserver.py +++ b/src/blenderbim/blenderbim/bim/data/webui/sioserver.py @@ -2,7 +2,9 @@ import sys import os import webbrowser -blenderbim_lib_path = os.environ.get("blenderbim_lib_path") +blenderbim_lib_path = os.environ.get("BLENDERBIM_LIB_PATH") +blenderbim_version = os.environ.get("BLENDERBIM_VERSION") + if blenderbim_lib_path: sys.path.insert(0, blenderbim_lib_path) @@ -52,9 +54,16 @@ class WebNamespace(socketio.AsyncNamespace): "web_operator", data, namespace="/blender", - room=data["blenderId"], + room=data.get("blenderId", None), ) + async def on_get_svg(self, sid, data): + print("hello world!") + file_path = data["path"] + with open(file_path, "r") as file: + svg_data = file.read() + await sio.emit("svg_data", svg_data, room=sid, namespace="/web") + async def send_cached_messages(self, sid): # Send cached messages to the connected web client for blenderId, messages in blender_messages.items(): @@ -98,6 +107,12 @@ class BlenderNamespace(socketio.AsyncNamespace): blender_messages[sid]["gantt_data"] = data await sio.emit("gantt_data", {"blenderId": sid, "data": data}, namespace="/web") + async def on_drawings_data(self, sid, data): + print(f"Drawings directory from Blender client {sid}") + print(data) + blender_messages[sid]["drwings_data"] = data + await sio.emit("drawings_data", {"blenderId": sid, "data": data}, namespace="/web") + # Attach namespaces sio.register_namespace(WebNamespace("/web")) @@ -108,26 +123,45 @@ sio.register_namespace(BlenderNamespace("/blender")) async def index(request): with open("templates/index.html", "r") as f: template = f.read() - html_content = pystache.render(template, {"port": sio_port}) + html_content = pystache.render(template, {"port": sio_port, "version": blenderbim_version}) return web.Response(text=html_content, content_type="text/html") async def gantt(request): with open("templates/gantt.html", "r") as f: template = f.read() + html_content = pystache.render(template, {"port": sio_port, "version": blenderbim_version}) + return web.Response(text=html_content, content_type="text/html") + + +async def drawings(request): + with open("templates/drawings.html", "r") as f: + template = f.read() html_content = pystache.render(template, {"port": sio_port}) return web.Response(text=html_content, content_type="text/html") -async def open_web_browser(app): - webbrowser.open(f"http://127.0.0.1:{sio_port}/") +async def on_startup(app): + pid_file = "running_pid.json" + + if os.path.exists(pid_file): + with open(pid_file, "r") as f: + pids = json.load(f) + else: + pids = {} + + pids[str(os.getpid())] = sio_port + + with open(pid_file, "w") as f: + json.dump(pids, f, indent=4) app.router.add_get("/", index) +app.router.add_get("/drawings", drawings) app.router.add_get("/gantt", gantt) app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt") app.router.add_static("/static/", path="./static", name="static") -app.on_startup.append(open_web_browser) +app.on_startup.append(on_startup) def main(): diff --git a/src/blenderbim/blenderbim/bim/data/webui/static/css/drawings.css b/src/blenderbim/blenderbim/bim/data/webui/static/css/drawings.css new file mode 100644 index 0000000000..9dd2437ecf --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/webui/static/css/drawings.css @@ -0,0 +1,249 @@ +:root { + --font-family: Arial, sans-serif; + --base-font-size: 16px; + --margin-tiny: 0.125rem; + --margin-small: 0.625rem; + --margin-medium: 1.25rem; + --margin-large: 2.5rem; + --padding-tiny: 0.125rem; + --padding-small: 0.625rem; + --padding-medium: 1rem; + --font-size-large: 1.2rem; + --logo-height: 2.5rem; + --nav-height: 2.5rem; +} + +:root.dark { + color-scheme: dark; + --bg-color: #252525; + --text-color: #e0e0e0; + --nav-bg-color: #121212; + --nav-border-color: #25682a; + --nav-link-color: #fff; + --nav-link-hover-color: #3fb449; + --warning-color: #FFDB8F; + --border-color: #464444; +} + +:root.light { + color-scheme: light; + --bg-color: #ffffff; + --text-color: #000000; + --nav-bg-color: #f8f8f8; + --nav-border-color: #cccccc; + --nav-link-color: #000000; + --nav-link-hover-color: #38a63d; + --warning-color: #FF4500; + --border-color: #222; +} + +html { + font-size: var(--base-font-size); + color: var(--text-color); +} + +body { + background-color: var(--bg-color); + color: var(--text-color); + margin: 0; + font-family: var(--font-family); +} + +#container { + margin-top: var(--margin-medium); + margin-left: var(--margin-small); + margin-right: var(--margin-small); + margin-bottom: var(--margin-medium); + height: calc(100vh - var(--nav-height)); + box-sizing: border-box; + display: flex; +} + +h3 { + margin-top: 0; +} + +nav { + background-color: var(--nav-bg-color); + height: var(--nav-height); + padding: var(--padding-medium) 0; + display: flex; + align-items: center; + position: relative; + border-bottom: 2px solid var(--nav-border-color); +} + +nav .logo { + margin-left: var(--margin-large); + height: var(--logo-height); +} + +nav ul { + list-style-type: none; + margin: 0; + margin-left: 0.0625rem; + display: flex; + flex-grow: 1; + justify-content: center; +} + +nav ul li { + margin-right: 3.125rem; +} + +nav ul li a { + text-decoration: none; + color: var(--nav-link-color); + font-size: var(--font-size-large); +} + +nav ul li a:hover, +nav ul li a.active { + color: var(--nav-link-hover-color); +} + +.warning { + color: var(--warning-color); + margin-bottom: var(--margin-small); + padding: var(--padding-tiny); + display: none; +} + +#toggle-theme { + border: none; + background: none; + cursor: pointer; + font-size: var(--font-size-large); + margin-right: var(--margin-medium); +} + +#toggle-theme:focus { + outline: none; +} + +#client-list { + position: absolute; + top: calc(0 + var(--nav-height)); + overflow-y: auto; + overflow: hidden; + transition: opacity 0.3s ease-out, visibility 0.3s ease-out; + border: 1px solid var(--table-border-color); + background-color: var(--nav-bg-color); + z-index: 1; + opacity: 0; + visibility: hidden; + width: auto; +} + +#client-list.show { + opacity: 1; + visibility: visible; +} + +#connected-list-div { + display: inline-block; +} + +.client { + padding: var(--margin-small); + border-bottom: 1px solid var(--table-border-color); + cursor: pointer; +} + +.client-details { + max-height: 0; + overflow: hidden; + padding-left: var(--padding-small); + transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out; + background-color: var(--nav-bg-color); + margin-top: var(--margin-tiny); + opacity: 0; +} + +.client-details.show { + max-height: none; + opacity: 1; +} + +.client-detail { + border-bottom: 1px solid var(--table-border-color); + padding: var(--padding-small); +} + +#show-connected-button { + width: auto; + background-color: var(--bg-color); + border: none; + font-size: var(--base-font-size); +} + +#show-connected-button:hover, +.scroll-button:hover { + cursor: pointer; +} + +.scroll-button { + font-size: var(--base-font-size); + margin-left: var(--margin-tiny); + margin-top: var(--margin-small); +} + + +.left { + flex: 1; + background-color: var(--bg-color); + display: flex; + flex-direction: column; + padding: 20px; +} + +.right { + flex: 4; + background-color: var(--bg-color); + border: 1px solid var(--border-color); +} + +#svg-container { + height: 100%; +} + +.panel { + background-color: var(--nav-bg-color); + border: 1px solid var(--border-color); + padding: 10px; + flex-grow: 1; + display: flex; + flex-direction: column; +} + +.card-header, +.card-body { + background-color: var(--bg-color); + border: 1px solid var(--border-color); + overflow: auto; +} + +.card { + background-color: var(--nav-bg-color); + overflow: auto; +} + +li.svg-name { + list-style-type: none; +} + +#dropdown-menu:focus { + outline: none; +} + +/* remove bootstrap css confilcts */ +*, +::after, +::before { + box-sizing: unset; +} + +.btn, +.btn:hover { + color: var(--nav-link-hover-color); +} \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/data/webui/static/css/gantt.css b/src/blenderbim/blenderbim/bim/data/webui/static/css/gantt.css index 1256b09080..f19ab94c28 100644 --- a/src/blenderbim/blenderbim/bim/data/webui/static/css/gantt.css +++ b/src/blenderbim/blenderbim/bim/data/webui/static/css/gantt.css @@ -1,13 +1,17 @@ :root { --font-family: Arial, sans-serif; + --base-font-size: 16px; + --margin-tiny: 0.125rem; --margin-small: 0.625rem; --margin-medium: 1.25rem; --margin-large: 2.5rem; - --padding-small: 0.125rem; + --padding-tiny: 0.125rem; + --padding-small: 0.625rem; --padding-medium: 1rem; --font-size-small: 1rem; --font-size-large: 1.2rem; --logo-height: 2.5rem; + --nav-height: 2.5rem; --folder-collapse-font-size: 0.75rem; --folder-collapse-font-family: Courier, "Courier New", monospace; --box-shadow: 0 0 0.7rem #5f5f5f66; @@ -31,6 +35,7 @@ --group-item-bg-color: #4b4b4b; --input-bg-color: #3b3b3b; --input-border-color: #000; + --details-border-color: #464444; } :root.light { @@ -42,6 +47,7 @@ --nav-link-color: #000000; --nav-link-hover-color: #38a63d; --warning-color: #FF4500; + --details-border-color: #222; } html { @@ -53,9 +59,13 @@ body { color: var(--primary-text-color); margin: 0; font-family: var(--font-family); + display: flex; + flex-direction: column; + min-height: 100vh; } #container { + flex: 1; margin-top: var(--margin-medium); margin-left: var(--margin-small); margin-right: var(--margin-small); @@ -107,7 +117,7 @@ nav ul li a.active { .warning { color: var(--warning-color); margin-bottom: var(--margin-small); - padding: var(--padding-small); + padding: var(--padding-tiny); display: none; } @@ -141,6 +151,87 @@ nav ul li a.active { outline: none; } +#client-list { + position: absolute; + top: calc(0 + var(--nav-height)); + overflow-y: auto; + overflow: hidden; + transition: opacity 0.3s ease-out, visibility 0.3s ease-out; + border: 1px solid var(--table-border-color); + background-color: var(--nav-bg-color); + z-index: 1; + opacity: 0; + visibility: hidden; + width: auto; +} + +#client-list.show { + opacity: 1; + visibility: visible; +} + +#connected-list-div { + display: inline-block; +} + +.client { + padding: var(--margin-small); + border-bottom: 1px solid var(--table-border-color); + cursor: pointer; +} + +.client-details { + max-height: 0; + overflow: hidden; + padding-left: var(--padding-small); + transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out; + background-color: var(--nav-bg-color); + margin-top: var(--margin-tiny); + opacity: 0; +} + +.client-details.show { + max-height: none; + opacity: 1; +} + +.client-detail { + border-bottom: 1px solid var(--table-border-color); + padding: var(--padding-small); +} + +#show-connected-button { + width: auto; + background-color: var(--bg-color); + border: none; + font-size: var(--base-font-size); +} + +#show-connected-button:hover, +.scroll-button:hover { + cursor: pointer; +} + +.scroll-button { + font-size: var(--base-font-size); + margin-left: var(--margin-tiny); + margin-top: var(--margin-small); +} + +footer { + background-color: var(--nav-bg-color); + text-align: right; + padding: var(--padding-tiny); + border-top: 1px solid var(--nav-border-color); +} + +footer p { + margin: 0; + color: var(--text-color); + margin-right: var(--margin-small); + font-size: 0.8rem; +} + .gantt-info { margin: 0.5rem; font-size: var(--font-size-small); diff --git a/src/blenderbim/blenderbim/bim/data/webui/static/css/index.css b/src/blenderbim/blenderbim/bim/data/webui/static/css/index.css index 7a480b42c8..37386ebea9 100644 --- a/src/blenderbim/blenderbim/bim/data/webui/static/css/index.css +++ b/src/blenderbim/blenderbim/bim/data/webui/static/css/index.css @@ -1,12 +1,16 @@ :root { --font-family: Arial, sans-serif; + --base-font-size: 16px; + --margin-tiny: 0.125rem; --margin-small: 0.625rem; --margin-medium: 1.25rem; --margin-large: 2.5rem; - --padding-small: 0.125rem; + --padding-tiny: 0.125rem; + --padding-small: 0.625rem; --padding-medium: 1rem; --font-size-large: 1.2rem; --logo-height: 2.5rem; + --nav-height: 2.5rem; } :root.dark { @@ -18,6 +22,7 @@ --nav-link-color: #fff; --nav-link-hover-color: #3fb449; --warning-color: #FFDB8F; + --table-border-color: #464444; } :root.light { @@ -29,6 +34,7 @@ --nav-link-color: #000000; --nav-link-hover-color: #38a63d; --warning-color: #FF4500; + --table-border-color: #222; } html { @@ -40,29 +46,26 @@ body { color: var(--text-color); margin: 0; font-family: var(--font-family); + display: flex; + flex-direction: column; + min-height: 100vh; } #container { + flex: 1; margin-top: var(--margin-medium); margin-left: var(--margin-small); margin-right: var(--margin-small); margin-bottom: var(--margin-medium); } -.table-container { - margin-bottom: var(--margin-large); -} - -.csv-table { - margin-top: var(--margin-small); -} - h3 { margin-top: 0; } nav { background-color: var(--nav-bg-color); + height: var(--nav-height); padding: var(--padding-medium) 0; display: flex; align-items: center; @@ -102,7 +105,7 @@ nav ul li a.active { .warning { color: var(--warning-color); margin-bottom: var(--margin-small); - padding: var(--padding-small); + padding: var(--padding-tiny); display: none; } @@ -118,6 +121,95 @@ nav ul li a.active { outline: none; } +#client-list { + position: absolute; + top: calc(0 + var(--nav-height)); + overflow-y: auto; + overflow: hidden; + transition: opacity 0.3s ease-out, visibility 0.3s ease-out; + border: 1px solid var(--table-border-color); + background-color: var(--nav-bg-color); + z-index: 1; + opacity: 0; + visibility: hidden; + width: auto; +} + +#client-list.show { + opacity: 1; + visibility: visible; +} + +#connected-list-div { + display: inline-block; +} + +.client { + padding: var(--margin-small); + border-bottom: 1px solid var(--table-border-color); + cursor: pointer; +} + +.client-details { + max-height: 0; + overflow: hidden; + padding-left: var(--padding-small); + transition: max-height 0.2s ease-out, padding 0.2s ease-out, opacity 0.1s ease-out; + background-color: var(--nav-bg-color); + margin-top: var(--margin-tiny); + opacity: 0; +} + +.client-details.show { + max-height: none; + opacity: 1; +} + +.client-detail { + border-bottom: 1px solid var(--table-border-color); + padding: var(--padding-small); +} + +#show-connected-button { + width: auto; + background-color: var(--bg-color); + border: none; + font-size: var(--base-font-size); +} + +#show-connected-button:hover, +.scroll-button:hover { + cursor: pointer; +} + +.scroll-button { + font-size: var(--base-font-size); + margin-left: var(--margin-tiny); + margin-top: var(--margin-small); +} + +footer { + background-color: var(--nav-bg-color); + text-align: right; + padding: var(--padding-tiny); + border-top: 1px solid var(--nav-border-color); +} + +footer p { + margin: 0; + color: var(--text-color); + margin-right: var(--margin-small); + font-size: 0.8rem; +} + +.table-container { + margin-bottom: var(--margin-large); +} + +.csv-table { + margin-top: var(--margin-small); +} + /* ------------ Overwriting Tabulator CSS rules ------------ */ :root.light .tabulator-header, @@ -127,6 +219,6 @@ nav ul li a.active { } :root.light .tabulator { - border-top: 1px solid #222; - border-bottom: 2px solid #222; + border-top: 1px solid var(--table-border-color); + border-bottom: 2px solid var(--table-border-color); } \ No newline at end of file diff --git a/src/blenderbim/blenderbim/bim/data/webui/static/js/drawings.js b/src/blenderbim/blenderbim/bim/data/webui/static/js/drawings.js new file mode 100644 index 0000000000..8a865dcab1 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/webui/static/js/drawings.js @@ -0,0 +1,263 @@ +// keeps track of blenders connected +const connectedClients = {}; +const allDrawings = {}; +let socket; + +// Document ready function +$(document).ready(function () { + var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches + ? "light" + : "dark"; + var theme = localStorage.getItem("theme") || systemTheme; + setTheme(theme); + + $("#dropdown-menu").change(function () { + const blenderId = $(this).attr("id"); + const ifcFile = $(this).val(); + displayDrawingsNames(blenderId, ifcFile); + }); + + connectSocket(); +}); + +// Function to connect to Socket.IO server +function connectSocket() { + const url = "ws://localhost:" + SOCKET_PORT + "/web"; + socket = io(url); + console.log("socket: ", socket); + + // Register socket event handlers + socket.on("blender_connect", handleBlenderConnect); + socket.on("blender_disconnect", handleBlenderDisconnect); + socket.on("connect", handleWebConnect); + socket.on("drawings_data", handleDrawingsData); + socket.on("svg_data", handleSvgData); + // socket.on("default_data", handleDefaultData); +} + +// function used to get drawings data from blenderbim +function handleWebConnect() { + const msg = { + sourcePage: "drawings", + operator: { + type: "getDrawings", + }, + }; + socket.emit("web_operator", msg); +} + +// Function to handle 'blender_connect' event +function handleBlenderConnect(blenderId) { + console.log("blender_connect: ", blenderId); + if (!connectedClients.hasOwnProperty(blenderId)) { + connectedClients[blenderId] = { + shown: false, + ifc_file: "", + }; + } +} + +// Function to handle 'blender_disconnect' event +function handleBlenderDisconnect(blenderId) { + console.log("blender_disconnect: ", blenderId); + if (connectedClients.hasOwnProperty(blenderId)) { + delete connectedClients[blenderId]; + // remove(blenderId); + } + + $("#blender-count").text(function (i, text) { + return parseInt(text, 10) - 1; + }); +} + +function handleDrawingsData(data) { + const blenderId = data["blenderId"]; + + console.log(data); + + const filename = data["data"]["ifc_file"]; + const drawings = data["data"]["drawings_data"]["drawings"]; + const sheets = data["data"]["drawings_data"]["sheets"]; + + allDrawings[filename] = { drawings: drawings, sheets: sheets }; + + console.log(connectedClients); + + if (connectedClients.hasOwnProperty(blenderId)) { + if (!connectedClients[blenderId].shown) { + connectedClients[blenderId] = { + shown: true, + ifc_file: filename, + }; + addSelectOption(blenderId, filename); + } else { + // update(blenderId, data, filename); + } + } else { + connectedClients[blenderId] = { + shown: true, + ifc_file: filename, + }; + addSelectOption(blenderId, filename); + } +} + +function handleSvgData(data) { + const svgContainer = document.getElementById("svg-container"); + svgContainer.innerHTML = ""; + svgContainer.style.backgroundColor = "white"; + + const dom = SVG(svgContainer).svg(data); + const svgElement = dom.node.children[0]; + + // to make the svg not grow outside the svg container + svgElement.setAttribute("width", "100%"); + svgElement.setAttribute("height", "100%"); + svgElement.style.maxWidth = "100%"; + svgElement.style.maxHeight = "100%"; + svgElement.style.width = "100%"; + svgElement.style.height = "100%"; + svgElement.style.boxSizing = "border-box"; // Ensure padding and borders are included in the element's total width and height + + const panZoomControls = svgPanZoom(svgElement, { + zoomEnabled: true, + controlIconsEnabled: true, + fit: true, + center: true, + }); +} + +function addSelectOption(blenderId, filename) { + const filenameOption = $("") + .attr("id", "blender-" + blenderId) + .val(filename) + .text(filename); + $("#dropdown-menu").append(filenameOption); +} + +function displayDrawingsNames(blenderId, ifcFile) { + drawings = allDrawings[ifcFile].drawings; + sheets = allDrawings[ifcFile].sheets; + + function createSvgNames(text, index, type) { + var label = $("
  • ") + .text(text) + .attr("id", type + "-" + index) + .addClass("svg-name") + .click(function () { + const id = $(this).attr("id").split("-"); + const index = parseInt(id[1]); + const type = id[0]; + const ifcFile = $("#dropdown-menu").val(); + + var path = ""; + + if (type === "drawing") { + path = allDrawings[ifcFile].drawings[index].path; + } else if (type === "sheet") { + path = allDrawings[ifcFile].sheets[index].path; + } + + const msg = { + path: path, + }; + console.log(msg); + socket.emit("get_svg", msg); + }); + + if (type === "drawing") $("#drawings-sub-panel").append(label); + else if (type === "sheet") $("#sheets-sub-panel").append(label); + } + + $("#drawings-sub-panel").empty(); + drawings.forEach(function (drawing, index) { + createSvgNames(drawing.name, index, "drawing"); + }); + + $("#sheets-sub-panel").empty(); + sheets.forEach(function (sheet, index) { + createSvgNames(sheet.name, index, "sheet"); + }); +} + +function setTheme(theme) { + if (theme === "light") { + $("html").removeClass("dark").addClass("light"); + $("#toggle-theme").html(''); + } else { + $("html").removeClass("light").addClass("dark"); + $("#toggle-theme").html(''); + } + localStorage.setItem("theme", theme); +} + +function toggleTheme() { + if ($("html").hasClass("dark")) { + setTheme("light"); + } else { + setTheme("dark"); + } +} + +function toggleClientList() { + var clientList = $("#client-list"); + + if (clientList.hasClass("show")) { + clientList.removeClass("show"); + return; + } + + clientList.empty(); + + $.each(connectedClients, function (id, client) { + if (!client.shown) return; + + const dropdownIcon = $("") + .addClass("fas fa-chevron-down") + .css("margin-left", "0.5rem"); + + const clientDiv = $("
    ").addClass("client").text(client.ifc_file); + + clientDiv.append(dropdownIcon); + + const clientDetailsDiv = $("
    ").addClass("client-details"); + + if (id) { + const clientId = $("
    ") + .addClass("client-detail") + .text(`Blender ID: ${id}`); + clientDetailsDiv.append(clientId); + } + + if (client.headers && client.headers.length) { + const clientHeaders = $("
    ") + .addClass("client-detail") + .text(`Table Headers: ${client.headers.join(", ")}`); + + const scrollButton = $(" + +
    + +
    +
    +
    +
    + + + +
    +
    +
    +
    + +
    + +
    +
    +
      + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
      + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/src/blenderbim/blenderbim/bim/data/webui/templates/gantt.html b/src/blenderbim/blenderbim/bim/data/webui/templates/gantt.html index 78044d45a3..280c116dbb 100644 --- a/src/blenderbim/blenderbim/bim/data/webui/templates/gantt.html +++ b/src/blenderbim/blenderbim/bim/data/webui/templates/gantt.html @@ -34,11 +34,23 @@ +
    + +
    +
    +
    +

    BlenderBIM Version: {{version}}

    +
    diff --git a/src/blenderbim/blenderbim/bim/data/webui/templates/index.html b/src/blenderbim/blenderbim/bim/data/webui/templates/index.html index e948b4c4be..e31204d197 100644 --- a/src/blenderbim/blenderbim/bim/data/webui/templates/index.html +++ b/src/blenderbim/blenderbim/bim/data/webui/templates/index.html @@ -41,11 +41,23 @@ +
    + +
    +
    +
    +

    BlenderBIM Version: {{version}}

    +
    diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 53cb68639e..e2158248ff 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -170,6 +170,11 @@ def subscribe_to(obj, data_path, callback): def refresh_ui_data(): + """Refresh cached UI data. + + Note that calling non-ifc-operators by itself doesn't refresh the UI data + and it need to be refreshed manually if needed. + """ from blenderbim.bim import modules for name, value in modules.items(): diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 867e51b222..c2b250f6dd 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -50,6 +50,8 @@ def draw_attribute(attribute, layout, copy_operator=None): return if value_name == "enum_value": prop_with_search(layout, attribute, "enum_value", text=attribute.name) + elif value_name == "filepath_value": + attribute.filepath_value.layout_file_select(layout, filter_glob=attribute.filter_glob, text=attribute.name) elif attribute.name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"]: propis = bpy.context.scene.BIMWorkScheduleProperties for item in propis.durations_attributes: diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 60d202194d..9018592086 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -551,7 +551,7 @@ class IfcImporter: for axis in axes: shape = tool.Loader.create_generic_shape(axis.AxisCurve) mesh = self.create_mesh(axis, shape) - obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh) + obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh) if tool.Blender.get_addon_preferences().lock_grids_on_import: obj.lock_location = (True, True, True) obj.lock_rotation = (True, True, True) @@ -781,7 +781,7 @@ class IfcImporter: mesh = bpy.data.meshes.new(mesh_name) mesh.from_pydata([mathutils.Vector(vertex) * self.unit_scale], [], []) - obj = bpy.data.objects.new("{}/{}".format(product.is_a(), product.Name), mesh) + obj = bpy.data.objects.new(tool.Loader.get_name(product), mesh) self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, placement_matrix)) self.link_element(product, obj) @@ -847,7 +847,7 @@ class IfcImporter: mesh.from_pydata(vertex_list, [], []) tool.Ifc.link(representation, mesh) - obj = bpy.data.objects.new("{}/{}".format(product.is_a(), product.Name), mesh) + obj = bpy.data.objects.new(tool.Loader.get_name(product), mesh) self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, placement_matrix)) self.link_element(product, obj) return product diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/data.py b/src/blenderbim/blenderbim/bim/module/aggregate/data.py index 8eef8b00bd..41d16fdd9a 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/data.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/data.py @@ -75,7 +75,7 @@ class AggregateData: return [] if not element: return [] - + product_linked_agg_group = [ r for r in getattr(aggregate, "HasAssignments", []) or [] @@ -85,7 +85,6 @@ class AggregateData: if not product_linked_agg_group: return [] - + total = len(product_linked_agg_group[0].RelatedObjects) return total - diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index 08652e6b50..f7adac7e8b 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -167,7 +167,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator): tool.Ifc, tool.Collector, tool.Spatial, - structure_obj=tool.Ifc.get_object(current_container), + container=current_container, element_obj=aggregate, ) core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index ff2292baad..aa52504670 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -99,7 +99,7 @@ class BIM_PT_aggregate(Panel): op = row.operator("bim.select_parts", icon="RESTRICT_SELECT_OFF", text="") op.obj = context.active_object.name - + class BIM_PT_linked_aggregate(Panel): bl_label = "Linked Aggregates" bl_idname = "BIM_PT_linked_aggregate" @@ -133,9 +133,8 @@ class BIM_PT_linked_aggregate(Panel): props = obj.BIMObjectAggregateProperties row = layout.row(align=True) - if element.Decomposes: - Number_Linked_Aggregates = AggregateData.data['total_linked_aggregate'] + Number_Linked_Aggregates = AggregateData.data["total_linked_aggregate"] if not Number_Linked_Aggregates: row.label(text="Not a Linked Aggregate") else: @@ -151,6 +150,3 @@ class BIM_PT_linked_aggregate(Panel): op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X") else: row.label(text="Not an Aggregate") - - - diff --git a/src/blenderbim/blenderbim/bim/module/attribute/operator.py b/src/blenderbim/blenderbim/bim/module/attribute/operator.py index bc6a983d9a..d7b1331beb 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/operator.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/operator.py @@ -21,6 +21,7 @@ import json import ifcopenshell import ifcopenshell.api import ifcopenshell.guid +import ifcopenshell.util.element import blenderbim.bim.helper import blenderbim.bim.handler import blenderbim.tool as tool @@ -36,7 +37,7 @@ class EnableEditingAttributes(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - obj = bpy.data.objects.get(self.obj) + obj = bpy.data.objects[self.obj] props = obj.BIMAttributeProperties props.attributes.clear() diff --git a/src/blenderbim/blenderbim/bim/module/attribute/ui.py b/src/blenderbim/blenderbim/bim/module/attribute/ui.py index 0680d5f654..f4eadf7eac 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/ui.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/ui.py @@ -45,8 +45,7 @@ def draw_ui(context, layout, attributes): row.label(text=attribute["name"]) # row.label(text=attribute["value"]) op = row.operator("bim.select_similar", text=attribute["value"], icon="NONE", emboss=False) - op.key = attribute['name'] - + op.key = attribute["name"] # TODO: reimplement, see #1222 # if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name: diff --git a/src/blenderbim/blenderbim/bim/module/bcf/operator.py b/src/blenderbim/blenderbim/bim/module/bcf/operator.py index 828b953d29..7b9079aa62 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/operator.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/operator.py @@ -66,10 +66,7 @@ class LoadBcfProject(bpy.types.Operator): if bcfxml.version.version_id.startswith("2"): print("No project, we will create one for BBIM.") bcfxml.project_info = bcf.v2.model.ProjectExtension( - project=bcf.v2.model.Project( - name=nameless, - project_id=str(uuid.uuid4()) - ), extension_schema="" + project=bcf.v2.model.Project(name=nameless, project_id=str(uuid.uuid4())), extension_schema="" ) if bcfxml.project.name is None: bcfxml.project.name = nameless @@ -971,13 +968,14 @@ class ActivateBcfViewpoint(bpy.types.Operator): self.set_colours(viewpoint) def set_exceptions(self, viewpoint, context): - if ( - not hasattr(viewpoint.visualization_info.components, "visibility") - or not hasattr(viewpoint.visualization_info.components.visibility.exceptions, "component") + if not hasattr(viewpoint.visualization_info.components, "visibility") or not hasattr( + viewpoint.visualization_info.components.visibility.exceptions, "component" ): return - exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions.component or []} + exception_global_ids = { + v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions.component or [] + } # print("default_visibility: {}".format(viewpoint.visualization_info.components.visibility.default_visibility)) if viewpoint.visualization_info.components.visibility.default_visibility: @@ -1034,7 +1032,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): context.area.type = old def set_openings_visibility(self, is_visible, context): - pass # We no longer have an openings collection + pass # We no longer have an openings collection def set_selection(self, viewpoint): if not viewpoint.visualization_info.components or not viewpoint.visualization_info.components.selection: @@ -1147,7 +1145,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): # https://github.com/buildingSMART/BCF-XML/tree/release_3_0/Documentation#coloring if lv == 8: t = tuple(int(value[i : i + lv // 4], 16) for i in range(0, lv, lv // 4)) - col = [t[1] / 255.0, t[2] / 255.0, t[3] / 255.0, t[0] / 255.0] + col = [t[1] / 255.0, t[2] / 255.0, t[3] / 255.0, t[0] / 255.0] else: t = tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)) col = [t[0] / 255.0, t[1] / 255.0, t[2] / 255.0, 1] diff --git a/src/blenderbim/blenderbim/bim/module/boundary/operator.py b/src/blenderbim/blenderbim/bim/module/boundary/operator.py index e8536076e0..e325674e11 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/operator.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/operator.py @@ -603,7 +603,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator): objs = context.selected_objects - if not any((element:=tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs): + if not any((element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace") for obj in objs): self.report({"INFO"}, "No IfcSpace elements selected.") return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/brick/data.py b/src/blenderbim/blenderbim/bim/module/brick/data.py index db8cb856d9..66ca14104f 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/data.py +++ b/src/blenderbim/blenderbim/bim/module/brick/data.py @@ -166,9 +166,9 @@ class BrickschemaReferencesData: results.append( { "id": reference.id(), - "identification": reference.ItemReference - if tool.Ifc.get_schema() == "IFC2X3" - else reference.Identification, + "identification": ( + reference.ItemReference if tool.Ifc.get_schema() == "IFC2X3" else reference.Identification + ), "name": reference.Name or "Unnamed", } ) diff --git a/src/blenderbim/blenderbim/bim/module/brick/prop.py b/src/blenderbim/blenderbim/bim/module/brick/prop.py index a9f271e927..e419271cc1 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/prop.py +++ b/src/blenderbim/blenderbim/bim/module/brick/prop.py @@ -34,6 +34,7 @@ import blenderbim.core.brick as core import blenderbim.tool.brick as tool from blenderbim.tool.brick import BrickStore + def update_active_brick_index(self, context): BrickschemaData.is_loaded = False @@ -53,7 +54,7 @@ def get_brick_entity_classes(self, context): return [(uri, uri.split("#")[-1], "") for uri in BrickStore.entity_classes[entity]] -def get_brick_roots(self, context): +def get_brick_roots(self, context): return [(root, root, "") for root in BrickStore.root_classes] @@ -64,13 +65,13 @@ def get_brick_relations(self, context): return relations relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", "")) return relations - def update_view(self, context): root = context.scene.BIMBrickProperties.brick_list_root core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False) + def split_screen_update_view(self, context): root = context.scene.BIMBrickProperties.split_screen_brick_list_root core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True) @@ -108,7 +109,11 @@ class BIMBrickProperties(PropertyGroup): # create relations split screen split_screen_toggled: BoolProperty(name="Split Screen Toggled", default=False) split_screen_bricks: CollectionProperty(name="Split Screen Bricks", type=Brick) - split_screen_active_brick_index: IntProperty(name="Split Screen Active Brick Index", update=update_active_brick_index) + split_screen_active_brick_index: IntProperty( + name="Split Screen Active Brick Index", update=update_active_brick_index + ) split_screen_active_brick_class: StringProperty(name="Split Screen Active Brick Class") split_screen_brick_breadcrumbs: CollectionProperty(name="Split Screen Brick Breadcrumbs", type=StrProperty) - split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view) \ No newline at end of file + split_screen_brick_list_root: EnumProperty( + name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view + ) diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index b09507ea83..8b2081310f 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -46,7 +46,7 @@ class BIM_PT_brickschema_project_info(Panel): def draw(self, context): self.props = context.scene.BIMBrickProperties - + if not BrickschemaData.data["is_loaded"]: row = self.layout.row(align=True) row.operator("bim.new_brick_file", text="Create Project") @@ -58,7 +58,7 @@ class BIM_PT_brickschema_project_info(Panel): row.label(text=BrickStore.path, icon="FILEBROWSER") else: row.label(text="No file", icon="FILEBROWSER") - + row = self.layout.row(align=True) if BrickStore.last_saved: row.label(text=BrickStore.last_saved, icon="TIME") @@ -130,7 +130,7 @@ class BIM_PT_brickschema_create_entity(Panel): row = self.layout.row(align=True) prop_with_search(row, self.props, "brick_entity_class", text="Type") - + row = self.layout.row(align=True) active = tool.Ifc.get_entity(context.active_object) if active and context.selected_objects: @@ -141,7 +141,7 @@ class BIM_PT_brickschema_create_entity(Panel): row = self.layout.row(align=True) row.operator("bim.add_brick", text="Create Entity") - + class BIM_PT_brickschema_viewport(Panel): bl_label = "Viewport" @@ -194,7 +194,9 @@ class BIM_PT_brickschema_viewport(Panel): row = grid_right.row() BIM_UL_bricks.split_screen = True - row.template_list("BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index") + row.template_list( + "BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index" + ) if BrickschemaData.data["active_relations"]: row = self.layout.row(align=True) @@ -213,7 +215,13 @@ class BIM_PT_brickschema_viewport(Panel): row.label(text="No selection", icon="INFO") else: prop_with_search(row, self.props, "new_brick_relation_type", text="") - row.label(text=split_screen_selection.label if split_screen_selection.label else split_screen_selection.name) + row.label( + text=( + split_screen_selection.label + if split_screen_selection.label + else split_screen_selection.name + ) + ) row.operator("bim.add_brick_relation", text="", icon="ADD") elif self.props.brick_create_relations_toggled: @@ -234,7 +242,11 @@ class BIM_PT_brickschema_viewport(Panel): row.label(text=relation["object_name"]) row = split.row(align=True) row.column().alignment = "RIGHT" - if self.props.brick_edit_relations_toggled and relation["predicate_uri"] and relation["predicate_name"] != "type": + if ( + self.props.brick_edit_relations_toggled + and relation["predicate_uri"] + and relation["predicate_name"] != "type" + ): op = row.operator("bim.remove_brick_relation", text="", icon="UNLINKED") op.predicate = relation["predicate_uri"] op.object = relation["object_uri"] @@ -263,6 +275,7 @@ class BIM_UL_bricks(UIList): row = split.row() row.label(text=str(item.total_items)) + class BIM_PT_ifc_brickschema_references(Panel): bl_label = "Brickschema References" bl_idname = "BIM_PT_ifc_brickschema_references" @@ -291,13 +304,13 @@ class BIM_PT_ifc_brickschema_references(Panel): row.label(text="No IFC Libraries") row.operator("bim.convert_brick_project", text="", icon="ADD") return - + elif not BrickschemaReferencesData.data["libraries"]: row = self.layout.row(align=True) row.label(text="No IFC Libraries: save the Brick project to create a new library", icon="ERROR") row = self.layout.row(align=True) - row.label(text="Libraries must have location pointing to a \".ttl\" file", icon="INFO") + row.label(text='Libraries must have location pointing to a ".ttl" file', icon="INFO") return row = self.layout.row(align=True) @@ -318,4 +331,4 @@ class BIM_PT_ifc_brickschema_references(Panel): row.label(text=reference["identification"], icon="ASSET_MANAGER") row.label(text=reference["name"]) row.operator("bim.unassign_library_reference", text="", icon="X").reference = reference["id"] - row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT").item = reference["identification"] \ No newline at end of file + row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT").item = reference["identification"] diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/operator.py b/src/blenderbim/blenderbim/bim/module/bsdd/operator.py index 1d64f03029..42963e756c 100644 --- a/src/blenderbim/blenderbim/bim/module/bsdd/operator.py +++ b/src/blenderbim/blenderbim/bim/module/bsdd/operator.py @@ -47,19 +47,27 @@ class SetActiveBSDDDictionary(bpy.types.Operator): class SearchBSDDClass(bpy.types.Operator): bl_idname = "bim.search_bsdd_classifications" bl_label = "Search bSDD Class" + bl_description = "Search for bSDD classes by the provided keyword" bl_options = {"REGISTER", "UNDO"} def execute(self, context): keyword = context.scene.BIMBSDDProperties.keyword - core.search_class(keyword, bsdd.Client(), tool.Bsdd) + classes_found = core.search_class(keyword, bsdd.Client(), tool.Bsdd) + self.report({"INFO"}, f"{classes_found} bSDD classes found for '{keyword}'.") return {"FINISHED"} class GetBSDDClassificationProperties(bpy.types.Operator): bl_idname = "bim.get_bsdd_classification_properties" - bl_label = "Search bSDD Classifications" + bl_label = "Search bSDD Class Properties" + bl_description = ( + "Search for bSDD class properties for the currently selected class. All non-IFC properties are skipped" + ) bl_options = {"REGISTER", "UNDO"} def execute(self, context): - core.get_class_properties(bsdd.Client(), tool.Bsdd) + pset_data = core.get_class_properties(bsdd.Client(), tool.Bsdd) + psets_found = len(pset_data) + props_found = sum(len(pset) for pset in pset_data.values()) + self.report({"INFO"}, f"{psets_found} psets found ({props_found} props) for the active classification.") return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/prop.py b/src/blenderbim/blenderbim/bim/module/bsdd/prop.py index 2a54c8f7ce..0d3aa18969 100644 --- a/src/blenderbim/blenderbim/bim/module/bsdd/prop.py +++ b/src/blenderbim/blenderbim/bim/module/bsdd/prop.py @@ -43,7 +43,6 @@ class BSDDDomain(PropertyGroup): class BSDDClassification(PropertyGroup): name: StringProperty(name="Name") reference_code: StringProperty(name="Reference Code") - description: StringProperty(name="Description") uri: StringProperty(name="Namespace URI") domain_name: StringProperty(name="Domain Name") domain_namespace_uri: StringProperty(name="Domain Namespace URI") @@ -62,6 +61,12 @@ class BIMBSDDProperties(PropertyGroup): classifications: CollectionProperty(name="Classifications", type=BSDDClassification) active_classification_index: IntProperty(name="Active Classification Index") keyword: StringProperty(name="Keyword") - should_filter_ifc_class: BoolProperty(name="Filter Active IFC Class", default=True) - load_preview_domains: BoolProperty(name="Load Preview Domains", default=False) + should_filter_ifc_class: BoolProperty( + name="Filter Active IFC Class", + description="Whether to search only for bSDD classes that match active object's IFC class", + default=True, + ) + load_preview_domains: BoolProperty( + name="Load Preview Domains", description="Whether it should load preview and inactive domains", default=False + ) classification_psets: CollectionProperty(name="Classification Psets", type=BSDDPset) diff --git a/src/blenderbim/blenderbim/bim/module/bsdd/ui.py b/src/blenderbim/blenderbim/bim/module/bsdd/ui.py index 899e8dcf2b..11d497e555 100644 --- a/src/blenderbim/blenderbim/bim/module/bsdd/ui.py +++ b/src/blenderbim/blenderbim/bim/module/bsdd/ui.py @@ -32,6 +32,7 @@ class BIM_PT_bsdd(Panel): def draw(self, context): props = context.scene.BIMBSDDProperties + layout = self.layout row = self.layout.row(align=True) row.prop(props, "load_preview_domains") if len(props.domains): @@ -53,6 +54,22 @@ class BIM_PT_bsdd(Panel): props, "active_domain_index", ) + if 0 <= props.active_domain_index < len(props.domains): + selected_domain = props.domains[props.active_domain_index] + else: + selected_domain = None + + if selected_domain: + layout.label(text="Selected domain:") + box = layout.box() + row = box.row(align=True) + row.label(text="Language") + row.label(text=selected_domain.default_language_code) + row = box.row(align=True) + row.label(text="Version") + row.label(text=selected_domain.version) + box.operator("bim.open_uri", text="Open bSDD In Browser", icon="URL").uri = selected_domain.uri + else: row = self.layout.row() row.operator("bim.load_bsdd_domains") @@ -61,14 +78,18 @@ class BIM_PT_bsdd(Panel): class BIM_UL_bsdd_domains(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: + props = context.scene.BIMBSDDProperties row = layout.row(align=True) if item.status != "Active": row.label(text=f"{item.name} ({item.organization_name_owner}) - {item.status}", icon="ERROR") else: row.label(text=f"{item.name} ({item.organization_name_owner})") - op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF") - op.name = item.name - op.uri = item.uri + if item.uri == props.active_uri: + row.label(text="", icon="URL") + else: + op = row.operator("bim.set_active_bsdd_domain", text="", icon="RESTRICT_SELECT_OFF") + op.name = item.name + op.uri = item.uri class BIM_UL_bsdd_classifications(UIList): @@ -77,3 +98,4 @@ class BIM_UL_bsdd_classifications(UIList): row = layout.row(align=True) row.label(text=item.reference_code) row.label(text=item.name) + row.operator("bim.open_uri", text="", icon="URL").uri = item.uri diff --git a/src/blenderbim/blenderbim/bim/module/cad/operator.py b/src/blenderbim/blenderbim/bim/module/cad/operator.py index 14f92ff9b7..61d00c680f 100644 --- a/src/blenderbim/blenderbim/bim/module/cad/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cad/operator.py @@ -144,7 +144,7 @@ class CadFillet(bpy.types.Operator): def execute(self, context): si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.radius = self.radius * si_conversion - + bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="EDIT") @@ -369,7 +369,7 @@ class CadOffset(bpy.types.Operator): si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.distance = self.distance * si_conversion - + bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="EDIT") @@ -507,8 +507,6 @@ class CadOffset(bpy.types.Operator): local_direction = (rotation_i @ direction).normalized() normals.append(local_direction) - - if len(normals) == 2: # https://stackoverflow.com/a/54042831/9627415 new_normal = (normals[0].lerp(normals[1], 0.5)).normalized() @@ -638,7 +636,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator): def execute(self, context): si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.radius = self.radius * si_conversion - + if self.has_selected_existing_arc(context): self.change_radius(context) else: @@ -814,8 +812,8 @@ class AddRectangle(bpy.types.Operator): def execute(self, context): si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.x = self.x * si_conversion - self.y = self.y * si_conversion - + self.y = self.y * si_conversion + obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) cursor = obj.matrix_world.inverted() @ context.scene.cursor.location diff --git a/src/blenderbim/blenderbim/bim/module/clash/decorator.py b/src/blenderbim/blenderbim/bim/module/clash/decorator.py index 43c40a339a..b7d5806741 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/decorator.py +++ b/src/blenderbim/blenderbim/bim/module/clash/decorator.py @@ -70,7 +70,7 @@ class ClashDecorator: blf.color(font_id, *color) if coords_2d: w, h = blf.dimensions(font_id, text) - coords_2d -= Vector((w * .5, 0)) + coords_2d -= Vector((w * 0.5, 0)) blf.position(font_id, coords_2d[0], coords_2d[1], 0) blf.draw(font_id, text) # Set your text here diff --git a/src/blenderbim/blenderbim/bim/module/clash/operator.py b/src/blenderbim/blenderbim/bim/module/clash/operator.py index f32e7bfad3..bd9d99a741 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/operator.py +++ b/src/blenderbim/blenderbim/bim/module/clash/operator.py @@ -496,4 +496,3 @@ class SelectSmartGroup(bpy.types.Operator): with bpy.context.temp_override(**context_override): bpy.ops.view3d.view_selected() return {"FINISHED"} - diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index d7db0a68cb..be5ce41c71 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -117,20 +117,29 @@ class AddClassificationFromBSDD(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3" - props = context.scene.BIMBSDDProperties - domain = [d for d in props.domains if d.name == props.active_domain][0] + domain = next((d for d in props.domains if d.name == props.active_domain and d.uri == props.active_uri), None) + + # Maybe user loaded preview domains, set it as active + # and then reloaded them without preview domains. + if not domain: + self.report( + {"INFO"}, + f"Couldn't find domain '{props.active_domain}' ({props.active_uri}). Try to reload bSDD dictionaries.", + ) + return + for element in tool.Ifc.get().by_type("IfcClassification"): - if element.Name == props.active_domain or (not is_ifc2x3 and element.Location == domain.uri): + if element.Name == props.active_domain or (tool.Classification.get_location(element) == domain.uri): + self.report({"INFO"}, f"Classification '{props.active_domain}' is already added to the project.") return + classification = ifcopenshell.api.run( "classification.add_classification", tool.Ifc.get(), classification=props.active_domain ) classification.Source = domain.organization_name_owner classification.Edition = domain.version - if not is_ifc2x3: - classification.Location = domain.uri + tool.Classification.set_location(classification, domain.uri) class EnableAddingManualClassification(bpy.types.Operator): @@ -400,8 +409,6 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): obj_type: bpy.props.StringProperty() def _execute(self, context): - is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3" - if self.obj_type == "Object": if context.selected_objects: objects = [o.name for o in context.selected_objects] @@ -416,9 +423,8 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): classification = None for element in tool.Ifc.get().by_type("IfcClassification"): - if ( - element.Name == bsdd_classification.domain_name - or (not is_ifc2x3 and element.Location == bsdd_classification.domain_namespace_uri) + if element.Name == bsdd_classification.domain_name or ( + tool.Classification.get_location(element) == bsdd_classification.domain_namespace_uri ): classification = element break @@ -427,8 +433,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): classification = ifcopenshell.api.run( "classification.add_classification", tool.Ifc.get(), classification=bsdd_classification.domain_name ) - if not is_ifc2x3: - classification.Location = bsdd_classification.domain_namespace_uri + tool.Classification.set_location(classification, bsdd_classification.domain_namespace_uri) for obj in objects: ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) diff --git a/src/blenderbim/blenderbim/bim/module/constraint/ui.py b/src/blenderbim/blenderbim/bim/module/constraint/ui.py index 0f3b522c7e..8b53f20b9c 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/ui.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/ui.py @@ -105,7 +105,7 @@ class BIM_PT_object_constraints(Panel): if constraint["type"] == "IfcObjective": icon = "LIGHT" else: - continue # Metric not implemented + continue # Metric not implemented row = self.layout.row(align=True) row.label(text=constraint["name"]) row.operator("bim.unassign_constraint", text="", icon="X").constraint = constraint["id"] diff --git a/src/blenderbim/blenderbim/bim/module/cost/data.py b/src/blenderbim/blenderbim/bim/module/cost/data.py index 09698a54be..64247f10e4 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/data.py +++ b/src/blenderbim/blenderbim/bim/module/cost/data.py @@ -164,7 +164,14 @@ class CostSchedulesData: if unit: data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) if quantity.is_a("IfcPhysicalSimpleQuantity"): - measure_class = quantity.wrapped_data.declaration().as_entity().attribute_by_index(3).type_of_attribute().declared_type().name() + measure_class = ( + quantity.wrapped_data.declaration() + .as_entity() + .attribute_by_index(3) + .type_of_attribute() + .declared_type() + .name() + ) if "Count" in measure_class: data["UnitSymbol"] = "U" diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 0df9354468..329cc2d86d 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -47,14 +47,14 @@ class BIM_PT_cost_schedules(Panel): row = self.layout.row(align=True) if CostSchedulesData.data["total_cost_schedules"]: row.alignment = "RIGHT" - row.operator("bim.export_cost_schedules",icon="EXPORT",text="Export All Schedules") + row.operator("bim.export_cost_schedules", icon="EXPORT", text="Export All Schedules") row = self.layout.row(align=True) row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT") else: row.label(text="No Cost Schedules found.", icon="TEXT") row.operator("bim.add_cost_schedule", icon="ADD", text="") - row.operator("bim.import_cost_schedule_csv",icon="IMPORT",text="") + row.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="") for schedule in CostSchedulesData.data["schedules"]: self.draw_cost_schedule_ui(schedule) @@ -96,9 +96,9 @@ class BIM_PT_cost_schedules(Panel): text="{}[{}]".format(cost_schedule["name"], cost_schedule["predefined_type"]), icon="LINENUMBERS_ON" ) row.operator("bim.enable_editing_cost_items", text="", icon="OUTLINER").cost_schedule = cost_schedule["id"] - row.operator( - "bim.enable_editing_cost_schedule_attributes", text="", icon="GREASEPENCIL" - ).cost_schedule = cost_schedule["id"] + row.operator("bim.enable_editing_cost_schedule_attributes", text="", icon="GREASEPENCIL").cost_schedule = ( + cost_schedule["id"] + ) row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule["id"] if self.props.active_cost_schedule_id == cost_schedule["id"]: if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES": @@ -616,9 +616,9 @@ class BIM_UL_cost_items_trait: def draw_parent_operator(self, row, cost_item_id): if self.props.active_cost_item_id: if self.props.active_cost_item_id != cost_item_id: - op = row.operator( - "bim.change_parent_cost_item", text="", icon="LINKED", emboss=False - ).new_parent = cost_item_id + op = row.operator("bim.change_parent_cost_item", text="", icon="LINKED", emboss=False).new_parent = ( + cost_item_id + ) else: row.label(text="", icon="BLANK1") diff --git a/src/blenderbim/blenderbim/bim/module/covering/prop.py b/src/blenderbim/blenderbim/bim/module/covering/prop.py index 76cd6ee3e0..f55235e028 100644 --- a/src/blenderbim/blenderbim/bim/module/covering/prop.py +++ b/src/blenderbim/blenderbim/bim/module/covering/prop.py @@ -18,7 +18,8 @@ import bpy from blenderbim.bim.prop import StrProperty, Attribute -#from blenderbim.bim.module.spatial.data import SpatialData + +# from blenderbim.bim.module.spatial.data import SpatialData from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -30,10 +31,13 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -#import blenderbim.tool as tool -#import blenderbim.core.geometry -#import ifcopenshell + +# import blenderbim.tool as tool +# import blenderbim.core.geometry +# import ifcopenshell class BIMCoveringProperties(PropertyGroup): - ceiling_height: bpy.props.FloatProperty(name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height") + ceiling_height: bpy.props.FloatProperty( + name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height" + ) diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py index 18fbfe9f6a..00405787ec 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -89,7 +89,7 @@ class BIM_PT_ifccsv(Panel): row = layout.row(align=True) op = row.operator("bim.search", text="Select", icon="VIEWZOOM") - op.property_group = "CsvProperties" + op.property_group = "CsvProperties" layout.separator() row = layout.row(align=True) @@ -128,6 +128,3 @@ class BIM_PT_ifccsv(Panel): row = layout.row(align=True) row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper()) row.operator("bim.import_ifccsv", icon="IMPORT") - - - diff --git a/src/blenderbim/blenderbim/bim/module/debug/ui.py b/src/blenderbim/blenderbim/bim/module/debug/ui.py index 88e9011f86..eea6d01c47 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/ui.py +++ b/src/blenderbim/blenderbim/bim/module/debug/ui.py @@ -90,9 +90,9 @@ class BIM_PT_debug(Panel): row.operator("bim.select_high_polygon_meshes").threshold = context.scene.BIMDebugProperties.number_of_polygons row.prop(props, "number_of_polygons", text="") row = layout.split(factor=0.7, align=True) - row.operator( - "bim.select_highest_polygon_meshes" - ).percentile = context.scene.BIMDebugProperties.percentile_of_polygons + row.operator("bim.select_highest_polygon_meshes").percentile = ( + context.scene.BIMDebugProperties.percentile_of_polygons + ) row.prop(props, "percentile_of_polygons", text="") row = layout.split(factor=0.5, align=True) @@ -140,9 +140,9 @@ class BIM_PT_debug(Panel): op = row.operator("bim.print_object_placement", icon="OBJECT_ORIGIN", text="") op.step_id = attribute.int_value if attribute.int_value: - row.operator( - "bim.inspect_from_step_id", icon="DISCLOSURE_TRI_RIGHT", text="" - ).step_id = attribute.int_value + row.operator("bim.inspect_from_step_id", icon="DISCLOSURE_TRI_RIGHT", text="").step_id = ( + attribute.int_value + ) if props.inverse_attributes: layout.label(text="Inverse attributes:") @@ -152,9 +152,9 @@ class BIM_PT_debug(Panel): row.prop(attribute, "name", text="") row.prop(attribute, "string_value", text="") if attribute.int_value: - row.operator( - "bim.inspect_from_step_id", icon="DISCLOSURE_TRI_RIGHT", text="" - ).step_id = attribute.int_value + row.operator("bim.inspect_from_step_id", icon="DISCLOSURE_TRI_RIGHT", text="").step_id = ( + attribute.int_value + ) if props.inverse_references: layout.label(text="Inverse references:") @@ -163,6 +163,6 @@ class BIM_PT_debug(Panel): row = layout.row(align=True) row.prop(attribute, "string_value", text="") if attribute.int_value: - row.operator( - "bim.inspect_from_step_id", icon="DISCLOSURE_TRI_RIGHT", text="" - ).step_id = attribute.int_value + row.operator("bim.inspect_from_step_id", icon="DISCLOSURE_TRI_RIGHT", text="").step_id = ( + attribute.int_value + ) diff --git a/src/blenderbim/blenderbim/bim/module/demo/ui.py b/src/blenderbim/blenderbim/bim/module/demo/ui.py index f00d4ae05b..d3709d61c5 100644 --- a/src/blenderbim/blenderbim/bim/module/demo/ui.py +++ b/src/blenderbim/blenderbim/bim/module/demo/ui.py @@ -29,6 +29,7 @@ import bpy from blenderbim.bim.module.demo.data import DemoData + # Every panel in the interface correlates to one of these classes. If you enable # the "Developer Extras" option in Blender, then you can actually right click on # any panel in Blender, and click "Edit Source". This will bring you to a ui.py diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index f5cd213081..c3e85ffe92 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -159,7 +159,7 @@ class BaseDecorator: objecttype = "NOTDEFINED" def __init__(self): - self.font_id = 0 # 0 is the default font + self.font_id = 0 # 0 is the default font # POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") @@ -2020,7 +2020,6 @@ class DecorationsHandler: for decorator in self.decorators.values(): decorator.font_id = font_id - def get_objects_and_decorators(self, collection): # TODO: do it in data instead of the handler for performance? results = [] diff --git a/src/blenderbim/blenderbim/bim/module/drawing/handler.py b/src/blenderbim/blenderbim/bim/module/drawing/handler.py index 9e45415d8f..136702dae1 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/handler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/handler.py @@ -41,9 +41,8 @@ def set_active_camera_resolution(scene): props = scene.camera.data.BIMCameraProperties ortho_scale = max((props.width, props.height)) aspect_ratio = props.width / props.height - if ( - (scene.camera.data.ortho_scale != ortho_scale) - or (scene.render.resolution_x / scene.render.resolution_y != aspect_ratio) + if (scene.camera.data.ortho_scale != ortho_scale) or ( + scene.render.resolution_x / scene.render.resolution_y != aspect_ratio ): scene.camera.data.ortho_scale = ortho_scale diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py index ba2405b23e..86b0293852 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py @@ -127,7 +127,7 @@ def format_distance( precision=None, decimal_places=None, suppress_zero_inches=False, - in_unit_length = False + in_unit_length=False, ): s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented) @@ -141,9 +141,9 @@ def format_distance( # Imperial Formatting if unit_system == "IMPERIAL": if in_unit_length: - if unit_length == 'INCHES': + if unit_length == "INCHES": toInches = 1 - if unit_length == 'FEET': + if unit_length == "FEET": toInches = 11.9999 else: toInches = 39.3700787401574887 @@ -222,11 +222,11 @@ def format_distance( # METRIC FORMATTING elif unit_system == "METRIC": if in_unit_length: - if unit_length == 'CENTIMETERS': - value = value/100 - if unit_length == 'MILLIMETERS': - value = value/1000 - + if unit_length == "CENTIMETERS": + value = value / 100 + if unit_length == "MILLIMETERS": + value = value / 1000 + if precision: value = precision * round(float(value) / precision) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 80558d6491..685a7b7603 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -1472,12 +1472,9 @@ class ActivateModel(bpy.types.Operator): CutDecorator.uninstall() - # save current visibility statuses for Views and Types collections + # save current visibility statuses visibility_status: dict[bpy.types.Object, bool] = {} - for col in bpy.data.collections["Views"].children: - for obj in col.objects: - visibility_status[obj] = obj.hide_get() - for obj in bpy.data.collections["Types"].objects: + for obj in bpy.data.objects: visibility_status[obj] = obj.hide_get() if not bpy.app.background: @@ -1508,6 +1505,7 @@ class ActivateModel(bpy.types.Operator): obj.hide_set(hide_status) tool.Blender.update_viewport() + blenderbim.bim.handler.refresh_ui_data() return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py index 72da3bbfa7..26c4c41bf6 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py @@ -560,7 +560,7 @@ class Scheduler: text_tag = self.svg.text("", **(text_params | {"font-size": "0"} | box_alignment_params)) - # TODO: Should be done without using magic number for self.font_width + # TODO: Should be done without using magic number for self.font_width if wrap_text: wrapped_lines = [] for line in text_lines: diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 2b47df9431..2118641839 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -123,18 +123,18 @@ class BIM_PT_element_filters(Panel): self.layout, props.include_filter_groups, ElementFiltersData, "drawing_include" ) row = self.layout.row(align=True) - row.operator( - "bim.edit_element_filter", icon="CHECKMARK", text="Save Include Filter" - ).filter_mode = "INCLUDE" + row.operator("bim.edit_element_filter", icon="CHECKMARK", text="Save Include Filter").filter_mode = ( + "INCLUDE" + ) row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE" elif props.filter_mode == "EXCLUDE": blenderbim.bim.helper.draw_filter( self.layout, props.exclude_filter_groups, ElementFiltersData, "drawing_exclude" ) row = self.layout.row(align=True) - row.operator( - "bim.edit_element_filter", icon="CHECKMARK", text="Save Exclude Filter" - ).filter_mode = "EXCLUDE" + row.operator("bim.edit_element_filter", icon="CHECKMARK", text="Save Exclude Filter").filter_mode = ( + "EXCLUDE" + ) row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE" else: row = self.layout.row(align=True) @@ -260,9 +260,9 @@ class BIM_PT_drawings(Panel): row2 = col.row(align=True) row2.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id - row2.operator( - "bim.duplicate_drawing", icon="COPYDOWN", text="" - ).drawing = active_drawing.ifc_definition_id + row2.operator("bim.duplicate_drawing", icon="COPYDOWN", text="").drawing = ( + active_drawing.ifc_definition_id + ) col = row.column() col.alignment = "RIGHT" @@ -339,9 +339,9 @@ class BIM_PT_schedules(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" row.operator("bim.open_schedule", icon="URL", text="").schedule = active_schedule.ifc_definition_id - row.operator( - "bim.build_schedule", icon="LINENUMBERS_ON", text="" - ).schedule = active_schedule.ifc_definition_id + row.operator("bim.build_schedule", icon="LINENUMBERS_ON", text="").schedule = ( + active_schedule.ifc_definition_id + ) row.operator("bim.remove_schedule", icon="X", text="").schedule = active_schedule.ifc_definition_id self.layout.template_list( @@ -612,13 +612,13 @@ class BIM_UL_sheets(bpy.types.UIList): row = layout.row(align=True) if item.is_sheet: if item.is_expanded: - row.operator( - "bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" - ).sheet = item.ifc_definition_id + row.operator("bim.contract_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").sheet = ( + item.ifc_definition_id + ) else: - row.operator( - "bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" - ).sheet = item.ifc_definition_id + row.operator("bim.expand_sheet", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").sheet = ( + item.ifc_definition_id + ) row.label(text=f"{item.identification} - {item.name}") else: diff --git a/src/blenderbim/blenderbim/bim/module/fm/__init__.py b/src/blenderbim/blenderbim/bim/module/fm/__init__.py index bae513a98f..0d8081b2a9 100644 --- a/src/blenderbim/blenderbim/bim/module/fm/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/fm/__init__.py @@ -22,7 +22,6 @@ from . import ui, prop, operator classes = ( operator.ExecuteIfcFM, operator.ExecuteIfcFMFederate, - operator.SelectFMIfcFile, operator.SelectFMSpreadsheetFiles, prop.BIMFMProperties, ui.BIM_PT_fm, diff --git a/src/blenderbim/blenderbim/bim/module/fm/operator.py b/src/blenderbim/blenderbim/bim/module/fm/operator.py index 77dd0ac72d..0c224b08e0 100644 --- a/src/blenderbim/blenderbim/bim/module/fm/operator.py +++ b/src/blenderbim/blenderbim/bim/module/fm/operator.py @@ -24,21 +24,8 @@ import logging import tempfile import ifcopenshell import blenderbim.tool as tool -from blenderbim.bim.operator import MultipleFileSelector -class SelectFMIfcFile(MultipleFileSelector): - bl_idname = "bim.select_fm_ifc_file" - bl_label = "Select FM IFC File" - filename_ext = ".ifc" - filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - - def execute(self, context): - props = context.scene.BIMFMProperties - - self.update_props(props, "ifc_file", props.ifc_files) - return {"FINISHED"} - class ExecuteIfcFM(bpy.types.Operator): bl_idname = "bim.execute_ifcfm" bl_label = "Execute IfcFM" @@ -49,7 +36,7 @@ class ExecuteIfcFM(bpy.types.Operator): @classmethod def poll(cls, context): props = context.scene.BIMFMProperties - return props.should_load_from_memory or props.ifc_file + return props.should_load_from_memory or props.ifc_files.single_file def invoke(self, context, event): props = context.scene.BIMFMProperties @@ -62,14 +49,13 @@ class ExecuteIfcFM(bpy.types.Operator): props = context.scene.BIMFMProperties ifc_file = tool.Ifc.get() filepaths = [] - if not (ifc_file and props.should_load_from_memory): - if len(props.ifc_files): - ifc_files = [ifcopenshell.open(f.name) for f in props.ifc_files] - filepaths = [f.name for f in props.ifc_files] - else: - ifc_files = [ifcopenshell.open(props.ifc_file)] - else: + if ifc_file and props.should_load_from_memory: ifc_files = [ifc_file] + else: + ifc_files = [ifcopenshell.open(f.name) for f in props.ifc_files.file_list] + + if len(ifc_files) > 1: + filepaths = [f.name for f in props.ifc_files.file_list] for i, ifc_file in enumerate(ifc_files): if filepaths: diff --git a/src/blenderbim/blenderbim/bim/module/fm/prop.py b/src/blenderbim/blenderbim/bim/module/fm/prop.py index f4a1868645..f69c6db1f9 100644 --- a/src/blenderbim/blenderbim/bim/module/fm/prop.py +++ b/src/blenderbim/blenderbim/bim/module/fm/prop.py @@ -18,7 +18,7 @@ import bpy from blenderbim.bim.module.fm.data import FMData -from blenderbim.bim.prop import StrProperty +from blenderbim.bim.prop import MultipleFileSelect, StrProperty from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -39,11 +39,10 @@ def get_engine(self, context): class BIMFMProperties(PropertyGroup): - ifc_file: StringProperty(default="", name="IFC File") - ifc_files: CollectionProperty(name="IFC Files", type=StrProperty) + ifc_files: PointerProperty(type=MultipleFileSelect) spreadsheet_files: CollectionProperty(name="Spreadsheets", type=StrProperty) should_load_from_memory: BoolProperty(default=False, name="Load from Memory") - engine: EnumProperty(items=get_engine, name="Engine") + engine: EnumProperty(items=get_engine, name="Engine", options=set()) format: EnumProperty( items=[ ("csv", "csv", ""), @@ -52,4 +51,5 @@ class BIMFMProperties(PropertyGroup): ], name="Format", default="ods", + options=set(), ) diff --git a/src/blenderbim/blenderbim/bim/module/fm/ui.py b/src/blenderbim/blenderbim/bim/module/fm/ui.py index ca5efe36f1..1456c89cb9 100644 --- a/src/blenderbim/blenderbim/bim/module/fm/ui.py +++ b/src/blenderbim/blenderbim/bim/module/fm/ui.py @@ -45,12 +45,8 @@ class BIM_PT_fm(Panel): row.prop(props, "should_load_from_memory") if not IfcStore.get_file() or not props.should_load_from_memory: - row = layout.row(align=True) - if len(props.ifc_files) > 1: - row.label(text=f"{len(props.ifc_files)} Files Selected") - else: - row.prop(props, "ifc_file") - row.operator("bim.select_fm_ifc_file", icon="FILE_FOLDER", text="") + row = layout.row() + props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)") row = layout.row() row.prop(props, "engine") diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py index c084a2425e..9abf87358b 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py @@ -85,15 +85,15 @@ class BIM_MT_object_set_origin(Menu): bl_label = "IFC Set Origin" def draw(self, context): - self.layout.operator( - "bim.override_origin_set", icon="PLUGIN", text="IFC Geometry to Origin" - ).origin_type = "GEOMETRY_ORIGIN" - self.layout.operator( - "bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Geometry" - ).origin_type = "ORIGIN_GEOMETRY" - self.layout.operator( - "bim.override_origin_set", icon="PLUGIN", text="IFC Origin to 3D Cursor" - ).origin_type = "ORIGIN_CURSOR" + self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Geometry to Origin").origin_type = ( + "GEOMETRY_ORIGIN" + ) + self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Geometry").origin_type = ( + "ORIGIN_GEOMETRY" + ) + self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to 3D Cursor").origin_type = ( + "ORIGIN_CURSOR" + ) self.layout.operator( "bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Surface)" ).origin_type = "ORIGIN_CENTER_OF_MASS" diff --git a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py index 303fd50ccc..fe5ff8d028 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py @@ -22,8 +22,6 @@ from . import ui, prop, operator classes = ( operator.AddGeoreferencing, operator.ConvertAngleToCoordinates, - operator.ConvertGlobalToLocal, - operator.ConvertLocalToGlobal, operator.DisableEditingGeoreferencing, operator.DisableEditingTrueNorth, operator.DisableEditingWCS, diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py index ce59bb231a..0ca923ea9c 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/data.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py @@ -21,7 +21,7 @@ import bpy import numpy as np import blenderbim.tool as tool import ifcopenshell.util.geolocation -from mathutils import Matrix +from mathutils import Matrix, Vector from ifcopenshell.util.doc import get_entity_doc @@ -44,6 +44,7 @@ class GeoreferenceData: cls.data["local_unit_symbol"] = cls.local_unit_symbol() cls.data["map_unit_symbol"] = cls.map_unit_symbol() cls.data["world_coordinate_system"] = cls.world_coordinate_system() + cls.data["local_origin"] = cls.local_origin() cls.is_loaded = True @classmethod @@ -162,8 +163,40 @@ class GeoreferenceData: result["has_transformation"] = True result["rotation"] = str(round(ifcopenshell.util.geolocation.yaxis2angle(*wcs[:, 1][:2]), 3)) result["x"], result["y"], result["z"] = wcs[:, 3][:3] + + props = bpy.context.scene.BIMGeoreferenceProperties + if props.has_blender_offset: + blender_xyz = ifcopenshell.util.geolocation.enh2xyz( + result["x"], + result["y"], + result["z"], + float(props.blender_offset_x), + float(props.blender_offset_y), + float(props.blender_offset_z), + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + ) + else: + blender_xyz = wcs[:, 3][:3] + + result["blender_x"], result["blender_y"], result["blender_z"] = blender_xyz + result["blender_location"] = Vector([co * unit_scale for co in blender_xyz]) + wcs[0][3] *= unit_scale wcs[1][3] *= unit_scale wcs[2][3] *= unit_scale result["matrix"] = Matrix(wcs) return result + + @classmethod + def local_origin(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + if not props.has_blender_offset: + return + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + x = float(props.blender_offset_x) * -1 + y = float(props.blender_offset_y) * -1 + z = float(props.blender_offset_z) * -1 + enh = ifcopenshell.util.geolocation.auto_xyz2enh(tool.Ifc.get(), 0, 0, 0) + xyz_si = Vector([x * unit_scale, y * unit_scale, z * unit_scale]) + return {"x": x, "y": y, "z": z, "enh": enh, "location": xyz_si} diff --git a/src/blenderbim/blenderbim/bim/module/georeference/decorator.py b/src/blenderbim/blenderbim/bim/module/georeference/decorator.py index 7a8fb64f56..581ad38958 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/decorator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/decorator.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import bpy import blf import gpu import bmesh @@ -61,10 +62,10 @@ class GeoreferenceDecorator: self.calculate_angles(context) props = context.scene.BIMGeoreferenceProperties e, n, h = [round(float(o), 7) for o in props.model_origin.split(",")] - - wcs_matrix = None - if GeoreferenceData.data["world_coordinate_system"]["has_transformation"]: - wcs_matrix = GeoreferenceData.data["world_coordinate_system"]["matrix"] + if (operation := GeoreferenceData.data["coordinate_operation"]) and (scale := operation.get("Scale", None)): + e *= scale + n *= scale + h *= scale self.addon_prefs = tool.Blender.get_addon_preferences() @@ -75,10 +76,14 @@ class GeoreferenceDecorator: blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) - text = "Blender Coordinates X: 0, Y: 0, Z: 0" + if props.has_blender_offset: + text = "Blender Origin" + text += f"\nBlender Coordinates ({GeoreferenceData.data['local_unit_symbol']}) X: 0, Y: 0, Z: 0" + else: + text = "IFC Local Origin" text += f"\nLocal Coordinates ({GeoreferenceData.data['local_unit_symbol']}) X: {props.blender_offset_x}, Y: {props.blender_offset_y}, Z: {props.blender_offset_z}" if GeoreferenceData.data["projected_crs"]: - text += f"\nMap Coordinates ({GeoreferenceData.data['local_unit_symbol']}) E: {e}, N: {n}, H: {h}" + text += f"\nMap Coordinates ({GeoreferenceData.data['map_unit_symbol']}) E: {e}, N: {n}, H: {h}" self.draw_text_at_position(context, text, Vector((0, -0.1, 0))) angle = Matrix.Rotation(radians(self.pn_angle), 4, "Z") @@ -111,25 +116,42 @@ class GeoreferenceDecorator: arc_mid = angle_half @ arc_start self.draw_text_at_position(context, f"{self.tn_angle}deg", arc_mid) - if wcs_matrix: - x, y, z = wcs_matrix.translation + if (wcs := GeoreferenceData.data["world_coordinate_system"]) and wcs["has_transformation"]: + text = "WCS" + if props.has_blender_offset: + text += f"\nBlender Coordinates ({GeoreferenceData.data['local_unit_symbol']}) X: {wcs['blender_x']}, Y: {wcs['blender_y']}, Z: {wcs['blender_z']}" + text += f"\nLocal Coordinates ({GeoreferenceData.data['local_unit_symbol']}) X: {wcs['x']}, Y: {wcs['y']}, Z: {wcs['z']}" if operation := GeoreferenceData.data["coordinate_operation"]: e = operation.get("Eastings") n = operation.get("Northings") h = operation.get("OrthogonalHeight") - text = f"WCS\n X: {x}, Y: {y}, Z: {z}\nE: {e}, N: {n}, H: {h}" - else: - text = f"WCS\n X: {x}, Y: {y}, Z: {z}" + text += f"\nMap Coordinates ({GeoreferenceData.data['map_unit_symbol']}) E: {e}, N: {n}, H: {h}" - if wcs_matrix.translation.length < 1000: - position = wcs_matrix.translation.copy() + if wcs["blender_location"].length < 1000: + position = wcs["blender_location"].copy() else: - position = wcs_matrix.translation.normalized() * 3 + position = wcs["blender_location"].normalized() * 3 text += "\n(Warning: Actual XYZ Not Shown)" position -= Vector((0, 0.1, 0)) self.draw_text_at_position(context, text, position) + if props.has_blender_offset: + text = "IFC Local Origin" + x = GeoreferenceData.data["local_origin"]["x"] + y = GeoreferenceData.data["local_origin"]["y"] + z = GeoreferenceData.data["local_origin"]["z"] + location = GeoreferenceData.data["local_origin"]["location"] + text += f"\nBlender Coordinates ({GeoreferenceData.data['local_unit_symbol']}) X: {x}, Y: {y}, Z: {z}" + text += f"\nLocal Coordinates ({GeoreferenceData.data['local_unit_symbol']}) X: 0, Y: 0, Z: 0" + if GeoreferenceData.data["projected_crs"]: + e, n, h = GeoreferenceData.data["local_origin"]["enh"] + text += f"\nMap Coordinates ({GeoreferenceData.data['map_unit_symbol']}) E: {e}, N: {n}, H: {h}" + if location.length > 1000: + location = location.normalized() * 3 + text += "\n(Warning: Actual XYZ Not Shown)" + self.draw_text_at_position(context, text, location) + def draw_text_at_position(self, context, text, position): coords_2d = location_3d_to_region_2d(context.region, context.region_data, position) if not coords_2d: @@ -144,10 +166,6 @@ class GeoreferenceDecorator: def draw_geometry(self, context): self.calculate_angles(context) - wcs_matrix = None - if GeoreferenceData.data["world_coordinate_system"]["has_transformation"]: - wcs_matrix = GeoreferenceData.data["world_coordinate_system"]["matrix"] - self.addon_prefs = tool.Blender.get_addon_preferences() decorator_color = self.addon_prefs.decorations_colour decorator_color_special = self.addon_prefs.decorator_color_special @@ -263,24 +281,50 @@ class GeoreferenceDecorator: verts, edges = arc_segments self.draw_batch("LINES", verts, decorator_color_special, edges) - if wcs_matrix: - if wcs_matrix.translation.length < 1000: - verts = [Vector((0, 0, 0)), wcs_matrix.translation] + if (wcs := GeoreferenceData.data["world_coordinate_system"]) and wcs["has_transformation"]: + if wcs["blender_location"].length < 1000: + verts = [Vector((0, 0, 0)), wcs["blender_location"]] edges = [[0, 1]] self.draw_batch("LINES", verts, decorator_color_special, edges) self.draw_batch("POINTS", verts[1:], decorator_color_special) else: + location = wcs["blender_location"].normalized() edges = [[0, 1]] - verts = [Vector((0, 0, 0)), wcs_matrix.translation.normalized() * 3] + verts = [Vector((0, 0, 0)), location * 3] self.draw_batch("LINES", verts, decorator_color_special, edges) - verts = [wcs_matrix.translation.normalized() * 3, wcs_matrix.translation.normalized() * 3.1] - self.draw_batch("LINES", verts, decorator_color_error, edges) - verts = [wcs_matrix.translation.normalized() * 3.12, wcs_matrix.translation.normalized() * 3.2] - self.draw_batch("LINES", verts, decorator_color_error, edges) - verts = [wcs_matrix.translation.normalized() * 3.22, wcs_matrix.translation.normalized() * 3.3] - self.draw_batch("LINES", verts, decorator_color_error, edges) - verts = [wcs_matrix.translation.normalized() * 3.32, wcs_matrix.translation.normalized() * 3.4] - self.draw_batch("LINES", verts, decorator_color_error, edges) + self.draw_dashed_line(location * 3, location * 6, decorator_color_error) + + props = context.scene.BIMGeoreferenceProperties + if props.has_blender_offset: + location = GeoreferenceData.data["local_origin"]["location"] + if location.length < 1000: + verts = [Vector((0, 0, 0)), location] + edges = [[0, 1]] + self.draw_batch("LINES", verts, decorator_color_special, edges) + self.draw_batch("POINTS", verts[1:], decorator_color_special) + else: + location = location.normalized() + edges = [[0, 1]] + verts = [Vector((0, 0, 0)), location * 3] + self.draw_batch("LINES", verts, decorator_color_special, edges) + self.draw_dashed_line(location * 3, location * 6, decorator_color_error) + + def draw_dashed_line(self, start, end, colour): + direction = (end - start).normalized() + distance = (end - start).length + current_distance = Vector((0, 0, 0)) + gap = 0.05 * direction + dash = 0.1 * direction + points = [start] + while current_distance.length < distance: + points.append(start + current_distance + dash) + points.append(start + current_distance + dash + gap) + current_distance += gap + dash + points.pop() + + edges = [[i, i + 1] for i in range(0, len(points), 2)] + verts = points + self.draw_batch("LINES", verts, colour, edges) def calculate_angles(self, context): self.pn_angle = 0.0 diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index e86e877f9c..23591ee305 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -87,37 +87,6 @@ class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): core.get_cursor_location(tool.Georeference) -class ConvertLocalToGlobal(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.convert_local_to_global" - bl_label = "Convert Local To Global" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Convert local coordinate to global coordinate" - - @classmethod - def poll(cls, context): - file = tool.Ifc.get() - props = context.scene.BIMGeoreferenceProperties - return file and props.local_coordinates.count(",") == 2 - - def _execute(self, context): - core.convert_local_to_global(tool.Georeference) - - -class ConvertGlobalToLocal(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.convert_global_to_local" - bl_label = "Convert Global To Local" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Convert global coordinate to local coordinate" - - @classmethod - def poll(cls, context): - file = tool.Ifc.get() - props = context.scene.BIMGeoreferenceProperties - return file and file.by_type("IfcUnitAssignment") and props.local_coordinates.count(",") == 2 - - def _execute(self, context): - core.convert_global_to_local(tool.Georeference) - class ConvertAngleToCoordinates(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.convert_angle_to_coord" bl_label = "Convert Angle To Y Axis" diff --git a/src/blenderbim/blenderbim/bim/module/georeference/prop.py b/src/blenderbim/blenderbim/bim/module/georeference/prop.py index dfa947e03c..28144ce578 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/prop.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/prop.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell.util.geolocation +import blenderbim.tool as tool from blenderbim.bim.prop import Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -101,6 +102,74 @@ def update_should_visualise(self, context): GeoreferenceDecorator.uninstall() +def update_blender_coordinates(self, context): + props = bpy.context.scene.BIMGeoreferenceProperties + if props.is_updating_coordinates: + return + props.is_updating_coordinates = True + blender_coordinates = tool.Georeference.get_coordinates("blender") + local_coordinates = ifcopenshell.util.geolocation.xyz2enh( + *blender_coordinates, + float(props.blender_offset_x), + float(props.blender_offset_y), + float(props.blender_offset_z), + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + ) + tool.Georeference.set_coordinates("local", local_coordinates) + tool.Georeference.set_coordinates( + "map", ifcopenshell.util.geolocation.auto_xyz2enh(tool.Ifc.get(), *local_coordinates) + ) + props.is_updating_coordinates = False + + +def update_local_coordinates(self, context): + props = bpy.context.scene.BIMGeoreferenceProperties + if props.is_updating_coordinates: + return + props.is_updating_coordinates = True + local_coordinates = tool.Georeference.get_coordinates("local") + tool.Georeference.set_coordinates( + "map", ifcopenshell.util.geolocation.auto_xyz2enh(tool.Ifc.get(), *local_coordinates) + ) + if props.has_blender_offset: + tool.Georeference.set_coordinates( + "blender", + ifcopenshell.util.geolocation.enh2xyz( + *local_coordinates, + float(props.blender_offset_x), + float(props.blender_offset_y), + float(props.blender_offset_z), + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + ), + ) + props.is_updating_coordinates = False + + +def update_map_coordinates(self, context): + props = bpy.context.scene.BIMGeoreferenceProperties + if props.is_updating_coordinates: + return + props.is_updating_coordinates = True + map_coordinates = tool.Georeference.get_coordinates("map") + local_coordinates = ifcopenshell.util.geolocation.auto_enh2xyz(tool.Ifc.get(), *map_coordinates) + tool.Georeference.set_coordinates("local", local_coordinates) + if props.has_blender_offset: + tool.Georeference.set_coordinates( + "blender", + ifcopenshell.util.geolocation.enh2xyz( + *local_coordinates, + float(props.blender_offset_x), + float(props.blender_offset_y), + float(props.blender_offset_z), + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + ), + ) + props.is_updating_coordinates = False + + class BIMGeoreferenceProperties(PropertyGroup): coordinate_operation_class: bpy.props.EnumProperty( items=get_coordinate_operation_class, name="Coordinate Operation Class" @@ -111,11 +180,24 @@ class BIMGeoreferenceProperties(PropertyGroup): is_editing_true_north: BoolProperty(name="Is Editing True North") coordinate_operation: CollectionProperty(name="Coordinate Operation", type=Attribute) projected_crs: CollectionProperty(name="Projected CRS", type=Attribute) + is_updating_coordinates: BoolProperty(name="Is Updating Coordinates", default=False) + blender_coordinates: StringProperty( + name="Blender Coordinates", + description='Formatted "x,y,z" (without quotes)', + default="0,0,0", + update=update_blender_coordinates, + ) local_coordinates: StringProperty( - name="Local Coordinates", description='Formatted "x,y,z" (without quotes)', default="0,0,0" + name="Local Coordinates", + description='Formatted "x,y,z" (without quotes)', + default="0,0,0", + update=update_local_coordinates, ) map_coordinates: StringProperty( - name="Map Coordinates", description='Formatted "x,y,z" (without quotes)', default="0,0,0" + name="Map Coordinates", + description='Formatted "x,y,z" (without quotes)', + default="0,0,0", + update=update_map_coordinates, ) should_visualise: BoolProperty( name="Should Visualise", diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index dcd2d391b4..3c0f8210fc 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -288,12 +288,15 @@ class BIM_PT_gis_calculator(Panel): props = context.scene.BIMGeoreferenceProperties - row = self.layout.row(align=True) - row.prop(props, "local_coordinates", text=f"Local ({GeoreferenceData.data['local_unit_symbol']})") - row.operator("bim.get_cursor_location", text="", icon="TRACKER") - row = self.layout.row() - row.prop(props, "map_coordinates", text=f"Map ({GeoreferenceData.data['map_unit_symbol']})") + if props.has_blender_offset: + row = self.layout.row(align=True) + row.prop(props, "blender_coordinates", text=f"Blender ({GeoreferenceData.data['local_unit_symbol']})") + row.operator("bim.get_cursor_location", text="", icon="TRACKER") row = self.layout.row(align=True) - row.operator("bim.convert_local_to_global", text="Local to Global") - row.operator("bim.convert_global_to_local", text="Global to Local") + row.prop(props, "local_coordinates", text=f"Local ({GeoreferenceData.data['local_unit_symbol']})") + if not props.has_blender_offset: + row.operator("bim.get_cursor_location", text="", icon="TRACKER") + + row = self.layout.row() + row.prop(props, "map_coordinates", text=f"Map ({GeoreferenceData.data['map_unit_symbol']})") diff --git a/src/blenderbim/blenderbim/bim/module/library/data.py b/src/blenderbim/blenderbim/bim/module/library/data.py index b223609961..fbe7a9d5d9 100644 --- a/src/blenderbim/blenderbim/bim/module/library/data.py +++ b/src/blenderbim/blenderbim/bim/module/library/data.py @@ -101,9 +101,9 @@ class LibraryReferencesData: results.append( { "id": library.id(), - "identification": library.ItemReference - if tool.Ifc.get_schema() == "IFC2X3" - else library.Identification, + "identification": ( + library.ItemReference if tool.Ifc.get_schema() == "IFC2X3" else library.Identification + ), "name": library.Name or "Unnamed", } ) diff --git a/src/blenderbim/blenderbim/bim/module/light/__init__.py b/src/blenderbim/blenderbim/bim/module/light/__init__.py index febd34c37e..664eb27f41 100644 --- a/src/blenderbim/blenderbim/bim/module/light/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/light/__init__.py @@ -34,10 +34,12 @@ classes = ( ui.BIM_PT_solar, ) + def register(): bpy.types.Scene.radiance_exporter_properties = bpy.props.PointerProperty(type=prop.RadianceExporterProperties) bpy.types.Scene.BIMSolarProperties = bpy.props.PointerProperty(type=prop.BIMSolarProperties) + def unregister(): del bpy.types.Scene.radiance_exporter_properties del bpy.types.Scene.BIMSolarProperties diff --git a/src/blenderbim/blenderbim/bim/module/light/decorator.py b/src/blenderbim/blenderbim/bim/module/light/decorator.py index 2637967494..e32a68d099 100644 --- a/src/blenderbim/blenderbim/bim/module/light/decorator.py +++ b/src/blenderbim/blenderbim/bim/module/light/decorator.py @@ -87,7 +87,6 @@ class SolarDecorator: blf.position(self.font_id, co[0], co[1], 0) blf.draw(self.font_id, line) - def draw_geometry(self, context): self.addon_prefs = tool.Blender.get_addon_preferences() decorator_color_special = self.addon_prefs.decorator_color_special @@ -153,8 +152,7 @@ class SolarDecorator: analemma_verts = sun_position.sun_calc.calc_analemma(context, h) coords.extend([origin @ v for v in analemma_verts]) for i in range(len(analemma_verts) - 1): - indices.append((coord_offset + i, - coord_offset + i+1)) + indices.append((coord_offset + i, coord_offset + i + 1)) coord_offset += len(analemma_verts) self.draw_batch("LINES", coords, decorator_color_special, indices) diff --git a/src/blenderbim/blenderbim/bim/module/light/operator.py b/src/blenderbim/blenderbim/bim/module/light/operator.py index bf250b92d7..c0afd1fff2 100644 --- a/src/blenderbim/blenderbim/bim/module/light/operator.py +++ b/src/blenderbim/blenderbim/bim/module/light/operator.py @@ -37,12 +37,13 @@ from blenderbim.bim.module.light.data import SolarData ifc_materials = [] + class ExportOBJ(bpy.types.Operator): """Exports the IFC File to OBJ""" + bl_idname = "export_scene.radiance" bl_label = "Export" bl_description = "Export the IFC to OBJ" - def execute(self, context): @@ -52,12 +53,11 @@ class ExportOBJ(bpy.types.Operator): context.scene.radiance_exporter_properties.is_exporting = True - # Conversion from IFC to OBJ # Settings for obj settings = ifcopenshell.geom.settings() serializer_settings = ifcopenshell.geom.serializer_settings() - + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS) settings.set("apply-default-materials", True) serializer_settings.set("use-element-guids", True) @@ -65,17 +65,17 @@ class ExportOBJ(bpy.types.Operator): if should_load_from_memory: ifc_file = tool.Ifc.get() - + else: ifc_file_path = context.scene.radiance_exporter_properties.ifc_file ifc_file = ifcopenshell.open(ifc_file_path) - + # for material in ifc_file.by_type("IfcMaterial"): # self.report({'INFO'}, f"Material: {material.Name}, ID: {material.id()}") obj_file_path = os.path.join(output_dir, "model.obj") mtl_file_path = os.path.join(output_dir, "model.mtl") - + serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings) serialiser.setFile(ifc_file) serialiser.setUnitNameAndMagnitude("METER", 1.0) @@ -84,10 +84,10 @@ class ExportOBJ(bpy.types.Operator): if ifc_file.schema in ("IFC2X3", "IFC4"): elements = ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcProxy") else: - elements = ifc_file.by_type("IfcElement") + elements = ifc_file.by_type("IfcElement") + + elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")] - elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")] - iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=elements) if iterator.initialize(): while True: @@ -102,30 +102,31 @@ class ExportOBJ(bpy.types.Operator): # print(material, dir(material)) # print(material.name) ifc_materials.append(material.name) - + serialiser.write(shape) if not iterator.next(): break - + serialiser.finalize() context.scene.radiance_exporter_properties.is_exporting = False - self.report({'INFO'}, "Exported OBJ file to: {}".format(obj_file_path)) + self.report({"INFO"}, "Exported OBJ file to: {}".format(obj_file_path)) - return {'FINISHED'} + return {"FINISHED"} class RadianceRender(bpy.types.Operator): """Radiance Rendering""" + bl_idname = "render_scene.radiance" bl_label = "Render" bl_description = "Renders the scene using Radiance" def execute(self, context): if pr is None: - self.report({'ERROR'}, "PyRadiance is not available. Cannot perform rendering.") - return {'CANCELLED'} - + self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.") + return {"CANCELLED"} + # Get the resolution from the user input props = context.scene.radiance_exporter_properties resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y @@ -138,31 +139,29 @@ class RadianceRender(bpy.types.Operator): obj_file_path = os.path.join(output_dir, "model.obj") - camera = self.get_active_camera(context) if camera is None: - self.report({'ERROR'}, "No active camera found in the scene. Please add a camera and set it as active.") - return {'CANCELLED'} + self.report({"ERROR"}, "No active camera found in the scene. Please add a camera and set it as active.") + return {"CANCELLED"} # Get camera position and direction camera_position, camera_direction = self.get_camera_data(camera) # Material processing # style = [] - + # with open(obj_file_path, "r") as obj_file: # for line in obj_file: # if line.startswith("usemtl"): # l = line.strip().split(" ") # style.append(l[1]) - # Check if json file is empty or not props = context.scene.radiance_exporter_properties if props.use_json_file: # Load data from JSON file - with open(props.json_file, 'r') as file: + with open(props.json_file, "r") as file: data = json.load(file) else: # Use in-UI material mappings @@ -170,7 +169,6 @@ class RadianceRender(bpy.types.Operator): print(data) - # Create materials.rad file materials_file = os.path.join(output_dir, "materials.rad") with open(materials_file, "w") as file: @@ -190,19 +188,19 @@ class RadianceRender(bpy.types.Operator): # print(i) # file.write("inherit alias " + i + " " + data.get(i, "white") + "\n") - self.report({'INFO'}, "Exported Materials Rad file to: {}".format(materials_file)) + self.report({"INFO"}, "Exported Materials Rad file to: {}".format(materials_file)) # Run obj2mesh rtm_file_path = os.path.join(output_dir, "model.rtm") - mesh_file_path = save_obj2mesh_output(obj_file_path, rtm_file_path, matfiles=[materials_file]) + mesh_file_path = save_obj2mesh_output(obj_file_path, rtm_file_path, matfiles=[materials_file]) # subprocess.run(["obj2mesh", "-a", materials_file, obj_file_path, rtm_file_path]) - self.report({'INFO'}, "obj2mesh output: {}".format(mesh_file_path)) + self.report({"INFO"}, "obj2mesh output: {}".format(mesh_file_path)) scene_file = os.path.join(output_dir, "scene.rad") with open(scene_file, "w") as file: # file.write("void mesh model\n1 " + rtm_file_path + "\n0\n0\n") file.write('void mesh model\n1 "' + rtm_file_path + '"\n0\n0\n') - self.report({'INFO'}, "Exported Scene file to: {}".format(scene_file)) + self.report({"INFO"}, "Exported Scene file to: {}".format(scene_file)) # Py Radiance Rendering code scene = pr.Scene("ascene") @@ -216,26 +214,31 @@ class RadianceRender(bpy.types.Operator): aview = pr.View(position=camera_position, direction=camera_direction) scene.add_view(aview) - image = pr.render(scene, ambbounce=1, resolution=(resolution_x, resolution_y), - quality=quality, detail=detail, variability=variability) - + image = pr.render( + scene, + ambbounce=1, + resolution=(resolution_x, resolution_y), + quality=quality, + detail=detail, + variability=variability, + ) + output_path = os.path.join(output_dir, f"{output_file_name}.{output_file_format.lower()}") - if output_file_format == 'HDR': + if output_file_format == "HDR": with open(output_path, "wb") as wtr: wtr.write(image) else: pass - - self.report({'INFO'}, "Radiance rendering completed. Output: {}".format(output_path)) - return {'FINISHED'} - + self.report({"INFO"}, "Radiance rendering completed. Output: {}".format(output_path)) + return {"FINISHED"} + def get_active_camera(self, context): if context.scene.camera: return context.scene.camera for obj in context.scene.objects: - if obj.type == 'CAMERA': + if obj.type == "CAMERA": return obj return None @@ -256,10 +259,11 @@ class RadianceRender(bpy.types.Operator): resolution_y = props.radiance_resolution_y return resolution_x, resolution_y + def save_obj2mesh_output(inp: Union[bytes, str, Path], output_file: str, **kwargs): output_bytes = pr.obj2mesh(inp, **kwargs) - with open(output_file, 'wb') as f: + with open(output_file, "wb") as f: f.write(output_bytes) return output_file @@ -333,6 +337,7 @@ class ViewFromSun(bpy.types.Operator): props.hour = props.hour # Just to refresh camera position return {"FINISHED"} + class RefreshIFCMaterials(bpy.types.Operator): bl_idname = "bim.refresh_ifc_materials" bl_label = "Refresh IFC Materials" @@ -341,7 +346,7 @@ class RefreshIFCMaterials(bpy.types.Operator): def execute(self, context): props = context.scene.radiance_exporter_properties ifc_file = tool.Ifc.get() if props.should_load_from_memory else ifcopenshell.open(props.ifc_file) - + for style in ifc_file.by_type("IfcSurfaceStyle"): for render_item in style.Styles: if render_item.is_a("IfcSurfaceStyleRendering"): @@ -349,5 +354,5 @@ class RefreshIFCMaterials(bpy.types.Operator): style_name = style.Name or f"Unnamed Style {render_item.id()}" if not props.get_material_mapping(style_name): props.add_material_mapping(style_id, style_name) - - return {'FINISHED'} + + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/light/prop.py b/src/blenderbim/blenderbim/bim/module/light/prop.py index 62d1abf24a..0029972e18 100644 --- a/src/blenderbim/blenderbim/bim/module/light/prop.py +++ b/src/blenderbim/blenderbim/bim/module/light/prop.py @@ -170,110 +170,88 @@ class RadianceExporterProperties(PropertyGroup): def get_mappings_dict(self): return {item["style_id"]: item["radiance_material"] for item in self.material_mappings} - + use_json_file: BoolProperty( name="Upload JSON", description="Toggle between uploading a JSON file and using in-UI material mapping", - default=False - ) - - is_exporting: bpy.props.BoolProperty( - name="Is Exporting", - description="Whether the OBJ export is in progress", - default=False + default=False, ) - material_mappings: bpy.props.CollectionProperty( - type= bpy.types.PropertyGroup, - name="Material Mappings" + is_exporting: bpy.props.BoolProperty( + name="Is Exporting", description="Whether the OBJ export is in progress", default=False ) + material_mappings: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup, name="Material Mappings") + should_load_from_memory: BoolProperty( name="Load from Memory", - default=False, + default=False, ) radiance_resolution_x: IntProperty( - name="X", - description="Horizontal resolution of the output image", - default=1920, - min=1 + name="X", description="Horizontal resolution of the output image", default=1920, min=1 ) radiance_resolution_y: IntProperty( - name="Y", - description="Vertical resolution of the output image", - default=1080, - min=1 + name="Y", description="Vertical resolution of the output image", default=1080, min=1 ) output_dir: StringProperty( name="Output Directory", description="Directory to output Radiance files", default="", subtype="DIR_PATH", - update=lambda self, context: self.update_output_dir(context) + update=lambda self, context: self.update_output_dir(context), ) ifc_file: StringProperty( name="IFC File", description="Path to the IFC file", default="", subtype="FILE_PATH", - update=lambda self, context: self.update_ifc_file(context) + update=lambda self, context: self.update_ifc_file(context), ) json_file: StringProperty( name="JSON File", description="Path to the JSON file", default="", subtype="FILE_PATH", - update=lambda self, context: self.update_json_file(context) + update=lambda self, context: self.update_json_file(context), ) - + radiance_quality: EnumProperty( name="Quality", description="Radiance rendering quality", - items=[ - ('LOW', "Low", "Low quality"), - ('MEDIUM', "Medium", "Medium quality"), - ('HIGH', "High", "High quality") - ], - default='MEDIUM' + items=[("LOW", "Low", "Low quality"), ("MEDIUM", "Medium", "Medium quality"), ("HIGH", "High", "High quality")], + default="MEDIUM", ) radiance_detail: EnumProperty( name="Detail", description="Radiance rendering detail", - items=[ - ('LOW', "Low", "Low detail"), - ('MEDIUM', "Medium", "Medium detail"), - ('HIGH', "High", "High detail") - ], - default='MEDIUM' + items=[("LOW", "Low", "Low detail"), ("MEDIUM", "Medium", "Medium detail"), ("HIGH", "High", "High detail")], + default="MEDIUM", ) radiance_variability: EnumProperty( name="Variability", description="Radiance rendering variability", items=[ - ('LOW', "Low", "Low variability"), - ('MEDIUM', "Medium", "Medium variability"), - ('HIGH', "High", "High variability") + ("LOW", "Low", "Low variability"), + ("MEDIUM", "Medium", "Medium variability"), + ("HIGH", "High", "High variability"), ], default="MEDIUM", ) output_file_name: StringProperty( - name="Output File Name", - description="Name of the output image file (without extension)", - default="render" + name="Output File Name", description="Name of the output image file (without extension)", default="render" ) - + output_file_format: EnumProperty( name="Output File Format", description="Format of the output image file", items=[ - ('HDR', "HDR", "High Dynamic Range"), + ("HDR", "HDR", "High Dynamic Range"), ], - default='HDR' + default="HDR", ) - class BIMSolarProperties(PropertyGroup): sites: EnumProperty(items=get_sites, name="Sites") latitude: FloatProperty(name="Latitude", min=-90, max=90, update=update_latlong) diff --git a/src/blenderbim/blenderbim/bim/module/light/ui.py b/src/blenderbim/blenderbim/bim/module/light/ui.py index 9c78261807..c9b2c045f5 100644 --- a/src/blenderbim/blenderbim/bim/module/light/ui.py +++ b/src/blenderbim/blenderbim/bim/module/light/ui.py @@ -26,11 +26,12 @@ sun_position = tool.Blender.get_sun_position_addon() class BIM_PT_radiance_exporter(bpy.types.Panel): """Creates a Panel in the render properties window""" + bl_label = "Radiance Exporter" bl_idname = "BIM_PT_radiance_exporter" - bl_options = {'DEFAULT_CLOSED'} - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" bl_context = "scene" bl_parent_id = "BIM_PT_tab_lighting" @@ -43,15 +44,13 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): if tool.Ifc.get(): row = self.layout.row() row.prop(props, "should_load_from_memory") - + if not tool.Ifc.get() or not props.should_load_from_memory: row = self.layout.row(align=True) row.prop(props, "ifc_file") - # row.operator("bim.select_ifctester_ifc_file", icon="FILE_FOLDER", text="") row = layout.row() row.prop(props, "output_dir") - row = layout.row() layout.prop(props, "use_json_file") @@ -69,22 +68,21 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): layout.operator("bim.refresh_ifc_materials") - layout.separator() row = layout.row() row.label(text="Resolution") row.prop(props, "radiance_resolution_x", text="X") - + row = layout.row() row.label(text="") row.prop(props, "radiance_resolution_y", text="Y") - + row = layout.row() row.prop(props, "radiance_quality") - + row = layout.row() row.prop(props, "radiance_detail") - + row = layout.row() row.prop(props, "radiance_variability") @@ -92,7 +90,7 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): row = layout.row() row.prop(props, "output_file_name") - + row = layout.row() row.prop(props, "output_file_format") layout.separator() @@ -104,12 +102,14 @@ class BIM_PT_radiance_exporter(bpy.types.Panel): row.operator("render_scene.radiance", text="Radiance Render") row.enabled = not props.is_exporting + class BIM_PT_solar(bpy.types.Panel): """Creates a Panel in the render properties window""" + bl_label = "Solar Access / Shadow" bl_idname = "BIM_PT_solar" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" bl_context = "scene" bl_parent_id = "BIM_PT_tab_solar_analysis" bl_options = {"HIDE_HEADER"} @@ -135,7 +135,7 @@ class BIM_PT_solar(bpy.types.Panel): sun_props = context.scene.sun_pos_properties row = self.layout.row() - row.prop(sun_props, "coordinates", icon='URL') + row.prop(sun_props, "coordinates", icon="URL") row = self.layout.row(align=True) row.prop(props, "latitude") row.prop(props, "longitude") @@ -146,20 +146,24 @@ class BIM_PT_solar(bpy.types.Panel): row.operator("bim.import_true_north", icon="IMPORT", text="") row = self.layout.row(align=True) - row.prop(props, "month", text={ - 1: "January", - 2: "February", - 3: "March", - 4: "April", - 5: "May", - 6: "June", - 7: "July", - 8: "August", - 9: "September", - 10: "October", - 11: "November", - 12: "December", - }[props.month]) + row.prop( + props, + "month", + text={ + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }[props.month], + ) row.prop(props, "day") row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/bim/module/material/data.py b/src/blenderbim/blenderbim/bim/module/material/data.py index 9139452cce..78bc53521f 100644 --- a/src/blenderbim/blenderbim/bim/module/material/data.py +++ b/src/blenderbim/blenderbim/bim/module/material/data.py @@ -258,7 +258,9 @@ class ObjectMaterialData: precision = bpy.context.scene.DocProperties.imperial_precision else: precision = None - formatted_thickness = format_distance(total_thickness, precision=precision, suppress_zero_inches=True, in_unit_length = True) + formatted_thickness = format_distance( + total_thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True + ) data["name"] = f"{formatted_thickness} - {data['name']}" if item.is_a("IfcMaterial"): data["material"] = item.Name or "Unnamed" diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index 5967b4d896..29beb20de8 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -120,6 +120,11 @@ def get_contexts(self, context): def update_material_name(self: "Material", context: bpy.types.Context) -> None: + # No need to update anything if it's either category + # or ifc_definition_id is not yet set + # (which occurs when Material still in the process of creation). + if self.is_category or not self.ifc_definition_id: + return ifc_file = tool.Ifc.get() name = self.name material = ifc_file.by_id(self.ifc_definition_id) diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 7ba072d237..2fc53765f1 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -223,7 +223,7 @@ class BIM_PT_object_material(Panel): row = self.layout.row(align=True) row.label(text="No Profiles Available") row.operator("bim.add_profile_def", icon="ADD", text="") - else: + else: row = self.layout.row(align=True) if ObjectMaterialData.data["set_item_name"] == "profile": prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="") @@ -235,7 +235,10 @@ class BIM_PT_object_material(Panel): row = self.layout.row(align=True) row.label(text="----- Exterior -----") for index, set_item in enumerate(ObjectMaterialData.data["set_items"]): - if len(self.props.material_set_item_profile_attributes) and self.props.active_material_set_item_id == set_item["id"]: + if ( + len(self.props.material_set_item_profile_attributes) + and self.props.active_material_set_item_id == set_item["id"] + ): self.draw_editable_set_item_profile_ui(set_item) elif self.props.active_material_set_item_id == set_item["id"]: self.draw_editable_set_item_ui(set_item) @@ -341,7 +344,9 @@ class BIM_PT_object_material(Panel): precision = bpy.context.scene.DocProperties.imperial_precision else: precision = None - formatted_thickness = format_distance(total_thickness, precision=precision, suppress_zero_inches=True, in_unit_length = True) + formatted_thickness = format_distance( + total_thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True + ) row = self.layout.row(align=True) row.label(text=f"Total Thickness: {formatted_thickness}") diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index f5505e68c5..8444b46fc1 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -143,7 +143,6 @@ classes = ( stair.EnableEditingStair, stair.RemoveStair, pie.OpenPieClass, - pie.PieUpdateContainer, pie.PieAddOpening, pie.VIEW3D_MT_PIE_bim, pie.VIEW3D_MT_PIE_bim_class, diff --git a/src/blenderbim/blenderbim/bim/module/model/array.py b/src/blenderbim/blenderbim/bim/module/model/array.py index a521ee500b..3f8beed3b2 100644 --- a/src/blenderbim/blenderbim/bim/module/model/array.py +++ b/src/blenderbim/blenderbim/bim/module/model/array.py @@ -84,9 +84,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object element = tool.Ifc.get_entity(obj) props = obj.BIMArrayProperties - + relating_obj = props.relating_array_object - + if relating_obj: element = tool.Ifc.get_entity(relating_obj) parent_globalid = ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Parent") @@ -102,9 +102,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator): props.use_local_space = data.get("use_local_space", False) props.sync_children = data.get("sync_children", False) props.method = data.get("method", "OFFSET") - + props.is_editing = self.item - + return {"FINISHED"} @@ -149,9 +149,9 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator): tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True) tool.Blender.Modifier.Array.constrain_children_to_parent(element) - #clears the relating_array_object so it doesn't show again next time + # clears the relating_array_object so it doesn't show again next time props.relating_array_object = None - + return {"FINISHED"} @@ -285,8 +285,11 @@ class SelectAllArrayObjects(bpy.types.Operator): array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element) tool.Blender.set_objects_selection( - context, active_object=array_objects[0], selected_objects=array_objects, clear_previous_selection=False - ) + context, + active_object=array_objects[0], + selected_objects=array_objects, + clear_previous_selection=False, + ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/covering.py b/src/blenderbim/blenderbim/bim/module/model/covering.py index efd1c47467..e1cc5b948b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/covering.py +++ b/src/blenderbim/blenderbim/bim/module/model/covering.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell +import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.core.covering as core @@ -31,9 +32,7 @@ class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operato @classmethod def poll(cls, context): - relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id) - relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id)) - return relating_type == "FLOORING" + return tool.Covering.covering_poll_relating_type_check(cls, context, "FLOORING") def _execute(self, context): try: @@ -50,9 +49,7 @@ class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator @classmethod def poll(cls, context): - relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id) - relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id)) - return relating_type == "CEILING" + return tool.Covering.covering_poll_relating_type_check(cls, context, "CEILING") def _execute(self, context): try: @@ -69,8 +66,10 @@ class RegenSelectedCoveringObject(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - element = tool.Ifc.get_entity(bpy.context.active_object) - return element and element.is_a("IfcCovering") + if (obj := context.active_object) and (element := tool.Ifc.get_entity(obj)) and element.is_a("IfcCovering"): + return True + cls.poll_message_set("IfcCovering must be selected.") + return False def _execute(self, context): try: @@ -89,11 +88,7 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato @classmethod def poll(cls, context): - element = tool.Ifc.get_entity(bpy.context.active_object) - relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id) - relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id)) - if element and element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2": - return context.selected_objects and relating_type == "FLOORING" + return tool.Covering.covering_poll_wall_selected(cls, context, "FLOORING") def _execute(self, context): # This only works based on a 2D plan only considering the standard @@ -117,11 +112,7 @@ class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator @classmethod def poll(cls, context): - element = tool.Ifc.get_entity(bpy.context.active_object) - relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id) - relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id)) - if element and element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2": - return context.selected_objects and relating_type == "CEILING" + return tool.Covering.covering_poll_wall_selected(cls, context, "CEILING") def _execute(self, context): # This only works based on a 2D plan only considering the standard diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index f8fcfd2612..ea6f6ae3d9 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -50,10 +50,11 @@ class AuthoringData: cls.props = bpy.context.scene.BIMModelProperties if ifc_element_type: cls.ifc_element_type = None if ifc_element_type == "all" else ifc_element_type + cls.data["default_container"] = cls.default_container() cls.data["ifc_element_type"] = cls.ifc_element_type cls.data["ifc_classes"] = cls.ifc_classes() cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes() - cls.data["predefined_type"] = cls.predefined_type() # only after .relating_type_id() + cls.data["predefined_type"] = cls.predefined_type() # only after .relating_type_id() cls.data["type_class"] = cls.type_class() # only after .type_class() @@ -74,6 +75,15 @@ class AuthoringData: cls.data["boundary_class"] = cls.boundary_class() cls.data["selected_material_usages"] = cls.selected_material_usages() + @classmethod + def default_container(cls) -> str | None: + props = bpy.context.scene.BIMSpatialDecompositionProperties + if props.default_container: + try: + return tool.Ifc.get().by_id(props.default_container).Name + except: + pass + @classmethod def boundary_class(cls): declaration = tool.Ifc.schema().declaration_by_name("IfcRelSpaceBoundary") diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index 7531c79ae4..8ce85307b8 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -541,7 +541,9 @@ class MEPGenerator: ifc_representation_class=None, ) - rel = ifcopenshell.api.run("material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run( + "material.assign_material", ifc_file, products=[element], type="IfcMaterialProfileSet" + ) profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( "material.add_profile", ifc_file, profile_set=profile_set, material=material diff --git a/src/blenderbim/blenderbim/bim/module/model/pie.py b/src/blenderbim/blenderbim/bim/module/model/pie.py index 7c55265d81..fe3a0e151a 100644 --- a/src/blenderbim/blenderbim/bim/module/model/pie.py +++ b/src/blenderbim/blenderbim/bim/module/model/pie.py @@ -63,28 +63,6 @@ class PieAddOpening(bpy.types.Operator): return {"FINISHED"} -class PieUpdateContainer(bpy.types.Operator): - bl_idname = "bim.pie_update_container" - bl_label = "Update Spatial Container" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - - def _execute(self, context): - for obj in context.selected_objects: - if not obj.BIMObjectProperties.ifc_definition_id: - continue - for collection in obj.users_collection: - spatial_obj = collection.BIMCollectionProperties.obj - if spatial_obj and spatial_obj.BIMObjectProperties.ifc_definition_id: - blenderbim.core.spatial.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, structure_obj=spatial_obj, element_obj=obj - ) - break - return {"FINISHED"} - - class VIEW3D_MT_PIE_bim(bpy.types.Menu): bl_label = "Geometry" @@ -93,7 +71,6 @@ class VIEW3D_MT_PIE_bim(bpy.types.Menu): pie.operator("bim.edit_object_placement") pie.operator("bim.update_representation").ifc_representation_class = "" pie.operator("bim.pie_add_opening") - pie.operator("bim.pie_update_container") pie.operator("bim.open_pie_class", text="Assign IFC Class") pie.operator("bim.aggregate_assign_object", text="Assign Aggregation") pie.operator("bim.aggregate_unassign_object", text="Unassign Aggregation") diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index c3664a17b2..fcedbbbbb3 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -225,9 +225,8 @@ class AddConstrTypeInstance(bpy.types.Operator): else: parent = ifcopenshell.util.element.get_container(building_element) if parent: - parent_obj = tool.Ifc.get_object(parent) blenderbim.core.spatial.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj + tool.Ifc, tool.Collector, tool.Spatial, container=parent, element_obj=obj ) # set occurrences properties for the types defined with modifiers diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 96f375fa1a..c1f011bb18 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -99,9 +99,6 @@ def is_object_array_applicable(self, obj): return ifcopenshell.util.element.get_pset(element, "BBIM_Array") - - - class BIMModelProperties(PropertyGroup): ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class) relating_type_id: bpy.props.EnumProperty( @@ -226,8 +223,6 @@ class BIMArrayProperties(PropertyGroup): ) - - class BIMStairProperties(PropertyGroup): def validate_nosing_value(self, context): if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0: diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py index 28b9f71b21..9adbc71647 100644 --- a/src/blenderbim/blenderbim/bim/module/model/space.py +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -61,10 +61,13 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): def msg_no_active_object(self, context): self.layout.label(text="No active object. Please select a wall") + def msg_no_active_wall(self, context): self.layout.label(text="The active object is not a wall. Please select a wall.") + def msg_no_container(self, context): self.layout.label(text="The wall is not contained. Please the selected wall in a building container") + def msg_no_selected_objects(self, context): self.layout.label(text="No selected objects found. Please select walls.") diff --git a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py index 8a1946896a..6e5d7d00b9 100644 --- a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py +++ b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py @@ -147,6 +147,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator): "edges": output_node.inputs["Edges"].sv_get(deepcopy=False, default=[]), "polygons": output_node.inputs["Polygons"].sv_get(deepcopy=False, default=[]), } + # sv_get returns array of 1 element arrays, for convenience we reduce it to just 1 level array def try_first_element(x): return x[0] if x else x diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 33b5bb0845..4719c5e59d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -503,6 +503,7 @@ class BimToolUI: def draw_type_selection_interface(cls): # shared by both sidebar and header row = cls.layout.row(align=True) + row.label(text=f"Container: {AuthoringData.data['default_container']}", icon="OUTLINER_COLLECTION") if AuthoringData.data["ifc_classes"]: if not AuthoringData.data["ifc_element_type"]: row.label(text="", icon="FILE_VOLUME") diff --git a/src/blenderbim/blenderbim/bim/module/owner/prop.py b/src/blenderbim/blenderbim/bim/module/owner/prop.py index 1884a4f719..e299b69954 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/prop.py +++ b/src/blenderbim/blenderbim/bim/module/owner/prop.py @@ -79,7 +79,11 @@ def get_actor_type_enum(self, context): return [ ("IfcPerson", "Person", get_entity_doc(version, "IfcPerson").get("description", "")), ("IfcOrganization", "Organisation", get_entity_doc(version, "IfcOrganization").get("description", "")), - ("IfcPersonAndOrganization", "User", get_entity_doc(version, "IfcPersonAndOrganization").get("description", "")), + ( + "IfcPersonAndOrganization", + "User", + get_entity_doc(version, "IfcPersonAndOrganization").get("description", ""), + ), ] diff --git a/src/blenderbim/blenderbim/bim/module/patch/operator.py b/src/blenderbim/blenderbim/bim/module/patch/operator.py index 9397b52f33..75414d0da7 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/operator.py +++ b/src/blenderbim/blenderbim/bim/module/patch/operator.py @@ -67,7 +67,6 @@ class ExecuteIfcPatch(bpy.types.Operator): bl_idname = "bim.execute_ifc_patch" bl_label = "Execute IFCPatch" file_format: bpy.props.StringProperty() - use_json_for_args: bpy.props.BoolProperty() @classmethod def poll(cls, context): @@ -76,9 +75,8 @@ class ExecuteIfcPatch(bpy.types.Operator): def execute(self, context): props = context.scene.BIMPatchProperties - if self.use_json_for_args or not props.ifc_patch_args_attr: - arguments = json.loads(props.ifc_patch_args or "[]") - else: + arguments = [] + if props.ifc_patch_args_attr: arguments = [arg.get_value() for arg in props.ifc_patch_args_attr] if props.should_load_from_memory and tool.Ifc.get(): @@ -124,18 +122,48 @@ class UpdateIfcPatchArguments(bpy.types.Operator): new_attr = patch_args.add() data_type = arg_info.get("type", "str") if isinstance(data_type, list): + if "file" in data_type: + data_type = ["file"] + data_type = [dt for dt in data_type if dt != "NoneType"][0] + new_attr.data_type = { - "Literal": "string", + "Literal": "enum", + "file": "file", "str": "string", "float": "float", "int": "integer", "bool": "boolean", }[data_type] - new_attr.name = arg_name + new_attr.name = self.pretty_arg_name(arg_name) + if new_attr.data_type == "enum": + new_attr.enum_items = json.dumps(arg_info.get("enum_items", [])) + new_attr.enum_value = arg_info.get("default", new_attr.get_value_default()) + continue + + if new_attr.data_type == "file": + new_attr.filepath_value.single_file = arg_info.get("default", new_attr.get_value_default()) + new_attr.filter_glob = arg_info.get("filter_glob", "*.ifc;*.ifczip;*.ifcxml") + continue + new_attr.set_value(arg_info.get("default", new_attr.get_value_default())) return {"FINISHED"} + def pretty_arg_name(self, arg_name: str): + words = [] + + for word in arg_name.split("_"): + word = word.strip().capitalize() + + replacements = [("Dir", "Directory"), ("Sql", "SQL")] + + for prev, new in replacements: + word = word.replace(prev, new) + + words.append(word) + + return " ".join(words) + class RunMigratePatch(bpy.types.Operator): bl_idname = "bim.run_migrate_patch" diff --git a/src/blenderbim/blenderbim/bim/module/patch/prop.py b/src/blenderbim/blenderbim/bim/module/patch/prop.py index e965e63e15..527584393f 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/prop.py +++ b/src/blenderbim/blenderbim/bim/module/patch/prop.py @@ -65,6 +65,5 @@ class BIMPatchProperties(PropertyGroup): ifc_patch_recipes: EnumProperty(items=get_ifcpatch_recipes, name="Recipes", update=update_ifc_patch_recipe) ifc_patch_input: StringProperty(default="", name="IFC Patch Input IFC") ifc_patch_output: StringProperty(default="", name="IFC Patch Output IFC") - ifc_patch_args: StringProperty(default="", name="Arguments") ifc_patch_args_attr: CollectionProperty(type=Attribute, name="Arguments") should_load_from_memory: BoolProperty(default=False, name="Load from Memory") diff --git a/src/blenderbim/blenderbim/bim/module/patch/ui.py b/src/blenderbim/blenderbim/bim/module/patch/ui.py index c5013c31ff..1b1ebfa517 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/ui.py +++ b/src/blenderbim/blenderbim/bim/module/patch/ui.py @@ -56,7 +56,4 @@ class BIM_PT_patch(bpy.types.Panel): if props.ifc_patch_args_attr: draw_attributes(props.ifc_patch_args_attr, layout) - else: - layout.row().prop(props, "ifc_patch_args") op = layout.operator("bim.execute_ifc_patch") - op.use_json_for_args = len(props.ifc_patch_args_attr) == 0 diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index 479faaaf77..bf4e23d5df 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -28,16 +28,15 @@ classes = ( operator.BIM_OT_load_clipping_planes, operator.BIM_OT_save_clipping_planes, operator.ChangeLibraryElement, + operator.ClearRecentIFCProjects, operator.CreateClippingPlane, operator.CreateProject, - operator.ClearRecentIFCProjects, operator.DisableCulling, operator.DisableEditingHeader, operator.EditHeader, operator.EnableCulling, operator.EnableEditingHeader, operator.ExportIFC, - operator.ExportIFCDeprecated, operator.FlipClippingPlane, operator.LinkIfc, operator.LoadLink, @@ -92,6 +91,7 @@ def register(): bpy.utils.register_tool(workspace.ExploreTool, after={"builtin.transform"}, separator=True, group=False) bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties) bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load) + bpy.types.TOPBAR_MT_file_import.append(ui.file_import_menu) bpy.types.TOPBAR_MT_file.prepend(ui.file_menu) bpy.types.TOPBAR_MT_file_context_menu.prepend(ui.file_menu) wm = bpy.context.window_manager diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index e60bf88929..b86e89f2c3 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -127,7 +127,9 @@ class CreateProject(bpy.types.Operator): bpy.data.meshes.remove(mesh) for mat in bpy.data.materials: bpy.data.materials.remove(mat) - core.create_project(tool.Ifc, tool.Georeference, tool.Project, tool.Spatial, schema=props.export_schema, template=template) + core.create_project( + tool.Ifc, tool.Georeference, tool.Project, tool.Spatial, schema=props.export_schema, template=template + ) blenderbim.bim.schema.reload(tool.Ifc.get().schema_identifier) tool.Blender.register_toolbar() @@ -728,12 +730,12 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): if not self.is_existing_ifc_file(): return {"FINISHED"} - if tool.Blender.is_default_scene(): + if self.should_start_fresh_session and tool.Blender.is_default_scene(): for obj in bpy.data.objects: bpy.data.objects.remove(obj) filepath = Path(self.get_filepath()) - context.scene.BIMProperties.ifc_file = str(filepath) + context.scene.BIMProperties.ifc_file = filepath.as_posix() context.scene.BIMProjectProperties.is_loading = True context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement")) tool.Blender.register_toolbar() @@ -741,6 +743,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): if not self.is_advanced: bpy.ops.bim.load_project_elements() + if not self.should_start_fresh_session: + bpy.ops.bim.convert_to_blender() except: blenderbim.last_error = traceback.format_exc() raise @@ -1233,6 +1237,10 @@ class ExportIFCBase: if bpy.data.is_saved: layout.prop(self, "use_relative_path") + layout.separator() + layout.label(text="Supported formats for export:") + layout.label(text=".ifc, .ifczip, .ifcjson") + def invoke(self, context, event): if not tool.Ifc.get(): bpy.ops.wm.save_mainfile("INVOKE_DEFAULT") @@ -1331,18 +1339,6 @@ class ExportIFC(ExportIFCBase, bpy.types.Operator): pass -# TODO: remove as deprecated, better wait couple releases since -# this operator is used for saving IFC files in user scripts. -class ExportIFCDeprecated(ExportIFCBase, bpy.types.Operator): - bl_idname = "export_ifc.bim" - - def execute(self, context): - msg = f"'{ExportIFCDeprecated.bl_idname}' operator name is deprecated, use '{ExportIFC.bl_idname}'." - self.report({"WARNING"}, msg) - print(msg) - return super().execute(context) - - class LoadLinkedProject(bpy.types.Operator): bl_idname = "bim.load_linked_project" bl_label = "Load a project for viewing only." diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index b53e6f808e..0ced23f63c 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -25,6 +25,11 @@ from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.project.data import ProjectData, LinksData +def file_import_menu(self, context): + op = self.layout.operator("bim.load_project", text="IFC (Geometry Only) (.ifc/.ifczip/.ifcxml)") + op.should_start_fresh_session = False + + class BIM_MT_project(Menu): bl_idname = "BIM_MT_project" bl_label = "New IFC Project" @@ -294,9 +299,8 @@ class BIM_PT_new_project_wizard(Panel): row.prop(props, "volume_unit", text="Volume Unit") prop_with_search(self.layout, pprops, "template_file", text="Template") - row = self.layout.row(align=True) + row = self.layout.row() row.operator("bim.create_project") - row.operator("bim.load_project").should_start_fresh_session = False class BIM_PT_project_library(Panel): diff --git a/src/blenderbim/blenderbim/bim/module/pset/data.py b/src/blenderbim/blenderbim/bim/module/pset/data.py index 35a48d6b02..c5207791d9 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/data.py +++ b/src/blenderbim/blenderbim/bim/module/pset/data.py @@ -51,13 +51,13 @@ class Data: psetqtos = ifcopenshell.util.element.get_psets( element, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False ) - for name, data in psetqtos.items(): + for name, data in sorted(psetqtos.items()): results.append( { "id": data["id"], "Name": name, "is_expanded": is_expanded.get(data["id"], True), - "Properties": [{"Name": k, "NominalValue": v} for k, v in data.items() if k != "id"], + "Properties": [{"Name": k, "NominalValue": v} for k, v in sorted(data.items()) if k != "id"], } ) return sorted(results, key=lambda v: v["Name"]) diff --git a/src/blenderbim/blenderbim/bim/module/pset/prop.py b/src/blenderbim/blenderbim/bim/module/pset/prop.py index 12ad36d779..0de52d366b 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/prop.py +++ b/src/blenderbim/blenderbim/bim/module/pset/prop.py @@ -170,6 +170,7 @@ def get_group_qto_names(self, context): qtonames[ifc_class] = blender_formatted_enum_from_psets(psets) return qtonames[ifc_class] + def get_profile_pset_names(self, context): global psetnames pprops = context.scene.BIMProfileProperties diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index 8a6f6d9346..e96088af94 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import bpy import blenderbim.tool as tool from bpy.types import Panel from blenderbim.bim.ifc import IfcStore @@ -34,16 +35,21 @@ from blenderbim.bim.module.pset.data import ( ProfilePsetsData, WorkSchedulePsetsData, ) +from typing import Any, Optional -def draw_property(prop, layout, copy_operator=None): +def draw_property( + prop: bpy.types.PropertyGroup, layout: bpy.types.UILayout, copy_operator: Optional[str] = None +) -> None: if prop.value_type == "IfcPropertySingleValue": draw_single_property(prop, layout, copy_operator) elif prop.value_type == "IfcPropertyEnumeratedValue": draw_enumerated_property(prop, layout, copy_operator) -def draw_single_property(prop, layout, copy_operator=None): +def draw_single_property( + prop: bpy.types.PropertyGroup, layout: bpy.types.UILayout, copy_operator: Optional[str] = None +) -> None: value_name = prop.metadata.get_value_name(display_only=True) if not value_name: layout.label(text=prop["Name"]) @@ -63,7 +69,9 @@ def draw_single_property(prop, layout, copy_operator=None): op.name = prop.metadata.name -def draw_enumerated_property(prop, layout, copy_operator=None): +def draw_enumerated_property( + prop: bpy.types.PropertyGroup, layout: bpy.types.UILayout, copy_operator: Optional[str] = None +) -> None: value_name = prop.metadata.get_value_name() if not value_name: layout.label(text=prop.metadata.name) @@ -78,13 +86,21 @@ def draw_enumerated_property(prop, layout, copy_operator=None): op.name = prop.metadata.name -def get_active_pset_obj_name(context, obj_type): +def get_active_pset_obj_name(context: bpy.types.Context, obj_type: tool.Ifc.OBJECT_TYPE) -> str: if obj_type in ("Object", "MaterialSet", "MaterialSetItem"): return context.active_object.name return "" -def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True): +def draw_psetqto_ui( + context: bpy.types.Context, + pset_id: int, + pset: dict[str, Any], + props: bpy.types.PropertyGroup, + layout: bpy.types.UILayout, + obj_type: tool.Ifc.OBJECT_TYPE, + allow_removing: bool = True, +) -> None: box = layout.box() row = box.row(align=True) if "is_expanded" not in pset: @@ -147,7 +163,7 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov row.scale_y = 0.8 row.label(text=prop["Name"]) op = row.operator("bim.select_similar", text=str(prop["NominalValue"]), icon="NONE", emboss=False) - op.key = pset['Name'] + op.key = pset["Name"] if not has_props_displayed: row = box.row() @@ -155,7 +171,9 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov row.label(text="No Properties Set") -def draw_psetqto_editable_ui(box, props, prop): +def draw_psetqto_editable_ui( + box: bpy.types.UILayout, props: bpy.types.PropertyGroup, prop: bpy.types.PropertyGroup +) -> None: row = box.row(align=True) draw_property(prop, row, copy_operator="bim.copy_property_to_selection") diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/data.py b/src/blenderbim/blenderbim/bim/module/pset_template/data.py index 77b6685a36..ac34395013 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/data.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/data.py @@ -75,7 +75,9 @@ class PsetTemplatesData: pset_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "pset") files = os.listdir(pset_dir) for f in files: - results.append((os.path.join(pset_dir, f), os.path.splitext(os.path.basename(f))[0], "Global Pset Template")) + results.append( + (os.path.join(pset_dir, f), os.path.splitext(os.path.basename(f))[0], "Global Pset Template") + ) pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir) if os.path.isdir(pset_dir): diff --git a/src/blenderbim/blenderbim/bim/module/qto/__init__.py b/src/blenderbim/blenderbim/bim/module/qto/__init__.py index a86ca04195..2510ce3182 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/qto/__init__.py @@ -25,6 +25,7 @@ classes = ( operator.CalculateFaceAreas, operator.CalculateFormworkArea, operator.CalculateObjectVolumes, + operator.CalculateSideFormworkArea, operator.CalculateSingleQuantity, operator.PerformQuantityTakeOff, prop.BIMQtoProperties, diff --git a/src/blenderbim/blenderbim/bim/module/qto/helper.py b/src/blenderbim/blenderbim/bim/module/qto/helper.py index 2c5aaec1c6..036055f8ac 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/helper.py +++ b/src/blenderbim/blenderbim/bim/module/qto/helper.py @@ -18,25 +18,28 @@ import bpy import bmesh +from typing import Callable -def calculate_height(obj): +def calculate_height(obj: bpy.types.Object) -> float: return obj.dimensions[2] -def calculate_edges_lengths(objs, context): +def calculate_edges_lengths(objs: list[bpy.types.Object], context: bpy.types.Context): return calculate_mesh_quantity(objs, context, lambda bm: sum((e.calc_length() for e in bm.edges if e.select))) -def calculate_faces_areas(objs, context): +def calculate_faces_areas(objs: list[bpy.types.Object], context: bpy.types.Context) -> float: return calculate_mesh_quantity(objs, context, lambda bm: sum((f.calc_area() for f in bm.faces if f.select))) -def calculate_volumes(objs, context): +def calculate_volumes(objs: list[bpy.types.Object], context: bpy.types.Context) -> float: return calculate_mesh_quantity(objs, context, lambda bm: bm.calc_volume()) -def calculate_mesh_quantity(objs: bpy.types.Object, context, operation): +def calculate_mesh_quantity( + objs: list[bpy.types.Object], context: bpy.types.Context, operation: Callable[[bmesh.types.BMesh], float] +) -> float: """Get the sum of the target quantity on all passed mesh objects :param objs: iterable of mesh object @@ -58,7 +61,7 @@ def calculate_mesh_quantity(objs: bpy.types.Object, context, operation): return result -def calculate_formwork_area(objs, context): +def calculate_formwork_area(objs: list[bpy.types.Object], context: bpy.types.Context) -> float: """ Formwork is defined as the surface area required to cover all exposed surfaces of one or more objects, excluding top surfaces (i.e. that have a @@ -85,6 +88,7 @@ def calculate_formwork_area(objs, context): copied_obj.name = "Formwork" copied_obj.BIMObjectProperties.ifc_definition_id = 0 modifier = copied_obj.modifiers.new("Formwork", "REMESH") + assert isinstance(modifier, bpy.types.RemeshModifier) modifier.mode = "SHARP" # This hardcoded value may be optimised through a better understanding of the octree division. # These values are based off some trial and error heuristics I've learned through experience. @@ -108,7 +112,7 @@ def calculate_formwork_area(objs, context): return result -def calculate_side_formwork_area(objs, context): +def calculate_side_formwork_area(objs: list[bpy.types.Object], context: bpy.types.Context) -> float: """ Side formwork is defined as the surface area required to cover all exposed surfaces of one or more objects, excluding top and bottom surfaces (i.e. diff --git a/src/blenderbim/blenderbim/bim/module/qto/operator.py b/src/blenderbim/blenderbim/bim/module/qto/operator.py index c07df67466..8fbdfadd7d 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/operator.py +++ b/src/blenderbim/blenderbim/bim/module/qto/operator.py @@ -99,6 +99,21 @@ class CalculateFormworkArea(bpy.types.Operator): return {"FINISHED"} +class CalculateSideFormworkArea(bpy.types.Operator): + bl_idname = "bim.calculate_side_formwork_area" + bl_label = "Calculate Side Formwork Area" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects and context.active_object + + def execute(self, context): + result = helper.calculate_side_formwork_area([o for o in context.selected_objects if o.type == "MESH"], context) + context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + return {"FINISHED"} + + class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.calculate_single_quantity" bl_label = "Calculate Single Quantity" @@ -137,7 +152,10 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.perform_quantity_take_off" bl_label = "Perform Quantity Take-off" bl_options = {"REGISTER", "UNDO"} - bl_description = "Perform a quantity take off based of a QTO rule configuration" + bl_description = ( + "Perform a quantity take off on selected objects based of a QTO rule configuration." + "If no objects are selected, quantities calculated for all available IfcElements." + ) @classmethod def poll(cls, context): @@ -162,4 +180,6 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator): ifc_file = tool.Ifc.get() results = ifc5d.qto.quantify(ifc_file, elements, rules) ifc5d.qto.edit_qtos(ifc_file, results) + + self.report({"INFO"}, f"Quantities are calculated for {len(elements)} elements.") return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index df44c2088a..2ac7faf426 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -350,9 +350,9 @@ class BIM_UL_resources(UIList): "bim.contract_resource", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" ).resource = item.ifc_definition_id else: - row.operator( - "bim.expand_resource", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" - ).resource = item.ifc_definition_id + row.operator("bim.expand_resource", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").resource = ( + item.ifc_definition_id + ) else: row.label(text="", icon="DOT") row.prop(item, "name", emboss=False, text="", icon=icon_map[resource["type"]]) diff --git a/src/blenderbim/blenderbim/bim/module/root/data.py b/src/blenderbim/blenderbim/bim/module/root/data.py index 6a2b348221..5b7f9c272a 100644 --- a/src/blenderbim/blenderbim/bim/module/root/data.py +++ b/src/blenderbim/blenderbim/bim/module/root/data.py @@ -201,7 +201,10 @@ class IfcClassData: if element: if element.is_a("IfcOpeningElement") or element.is_a("IfcOpeningStandardCase"): return False - if element.is_a() in ("IfcWindowStyle", "IfcDoorStyle"): #see https://github.com/IfcOpenShell/IfcOpenShell/issues/4622#issuecomment-2095676368 + if element.is_a() in ( + "IfcWindowStyle", + "IfcDoorStyle", + ): # see https://github.com/IfcOpenShell/IfcOpenShell/issues/4622#issuecomment-2095676368 return True for product in cls.ifc_products(): if element.is_a(product[0]): diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py index 0c4da854ef..0e81c4f2e0 100644 --- a/src/blenderbim/blenderbim/bim/module/search/operator.py +++ b/src/blenderbim/blenderbim/bim/module/search/operator.py @@ -175,6 +175,9 @@ class Search(Operator): props = context.scene.CsvProperties elif self.property_group == "BIMSearchProperties": props = context.scene.BIMSearchProperties + else: + raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.") + results = ifcopenshell.util.selector.filter_elements( tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups) ) @@ -361,9 +364,7 @@ class SaveColourscheme(Operator, tool.Ifc.Operator): description["colourscheme_query"] = query group.Description = json.dumps(description) else: - description = json.dumps( - {"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query} - ) + description = json.dumps({"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}) group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description) def invoke(self, context, event): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 57c425ddb9..f20d7d040e 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -355,6 +355,7 @@ def update_task_bar_list(self, context): else: tool.Sequence.remove_task_bar(self.ifc_definition_id) + class Task(PropertyGroup): name: StringProperty(name="Name", update=updateTaskName) identification: StringProperty(name="Identification", update=updateTaskIdentification) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 8239528f8d..284ce058dd 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -188,9 +188,9 @@ class BIM_PT_work_schedules(Panel): row1.label(text="Schedule tools") row1 = col.row(align=True) row1.alignment = "RIGHT" - row1.operator( - "bim.generate_gantt_chart", text="Generate Gantt", icon="NLA" - ).work_schedule = work_schedule_id + row1.operator("bim.generate_gantt_chart", text="Generate Gantt", icon="NLA").work_schedule = ( + work_schedule_id + ) row1.operator( "bim.recalculate_schedule", text="Re-calculate Schedule", icon="FILE_REFRESH" ).work_schedule = work_schedule_id @@ -226,9 +226,9 @@ class BIM_PT_work_schedules(Panel): ) col2 = grid.column() row = col2.row(align=True) - row.operator( - "bim.enable_editing_work_schedule_tasks", text="Tasks", icon="ACTION" - ).work_schedule = work_schedule_id + row.operator("bim.enable_editing_work_schedule_tasks", text="Tasks", icon="ACTION").work_schedule = ( + work_schedule_id + ) row.operator( "bim.enable_editing_work_schedule", text="Attributes", icon="GREASEPENCIL" ).work_schedule = work_schedule_id @@ -266,12 +266,12 @@ class BIM_PT_work_schedules(Panel): row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA") row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING") row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id - row2.operator( - "bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO" - ).task = ifc_definition_id - row2.operator( - "bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL" - ).task = ifc_definition_id + row2.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = ( + ifc_definition_id + ) + row2.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = ( + ifc_definition_id + ) row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id row.operator("bim.duplicate_task", text="Copy", icon="DUPLICATE").task = ifc_definition_id row.operator("bim.remove_task", text="Delete", icon="X").task = ifc_definition_id @@ -876,13 +876,13 @@ class BIM_UL_tasks(UIList): row.label(text="", icon="BLANK1") if item.has_children: if item.is_expanded: - row.operator( - "bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" - ).task = item.ifc_definition_id + row.operator("bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN").task = ( + item.ifc_definition_id + ) else: - row.operator( - "bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" - ).task = item.ifc_definition_id + row.operator("bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT").task = ( + item.ifc_definition_id + ) else: row.label(text="", icon="DOT") diff --git a/src/blenderbim/blenderbim/bim/module/spatial/__init__.py b/src/blenderbim/blenderbim/bim/module/spatial/__init__.py index df5a781828..bdb5c81e8c 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/__init__.py @@ -21,7 +21,6 @@ from . import ui, prop, operator, workspace classes = ( operator.AssignContainer, - operator.ChangeSpatialLevel, operator.ContractContainer, operator.CopyToContainer, operator.DeleteContainer, @@ -39,14 +38,11 @@ classes = ( operator.SelectSimilarContainer, operator.SetContainerVisibility, operator.SetDefaultContainer, - prop.SpatialElement, prop.Element, - prop.BIMSpatialProperties, prop.BIMObjectSpatialProperties, prop.BIMContainer, prop.BIMSpatialDecompositionProperties, ui.BIM_PT_spatial, - ui.BIM_UL_containers, ui.BIM_UL_containers_manager, ui.BIM_UL_elements, ui.BIM_PT_spatial_decomposition, @@ -57,14 +53,14 @@ classes = ( def register(): if not bpy.app.background: bpy.utils.register_tool(workspace.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False) - bpy.types.Scene.BIMSpatialProperties = bpy.props.PointerProperty(type=prop.BIMSpatialProperties) bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties) - bpy.types.Scene.BIMSpatialDecompositionProperties = bpy.props.PointerProperty(type=prop.BIMSpatialDecompositionProperties) + bpy.types.Scene.BIMSpatialDecompositionProperties = bpy.props.PointerProperty( + type=prop.BIMSpatialDecompositionProperties + ) def unregister(): if not bpy.app.background: bpy.utils.unregister_tool(workspace.SpatialTool) - del bpy.types.Scene.BIMSpatialProperties del bpy.types.Object.BIMObjectSpatialProperties del bpy.types.Scene.BIMSpatialDecompositionProperties diff --git a/src/blenderbim/blenderbim/bim/module/spatial/data.py b/src/blenderbim/blenderbim/bim/module/spatial/data.py index 63fe167a81..7959e28bca 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/data.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/data.py @@ -37,7 +37,8 @@ class SpatialData: if cls.data["poll"]: cls.data.update( { - "parent_container_id": cls.parent_container_id(), + "default_container": cls.default_container(), + "selected_containers": cls.selected_containers(), "is_directly_contained": cls.is_directly_contained(), "label": cls.label(), "references": cls.references(), @@ -56,14 +57,17 @@ class SpatialData: return False @classmethod - def parent_container_id(cls): - container_id = bpy.context.scene.BIMSpatialProperties.active_container_id - if not container_id: - return - container = tool.Ifc.get_entity_by_id(container_id) - if not container or container.is_a("IfcProject"): - return - return container.Decomposes[0].RelatingObject.id() + def default_container(cls) -> str | None: + props = bpy.context.scene.BIMSpatialDecompositionProperties + if props.default_container: + try: + return tool.Ifc.get().by_id(props.default_container).Name + except: + pass + + @classmethod + def selected_containers(cls): + return sorted([e.Name or "Unnamed" for e in tool.Spatial.get_selected_containers()]) @classmethod def label(cls): @@ -97,7 +101,7 @@ class SpatialDecompositionData: } @classmethod - def default_container(cls) -> str: + def default_container(cls) -> str | None: props = bpy.context.scene.BIMSpatialDecompositionProperties if props.default_container: try: diff --git a/src/blenderbim/blenderbim/bim/module/spatial/operator.py b/src/blenderbim/blenderbim/bim/module/spatial/operator.py index 77d2652f65..f705b2cf84 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/operator.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/operator.py @@ -29,13 +29,21 @@ import blenderbim.bim.handler class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.reference_structure" bl_label = "Reference Structure" + bl_description = ( + "Reference selected objects from all selected structures.\n\n" + "Currently we do not support referencing structures in other structures " + "though it is allowed in IFC4X3" + ) bl_options = {"REGISTER", "UNDO"} - structure: bpy.props.IntProperty() def _execute(self, context): - sprops = context.scene.BIMSpatialProperties - containers = [tool.Ifc.get().by_id(c.ifc_definition_id) for c in sprops.containers if c.is_selected] - for obj in context.selected_objects: + objs = tool.Spatial.get_selected_objects_without_containers() + if not objs: + self.report({"INFO"}, "No non-spatial objects are selected.") + return + + containers = tool.Spatial.get_selected_containers() + for obj in objs: element = tool.Ifc.get_entity(obj) if not element: continue @@ -46,13 +54,21 @@ class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator): class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.dereference_structure" bl_label = "Dereference Structure" + bl_description = ( + "Dereference selected objects from all selected structures.\n\n" + "Currently we do not support referencing structures in other structures " + "though it is allowed in IFC4X3" + ) bl_options = {"REGISTER", "UNDO"} - structure: bpy.props.IntProperty() def _execute(self, context): - sprops = context.scene.BIMSpatialProperties - containers = [tool.Ifc.get().by_id(c.ifc_definition_id) for c in sprops.containers if c.is_selected] - for obj in context.selected_objects: + objs = tool.Spatial.get_selected_objects_without_containers() + if not objs: + self.report({"INFO"}, "No non-spatial objects are selected.") + return + + containers = tool.Spatial.get_selected_containers() + for obj in objs: element = tool.Ifc.get_entity(obj) if not element: continue @@ -63,15 +79,15 @@ class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator): class AssignContainer(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_container" bl_label = "Assign Container" + bl_description = "Assign current default container to the selected objects" bl_options = {"REGISTER", "UNDO"} - structure: bpy.props.IntProperty() def _execute(self, context): - structure_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.structure)) - for element_obj in context.selected_objects: - core.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, structure_obj=structure_obj, element_obj=element_obj - ) + if container := tool.Root.get_default_container(): + for element_obj in context.selected_objects: + core.assign_container( + tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj + ) class EnableEditingContainer(bpy.types.Operator, tool.Ifc.Operator): @@ -83,16 +99,6 @@ class EnableEditingContainer(bpy.types.Operator, tool.Ifc.Operator): core.enable_editing_container(tool.Spatial, obj=context.active_object) -class ChangeSpatialLevel(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.change_spatial_level" - bl_label = "Change Spatial Level" - bl_options = {"REGISTER", "UNDO"} - parent: bpy.props.IntProperty() - - def _execute(self, context): - core.change_spatial_level(tool.Spatial, parent=tool.Ifc.get().by_id(self.parent)) - - class DisableEditingContainer(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.disable_editing_container" bl_label = "Disable Editing Container" @@ -114,30 +120,36 @@ class RemoveContainer(bpy.types.Operator, tool.Ifc.Operator): class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator): """ - Copies selected 3D elements in the viewport to checkmarked spatial containers + Copies selected 3D elements in the viewport to the selected spatial containers Example: bulk copy a wall to multiple storeys 1. Select one or more 3D elements in the viewport - 2. Enable the checkmark next to one or more containers in the container list below to select it + 2. Select one or more spatial containers in the viewport 3. Press this button 4. The copied elements will have a new position relative to the destination containers - """ + + Copying containers to other containers currently is not supported.""" bl_idname = "bim.copy_to_container" bl_label = "Copy To Container" bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - sprops = context.scene.BIMSpatialProperties + objs = tool.Spatial.get_selected_objects_without_containers() + if not objs: + self.report({"INFO"}, "No non-spatial objects are selected.") + return + + containers = tool.Spatial.get_selected_containers() # Track decompositions so they can be recreated after the operation - relationships = tool.Root.get_decomposition_relationships(context.selected_objects) + relationships = tool.Root.get_decomposition_relationships(objs) old_to_new = {} - containers = [tool.Ifc.get().by_id(c.ifc_definition_id) for c in sprops.containers if c.is_selected] - for obj in context.selected_objects: + for obj in objs: result_objs = core.copy_to_container(tool.Ifc, tool.Collector, tool.Spatial, obj=obj, containers=containers) if result_objs: old_to_new[tool.Ifc.get_entity(obj)] = result_objs + # Recreate decompositions tool.Root.recreate_decompositions(relationships, old_to_new) blenderbim.bim.handler.refresh_ui_data() @@ -237,15 +249,36 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.select_decomposed_elements" bl_label = "Select Children" bl_options = {"REGISTER", "UNDO"} + should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"}) container: bpy.props.IntProperty() ifc_class: bpy.props.StringProperty() + relating_type: bpy.props.IntProperty() @classmethod def description(cls, context, operator): - return f"Select all elements contained in this {operator.ifc_class}" + return "Select all contained elements filtered by this type" + "\nALT+CLICK to select all contained elements" + + def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.alt: + self.should_filter = False + return self.execute(context) def _execute(self, context): - core.select_decomposed_elements(tool.Spatial, container=tool.Ifc.get().by_id(self.container)) + if self.should_filter: + ifc_class = self.ifc_class + relating_type = tool.Ifc.get().by_id(self.relating_type) if self.relating_type else None + is_untyped = self.relating_type == 0 + else: + ifc_class = "" + relating_type = None + is_untyped = False + core.select_decomposed_elements( + tool.Spatial, + container=tool.Ifc.get().by_id(self.container), + ifc_class=ifc_class, + relating_type=relating_type, + is_untyped=is_untyped, + ) class SetDefaultContainer(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/blenderbim/blenderbim/bim/module/spatial/prop.py b/src/blenderbim/blenderbim/bim/module/spatial/prop.py index 2afee7fa23..6e5ffae81d 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/prop.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/prop.py @@ -32,6 +32,7 @@ from bpy.props import ( ) import blenderbim.tool as tool import ifcopenshell +import ifcopenshell.util.element def get_subelement_class(self, context): @@ -71,6 +72,7 @@ def update_relating_container_from_object(self, context): return container = ifcopenshell.util.element.get_container(element) if container: + # TODO: currently broken and relating_container_object is not used in UI. bpy.ops.bim.assign_container(structure=container.id()) else: bpy.ops.bim.disable_editing_container() @@ -82,20 +84,6 @@ def is_object_applicable(self, obj): return bool(element) -class SpatialElement(PropertyGroup): - name: StringProperty(name="Name") - long_name: StringProperty(name="Long Name") - has_decomposition: BoolProperty(name="Has Decomposition") - ifc_definition_id: IntProperty(name="IFC Definition ID") - is_selected: BoolProperty(name="Is Selected") - - -class BIMSpatialProperties(PropertyGroup): - containers: CollectionProperty(name="Containers", type=SpatialElement) - active_container_index: IntProperty(name="Active Container Index") - active_container_id: IntProperty(name="Active Container Id") - - class BIMObjectSpatialProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") relating_container_object: PointerProperty( @@ -123,6 +111,7 @@ class Element(PropertyGroup): name: StringProperty(name="Name") is_class: BoolProperty(name="Is Class", default=False) is_type: BoolProperty(name="Is Type", default=False) + ifc_definition_id: IntProperty(name="IFC Definition ID") total: IntProperty(name="Total") @@ -144,3 +133,8 @@ class BIMSpatialDecompositionProperties(PropertyGroup): def active_container(self): if self.containers and self.active_container_index < len(self.containers): return self.containers[self.active_container_index] + + @property + def active_element(self): + if self.elements and self.active_element_index < len(self.elements): + return self.elements[self.active_element_index] diff --git a/src/blenderbim/blenderbim/bim/module/spatial/ui.py b/src/blenderbim/blenderbim/bim/module/spatial/ui.py index 9dbf4b4856..c571dbe76b 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/ui.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/ui.py @@ -36,27 +36,39 @@ class BIM_PT_spatial(Panel): return SpatialData.data["poll"] def draw(self, context): + # TODO: expose relating_container_object so users could + # assign container without switching default container back and forth + # just for 1 operation. + if not SpatialData.is_loaded: SpatialData.load() - props = context.scene.BIMSpatialProperties osprops = context.active_object.BIMObjectSpatialProperties if osprops.is_editing: - row = self.layout.row(align=True) - if SpatialData.data["parent_container_id"]: - op = row.operator("bim.change_spatial_level", text="", icon="FRAME_PREV") - op.parent = SpatialData.data["parent_container_id"] - if props.containers and props.active_container_index < len(props.containers): - op = row.operator("bim.assign_container", icon="CHECKMARK") - op.structure = props.containers[props.active_container_index].ifc_definition_id - row.operator("bim.reference_structure", icon="LINKED", text="") - row.operator("bim.dereference_structure", icon="UNLINKED", text="") - row.operator("bim.copy_to_container", icon="COPYDOWN", text="") - row.operator("bim.disable_editing_container", icon="CANCEL", text="") + if SpatialData.data["default_container"]: + row = self.layout.row(align=True) + row.label(text=f"Target: {SpatialData.data['default_container']}", icon="OUTLINER_COLLECTION") + row.operator("bim.assign_container", icon="CHECKMARK", text="Reassign Container") + row.operator("bim.disable_editing_container", icon="CANCEL", text="") - self.layout.template_list("BIM_UL_containers", "", props, "containers", props, "active_container_index") - self.layout.prop(osprops, "relating_container_object") + if SpatialData.data["selected_containers"]: + row = self.layout.row() + row.label(text=f"{len(SpatialData.data['selected_containers'])} Selected Containers") + for name in SpatialData.data["selected_containers"][:3]: + row = self.layout.row() + row.label(text=name, icon="OUTLINER_COLLECTION") + if len(SpatialData.data["selected_containers"]) > 3: + row = self.layout.row() + row.label(text=f"... {len(SpatialData.data['selected_containers']) - 3} More") + row = self.layout.row(align=True) + row.operator("bim.reference_structure", icon="LINKED", text="Reference Selected") + row.operator("bim.dereference_structure", icon="UNLINKED", text="") + row = self.layout.row() + row.operator("bim.copy_to_container", icon="COPYDOWN", text="Copy Object To Selected") + else: + row = self.layout.row() + row.label(text="No Selected Containers") else: row = self.layout.row(align=True) if SpatialData.data["label"]: @@ -69,26 +81,15 @@ class BIM_PT_spatial(Panel): else: row.label(text="No Spatial Container") row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="") - for reference in SpatialData.data["references"]: + + references = SpatialData.data["references"] + if references: + self.layout.label(text="Referenced In Structures:") + for reference in references: row = self.layout.row() row.label(text=reference, icon="LINKED") - - -class BIM_UL_containers(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - if item.has_decomposition: - op = layout.operator("bim.change_spatial_level", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) - op.parent = item.ifc_definition_id - layout.label(text=item.name) - layout.label(text=item.long_name) - layout.prop( - item, - "is_selected", - icon="CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT", - text="", - emboss=False, - ) + else: + self.layout.label(text="No References In Structures") class BIM_PT_spatial_decomposition(Panel): @@ -109,7 +110,7 @@ class BIM_PT_spatial_decomposition(Panel): SpatialDecompositionData.load() self.props = context.scene.BIMSpatialDecompositionProperties - if SpatialDecompositionData.data['default_container']: + if SpatialDecompositionData.data["default_container"]: row = self.layout.row(align=True) row.label( text=f"Default: {SpatialDecompositionData.data['default_container']}", @@ -161,15 +162,23 @@ class BIM_PT_spatial_decomposition(Panel): if not self.props.total_elements: row = self.layout.row(align=True) - row.label(text="No Contained Elements", icon="FILE_3D") + row.label(text=f"{self.props.active_container.ifc_class} > No Contained Elements", icon="FILE_3D") row.prop(self.props, "should_include_children", text="", icon="OUTLINER") return row = self.layout.row(align=True) - row.label(text=f"{self.props.total_elements} Contained Elements", icon="FILE_3D") + row.label( + text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Contained Elements", + icon="FILE_3D", + ) row.prop(self.props, "should_include_children", text="", icon="OUTLINER") op = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="") - op.ifc_class = self.props.active_container.ifc_class + if self.props.active_element.is_class: + op.ifc_class = self.props.active_element.name + op.relating_type = 0 + elif self.props.active_element.is_type: + op.ifc_class = "" + op.relating_type = self.props.active_element.ifc_definition_id op.container = ifc_definition_id self.layout.template_list( @@ -187,7 +196,20 @@ class BIM_UL_containers_manager(UIList): if item: row = layout.row(align=True) self.draw_hierarchy(row, item) - row.prop(item, "name", emboss=False, text="") + icon = { + "IfcProject": "FILE", + "IfcSite": "WORLD", + "IfcBuilding": "HOME", + "IfcBuildingStorey": "LINENUMBERS_OFF", + "IfcSpace": "ANTIALIASED", + "IfcFacilityPart": "MOD_FLUID", + "IfcBridgePart": "MOD_FLUID", + "IfcFacilityPartCommon": "MOD_FLUID", + "IfcMarinePart": "MOD_FLUID", + "IfcRailwayPart": "MOD_FLUID", + "IfcRoadPart": "MOD_FLUID", + }.get(item.ifc_class, "META_PLANE") + row.prop(item, "name", emboss=False, text="", icon=icon) if item.long_name: row.prop(item, "long_name", emboss=False, text="") col = row.column() @@ -208,24 +230,6 @@ class BIM_UL_containers_manager(UIList): ) else: row.label(text="", icon="BLANK1") - if item.ifc_class == "IfcProject": - row.label(text="", icon="FILE") - else: - icon = { - "IfcSite": "WORLD", - "IfcBuilding": "HOME", - "IfcBuildingStorey": "LINENUMBERS_OFF", - "IfcSpace": "ANTIALIASED", - "IfcFacilityPart": "MOD_FLUID", - "IfcBridgePart": "MOD_FLUID", - "IfcFacilityPartCommon": "MOD_FLUID", - "IfcMarinePart": "MOD_FLUID", - "IfcRailwayPart": "MOD_FLUID", - "IfcRoadPart": "MOD_FLUID", - }.get(item.ifc_class, "META_PLANE") - op = row.operator("bim.select_decomposed_elements", icon=icon, text="", emboss=False) - op.ifc_class = item.ifc_class - op.container = item.ifc_definition_id class BIM_UL_elements(UIList): @@ -240,7 +244,6 @@ class BIM_UL_elements(UIList): col.label(text=str(item.total)) elif item.is_type: row.label(text="", icon="BLANK1") - row.label(text="", icon="DISCLOSURE_TRI_DOWN") row.label(text=item.name) col = row.column() col.alignment = "RIGHT" diff --git a/src/blenderbim/blenderbim/bim/module/structural/data.py b/src/blenderbim/blenderbim/bim/module/structural/data.py index 2d08c5a5e0..5c649a65b0 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/data.py +++ b/src/blenderbim/blenderbim/bim/module/structural/data.py @@ -226,7 +226,7 @@ class StructuralLoadsData: cls.data = { "total_loads": cls.total_loads(), "load_classes": cls.load_classes(), - "structural_load_types": cls.structural_load_types() + "structural_load_types": cls.structural_load_types(), } cls.is_loaded = True diff --git a/src/blenderbim/blenderbim/bim/module/structural/prop.py b/src/blenderbim/blenderbim/bim/module/structural/prop.py index 6c801f583f..396b9228bd 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/prop.py +++ b/src/blenderbim/blenderbim/bim/module/structural/prop.py @@ -42,9 +42,9 @@ def get_applicable_structural_load_types(self, context): def updateApplicableStructuralLoadTypes(self, context): - StructuralLoadCasesData.data[ - "applicable_structural_load_types" - ] = StructuralLoadCasesData.applicable_structural_load_types() + StructuralLoadCasesData.data["applicable_structural_load_types"] = ( + StructuralLoadCasesData.applicable_structural_load_types() + ) def get_applicable_structural_loads(self, context): diff --git a/src/blenderbim/blenderbim/bim/module/structural/ui.py b/src/blenderbim/blenderbim/bim/module/structural/ui.py index f92cd69bd5..19026a5399 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/ui.py +++ b/src/blenderbim/blenderbim/bim/module/structural/ui.py @@ -388,9 +388,9 @@ class BIM_PT_structural_load_cases(Panel): elif self.props.active_load_case_id: row.operator("bim.remove_structural_load_case", text="", icon="X").load_case = load_case["id"] else: - row.operator( - "bim.enable_editing_structural_load_case_groups", text="", icon="GHOST_ENABLED" - ).load_case = load_case["id"] + row.operator("bim.enable_editing_structural_load_case_groups", text="", icon="GHOST_ENABLED").load_case = ( + load_case["id"] + ) row.operator("bim.enable_editing_structural_load_case", text="", icon="GREASEPENCIL").load_case = load_case[ "id" ] @@ -547,9 +547,9 @@ class BIM_PT_boundary_conditions(Panel): row = self.layout.row(align=True) prop_with_search(row, self.props, "boundary_condition_types", text="") - row.operator( - "bim.add_boundary_condition", text="", icon="ADD" - ).ifc_class = self.props.boundary_condition_types + row.operator("bim.add_boundary_condition", text="", icon="ADD").ifc_class = ( + self.props.boundary_condition_types + ) else: row.operator("bim.load_boundary_conditions", text="", icon="GREASEPENCIL") diff --git a/src/blenderbim/blenderbim/bim/module/style/data.py b/src/blenderbim/blenderbim/bim/module/style/data.py index 7bcb05482b..0970221caa 100644 --- a/src/blenderbim/blenderbim/bim/module/style/data.py +++ b/src/blenderbim/blenderbim/bim/module/style/data.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell +import ifcopenshell.util.schema import blenderbim.tool as tool from ifcopenshell.util.doc import get_entity_doc from typing import Union diff --git a/src/blenderbim/blenderbim/bim/module/style/operator.py b/src/blenderbim/blenderbim/bim/module/style/operator.py index e94e925404..da5079aae6 100644 --- a/src/blenderbim/blenderbim/bim/module/style/operator.py +++ b/src/blenderbim/blenderbim/bim/module/style/operator.py @@ -535,22 +535,22 @@ class RemoveTextureMap(bpy.types.Operator): return {"FINISHED"} -class EnableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator): +class EnableAddingPresentationStyle(bpy.types.Operator): bl_idname = "bim.enable_adding_presentation_style" bl_label = "Enable Add Presentation Style" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): - style_type = bpy.context.scene.BIMStylesProperties.style_type + style_type = tool.Style.get_active_style_type() if style_type != "IfcSurfaceStyle": cls.poll_message_set(f"Adding {style_type} is not currently supported.") return False return True - def _execute(self, context): - props = bpy.context.scene.BIMStylesProperties - props.is_adding = True + def execute(self, context): + tool.Style.enable_adding_presentation_style() + return {"FINISHED"} class DuplicateStyle(bpy.types.Operator, tool.Ifc.Operator): @@ -569,14 +569,14 @@ class DuplicateStyle(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.load_styles(style_type=style_type) -class DisableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator): +class DisableAddingPresentationStyle(bpy.types.Operator): bl_idname = "bim.disable_adding_presentation_style" bl_label = "Disable Add Presentation Style" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): - props = bpy.context.scene.BIMStylesProperties - props.is_adding = False + def execute(self, context): + tool.Style.disable_adding_presentation_style() + return {"FINISHED"} class AddPresentationStyle(bpy.types.Operator, tool.Ifc.Operator): @@ -629,7 +629,7 @@ class AddPresentationStyle(bpy.types.Operator, tool.Ifc.Operator): else: self.report({"INFO"}, f"Adding style of type {props.style_type} is not yet supported.") - props.is_adding = False + tool.Style.disable_adding_presentation_style() core.load_styles(tool.Style, style_type=props.style_type) diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index 4ad2922074..bc34669380 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -191,9 +191,9 @@ class BIM_PT_ports(Panel): if connected_obj_name: connected_obj = bpy.data.objects[connected_obj_name] cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port.id() - cols[4].operator( - "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" - ).ifc_id = connected_obj.BIMObjectProperties.ifc_definition_id + cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( + connected_obj.BIMObjectProperties.ifc_definition_id + ) cols[5].label(text=connected_obj_name) else: cols[3].label(text="", icon="UNLINKED") @@ -248,9 +248,9 @@ class BIM_PT_port(Panel): relating_object = bpy.data.objects[relating_object_name] row.label(text="Port located on:") row.label(text=relating_object_name) - row.operator( - "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" - ).ifc_id = relating_object.BIMObjectProperties.ifc_definition_id + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( + relating_object.BIMObjectProperties.ifc_definition_id + ) # object connected to the port row = layout.row(align=True) @@ -259,9 +259,9 @@ class BIM_PT_port(Panel): connected_object = bpy.data.objects[connected_object_name] row.label(text="Port connected to:") row.label(text=connected_object_name) - row.operator( - "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" - ).ifc_id = connected_object.BIMObjectProperties.ifc_definition_id + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( + connected_object.BIMObjectProperties.ifc_definition_id + ) else: row.label(text="Port is not connected to any element") @@ -308,9 +308,9 @@ class BIM_PT_flow_controls(Panel): op.flow_control = control_id op.flow_element = flow_element_id op.assign = False - row.operator( - "bim.select_entity", text="", icon="RESTRICT_SELECT_OFF" - ).ifc_id = displayed_object.BIMObjectProperties.ifc_definition_id + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( + displayed_object.BIMObjectProperties.ifc_definition_id + ) row.label(text=f"{displayed_object_name}") element = tool.Ifc.get_entity(context.active_object) diff --git a/src/blenderbim/blenderbim/bim/module/tester/__init__.py b/src/blenderbim/blenderbim/bim/module/tester/__init__.py index 12f30eb019..fa3ab04089 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/tester/__init__.py @@ -21,8 +21,6 @@ from . import ui, prop, operator classes = ( operator.ExecuteIfcTester, - operator.SelectSpecs, - operator.SelectIfcTesterIfcFile, operator.SelectRequirement, operator.SelectFailedEntities, operator.SelectEntity, @@ -32,7 +30,7 @@ classes = ( prop.IfcTesterProperties, ui.BIM_PT_tester, ui.BIM_UL_tester_specifications, - ui.BIM_UL_tester_failed_entities + ui.BIM_UL_tester_failed_entities, ) @@ -42,4 +40,3 @@ def register(): def unregister(): del bpy.types.Scene.IfcTesterProperties - diff --git a/src/blenderbim/blenderbim/bim/module/tester/operator.py b/src/blenderbim/blenderbim/bim/module/tester/operator.py index 2885617055..27cc2f4523 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/operator.py +++ b/src/blenderbim/blenderbim/bim/module/tester/operator.py @@ -27,7 +27,6 @@ import ifctester.reporter import ifcopenshell import blenderbim.tool as tool import blenderbim.bim.handler -from blenderbim.bim.operator import MultipleFileSelector from pathlib import Path @@ -38,7 +37,7 @@ class ExecuteIfcTester(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): props = context.scene.IfcTesterProperties - return (props.ifc_file or props.should_load_from_memory) and props.specs + return (props.ifc_files.single_file or props.should_load_from_memory) and props.specs def execute(self, context): props = context.scene.IfcTesterProperties @@ -49,13 +48,13 @@ class ExecuteIfcTester(bpy.types.Operator, tool.Ifc.Operator): ifc_data = tool.Ifc.get() ifc_path = tool.Ifc.get_path() - for f in props.specs_files: + for f in props.specs.file_list: self.execute_tester(ifc_data, ifc_path, f.name) else: - for ifc_file in props.ifc_files: + for ifc_file in props.ifc_files.file_list: ifc_data = ifcopenshell.open(ifc_file.name) - for f in props.specs_files: + for f in props.specs.file_list: self.execute_tester(ifc_data, ifc_file.name, f.name) blenderbim.bim.handler.refresh_ui_data() @@ -103,33 +102,6 @@ class ExecuteIfcTester(bpy.types.Operator, tool.Ifc.Operator): new_spec.status = spec["status"] -class SelectSpecs(MultipleFileSelector): - bl_idname = "bim.select_specs" - bl_label = "Select IDS" - filename_ext = ".ids" - filter_glob: bpy.props.StringProperty(default="*.ids;*.xml", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - props = context.scene.IfcTesterProperties - - self.update_props(props, "specs", props.specs_files) - return {"FINISHED"} - - -class SelectIfcTesterIfcFile(MultipleFileSelector): - bl_idname = "bim.select_ifctester_ifc_file" - bl_label = "Select IfcTester IFC File" - filename_ext = ".ifc" - filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - - def execute(self, context): - props = context.scene.IfcTesterProperties - - self.update_props(props, "ifc_file", props.ifc_files) - return {"FINISHED"} - - class SelectRequirement(bpy.types.Operator): bl_idname = "bim.select_requirement" bl_label = "Select Specification" diff --git a/src/blenderbim/blenderbim/bim/module/tester/prop.py b/src/blenderbim/blenderbim/bim/module/tester/prop.py index bc2f65faa1..7c496c7d84 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/prop.py +++ b/src/blenderbim/blenderbim/bim/module/tester/prop.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . from blenderbim.bim.module.tester.data import TesterData -from blenderbim.bim.prop import StrProperty +from blenderbim.bim.prop import StrProperty, MultipleFileSelect from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -48,10 +48,8 @@ class FailedEntities(PropertyGroup): class IfcTesterProperties(PropertyGroup): - specs: StringProperty(default="", name="IDS File(s)") - specs_files: CollectionProperty(name="IDS Files", type=StrProperty) - ifc_file: StringProperty(default="", name="IFC File(s)") - ifc_files: CollectionProperty(name="IFC Files", type=StrProperty) + specs: PointerProperty(type=MultipleFileSelect) + ifc_files: PointerProperty(type=MultipleFileSelect) should_load_from_memory: BoolProperty(default=False, name="Load from Memory", options=set()) generate_html_report: BoolProperty(default=False, name="Generate HTML report", options=set()) generate_ods_report: BoolProperty(default=False, name="Generate ODS report", options=set()) diff --git a/src/blenderbim/blenderbim/bim/module/tester/ui.py b/src/blenderbim/blenderbim/bim/module/tester/ui.py index e864580691..29b2f35eb9 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/ui.py +++ b/src/blenderbim/blenderbim/bim/module/tester/ui.py @@ -50,19 +50,10 @@ class BIM_PT_tester(Panel): if not tool.Ifc.get() or not props.should_load_from_memory: row = self.layout.row(align=True) - if len(props.ifc_files) > 1: - row.label(text=f"{len(props.ifc_files)} Files Selected") - else: - row.prop(props, "ifc_file") - - row.operator("bim.select_ifctester_ifc_file", icon="FILE_FOLDER", text="") + props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)") row = self.layout.row(align=True) - if len(props.specs_files) > 1: - row.label(text=f"{len(props.specs_files)} Files Selected") - else: - row.prop(props, "specs") - row.operator("bim.select_specs", icon="FILE_FOLDER", text="") + props.specs.layout_file_select(row, "*.ids;*.xml", "IDS File(s)") row = self.layout.row() row.operator("bim.execute_ifc_tester") @@ -98,7 +89,7 @@ class BIM_PT_tester(Panel): row = box.row(align=True) row.label(text=requirement["description"], icon="CHECKMARK" if requirement["status"] else "CANCEL") if not requirement["status"]: - op = row.operator("bim.select_requirement", text="", icon="LONGDISPLAY") + op = row.operator("bim.select_requirement", text="", icon="LONGDISPLAY") op2 = row.operator("bim.select_failed_entities", text="", icon="RESTRICT_SELECT_OFF") op.spec_index = props.active_specification_index op.req_index = i diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index f368f9a73b..80c48dbc99 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -567,5 +567,5 @@ class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - purged_types = core.purge_unused_types(tool.Ifc, tool.Type) + purged_types = core.purge_unused_types(tool.Ifc, tool.Type, tool.Geometry) self.report({"INFO"}, f"{purged_types} types were purged.") diff --git a/src/blenderbim/blenderbim/bim/module/type/ui.py b/src/blenderbim/blenderbim/bim/module/type/ui.py index 6275ae52ef..8949b77bb3 100644 --- a/src/blenderbim/blenderbim/bim/module/type/ui.py +++ b/src/blenderbim/blenderbim/bim/module/type/ui.py @@ -88,7 +88,7 @@ class BIM_PT_type(Panel): if TypeData.data["relating_type"]: row.label(text=TypeData.data["relating_type"]["name"]) op = row.operator("bim.select_type", icon="OBJECT_DATA", text="") - op.relating_type = 0 #will only select the relating types of only the selected objects + op.relating_type = 0 # will only select the relating types of only the selected objects row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="") row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="") row.operator("bim.unassign_type", icon="X", text="") diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index e68f818176..164038b1df 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -83,6 +83,7 @@ class SwitchTab(bpy.types.Operator): class OpenUri(bpy.types.Operator): bl_idname = "bim.open_uri" bl_label = "Open URI" + bl_description = "Open the URL in your Web Browser" uri: bpy.props.StringProperty() def execute(self, context): @@ -141,26 +142,35 @@ class SelectURIAttribute(bpy.types.Operator): context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} -class MultipleFileSelector(bpy.types.Operator): - bl_label = "Select Multiple Files" + +class BIM_OT_multiple_file_selector(bpy.types.Operator): + """Open Blender's file explorer to select one or multiple files.""" + + bl_idname = "bim.multiple_file_selector" + bl_label = "Select File(s)" bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement) + filter_glob: bpy.props.StringProperty(default="*", options={"HIDDEN"}) + filepath: bpy.props.StringProperty(subtype="FILE_PATH") - def update_props(self, props, single_file: str, file_list: bpy.types.CollectionProperty): + @classmethod + def poll(self, context): + return getattr(context, "file_props", None) is not None + + def execute(self, context): dirname = os.path.dirname(self.filepath) - file_list.clear() - - for f in self.files: - new = file_list.add() - new.name = os.path.join(dirname, f.name) - setattr(props, single_file, self.filepath) + self.file_props.single_file = self.filepath + self.file_props.set_file_list(dirname, [f.name for f in self.files]) + return {"FINISHED"} def invoke(self, context, event): + self.file_props = context.file_props context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} + class SelectIfcFile(bpy.types.Operator, IFCFileSelector): bl_idname = "bim.select_ifc_file" bl_label = "Select IFC File" @@ -456,7 +466,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator): group.interface.new_socket(name="Line Decorator", in_out="OUTPUT", socket_type="NodeSocketFloat") else: group.inputs.new("NodeSocketFloat", "Value") - group.inputs["Value"].default_value = 1.0 # Mandatory multiplier for the last node group + # Mandatory multiplier for the last node group + group.inputs["Value"].default_value = 1.0 group.inputs.new("NodeSocketVector", "Vector") group.inputs.new("NodeSocketFloat", "Line Decorator") group.outputs.new("NodeSocketFloat", "Value") @@ -524,12 +535,14 @@ class BIM_OT_add_section_plane(bpy.types.Operator): mix_decorator = group.nodes.new(type="ShaderNodeMixShader") mix_decorator.name = "Line Decorator Mix" - mix_decorator.inputs[0].default_value = 0 # Directly pass input shader when there is no cutaway + # Directly pass input shader when there is no cutaway + mix_decorator.inputs[0].default_value = 0 mix_decorator.location = group_output.location - Vector((200, 0)) mix_section = group.nodes.new(type="ShaderNodeMixShader") mix_section.name = "Section Mix" - mix_section.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway + # Directly pass input shader when there is no cutaway + mix_section.inputs[0].default_value = 1 mix_section.location = mix_decorator.location - Vector((200, 200)) transparent = nodes.new(type="ShaderNodeBsdfTransparent") @@ -810,20 +823,6 @@ class RemoveIfcFile(bpy.types.Operator): return {"FINISHED"} -class BIM_OT_open_webbrowser(bpy.types.Operator): - bl_idname = "bim.open_webbrowser" - bl_description = "Open the URL in your Web Browser" - bl_label = "Open URL" - - url: bpy.props.StringProperty() - - def execute(self, context): - import webbrowser - - webbrowser.open(self.url) - return {"FINISHED"} - - class FetchObjectPassport(bpy.types.Operator): bl_idname = "bim.fetch_object_passport" bl_label = "Fetch Object Passport" @@ -985,8 +984,8 @@ class BIM_OT_show_description(bpy.types.Operator): for line in wrapper.wrap(self.attr_name + " : " + self.description): layout.label(text=line) if self.url: - url_op = layout.operator("bim.open_webbrowser", icon="URL", text="Online IFC Documentation") - url_op.url = self.url + url_op = layout.operator("bim.open_uri", icon="URL", text="Online IFC Documentation") + url_op.uri = self.url @classmethod def description(cls, context, properties): diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index b94dc4ef68..9204446711 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -156,6 +156,34 @@ class ObjProperty(PropertyGroup): obj: bpy.props.PointerProperty(type=bpy.types.Object) +def update_single_file(self, context): + self.file_list.clear() + new = self.file_list.add() + new.name = self.single_file + + +class MultipleFileSelect(PropertyGroup): + single_file: bpy.props.StringProperty(name="Single File Path", description="", update=update_single_file) + file_list: bpy.props.CollectionProperty(type=StrProperty) + + def set_file_list(self, dirname: str, files: list[str]): + self.file_list.clear() + + for f in files: + new = self.file_list.add() + new.name = os.path.join(dirname, f) + + def layout_file_select(self, layout, filter_glob="", text=""): + if len(self.file_list) > 1: + layout.label(text=f"{len(self.file_list)} Files Selected") + else: + layout.prop(self, "single_file", text=text) + + layout.context_pointer_set("file_props", self) + op = layout.operator("bim.multiple_file_selector", icon="FILE_FOLDER", text="") + op.filter_glob = filter_glob + + def update_attribute_value(self, context): value_name = self.get_value_name() if value_name: @@ -248,6 +276,8 @@ class Attribute(PropertyGroup): enum_items: StringProperty(name="Value") enum_descriptions: CollectionProperty(type=StrProperty) enum_value: EnumProperty(items=get_attribute_enum_values, name="Value", update=update_attribute_value) + filepath_value: PointerProperty(type=MultipleFileSelect) + filter_glob: StringProperty() is_null: BoolProperty(name="Is Null", update=update_is_null) is_optional: BoolProperty(name="Is Optional") is_uri: BoolProperty(name="Is Uri", default=False) @@ -263,6 +293,8 @@ class Attribute(PropertyGroup): return None if self.data_type == "string": return self.string_value.replace("\\n", "\n") + if self.data_type == "file": + return [f.name for f in self.filepath_value.file_list] return getattr(self, str(self.get_value_name()), None) def get_value_default(self): @@ -276,6 +308,8 @@ class Attribute(PropertyGroup): return False elif self.data_type == "enum": return "0" + elif self.data_type == "file": + return "" def get_value_name(self, display_only=False): if self.data_type == "string": @@ -290,6 +324,8 @@ class Attribute(PropertyGroup): return "float_value" elif self.data_type == "enum": return "enum_value" + elif self.data_type == "file": + return "filepath_value" def set_value(self, value): if isinstance(value, str): diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 360f351759..e2c3679f32 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -1110,5 +1110,5 @@ def draw_custom_context_menu(self, context): url = docs.get("spec_url", "") if url: layout.separator() - url_op = layout.operator("bim.open_webbrowser", icon="URL", text="Online IFC Documentation") - url_op.url = url + url_op = layout.operator("bim.open_uri", icon="URL", text="Online IFC Documentation") + url_op.uri = url diff --git a/src/blenderbim/blenderbim/core/aggregate.py b/src/blenderbim/blenderbim/core/aggregate.py index 00fd67b3c8..a8773b8536 100644 --- a/src/blenderbim/blenderbim/core/aggregate.py +++ b/src/blenderbim/blenderbim/core/aggregate.py @@ -45,9 +45,7 @@ def assign_object( relating_object = ifc.get_entity(relating_obj) if aggregator.has_physical_body_representation(relating_object): raise AggregateRepresentationError - rel = ifc.run( - "aggregate.assign_object", products=[ifc.get_entity(related_obj)], relating_object=relating_object - ) + rel = ifc.run("aggregate.assign_object", products=[ifc.get_entity(related_obj)], relating_object=relating_object) collector.assign(relating_obj) collector.assign(related_obj) aggregator.disable_editing(related_obj) diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index 58e7afed18..c31586ebd7 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -133,7 +133,11 @@ def convert_ifc_to_brick(brick, namespace=None, library=None): for element in distribution_elements: feeds = brick.get_element_feeds(element) for downstream_equipment in feeds: - brick.add_relation(equipment_uris[element], "https://brickschema.org/schema/Brick#feeds", equipment_uris[downstream_equipment]) + brick.add_relation( + equipment_uris[element], + "https://brickschema.org/schema/Brick#feeds", + equipment_uris[downstream_equipment], + ) brick.run_refresh_brick_viewer() diff --git a/src/blenderbim/blenderbim/core/bsdd.py b/src/blenderbim/blenderbim/core/bsdd.py index cb55e07b8a..b18eae23c5 100644 --- a/src/blenderbim/blenderbim/core/bsdd.py +++ b/src/blenderbim/blenderbim/core/bsdd.py @@ -1,30 +1,58 @@ -def get_class_properties(client, bsdd): +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Any + +if TYPE_CHECKING: + import bpy + import ifcopenshell + import bsdd + import blenderbim.tool as tool + + +def get_class_properties(client: bsdd.Client, bsdd: tool.Bsdd) -> dict[str, dict[str, Any]]: bsdd.clear_class_psets() data = bsdd.get_active_class_data(client) pset_dict = bsdd.get_property_dict(data) if pset_dict is None: - return + return {} bsdd.create_class_psets(pset_dict) + return pset_dict -def load_bsdd(client, bsdd): +def load_bsdd(client: bsdd.Client, bsdd: tool.Bsdd) -> None: bsdd.clear_domains() - if bsdd.should_load_preview_domains: + if bsdd.should_load_preview_domains(): dictionaries = bsdd.get_dictionaries(client) else: dictionaries = bsdd.get_dictionaries(client, "Active") bsdd.create_dictionaries(dictionaries) -def search_class(keyword: str, client, bsdd): +def search_class(keyword: str, client: bsdd.Client, bsdd: tool.Bsdd) -> int: bsdd.clear_classes() - if len(keyword) < 3: - return related_entities = bsdd.get_related_ifc_entities(keyword) active_dictionary_uri = bsdd.get_active_dictionary_uri() - classes: dict = bsdd.search_class(client, keyword, [active_dictionary_uri], related_entities) + classes = bsdd.search_class(client, keyword, [active_dictionary_uri], related_entities) bsdd.create_classes(classes) + return len(classes) -def set_active_bsdd_dictionary(name: str, uri: str, bsdd): +def set_active_bsdd_dictionary(name: str, uri: str, bsdd: tool.Bsdd) -> None: bsdd.set_active_bsdd(name, uri) diff --git a/src/blenderbim/blenderbim/core/covering.py b/src/blenderbim/blenderbim/core/covering.py index fd85fa4aed..123da50899 100644 --- a/src/blenderbim/blenderbim/core/covering.py +++ b/src/blenderbim/blenderbim/core/covering.py @@ -16,8 +16,16 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +from __future__ import annotations +from typing import TYPE_CHECKING, Optional -def add_instance_flooring_covering_from_cursor(ifc, root, spatial): +if TYPE_CHECKING: + import bpy + import ifcopenshell + import blenderbim.tool as tool + + +def add_instance_flooring_covering_from_cursor(ifc: tool.Ifc, root: tool.Root, spatial: tool.Spatial) -> None: if not root.get_default_container(): raise NoDefaultContainer() @@ -32,7 +40,7 @@ def add_instance_flooring_covering_from_cursor(ifc, root, spatial): relating_type = None if selected_objects and active_obj: - x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) + x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj) else: x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() @@ -59,7 +67,9 @@ def add_instance_flooring_covering_from_cursor(ifc, root, spatial): spatial.regen_obj_representation(obj, body) -def add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial): +def add_instance_ceiling_covering_from_cursor( + ifc: tool.Ifc, root: tool.Root, covering: tool.Covering, spatial: tool.Spatial +) -> None: if not root.get_default_container(): raise NoDefaultContainer() @@ -74,7 +84,7 @@ def add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial): relating_type = None if selected_objects and active_obj: - x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) + x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj) else: x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() ceiling_height = covering.get_z_from_ceiling_height() @@ -101,7 +111,7 @@ def add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial): spatial.regen_obj_representation(obj, body) -def regen_selected_covering_object(root, spatial): +def regen_selected_covering_object(root: tool.Root, spatial: tool.Spatial) -> None: if not root.get_default_container(): raise NoDefaultContainer() @@ -109,7 +119,7 @@ def regen_selected_covering_object(root, spatial): selected_objects = spatial.get_selected_objects() if selected_objects and active_obj: - x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) + x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj) space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y) @@ -132,7 +142,7 @@ def regen_selected_covering_object(root, spatial): # TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS -def add_instance_flooring_coverings_from_walls(root, spatial): +def add_instance_flooring_coverings_from_walls(root: tool.Root, spatial: tool.Spatial) -> None: if not root.get_default_container(): raise NoDefaultContainer() @@ -158,7 +168,7 @@ def add_instance_flooring_coverings_from_walls(root, spatial): spatial.regen_obj_representation(obj, body) -def add_instance_ceiling_coverings_from_walls(root, spatial, covering): +def add_instance_ceiling_coverings_from_walls(root: tool.Root, spatial: tool.Spatial, covering: tool.Covering) -> None: if not root.get_default_container(): raise NoDefaultContainer() diff --git a/src/blenderbim/blenderbim/core/demo.py b/src/blenderbim/blenderbim/core/demo.py index a31be242d4..6de3f15adc 100644 --- a/src/blenderbim/blenderbim/core/demo.py +++ b/src/blenderbim/blenderbim/core/demo.py @@ -40,6 +40,7 @@ # described in regular sentences in under 50 lines. That is the purpose of the # core. + # Here's the simplest possible core function. It does one thing only. Remember: # core functions delegate tasks to tools, so all core functions need at least # one tool. diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index f7e3c23d55..a8c19fa42a 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -18,11 +18,12 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: import bpy import ifcopenshell + import ifcopenshell.util.representation import blenderbim.tool as tool @@ -225,7 +226,11 @@ def disable_editing_drawings(drawing: tool.Drawing) -> None: def add_drawing( - ifc: tool.Ifc, collector: tool.Collector, drawing: tool.Drawing, target_view=None, location_hint=None + ifc: tool.Ifc, + collector: tool.Collector, + drawing: tool.Drawing, + target_view: Union[ifcopenshell.util.representation.TARGET_VIEW, None] = None, + location_hint: Union[str, None] = None, ) -> None: drawing_name = drawing.ensure_unique_drawing_name(drawing.generate_drawing_name(target_view, location_hint)) drawing_matrix = drawing.generate_drawing_matrix(target_view, location_hint) diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index 63ba4e6961..9861d5b1a2 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -126,8 +126,7 @@ def switch_representation( if not current_obj_data and geometry.is_text_literal(representation): return - use_immediate_repr = apply_openings and bool(getattr(entity, "HasOpenings", None)) - use_immediate_repr = use_immediate_repr or geometry.has_material_style_override(entity) + use_immediate_repr = geometry.should_use_immediate_representation(entity, apply_openings) if use_immediate_repr: # if it has openings make sure to switch to element's mapped representation representation = geometry.unresolve_type_representation(representation, entity) @@ -169,8 +168,11 @@ def get_representation_ifc_parameters( def remove_representation( ifc: tool.Ifc, geometry: tool.Geometry, obj: bpy.types.Object, representation: ifcopenshell.entity_instance ) -> None: - """Consider changing obj representation before using the function, - otherwise it will replace object with empty.""" + """Remove IFC representation from an object. + + If removed representation is active will automatically change it to some other one. + If it is the object's only representation, object will be recreated as an empty. + """ element = ifc.get_entity(obj) assert element diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py index 2d990c8d37..9453845067 100644 --- a/src/blenderbim/blenderbim/core/georeference.py +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -46,19 +46,11 @@ def edit_georeferencing(ifc, georeference): def get_cursor_location(georeference): - georeference.set_coordinates("local", georeference.get_cursor_location()) - - -def convert_local_to_global(georeference): - coordinates = georeference.xyz2enh(georeference.get_coordinates("local")) - georeference.set_coordinates("map", coordinates) - georeference.set_cursor_location() - - -def convert_global_to_local(georeference): - coordinates = georeference.enh2xyz(georeference.get_coordinates("map")) - georeference.set_coordinates("local", coordinates) - georeference.set_cursor_location() + location = georeference.get_cursor_location() + if georeference.has_blender_offset: + georeference.set_coordinates("blender", location) + else: + georeference.set_coordinates("local", location) def convert_angle_to_coord(georeference, type): diff --git a/src/blenderbim/blenderbim/core/material.py b/src/blenderbim/blenderbim/core/material.py index 8c12d2b8c5..fba7082cc1 100644 --- a/src/blenderbim/blenderbim/core/material.py +++ b/src/blenderbim/blenderbim/core/material.py @@ -118,7 +118,6 @@ def assign_material( continue ifc.run("material.assign_material", products=[element], type=material_type, material=material) - material_tool.ensure_material_assigned(elements=[element], material_type=material_type, material=material) assigned_material = material_tool.get_material(element) assert assigned_material # Type checker. @@ -128,6 +127,7 @@ def assign_material( material_tool.add_material_to_set(material_set=material, material=default_material) elif material_tool.is_a_material_set(assigned_material): material_tool.add_material_to_set(material_set=assigned_material, material=material) + material_tool.ensure_material_assigned(elements=[element], material_type=material_type, material=material) def unassign_material(ifc: tool.Ifc, material_tool: tool.Material, objects: list[bpy.types.Object]) -> None: diff --git a/src/blenderbim/blenderbim/core/project.py b/src/blenderbim/blenderbim/core/project.py index ecf04d1c01..df8909df5f 100644 --- a/src/blenderbim/blenderbim/core/project.py +++ b/src/blenderbim/blenderbim/core/project.py @@ -25,7 +25,14 @@ if TYPE_CHECKING: import blenderbim.tool as tool -def create_project(ifc: tool.Ifc, georeference: tool.Georeference, project: tool.Project, spatial: tool.Spatial, schema: str, template: Optional[str] = None) -> None: +def create_project( + ifc: tool.Ifc, + georeference: tool.Georeference, + project: tool.Project, + spatial: tool.Spatial, + schema: str, + template: Optional[str] = None, +) -> None: if ifc.get(): return diff --git a/src/blenderbim/blenderbim/core/pset.py b/src/blenderbim/blenderbim/core/pset.py index c21b59ccf5..ab2370db38 100644 --- a/src/blenderbim/blenderbim/core/pset.py +++ b/src/blenderbim/blenderbim/core/pset.py @@ -17,7 +17,24 @@ # along with BlenderBIM Add-on. If not, see . -def copy_property_to_selection(ifc, pset, is_pset=True, obj=None, pset_name=None, prop_name=None, prop_value=None): +from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Union, Any + +if TYPE_CHECKING: + import bpy + import ifcopenshell + import blenderbim.tool as tool + + +def copy_property_to_selection( + ifc: tool.Ifc, + pset: tool.Pset, + obj: bpy.types.Object, + pset_name: str, + prop_name: str, + prop_value: Any, + is_pset: bool = True, +) -> None: element = ifc.get_entity(obj) if not element: return @@ -30,7 +47,9 @@ def copy_property_to_selection(ifc, pset, is_pset=True, obj=None, pset_name=None ifc.run("pset.edit_qto", qto=ifc_pset, properties={prop_name: prop_value}) -def add_pset(ifc, pset, blender, obj_name, obj_type): +def add_pset( + ifc: tool.Ifc, pset: tool.Pset, blender: tool.Blender, obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE +) -> None: pset_name = pset.get_pset_name(obj_name, obj_type, pset_type="PSET") if obj_type == "Object": elements = [ifc.get_entity(obj) for obj in blender.get_selected_objects()] @@ -44,7 +63,14 @@ def add_pset(ifc, pset, blender, obj_name, obj_type): pset.enable_pset_editing(pset_id=0, pset_name=pset_name, pset_type="PSET", obj=obj_name, obj_type=obj_type) -def enable_pset_editing(pset_tool, pset, pset_name, pset_type, obj_name, obj_type): +def enable_pset_editing( + pset_tool: tool.Pset, + pset: ifcopenshell.entity_instance, + pset_name: str, + pset_type: tool.Pset.PSET_TYPE, + obj_name: str, + obj_type: tool.Ifc.OBJECT_TYPE, +) -> None: props = pset_tool.get_pset_props(obj_name, obj_type) pset_tool.clear_blender_pset_properties(props) @@ -63,6 +89,6 @@ def enable_pset_editing(pset_tool, pset, pset_name, pset_type, obj_name, obj_typ pset_tool.enable_proposed_pset(props, pset_name, pset_type, has_template) -def add_proposed_prop(pset, obj_name, obj_type, name, value): +def add_proposed_prop(pset: tool.Pset, obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE, name: str, value: Any) -> None: props = pset.get_pset_props(obj_name, obj_type) pset.add_proposed_property(name, pset.cast_string_to_primitive(value), props) diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index ed2ff20dff..7ff8457309 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -49,17 +49,13 @@ def assign_container( ifc: tool.Ifc, collector: tool.Collector, spatial: tool.Spatial, - structure_obj: Optional[bpy.types.Object] = None, + container: ifcopenshell.entity_instance, element_obj: Optional[bpy.types.Object] = None, ) -> Union[ifcopenshell.entity_instance, None]: - if not spatial.can_contain(structure_obj, element_obj): + if not spatial.can_contain(container, element_obj): return - assert element_obj and structure_obj # Type checker. - rel = ifc.run( - "spatial.assign_container", - products=[ifc.get_entity(element_obj)], - relating_structure=ifc.get_entity(structure_obj), - ) + assert element_obj # Type checker. + rel = ifc.run("spatial.assign_container", products=[ifc.get_entity(element_obj)], relating_structure=container) spatial.disable_editing(element_obj) collector.assign(element_obj) return rel @@ -67,17 +63,12 @@ def assign_container( def enable_editing_container(spatial: tool.Spatial, obj: bpy.types.Object) -> None: spatial.enable_editing(obj) - spatial.import_containers() def disable_editing_container(spatial: tool.Spatial, obj: bpy.types.Object) -> None: spatial.disable_editing(obj) -def change_spatial_level(spatial: tool.Spatial, parent: Union[ifcopenshell.entity_instance, None] = None) -> None: - spatial.import_containers(parent=parent) - - def remove_container(ifc: tool.Ifc, collector: tool.Collector, obj: bpy.types.Object) -> None: ifc.run("spatial.unassign_container", products=[ifc.get_entity(obj)]) collector.assign(obj) @@ -104,7 +95,7 @@ def copy_to_container( copied_obj = spatial.duplicate_object_and_data(obj) spatial.set_relative_object_matrix(copied_obj, to_container_obj, matrix) result_objs.append(spatial.run_root_copy_class(obj=copied_obj)) - spatial.run_spatial_assign_container(structure_obj=to_container_obj, element_obj=copied_obj) + spatial.run_spatial_assign_container(container=to_container, element_obj=copied_obj) spatial.disable_editing(obj) return result_objs @@ -149,8 +140,21 @@ def delete_container( spatial.import_spatial_decomposition() -def select_decomposed_elements(spatial: tool.Spatial, container: ifcopenshell.entity_instance) -> None: - spatial.select_products(spatial.get_decomposed_elements(container)) +def select_decomposed_elements( + spatial: tool.Spatial, + container: ifcopenshell.entity_instance, + ifc_class: str | None = None, + relating_type: ifcopenshell.entity_instance | None = None, + is_untyped: bool = False, +) -> None: + elements = spatial.get_decomposed_elements(container) + if ifc_class: + elements = spatial.filter_elements_by_class(elements, ifc_class) + elif relating_type: + elements = spatial.filter_elements_by_relating_type(elements, relating_type) + elif is_untyped: + elements = spatial.filter_elements_by_untyped(elements) + spatial.select_products(elements) def generate_space(ifc: tool.Ifc, model: tool.Model, root: tool.Root, spatial: tool.Spatial, type: tool.Type) -> None: @@ -169,7 +173,7 @@ def generate_space(ifc: tool.Ifc, model: tool.Model, root: tool.Root, spatial: t relating_type = None if selected_objects and active_obj: - x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) + x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj) element = ifc.get_entity(active_obj) else: x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() diff --git a/src/blenderbim/blenderbim/core/style.py b/src/blenderbim/blenderbim/core/style.py index 47f76b378d..b98c749c42 100644 --- a/src/blenderbim/blenderbim/core/style.py +++ b/src/blenderbim/blenderbim/core/style.py @@ -57,11 +57,14 @@ def update_external_style( def remove_style(ifc: tool.Ifc, style_tool: tool.Style, style: ifcopenshell.entity_instance) -> None: obj = ifc.get_object(style) + # Get style_type before removing object as later StylesData might fail to load + # due object not yet removed completely. + style_type = style_tool.get_active_style_type() ifc.unlink(element=style) ifc.run("style.remove_style", style=style) style_tool.delete_object(obj) if style_tool.is_editing_styles(): - style_tool.import_presentation_styles(style_tool.get_active_style_type()) + style_tool.import_presentation_styles(style_type) def update_style_colours(ifc: tool.Ifc, style: tool.Style, obj: bpy.types.Material, verbose: bool = False) -> None: diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 1b31080047..f5d3b55ea7 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -65,10 +65,12 @@ class Demo: @interface class Aggregate: - def can_aggregate(cls, relating_object, related_object): pass + def can_aggregate(cls, relating_obj, related_obj): pass + def has_physical_body_representation(cls, element): pass def disable_editing(cls, obj): pass def enable_editing(cls, obj): pass def get_container(cls, element): pass + def get_relating_object(cls, related_element): pass @interface @@ -139,6 +141,7 @@ class Brick: def clear_breadcrumbs(cls, split_screen=False): pass +@interface class Bsdd: def clear_class_psets(cls): pass def clear_classes(cls): pass @@ -167,6 +170,12 @@ class Clash: def look_at(cls, target, location): pass +@interface +class Classification: + def get_location(cls, classification): pass + def set_location(cls, classification): pass + + @interface class Collector: def assign(cls, obj): pass @@ -373,6 +382,7 @@ class Geometry: def clear_modifiers(cls, obj): pass def clear_scale(cls, obj): pass def delete_data(cls, data): pass + def delete_ifc_object(cls, obj): pass def does_representation_id_exist(cls, representation_id): pass def duplicate_object_data(cls, obj): pass def get_cartesian_point_coordinate_offset(cls, obj): pass @@ -413,6 +423,9 @@ class Geometry: def unresolve_type_representation(cls, representation, element): pass def delete_opening_object_placement(cls, opening): pass def switch_from_representation(cls, obj, representation): pass + def get_blender_offset_type(cls, obj): pass + def has_material_style_override(cls, obj): pass + def should_use_immediate_representation(cls, entity, apply_openings): pass @interface @@ -440,6 +453,7 @@ class Georeference: def set_cursor_location(cls, coordinates): pass def set_ifc_grid_north(cls): pass def set_ifc_true_north(cls): pass + def set_model_origin(cls): pass def set_vector_coordinates(cls, vector_coordinates, type): pass def set_wcs(cls, matrix): pass def xyz2enh(cls, coordinates): pass @@ -489,23 +503,30 @@ class Material: def add_material_to_set(cls, material_set, material): pass def disable_editing_material(cls): pass def disable_editing_materials(cls): pass + def duplicate_material(cls, material): pass def enable_editing_material(cls, material): pass def enable_editing_materials(cls): pass - def get_active_material(cls): pass + def ensure_material_assigned(cls, elements, material_type, material): pass + def ensure_material_unassigned(cls, elements): pass + def get_active_material_item(cls): pass def get_active_material_type(cls): pass - def get_active_object_material_type(cls, obj): pass + def get_default_material(cls): pass def get_elements_by_material(cls, material): pass - def get_material(cls, element, should_inherit): pass def get_material_attributes(cls): pass + def get_material(cls, element, should_inherit: bool = False): pass + def get_object_ui_active_material(cls): pass + def get_object_ui_material_type(cls): pass + def get_style(cls, material): pass def get_type(cls, element): pass def has_material_profile(cls, element): pass - def import_material_definitions(cls, material_type): pass + def import_material_definitions(cls, material_type: str): pass def is_a_flow_segment(cls, element): pass def is_a_material_set(cls, material): pass def is_editing_materials(cls): pass def is_material_used_in_sets(cls, material): pass def load_material_attributes(cls, material): pass def replace_material_with_material_profile(cls, element): pass + def update_elements_using_material(cls, material): pass @interface @@ -831,11 +852,10 @@ class Sequence: @interface class Spatial: - def can_contain(cls, structure_obj, element_obj): pass + def can_contain(cls, container, element_obj): pass def can_reference(cls, structure, element): pass def contract_container(cls, container): pass def copy_xy(cls, src_obj, destination_obj): pass - def import_spatial_element(cls, element, level_index): pass def deselect_objects(cls): pass def disable_editing(cls, obj): pass def duplicate_object_and_data(cls, obj): pass @@ -843,6 +863,9 @@ class Spatial: def edit_container_name(cls, container, name): pass def enable_editing(cls, obj): pass def expand_container(cls, container): pass + def filter_elements_by_class(cls, elements, ifc_class): pass + def filter_elements_by_relating_type(cls, elements, relating_type): pass + def filter_elements_by_untyped(cls, elements): pass def filter_products(cls, products, action): pass def get_active_container(cls): pass def get_container(cls, element): pass @@ -851,24 +874,27 @@ class Spatial: def get_relative_object_matrix(cls, target_obj, relative_to_obj): pass def get_selected_product_types(cls): pass def get_selected_products(cls): pass - def import_containers(cls, parent=None): pass def import_spatial_decomposition(cls): pass - def run_root_copy_class(cls, obj=None): pass - def run_spatial_assign_container(cls, structure_obj=None, element_obj=None): pass + def import_spatial_element(cls, element, level_index): pass + def load_contained_elements(cls): pass + def run_root_copy_class(cls, obj): pass + def run_spatial_assign_container(cls, container, element_obj): pass def run_spatial_import_spatial_decomposition(cls): pass def select_object(cls, obj): pass def select_products(cls, products, unhide=False): pass def set_active_object(cls, obj): pass def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass - def set_default_container(cls, container): pass def show_scene_objects(cls): pass - #HERE STARTS SPATIAL TOOL + + # HERE STARTS SPATIAL TOOL def is_bounding_class(cls, visible_element): pass def get_space_polygon_from_context_visible_objects(cls, x, y): pass - def get_boundary_lines_from_context_visible_objects(cls, visible_objects): pass + def debug_shape(cls, foo): pass + def debug_line(cls, start, end): pass + def get_boundary_lines_from_context_visible_objects(cls): pass def get_gross_mesh_from_element(cls, visible_element): pass def create_mesh_from_shape(cls, shape): pass - def get_x_y_z_h_mat_from_active_obj(cls, active_obj): pass + def get_x_y_z_h_mat_from_obj(cls, obj): pass def get_x_y_z_h_mat_from_cursor(cls): pass def get_union_shape_from_selected_objects(cls): pass def get_boundary_elements(cls, selected_objects): pass @@ -892,17 +918,20 @@ class Spatial: def get_active_obj_z(cls): pass def get_active_obj_height(cls): pass def get_relating_type_id(cls): pass - def translate_obj_to_z_location(cls, obj): pass + def translate_obj_to_z_location(cls, obj, z): pass def get_2d_vertices_from_obj(cls, obj): pass def get_scaled_2d_vertices(cls, points): pass def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass def get_body_representation(cls, obj): pass def assign_ifcspace_class_to_obj(cls, obj): pass def assign_type_to_obj(cls, obj): pass - def assign_relating_type_to_element(cls, ifc, Type, element, relating_type): pass - def regen_obj_representation(cls, ifc, geometry, obj, body): pass + def assign_relating_type_to_element(cls, ifc, type, element, relating_type): pass + def regen_obj_representation(cls, obj, body): pass def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass def toggle_hide_spaces(cls, spaces): pass + def set_default_container(cls, container): pass + def guess_default_container(cls): pass + def get_selected_containers(cls): pass @interface class Covering: @@ -993,7 +1022,6 @@ class Type: def get_representation_context(cls, representation): pass def get_type_occurrences(cls, element_type): pass def has_material_usage(cls, element): pass - def remove_object(cls, obj): pass def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass def run_geometry_switch_representation(cls, obj=None, representation=None, should_reload=None, is_global=None): pass diff --git a/src/blenderbim/blenderbim/core/type.py b/src/blenderbim/blenderbim/core/type.py index 51f52d9c7e..203d5e13e4 100644 --- a/src/blenderbim/blenderbim/core/type.py +++ b/src/blenderbim/blenderbim/core/type.py @@ -40,15 +40,15 @@ def assign_type( type_tool.disable_editing(obj) -def purge_unused_types(ifc: tool.Ifc, type: tool.Type) -> int: +def purge_unused_types(ifc: tool.Ifc, type: tool.Type, geometry: tool.Geometry) -> int: """Remove all types without occurrences, return an amount of the removed types.""" purged_types = 0 for element_type in type.get_model_types(): if not type.get_type_occurrences(element_type): obj = ifc.get_object(element_type) - ifc.run("root.remove_product", product=element_type) - purged_types += 1 if obj: - ifc.unlink(element=element_type) - type.remove_object(obj) + geometry.delete_ifc_object(obj) + else: + ifc.run("root.remove_product", product=element_type) + purged_types += 1 return purged_types diff --git a/src/blenderbim/blenderbim/core/web.py b/src/blenderbim/blenderbim/core/web.py index f5a429b69d..581ca35772 100644 --- a/src/blenderbim/blenderbim/core/web.py +++ b/src/blenderbim/blenderbim/core/web.py @@ -6,6 +6,10 @@ def connect_websocket_server(web, port): # check if port already has a server listening to it if web.is_port_available(port): web.start_websocket_server(port) + if web.has_started(port): + web.connect_websocket_server(port) + web.open_web_browser(port) + return web.connect_websocket_server(port) diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index 716915f4f5..3f80a685bc 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -23,6 +23,7 @@ from blenderbim.tool.brick import Brick from blenderbim.tool.bsdd import Bsdd from blenderbim.tool.cad import Cad from blenderbim.tool.clash import Clash +from blenderbim.tool.classification import Classification from blenderbim.tool.collector import Collector from blenderbim.tool.context import Context from blenderbim.tool.debug import Debug diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 004c4cc34b..17010b1070 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -32,6 +32,7 @@ from mathutils import Vector from pathlib import Path from blenderbim.bim.ifc import IFC_CONNECTED_TYPE from typing import Any, Optional, Union, Literal, Iterable, Callable +from typing_extensions import assert_never VIEWPORT_ATTRIBUTES = [ @@ -163,8 +164,14 @@ class Blender(blenderbim.core.tool.Blender): @classmethod def get_obj_ifc_definition_id( - cls, obj: Optional[str] = None, obj_type: Optional[str] = None, context: Optional[bpy.types.Context] = None + cls, + obj: Optional[str] = None, + obj_type: Optional[tool.Ifc.OBJECT_TYPE] = None, + context: Optional[bpy.types.Context] = None, ) -> Union[int, None]: + # TODO: is it ever used as None? + if obj_type is None: + return None if context is None: context = bpy.context if obj_type == "Object": @@ -200,6 +207,7 @@ class Blender(blenderbim.core.tool.Blender): elif obj_type == "Group": prop = context.scene.BIMGroupProperties return prop.groups[prop.active_group_index].ifc_definition_id + assert_never(obj_type) @classmethod def is_ifc_object(cls, obj: bpy.types.Object) -> bool: diff --git a/src/blenderbim/blenderbim/tool/brick.py b/src/blenderbim/blenderbim/tool/brick.py index 16d86d5fa3..cd1c3a115b 100644 --- a/src/blenderbim/blenderbim/tool/brick.py +++ b/src/blenderbim/blenderbim/tool/brick.py @@ -149,7 +149,10 @@ class Brick(blenderbim.core.tool.Brick): <{brick_uri}> rdfs:label ?label . } LIMIT 1 - """.replace("{brick_uri}", brick_uri)) + """.replace( + "{brick_uri}", brick_uri + ) + ) name = None for row in query: name = str(row.get("label")) @@ -442,8 +445,6 @@ class Brick(blenderbim.core.tool.Brick): bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear() - - class BrickStore: schema = None # this is now a os path path = None # file path if the project was loaded in diff --git a/src/blenderbim/blenderbim/tool/bsdd.py b/src/blenderbim/blenderbim/tool/bsdd.py index 6626a3953c..bea52d3b85 100644 --- a/src/blenderbim/blenderbim/tool/bsdd.py +++ b/src/blenderbim/blenderbim/tool/bsdd.py @@ -2,10 +2,11 @@ import blenderbim.core.tool import blenderbim.tool as tool import bpy import json +import bsdd +from typing import Any, Union, Optional class Bsdd(blenderbim.core.tool.Bsdd): - @classmethod def clear_class_psets(cls) -> None: bpy.context.scene.BIMBSDDProperties.classification_psets.clear() @@ -19,7 +20,7 @@ class Bsdd(blenderbim.core.tool.Bsdd): bpy.context.scene.BIMBSDDProperties.domains.clear() @classmethod - def create_class_psets(cls, pset_dict: dict): + def create_class_psets(cls, pset_dict: dict[str, dict[str, Any]]) -> None: props = bpy.context.scene.BIMBSDDProperties data_type_map = { "String": "string", @@ -37,21 +38,22 @@ class Bsdd(blenderbim.core.tool.Bsdd): new2.data_type = "enum" else: new2.data_type = data_type_map[data["data_type"]] + new2.description = data["description"] + new2.ifc_class = data["ifc_class"] @classmethod - def create_classes(cls, class_dict): + def create_classes(cls, class_dict: list[bsdd.ClassSearchResponseClassContractV1]) -> None: props = bpy.context.scene.BIMBSDDProperties for _class in sorted(class_dict, key=lambda c: c["referenceCode"]): prop = props.classifications.add() prop.name = _class["name"] prop.reference_code = _class["referenceCode"] - prop.description = _class.get("description", "") prop.uri = _class["uri"] prop.domain_name = _class["dictionaryName"] prop.domain_namespace_uri = _class["dictionaryUri"] @classmethod - def create_dictionaries(cls, dictionaries: dict): + def create_dictionaries(cls, dictionaries: list[bsdd.DictionaryContractV1]) -> None: props = bpy.context.scene.BIMBSDDProperties for dictionary in sorted(dictionaries, key=lambda d: d["name"]): new = props.domains.add() @@ -63,7 +65,7 @@ class Bsdd(blenderbim.core.tool.Bsdd): new.version = dictionary["version"] @classmethod - def get_active_class_data(cls, client): + def get_active_class_data(cls, client: bsdd.Client) -> Union[bsdd.ClassContractV1, dict]: prop = bpy.context.scene.BIMBSDDProperties bsdd_classification = prop.classifications[prop.active_classification_index] if not bsdd_classification: @@ -75,19 +77,23 @@ class Bsdd(blenderbim.core.tool.Bsdd): return bpy.context.scene.BIMBSDDProperties.active_uri @classmethod - def get_dictionaries(cls, client, status=None) -> list: + def get_dictionaries(cls, client: bsdd.Client, status: Optional[str] = None) -> list[bsdd.DictionaryContractV1]: response = client.get_dictionary() dicts = response.get("dictionaries") or [] if status is not None: - dicts = filter(lambda d: d["status"] == status, dicts) + dicts = list(filter(lambda d: d["status"] == status, dicts)) return dicts @classmethod - def get_property_dict(cls, class_data: dict): + def get_property_dict(cls, class_data: Union[bsdd.ClassContractV1, dict]) -> Union[dict[str, dict[str, Any]], None]: properties = class_data.get("classProperties", None) if not properties: return None + ifc_class = class_data.get("relatedIfcEntityNames") or "" + if ifc_class: + ifc_class = ifc_class[0] + psets = {} for prop in properties: if prop.get("propertyDictionaryName") != "IFC": @@ -104,15 +110,22 @@ class Bsdd(blenderbim.core.tool.Bsdd): possible_values = prop.get("allowedValues", []) or [] possible_values = [v["value"] for v in possible_values] - psets[pset][prop["name"]] = {"data_type": prop["dataType"], "possible_values": possible_values} + description = prop.get("description", "") + + psets[pset][prop["name"]] = { + "data_type": prop["dataType"], + "possible_values": possible_values, + "description": description, + "ifc_class": ifc_class, + } return psets @classmethod - def get_related_ifc_entities(cls, keyword): + def get_related_ifc_entities(cls, keyword: str) -> list[str]: active_object = bpy.context.active_object related_ifc_entities = [] if len(keyword) < 3: - return {"FINISHED"} + return [] if cls.should_filter_ifc_class() and active_object: element = tool.Ifc.get_entity(active_object) if element: @@ -120,13 +133,20 @@ class Bsdd(blenderbim.core.tool.Bsdd): return related_ifc_entities @classmethod - def search_class(cls, client, keyword, dictionary_uris, related_ifc_entities) -> dict: - response = client.search_class(keyword, dictionary_uris=dictionary_uris, - related_ifc_entities=related_ifc_entities) + def search_class( + cls, + client: bsdd.Client, + keyword: str, + dictionary_uris: Union[list[str], None], + related_ifc_entities: Union[list[str], None], + ) -> list[bsdd.ClassSearchResponseClassContractV1]: + response = client.search_class( + keyword, dictionary_uris=dictionary_uris, related_ifc_entities=related_ifc_entities + ) return response.get("classes", []) @classmethod - def set_active_bsdd(cls, name, uri): + def set_active_bsdd(cls, name: str, uri: str) -> None: props = bpy.context.scene.BIMBSDDProperties props.active_domain = name props.active_uri = uri diff --git a/src/blenderbim/blenderbim/tool/classification.py b/src/blenderbim/blenderbim/tool/classification.py new file mode 100644 index 0000000000..0d07c91c7f --- /dev/null +++ b/src/blenderbim/blenderbim/tool/classification.py @@ -0,0 +1,48 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +from __future__ import annotations +import bpy +import ifcopenshell.api +import ifcopenshell.util.classification +import blenderbim.core.tool +import blenderbim.tool as tool +import blenderbim.bim +from typing import Any, Optional, Union, Literal, Iterable, Callable +from typing_extensions import assert_never + + +class Classification(blenderbim.core.tool.Classification): + @classmethod + def get_location(cls, classification: ifcopenshell.entity_instance) -> Union[str, None]: + schema = classification.file.schema + if schema in ("IFC4", "IFC4X3"): + return classification[5] + elif schema == "IFC2X3": + return None + assert_never(schema) + + @classmethod + def set_location(cls, classification: ifcopenshell.entity_instance, location: Union[str, None]) -> None: + schema = classification.file.schema + if schema in ("IFC4", "IFC4X3"): + classification[5] = location + return + elif schema == "IFC2X3": + return + assert_never(schema) diff --git a/src/blenderbim/blenderbim/tool/covering.py b/src/blenderbim/blenderbim/tool/covering.py index decc60bc06..d73bb629de 100644 --- a/src/blenderbim/blenderbim/tool/covering.py +++ b/src/blenderbim/blenderbim/tool/covering.py @@ -20,6 +20,7 @@ import bpy import bmesh import shapely import ifcopenshell +import ifcopenshell.util.element import blenderbim.core.tool import blenderbim.core.root import blenderbim.core.spatial @@ -30,24 +31,49 @@ from math import pi from mathutils import Vector, Matrix from shapely import Polygon, MultiPolygon + class Covering(blenderbim.core.tool.Covering): @classmethod - def get_z_from_ceiling_height(cls): + def get_z_from_ceiling_height(cls) -> float: props = bpy.context.scene.BIMCoveringProperties return props.ceiling_height -# def toggle_spaces_visibility_wired_and_textured(cls, spaces): -# first_obj = tool.Ifc.get_object(spaces[0]) -# if bpy.data.objects[first_obj.name].display_type == "TEXTURED": -# for space in spaces: -# obj = tool.Ifc.get_object(space) -# bpy.data.objects[obj.name].show_wire = True -# bpy.data.objects[obj.name].display_type = "WIRE" -# return -# -# elif bpy.data.objects[first_obj.name].display_type == "WIRE": -# for space in spaces: -# obj = tool.Ifc.get_object(space) -# bpy.data.objects[obj.name].show_wire = False -# bpy.data.objects[obj.name].display_type = "TEXTURED" -# return + # def toggle_spaces_visibility_wired_and_textured(cls, spaces): + # first_obj = tool.Ifc.get_object(spaces[0]) + # if bpy.data.objects[first_obj.name].display_type == "TEXTURED": + # for space in spaces: + # obj = tool.Ifc.get_object(space) + # bpy.data.objects[obj.name].show_wire = True + # bpy.data.objects[obj.name].display_type = "WIRE" + # return + # + # elif bpy.data.objects[first_obj.name].display_type == "WIRE": + # for space in spaces: + # obj = tool.Ifc.get_object(space) + # bpy.data.objects[obj.name].show_wire = False + # bpy.data.objects[obj.name].display_type = "TEXTURED" + # return + + @classmethod + def covering_poll_wall_selected( + cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str + ) -> bool: + if not context.selected_objects or not context.active_object: + operator.poll_message_set("No objects selected.") + return False + element = tool.Ifc.get_entity(context.active_object) + if not element or not element.is_a("IfcWall") or not tool.Model.get_usage_type(element) == "LAYER2": + operator.poll_message_set("LAYER2 based IfcWall must be selected.") + return False + return cls.covering_poll_relating_type_check(operator, context, covering_type) + + @classmethod + def covering_poll_relating_type_check( + cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str + ) -> bool: + relating_type_id = int(context.scene.BIMModelProperties.relating_type_id) + relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id)) + if relating_type != covering_type: + operator.poll_message_set(f"Select IfcCoveringType with predefined type '{covering_type}'.") + return False + return True diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index ea78066650..b8d18d689f 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -19,6 +19,7 @@ import os import re import collections +import collections.abc import bpy import math import json @@ -79,11 +80,11 @@ class Drawing(blenderbim.core.tool.Drawing): # fmt: on @classmethod - def canonicalise_class_name(cls, name): + def canonicalise_class_name(cls, name: str) -> str: return re.sub("[^0-9a-zA-Z]+", "", name) @classmethod - def copy_representation(cls, source, dest): + def copy_representation(cls, source: ifcopenshell.entity_instance, dest: ifcopenshell.entity_instance) -> None: if source.Representation: dest.Representation = ifcopenshell.util.element.copy_deep( tool.Ifc.get(), source.Representation, exclude=["IfcGeometricRepresentationContext"] @@ -114,7 +115,9 @@ class Drawing(blenderbim.core.tool.Drawing): return obj @classmethod - def ensure_annotation_in_drawing_plane(cls, obj, camera=None): + def ensure_annotation_in_drawing_plane( + cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None + ) -> None: """Make sure annotation object is going to be visible in the camera view""" def get_camera_from_annotation_object(obj): @@ -138,7 +141,9 @@ class Drawing(blenderbim.core.tool.Drawing): ANNOTATION_TYPES_SUPPORT_SETUP = ("STAIR_ARROW", "TEXT", "REVISION_CLOUD", "FILL_AREA") @classmethod - def setup_annotation_object(cls, obj, object_type, related_object=None): + def setup_annotation_object( + cls, obj: bpy.types.Object, object_type: str, related_object: Optional[bpy.types.Object] = None + ) -> None: """Finish object's adjustments after both object and entity are created""" if not related_object: @@ -205,7 +210,9 @@ class Drawing(blenderbim.core.tool.Drawing): tool.Drawing.update_text_value(obj) @classmethod - def is_annotation_object_type(cls, element, object_types): + def is_annotation_object_type( + cls, element: ifcopenshell.entity_instance, object_types: Union[str, list[str]] + ) -> bool: if not isinstance(object_types, collections.abc.Iterable): object_types = [object_types] @@ -226,7 +233,9 @@ class Drawing(blenderbim.core.tool.Drawing): return False @classmethod - def get_annotation_representation(cls, element): + def get_annotation_representation( + cls, element: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, None]: rep = ifcopenshell.util.representation.get_representation( element, "Plan", "Annotation" ) or ifcopenshell.util.representation.get_representation(element, "Model", "Annotation") @@ -237,7 +246,9 @@ class Drawing(blenderbim.core.tool.Drawing): return rep @classmethod - def create_camera(cls, name, matrix, location_hint): + def create_camera( + cls, name: str, matrix: Matrix, location_hint: Literal["PERPSECTIVE", "ORTHOGRAPHIC"] + ) -> bpy.types.Object: camera = bpy.data.objects.new(name, bpy.data.cameras.new(name)) camera.location = (0, 0, 1.5) # The view shall be 1.5m above the origin camera.data.show_limits = True @@ -257,7 +268,7 @@ class Drawing(blenderbim.core.tool.Drawing): return camera @classmethod - def create_svg_schedule(cls, schedule): + def create_svg_schedule(cls, schedule: ifcopenshell.entity_instance) -> None: import blenderbim.bim.module.drawing.scheduler as scheduler schedule_creator = scheduler.Scheduler() @@ -276,7 +287,7 @@ class Drawing(blenderbim.core.tool.Drawing): return uri @classmethod - def add_drawings(cls, sheet): + def add_drawings(cls, sheet: ifcopenshell.entity_instance) -> None: import blenderbim.bim.module.drawing.sheeter as sheeter sheet_builder = sheeter.SheetBuilder() @@ -293,7 +304,7 @@ class Drawing(blenderbim.core.tool.Drawing): sheet_builder.add_drawing(drawing_references[drawing_annotation.Name], drawing_annotation, sheet) @classmethod - def delete_collection(cls, collection): + def delete_collection(cls, collection: bpy.types.Collection) -> None: bpy.data.collections.remove(collection, do_unlink=True) @classmethod @@ -312,19 +323,19 @@ class Drawing(blenderbim.core.tool.Drawing): bpy.data.objects.remove(obj) @classmethod - def disable_editing_drawings(cls): + def disable_editing_drawings(cls) -> None: bpy.context.scene.DocProperties.is_editing_drawings = False @classmethod - def disable_editing_schedules(cls): + def disable_editing_schedules(cls) -> None: bpy.context.scene.DocProperties.is_editing_schedules = False @classmethod - def disable_editing_references(cls): + def disable_editing_references(cls) -> None: bpy.context.scene.DocProperties.is_editing_references = False @classmethod - def disable_editing_sheets(cls): + def disable_editing_sheets(cls) -> None: bpy.context.scene.DocProperties.is_editing_sheets = False @classmethod @@ -344,19 +355,19 @@ class Drawing(blenderbim.core.tool.Drawing): bpy.ops.object.mode_set(mode="EDIT") @classmethod - def enable_editing_drawings(cls): + def enable_editing_drawings(cls) -> None: bpy.context.scene.DocProperties.is_editing_drawings = True @classmethod - def enable_editing_schedules(cls): + def enable_editing_schedules(cls) -> None: bpy.context.scene.DocProperties.is_editing_schedules = True @classmethod - def enable_editing_references(cls): + def enable_editing_references(cls) -> None: bpy.context.scene.DocProperties.is_editing_references = True @classmethod - def enable_editing_sheets(cls): + def enable_editing_sheets(cls) -> None: bpy.context.scene.DocProperties.is_editing_sheets = True @classmethod @@ -368,14 +379,14 @@ class Drawing(blenderbim.core.tool.Drawing): obj.BIMAssignedProductProperties.is_editing_product = True @classmethod - def ensure_unique_drawing_name(cls, name): + def ensure_unique_drawing_name(cls, name: str) -> str: names = [e.Name for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"] while name in names: name += "-X" return name @classmethod - def ensure_unique_identification(cls, identification): + def ensure_unique_identification(cls, identification: str) -> str: if tool.Ifc.get_schema() == "IFC2X3": ids = [d.DocumentId for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"] else: @@ -431,7 +442,7 @@ class Drawing(blenderbim.core.tool.Drawing): return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Annotation", target_view) @classmethod - def get_body_context(cls): + def get_body_context(cls) -> ifcopenshell.entity_instance: return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") @classmethod @@ -462,15 +473,15 @@ class Drawing(blenderbim.core.tool.Drawing): return os.path.splitext(os.path.basename(path))[0] @classmethod - def get_path_with_ext(cls, path, ext): + def get_path_with_ext(cls, path: str, ext: str) -> str: return os.path.splitext(path)[0] + f".{ext}" @classmethod - def get_unit_system(cls): + def get_unit_system(cls) -> Literal["NONE", "METRIC", "IMPERIAL"]: return bpy.context.scene.unit_settings.system @classmethod - def get_drawing_collection(cls, drawing): + def get_drawing_collection(cls, drawing: ifcopenshell.entity_instance) -> Union[bpy.types.Collection, None]: obj = tool.Ifc.get_object(drawing) if obj: return obj.BIMObjectProperties.collection @@ -488,8 +499,8 @@ class Drawing(blenderbim.core.tool.Drawing): return rel.RelatingDocument @classmethod - def get_drawing_references(cls, drawing): - results = set() + def get_drawing_references(cls, drawing: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + results: set[ifcopenshell.entity_instance] = set() for inverse in tool.Ifc.get().get_inverse(drawing): if inverse.is_a("IfcRelAssignsToProduct") and inverse.RelatingProduct == drawing: results.update(inverse.RelatedObjects) @@ -505,7 +516,7 @@ class Drawing(blenderbim.core.tool.Drawing): return rel.RelatedObjects @classmethod - def get_ifc_representation_class(cls, object_type): + def get_ifc_representation_class(cls, object_type: str) -> str: if object_type == "TEXT": return "IfcTextLiteral" elif object_type == "TEXT_LEADER": @@ -517,7 +528,9 @@ class Drawing(blenderbim.core.tool.Drawing): return element.Name @classmethod - def generate_drawing_matrix(cls, target_view, location_hint): + def generate_drawing_matrix( + cls, target_view: ifcopenshell.util.representation.TARGET_VIEW, location_hint: str + ) -> Matrix: x, y, z = (0, 0, 0) if location_hint == 0 else bpy.context.scene.cursor.matrix.translation if target_view == "PLAN_VIEW": if location_hint: @@ -554,12 +567,14 @@ class Drawing(blenderbim.core.tool.Drawing): return mathutils.Matrix() @classmethod - def generate_sheet_identification(cls): + def generate_sheet_identification(cls) -> str: number = len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"]) return "A" + str(number).zfill(2) @classmethod - def get_text_literal(cls, obj, return_list=False): + def get_text_literal( + cls, obj: bpy.types.Object, return_list: bool = False + ) -> Union[ifcopenshell.entity_instance, None, list[ifcopenshell.entity_instance]]: element = tool.Ifc.get_entity(obj) if not element: return @@ -575,11 +590,11 @@ class Drawing(blenderbim.core.tool.Drawing): return items[0] @classmethod - def is_editing_sheets(cls): + def is_editing_sheets(cls) -> bool: return bpy.context.scene.DocProperties.is_editing_sheets @classmethod - def remove_literal_from_annotation(cls, obj, literal): + def remove_literal_from_annotation(cls, obj: bpy.types.Object, literal: ifcopenshell.entity_instance) -> None: element = tool.Ifc.get_entity(obj) if not element: return @@ -617,7 +632,9 @@ class Drawing(blenderbim.core.tool.Drawing): cls.remove_literal_from_annotation(obj, literal) @classmethod - def add_literal_to_annotation(cls, obj, Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left"): + def add_literal_to_annotation( + cls, obj: bpy.types.Object, Literal: str = "Literal", Path: str = "RIGHT", BoxAlignment: str = "bottom-left" + ) -> Union[ifcopenshell.entity_instance, None]: element = tool.Ifc.get_entity(obj) if not element: return @@ -831,7 +848,7 @@ class Drawing(blenderbim.core.tool.Drawing): new.identification = schedule.Identification @classmethod - def import_sheets(cls): + def import_sheets(cls) -> None: props = bpy.context.scene.DocProperties expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded} props.sheets.clear() @@ -868,7 +885,7 @@ class Drawing(blenderbim.core.tool.Drawing): new.reference_type = reference_description @classmethod - def get_active_sheet(cls, context): + def get_active_sheet(cls, context: bpy.types.Context) -> bpy.types.PropertyGroup: props = context.scene.DocProperties return next(s for s in props.sheets[: props.active_sheet_index + 1][::-1] if s.is_sheet) @@ -904,7 +921,7 @@ class Drawing(blenderbim.core.tool.Drawing): obj.BIMAssignedProductProperties.relating_product = None @classmethod - def open_with_user_command(cls, user_command, path): + def open_with_user_command(cls, user_command: str, path: str) -> None: if user_command: commands = json.loads(user_command) replacements = {"path": path} @@ -920,15 +937,15 @@ class Drawing(blenderbim.core.tool.Drawing): subprocess.call(("xdg-open", path)) @classmethod - def open_spreadsheet(cls, uri): + def open_spreadsheet(cls, uri: str) -> None: cls.open_with_user_command(tool.Blender.get_addon_preferences().spreadsheet_command, uri) @classmethod - def open_svg(cls, uri): + def open_svg(cls, uri: str) -> None: cls.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, uri) @classmethod - def open_layout_svg(cls, uri): + def open_layout_svg(cls, uri: str) -> None: cls.open_with_user_command(tool.Blender.get_addon_preferences().layout_svg_command, uri) @classmethod @@ -954,15 +971,17 @@ class Drawing(blenderbim.core.tool.Drawing): ) @classmethod - def set_drawing_collection_name(cls, drawing, collection): + def set_drawing_collection_name( + cls, drawing: ifcopenshell.entity_instance, collection: bpy.types.Collection + ) -> None: collection.name = tool.Loader.get_name(drawing) @classmethod - def set_name(cls, element, name): + def set_name(cls, element: ifcopenshell.entity_instance, name: str) -> None: element.Name = name @classmethod - def show_decorations(cls): + def show_decorations(cls) -> None: bpy.context.scene.DocProperties.should_draw_decorations = True @classmethod @@ -1013,15 +1032,15 @@ class Drawing(blenderbim.core.tool.Drawing): # TODO below this point is highly experimental prototype code with no tests @classmethod - def does_file_exist(cls, uri): + def does_file_exist(cls, uri: str) -> bool: return os.path.exists(uri) @classmethod - def delete_file(cls, uri): + def delete_file(cls, uri: str) -> None: os.remove(uri) @classmethod - def move_file(cls, src, dest): + def move_file(cls, src: str, dest: str) -> None: try: shutil.move(src, dest) except: @@ -1029,7 +1048,9 @@ class Drawing(blenderbim.core.tool.Drawing): shutil.copy(src, dest) @classmethod - def generate_drawing_name(cls, target_view, location_hint): + def generate_drawing_name( + cls, target_view: ifcopenshell.util.representation.TARGET_VIEW, location_hint: str + ) -> str: if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and location_hint: location = tool.Ifc.get().by_id(location_hint) if target_view == "REFLECTED_PLAN_VIEW": @@ -1042,7 +1063,7 @@ class Drawing(blenderbim.core.tool.Drawing): return target_view @classmethod - def get_default_layout_path(cls, identification, name): + def get_default_layout_path(cls, identification: str, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] layouts_dir = ( ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") @@ -1051,7 +1072,7 @@ class Drawing(blenderbim.core.tool.Drawing): return os.path.join(layouts_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") @classmethod - def get_default_sheet_path(cls, identification, name): + def get_default_sheet_path(cls, identification: str, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] sheets_dir = ( ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") @@ -1060,7 +1081,7 @@ class Drawing(blenderbim.core.tool.Drawing): return os.path.join(sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") @classmethod - def get_default_titleblock_path(cls, name): + def get_default_titleblock_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] titleblocks_dir = ( ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") @@ -1069,7 +1090,7 @@ class Drawing(blenderbim.core.tool.Drawing): return os.path.join(titleblocks_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") @classmethod - def get_default_drawing_path(cls, name): + def get_default_drawing_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] drawings_dir = ( ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") @@ -1078,11 +1099,11 @@ class Drawing(blenderbim.core.tool.Drawing): return os.path.join(drawings_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") @classmethod - def sanitise_filename(cls, name): + def sanitise_filename(cls, name: str) -> str: return "".join(x for x in name if (x.isalnum() or x in "._- ")) @classmethod - def get_default_drawing_resource_path(cls, resource): + def get_default_drawing_resource_path(cls, resource: str) -> Union[str, None]: project = tool.Ifc.get().by_type("IfcProject")[0] resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr( bpy.context.scene.DocProperties, f"{resource.lower()}_path" @@ -1091,12 +1112,12 @@ class Drawing(blenderbim.core.tool.Drawing): return resource_path.replace("\\", "/") @classmethod - def get_default_shading_style(cls): + def get_default_shading_style(cls) -> str: dprops = bpy.context.scene.DocProperties return dprops.shadingstyle_default @classmethod - def setup_shading_styles_path(cls, resource_path): + def setup_shading_styles_path(cls, resource_path: str) -> None: resource_path = tool.Ifc.resolve_uri(resource_path) os.makedirs(os.path.dirname(resource_path), exist_ok=True) if not os.path.exists(resource_path): @@ -1387,7 +1408,7 @@ class Drawing(blenderbim.core.tool.Drawing): clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW") - def clone(src): + def clone(src: bpy.types.Object) -> bpy.types.Object: dst = src.copy() dst.data = dst.data.copy() dst.name = dst.name.replace("IfcGridAxis/", "") @@ -1395,13 +1416,13 @@ class Drawing(blenderbim.core.tool.Drawing): dst.data.BIMMeshProperties.ifc_definition_id = 0 return dst - def disassemble(obj): + def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]: mesh = bmesh.new() mesh.verts.ensure_lookup_table() mesh.from_mesh(obj.data) return obj, mesh - def assemble(obj, mesh): + def assemble(obj: bpy.types.Object, mesh: bmesh.types.BMesh) -> bpy.types.Object: mesh.to_mesh(obj.data) return obj @@ -1413,7 +1434,7 @@ class Drawing(blenderbim.core.tool.Drawing): obj.matrix_world.translation += annotation_offset return obj, mesh - def clip_to_camera_boundary(mesh): + def clip_to_camera_boundary(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh: mesh.verts.ensure_lookup_table() points = [v.co for v in mesh.verts[0:2]] points = helper.clip_segment(bounds, points) @@ -1423,7 +1444,7 @@ class Drawing(blenderbim.core.tool.Drawing): mesh.verts[1].co = points[1] return mesh - def draw_grids_vertically(mesh): + def draw_grids_vertically(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh: mesh.verts.ensure_lookup_table() points = [v.co for v in mesh.verts[0:2]] points = helper.elevate_segment(bounds, points) @@ -1534,11 +1555,11 @@ class Drawing(blenderbim.core.tool.Drawing): return text @classmethod - def sync_object_representation(cls, obj): + def sync_object_representation(cls, obj: bpy.types.Object) -> None: bpy.ops.bim.update_representation(obj=obj.name) @classmethod - def sync_object_placement(cls, obj): + def sync_object_placement(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: blender_matrix = np.array(obj.matrix_world) element = tool.Ifc.get_entity(obj) if (obj.scale - mathutils.Vector((1.0, 1.0, 1.0))).length > 1e-4: @@ -1552,7 +1573,7 @@ class Drawing(blenderbim.core.tool.Drawing): return element @classmethod - def sync_grid_axis_object_placement(cls, obj, element): + def sync_grid_axis_object_placement(cls, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0] grid_obj = tool.Ifc.get_object(grid) if grid_obj: @@ -1568,11 +1589,11 @@ class Drawing(blenderbim.core.tool.Drawing): return document.HasDocumentReferences or [] @classmethod - def get_references_with_location(cls, location): + def get_references_with_location(cls, location: Union[str, None]) -> list[ifcopenshell.entity_instance]: return [r for r in tool.Ifc.get().by_type("IfcDocumentReference") if r.Location == location] @classmethod - def update_embedded_svg_location(cls, uri, reference, new_location): + def update_embedded_svg_location(cls, uri: str, reference: ifcopenshell.entity_instance, new_location: str) -> None: tree = etree.parse(uri) root = tree.getroot() rel_location = os.path.relpath(new_location, os.path.dirname(uri)) @@ -1612,11 +1633,13 @@ class Drawing(blenderbim.core.tool.Drawing): return attributes @classmethod - def get_reference_location(cls, reference): + def get_reference_location(cls, reference: ifcopenshell.entity_instance) -> Union[str, None]: return reference.Location @classmethod - def get_reference_element(cls, reference): + def get_reference_element( + cls, reference: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, None]: if tool.Ifc.get_schema() == "IFC2X3": refs = [r for r in tool.Ifc.get().by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference] else: @@ -1625,12 +1648,12 @@ class Drawing(blenderbim.core.tool.Drawing): return refs[0].RelatedObjects[0] @classmethod - def get_drawing_human_scale(cls, drawing): + def get_drawing_human_scale(cls, drawing: ifcopenshell.entity_instance) -> str: pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") or {} return "NTS" if pset.get("IsNTS", False) else pset.get("HumanScale", "NTS") @classmethod - def get_drawing_metadata(cls, drawing): + def get_drawing_metadata(cls, drawing: ifcopenshell.entity_instance) -> list[str]: # fmt: off return [ v.strip() @@ -1642,7 +1665,7 @@ class Drawing(blenderbim.core.tool.Drawing): # fmt: on @classmethod - def get_annotation_z_index(cls, drawing): + def get_annotation_z_index(cls, drawing: ifcopenshell.entity_instance) -> float: return ifcopenshell.util.element.get_pset(drawing, "EPset_Annotation", "ZIndex") or 0 @classmethod @@ -1654,11 +1677,11 @@ class Drawing(blenderbim.core.tool.Drawing): return symbol @classmethod - def has_linework(cls, drawing): + def has_linework(cls, drawing: ifcopenshell.entity_instance) -> bool: return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("HasLinework", False) @classmethod - def has_annotation(cls, drawing): + def has_annotation(cls, drawing: ifcopenshell.entity_instance) -> bool: return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("HasAnnotation", False) @classmethod @@ -1723,19 +1746,21 @@ class Drawing(blenderbim.core.tool.Drawing): return elements @classmethod - def get_annotation_element(cls, element): + def get_annotation_element(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: for rel in element.HasAssignments: if rel.is_a("IfcRelAssignsToProduct"): return rel.RelatingProduct @classmethod - def get_drawing_reference(cls, drawing): + def get_drawing_reference(cls, drawing: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: for rel in drawing.HasAssociations: if rel.is_a("IfcRelAssociatesDocument"): return rel.RelatingDocument @classmethod - def get_reference_document(cls, reference): + def get_reference_document( + cls, reference: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, None]: if tool.Ifc.get_schema() == "IFC2X3": return reference.ReferenceToDocument[0] return reference.ReferencedDocument @@ -1749,13 +1774,13 @@ class Drawing(blenderbim.core.tool.Drawing): tool.Ifc.get_object(product).select_set(True) @classmethod - def is_drawing_active(cls): + def is_drawing_active(cls) -> bool: camera = bpy.context.scene.camera 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): + def is_camera_orthographic(cls) -> bool: camera = bpy.context.scene.camera return True if (camera and camera.data.type == "ORTHO") else False @@ -1764,7 +1789,7 @@ class Drawing(blenderbim.core.tool.Drawing): return drawing.id() == bpy.context.scene.DocProperties.active_drawing_id @classmethod - def run_drawing_activate_model(cls): + def run_drawing_activate_model(cls) -> None: bpy.ops.bim.activate_model() @classmethod @@ -1906,7 +1931,15 @@ class Drawing(blenderbim.core.tool.Drawing): ) @classmethod - def is_in_camera_view(cls, obj, camera_inverse_matrix, x, y, clip_start, clip_end): + def is_in_camera_view( + cls, + obj: bpy.types.Object, + camera_inverse_matrix: Matrix, + x: float, + y: float, + clip_start: float, + clip_end: float, + ) -> bool: local_bbox = [camera_inverse_matrix @ obj.matrix_world @ Vector(v) for v in obj.bound_box] local_x = [v.x for v in local_bbox] local_y = [v.y for v in local_bbox] @@ -1921,14 +1954,14 @@ class Drawing(blenderbim.core.tool.Drawing): return True @classmethod - def is_intersecting_camera(cls, obj, camera): + def is_intersecting_camera(cls, obj: bpy.types.Object, camera: bpy.types.Object) -> bool: # Based on separating axis theorem plane_co = camera.matrix_world.translation plane_no = camera.matrix_world.col[2].xyz return cls.is_intersecting_plane(obj, plane_co, plane_no) @classmethod - def is_intersecting_plane(cls, obj, plane_co, plane_no): + def is_intersecting_plane(cls, obj: bpy.types.Object, plane_co: Vector, plane_no: Vector) -> bool: # Broadphase check using the bounding box bounding_box_world_coords = [obj.matrix_world @ Vector(coord) for coord in obj.bound_box] bounding_box_signed_distances = [plane_no.dot(v - plane_co) for v in bounding_box_world_coords] @@ -1958,7 +1991,7 @@ class Drawing(blenderbim.core.tool.Drawing): return pos_exists and neg_exists @classmethod - def bisect_mesh(cls, obj, camera): + def bisect_mesh(cls, obj: bpy.types.Object, camera: bpy.types.Object) -> tuple[list[Vector], list[list[int]]]: camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world plane_co = camera_matrix.translation plane_no = camera_matrix.col[2].xyz @@ -1969,7 +2002,9 @@ class Drawing(blenderbim.core.tool.Drawing): return cls.bisect_mesh_with_plane(obj, plane_co, plane_no, global_offset=global_offset) @classmethod - def bisect_mesh_with_plane(cls, obj, plane_co, plane_no, global_offset=None): + def bisect_mesh_with_plane( + cls, obj: bpy.types.Object, plane_co: Vector, plane_no: Vector, global_offset: Optional[Vector] = None + ) -> tuple[list[Vector], list[list[int]]]: if global_offset is None: global_offset = Vector() @@ -1980,9 +2015,9 @@ class Drawing(blenderbim.core.tool.Drawing): geom = bm.verts[:] + bm.edges[:] + bm.faces[:] results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no) - vert_map = {} - verts = [] - edges = [] + vert_map: dict[int, int] = {} + verts: list[Vector] = [] + edges: list[list[int]] = [] i = 0 for geom in results["geom_cut"]: if isinstance(geom, bmesh.types.BMVert): @@ -1998,12 +2033,12 @@ class Drawing(blenderbim.core.tool.Drawing): return verts, edges @classmethod - def get_scale_ratio(cls, scale): + def get_scale_ratio(cls, scale: str) -> float: numerator, denominator = scale.split("/") return float(numerator) / float(denominator) @classmethod - def get_diagram_scale(cls, obj): + def get_diagram_scale(cls, obj: bpy.types.Object) -> dict[str, float]: props = obj.data.BIMCameraProperties scale = props.diagram_scale if scale != "CUSTOM": @@ -2029,7 +2064,7 @@ class Drawing(blenderbim.core.tool.Drawing): return {"HumanScale": human_scale, "Scale": scale} @classmethod - def convert_scale_string(cls, value): + def convert_scale_string(cls, value: str) -> float: try: return float(value) except: @@ -2060,7 +2095,7 @@ class Drawing(blenderbim.core.tool.Drawing): return result * 0.0254 @classmethod - def extend_line(cls, start, end, distance): + def extend_line(cls, start: Vector, end: Vector, distance: float) -> tuple[list[float], list[float]]: start = np.array(start) end = np.array(end) direction = end - start @@ -2068,8 +2103,8 @@ class Drawing(blenderbim.core.tool.Drawing): return (start - offset).tolist(), (end + offset).tolist() @classmethod - def get_sheet_references(cls, drawing): - sheet_references = [] + def get_sheet_references(cls, drawing: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: + sheet_references: list[ifcopenshell.entity_instance] = [] drawing_reference = cls.get_drawing_document(drawing) for sheet in tool.Ifc.get().by_type("IfcDocumentInformation"): if not sheet.Scope == "SHEET": @@ -2082,7 +2117,7 @@ class Drawing(blenderbim.core.tool.Drawing): return sheet_references @classmethod - def get_camera_matrix(cls, camera): + def get_camera_matrix(cls, camera: bpy.types.Object) -> Matrix: matrix_world = camera.matrix_world.copy().normalized() location, rotation, scale = matrix_world.decompose() if scale.x < 0 or scale.y < 0 or scale.z < 0: diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 780e815ee5..2d2c9b34e4 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -927,6 +927,7 @@ class Geometry(blenderbim.core.tool.Geometry): new_representation = None for r in cls.get_representations_iter(element): + r = tool.Geometry.resolve_mapped_representation(r) if r != representation: new_representation = r break @@ -1227,3 +1228,9 @@ class Geometry(blenderbim.core.tool.Geometry): return material_style = tool.Material.get_style(materials[0]) return material_style + + @classmethod + def should_use_immediate_representation(cls, element: ifcopenshell.entity_instance, apply_openings: bool) -> bool: + use_immediate_repr = apply_openings and bool(getattr(element, "HasOpenings", None)) + use_immediate_repr = use_immediate_repr or cls.has_material_style_override(element) + return use_immediate_repr diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py index 85d9ca5570..577a173259 100644 --- a/src/blenderbim/blenderbim/tool/georeference.py +++ b/src/blenderbim/blenderbim/tool/georeference.py @@ -21,6 +21,9 @@ import json import numpy as np import ifcopenshell import ifcopenshell.api.georeference +import ifcopenshell.util.geolocation +import ifcopenshell.util.placement +import ifcopenshell.util.unit import blenderbim.core.tool import blenderbim.tool as tool import blenderbim.bim.helper @@ -222,17 +225,13 @@ class Georeference(blenderbim.core.tool.Georeference): @classmethod def set_coordinates(cls, io, coordinates): - if io == "local": - bpy.context.scene.BIMGeoreferenceProperties.local_coordinates = ",".join([str(o) for o in coordinates]) - elif io == "map": - bpy.context.scene.BIMGeoreferenceProperties.map_coordinates = ",".join([str(o) for o in coordinates]) + props = bpy.context.scene.BIMGeoreferenceProperties + setattr(props, f"{io}_coordinates", ",".join([str(o) for o in coordinates])) @classmethod def get_coordinates(cls, io): - if io == "local": - return [float(co) for co in bpy.context.scene.BIMGeoreferenceProperties.local_coordinates.split(",")] - elif io == "map": - return [float(co) for co in bpy.context.scene.BIMGeoreferenceProperties.map_coordinates.split(",")] + props = bpy.context.scene.BIMGeoreferenceProperties + return [float(co) for co in getattr(props, f"{io}_coordinates").split(",")] @classmethod def get_cursor_location(cls): @@ -383,3 +382,7 @@ class Georeference(blenderbim.core.tool.Georeference): ) angle = tool.Cad.normalise_angle(angle) gprops.model_project_north = str(angle) + + @classmethod + def has_blender_offset(cls) -> bool: + return bpy.context.scene.BIMGeoreferenceProperties.has_blender_offset diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index 43b8e9bd46..77be4a34db 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -26,10 +26,23 @@ import blenderbim.core.tool import blenderbim.bim.handler import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore, IFC_CONNECTED_TYPE -from typing import Optional, Union, Any, final +from typing import Optional, Union, Any, final, Literal class Ifc(blenderbim.core.tool.Ifc): + OBJECT_TYPE = Literal[ + "Object", + "Material", + "MaterialSet", + "MaterialSetItem", + "Task", + "Cost", + "Resource", + "Profile", + "WorkSchedule", + "Group", + ] + @classmethod def run(cls, command: str, **kwargs) -> Any: return ifcopenshell.api.run(command, IfcStore.get_file(), **kwargs) diff --git a/src/blenderbim/blenderbim/tool/ifcgit.py b/src/blenderbim/blenderbim/tool/ifcgit.py index 8438344f1b..81b397964a 100644 --- a/src/blenderbim/blenderbim/tool/ifcgit.py +++ b/src/blenderbim/blenderbim/tool/ifcgit.py @@ -1,3 +1,22 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +from __future__ import annotations import os import re import bpy @@ -5,6 +24,7 @@ import logging from blenderbim.bim import import_ifc from blenderbim.bim.ifc import IfcStore import blenderbim.tool as tool +from typing import TYPE_CHECKING, Union # allows git import even if git executable isn't found os.environ["GIT_PYTHON_REFRESH"] = "quiet" @@ -13,14 +33,20 @@ try: except ImportError: print("Warning: GitPython not available.") +if TYPE_CHECKING: + import git + + class IfcGit: + STEP_IDS = dict[str, set[int]] + @classmethod - def init_repo(cls, path_dir): + def init_repo(cls, path_dir: str) -> None: IfcGitRepo.repo = git.Repo.init(path_dir) cls.config_info_attributes(IfcGitRepo.repo) @classmethod - def clone_repo(cls, remote_url, local_folder): + def clone_repo(cls, remote_url: str, local_folder: str) -> git.Repo: IfcGitRepo.repo = git.Repo.clone_from( url=remote_url, to_path=local_folder, @@ -29,7 +55,7 @@ class IfcGit: return IfcGitRepo.repo @classmethod - def load_anyifc(cls, repo): + def load_anyifc(cls, repo: git.Repo) -> bool: working_dir = repo.working_dir for item in os.listdir(working_dir): path = os.path.join(working_dir, item) @@ -39,11 +65,11 @@ class IfcGit: return False @classmethod - def get_path_dir(cls, path_ifc): + def get_path_dir(cls, path_ifc: str) -> str: return os.path.abspath(os.path.dirname(path_ifc)) @classmethod - def repo_from_path(cls, path): + def repo_from_path(cls, path: str) -> Union[git.Repo, None]: """Returns a Git repository object or None""" if os.path.isdir(path): @@ -71,7 +97,7 @@ class IfcGit: return repo @classmethod - def add_file_to_repo(cls, repo, path_file): + def add_file_to_repo(cls, repo: git.Repo, path_file: str) -> None: if os.name == "nt": cls.dos2unix(path_file) repo.index.add(path_file) @@ -79,11 +105,11 @@ class IfcGit: bpy.ops.ifcgit.refresh() @classmethod - def git_checkout(cls, path_file): + def git_checkout(cls, path_file: str) -> None: IfcGitRepo.repo.git.checkout(path_file) @classmethod - def checkout_new_branch(cls, path_file): + def checkout_new_branch(cls, path_file: str) -> None: """Create a branch and move uncommitted changes to this branch""" props = bpy.context.scene.IfcGitProperties if props.new_branch_name: @@ -93,7 +119,7 @@ class IfcGit: bpy.ops.ifcgit.refresh() @classmethod - def git_commit(cls, path_file): + def git_commit(cls, path_file: str) -> None: props = bpy.context.scene.IfcGitProperties repo = IfcGitRepo.repo if os.name == "nt": @@ -103,7 +129,7 @@ class IfcGit: props.commit_message = "" @classmethod - def add_tag(cls, repo): + def add_tag(cls, repo: git.Repo) -> None: props = bpy.context.scene.IfcGitProperties item = props.ifcgit_commits[props.commit_index] repo.create_tag(props.new_tag_name, ref=item.hexsha, message=props.new_tag_message) @@ -111,19 +137,19 @@ class IfcGit: props.new_tag_message = "" @classmethod - def delete_tag(cls, repo, tag_name): + def delete_tag(cls, repo: git.Repo, tag_name: git.TagReference) -> None: if tag_name in repo.tags: repo.delete_tag(tag_name) @classmethod - def add_remote(cls, repo): + def add_remote(cls, repo: git.Repo) -> None: props = bpy.context.scene.IfcGitProperties repo.create_remote(name=props.remote_name, url=props.remote_url) props.remote_name = "" props.remote_url = "" @classmethod - def delete_remote(cls, repo): + def delete_remote(cls, repo: git.Repo) -> None: props = bpy.context.scene.IfcGitProperties remote_name = props.select_remote if remote_name in repo.remotes: @@ -132,7 +158,7 @@ class IfcGit: props.select_remote = repo.remotes[0].name @classmethod - def push(cls, repo, remote_name, branch_name): + def push(cls, repo: git.Repo, remote_name: str, branch_name: str) -> Union[str, None]: cls.config_push(repo) remote = repo.remotes[remote_name] try: @@ -141,7 +167,7 @@ class IfcGit: return exc.stderr @classmethod - def create_new_branch(cls): + def create_new_branch(cls) -> None: """Convert a detached HEAD into a branch""" props = bpy.context.scene.IfcGitProperties repo = IfcGitRepo.repo @@ -153,7 +179,7 @@ class IfcGit: bpy.ops.ifcgit.refresh() @classmethod - def clear_commits_list(cls): + def clear_commits_list(cls) -> None: area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") area.spaces[0].shading.color_type = "MATERIAL" props = bpy.context.scene.IfcGitProperties @@ -162,7 +188,7 @@ class IfcGit: props.ifcgit_commits.clear() @classmethod - def get_commits_list(cls, path_ifc, lookup): + def get_commits_list(cls, path_ifc: str, lookup: dict[str, Any]) -> None: props = bpy.context.scene.IfcGitProperties repo = cls.repo_from_path(path_ifc) @@ -204,14 +230,14 @@ class IfcGit: list_item.tags[-1].message = tag.tag.message @classmethod - def refresh_revision_list(cls, path_ifc): + def refresh_revision_list(cls, path_ifc: str) -> None: repo = cls.repo_from_path(path_ifc) cls.clear_commits_list() lookup = cls.tags_by_hexsha(repo) cls.get_commits_list(path_ifc, lookup) @classmethod - def is_valid_ref_format(cls, string): + def is_valid_ref_format(cls, string: str) -> Union[re.Match[str], None]: """Check a bare branch or tag name is valid""" return re.match( @@ -220,7 +246,7 @@ class IfcGit: ) @classmethod - def load_project(cls, path_ifc=""): + def load_project(cls, path_ifc: str = "") -> None: """Clear and load an ifc project""" if path_ifc: @@ -247,7 +273,7 @@ class IfcGit: bpy.ops.object.select_all(action="DESELECT") @classmethod - def branches_by_hexsha(cls, repo): + def branches_by_hexsha(cls, repo: git.Repo) -> dict[str, Any]: """reverse lookup for branches""" result = {} @@ -266,7 +292,7 @@ class IfcGit: return result @classmethod - def tags_by_hexsha(cls, repo): + def tags_by_hexsha(cls, repo: git.Repo) -> dict[str, Any]: """reverse lookup for tags""" result = {} @@ -278,7 +304,7 @@ class IfcGit: return result @classmethod - def ifc_diff_ids(cls, repo, hash_a, hash_b, path_ifc): + def ifc_diff_ids(cls, repo: git.Repo, hash_a: str, hash_b: str, path_ifc: str) -> STEP_IDS: """Given two revision hashes and a filename, retrieve""" """step-ids of modified, added and removed entities""" @@ -308,7 +334,7 @@ class IfcGit: } @classmethod - def get_revisions_step_ids(cls): + def get_revisions_step_ids(cls) -> Union[STEP_IDS, None]: path_ifc = bpy.data.scenes["Scene"].BIMProperties.ifc_file props = bpy.context.scene.IfcGitProperties @@ -340,7 +366,7 @@ class IfcGit: return step_ids @classmethod - def get_modified_shape_object_step_ids(cls, step_ids): + def get_modified_shape_object_step_ids(cls, step_ids: STEP_IDS) -> STEP_IDS: model = tool.Ifc.get() modified_shape_object_step_ids = {"modified": []} @@ -352,7 +378,7 @@ class IfcGit: return modified_shape_object_step_ids @classmethod - def update_step_ids(cls, step_ids, modified_shape_object_step_ids): + def update_step_ids(cls, step_ids: STEP_IDS, modified_shape_object_step_ids: STEP_IDS) -> STEP_IDS: final_step_ids = {} final_step_ids["added"] = step_ids["added"] @@ -361,7 +387,7 @@ class IfcGit: return final_step_ids @classmethod - def colourise(cls, step_ids): + def colourise(cls, step_ids: STEP_IDS) -> None: area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") area.spaces[0].shading.color_type = "OBJECT" bpy.ops.object.select_all(action="DESELECT") @@ -383,7 +409,7 @@ class IfcGit: obj.color = (1.0, 1.0, 1.0, 0.5) @classmethod - def switch_to_revision_item(cls): + def switch_to_revision_item(cls) -> None: props = bpy.context.scene.IfcGitProperties repo = IfcGitRepo.repo item = props.ifcgit_commits[props.commit_index] @@ -398,13 +424,13 @@ class IfcGit: repo.git.checkout(item.hexsha) @classmethod - def delete_collection(cls, blender_collection): + def delete_collection(cls, blender_collection: bpy.types.Collection) -> None: for obj in blender_collection.objects: bpy.data.objects.remove(obj, do_unlink=True) bpy.data.collections.remove(blender_collection) @classmethod - def is_valid_branch_name(cls, new_branch_name): + def is_valid_branch_name(cls, new_branch_name: str): """Check if a branch name is valid and doesn't conflict with existing branches""" if not cls.is_valid_ref_format(new_branch_name): return False @@ -413,7 +439,7 @@ class IfcGit: return True @classmethod - def config_ifcmerge(cls): + def config_ifcmerge(cls) -> None: config_reader = IfcGitRepo.repo.config_reader() section = 'mergetool "ifcmerge"' if not config_reader.has_section(section): @@ -427,7 +453,7 @@ class IfcGit: config_writer.set_value(section, "trustExitCode", True) @classmethod - def config_push(cls, repo): + def config_push(cls, repo: git.Repo) -> None: """Set push.autoSetupRemote""" config_reader = repo.config_reader() if not config_reader.has_section("push"): @@ -436,7 +462,7 @@ class IfcGit: config_writer.set_value("push", "autoSetupRemote", True) @classmethod - def config_info_attributes(cls, repo): + def config_info_attributes(cls, repo: git.Repo) -> None: """Set IFC files as text in .git/info/attributes""" path_attributes = os.path.join(repo.git_dir, "info", "attributes") if not os.path.exists(path_attributes): @@ -445,7 +471,7 @@ class IfcGit: f.write("*.ifc text") @classmethod - def dos2unix(cls, path_file): + def dos2unix(cls, path_file: str) -> None: with open(path_file, "rb") as infile: content = infile.read() with open(path_file, "wb") as output: @@ -453,7 +479,7 @@ class IfcGit: output.write(line + b"\n") @classmethod - def execute_merge(cls, path_ifc, operator): + def execute_merge(cls, path_ifc: str, operator: bpy.types.Operator) -> Union[None, False]: props = bpy.context.scene.IfcGitProperties repo = IfcGitRepo.repo item = props.ifcgit_commits[props.commit_index] @@ -497,7 +523,7 @@ class IfcGit: cls.refresh_revision_list(path_ifc) @classmethod - def entity_log(cls, path_ifc, step_id): + def entity_log(cls, path_ifc: str, step_id: int) -> str: """Raw git log for this entity""" repo = IfcGitRepo.repo if not repo: @@ -513,4 +539,4 @@ class IfcGit: class IfcGitRepo: - repo = None + repo: git.Repo = None diff --git a/src/blenderbim/blenderbim/tool/loader.py b/src/blenderbim/blenderbim/tool/loader.py index 6e530656a0..3bfbc62cd0 100644 --- a/src/blenderbim/blenderbim/tool/loader.py +++ b/src/blenderbim/blenderbim/tool/loader.py @@ -91,6 +91,8 @@ class Loader(blenderbim.core.tool.Loader): @classmethod def get_name(cls, element: ifcopenshell.entity_instance) -> str: + if element.is_a("IfcGridAxis"): + return "{}/{}".format(element.is_a(), element.AxisTag) return "{}/{}".format(element.is_a(), getattr(element, "Name", "None")) @classmethod @@ -695,7 +697,7 @@ class Loader(blenderbim.core.tool.Loader): cls.settings.false_origin = ifcopenshell.util.geolocation.auto_xyz2enh( ifc_file, *offset_point, should_return_in_map_units=False ) - if angle := ifcopenshell.util.geolocation.get_grid_north(ifc_file): + if (angle := ifcopenshell.util.geolocation.get_grid_north(ifc_file)) and not tool.Cad.is_x(angle, 0): cls.settings.project_north = angle cls.set_manual_blender_offset(ifc_file) diff --git a/src/blenderbim/blenderbim/tool/material.py b/src/blenderbim/blenderbim/tool/material.py index 31c1054e48..df57363c74 100644 --- a/src/blenderbim/blenderbim/tool/material.py +++ b/src/blenderbim/blenderbim/tool/material.py @@ -92,6 +92,7 @@ class Material(blenderbim.core.tool.Material): get_name = lambda x: "Unnamed" materials = sorted(tool.Ifc.get().by_type(material_type), key=get_name) categories = defaultdict(list) + if material_type == "IfcMaterial": category_index_to_reselect = None @@ -112,8 +113,9 @@ class Material(blenderbim.core.tool.Material): for material in mats if cat.is_expanded else []: new = props.materials.add() - new.ifc_definition_id = material.id() + # Assign name before assigning ifc_definition_id to avoid triggering IFC update. new.name = get_name(material) + new.ifc_definition_id = material.id() new.total_elements = len( ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material) ) @@ -124,8 +126,9 @@ class Material(blenderbim.core.tool.Material): return for material in materials: new = props.materials.add() - new.ifc_definition_id = material.id() + # Assign name before assigning ifc_definition_id to avoid triggering IFC update. new.name = get_name(material) + new.ifc_definition_id = material.id() new.total_elements = len(ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material)) @classmethod @@ -318,7 +321,8 @@ class Material(blenderbim.core.tool.Material): ) -> None: """Ensure blender materials are updated after a material assignment. - Designed to be called after material.assign_material API call.""" + Designed to be called after material.assign_material API call. + For material sets it should be called after some item is already added to the material set.""" # NOTE: adding/editing/removing layers/profiles/constituents is not supported. # Adding support for profiles/layers it only will be possible when we'll be adding styles diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 4f79e13a16..46dcc4e6b8 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -1308,7 +1308,8 @@ class Model(blenderbim.core.tool.Model): obj = next(iter(mesh_users)) element = tool.Ifc.get_entity(obj) - own_material = ifcopenshell.util.element.get_material(element, should_inherit=False) + assert element # Type checker. + own_material = next(iter(ifcopenshell.util.element.get_materials(element, should_inherit=False)), None) inherited_mstyle = tool.Geometry.get_inherited_material_style(element) if assigned_material: diff --git a/src/blenderbim/blenderbim/tool/pset.py b/src/blenderbim/blenderbim/tool/pset.py index 9f297753f3..d278e20a7c 100644 --- a/src/blenderbim/blenderbim/tool/pset.py +++ b/src/blenderbim/blenderbim/tool/pset.py @@ -18,14 +18,18 @@ import bpy import ifcopenshell +import ifcopenshell.util.attribute import ifcopenshell.util.element import blenderbim.core.tool import blenderbim.tool as tool import blenderbim.bim.schema -from typing import Union +from typing import Union, Literal, Any +from typing_extensions import assert_never class Pset(blenderbim.core.tool.Pset): + PSET_TYPE = Literal["PSET", "QTO"] + @classmethod def get_element_pset( cls, element: ifcopenshell.entity_instance, pset_name: str @@ -35,7 +39,7 @@ class Pset(blenderbim.core.tool.Pset): return tool.Ifc.get().by_id(pset["id"]) @classmethod - def get_pset_props(cls, obj, obj_type): + def get_pset_props(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE) -> bpy.types.PropertyGroup: if obj_type == "Object": return bpy.data.objects.get(obj).PsetProperties elif obj_type == "Material": @@ -54,9 +58,10 @@ class Pset(blenderbim.core.tool.Pset): return bpy.context.scene.WorkSchedulePsetProperties elif obj_type == "Group": return bpy.context.scene.GroupPsetProperties + assert_never(obj_type) @classmethod - def get_pset_name(cls, obj, obj_type, pset_type="PSET"): + def get_pset_name(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE, pset_type: PSET_TYPE = "PSET") -> str: props = cls.get_pset_props(obj, obj_type) name = props.pset_name if pset_type == "PSET" else props.qto_name if name == "BBIM_CUSTOM": @@ -73,7 +78,7 @@ class Pset(blenderbim.core.tool.Pset): ) @classmethod - def is_pset_empty(cls, pset): + def is_pset_empty(cls, pset: ifcopenshell.entity_instance) -> bool: pset_dict = ifcopenshell.util.element.get_property_definition(pset) del pset_dict["id"] for value in pset_dict.values(): @@ -82,14 +87,16 @@ class Pset(blenderbim.core.tool.Pset): return True @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 + def enable_pset_editing( + cls, pset_id: int, pset_name: str, pset_type: PSET_TYPE, obj: str, obj_type: tool.Ifc.OBJECT_TYPE + ) -> None: + # TODO REFACTOR ONCE tool/CORE functions are available bpy.ops.bim.enable_pset_editing( pset_id=0, pset_name=cls.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type ) @classmethod - def import_pset_from_existing(cls, pset, props): + def import_pset_from_existing(cls, pset: ifcopenshell.entity_instance, props: bpy.types.PropertyGroup) -> None: pset_props = [] if pset.is_a("IfcElementQuantity"): pset_props = pset.Quantities @@ -98,7 +105,7 @@ class Pset(blenderbim.core.tool.Pset): elif pset.is_a("IfcMaterialProperties") or pset.is_a("IfcProfileProperties"): pset_props = pset.Properties - for prop in pset_props: + for prop in sorted(pset_props, key=lambda p: p.Name): if props.properties.get(prop.Name): continue # This property has already been added from a template if prop.is_a("IfcPropertyEnumeratedValue"): @@ -134,7 +141,7 @@ class Pset(blenderbim.core.tool.Pset): metadata.set_value(metadata.get_value_default() if metadata.is_null else value) @classmethod - def get_prop_template_primitive_type(cls, prop_template): + def get_prop_template_primitive_type(cls, prop_template: ifcopenshell.entity_instance) -> str: if prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]: return "float" elif prop_template.TemplateType == "Q_COUNT": @@ -144,7 +151,9 @@ class Pset(blenderbim.core.tool.Pset): ) @classmethod - def import_enumerated_value_from_template(cls, prop_template, data, props): + def import_enumerated_value_from_template( + cls, prop_template: ifcopenshell.entity_instance, data: dict[str, Any], props: bpy.types.PropertyGroup + ) -> None: enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues] selected_enum_items = data.get(prop_template.Name, []) or [] @@ -167,7 +176,13 @@ class Pset(blenderbim.core.tool.Pset): new.is_selected = enum in selected_enum_items @classmethod - def import_single_value_from_template(cls, pset_template, prop_template, data, props): + def import_single_value_from_template( + cls, + pset_template: ifcopenshell.entity_instance, + prop_template: ifcopenshell.entity_instance, + data: dict[str, Any], + props: bpy.types.PropertyGroup, + ) -> None: prop = props.properties.add() prop.name = prop_template.Name prop.value_type = "IfcPropertySingleValue" @@ -207,13 +222,18 @@ class Pset(blenderbim.core.tool.Pset): blenderbim.bim.helper.add_attribute_description(metadata, prop_template) @classmethod - def import_pset_from_template(cls, pset_template, pset, props): + def import_pset_from_template( + cls, + pset_template: ifcopenshell.entity_instance, + pset: Union[ifcopenshell.entity_instance, None], + props: bpy.types.PropertyGroup, + ) -> None: if pset: data = ifcopenshell.util.element.get_property_definition(pset) del data["id"] else: data = {} - for prop_template in pset_template.HasPropertyTemplates: + for prop_template in sorted(pset_template.HasPropertyTemplates, key=lambda p: p.Name): if not prop_template.is_a("IfcSimplePropertyTemplate"): continue # Other types not yet supported if prop_template.TemplateType == "P_SINGLEVALUE": @@ -231,28 +251,32 @@ class Pset(blenderbim.core.tool.Pset): pass @classmethod - def clear_blender_pset_properties(cls, props): + def clear_blender_pset_properties(cls, props: bpy.types.PropertyGroup) -> None: props.properties.clear() @classmethod - def set_active_pset(cls, props, pset, has_template): + def set_active_pset( + cls, props: bpy.types.PropertyGroup, pset: ifcopenshell.entity_instance, has_template: bool + ) -> None: props.active_pset_id = pset.id() props.active_pset_name = pset.Name props.active_pset_has_template = has_template @classmethod - def enable_proposed_pset(cls, props, pset_name, pset_type, has_template): + def enable_proposed_pset( + cls, props: bpy.types.PropertyGroup, pset_name: str, pset_type: PSET_TYPE, has_template: bool + ) -> None: props.active_pset_id = 0 props.active_pset_name = pset_name or "My_Data" props.active_pset_type = pset_type props.active_pset_has_template = has_template @classmethod - def get_pset_template(cls, name): + def get_pset_template(cls, name: str) -> Union[ifcopenshell.entity_instance, None]: return blenderbim.bim.schema.ifc.psetqto.get_by_name(name) @classmethod - def add_proposed_property(cls, name, value, props): + def add_proposed_property(cls, name: str, value: Any, props: bpy.types.PropertyGroup) -> None: if props.properties.get(name): return prop = props.properties.add() @@ -265,7 +289,7 @@ class Pset(blenderbim.core.tool.Pset): metadata.set_value(metadata.get_value_default() if metadata.is_null else value) @classmethod - def cast_string_to_primitive(cls, value: str): + def cast_string_to_primitive(cls, value: str) -> Any: value = value.strip() if value.lower() == "true": return True diff --git a/src/blenderbim/blenderbim/tool/qto.py b/src/blenderbim/blenderbim/tool/qto.py index 51118cb61a..a014542eca 100644 --- a/src/blenderbim/blenderbim/tool/qto.py +++ b/src/blenderbim/blenderbim/tool/qto.py @@ -28,6 +28,7 @@ from typing import Optional, Union, Literal QuantityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"] + class Qto(blenderbim.core.tool.Qto): @classmethod def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float: diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py index 5102b1f27b..674a95ba60 100644 --- a/src/blenderbim/blenderbim/tool/spatial.py +++ b/src/blenderbim/blenderbim/tool/spatial.py @@ -39,23 +39,20 @@ import json from math import pi from mathutils import Vector, Matrix from shapely import Polygon -from typing import Generator, Optional, Union, Literal, Any +from typing import Generator, Optional, Union, Literal, List class Spatial(blenderbim.core.tool.Spatial): @classmethod - def can_contain( - cls, structure_obj: Union[bpy.types.Object, None], element_obj: Union[bpy.types.Object, None] - ) -> bool: - structure = tool.Ifc.get_entity(structure_obj) + def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool: element = tool.Ifc.get_entity(element_obj) - if not structure or not element: + if not element: return False if tool.Ifc.get_schema() == "IFC2X3": - if not structure.is_a("IfcSpatialStructureElement"): + if not container.is_a("IfcSpatialStructureElement"): return False else: - if not structure.is_a("IfcSpatialStructureElement") and not structure.is_a( + if not container.is_a("IfcSpatialStructureElement") and not container.is_a( "IfcExternalSpatialStructureElement" ): return False @@ -111,41 +108,16 @@ class Spatial(blenderbim.core.tool.Spatial): def get_relative_object_matrix(cls, target_obj: bpy.types.Object, relative_to_obj: bpy.types.Object) -> Matrix: return relative_to_obj.matrix_world.inverted() @ target_obj.matrix_world - @classmethod - def import_containers(cls, parent: Optional[ifcopenshell.entity_instance] = None) -> None: - props = bpy.context.scene.BIMSpatialProperties - props.containers.clear() - - if not parent: - parent = tool.Ifc.get().by_type("IfcProject")[0] - - props.active_container_id = parent.id() - - for rel in parent.IsDecomposedBy or []: - related_objects = [] - for element in rel.RelatedObjects: - # skip objects without placements - if not element.is_a("IfcProduct"): - continue - related_objects.append((element, ifcopenshell.util.placement.get_storey_elevation(element))) - related_objects = sorted(related_objects, key=lambda e: e[1]) - for element, _ in related_objects: - new = props.containers.add() - new.name = element.Name or "Unnamed" - new.long_name = element.LongName or "" - new.has_decomposition = bool(element.IsDecomposedBy) - new.ifc_definition_id = element.id() - @classmethod def run_root_copy_class(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance: return blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj) @classmethod def run_spatial_assign_container( - cls, structure_obj: bpy.types.Object, element_obj: bpy.types.Object + cls, container: ifcopenshell.entity_instance, element_obj: bpy.types.Object ) -> Union[ifcopenshell.entity_instance, None]: return blenderbim.core.spatial.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, structure_obj=structure_obj, element_obj=element_obj + tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj ) @classmethod @@ -244,9 +216,10 @@ class Spatial(blenderbim.core.tool.Spatial): continue element_type = ifcopenshell.util.element.get_type(element) ifc_class = element.is_a() + ifc_definition_id = element_type.id() if element_type else 0 type_name = element_type.Name or "Unnamed" if element_type else "Untyped" - results.setdefault(ifc_class, {}).setdefault(type_name, 0) - results[ifc_class][type_name] += 1 + results.setdefault(ifc_class, {}).setdefault(ifc_definition_id, {"total": 0, "type_name": type_name}) + results[ifc_class][ifc_definition_id]["total"] += 1 total_elements = 0 for ifc_class in sorted(results.keys()): @@ -254,17 +227,34 @@ class Spatial(blenderbim.core.tool.Spatial): new.name = ifc_class new.is_class = True total = 0 - for type_name in sorted(results[ifc_class].keys()): + for ifc_definition_id in sorted( + results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"] + ): new2 = props.elements.add() - new2.name = type_name new2.is_type = True - new2.total = results[ifc_class][type_name] + new2.name = results[ifc_class][ifc_definition_id]["type_name"] + new2.total = results[ifc_class][ifc_definition_id]["total"] + new2.ifc_definition_id = ifc_definition_id total += new2.total new.total = total total_elements += total props.total_elements = total_elements + @classmethod + def filter_elements_by_class(cls, elements: List[ifcopenshell.entity_instance], ifc_class: str): + return [e for e in elements if e.is_a(ifc_class)] + + @classmethod + def filter_elements_by_relating_type( + cls, elements: List[ifcopenshell.entity_instance], relating_type: ifcopenshell.entity_instance + ): + return [e for e in elements if ifcopenshell.util.element.get_type(e) == relating_type] + + @classmethod + def filter_elements_by_untyped(cls, elements: List[ifcopenshell.entity_instance]): + return [e for e in elements if not ifcopenshell.util.element.get_type(e)] + @classmethod def import_spatial_decomposition(cls) -> None: props = bpy.context.scene.BIMSpatialDecompositionProperties @@ -402,10 +392,11 @@ class Spatial(blenderbim.core.tool.Spatial): continue old_mesh = obj.data + assert isinstance(old_mesh, bpy.types.Mesh) if visible_element.HasOpenings: new_mesh = cls.get_gross_mesh_from_element(visible_element) else: - new_mesh = obj.data.copy() + new_mesh = old_mesh.copy() obj.data = new_mesh # Boundary objects are likely triangulated. If a triangulated quad @@ -473,15 +464,15 @@ class Spatial(blenderbim.core.tool.Spatial): return mesh @classmethod - def get_x_y_z_h_mat_from_active_obj(cls, active_obj: bpy.types.Object) -> tuple[float, float, float, float, Matrix]: - mat = active_obj.matrix_world - local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector()) + def get_x_y_z_h_mat_from_obj(cls, obj: bpy.types.Object) -> tuple[float, float, float, float, Matrix]: + mat = obj.matrix_world + local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector()) global_bbox_center = mat @ local_bbox_center x = global_bbox_center.x y = global_bbox_center.y - z = (mat @ Vector(active_obj.bound_box[0])).z + z = (mat @ Vector(obj.bound_box[0])).z - h = active_obj.dimensions.z + h = obj.dimensions.z return x, y, z, h, mat @classmethod @@ -499,9 +490,15 @@ class Spatial(blenderbim.core.tool.Spatial): selected_objects = bpy.context.selected_objects boundary_elements = cls.get_boundary_elements(selected_objects) polys = cls.get_polygons(boundary_elements) - converted_tolerance = cls.get_converted_tolerance(tolerance=0.03) - union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2) - union = cls.get_purged_inner_holes_poly(union_geom=union, min_area=cls.get_converted_tolerance(tolerance=0.1)) + converted_tolerance = cls.get_converted_tolerance(tolerance_si=0.03) + union = shapely.ops.unary_union(polys).buffer( + converted_tolerance, + cap_style=shapely.constructive.BufferCapStyle.flat, + join_style=shapely.constructive.BufferJoinStyle.mitre, + ) + union = cls.get_purged_inner_holes_poly( + union_geom=union, min_area=cls.get_converted_tolerance(tolerance_si=0.1) + ) return union @classmethod @@ -531,28 +528,20 @@ class Spatial(blenderbim.core.tool.Spatial): @classmethod def get_obj_base_points(cls, obj: bpy.types.Object) -> dict[str, tuple[float, float]]: - x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box] - y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box] + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + bbox_ws = [obj.matrix_world @ Vector(v) / si_conversion for v in obj.bound_box] return { - "low_left": (x_values[0], y_values[0]), - "high_left": (x_values[3], y_values[3]), - "low_right": (x_values[4], y_values[4]), - "high_right": (x_values[7], y_values[7]), + "low_left": (bbox_ws[0].x, bbox_ws[0].y), + "high_left": (bbox_ws[3].x, bbox_ws[3].y), + "low_right": (bbox_ws[4].x, bbox_ws[4].y), + "high_right": (bbox_ws[7].x, bbox_ws[7].y), } @classmethod - def get_converted_tolerance(cls, tolerance: float) -> float: + def get_converted_tolerance(cls, tolerance_si: float) -> float: model = tool.Ifc.get() - project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT") - prefix = getattr(project_unit, "Prefix", None) - - return ifcopenshell.util.unit.convert( - value=tolerance, - from_prefix=None, - from_unit="METRE", - to_prefix=prefix, - to_unit=project_unit.Name, - ) + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(model) + return tolerance_si / si_conversion @classmethod def get_purged_inner_holes_poly(cls, union_geom: Polygon, min_area: float) -> Polygon: @@ -585,8 +574,13 @@ class Spatial(blenderbim.core.tool.Spatial): @classmethod def get_buffered_poly_from_linear_ring(cls, linear_ring: shapely.LinearRing) -> Polygon: poly = Polygon(linear_ring) - converted_tolerance = cls.get_converted_tolerance(tolerance=0.03) - poly = poly.buffer(converted_tolerance, single_sided=True, cap_style=2, join_style=2) + converted_tolerance = cls.get_converted_tolerance(tolerance_si=0.03) + poly = poly.buffer( + converted_tolerance, + single_sided=True, + cap_style=shapely.BufferCapStyle.flat, + join_style=shapely.BufferJoinStyle.mitre, + ) return poly @classmethod @@ -597,8 +591,10 @@ class Spatial(blenderbim.core.tool.Spatial): bm.edges.index_update() mat_invert = mat.inverted() - - new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.exterior.coords[0:-1]] + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + new_verts = [ + bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion)) for v in poly.exterior.coords[0:-1] + ] [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) @@ -868,3 +864,26 @@ class Spatial(blenderbim.core.tool.Spatial): if subelement: return subelement return None + + @classmethod + def get_selected_containers(cls) -> List[ifcopenshell.entity_instance]: + results = [] + for obj in tool.Blender.get_selected_objects(): + if (element := tool.Ifc.get_entity(obj)) and tool.Root.is_spatial_element(element): + results.append(element) + return results + + @classmethod + def get_selected_objects_without_containers(cls) -> list[bpy.types.Object]: + """Get selected objects skipping spatial elements. + + Useful for operators that are using selected objects to identify selected containers. + Note that those operators are typically have a limitation since they can't tell + objects to operate on from containers that should be used in the operation. + + E.g. we cannot bim.copy_to_container containers to other containers.""" + results: list[bpy.types.Object] = [] + for obj in tool.Blender.get_selected_objects(): + if (element := tool.Ifc.get_entity(obj)) and not tool.Root.is_spatial_element(element): + results.append(obj) + return results diff --git a/src/blenderbim/blenderbim/tool/style.py b/src/blenderbim/blenderbim/tool/style.py index fc0de8849f..9ce8056b39 100644 --- a/src/blenderbim/blenderbim/tool/style.py +++ b/src/blenderbim/blenderbim/tool/style.py @@ -55,6 +55,18 @@ class Style(blenderbim.core.tool.Style): def delete_object(cls, obj: bpy.types.Material) -> None: tool.Blender.remove_data_block(obj) + @classmethod + def enable_adding_presentation_style(cls) -> None: + props = bpy.context.scene.BIMStylesProperties + props.is_adding = True + props.update_graph = False + + @classmethod + def disable_adding_presentation_style(cls) -> None: + props = bpy.context.scene.BIMStylesProperties + props.is_adding = False + props.update_graph = True + @classmethod def disable_editing(cls) -> None: bpy.context.scene.BIMStylesProperties.is_editing_style = 0 diff --git a/src/blenderbim/blenderbim/tool/type.py b/src/blenderbim/blenderbim/tool/type.py index c9de381afb..6e8475be33 100644 --- a/src/blenderbim/blenderbim/tool/type.py +++ b/src/blenderbim/blenderbim/tool/type.py @@ -99,10 +99,6 @@ class Type(blenderbim.core.tool.Type): return "Usage" in material.is_a() return False - @classmethod - def remove_object(cls, obj: bpy.types.Object) -> None: - bpy.data.objects.remove(obj) - @classmethod def run_geometry_add_representation( cls, diff --git a/src/blenderbim/blenderbim/tool/web.py b/src/blenderbim/blenderbim/tool/web.py index d15d5f0a6f..4c836cb6c8 100644 --- a/src/blenderbim/blenderbim/tool/web.py +++ b/src/blenderbim/blenderbim/tool/web.py @@ -22,6 +22,7 @@ import blenderbim.core.tool import blenderbim.tool as tool import ifcopenshell.api.sequence from typing import Any, Dict, Optional +import time import socket import sys import os @@ -122,7 +123,8 @@ class Web(blenderbim.core.tool.Web): blenderbim_lib_path = os.path.join(blenderbim_path, "libs", "site", "packages") env = os.environ.copy() - env["blenderbim_lib_path"] = str(blenderbim_lib_path) + env["BLENDERBIM_LIB_PATH"] = str(blenderbim_lib_path) + env["BLENDERBIM_VERSION"] = tool.Blender.get_blenderbim_version() ws_process = subprocess.Popen( [sys.executable, ws_path, "--p", str(port), "--host", "127.0.0.1"], @@ -130,19 +132,6 @@ class Web(blenderbim.core.tool.Web): env=env, ) - pid_file = os.path.join(webui_path, "running_pid.json") - - if os.path.exists(pid_file): - with open(pid_file, "r") as f: - pids = json.load(f) - else: - pids = {} - - pids[str(ws_process.pid)] = port - - with open(pid_file, "w") as f: - json.dump(pids, f, indent=4) - cls.set_is_running(True) @classmethod @@ -223,12 +212,29 @@ class Web(blenderbim.core.tool.Web): with open(pid_file, "w") as f: json.dump(pids, f, indent=4) - ws_process.terminate() - ws_process.wait() + ws_process.kill() ws_process = None cls.set_is_running(False) - print("Websocket server terminated successfully") + print("Websocket server killed successfully") + + @classmethod + def has_started(cls, port): + max_time = 5 + start = time.time() + while True: + if time.time() - start > max_time: + return False + webui_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "webui") + pid_file = os.path.join(webui_path, "running_pid.json") + try: + with open(pid_file, "r") as f: + data = json.load(f) + if port in data.values(): + return True + except: + pass + time.sleep(0.1) @classmethod def send_webui_data( @@ -279,6 +285,8 @@ class Web(blenderbim.core.tool.Web): cls.handle_csv_operator(operator["operator"]) elif operator["sourcePage"] == "gantt": cls.handle_gantt_operator(operator["operator"]) + elif operator["sourcePage"] == "drawings": + cls.handle_drawings_operator(operator["operator"]) return 1.0 @classmethod @@ -323,6 +331,29 @@ class Web(blenderbim.core.tool.Web): gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)} cls.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data") + @classmethod + def handle_drawings_operator(cls, operator_data: dict) -> None: + if operator_data["type"] == "getDrawings": + drawings_data = [] + sheets_data = [] + ifc_file_dir = os.path.dirname(bpy.context.scene.BIMProperties.ifc_file) + + sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"] + for sheet in sorted(sheets, key=lambda s: getattr(s, "Identification", getattr(s, "DocumentId", None))): + for reference in tool.Drawing.get_document_references(sheet): + reference_description = tool.Drawing.get_reference_description(reference) + reference_location = tool.Drawing.get_reference_location(reference) + reference_name = os.path.basename(reference_location) + reference_path = os.path.join(ifc_file_dir, reference_location) + if reference_description == "SHEET": + sheets_data.append({"name": reference_name, "path": reference_path}) + if reference_description == "DRAWING": + drawings_data.append({"name": reference_name, "path": reference_path}) + + cls.send_webui_data( + data={"drawings": drawings_data, "sheets": sheets_data}, data_key="drawings_data", event="drawings_data" + ) + @classmethod def open_web_browser(cls, port: int) -> None: webbrowser.open(f"http://127.0.0.1:{port}/") diff --git a/src/blenderbim/docs/conf.py b/src/blenderbim/docs/conf.py index 0377e5f07d..b113508d98 100644 --- a/src/blenderbim/docs/conf.py +++ b/src/blenderbim/docs/conf.py @@ -29,6 +29,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os + # import sys # sys.path.insert(0, os.path.abspath('.')) @@ -103,7 +104,7 @@ html_theme_options = { "color-link--hover": "#d98014", "color-link--visited--hover": "#d98014", "color-admonition-text": "#651fff", - "font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji" + "font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji", }, "dark_css_variables": { "color-brand-primary": "#39b54a", @@ -118,9 +119,8 @@ html_theme_options = { "color-link--hover": "#d98014", "color-link--visited--hover": "#d98014", "color-admonition-text": "#EEEEEC", - "font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji" + "font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji", }, - "footer_icons": [ { "name": "IfcOpenShell", diff --git a/src/blenderbim/docs/devs/installation.rst b/src/blenderbim/docs/devs/installation.rst index cb9be95372..1f652b2806 100644 --- a/src/blenderbim/docs/devs/installation.rst +++ b/src/blenderbim/docs/devs/installation.rst @@ -150,7 +150,6 @@ Required Python modules to be stored in ``libs/site/packages/`` are: ifcopenshell bcf ifcclash - bimtester ifccobie ifccsv ifcdiff @@ -172,7 +171,6 @@ Required Python modules to be stored in ``libs/site/packages/`` are: elementpath six lark-parser - behave parse parse_type xlsxwriter @@ -184,8 +182,7 @@ Required Python modules to be stored in ``libs/site/packages/`` are: Notes: 1. ``ifcopenshell`` almost always requires the latest version due to the fast paced nature of the add-on development. -2. ``behave`` requires `patches `__. -3. ``ifcjson`` can be found `here `__. +2. ``ifcjson`` can be found `here `__. Required static assets are: :: diff --git a/src/blenderbim/pytest.ini b/src/blenderbim/pytest.ini index 2f55059a87..1543b724c6 100644 --- a/src/blenderbim/pytest.ini +++ b/src/blenderbim/pytest.ini @@ -2,7 +2,6 @@ markers = aggregate attribute - bimtester brick classification context diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index 653eca7ebb..db04fed5d9 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -1,5 +1,6 @@ try: import bpy + # Ensure it's not fake-bpy-module. if not hasattr(bpy, "context"): raise ModuleNotFoundError diff --git a/src/blenderbim/scripts/classifications/brick_classifiction.py b/src/blenderbim/scripts/classifications/brick_classifiction.py index 3ff2d69f83..9a9d42353e 100644 --- a/src/blenderbim/scripts/classifications/brick_classifiction.py +++ b/src/blenderbim/scripts/classifications/brick_classifiction.py @@ -1,23 +1,27 @@ import ifcopenshell from brickschema.graph import Graph -class Generator(): + +class Generator: def __init__(self): - self.file = ifcopenshell.file(schema='IFC4') + self.file = ifcopenshell.file(schema="IFC4") self.schema = Graph() - self.schema.load_file("Brick.ttl") # stable 1.3 loaded from external file + self.schema.load_file("Brick.ttl") # stable 1.3 loaded from external file def generate(self): - classification = self.file.create_entity('IfcClassification', **{ - 'Source': 'Brick', - 'Edition': 'August-11', - 'EditionDate': '2023-08-11', - 'Name': 'Brick', - 'Description': '', - 'Location': 'https://brickschema.org/schema/Brick', - 'ReferenceTokens': [] - }) - + classification = self.file.create_entity( + "IfcClassification", + **{ + "Source": "Brick", + "Edition": "August-11", + "EditionDate": "2023-08-11", + "Name": "Brick", + "Description": "", + "Location": "https://brickschema.org/schema/Brick", + "ReferenceTokens": [], + } + ) + query = self.schema.query( """ PREFIX brick: @@ -54,11 +58,11 @@ class Generator(): for row in query: # location is the entity URI location = row.get("entity") - + # description is the entity's definition description = row.get("definition") if not description: - description = '' + description = "" # name is the entity's label if it exists, else the name in the URI name = row.get("label") @@ -66,13 +70,11 @@ class Generator(): name = location.split("#")[-1].replace("_", " ") # create the reference - ref = self.file.create_entity('IfcClassificationReference', **{ - 'Location': location, - 'Description': description, - 'Identification': name, - 'Name': name - }) - + ref = self.file.create_entity( + "IfcClassificationReference", + **{"Location": location, "Description": description, "Identification": name, "Name": name} + ) + # get all parents of the entity query = self.schema.query( """ @@ -88,7 +90,7 @@ class Generator(): # filter parents for the brick entity for row in query: parent = row.get("parent").toPython() - if "brickschema.org" in parent and parent in references.keys(): + if "brickschema.org" in parent and parent in references.keys(): # some parents are not a brick type, so those should be ignored # some entities have multiple parents that are a brick type # since the query traverses down the graph, we can assume at least one parent has been seen, so pick that one @@ -96,7 +98,7 @@ class Generator(): break parent = None - # assign reference source + # assign reference source if parent: ref.ReferencedSource = references[parent] else: @@ -105,7 +107,8 @@ class Generator(): # save this reference for future reference sources references[location.toPython()] = ref - self.file.write('Brick.ifc') + self.file.write("Brick.ifc") + generator = Generator() -generator.generate() \ No newline at end of file +generator.generate() diff --git a/src/blenderbim/scripts/classifications/vbis.py b/src/blenderbim/scripts/classifications/vbis.py index da06cf8058..fe77308f82 100644 --- a/src/blenderbim/scripts/classifications/vbis.py +++ b/src/blenderbim/scripts/classifications/vbis.py @@ -1,30 +1,35 @@ import csv import os import json -#import pystache + +# import pystache import subprocess import ifcopenshell from pathlib import Path -class Generator(): + +class Generator: def __init__(self): - self.file = ifcopenshell.file(schema='IFC4') - self.out_dir = './' + self.file = ifcopenshell.file(schema="IFC4") + self.out_dir = "./" def generate(self): - classification = self.file.create_entity('IfcClassification', **{ - 'Source': 'VBIS', - 'Edition': 'May-20', - 'EditionDate': '2020-05-01', - 'Name': 'VBIS', - 'Description': '', - 'Location': 'https://vbis.com.au/', - 'ReferenceTokens': ['-'] - }) + classification = self.file.create_entity( + "IfcClassification", + **{ + "Source": "VBIS", + "Edition": "May-20", + "EditionDate": "2020-05-01", + "Name": "VBIS", + "Description": "", + "Location": "https://vbis.com.au/", + "ReferenceTokens": ["-"], + } + ) # We assume all the Excel spreadsheets are re-exported in CSV format - for filename in Path('./').glob('*.csv'): - with open(filename, newline='') as csvfile: - reader = csv.reader(csvfile, delimiter=',', quotechar='"') + for filename in Path("./").glob("*.csv"): + with open(filename, newline="") as csvfile: + reader = csv.reader(csvfile, delimiter=",", quotechar='"') name = None levels = [None, None, None, None] ref = classification @@ -39,17 +44,17 @@ class Generator(): name = row[2] elif row[3]: name = row[3] - ref = self.file.create_entity('IfcClassificationReference', **{ - 'Identification': row[4], - 'Name': name - }) - cur_level = len(row[4].split('-')) - 1 + ref = self.file.create_entity( + "IfcClassificationReference", **{"Identification": row[4], "Name": name} + ) + cur_level = len(row[4].split("-")) - 1 levels[cur_level] = ref if cur_level == 0: ref.ReferencedSource = classification else: - ref.ReferencedSource = levels[cur_level-1] - self.file.write('VBIS.ifc') + ref.ReferencedSource = levels[cur_level - 1] + self.file.write("VBIS.ifc") + generator = Generator() generator.generate() diff --git a/src/blenderbim/scripts/classifications/xml_classification.py b/src/blenderbim/scripts/classifications/xml_classification.py index 28ed1cd9fc..bc38df774b 100644 --- a/src/blenderbim/scripts/classifications/xml_classification.py +++ b/src/blenderbim/scripts/classifications/xml_classification.py @@ -7,86 +7,92 @@ import ifcopenshell import ifcopenshell.guid import os + class IFC4Extractor: def __init__(self, xml_file): self.xml_file = xml_file self.tree = ET.parse(self.xml_file) self.root = self.tree.getroot() - self.ns = {'xs': 'http://www.w3.org/2001/XMLSchema'} + self.ns = {"xs": "http://www.w3.org/2001/XMLSchema"} self.file = ifcopenshell.file() self.data = { - 'BB/SfB (3/4 cijfers)': {'source': 'Regie der Gebouwen', 'tokens': ['.']}, - 'BIMTypeCode': {'source': 'BIMStockholm', 'tokens': None}, - 'Common Arrangement of Work Sections (CAWS)': {'source': 'NBS', 'tokens': ['/']}, - 'CBI Classification - Level 2': {'source': 'Masterspec', 'tokens': None}, - 'CBI Classification - Level 4': {'source': 'Masterspec', 'tokens': None}, - 'Rumsfunktionskoder CC001 - 001': {'source': 'BIMAlliance', 'tokens': ['-']}, - 'CCS': {'source': 'Molio', 'tokens': None}, - 'CCTB': {'source': 'CCT-Bâtiments', 'tokens': ['.']}, - 'Funktionskoder Regionservice CD001 - 001': {'source': 'BIMAlliance', 'tokens': None}, - 'Rumsfunktion Blekinge CD002 - 001': {'source': 'BIMAlliance', 'tokens': None}, - 'EcoQuaestor Codetabel': {'source': 'EcoQuaestor', 'tokens': ['.', '-']}, - 'GuBIMclass CA': {'source': 'GuBIMClass', 'tokens': ['.']}, - 'GuBIMclass ES': {'source': 'GuBIMClass', 'tokens': ['.']}, - 'MasterFormat': {'source': 'CSI', 'tokens': [' ', '.']}, - 'NATSPEC Worksections': {'source': 'NATSPEC', 'tokens': None}, - 'NBS Create': {'source': 'NBS', 'tokens': ['_', '/']}, - 'NL/SfB (4 cijfers)': {'source': 'BIMLoket', 'tokens': ['.']}, - 'NS 3451 - Bygningsdelstabell': {'source': 'Standard Norge', 'tokens': None}, - 'OmniClass': {'source': 'OmniClass', 'tokens': ['-', ' ']}, - 'ÖNORM 6241-2': {'source': 'freeBIM 2', 'tokens': None}, - 'RICS NRM1': {'source': 'RICS', 'tokens': ['.']}, - 'RICS NRM3': {'source': 'RICS', 'tokens': ['.']}, - 'SFG20': {'source': 'SFG20', 'tokens': ['-']}, - 'SINAPI': {'source': 'Caixa', 'tokens': ['/']}, - 'STABU-Element': {'source': 'STABU', 'tokens': ['.']}, - 'TALO 2000 Building Component Classification': {'source': 'Rakennustieto', 'tokens': ['.']}, - 'TALO 2000 Hankenimikkeistö': {'source': 'Rakennustieto', 'tokens': ['.']}, - 'Uniclass': {'source': 'RIBA Enterprises Ltd', 'tokens': ['_']}, - 'Uniclass 2015': {'source': 'RIBA Enterprises Ltd', 'tokens': ['_']}, - 'UniFormat': {'source': 'UniFormat', 'tokens': ['.']}, - 'Uniformat': {'source': 'UniFormat', 'tokens': ['.']}, - 'VMSW': {'source': 'VMSW', 'tokens': ['.']} + "BB/SfB (3/4 cijfers)": {"source": "Regie der Gebouwen", "tokens": ["."]}, + "BIMTypeCode": {"source": "BIMStockholm", "tokens": None}, + "Common Arrangement of Work Sections (CAWS)": {"source": "NBS", "tokens": ["/"]}, + "CBI Classification - Level 2": {"source": "Masterspec", "tokens": None}, + "CBI Classification - Level 4": {"source": "Masterspec", "tokens": None}, + "Rumsfunktionskoder CC001 - 001": {"source": "BIMAlliance", "tokens": ["-"]}, + "CCS": {"source": "Molio", "tokens": None}, + "CCTB": {"source": "CCT-Bâtiments", "tokens": ["."]}, + "Funktionskoder Regionservice CD001 - 001": {"source": "BIMAlliance", "tokens": None}, + "Rumsfunktion Blekinge CD002 - 001": {"source": "BIMAlliance", "tokens": None}, + "EcoQuaestor Codetabel": {"source": "EcoQuaestor", "tokens": [".", "-"]}, + "GuBIMclass CA": {"source": "GuBIMClass", "tokens": ["."]}, + "GuBIMclass ES": {"source": "GuBIMClass", "tokens": ["."]}, + "MasterFormat": {"source": "CSI", "tokens": [" ", "."]}, + "NATSPEC Worksections": {"source": "NATSPEC", "tokens": None}, + "NBS Create": {"source": "NBS", "tokens": ["_", "/"]}, + "NL/SfB (4 cijfers)": {"source": "BIMLoket", "tokens": ["."]}, + "NS 3451 - Bygningsdelstabell": {"source": "Standard Norge", "tokens": None}, + "OmniClass": {"source": "OmniClass", "tokens": ["-", " "]}, + "ÖNORM 6241-2": {"source": "freeBIM 2", "tokens": None}, + "RICS NRM1": {"source": "RICS", "tokens": ["."]}, + "RICS NRM3": {"source": "RICS", "tokens": ["."]}, + "SFG20": {"source": "SFG20", "tokens": ["-"]}, + "SINAPI": {"source": "Caixa", "tokens": ["/"]}, + "STABU-Element": {"source": "STABU", "tokens": ["."]}, + "TALO 2000 Building Component Classification": {"source": "Rakennustieto", "tokens": ["."]}, + "TALO 2000 Hankenimikkeistö": {"source": "Rakennustieto", "tokens": ["."]}, + "Uniclass": {"source": "RIBA Enterprises Ltd", "tokens": ["_"]}, + "Uniclass 2015": {"source": "RIBA Enterprises Ltd", "tokens": ["_"]}, + "UniFormat": {"source": "UniFormat", "tokens": ["."]}, + "Uniformat": {"source": "UniFormat", "tokens": ["."]}, + "VMSW": {"source": "VMSW", "tokens": ["."]}, } def extract(self): - for system in self.tree.findall('Classification/System'): - project = self.file.create_entity('IfcProjectLibrary', Name=system.find('Name').text) - parent = self.file.create_entity('IfcClassification', **{ - 'Source': self.data[system.find('Name').text]['source'], - 'Edition': system.find('EditionVersion').text, - 'EditionDate': self.get_edition_date(system), - 'Name': system.find('Name').text, - 'Description': system.find('Description').text, - 'Location': system.find('Source').text, - 'ReferenceTokens': self.data[system.find('Name').text]['tokens'] - }) - self.file.createIfcRelAssociatesClassification( - ifcopenshell.guid.new(), None, None, None, [project], parent) - children = system.findall('Items/Item') + for system in self.tree.findall("Classification/System"): + project = self.file.create_entity("IfcProjectLibrary", Name=system.find("Name").text) + parent = self.file.create_entity( + "IfcClassification", + **{ + "Source": self.data[system.find("Name").text]["source"], + "Edition": system.find("EditionVersion").text, + "EditionDate": self.get_edition_date(system), + "Name": system.find("Name").text, + "Description": system.find("Description").text, + "Location": system.find("Source").text, + "ReferenceTokens": self.data[system.find("Name").text]["tokens"], + }, + ) + self.file.createIfcRelAssociatesClassification(ifcopenshell.guid.new(), None, None, None, [project], parent) + children = system.findall("Items/Item") self.get_children(children, parent) def get_children(self, children, parent): for child in children: - reference = self.file.create_entity('IfcClassificationReference', **{ - 'Identification': child.find('ID').text, - 'Name': child.find('Name').text, - 'ReferencedSource': parent, - 'Description': child.find('Description').text - }) - children = child.find('Children') + reference = self.file.create_entity( + "IfcClassificationReference", + **{ + "Identification": child.find("ID").text, + "Name": child.find("Name").text, + "ReferencedSource": parent, + "Description": child.find("Description").text, + }, + ) + children = child.find("Children") if children: self.get_children(children, reference) def get_edition_date(self, system): - d = system.find('EditionDate') - return '{}-{:02}-{:02}'.format( - d.find('Year').text, int(d.find('Month').text), int(d.find('Day').text)) + d = system.find("EditionDate") + return "{}-{:02}-{:02}".format(d.find("Year").text, int(d.find("Month").text), int(d.find("Day").text)) def export(self, filename): self.file.write(filename) -for filename in os.listdir('xml/'): - extractor = IFC4Extractor(f'xml/{filename}') + +for filename in os.listdir("xml/"): + extractor = IFC4Extractor(f"xml/{filename}") extractor.extract() - extractor.export(f'ifc/{filename[0:-3]}ifc') + extractor.export(f"ifc/{filename[0:-3]}ifc") diff --git a/src/blenderbim/scripts/generate_demo_library.py b/src/blenderbim/scripts/generate_demo_library.py index add4fa246d..8a9242f256 100644 --- a/src/blenderbim/scripts/generate_demo_library.py +++ b/src/blenderbim/scripts/generate_demo_library.py @@ -155,10 +155,16 @@ class LibraryGenerator: self.create_line_type("MEDIUM", "medium") self.create_line_type("THICK", "thick") self.create_line_type("STRONG", "strong") - self.create_text_type("SETOUT-TAG", "setout-tag", ["E ``round({{easting}}, 0.001)``", "N ``round({{northing}}, 0.001)``"]) + self.create_text_type( + "SETOUT-TAG", "setout-tag", ["E ``round({{easting}}, 0.001)``", "N ``round({{northing}}, 0.001)``"] + ) self.create_text_type("DOOR-TAG", "door-tag", ["{{type.Name}}", "{{Name}}"]) self.create_text_type("WINDOW-TAG", "window-tag", ["{{Name}}"]) - self.create_text_type("SPACE-TAG", "space-tag", ["{{Name}}", "{{Description}}", "``round({{Qto_SpaceBaseQuantities.NetFloorArea}}, 0.01)``"]) + self.create_text_type( + "SPACE-TAG", + "space-tag", + ["{{Name}}", "{{Description}}", "``round({{Qto_SpaceBaseQuantities.NetFloorArea}}, 0.01)``"], + ) self.create_text_type("MATERIAL-TAG", "rectangle-tag", ["{{material.Name}}"]) self.create_text_type("TYPE-TAG", "capsule-tag", ["{{type.Name}}"]) self.create_text_type("NAME-TAG", "capsule-tag", ["{{Name}}"]) @@ -189,9 +195,11 @@ class LibraryGenerator: self.file.createIfcDirection((0.0, 0.0, 1.0)), self.file.createIfcDirection((1.0, 0.0, 0.0)), ) - items.append(self.file.createIfcTextLiteralWithExtent( - literal, origin, "RIGHT", self.file.createIfcPlanarExtent(1000, 1000), "center" - )) + items.append( + self.file.createIfcTextLiteralWithExtent( + literal, origin, "RIGHT", self.file.createIfcPlanarExtent(1000, 1000), "center" + ) + ) representation = self.file.createIfcShapeRepresentation( self.representations["model_annotation"], @@ -205,22 +213,30 @@ class LibraryGenerator: def create_layer_type(self, ifc_class, name, thickness): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet" + ) layer_set = rel.RelatingMaterial layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=layer_set, material=self.material) layer.LayerThickness = thickness - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) return element def create_profile_type(self, ifc_class, name, profile): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet" + ) profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( "material.add_profile", self.file, profile_set=profile_set, material=self.material ) ifcopenshell.api.run("material.assign_profile", self.file, material_profile=material_profile, profile=profile) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) def create_type(self, ifc_class, name, representations): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) @@ -252,7 +268,9 @@ class LibraryGenerator: ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation ) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) LibraryGenerator().generate() diff --git a/src/blenderbim/scripts/generate_entourage_library.py b/src/blenderbim/scripts/generate_entourage_library.py index 07ddcf7e01..69476454e8 100644 --- a/src/blenderbim/scripts/generate_entourage_library.py +++ b/src/blenderbim/scripts/generate_entourage_library.py @@ -131,7 +131,9 @@ class LibraryGenerator: ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation ) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) if __name__ == "__main__": diff --git a/src/blenderbim/scripts/generate_furniture_library.py b/src/blenderbim/scripts/generate_furniture_library.py index 9a4c12492f..486fe8494c 100644 --- a/src/blenderbim/scripts/generate_furniture_library.py +++ b/src/blenderbim/scripts/generate_furniture_library.py @@ -244,7 +244,9 @@ class LibraryGenerator: polyline = builder.polyline((V(0, 0), V(width, 0))) _, _, first_semicircle = builder.create_transition_arc_ifc(width, depth, create_ifc_curve=True) - _, _, second_semicircle = builder.create_transition_arc_ifc(second_arc_width, second_arc_depth, create_ifc_curve=True) + _, _, second_semicircle = builder.create_transition_arc_ifc( + second_arc_width, second_arc_depth, create_ifc_curve=True + ) builder.translate(second_semicircle, V((width - second_arc_width) / 2, 0)) builder.translate([polyline, first_semicircle, second_semicircle], shift_to_center) @@ -934,34 +936,46 @@ class LibraryGenerator: overall_width, overall_depth, overall_height = (900.0, 750.0, 2100.0) representation_3d, representation_2d = create_shower_or_bathtube(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Shower 75x90", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Shower 75x90", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (800.0, 800.0, 2100.0) representation_3d, representation_2d = create_shower_or_bathtube(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Shower 80x80", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Shower 80x80", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (900.0, 900.0, 2100.0) representation_3d, representation_2d = create_shower_or_bathtube(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Shower 90x90", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Shower 90x90", representation_3d, representation_2d + ) # bathtubs overall_width, overall_depth, overall_height = (1600.0, 700.0, 550.0) representation_3d, representation_2d = create_shower_or_bathtube( overall_width, overall_depth, overall_height, bathtube=True ) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Small Bathtub", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Small Bathtub", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (1700.0, 750.0, 550.0) representation_3d, representation_2d = create_shower_or_bathtube( overall_width, overall_depth, overall_height, bathtube=True ) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Medium Bathtub", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Medium Bathtub", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (1800.0, 800.0, 550.0) representation_3d, representation_2d = create_shower_or_bathtube( overall_width, overall_depth, overall_height, bathtube=True ) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Large Bathtub", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Large Bathtub", representation_3d, representation_2d + ) def create_toilet(width, depth, height, seat_level=0): shift_to_center = V(-width / 2, 0) @@ -1130,9 +1144,7 @@ class LibraryGenerator: back_wall = builder.rectangle(V(width, height)) # example of rotating and extruding by Y axis btw back_wall_extruded = builder.extrude( - back_wall, - back_wall_depth + back_wall_border_mask_depth, - **builder.extrude_kwargs("Y") + back_wall, back_wall_depth + back_wall_border_mask_depth, **builder.extrude_kwargs("Y") ) items_3d_to_center.append(back_wall_extruded) @@ -1202,15 +1214,21 @@ class LibraryGenerator: overall_width, overall_depth, overall_height = (350.0, 350.0, 650.0) representation_3d, representation_2d = create_urinal(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Small Urinal", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Small Urinal", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (400.0, 400.0, 650.0) representation_3d, representation_2d = create_urinal(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Medium Urinal", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Medium Urinal", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (450.0, 450.0, 650.0) representation_3d, representation_2d = create_urinal(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Large Urinal", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Large Urinal", representation_3d, representation_2d + ) def create_basin(width, depth, height): second_width = 0.9 * width @@ -1251,19 +1269,27 @@ class LibraryGenerator: overall_width, overall_depth, overall_height = (450.0, 400.0, 250.0) representation_3d, representation_2d = create_basin(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Extra Small Basin", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Extra Small Basin", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (500.0, 450.0, 250.0) representation_3d, representation_2d = create_basin(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Small Basin", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Small Basin", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (550.0, 500.0, 250.0) representation_3d, representation_2d = create_basin(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Medium Basin", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Medium Basin", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (600.0, 550.0, 250.0) representation_3d, representation_2d = create_basin(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcSanitaryTerminalType", "Neufert Large Basin", representation_3d, representation_2d) + self.create_explicit_type( + "IfcSanitaryTerminalType", "Neufert Large Basin", representation_3d, representation_2d + ) def create_desk(width, depth, height, return_representations=False): leg_width = 50.0 @@ -1421,7 +1447,9 @@ class LibraryGenerator: overall_width, overall_depth, overall_height = (500.0, 570.0, 820.0) representation_3d, representation_2d = create_dishwasher(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcElectricApplianceType", "Neufert Dishwasher", representation_3d, representation_2d) + self.create_explicit_type( + "IfcElectricApplianceType", "Neufert Dishwasher", representation_3d, representation_2d + ) def create_cooktop(width, depth, height): items_3d, items_2d = create_box_objects(width, depth, height) @@ -1442,15 +1470,21 @@ class LibraryGenerator: overall_width, overall_depth, overall_height = (580.0, 510.0, 50.0) representation_3d, representation_2d = create_cooktop(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcElectricApplianceType", "Neufert Cooktop 58x51", representation_3d, representation_2d) + self.create_explicit_type( + "IfcElectricApplianceType", "Neufert Cooktop 58x51", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (750.0, 510.0, 50.0) representation_3d, representation_2d = create_cooktop(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcElectricApplianceType", "Neufert Cooktop 75x51", representation_3d, representation_2d) + self.create_explicit_type( + "IfcElectricApplianceType", "Neufert Cooktop 75x51", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (890.0, 510.0, 50.0) representation_3d, representation_2d = create_cooktop(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcElectricApplianceType", "Neufert Cooktop 89x51", representation_3d, representation_2d) + self.create_explicit_type( + "IfcElectricApplianceType", "Neufert Cooktop 89x51", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (1750.0, 600.0, 850.0) representation_3d, representation_2d = create_box_objects( @@ -1520,7 +1554,9 @@ class LibraryGenerator: representation_3d, representation_2d = create_box_objects( overall_width, overall_depth, overall_height, return_representations=True ) - self.create_explicit_type("IfcElectricApplianceType", "Generic Small Fridge Zone", representation_3d, representation_2d) + self.create_explicit_type( + "IfcElectricApplianceType", "Generic Small Fridge Zone", representation_3d, representation_2d + ) overall_width, overall_depth, overall_height = (900.0, 700.0, 1900.0) representation_3d, representation_2d = create_box_objects( @@ -1534,7 +1570,9 @@ class LibraryGenerator: representation_3d, representation_2d = create_box_objects( overall_width, overall_depth, overall_height, return_representations=True ) - self.create_explicit_type("IfcElectricApplianceType", "Generic Large Fridge Zone", representation_3d, representation_2d) + self.create_explicit_type( + "IfcElectricApplianceType", "Generic Large Fridge Zone", representation_3d, representation_2d + ) def create_sink(width, depth, height, add_table): shift_to_center = V(-width / 2, -depth) @@ -1670,7 +1708,9 @@ class LibraryGenerator: overall_width, overall_depth, overall_height = (595.0, 680.0, 850.0) representation_3d, representation_2d = create_washing_machine(overall_width, overall_depth, overall_height) - self.create_explicit_type("IfcElectricApplianceType", "Neufert Washing Machine", representation_3d, representation_2d) + self.create_explicit_type( + "IfcElectricApplianceType", "Neufert Washing Machine", representation_3d, representation_2d + ) def create_dryer_machine(width, depth, height): items_3d, items_2d = create_box_objects(width, depth, height) @@ -1797,13 +1837,17 @@ class LibraryGenerator: ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation_2d ) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) return element def create_layer_set_type(self, name, data): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=data["type"], name=name) element.Description = data["Description"] - rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet" + ) layer_set = rel.RelatingMaterial for layer_data in data["Layers"]: layer = ifcopenshell.api.run( @@ -1811,33 +1855,43 @@ class LibraryGenerator: ) layer.Name = layer_data[0] layer.LayerThickness = layer_data[2] - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) return element def create_layer_type(self, ifc_class, name, thickness): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet" + ) layer_set = rel.RelatingMaterial layer = ifcopenshell.api.run( "material.add_layer", self.file, layer_set=layer_set, material=self.materials["TBD"]["ifc"] ) layer.LayerThickness = thickness - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) return element def create_profile_type(self, ifc_class, name, profile): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) - rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet" + ) profile_set = rel.RelatingMaterial material_profile = ifcopenshell.api.run( "material.add_profile", self.file, profile_set=profile_set, - material=self.material + material=self.material, # material=self.materials["TBD"]["ifc"] ) ifcopenshell.api.run("material.assign_profile", self.file, material_profile=material_profile, profile=profile) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) def create_type(self, ifc_class, name, representations): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name) @@ -1869,7 +1923,9 @@ class LibraryGenerator: ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation ) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) if __name__ == "__main__": diff --git a/src/blenderbim/scripts/generate_landscape_library.py b/src/blenderbim/scripts/generate_landscape_library.py index 06cec8c6df..526b0b3a9c 100644 --- a/src/blenderbim/scripts/generate_landscape_library.py +++ b/src/blenderbim/scripts/generate_landscape_library.py @@ -394,9 +394,9 @@ class LibraryGenerator: TreeData("Lollipop Tree (14m)", "simple", (14.0, 7.5, 0.44)), TreeData("Lollipop Tree (18m)", "simple", (18.0, 9.5, 0.44)), TreeData("Lollipop Tree (25m)", "simple", (25.0, 13.5, 0.50)), - TreeData("Tree Small (8m)", "low_poly", (8.0, 6.0, 0.0, .8, 1.0, 0.2, 10)), - TreeData("Tree Medium (12m)", "low_poly", (12.0, 10.0, 0.0, .8, 2.0, 0.4, 20)), - TreeData("Tree Big (20m)", "low_poly", (20.0, 15.0, 0.0, .8, 4.0, 0.6, 30)), + TreeData("Tree Small (8m)", "low_poly", (8.0, 6.0, 0.0, 0.8, 1.0, 0.2, 10)), + TreeData("Tree Medium (12m)", "low_poly", (12.0, 10.0, 0.0, 0.8, 2.0, 0.4, 20)), + TreeData("Tree Big (20m)", "low_poly", (20.0, 15.0, 0.0, 0.8, 4.0, 0.6, 30)), ] for tree_data in trees: @@ -409,7 +409,7 @@ class LibraryGenerator: # From tree species table - with open(os.path.join(BLEND_DIR, "tree_species.csv"), 'r') as csvfile: + with open(os.path.join(BLEND_DIR, "tree_species.csv"), "r") as csvfile: reader = csv.reader(csvfile) for i, row in enumerate(reader): if i == 0: @@ -418,9 +418,7 @@ class LibraryGenerator: data = [float(x) for x in row[2:]] preset_data = preset.preset_class(*data)._asdict() tree_geometry = preset.generator(builder, **preset_data) - self.create_explicit_type( - "IfcGeographicElementType", row[1], **self.get_representations(tree_geometry) - ) + self.create_explicit_type("IfcGeographicElementType", row[1], **self.get_representations(tree_geometry)) self.file.write(output_filename) @@ -447,7 +445,9 @@ class LibraryGenerator: ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation_2d ) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) return element def create_type(self, ifc_class, name, representations): @@ -480,8 +480,9 @@ class LibraryGenerator: ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation ) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) - + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) if __name__ == "__main__": diff --git a/src/blenderbim/scripts/generate_site_library.py b/src/blenderbim/scripts/generate_site_library.py index 73a9ad96a7..82cc413372 100644 --- a/src/blenderbim/scripts/generate_site_library.py +++ b/src/blenderbim/scripts/generate_site_library.py @@ -98,7 +98,9 @@ class LibraryGenerator: ifcopenshell.api.run( "geometry.assign_representation", self.file, product=element, representation=representation ) - ifcopenshell.api.run("project.assign_declaration", self.file, definitions=[element], relating_context=self.library) + ifcopenshell.api.run( + "project.assign_declaration", self.file, definitions=[element], relating_context=self.library + ) LibraryGenerator().generate() diff --git a/src/blenderbim/scripts/headless_import.py b/src/blenderbim/scripts/headless_import.py index 07596d6c08..441d322b4d 100644 --- a/src/blenderbim/scripts/headless_import.py +++ b/src/blenderbim/scripts/headless_import.py @@ -12,12 +12,12 @@ props = bpy.context.scene.BIMGeoreferenceProperties # georeferencing coordinates in the IFC Georeferencing panel before filling out # these details. -#props.blender_eastings = '334134181.0' -#props.blender_northings = '6254570511.0' -#props.blender_orthogonal_height = '0.0' -#props.blender_x_axis_abscissa = '0.147487884244522' -#props.blender_x_axis_ordinate = '0.989063862448262' -#props.has_blender_offset = True +# props.blender_eastings = '334134181.0' +# props.blender_northings = '6254570511.0' +# props.blender_orthogonal_height = '0.0' +# props.blender_x_axis_abscissa = '0.147487884244522' +# props.blender_x_axis_ordinate = '0.989063862448262' +# props.has_blender_offset = True props = bpy.context.scene.BIMProjectProperties diff --git a/src/blenderbim/scripts/installation/dev_environment.bat b/src/blenderbim/scripts/installation/dev_environment.bat index c56f544092..bf7da943e4 100644 --- a/src/blenderbim/scripts/installation/dev_environment.bat +++ b/src/blenderbim/scripts/installation/dev_environment.bat @@ -1,21 +1,25 @@ @echo off -rem SETUP BLENDER-BIM LIVE DEVELOPMENT ENVIRONMENT -rem Update BLENDER_VERSION, PYTHON_VERSION, BBIM_REPO in the script bellow. -rem Put the script to the folder where IfcOpenShell git repository is located -rem (script will try to clone IfcOpenShell.git if it's not present). +echo SETUP BLENDERBIM ADD-ON LIVE DEVELOPMENT ENVIRONMENT +echo Update BLENDER, BLENDER_VERSION, PYTHON_VERSION, BBIM_REPO, BLENDER_USER_NAME in the script bellow. +echo Put the script at the root of your IfcOpenShell git repository +echo This script needs to be run as administrator (to create symbolic links) +echo Make sure you have followed these steps before proceeding :) + +pause + SET BLENDER_VERSION=4.2 SET PYTHON_VERSION=3.11 SET BBIM_REPO=user_default +rem Can be useful e.g. if you're running script from administrator account +rem and need to setup dev environment for some other user. +SET BLENDER_USER_NAME=%USERNAME% -SET BLENDER=%appdata%\Blender Foundation\Blender\%BLENDER_VERSION% +SET BLENDER=%HOMEDRIVE%\Users\%BLENDER_USER_NAME%\AppData\Roaming\Blender Foundation\Blender\%BLENDER_VERSION% SET BLENDER_LOCAL=%BLENDER%\extensions\.local\lib\python%PYTHON_VERSION%\site-packages SET BLENDERBIM=%BLENDER_LOCAL%\blenderbim SET BLENDER_EXT=%BLENDER%\extensions\%BBIM_REPO%\blenderbim -git clone https://github.com/IfcOpenShell/IfcOpenShell.git -cd IfcOpenShell - echo Removing the Blender add-on Python code... del "%BLENDER_EXT%\__init__.py" rd /S /Q "%BLENDERBIM%" diff --git a/src/blenderbim/scripts/replace_drawing_path.py b/src/blenderbim/scripts/replace_drawing_path.py index 3a9205305a..11870c4513 100644 --- a/src/blenderbim/scripts/replace_drawing_path.py +++ b/src/blenderbim/scripts/replace_drawing_path.py @@ -40,26 +40,30 @@ if not file: print("No ifc file") sys.exit() -annotations = file.by_type('IfcAnnotation') +annotations = file.by_type("IfcAnnotation") count = 0 for annotation in annotations: - if annotation.ObjectType == 'DRAWING': + if annotation.ObjectType == "DRAWING": psets = ifcopenshell.util.element.get_psets(annotation) - pset = psets['EPset_Drawing'] - pset_ifc = file.by_id(pset['id']) + pset = psets["EPset_Drawing"] + pset_ifc = file.by_id(pset["id"]) wrong_sep = "" - if platform == 'linux' or platform == 'linux2' or platform == 'darwin': + if platform == "linux" or platform == "linux2" or platform == "darwin": wrong_sep = "\\" - if platform == 'win32': + if platform == "win32": wrong_sep = "/" - ifcopenshell.api.run("pset.edit_pset", file, pset = pset_ifc, properties = { - 'Stylesheet' : pset['Stylesheet'].replace(wrong_sep, os.path.sep), - 'Markers' : pset['Markers'].replace(wrong_sep, os.path.sep), - 'Symbols' : pset['Symbols'].replace(wrong_sep, os.path.sep), - 'Patterns' : pset['Patterns'].replace(wrong_sep, os.path.sep), - }) - count+=1 + ifcopenshell.api.run( + "pset.edit_pset", + file, + pset=pset_ifc, + properties={ + "Stylesheet": pset["Stylesheet"].replace(wrong_sep, os.path.sep), + "Markers": pset["Markers"].replace(wrong_sep, os.path.sep), + "Symbols": pset["Symbols"].replace(wrong_sep, os.path.sep), + "Patterns": pset["Patterns"].replace(wrong_sep, os.path.sep), + }, + ) + count += 1 print(f"DONE! Checked {count} drawings") - diff --git a/src/blenderbim/scripts/shape_builder_examples.py b/src/blenderbim/scripts/shape_builder_examples.py index 6561f0badc..806a7b9f8a 100644 --- a/src/blenderbim/scripts/shape_builder_examples.py +++ b/src/blenderbim/scripts/shape_builder_examples.py @@ -9,7 +9,7 @@ V = lambda *x: Vector([float(i) for i in x]) def simple_uses(): ifc_file = ifcopenshell.file() builder = ShapeBuilder(ifc_file) - triangle_curve = builder.polyline(( (0., 0.), (1., 2.), (2., 0.) ), closed=True) + triangle_curve = builder.polyline(((0.0, 0.0), (1.0, 2.0), (2.0, 0.0)), closed=True) print(triangle_curve) rectangle_curve = builder.rectangle() @@ -18,26 +18,19 @@ def simple_uses(): circle_curve = builder.circle() print(circle_curve) - translated_rect = builder.translate(rectangle_curve, Vector( (2.5, 2.5) ), create_copy=True) + translated_rect = builder.translate(rectangle_curve, Vector((2.5, 2.5)), create_copy=True) print(translated_rect) - rotated_triangle = builder.rotate(triangle_curve, 90, - pivot_point=Vector( (1., 1.) ), - counter_clockwise=True, - create_copy=True) + rotated_triangle = builder.rotate( + triangle_curve, 90, pivot_point=Vector((1.0, 1.0)), counter_clockwise=True, create_copy=True + ) print(rotated_triangle) - a, b = builder.translate( - [rectangle_curve, circle_curve], - Vector( (5., 5.) ), create_copy=True) + a, b = builder.translate([rectangle_curve, circle_curve], Vector((5.0, 5.0)), create_copy=True) print(f"Multiple translated objects: {a}, {b}") - c, d = builder.rotate( - [a, b], 90, - pivot_point=Vector( (0., 0.) ), - counter_clockwise=False, - create_copy=True) - print(f'Multiple rotated objects: {c}, {d}') + c, d = builder.rotate([a, b], 90, pivot_point=Vector((0.0, 0.0)), counter_clockwise=False, create_copy=True) + print(f"Multiple rotated objects: {c}, {d}") mirrored_objects = builder.mirror( [a, b], @@ -57,8 +50,7 @@ def simple_uses(): print(rotated_circle) profile = builder.profile( - rectangle_curve, "test_profile", - inner_curves=[builder.circle(center=Vector((0.5, 0.5)), radius=0.2)] + rectangle_curve, "test_profile", inner_curves=[builder.circle(center=Vector((0.5, 0.5)), radius=0.2)] ) print(profile) @@ -129,20 +121,14 @@ def mirror_placement_test(): rect_profile = builder.profile(rect, inner_curves=rect_small) - pl = builder.extrude(rect_profile, - magnitude=7.0, - position_z_axis=V(0, -1, 0), - extrusion_vector=V(0, 0, -1), - position=V(0, 5, 0) + pl = builder.extrude( + rect_profile, magnitude=7.0, position_z_axis=V(0, -1, 0), extrusion_vector=V(0, 0, -1), position=V(0, 5, 0) ) rect = builder.rectangle(size=size) - back_wall = builder.extrude(rect, - magnitude=2.0, - position_z_axis=V(0, -1, 0), - extrusion_vector=V(0, 0, -1), - position=V(0, 1, 0) + back_wall = builder.extrude( + rect, magnitude=2.0, position_z_axis=V(0, -1, 0), extrusion_vector=V(0, 0, -1), position=V(0, 1, 0) ) items_3d = [pl, back_wall] @@ -195,20 +181,18 @@ def curve_between_two_points_test(): width, depth = (200.0, 200.0) curve_coords = ( - (V(0, depth), V(-width, 0)), # ccw - (V(0, depth), V(width, 0)), # cw - (V(0, -depth), V(width, 0)), # ccw - (V(0, -depth), V(-width, 0)), # cw - - (V(+width/2, 0), V(0, depth)), # ccw - (V(-width/2, 0), V(0, depth)), # cw - (V(-width/2, 0), V(0, -depth)), # ccw - (V(+width/2, 0), V(0, -depth)), # cw - - (V(0, depth/2), V(+width, 0)), # cw - (V(0, depth/2), V(-width, 0)), # ccw - (V(0, -depth/2), V(+width, 0)), # ccw - (V(0, -depth/2), V(-width, 0)), # cw + (V(0, depth), V(-width, 0)), # ccw + (V(0, depth), V(width, 0)), # cw + (V(0, -depth), V(width, 0)), # ccw + (V(0, -depth), V(-width, 0)), # cw + (V(+width / 2, 0), V(0, depth)), # ccw + (V(-width / 2, 0), V(0, depth)), # cw + (V(-width / 2, 0), V(0, -depth)), # ccw + (V(+width / 2, 0), V(0, -depth)), # cw + (V(0, depth / 2), V(+width, 0)), # cw + (V(0, depth / 2), V(-width, 0)), # ccw + (V(0, -depth / 2), V(+width, 0)), # ccw + (V(0, -depth / 2), V(-width, 0)), # cw ) items_2d = [builder.curve_between_two_points(c) for c in curve_coords] diff --git a/src/blenderbim/scripts/standalone_blender_import.py b/src/blenderbim/scripts/standalone_blender_import.py index 6d2aa3e28e..c679525b35 100644 --- a/src/blenderbim/scripts/standalone_blender_import.py +++ b/src/blenderbim/scripts/standalone_blender_import.py @@ -6,7 +6,7 @@ import multiprocessing class BlenderImporter: def __init__(self): - self.file = ifcopenshell.open('/home/dion/untitled.ifc') + self.file = ifcopenshell.open("/home/dion/untitled.ifc") self.cache_path = "cache.h5" self.should_use_cpu_multiprocessing = True self.deflection_tolerance = 0.001 @@ -77,9 +77,7 @@ class BlenderImporter: return results settings = self.settings_2d if is_curve else self.settings if self.should_use_cpu_multiprocessing: - iterator = ifcopenshell.geom.iterator( - settings, self.file, multiprocessing.cpu_count(), include=products - ) + iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products) else: iterator = ifcopenshell.geom.iterator(settings, self.file, include=products) cache = self.get_cache() @@ -120,4 +118,5 @@ class BlenderImporter: except: return + BlenderImporter().execute() diff --git a/src/blenderbim/test/bim/feature/aggregate.feature b/src/blenderbim/test/bim/feature/aggregate.feature index 7e6e498f56..bf75754b36 100644 --- a/src/blenderbim/test/bim/feature/aggregate.feature +++ b/src/blenderbim/test/bim/feature/aggregate.feature @@ -22,9 +22,7 @@ Scenario: Assign object And I press "bim.enable_editing_aggregate" And the variable "relating_object" is "tool.Ifc.get().by_type('IfcSite')[0].id()" When I press "bim.aggregate_assign_object(relating_object={relating_object})" - Then the object "IfcSite/My Site" is in the collection "IfcSite/My Site" - And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey" - And the collection "IfcBuildingStorey/My Storey" is in the collection "IfcSite/My Site" + Then the object "IfcBuildingStorey/My Storey" is aggregated by object "IfcSite/My Site" Scenario: Unassign object Given an empty IFC project @@ -34,29 +32,7 @@ Scenario: Unassign object And I press "bim.aggregate_assign_object(relating_object={relating_object})" And the object "IfcBuildingStorey/My Storey" is selected When I press "bim.aggregate_unassign_object" - Then the object "IfcSite/My Site" is in the collection "IfcSite/My Site" - And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey" - And the collection "IfcBuildingStorey/My Storey" is in the collection "IfcProject/My Project" - -Scenario: Unassign object - multiple objects are contained again to their indirect container - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_class" to "IfcMember" - And I press "bim.assign_class" - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_class" to "IfcCovering" - And I press "bim.assign_class" - And the object "IfcMember/Cube" is selected - And additionally the object "IfcCovering/Cube" is selected - And I press "bim.add_aggregate(ifc_class='IfcWall')" - And the object "IfcMember/Cube" is selected - And additionally the object "IfcCovering/Cube" is selected - When I press "bim.aggregate_unassign_object" - Then the object "IfcMember/Cube" is in the collection "IfcBuildingStorey/My Storey" - And the object "IfcCovering/Cube" is in the collection "IfcBuildingStorey/My Storey" + Then the object "IfcBuildingStorey/My Storey" has no aggregate Scenario: Add aggregate Given an empty IFC project @@ -67,23 +43,9 @@ Scenario: Add aggregate And I press "bim.assign_class" And the object "IfcWall/Cube" is selected When I press "bim.add_aggregate" - Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly" - And the object "IfcElementAssembly/Assembly" is in the collection "IfcElementAssembly/Assembly" - And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey" - -Scenario: Add aggregate - with the aggregate inheriting the existing spatial collection - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" - And I press "bim.assign_class" - And the object "IfcWall/Cube" is placed in the collection "IfcBuildingStorey/My Storey" - And the object "IfcWall/Cube" is selected - When I press "bim.add_aggregate" - Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly" - And the object "IfcElementAssembly/Assembly" is in the collection "IfcElementAssembly/Assembly" - And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey" + Then the object "IfcElementAssembly/Default_Name" exists + And the object "IfcWall/Cube" is aggregated by object "IfcElementAssembly/Default_Name" + And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey" Scenario: Add aggregate - add a nested aggregate Given an empty IFC project @@ -92,15 +54,15 @@ Scenario: Add aggregate - add a nested aggregate And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And the object "IfcWall/Cube" is placed in the collection "IfcBuildingStorey/My Storey" And the object "IfcWall/Cube" is selected When I press "bim.add_aggregate" And the object "IfcWall/Cube" is selected - And I press "bim.add_aggregate" - Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly.001" - And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001" - And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly" - And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey" + And I press "bim.add_aggregate(aggregate_name='Default_Name2')" + Then the object "IfcElementAssembly/Default_Name" exists + And the object "IfcElementAssembly/Default_Name" is contained in object "IfcBuildingStorey/My Storey" + And the object "IfcElementAssembly/Default_Name2" exists + And the object "IfcWall/Cube" is aggregated by object "IfcElementAssembly/Default_Name2" + And the object "IfcElementAssembly/Default_Name2" is aggregated by object "IfcElementAssembly/Default_Name" Scenario: Add aggregate - add multiple elements to a custom aggregate class Given an empty IFC project @@ -115,8 +77,7 @@ Scenario: Add aggregate - add multiple elements to a custom aggregate class And I press "bim.assign_class" And the object "IfcMember/Cube" is selected And additionally the object "IfcCovering/Cube" is selected - When I press "bim.add_aggregate(ifc_class='IfcWall')" - Then the object "IfcMember/Cube" is in the collection "IfcWall/Assembly" - And the object "IfcCovering/Cube" is in the collection "IfcWall/Assembly" - And the object "IfcWall/Assembly" is in the collection "IfcWall/Assembly" - And the collection "IfcWall/Assembly" is in the collection "IfcBuildingStorey/My Storey" + When I press "bim.add_aggregate(ifc_class='IfcWall', aggregate_name='Assembly')" + Then the object "IfcMember/Cube" is aggregated by object "IfcWall/Assembly" + And the object "IfcCovering/Cube" is aggregated by object "IfcWall/Assembly" + And the object "IfcWall/Assembly" is contained in object "IfcBuildingStorey/My Storey" diff --git a/src/blenderbim/test/bim/feature/attribute.feature b/src/blenderbim/test/bim/feature/attribute.feature index 0810ec6068..04ef05a201 100644 --- a/src/blenderbim/test/bim/feature/attribute.feature +++ b/src/blenderbim/test/bim/feature/attribute.feature @@ -4,34 +4,34 @@ Feature: Attribute Scenario: Enable editing attributes Given an empty IFC project And the object "IfcSite/My Site" is selected - When I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" + When I press "bim.enable_editing_attributes(obj='IfcSite/My Site')" Then nothing happens Scenario: Disable editing attributes Given an empty IFC project And the object "IfcSite/My Site" is selected - And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" - When I press "bim.disable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" + And I press "bim.enable_editing_attributes(obj='IfcSite/My Site')" + When I press "bim.disable_editing_attributes(obj='IfcSite/My Site')" Then nothing happens Scenario: Edit attributes Given an empty IFC project And the object "IfcSite/My Site" is selected - And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" + And I press "bim.enable_editing_attributes(obj='IfcSite/My Site')" And I set "active_object.BIMAttributeProperties.attributes[1].string_value" to "Name" - When I press "bim.edit_attributes(obj='IfcSite/My Site', obj_type='Object')" + When I press "bim.edit_attributes(obj='IfcSite/My Site')" Then the object "IfcSite/Name" is an "IfcSite" And the object "IfcSite/My Site" does not exist Scenario: Edit attributes - longitude / latitude Given an empty IFC project And the object "IfcSite/My Site" is selected - And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" + And I press "bim.enable_editing_attributes(obj='IfcSite/My Site')" Then "active_object.BIMAttributeProperties.attributes[6].name" is "RefLatitude" When I set "active_object.BIMAttributeProperties.attributes[6].string_value" to "[1,2]" - And I press "bim.edit_attributes(obj='IfcSite/My Site', obj_type='Object')" - And I press "bim.enable_editing_attributes(obj='IfcSite/My Site', obj_type='Object')" - And I press "bim.edit_attributes(obj='IfcSite/My Site', obj_type='Object')" + And I press "bim.edit_attributes(obj='IfcSite/My Site')" + And I press "bim.enable_editing_attributes(obj='IfcSite/My Site')" + And I press "bim.edit_attributes(obj='IfcSite/My Site')" Then nothing happens Scenario: Copy attribute to selected @@ -48,7 +48,7 @@ Scenario: Copy attribute to selected And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And additionally the object "IfcWall/Cube.001" is selected - And I press "bim.enable_editing_attributes(obj='IfcWall/Cube.001', obj_type='Object')" + And I press "bim.enable_editing_attributes(obj='IfcWall/Cube.001')" And I set "active_object.BIMAttributeProperties.attributes[2].string_value" to "Foo" When I press "bim.copy_attribute_to_selection(name='Description')" Then nothing happens diff --git a/src/blenderbim/test/bim/feature/bimtester.feature b/src/blenderbim/test/bim/feature/bimtester.feature deleted file mode 100644 index dd4629ad7e..0000000000 --- a/src/blenderbim/test/bim/feature/bimtester.feature +++ /dev/null @@ -1,9 +0,0 @@ -@bimtester -Feature: Bimtester - -Scenario: Execute Bimtester - Given an empty IFC project - When I set "scene.BimTesterProperties.should_load_from_memory" to "True" - And I set "scene.BimTesterProperties.feature" to "{cwd}/test/files/sample.feature" - And I press "bim.execute_bim_tester" - Then nothing happens diff --git a/src/blenderbim/test/bim/feature/classification.feature b/src/blenderbim/test/bim/feature/classification.feature index 59a1e326f5..7b13afe35b 100644 --- a/src/blenderbim/test/bim/feature/classification.feature +++ b/src/blenderbim/test/bim/feature/classification.feature @@ -192,13 +192,14 @@ Scenario: Add classification reference - material And the variable "classification" is "{classification_ifc}.by_type('IfcClassification')[0].id()" And I set "scene.BIMClassificationProperties.available_classifications" to "{classification}" And I press "bim.add_classification" - And I add a cube - And I add a material - And I press "bim.add_material(obj='Material')" + And I press "bim.add_material(name='Material')" + And I press "bim.load_materials" + And I press "bim.expand_material_category(category='Uncategorised')" + And I set "scene.BIMMaterialProperties.active_material_index" to "1" And the variable "material" is "{ifc}.by_type('IfcMaterial')[0].id()" And I press "bim.change_classification_level(parent_id={classification})" And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()" - When I press "bim.add_classification_reference(reference={reference}, obj='Material', obj_type='Material')" + When I press "bim.add_classification_reference(reference={reference}, obj_type='Material')" Then nothing happens Scenario: Remove classification reference - material @@ -207,14 +208,14 @@ Scenario: Remove classification reference - material And the variable "classification" is "{classification_ifc}.by_type('IfcClassification')[0].id()" And I set "scene.BIMClassificationProperties.available_classifications" to "{classification}" And I press "bim.add_classification" - And I add a cube - And I add a material - And I press "bim.add_material(obj='Material')" - And the variable "material" is "{ifc}.by_type('IfcMaterial')[0].id()" + And I press "bim.add_material(name='Material')" + And I press "bim.load_materials" + And I press "bim.expand_material_category(category='Uncategorised')" + And I set "scene.BIMMaterialProperties.active_material_index" to "1" And I press "bim.change_classification_level(parent_id={classification})" And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()" - And I press "bim.add_classification_reference(reference={reference}, obj='Material', obj_type='Material')" - When I press "bim.remove_classification_reference(reference={reference}, obj='Material', obj_type='Material')" + And I press "bim.add_classification_reference(reference={reference}, obj_type='Material')" + When I press "bim.remove_classification_reference(reference={reference}, obj_type='Material')" Then nothing happens Scenario: Add classification reference - cost diff --git a/src/blenderbim/test/bim/feature/cost.feature b/src/blenderbim/test/bim/feature/cost.feature index e74e6b003d..9cb611aa70 100644 --- a/src/blenderbim/test/bim/feature/cost.feature +++ b/src/blenderbim/test/bim/feature/cost.feature @@ -377,7 +377,7 @@ Scenario: Assign cost item quantity - quantity based And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')" - And I press "bim.calculate_all_quantities" + And I press "bim.perform_quantity_take_off" When I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='NetVolume')" Then nothing happens diff --git a/src/blenderbim/test/bim/feature/covering.feature b/src/blenderbim/test/bim/feature/covering.feature index bb7bdaf45d..0800e0edb5 100644 --- a/src/blenderbim/test/bim/feature/covering.feature +++ b/src/blenderbim/test/bim/feature/covering.feature @@ -8,8 +8,36 @@ Scenario: Execute generate flooring coverings from walls And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + # 1st wall And I press "bim.add_constr_type_instance" And the object "IfcWall/Wall" is selected - When I press "bim.add_instance_flooring_coverings_from_walls" - Then nothing happens - + And I press "bim.change_layer_length(length=3.6)" + # 2nd wall + And the cursor is at "3.6,0.1,3" + And I set "scene.BIMModelProperties.length" to "2.0" + And I press "bim.add_constr_type_instance" + # 3rd wall + And the cursor is at "3.5,2.1,3" + And I set "scene.BIMModelProperties.length" to "3.5" + And I press "bim.add_constr_type_instance" + # 4th wall + And the cursor is at "0,2.0,0" + And I set "scene.BIMModelProperties.length" to "1.9" + And I press "bim.add_constr_type_instance" + # add_instance_flooring_coverings_from_walls is expecting FLOORING predefined type. + And the object "IfcCoveringType/COV30" is selected + And I press "bim.enable_editing_attributes(obj='IfcCoveringType/COV30')" + And I set "active_object.BIMAttributeProperties.attributes[6].enum_value" to "FLOORING" + And I press "bim.edit_attributes(obj='IfcCoveringType/COV30')" + # Run the operator. + When the object "IfcWall/Wall" is selected + And additionally the object "IfcWall/Wall.001" is selected + And additionally the object "IfcWall/Wall.002" is selected + And additionally the object "IfcWall/Wall.003" is selected + And I set "scene.BIMModelProperties.ifc_class" to "IfcCoveringType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcCoveringType') if e.Name == 'COV30'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.add_instance_flooring_coverings_from_walls" + Then the object "IfcCovering/Covering0" exists + And the object "IfcCovering/Covering0" is at "1.8,1.05,0.0" + And the object "IfcCovering/Covering0" dimensions are "3.4,1.9,0.03" diff --git a/src/blenderbim/test/bim/feature/document.feature b/src/blenderbim/test/bim/feature/document.feature index 2157a79b2f..db14740fbe 100644 --- a/src/blenderbim/test/bim/feature/document.feature +++ b/src/blenderbim/test/bim/feature/document.feature @@ -89,7 +89,6 @@ Scenario: Assign document And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" When I press "bim.assign_document(document={reference})" @@ -106,7 +105,6 @@ Scenario: Unassign document And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.assign_document(document={reference})" diff --git a/src/blenderbim/test/bim/feature/drawing.feature b/src/blenderbim/test/bim/feature/drawing.feature index 28c2b5d80c..762be4e8ef 100644 --- a/src/blenderbim/test/bim/feature/drawing.feature +++ b/src/blenderbim/test/bim/feature/drawing.feature @@ -93,7 +93,6 @@ Scenario: Remove drawing - deleting active drawing And the collection "IfcAnnotation/PLAN_VIEW" exists And I set "scene.DocProperties.active_drawing_index" to "0" And I press "bim.activate_drawing(drawing={drawing})" - And the object "IfcAnnotation/PLAN_VIEW" is selected - When I press "bim.override_object_delete" + When the object "IfcAnnotation/PLAN_VIEW" is selected + And I press "bim.override_object_delete" Then the collection "IfcAnnotation/PLAN_VIEW" does not exist - diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index 8e5a8c7214..efb5e81660 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -50,7 +50,6 @@ Scenario: Add representation - add a representation with a scale factor applied And the object "Cube" is selected When the object "Cube" is scaled to "2" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" has no scale @@ -63,7 +62,6 @@ Scenario: Add representation - add a representation with a scale factor removed And the object "Cube" is selected When the object "Cube" is scaled to "2" And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" Then the object "IfcWall/Cube" has no scale @@ -95,7 +93,7 @@ Scenario: Switch representation - current edited representation is updated prior And I press "bim.switch_representation(ifc_definition_id={representation}, should_reload=True)" And the variable "representation" is "[r for r in {ifc}.by_type('IfcShapeRepresentation') if r.RepresentationType=='Annotation2D'][0].id()" And I press "bim.switch_representation(ifc_definition_id={representation}, should_reload=True)" - When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" Then the object "IfcWall/Cube" dimensions are "4,4,0" Scenario: Switch representation - current edited representation is discarded if switching to a box @@ -110,7 +108,7 @@ Scenario: Switch representation - current edited representation is discarded if And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)" And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)" - When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" Then the object "IfcWall/Cube" dimensions are "2,2,2" Scenario: Switch representation - existing Blender modifiers must be purged @@ -133,8 +131,10 @@ Scenario: Remove representation - remove an active representation And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" - And I press "bim.remove_representation(representation_id={representation})" + When the variable "representation_body" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" + And the variable "representation_bbox" is "{ifc}.by_type('IfcShapeRepresentation')[1].id()" + And I press "bim.remove_representation(representation_id={representation_body})" + And I press "bim.remove_representation(representation_id={representation_bbox})" Then the object "IfcWall/Cube" has no data Scenario: Remove representation - remove an unloaded representation @@ -162,8 +162,10 @@ Scenario: Remove representation - remove an instanced representation from an act And I press "bim.add_constr_type_instance" And I press "bim.add_constr_type_instance" And the object "IfcWallType/Cube" is selected - When the variable "representation" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()" - And I press "bim.remove_representation(representation_id={representation})" + When the variable "representation_body" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()" + And the variable "representation_bbox" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[0].MappedRepresentation.id()" + And I press "bim.remove_representation(representation_id={representation_body})" + And I press "bim.remove_representation(representation_id={representation_bbox})" Then the object "IfcWallType/Cube" has no data Then the object "IfcWall/Wall" has no data Then the object "IfcWall/Wall.001" has no data @@ -181,8 +183,10 @@ Scenario: Remove representation - remove an instanced representation from an act And I press "bim.add_constr_type_instance" And I press "bim.add_constr_type_instance" And the object "IfcWall/Wall" is selected - When the variable "representation" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()" - And I press "bim.remove_representation(representation_id={representation})" + When the variable "representation_body" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()" + And the variable "representation_bbox" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[0].id()" + And I press "bim.remove_representation(representation_id={representation_body})" + And I press "bim.remove_representation(representation_id={representation_bbox})" Then the object "IfcWallType/Cube" has no data Then the object "IfcWall/Wall" has no data Then the object "IfcWall/Wall.001" has no data @@ -192,7 +196,6 @@ Scenario: Update representation - updating a tessellation And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I press "bim.update_representation(obj='IfcWall/Cube')" @@ -203,7 +206,6 @@ Scenario: Update representation - updating a layered extrusion And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And I add an empty @@ -211,7 +213,7 @@ Scenario: Update representation - updating a layered extrusion And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -239,7 +241,7 @@ Scenario: Update representation - updating a profiled extrusion And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -331,19 +333,24 @@ Scenario: Override duplicate move - copying a coloured representation Given an empty IFC project And I add a cube And the object "Cube" is selected - And I add a material And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected - And the material "Material" colour is set to "1,0,0,1" + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" + And I set "scene.BIMStylesProperties.surface_colour" to "[1.0,0.0,0.0]" + And I press "bim.add_presentation_style" + And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" + And I press "bim.assign_style_to_selected(style_id={style})" When I duplicate the selected objects - And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" - Then the material "Material" colour is "1,0,0,1" - And the object "IfcWall/Cube" has the material "Material" - And the object "IfcWall/Cube.001" has the material "Material" + Then the material "Style" colour is "1,0,0,1" + And the object "IfcWall/Cube" has the material "Style" + And the object "IfcWall/Cube.001" has the material "Style" Scenario: Override duplicate move - copying a type instance with a representation map Given an empty IFC project @@ -373,7 +380,7 @@ Scenario: Override duplicate move - copying a layered extrusion And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -403,7 +410,7 @@ Scenario: Override duplicate move - copying a profiled extrusion And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -426,18 +433,14 @@ Scenario: Override duplicate move - copying an aggregate And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected - When I press "bim.add_aggregate" - Then the object "IfcWall/Cube" is in the collection "IfcElementAssembly/Assembly" - And the object "IfcElementAssembly/Assembly" is in the collection "IfcElementAssembly/Assembly" - And the collection "IfcElementAssembly/Assembly" is in the collection "IfcBuildingStorey/My Storey" + When I press "bim.add_aggregate(aggregate_name='Assembly')" When the object "IfcWall/Cube" is selected And additionally the object "IfcElementAssembly/Assembly" is selected When I duplicate the selected objects Then the object "IfcWall/Cube.001" exists - And the object "IfcWall/Cube.001" is in the collection "IfcElementAssembly/Assembly.001" And the object "IfcElementAssembly/Assembly.001" exists - And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001" - And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey" + And the object "IfcWall/Cube.001" is aggregated by object "IfcElementAssembly/Assembly.001" + And the object "IfcElementAssembly/Assembly.001" is contained in object "IfcBuildingStorey/My Storey" Scenario: Override duplicate move - copying objects with connection Given an empty IFC project @@ -450,7 +453,7 @@ Scenario: Override duplicate move - copying objects with connection And the object "IfcWall/Wall" dimensions are "1,0.1,3" And the object "IfcWall/Wall" bottom left corner is at "0,0,0" When I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" When I press "bim.add_constr_type_instance" Then the object "IfcSlab/Slab" is an "IfcSlab" @@ -606,4 +609,3 @@ Scenario: Refresh linked aggregate - after duplicating an object Then the object "IfcWall/Wall_01.001" exists And the object "IfcWall/Wall_02.001" exists And the object "IfcWall/Wall_03.001" exists - diff --git a/src/blenderbim/test/bim/feature/material.feature b/src/blenderbim/test/bim/feature/material.feature index 438eb93358..27db393e65 100644 --- a/src/blenderbim/test/bim/feature/material.feature +++ b/src/blenderbim/test/bim/feature/material.feature @@ -3,13 +3,13 @@ Feature: Material Scenario: Load materials Given an empty IFC project - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" When I press "bim.load_materials" Then nothing happens Scenario: Disable editing materials Given an empty IFC project - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I press "bim.load_materials" When I press "bim.disable_editing_materials" Then nothing happens @@ -17,8 +17,8 @@ Scenario: Disable editing materials Scenario: Load materials - then add material Given an empty IFC project And I press "bim.load_materials" - When I press "bim.add_material(obj='')" - Then the material "Default" exists + When I press "bim.add_material()" + Then the IFC material "Default" exists Scenario: Load materials - then add material set Given an empty IFC project @@ -29,26 +29,8 @@ Scenario: Load materials - then add material set Scenario: Add default material Given an empty IFC project - When I press "bim.add_material(obj='')" - Then the material "Default" exists - -Scenario: Add material - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I add a material - When I press "bim.add_material(obj='Material')" - Then the material "Material" is an IFC material - -Scenario: Remove material - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I add a material - And I press "bim.add_material(obj='Material')" - And the variable "material" is "{ifc}.by_type('IfcMaterial')[0].id()" - When I press "bim.remove_material(material={material})" - Then the material "Material" does not exist + When I press "bim.add_material()" + Then the IFC material "Default" exists Scenario: Remove material set Given an empty IFC project @@ -66,10 +48,10 @@ Scenario: Assign material - single material And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial" And I press "bim.assign_material" - Then the object "IfcWall/Cube" has the material "Default" + Then the object "IfcWall/Cube" has the IFC material "Default" Scenario: Unassign material - single material Given an empty IFC project @@ -78,7 +60,7 @@ Scenario: Unassign material - single material And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial" And I press "bim.assign_material" When I press "bim.unassign_material" @@ -91,7 +73,7 @@ Scenario: Enable editing assigned material - single material And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial" And I press "bim.assign_material" When I press "bim.enable_editing_assigned_material" @@ -104,7 +86,7 @@ Scenario: Disable editing assigned material - single material And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -118,7 +100,7 @@ Scenario: Edit assigned material - single material And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -133,7 +115,7 @@ Scenario: Assign material - material layer set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" Then the object "IfcWallType/Empty" does not have the material "Default" @@ -146,7 +128,7 @@ Scenario: Unassign material - material layer set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" When I press "bim.unassign_material" @@ -166,7 +148,7 @@ Scenario: Unassign material - removing inherited material And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" @@ -175,7 +157,7 @@ Scenario: Unassign material - removing inherited material And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default" - + When I press "bim.unassign_material" Then the object "IfcWall/Cube" has no IFC materials @@ -191,7 +173,7 @@ Scenario: Enable editing assigned material - material layer set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" When I press "bim.enable_editing_assigned_material" @@ -209,7 +191,7 @@ Scenario: Disable editing assigned material - material layer set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -228,7 +210,7 @@ Scenario: Edit assigned material - material layer set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -243,7 +225,7 @@ Scenario: Assign material - material profile set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet" And I press "bim.assign_material" Then the object "IfcWallType/Empty" does not have the material "Default" @@ -256,7 +238,7 @@ Scenario: Unassign material - material profile set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet" And I press "bim.assign_material" When I press "bim.unassign_material" @@ -274,7 +256,7 @@ Scenario: Enable editing assigned material - material profile set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet" And I press "bim.assign_material" When I press "bim.enable_editing_assigned_material" @@ -292,7 +274,7 @@ Scenario: Disable editing assigned material - material profile set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -311,7 +293,7 @@ Scenario: Edit assigned material - material profile set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -326,7 +308,7 @@ Scenario: Assign material - material constituent set And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet" And I press "bim.assign_material" Then nothing happens @@ -338,7 +320,7 @@ Scenario: Unassign material - material constituent set And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet" And I press "bim.assign_material" When I press "bim.unassign_material" @@ -351,7 +333,7 @@ Scenario: Enable editing assigned material - material constituent set And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet" And I press "bim.assign_material" When I press "bim.enable_editing_assigned_material" @@ -364,7 +346,7 @@ Scenario: Disable editing assigned material - material constituent set And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -378,7 +360,7 @@ Scenario: Edit assigned material - material constituent set And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -389,7 +371,7 @@ Scenario: Edit assigned material - material constituent set Scenario: Select by material Given an empty IFC project And I press "bim.load_materials" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And the variable "material" is "{ifc}.by_type('IfcMaterial')[0].id()" When I press "bim.select_by_material(material={material})" Then nothing happens @@ -397,14 +379,14 @@ Scenario: Select by material Scenario: Expand material category Given an empty IFC project And I press "bim.load_materials" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" When I press "bim.expand_material_category(category='')" Then nothing happens Scenario: Contract material category Given an empty IFC project And I press "bim.load_materials" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I press "bim.expand_material_category(category='')" When I press "bim.contract_material_category(category='')" Then nothing happens @@ -435,7 +417,7 @@ Scenario: Add material set layer And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" @@ -457,7 +439,7 @@ Scenario: Remove material set layer And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" diff --git a/src/blenderbim/test/bim/feature/misc.feature b/src/blenderbim/test/bim/feature/misc.feature index e20ef274a8..65173ca76e 100644 --- a/src/blenderbim/test/bim/feature/misc.feature +++ b/src/blenderbim/test/bim/feature/misc.feature @@ -33,7 +33,8 @@ Scenario: Resize to storey And I press "bim.assign_class" And the object "IfcWall/Cube" is selected And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()" - And I press "bim.assign_container(structure={storey})" + And I press "bim.set_default_container(container={storey})" + And I press "bim.assign_container()" When I press "bim.resize_to_storey(total_storeys=1)" Then nothing happens @@ -56,4 +57,4 @@ Scenario: Enabling and disabling IFC Sverchok And I press "preferences.addon_enable(module="sverchok")" And I press "preferences.addon_enable(module="ifcsverchok")" And I press "preferences.addon_disable(module="sverchok")" - And I press "preferences.addon_disable(module="ifcsverchok")" \ No newline at end of file + And I press "preferences.addon_disable(module="ifcsverchok")" diff --git a/src/blenderbim/test/bim/feature/model.feature b/src/blenderbim/test/bim/feature/model.feature index bf9208b012..7467d6af86 100644 --- a/src/blenderbim/test/bim/feature/model.feature +++ b/src/blenderbim/test/bim/feature/model.feature @@ -66,14 +66,6 @@ Scenario: Add grid And the object "IfcGridAxis/02" is an "IfcGridAxis" And the object "IfcGridAxis/03" is an "IfcGridAxis" -Scenario: Pie update container - Given an empty Blender session - And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" - Then the object "IfcSlab/Slab" is in the collection "IfcBuildingStorey/Ground Floor" - When the object "IfcSlab/Slab" is placed in the collection "IfcBuildingStorey/Level 1" - And I press "bim.pie_update_container" - Then nothing happens - Scenario: Add a wall Given an empty IFC project And I load the demo construction library @@ -272,7 +264,7 @@ Scenario: Add a slab Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" When I press "bim.hotkey(hotkey='S_A')" Then the object "IfcSlab/Slab" is an "IfcSlab" @@ -284,7 +276,7 @@ Scenario: Enable editing a slab profile Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I press "bim.hotkey(hotkey='S_A')" And the object "IfcSlab/Slab" is selected @@ -297,7 +289,7 @@ Scenario: Disable editing a slab profile Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I press "bim.hotkey(hotkey='S_A')" And the object "IfcSlab/Slab" is selected @@ -311,7 +303,7 @@ Scenario: Edit a slab profile Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR150'][0].id()" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" And I press "bim.hotkey(hotkey='S_A')" And the object "IfcSlab/Slab" is selected diff --git a/src/blenderbim/test/bim/feature/patch.feature b/src/blenderbim/test/bim/feature/patch.feature index 87e44d50ef..31ceb71c05 100644 --- a/src/blenderbim/test/bim/feature/patch.feature +++ b/src/blenderbim/test/bim/feature/patch.feature @@ -6,8 +6,14 @@ Scenario: Execute IFC Patch - file in memory And I set "scene.BIMPatchProperties.ifc_patch_recipes" to "OffsetObjectPlacements" And I set "scene.BIMPatchProperties.should_load_from_memory" to "True" And I set "scene.BIMPatchProperties.ifc_patch_output" to "{cwd}/test/files/temp/basic-patched.ifc" - And I set "scene.BIMPatchProperties.ifc_patch_args" to "[123454321,0,0,0]" - When I press "bim.execute_ifc_patch(use_json_for_args=True)" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['X'].string_value" to "123454321" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Y'].string_value" to "0" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Z'].string_value" to "0" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Should Rotate First'].bool_value" to "True" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Ax'].string_value" to "0" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Ay'].string_value" to "0" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Az'].string_value" to "0" + When I press "bim.execute_ifc_patch()" Then the file "{cwd}/test/files/temp/basic-patched.ifc" should contain "123454321" Scenario: Execute IFC Patch - file from disk @@ -15,8 +21,14 @@ Scenario: Execute IFC Patch - file from disk And I set "scene.BIMPatchProperties.ifc_patch_recipes" to "OffsetObjectPlacements" And I set "scene.BIMPatchProperties.ifc_patch_input" to "{cwd}/test/files/basic.ifc" And I set "scene.BIMPatchProperties.ifc_patch_output" to "{cwd}/test/files/temp/basic-patched.ifc" - And I set "scene.BIMPatchProperties.ifc_patch_args" to "[123454321,0,0,0]" - When I press "bim.execute_ifc_patch(use_json_for_args=True)" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['X'].string_value" to "123454321" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Y'].string_value" to "0" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Z'].string_value" to "0" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Should Rotate First'].bool_value" to "True" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Ax'].string_value" to "0" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Ay'].string_value" to "0" + And I set "scene.BIMPatchProperties.ifc_patch_args_attr['Az'].string_value" to "0" + When I press "bim.execute_ifc_patch()" Then the file "{cwd}/test/files/temp/basic-patched.ifc" should contain "123454321" Scenario: Run migrate patch diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature index a1bf84113b..11a3023340 100644 --- a/src/blenderbim/test/bim/feature/project.feature +++ b/src/blenderbim/test/bim/feature/project.feature @@ -833,31 +833,31 @@ Scenario: Unlink IFC Scenario: Export IFC - blank project Given an empty IFC project - When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" Then nothing happens Scenario: Export IFC - with basic contents Given an empty Blender session And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" - When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/temp/export.ifc" Scenario: Export IFC - with basic contents and saving as another file Given an empty Blender session And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" - When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc', should_save_as=True)" + When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc', should_save_as=True)" Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/temp/export.ifc" Scenario: Export IFC - with basic contents and saving as IfcJSON where import is not supported Given an empty Blender session And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" - When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifcjson', should_save_as=True)" + When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifcjson', should_save_as=True)" Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/basic.ifc" Scenario: Export IFC - with basic contents and round-tripping an IfcZip Given an empty Blender session And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" - When I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifczip', should_save_as=True)" + When I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifczip', should_save_as=True)" Then "scene.BIMProperties.ifc_file" is "{cwd}/test/files/basic.ifc" When an empty Blender session is started And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifczip')" @@ -867,14 +867,14 @@ Scenario: Export IFC - with basic contents and saving as a relative path Given an empty Blender session And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" When I press "wm.save_mainfile(filepath='{cwd}/test/files/temp/export.blend')" - And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc', use_relative_path=True, save_as_invoked=True)" + And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc', use_relative_path=True, save_as_invoked=True)" Then "scene.BIMProperties.ifc_file" is "export.ifc" Scenario: Export IFC - with deleted objects synchronised Given an empty IFC project When the object "IfcBuildingStorey/My Storey" is selected And I delete the selected objects - And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" Then the object "IfcBuildingStorey/My Storey" does not exist @@ -882,7 +882,7 @@ Scenario: Export IFC - with deleted objects synchronised Scenario: Export IFC - with moved object location synchronised Given an empty IFC project When the object "IfcBuildingStorey/My Storey" is moved to "0,0,1" - And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" Then the object "IfcBuildingStorey/My Storey" is at "0,0,1" @@ -891,7 +891,7 @@ Scenario: Export IFC - with moved grid axis location synchronised Given an empty IFC project And I press "mesh.add_grid" When the object "IfcGridAxis/01" is moved to "1,0,0" - And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" Then the object "IfcGridAxis/01" bottom left corner is at "1,-2,0" @@ -905,23 +905,7 @@ Scenario: Export IFC - with changed object scale synchronised And I press "bim.assign_class" And the object "IfcWall/Cube" is selected When the object "IfcWall/Cube" is scaled to "2" - And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" + And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')" And an empty Blender session is started And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc')" Then the object "IfcWall/Cube" dimensions are "4,4,4" - -Scenario: Export IFC - with changed style element synchronised - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I add a material - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" - And I press "bim.assign_class" - And the object "IfcWall/Cube" is selected - When I add a material - And the material "Material.001" colour is set to "1,0,0,1" - And I press "export_ifc.bim(filepath='{cwd}/test/files/temp/export.ifc')" - And an empty Blender session is started - And I press "bim.load_project(filepath='{cwd}/test/files/temp/export.ifc', should_start_fresh_session=False)" - Then the material "Material.001" colour is "1,0,0,1" diff --git a/src/blenderbim/test/bim/feature/pset.feature b/src/blenderbim/test/bim/feature/pset.feature index c769559e1f..c38a9a9507 100644 --- a/src/blenderbim/test/bim/feature/pset.feature +++ b/src/blenderbim/test/bim/feature/pset.feature @@ -50,20 +50,16 @@ Scenario: Enable pset editing - object Scenario: Enable pset editing - material Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" - And I press "bim.assign_class" - And I press "bim.add_material(obj='')" - And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial" - And I press "bim.assign_material" - And the object "IfcWall/Cube" is selected - And I press "bim.add_pset(obj='Default', obj_type='Material')" - And I set "active_object.active_material.PsetProperties.properties[0].metadata.float_value" to "0.42" - And I press "bim.edit_pset(obj='Default', obj_type='Material')" + And I press "bim.add_material()" + And I press "bim.load_materials" + And I press "bim.expand_material_category(category='Uncategorised')" + And I set "scene.BIMMaterialProperties.active_material_index" to "1" + And I set "scene.MaterialPsetProperties.pset_name" to "Pset_MaterialCommon" + And I press "bim.add_pset(obj='', obj_type='Material')" + And I set "scene.MaterialPsetProperties.properties[0].metadata.float_value" to "0.42" + And I press "bim.edit_pset(obj='', obj_type='Material')" And the variable "pset" is "{ifc}.by_type('IfcMaterialProperties')[-1].id()" - When I press "bim.enable_pset_editing(pset_id={pset}, obj='Default', obj_type='Material')" + When I press "bim.enable_pset_editing(pset_id={pset}, obj='', obj_type='Material')" Then nothing happens Scenario: Enable pset editing - profile @@ -143,15 +139,10 @@ Scenario: Disable pset editing - object Scenario: Disable pset editing - material Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" - And I press "bim.assign_class" - And I press "bim.add_material(obj='')" - And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial" - And I press "bim.assign_material" - And the object "IfcWall/Cube" is selected + And I press "bim.add_material()" + And I press "bim.load_materials" + And I press "bim.expand_material_category(category='Uncategorised')" + And I set "scene.BIMMaterialProperties.active_material_index" to "1" And I press "bim.add_pset(obj='Default', obj_type='Material')" When I press "bim.disable_pset_editing(obj='Default', obj_type='Material')" Then nothing happens @@ -236,17 +227,12 @@ Scenario: Edit qto - object Scenario: Edit pset - material Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" - And I press "bim.assign_class" - And I press "bim.add_material(obj='')" - And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial" - And I press "bim.assign_material" - And the object "IfcWall/Cube" is selected - And I press "bim.add_pset(obj='Default', obj_type='Material')" - When I press "bim.edit_pset(obj='Default', obj_type='Material')" + And I press "bim.add_material()" + And I press "bim.load_materials" + And I press "bim.expand_material_category(category='Uncategorised')" + And I set "scene.BIMMaterialProperties.active_material_index" to "1" + And I press "bim.add_pset(obj='', obj_type='Material')" + When I press "bim.edit_pset(obj='', obj_type='Material')" Then nothing happens Scenario: Edit pset - profile @@ -394,15 +380,15 @@ Scenario: Edit pset length property Scenario: Edit qset length property Given an empty IFC project And I press "mesh.add_clever_stair" - And I press "bim.calculate_all_quantities" + And I press "bim.perform_quantity_take_off" And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Qto_StairFlightBaseQuantities').id()" And the variable "si_conversion" is "ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())" And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')" # Testing Q_LENGTH type of prop Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH" - And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397" - And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257" + And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2156.485" + And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.156" When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350" Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35" diff --git a/src/blenderbim/test/bim/feature/qto.feature b/src/blenderbim/test/bim/feature/qto.feature index 85b4d59cc7..c3cbced3b9 100644 --- a/src/blenderbim/test/bim/feature/qto.feature +++ b/src/blenderbim/test/bim/feature/qto.feature @@ -35,34 +35,23 @@ Scenario: Execute qto method - formwork areas And I add a cube of size "1" at "1,0,0" And the object "Cube" is selected And additionally the object "Cube.001" is selected - When I set "scene.BIMQtoProperties.qto_methods" to "FORMWORK" - And I press "bim.execute_qto_method" + When I press "bim.calculate_formwork_area" Then "scene.BIMQtoProperties.qto_result" is "21.5" Scenario: Execute qto method - side formwork areas Given an empty Blender session And I add a cube And the object "Cube" is selected - When I set "scene.BIMQtoProperties.qto_methods" to "SIDE_FORMWORK" - And I press "bim.execute_qto_method" + When I press "bim.calculate_side_formwork_area" Then "scene.BIMQtoProperties.qto_result" is "16.0" - Scenario: Assign objects base qto - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" - When I press "bim.assign_objects_base_qto" - Then "active_object.PsetProperties.qto_name" is "Qto_WallBaseQuantities" - Scenario: Calculate all quantities Given an empty IFC project And I add a cube And the object "Cube" is selected And I press "bim.assign_class(ifc_class='IfcWall', predefined_type='SOLIDWALL')" - When I press "bim.calculate_all_quantities" + When I press "bim.perform_quantity_take_off" And the variable "qset_id" is "{ifc}.by_type('IfcElementQuantity')[0].id()" And I press "bim.enable_pset_editing(pset_id={qset_id}, obj='IfcWall/Cube', obj_type='Object')" - And I press "bim.disable_pset_editing(obj='IfcWall/Cube', obj_type='Object')" - Then "active_object.PsetProperties.qto_name" is "Qto_WallBaseQuantities" - Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is "2000.0" + Then "active_object.PsetProperties.active_pset_name" is "Qto_WallBaseQuantities" + And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2000.0" diff --git a/src/blenderbim/test/bim/feature/resource.feature b/src/blenderbim/test/bim/feature/resource.feature index ac7522e774..614d362c11 100644 --- a/src/blenderbim/test/bim/feature/resource.feature +++ b/src/blenderbim/test/bim/feature/resource.feature @@ -32,7 +32,7 @@ Scenario: Add Labor Resource Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" Then nothing happens @@ -41,7 +41,7 @@ Scenario: Enable Editing Resource Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.enable_editing_resource(resource={crew_resource})" Then nothing happens @@ -49,7 +49,7 @@ Scenario: Edit Resource Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.enable_editing_resource(resource={crew_resource})" And I set "scene.BIMResourceProperties.resource_attributes.get('Name').string_value" to "Foo" When I press "bim.edit_resource()" @@ -59,7 +59,7 @@ Scenario: Remove Resource Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.enable_editing_resource(resource={crew_resource})" And I set "scene.BIMResourceProperties.resource_attributes.get('Name').string_value" to "Foo" When I press "bim.edit_resource()" @@ -70,9 +70,9 @@ Scenario: Remove Parent Resource Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_time(resource={labor_resource})" And I set "scene.BIMResourceProperties.resource_time_attributes.get('Name').string_value" to "TimelyFoo" When I press "bim.edit_resource_time()" @@ -83,9 +83,9 @@ Scenario: Enable Editing Resource time Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_time(resource={labor_resource})" Then nothing happens @@ -93,9 +93,9 @@ Scenario: Disable editing resource time Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I press "bim.enable_editing_resource_time(resource={labor_resource})" When I press "bim.disable_editing_resource()" Then nothing happens @@ -104,9 +104,9 @@ Scenario: Edit Resource time Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_time(resource={labor_resource})" And I set "scene.BIMResourceProperties.resource_time_attributes.get('ScheduleUsage').float_value" to "305.25" When I press "bim.edit_resource_time()" @@ -116,9 +116,9 @@ Scenario: Enable Editing Resource Costs Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_costs(resource={labor_resource})" Then nothing happens @@ -126,9 +126,9 @@ Scenario: Disable Editing Resource Costs Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.disable_editing_resource()" Then nothing happens @@ -138,9 +138,9 @@ Scenario: Add Resource Fixed Cost Value Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" Then nothing happens @@ -149,12 +149,12 @@ Scenario: Edit Resource Cost Value Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" - And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" + And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})" Then nothing happens @@ -162,12 +162,12 @@ Scenario: Enable Editing Resource Cost Value Formula Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" - And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" + And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})" Then nothing happens @@ -175,12 +175,12 @@ Scenario: Edit Resource Cost Value Formula Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" - And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" + And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})" And I set "scene.BIMResourceProperties.cost_value_formula" to "220*0.2" When I press "bim.edit_resource_cost_value_formula(cost_value={cost_value})" @@ -190,12 +190,12 @@ Scenario: Edit Resource Cost Value Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_costs(resource={labor_resource})" When I press "bim.add_cost_value(parent={labor_resource}, cost_type="FIXED")" - And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" + And the variable "cost_value" is "IfcStore.get_file().by_type('IfcCostValue')[0].id()" When I press "bim.enable_editing_resource_cost_value(cost_value={cost_value})" And I set "scene.BIMResourceProperties.cost_value_attributes.get('AppliedValue').float_value" to "1.00" When I press "bim.edit_resource_cost_value(cost_value={cost_value})" @@ -205,11 +205,11 @@ Scenario: Enable Editing Resource Quantity Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})" - When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityArea")" + When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityTime")" When I press "bim.enable_editing_resource_quantity(resource={labor_resource})" Then nothing happens @@ -217,27 +217,26 @@ Scenario: Edit Resource Quantity Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})" - When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityArea")" + When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityTime")" When I press "bim.enable_editing_resource_quantity(resource={labor_resource})" - And I set "scene.BIMResourceProperties.quantity_attributes.get('AreaValue').float_value" to "50.00" - And the variable "quantity_area" is "IfcStore.get_file().by_type('IfcQuantityArea')[0].id()" - When I press "bim.edit_resource_quantity(physical_quantity={quantity_area})" + And I set "scene.BIMResourceProperties.quantity_attributes.get('TimeValue').float_value" to "50.00" + And the variable "quantity_time" is "IfcStore.get_file().by_type('IfcQuantityTime')[0].id()" + When I press "bim.edit_resource_quantity(physical_quantity={quantity_time})" Then nothing happens Scenario: Remove Resource Quantity Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" When I press "bim.enable_editing_resource_base_quantity(resource={labor_resource})" - When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityArea")" - And the variable "quantity_area" is "IfcStore.get_file().by_type('IfcQuantityArea')[0].id()" + When I press "bim.add_resource_quantity(resource={labor_resource}, ifc_class="IfcQuantityTime")" When I press "bim.remove_resource_quantity(resource={labor_resource})" Then nothing happens @@ -245,9 +244,9 @@ Scenario: Add Productivity data Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I set "scene.BIMResourceProperties.active_resource_index" to "1" And I press "bim.add_productivity_data" And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" @@ -284,9 +283,9 @@ Scenario: Calculate Resource Work And I press "bim.edit_pset(obj='IfcWall/Cube', obj_type='Object')" When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I set "scene.BIMResourceProperties.active_resource_index" to "1" And I press "bim.add_productivity_data" And I set "scene.BIMResourceProperties.should_show_resource_tools" to "True" @@ -294,41 +293,41 @@ Scenario: Calculate Resource Work And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea" And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5" When I press "bim.edit_productivity_data()" - And the variable "productivity_data" is "IfcStore.get_file().by_type('IfcPropertySet')[-1].id()" + And the variable "productivity_data" is "IfcStore.get_file().by_type('IfcPropertySet')[-1].id()" When I press "bim.calculate_resource_work(resource={labor_resource})" - Then nothing happens + Then nothing happens Scenario: Assign Resource Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected - And I press "bim.assign_resource(resource={labor_resource}, related_object="")" + And I press "bim.assign_resource(resource={labor_resource})" Then nothing happens -Scenario: UnAssign Resource +Scenario: Unassign Resource Given an empty IFC project When I press "bim.load_resources" When I press "bim.add_resource(ifc_class="IfcCrewResource")" - And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" + And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()" When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})" - And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" + And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()" And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected - And I press "bim.assign_resource(resource={labor_resource}, related_object="")" + And I press "bim.assign_resource(resource={labor_resource})" And the object "IfcWall/Cube" is selected - And I press "bim.unassign_resource(resource={labor_resource}, related_object="")" - Then nothing happens \ No newline at end of file + And I press "bim.unassign_resource(resource={labor_resource})" + Then nothing happens diff --git a/src/blenderbim/test/bim/feature/root.feature b/src/blenderbim/test/bim/feature/root.feature index 14a47a6666..ee26ac39ed 100644 --- a/src/blenderbim/test/bim/feature/root.feature +++ b/src/blenderbim/test/bim/feature/root.feature @@ -20,15 +20,20 @@ Scenario: Unlink object Given an empty IFC project And I add a cube And the object "Cube" is selected - And I add a material And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And I press "bim.add_style()" + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" + And I press "bim.add_presentation_style" + And the object "IfcWall/Cube" is selected + And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" + And I press "bim.assign_style_to_selected(style_id={style})" When I press "bim.unlink_object(obj='IfcWall/Cube')" Then the object "Cube" is not an IFC element - And the material "Material" is an IFC style - And the material "Material.001" is not an IFC style + And the material "Style" is an IFC style + And the material "Style.001" is not an IFC style Scenario: Copy class Given an empty IFC project @@ -59,7 +64,7 @@ Scenario: Assign a type class to a cube And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" Then the object "IfcWallType/Cube" is an "IfcWallType" - And the object "IfcWallType/Cube" is in the collection "Types" + And the object "IfcWallType/Cube" is in the collection "IfcTypeProduct" And the object "IfcWallType/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW" Scenario: Assign a spatial class to a cube @@ -83,7 +88,6 @@ Scenario: Assign a spatial class to a cube already in a collection And I press "bim.assign_class" Then the object "IfcSpace/Cube" is an "IfcSpace" And the object "IfcSpace/Cube" is in the collection "IfcSpace/Cube" - And the collection "IfcSpace/Cube" is in the collection "IfcBuildingStorey/My Storey" And the object "IfcSpace/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW" Scenario: Assign a class to a cube in a collection @@ -113,4 +117,3 @@ Scenario: Copy a storey Then the object "IfcBuildingStorey/My Storey" and "IfcBuildingStorey/My Storey.001" are different elements And the object "IfcBuildingStorey/My Storey" is in the collection "IfcBuildingStorey/My Storey" And the object "IfcBuildingStorey/My Storey.001" is in the collection "IfcBuildingStorey/My Storey.001" - And the collection "IfcBuildingStorey/My Storey.001" is in the collection "IfcBuilding/My Building" diff --git a/src/blenderbim/test/bim/feature/search.feature b/src/blenderbim/test/bim/feature/search.feature index b82d780569..18bef26f3f 100644 --- a/src/blenderbim/test/bim/feature/search.feature +++ b/src/blenderbim/test/bim/feature/search.feature @@ -12,5 +12,5 @@ Scenario: Select all walls And I set "scene.BIMSearchProperties.facet" to "entity" And I press "bim.add_filter(index=0, type='entity', module='search')" And I set "scene.BIMSearchProperties.filter_groups[0].filters[0].value" to "IfcWall" - When I press "bim.search" + When I press "bim.search(property_group='BIMSearchProperties')" Then the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/sequence.feature b/src/blenderbim/test/bim/feature/sequence.feature index ecbaa64063..bd56df34c1 100644 --- a/src/blenderbim/test/bim/feature/sequence.feature +++ b/src/blenderbim/test/bim/feature/sequence.feature @@ -350,7 +350,6 @@ Scenario: Animate the construction of a wall And I add a cube And the object "Cube" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/spatial.feature b/src/blenderbim/test/bim/feature/spatial.feature index b400efbec6..0b3718f511 100644 --- a/src/blenderbim/test/bim/feature/spatial.feature +++ b/src/blenderbim/test/bim/feature/spatial.feature @@ -27,7 +27,8 @@ Scenario: Assign container And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" - When I press "bim.assign_container(structure={site})" + And I press "bim.set_default_container(container={site})" + When I press "bim.assign_container()" Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site" Scenario: Copy to container @@ -37,10 +38,10 @@ Scenario: Copy to container And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And the object "IfcWall/Cube" is selected + And the object "IfcSite/My Site" is selected + And additionally the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" - When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" - And I press "bim.copy_to_container" + When I press "bim.copy_to_container" Then the object "IfcWall/Cube.001" is in the collection "IfcSite/My Site" Scenario: Reference structure @@ -50,10 +51,10 @@ Scenario: Reference structure And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And the object "IfcWall/Cube" is selected + And the object "IfcBuilding/My Building" is selected + And additionally the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" - When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" - And I press "bim.reference_structure" + When I press "bim.reference_structure" Then nothing happens Scenario: Dereference structure @@ -63,10 +64,10 @@ Scenario: Dereference structure And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" - And the object "IfcWall/Cube" is selected + And the object "IfcBuilding/My Building" is selected + And additionally the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" - When I set "scene.BIMSpatialProperties.containers[0].is_selected" to "True" - And I press "bim.reference_structure" + When I press "bim.reference_structure" And I press "bim.dereference_structure" Then nothing happens @@ -80,7 +81,8 @@ Scenario: Select container And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" - And I press "bim.assign_container(structure={site})" + And I press "bim.set_default_container(container={site})" + And I press "bim.assign_container()" When I press "bim.select_container" Then nothing happens @@ -94,7 +96,8 @@ Scenario: Select similar container And the object "IfcWall/Cube" is selected And I press "bim.enable_editing_container" And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()" - And I press "bim.assign_container(structure={site})" + And I press "bim.set_default_container(container={site})" + And I press "bim.assign_container()" When I press "bim.select_similar_container" Then nothing happens diff --git a/src/blenderbim/test/bim/feature/style.feature b/src/blenderbim/test/bim/feature/style.feature index 7fc685144a..7e4c70b35f 100644 --- a/src/blenderbim/test/bim/feature/style.feature +++ b/src/blenderbim/test/bim/feature/style.feature @@ -1,94 +1,120 @@ @style Feature: Style -Scenario: Update style colours - Given an empty IFC project - And I add a cube - And I add a material - And I press "bim.add_style" - When I press "bim.update_style_colours" - Then nothing happens - -Scenario: Remove style - Given an empty IFC project - And I add a cube - And I add a material - And I press "bim.add_style" - And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" - When I press "bim.remove_style(style={style})" - Then nothing happens - -Scenario: Add style to current blender material - Given an empty IFC project - And I add a cube - And I add a material - When I press "bim.add_style" - Then nothing happens Scenario: Unlink style Given an empty IFC project And I add a cube - And I add a material - And I press "bim.add_style" - When I press "bim.unlink_style" - Then the material "Material" is not an IFC style + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" + And I press "bim.add_presentation_style" + And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" + And I press "bim.assign_style_to_selected(style_id={style})" + When I press "bim.unlink_style(blender_material='Style')" + Then the material "Style" is not an IFC style Scenario: Add a new style Given an empty IFC project And I press "bim.load_styles(style_type='IfcSurfaceStyle')" And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" + And I set "scene.BIMStylesProperties.surface_colour" to "[1.0,0.0,0.0]" When I press "bim.add_presentation_style" - Then nothing happens + Then the material "Style" colour is "1,0,0,1" + And the material "Style" is an IFC style Scenario: Disable adding a new style Given an empty IFC project And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" When I press "bim.disable_adding_presentation_style" Then nothing happens +Scenario: Remove style + Given an empty IFC project + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" + And I set "scene.BIMStylesProperties.surface_colour" to "[1.0,0.0,0.0]" + And I press "bim.add_presentation_style" + And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" + When I press "bim.remove_style(style={style})" + Then the material "Style" does not exist + Scenario: Edit style Given an empty IFC project And I press "bim.load_styles(style_type='IfcSurfaceStyle')" And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" And I press "bim.add_presentation_style" And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" And I press "bim.enable_editing_style(style={style})" + And I set "scene.BIMStylesProperties.attributes[0].string_value" to "NewStyle" When I press "bim.edit_style" - Then nothing happens + Then the material "Style" does not exist + And the material "NewStyle" exists Scenario: Disable editing style Given an empty IFC project And I press "bim.load_styles(style_type='IfcSurfaceStyle')" And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" And I press "bim.add_presentation_style" And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" And I press "bim.enable_editing_style(style={style})" + And I set "scene.BIMStylesProperties.attributes[0].string_value" to "NewStyle" When I press "bim.disable_editing_style" - Then nothing happens + Then the material "NewStyle" does not exist + And the material "Style" exists Scenario: Load styles Given an empty IFC project - And I add a cube - And I add a material - And I press "bim.add_style" When I press "bim.load_styles(style_type='IfcSurfaceStyle')" Then nothing happens Scenario: Disable editing styles Given an empty IFC project - And I add a cube - And I add a material - And I press "bim.add_style" And I press "bim.load_styles(style_type='IfcSurfaceStyle')" When I press "bim.disable_editing_styles" Then nothing happens +Scenario: Assign style to selected + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected + And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" + And I press "bim.add_presentation_style" + And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" + When I press "bim.assign_style_to_selected(style_id={style})" + Then the object "IfcWall/Cube" has the material "Style" + Scenario: Select by style Given an empty IFC project And I add a cube - And I add a material - And I press "bim.add_style" + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected And I press "bim.load_styles(style_type='IfcSurfaceStyle')" + And I press "bim.enable_adding_presentation_style" + And I set "scene.BIMStylesProperties.style_name" to "Style" + And I press "bim.add_presentation_style" And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" - When I press "bim.select_by_style(style={style})" - Then nothing happens + And I press "bim.assign_style_to_selected(style_id={style})" + And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" + When I deselect all objects + And I press "bim.select_by_style(style={style})" + Then the object "IfcWall/Cube" is selected diff --git a/src/blenderbim/test/bim/feature/type.feature b/src/blenderbim/test/bim/feature/type.feature index 2e0fd06206..6b68fe7566 100644 --- a/src/blenderbim/test/bim/feature/type.feature +++ b/src/blenderbim/test/bim/feature/type.feature @@ -85,13 +85,12 @@ Scenario: Assign type - assign to a type with a material layer set, which automa And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()" And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "SweptSolid" representation of "Model/Body/MODEL_VIEW" - And the object "IfcWall/Cube" has the material "Default" And the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default" And the object "IfcWall/Cube" dimensions are "2,.1,3" @@ -108,7 +107,7 @@ Scenario: Assign type - assign to a different type with a material layer set And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" And the variable "type" is "{ifc}.by_type('IfcWallType')[-1].id()" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" And I press "bim.assign_material" And I add an empty @@ -127,7 +126,6 @@ Scenario: Assign type - assign to a different type with a material layer set And I press "bim.edit_assigned_material(material_set={layer_set})" When I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "SweptSolid" representation of "Model/Body/MODEL_VIEW" - And the object "IfcWall/Cube" has the material "Default" And the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default" And the object "IfcWall/Cube" dimensions are "2,.1,3" When I press "bim.assign_type(relating_type={type2}, related_object='IfcWall/Cube')" @@ -146,7 +144,7 @@ Scenario: Assign type - assign to a type with a material profile set And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_material(obj='')" + And I press "bim.add_material()" And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet" And I press "bim.assign_material" And I press "bim.enable_editing_assigned_material" diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index c51f1e4ae9..05b2eb6c8d 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -22,6 +22,7 @@ import traceback import webbrowser import numpy as np import ifcopenshell +import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.bim from blenderbim.bim.ifc import IfcStore @@ -31,6 +32,7 @@ from pytest_bdd import scenarios, given, when, then, parsers from mathutils import Vector from math import radians from pathlib import Path +from typing import Union scenarios("feature") @@ -80,7 +82,7 @@ def an_empty_blender_session(): bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bpy.context.scene.BIMProjectProperties.template_file = "0" - bpy.context.preferences.addons["blenderbim"].preferences.should_play_chaching_sound = False + tool.Blender.get_addon_preferences().should_play_chaching_sound = False @given("an empty IFC project") @@ -324,7 +326,7 @@ def i_rename_the_object_name1_to_name2(name1, name2): @given(parsers.parse('I set "{prop}" to "{value}"')) @when(parsers.parse('I set "{prop}" to "{value}"')) -def i_set_prop_to_value(prop, value): +def i_set_prop_to_value(prop: str, value: str) -> None: value = replace_variables(value) try: eval(f"bpy.context.{prop}") @@ -467,7 +469,7 @@ def the_object_name_data_is_a_type_representation_of_context(name, type, context @then(parsers.parse('the material "{name}" exists')) -def the_material_name_exists(name) -> bpy.types.Material: +def the_material_name_exists(name: str) -> bpy.types.Material: obj = bpy.data.materials.get(name) if not obj: assert False, f'The material "{name}" does not exist' @@ -479,6 +481,25 @@ def the_material_name_does_not_exist(name): assert bpy.data.materials.get(name) is None, "Material exists" +def get_ifc_material_by_name(name: str) -> Union[ifcopenshell.entity_instance, None]: + ifc_file = tool.Ifc.get() + material = next((m for m in ifc_file.by_type("IfcMaterial") if m.Name == name), None) + return material + + +@then(parsers.parse('the IFC material "{name}" exists')) +def the_ifc_material_name_exists(name: str) -> ifcopenshell.entity_instance: + material = get_ifc_material_by_name(name) + if not material: + assert False, f'The IFC material "{name}" does not exist' + return material + + +@then(parsers.parse('the material "{name}" does not exist')) +def the_ifc_material_name_does_not_exist(name): + assert get_ifc_material_by_name(name) is None, "IFC Material exists" + + @then("an IFC file does not exist") def an_ifc_file_does_not_exist(): ifc = IfcStore.get_file() @@ -663,33 +684,40 @@ def prop_is_roughly_value(prop, value): value = replace_variables(value) is_value = False try: - exec(f'assert round(bpy.context.{prop}, 5) == "{value}"') + exec(f'assert round(bpy.context.{prop}, 3) == "{value}"') is_value = True except: try: - exec(f"assert round(bpy.context.{prop}, 5) == {value}") + exec(f"assert round(bpy.context.{prop}, 3) == {value}") is_value = True except: pass if not is_value: print(f"bpy.context.{prop}") - actual_value = round(eval(f"bpy.context.{prop}"), 5) + actual_value = round(eval(f"bpy.context.{prop}"), 3) assert False, f"Value is {actual_value}" @then(parsers.parse('the object "{name}" has a cartesian point offset of "{offset}"')) -def the_object_name_has_a_cartesian_point_offset_of_offset(name, offset): +def the_object_name_has_a_cartesian_point_offset_of_offset(name: str, offset: str) -> None: offset = replace_variables(offset) obj = the_object_name_exists(name) - assert obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT" + assert obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT" obj_offset = np.array(tuple(map(float, obj.BIMObjectProperties.cartesian_point_offset.split(",")))) offset = np.array(tuple(map(float, offset.split(",")))) assert np.allclose(obj_offset, offset) @then(parsers.parse('the object "{name}" has the material "{material}"')) -def the_object_name_has_the_material_material(name, material): - assert material in [ms.material.name for ms in the_object_name_exists(name).material_slots] +def the_object_name_has_the_material_material(name: str, material: str) -> None: + assert material in [ms.material.name for ms in the_object_name_exists(name).material_slots if ms.material] + + +@then(parsers.parse('the object "{name}" has the IFC material "{material_name}"')) +def the_object_name_has_the_ifc_material_material_name(name: str, material_name: str): + element = tool.Ifc.get_entity(the_object_name_exists(name)) + material = ifcopenshell.util.element.get_material(element) + assert material and material.Name == material_name @then( @@ -715,7 +743,7 @@ def the_object_name_has_a_thickness_thick_layered_material_containing_the_materi @then(parsers.parse('the object "{name}" has no IFC materials')) -def the_object_has_no_ifc_materials(name): +def the_object_has_no_ifc_materials(name: str) -> None: element = tool.Ifc.get_entity(the_object_name_exists(name)) material = ifcopenshell.util.element.get_material(element) assert material is None @@ -840,6 +868,37 @@ def the_object_name_is_contained_in_container_name(name, container_name): assert container.Name == container_name, f'Object "{name}" is in {container}' +@then(parsers.parse('the object "{name}" is contained in object "{container_name}"')) +def the_object_name_is_contained_in_object_container_name(name: str, container_name: str) -> None: + ifc = an_ifc_file_exists() + element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + container = ifcopenshell.util.element.get_container(element) + if not container: + assert False, f'Object "{name}" is not in any container' + container_obj = tool.Ifc.get_object(container) + assert container_obj.name == container_name, f'Object "{name}" is in {container_obj}' + + +@then(parsers.parse('the object "{name}" is aggregated by object "{aggregate_name}"')) +def the_object_name_is_aggregated_by_object_aggregate_name(name: str, aggregate_name: str) -> None: + ifc = an_ifc_file_exists() + element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + aggregate = ifcopenshell.util.element.get_aggregate(element) + if not aggregate: + assert False, f'Object "{name}" is not aggregated by any element' + aggregate_obj = tool.Ifc.get_object(aggregate) + assert aggregate_obj.name == aggregate_name, f'Object "{name}" is aggregated by {aggregate_obj}' + + +@then(parsers.parse('the object "{name}" has no aggregate')) +def the_object_name_has_no_aggregate(name: str) -> None: + ifc = an_ifc_file_exists() + element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + aggregate = ifcopenshell.util.element.get_aggregate(element) + if aggregate: + assert False, f'Object "{name}" is aggregated by element "{aggregate}"' + + @then(parsers.parse('the file "{name}" should contain "{value}"')) def the_file_name_should_contain_value(name, value): name = replace_variables(name) diff --git a/src/blenderbim/test/core/test_aggregate.py b/src/blenderbim/test/core/test_aggregate.py index e4f3efe58d..81e967de94 100644 --- a/src/blenderbim/test/core/test_aggregate.py +++ b/src/blenderbim/test/core/test_aggregate.py @@ -36,6 +36,7 @@ class TestAssignObject: def test_run(self, ifc, aggregate, collector): aggregate.can_aggregate("relating_obj", "related_obj").should_be_called().will_return(True) ifc.get_entity("relating_obj").should_be_called().will_return("relating_object") + aggregate.has_physical_body_representation("relating_object").should_be_called().will_return(False) ifc.get_entity("related_obj").should_be_called().will_return("related_object") ifc.run( "aggregate.assign_object", products=["related_object"], relating_object="relating_object" diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index e410fbe9b9..275c8e81f9 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -159,13 +159,21 @@ class TestAddBrick: brick.run_assign_brick_reference(element="element", library="library", brick_uri="brick_uri").should_be_called() brick.run_refresh_brick_viewer().should_be_called() subject.add_brick( - ifc, brick, element="element", namespace="namespace", brick_class="brick_class", library="library", label="label" + ifc, + brick, + element="element", + namespace="namespace", + brick_class="brick_class", + library="library", + label="label", ) def test_adding_a_plain_brick(self, ifc, brick): brick.add_brick("namespace", "brick_class", "label").should_be_called() brick.run_refresh_brick_viewer().should_be_called() - subject.add_brick(ifc, brick, element=None, namespace="namespace", brick_class="brick_class", library=None, label="label") + subject.add_brick( + ifc, brick, element=None, namespace="namespace", brick_class="brick_class", library=None, label="label" + ) class TestAddBrickRelation: @@ -197,24 +205,42 @@ class TestConvertIfcToBrick: brick.get_convertable_brick_elements().should_be_called().will_return({"element", "downstream_element"}) brick.get_brick_class("element").should_be_called().will_return("element_class") - brick.add_brick_from_element("element", "namespace", "element_class").should_be_called().will_return("element_uri") + brick.add_brick_from_element("element", "namespace", "element_class").should_be_called().will_return( + "element_uri" + ) brick.get_element_container("element").should_be_called().will_return("space") - brick.add_relation("element_uri", "https://brickschema.org/schema/Brick#hasLocation", "space_uri").should_be_called() + brick.add_relation( + "element_uri", "https://brickschema.org/schema/Brick#hasLocation", "space_uri" + ).should_be_called() brick.get_element_systems("element").should_be_called().will_return(["system"]) - brick.add_relation("system_uri", "https://brickschema.org/schema/Brick#hasPart", "element_uri").should_be_called() - brick.run_assign_brick_reference(element="element", library="library", brick_uri="element_uri").should_be_called() + brick.add_relation( + "system_uri", "https://brickschema.org/schema/Brick#hasPart", "element_uri" + ).should_be_called() + brick.run_assign_brick_reference( + element="element", library="library", brick_uri="element_uri" + ).should_be_called() brick.get_brick_class("downstream_element").should_be_called().will_return("downstream_element_class") - brick.add_brick_from_element("downstream_element", "namespace", "downstream_element_class").should_be_called().will_return("downstream_element_uri") + brick.add_brick_from_element( + "downstream_element", "namespace", "downstream_element_class" + ).should_be_called().will_return("downstream_element_uri") brick.get_element_container("downstream_element").should_be_called().will_return("space") - brick.add_relation("downstream_element_uri", "https://brickschema.org/schema/Brick#hasLocation", "space_uri").should_be_called() + brick.add_relation( + "downstream_element_uri", "https://brickschema.org/schema/Brick#hasLocation", "space_uri" + ).should_be_called() brick.get_element_systems("downstream_element").should_be_called().will_return(["system"]) - brick.add_relation("system_uri", "https://brickschema.org/schema/Brick#hasPart", "downstream_element_uri").should_be_called() - brick.run_assign_brick_reference(element="downstream_element", library="library", brick_uri="downstream_element_uri").should_be_called() + brick.add_relation( + "system_uri", "https://brickschema.org/schema/Brick#hasPart", "downstream_element_uri" + ).should_be_called() + brick.run_assign_brick_reference( + element="downstream_element", library="library", brick_uri="downstream_element_uri" + ).should_be_called() brick.get_element_feeds("element").should_be_called().will_return(["downstream_element"]) brick.get_element_feeds("downstream_element").should_be_called().will_return([]) - brick.add_relation("element_uri", "https://brickschema.org/schema/Brick#feeds", "downstream_element_uri").should_be_called() + brick.add_relation( + "element_uri", "https://brickschema.org/schema/Brick#feeds", "downstream_element_uri" + ).should_be_called() brick.run_refresh_brick_viewer().should_be_called() subject.convert_ifc_to_brick(brick, namespace="namespace", library="library") diff --git a/src/blenderbim/test/core/test_geometry.py b/src/blenderbim/test/core/test_geometry.py index ad820d310d..c5c0b3b443 100644 --- a/src/blenderbim/test/core/test_geometry.py +++ b/src/blenderbim/test/core/test_geometry.py @@ -26,6 +26,7 @@ class TestEditObjectPlacement: ifc.get_entity("obj").should_be_called().will_return("element") geometry.clear_cache("element").should_be_called() geometry.clear_scale("obj").should_be_called() + geometry.get_blender_offset_type("obj").should_be_called() surveyor.get_absolute_matrix("obj").should_be_called().will_return("matrix") ifc.run("geometry.edit_object_placement", product="element", matrix="matrix").should_be_called() geometry.record_object_position("obj").should_be_called() @@ -193,9 +194,12 @@ class TestSwitchRepresentation: def test_switching_to_a_freshly_loaded_representation(self, ifc, geometry): geometry.is_edited("obj").should_be_called().will_return(False) geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") + geometry.should_use_immediate_representation("element", True).should_be_called().will_return(False) geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.get_representation_data("representation").should_be_called().will_return(None) - geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return("new_data") + geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return( + "new_data" + ) geometry.get_representation_name("representation").should_be_called().will_return("name") geometry.rename_object("new_data", "name").should_be_called() geometry.link("representation", "new_data").should_be_called() @@ -218,9 +222,12 @@ class TestSwitchRepresentation: def test_switching_to_a_reloaded_representation_and_deleting_the_existing_data(self, ifc, geometry): geometry.is_edited("obj").should_be_called().will_return(False) geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") + geometry.should_use_immediate_representation("element", True).should_be_called().will_return(False) geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.get_representation_data("representation").should_be_called().will_return("existing_data") - geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return("new_data") + geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return( + "new_data" + ) geometry.get_representation_name("representation").should_be_called().will_return("name") geometry.rename_object("new_data", "name").should_be_called() geometry.link("representation", "new_data").should_be_called() @@ -244,13 +251,35 @@ class TestSwitchRepresentation: def test_switching_to_an_existing_representation(self, ifc, geometry): geometry.is_edited("obj").should_be_called().will_return(False) + ifc.get_entity("obj").should_be_called().will_return("element") geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") + geometry.should_use_immediate_representation("element", True).should_be_called().will_return(False) geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.get_representation_data("representation").should_be_called().will_return("data") geometry.change_object_data("obj", "data", is_global=True).should_be_called() geometry.record_object_materials("obj").should_be_called() geometry.clear_modifiers("obj").should_be_called() + geometry.clear_cache("element").should_be_called() + subject.switch_representation( + ifc, + geometry, + obj="obj", + representation="mapped_rep", + should_reload=False, + is_global=True, + should_sync_changes_first=True, + ) + + def test_switching_to_an_existing_representation_reuse_representation(self, ifc, geometry): + geometry.is_edited("obj").should_be_called().will_return(False) ifc.get_entity("obj").should_be_called().will_return("element") + geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") + geometry.should_use_immediate_representation("element", True).should_be_called().will_return(True) + geometry.unresolve_type_representation("mapped_rep", "element").should_be_called().will_return("representation") + geometry.get_representation_data("representation").should_be_called().will_return("data") + geometry.change_object_data("obj", "data", is_global=False).should_be_called() + geometry.record_object_materials("obj").should_be_called() + geometry.clear_modifiers("obj").should_be_called() geometry.clear_cache("element").should_be_called() subject.switch_representation( ifc, @@ -269,6 +298,7 @@ class TestSwitchRepresentation: geometry.run_geometry_update_representation(obj="obj").should_be_called() geometry.does_representation_id_exist("representation_id").should_be_called().will_return(True) geometry.get_object_data("obj").should_be_called().will_return("current_obj_data") + geometry.should_use_immediate_representation("element", True).should_be_called().will_return(False) geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.get_representation_data("representation").should_be_called().will_return("data") geometry.change_object_data("obj", "data", is_global=False).should_be_called() diff --git a/src/blenderbim/test/core/test_project.py b/src/blenderbim/test/core/test_project.py index 0cfa3295c4..d414bd0dca 100644 --- a/src/blenderbim/test/core/test_project.py +++ b/src/blenderbim/test/core/test_project.py @@ -18,13 +18,13 @@ import blenderbim.core.project as subject -from test.core.bootstrap import ifc, project +from test.core.bootstrap import ifc, project, spatial, georeference class TestCreateProject: - def test_do_nothing_if_a_project_already_exists(self, ifc, project): + def test_do_nothing_if_a_project_already_exists(self, ifc, georeference, project, spatial): ifc.get().should_be_called().will_return("ifc") - subject.create_project(ifc, project, schema="IFC4", template=None) + subject.create_project(ifc, georeference, project, spatial, schema="IFC4", template=None) def check_contexts(self, project): project.run_context_add_context( @@ -67,7 +67,7 @@ class TestCreateProject: context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent="plan" ).should_be_called() - def test_create_an_ifc4_project(self, ifc, project): + def test_create_an_ifc4_project(self, ifc, georeference, project, spatial): ifc.get().should_be_called().will_return(None) ifc.run("project.create_file", version="IFC4").should_be_called().will_return("ifc") ifc.set("ifc").should_be_called() @@ -92,14 +92,17 @@ class TestCreateProject: project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called() project.set_context("body").should_be_called() + spatial.run_spatial_import_spatial_decomposition().should_be_called() + spatial.guess_default_container().should_be_called().will_return(None) project.load_default_thumbnails().should_be_called() project.set_default_context().should_be_called() project.set_default_modeling_dimensions().should_be_called() + georeference.set_model_origin().should_be_called() - subject.create_project(ifc, project, schema="IFC4", template=None) + subject.create_project(ifc, georeference, project, spatial, schema="IFC4", template=None) - def test_appending_project_template_types_if_specified(self, ifc, project): + def test_create_an_ifc4_project_with_guessing_default_container(self, ifc, georeference, project, spatial): ifc.get().should_be_called().will_return(None) ifc.run("project.create_file", version="IFC4").should_be_called().will_return("ifc") ifc.set("ifc").should_be_called() @@ -124,16 +127,55 @@ class TestCreateProject: project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called() project.set_context("body").should_be_called() + spatial.run_spatial_import_spatial_decomposition().should_be_called() + spatial.guess_default_container().should_be_called().will_return("default_container") + spatial.set_default_container("default_container").should_be_called() + + project.load_default_thumbnails().should_be_called() + project.set_default_context().should_be_called() + project.set_default_modeling_dimensions().should_be_called() + georeference.set_model_origin().should_be_called() + + subject.create_project(ifc, georeference, project, spatial, schema="IFC4", template=None) + + def test_appending_project_template_types_if_specified(self, ifc, georeference, project, spatial): + ifc.get().should_be_called().will_return(None) + ifc.run("project.create_file", version="IFC4").should_be_called().will_return("ifc") + ifc.set("ifc").should_be_called() + + project.create_empty("My Project").should_be_called().will_return("project") + project.create_empty("My Site").should_be_called().will_return("site") + project.create_empty("My Building").should_be_called().will_return("building") + project.create_empty("My Storey").should_be_called().will_return("storey") + project.run_root_assign_class( + obj="project", ifc_class="IfcProject", should_add_representation=False + ).should_be_called() + project.run_unit_assign_scene_units().should_be_called() + + self.check_contexts(project) + + project.run_root_assign_class(obj="site", ifc_class="IfcSite", context="body").should_be_called() + project.run_root_assign_class(obj="building", ifc_class="IfcBuilding", context="body").should_be_called() + project.run_root_assign_class(obj="storey", ifc_class="IfcBuildingStorey", context="body").should_be_called() + + project.run_aggregate_assign_object(relating_obj="project", related_obj="site").should_be_called() + project.run_aggregate_assign_object(relating_obj="site", related_obj="building").should_be_called() + project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called() + + project.set_context("body").should_be_called() + spatial.run_spatial_import_spatial_decomposition().should_be_called() + spatial.guess_default_container().should_be_called().will_return(None) project.append_all_types_from_template("template").should_be_called() project.load_default_thumbnails().should_be_called() project.set_default_context().should_be_called() project.set_default_modeling_dimensions().should_be_called() + georeference.set_model_origin().should_be_called() - subject.create_project(ifc, project, schema="IFC4", template="template") + subject.create_project(ifc, georeference, project, spatial, schema="IFC4", template="template") - def test_create_an_ifc2x3_project_with_owner_defaults(self, ifc, project): + def test_create_an_ifc2x3_project_with_owner_defaults(self, ifc, georeference, project, spatial): ifc.get().should_be_called().will_return(None) ifc.run("project.create_file", version="IFC2X3").should_be_called().will_return("ifc") ifc.set("ifc").should_be_called() @@ -165,9 +207,12 @@ class TestCreateProject: project.run_aggregate_assign_object(relating_obj="building", related_obj="storey").should_be_called() project.set_context("body").should_be_called() + spatial.run_spatial_import_spatial_decomposition().should_be_called() + spatial.guess_default_container().should_be_called().will_return(None) project.load_default_thumbnails().should_be_called() project.set_default_context().should_be_called() project.set_default_modeling_dimensions().should_be_called() + georeference.set_model_origin().should_be_called() - subject.create_project(ifc, project, schema="IFC2X3", template=None) + subject.create_project(ifc, georeference, project, spatial, schema="IFC2X3", template=None) diff --git a/src/blenderbim/test/core/test_root.py b/src/blenderbim/test/core/test_root.py index 65a99d626d..4f2a447ccf 100644 --- a/src/blenderbim/test/core/test_root.py +++ b/src/blenderbim/test/core/test_root.py @@ -116,7 +116,9 @@ class TestAssignClass: ifc_representation_class="ifc_representation_class", ) - def test_assign_a_class_with_geometry_and_autodetected_spatial_container(self, ifc, collector, root): + def test_assign_a_class_with_geometry_and_autodetected_spatial_container_spatial_element( + self, ifc, collector, root + ): ifc.get_entity("obj").should_be_called().will_return(None) root.get_object_name("obj").should_be_called().will_return("name") ifc.run( @@ -127,7 +129,11 @@ class TestAssignClass: root.run_geometry_add_representation( obj="obj", context="context", ifc_representation_class="ifc_representation_class", profile_set_usage=None ).should_be_called() - collector.sync("obj").should_be_called() + + root.get_default_container().should_be_called().will_return("default_container") + root.is_spatial_element("element").should_be_called().will_return(True) + ifc.run("aggregate.assign_object", products=["element"], relating_object="default_container").should_be_called() + collector.assign("obj").should_be_called() subject.assign_class( ifc, @@ -141,7 +147,9 @@ class TestAssignClass: ifc_representation_class="ifc_representation_class", ) - def test_not_adding_a_representation_if_requested(self, ifc, collector, root): + def test_assign_a_class_with_geometry_and_autodetected_spatial_container_non_spatial_containable( + self, ifc, collector, root + ): ifc.get_entity("obj").should_be_called().will_return(None) root.get_object_name("obj").should_be_called().will_return("name") ifc.run( @@ -149,7 +157,39 @@ class TestAssignClass: ).should_be_called().will_return("element") root.set_object_name("obj", "element").should_be_called() ifc.link("element", "obj").should_be_called() - collector.sync("obj").should_be_called() + root.run_geometry_add_representation( + obj="obj", context="context", ifc_representation_class="ifc_representation_class", profile_set_usage=None + ).should_be_called() + + root.get_default_container().should_be_called().will_return("default_container") + root.is_spatial_element("element").should_be_called().will_return(False) + root.is_containable("element").should_be_called().will_return(True) + ifc.run( + "spatial.assign_container", products=["element"], relating_structure="default_container" + ).should_be_called() + + collector.assign("obj").should_be_called() + subject.assign_class( + ifc, + collector, + root, + obj="obj", + ifc_class="ifc_class", + predefined_type="predefined_type", + should_add_representation=True, + context="context", + ifc_representation_class="ifc_representation_class", + ) + + def test_not_adding_a_representation_if_requested_no_default_container(self, ifc, collector, root): + ifc.get_entity("obj").should_be_called().will_return(None) + root.get_object_name("obj").should_be_called().will_return("name") + ifc.run( + "root.create_entity", ifc_class="ifc_class", predefined_type="predefined_type", name="name" + ).should_be_called().will_return("element") + root.set_object_name("obj", "element").should_be_called() + ifc.link("element", "obj").should_be_called() + root.get_default_container().should_be_called().will_return(None) collector.assign("obj").should_be_called() subject.assign_class( ifc, diff --git a/src/blenderbim/test/core/test_spatial.py b/src/blenderbim/test/core/test_spatial.py index 8af6f8dffb..d9f2c12421 100644 --- a/src/blenderbim/test/core/test_spatial.py +++ b/src/blenderbim/test/core/test_spatial.py @@ -30,30 +30,29 @@ class TestReferenceStructure: class TestDereferenceStructure: def test_run(self, ifc, spatial): spatial.can_reference("structure", "element").should_be_called().will_return(True) - ifc.run("spatial.dereference_structure", products=["element"], relating_structure="structure").should_be_called() + ifc.run( + "spatial.dereference_structure", products=["element"], relating_structure="structure" + ).should_be_called() subject.dereference_structure(ifc, spatial, structure="structure", element="element") class TestAssignContainer: def test_run(self, ifc, collector, spatial): - spatial.can_contain("structure_obj", "element_obj").should_be_called().will_return(True) - ifc.get_entity("structure_obj").should_be_called().will_return("structure") + spatial.can_contain("container", "element_obj").should_be_called().will_return(True) ifc.get_entity("element_obj").should_be_called().will_return("element") ifc.run( - "spatial.assign_container", products=["element"], relating_structure="structure" + "spatial.assign_container", products=["element"], relating_structure="container" ).should_be_called().will_return("rel") spatial.disable_editing("element_obj").should_be_called() collector.assign("element_obj").should_be_called() assert ( - subject.assign_container(ifc, collector, spatial, structure_obj="structure_obj", element_obj="element_obj") - == "rel" + subject.assign_container(ifc, collector, spatial, container="container", element_obj="element_obj") == "rel" ) class TestEnableEditingContainer: def test_run(self, spatial): spatial.enable_editing("obj").should_be_called() - spatial.import_containers().should_be_called() subject.enable_editing_container(spatial, obj="obj") @@ -63,12 +62,6 @@ class TestDisableEditingContainer: subject.disable_editing_container(spatial, obj="obj") -class TestChangeSpatialLevel: - def test_run(self, spatial): - spatial.import_containers(parent="parent").should_be_called() - subject.change_spatial_level(spatial, parent="parent") - - class TestRemoveContainer: def test_run(self, ifc, collector): ifc.get_entity("obj").should_be_called().will_return("element") @@ -88,7 +81,7 @@ class TestCopyToContainer: spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj") spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called() spatial.run_root_copy_class(obj="new_obj").should_be_called() - spatial.run_spatial_assign_container(structure_obj="to_container_obj", element_obj="new_obj").should_be_called() + spatial.run_spatial_assign_container(container="to_container", element_obj="new_obj").should_be_called() spatial.disable_editing("obj").should_be_called() @@ -103,7 +96,7 @@ class TestCopyToContainer: spatial.duplicate_object_and_data("obj").should_be_called().will_return("new_obj") spatial.set_relative_object_matrix("new_obj", "to_container_obj", "matrix").should_be_called() spatial.run_root_copy_class(obj="new_obj").should_be_called() - spatial.run_spatial_assign_container(structure_obj="to_container_obj", element_obj="new_obj").should_be_called() + spatial.run_spatial_assign_container(container="to_container", element_obj="new_obj").should_be_called() spatial.disable_editing("obj").should_be_called() @@ -112,11 +105,9 @@ class TestCopyToContainer: class TestSelectContainer: def test_run(self, ifc, spatial): - ifc.get_entity("obj").should_be_called().will_return("element") - spatial.get_container("element").should_be_called().will_return("container") ifc.get_object("container").should_be_called().will_return("container_obj") spatial.set_active_object("container_obj").should_be_called() - subject.select_container(ifc, spatial, obj="obj") + subject.select_container(ifc, spatial, container="container") class TestSelectSimilarContainer: diff --git a/src/blenderbim/test/core/test_style.py b/src/blenderbim/test/core/test_style.py index 0998a6953e..af0b2c0cc0 100644 --- a/src/blenderbim/test/core/test_style.py +++ b/src/blenderbim/test/core/test_style.py @@ -54,6 +54,7 @@ class TestRemoveStyle: ifc.unlink(element="style").should_be_called() ifc.run("style.remove_style", style="style").should_be_called() style.delete_object("obj").should_be_called() + style.get_active_style_type().should_be_called().will_return("style_type") def test_removing_a_style(self, ifc, style): self.remove_a_style_common(ifc, style) @@ -63,7 +64,6 @@ class TestRemoveStyle: def test_removing_a_style_and_reloading_imported_styles(self, ifc, style): self.remove_a_style_common(ifc, style) style.is_editing_styles().should_be_called().will_return(True) - style.get_active_style_type().should_be_called().will_return("style_type") style.import_presentation_styles("style_type").should_be_called() subject.remove_style(ifc, style, style="style") diff --git a/src/blenderbim/test/core/test_type.py b/src/blenderbim/test/core/test_type.py index d5687ac3c9..ea684ee771 100644 --- a/src/blenderbim/test/core/test_type.py +++ b/src/blenderbim/test/core/test_type.py @@ -17,7 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import blenderbim.core.type as subject -from test.core.bootstrap import ifc, type +from test.core.bootstrap import ifc, type, geometry class TestAssignType: @@ -42,11 +42,16 @@ class TestAssignType: class TestPurgeUnusedTypes: - def test_run(self, ifc, type): + def test_purge_types_obj_found(self, ifc, type, geometry): type.get_model_types().should_be_called().will_return(["element_type"]) type.get_type_occurrences("element_type").should_be_called().will_return([]) - ifc.run("root.remove_product", product="element_type").should_be_called() ifc.get_object("element_type").should_be_called().will_return("obj") - ifc.unlink(element="element_type").should_be_called() - type.remove_object("obj").should_be_called() - subject.purge_unused_types(ifc, type) + geometry.delete_ifc_object("obj").should_be_called() + subject.purge_unused_types(ifc, type, geometry) + + def test_purge_types_obj_not_found(self, ifc, type, geometry): + type.get_model_types().should_be_called().will_return(["element_type"]) + type.get_type_occurrences("element_type").should_be_called().will_return([]) + ifc.get_object("element_type").should_be_called().will_return(None) + ifc.run("root.remove_product", product="element_type").should_be_called() + subject.purge_unused_types(ifc, type, geometry) diff --git a/src/blenderbim/test/core/test_unit.py b/src/blenderbim/test/core/test_unit.py index 4b10abcc1d..fa111f6a0f 100644 --- a/src/blenderbim/test/core/test_unit.py +++ b/src/blenderbim/test/core/test_unit.py @@ -26,15 +26,15 @@ class TestAssignSceneUnits: unit.get_scene_unit_si_prefix("LENGTHUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_si_prefix("AREAUNIT").should_be_called().will_return("prefix") unit.get_scene_unit_si_prefix("VOLUMEUNIT").should_be_called().will_return("prefix") - ifc.run( - "unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix" - ).should_be_called().will_return("lengthunit") + ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix="prefix").should_be_called().will_return( + "lengthunit" + ) ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix="prefix").should_be_called().will_return("areaunit") - ifc.run( - "unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix" - ).should_be_called().will_return("volumeunit") + ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix="prefix").should_be_called().will_return( + "volumeunit" + ) ifc.run("unit.add_conversion_based_unit", name="degree").should_be_called().will_return("planeangleunit") diff --git a/src/blenderbim/test/tool/test_material.py b/src/blenderbim/test/tool/test_material.py index f998767d6e..a950e1b2d9 100644 --- a/src/blenderbim/test/tool/test_material.py +++ b/src/blenderbim/test/tool/test_material.py @@ -150,4 +150,3 @@ class TestIsMaterialUsedInSets(NewFile): material_set.MaterialLayers = [material_set_item] material_set_item.Material = material assert subject.is_material_used_in_sets(material) is True - diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py index 65278949db..206649a22d 100644 --- a/src/blenderbim/test/tool/test_model.py +++ b/src/blenderbim/test/tool/test_model.py @@ -448,7 +448,9 @@ class TestUsingArrays(NewFile): class TestApplyIfcMaterialChanges(NewFile): def get_used_styles(self, obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]: ifc_file = tool.Ifc.get() - return {ifc_file.by_id(s.material.BIMStyleProperties.ifc_definition_id) for s in obj.material_slots if s.material} + return { + ifc_file.by_id(s.material.BIMStyleProperties.ifc_definition_id) for s in obj.material_slots if s.material + } def get_mesh(self, obj: bpy.types.Object) -> bpy.types.Mesh: mesh = obj.data diff --git a/src/blenderbim/test/tool/test_project.py b/src/blenderbim/test/tool/test_project.py index 3d3732e178..feb3fb1607 100644 --- a/src/blenderbim/test/tool/test_project.py +++ b/src/blenderbim/test/tool/test_project.py @@ -104,7 +104,14 @@ class TestSetDefaultContext(NewFile): tool.Ifc.set(ifc) ifc.createIfcProject() model = ifcopenshell.api.run("context.add_context", ifc, context_type="Model") - body = ifcopenshell.api.run("context.add_context", ifc, parent=model, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW") + body = ifcopenshell.api.run( + "context.add_context", + ifc, + parent=model, + context_type="Model", + context_identifier="Body", + target_view="MODEL_VIEW", + ) subject.set_default_context() assert bpy.context.scene.BIMRootProperties.contexts == str(body.id()) diff --git a/src/blenderbim/test/tool/test_spatial.py b/src/blenderbim/test/tool/test_spatial.py index 602a41b8f9..a048ada2c3 100644 --- a/src/blenderbim/test/tool/test_spatial.py +++ b/src/blenderbim/test/tool/test_spatial.py @@ -40,7 +40,7 @@ class TestCanContain(NewFile): element = ifc.createIfcWall() element_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure_obj, element_obj) is True + assert subject.can_contain(structure, element_obj) is True def test_a_spatial_structure_element_can_contain_an_element_ifc2x3(self): ifc = ifcopenshell.file(schema="IFC2X3") @@ -51,7 +51,7 @@ class TestCanContain(NewFile): element = ifc.createIfcWall() element_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure_obj, element_obj) is True + assert subject.can_contain(structure, element_obj) is True def test_a_spatial_zone_element_cannot_contain_an_element(self): ifc = ifcopenshell.file() @@ -62,7 +62,7 @@ class TestCanContain(NewFile): element = ifc.createIfcWall() element_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure_obj, element_obj) is False + assert subject.can_contain(structure, element_obj) is False def test_unlinked_elements_cannot_contain_anything(self): structure_obj = bpy.data.objects.new("Object", None) @@ -78,7 +78,7 @@ class TestCanContain(NewFile): element = ifc.createIfcWall() element_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure_obj, element_obj) is False + assert subject.can_contain(structure, element_obj) is False def test_a_non_element_cannot_be_contained(self): ifc = ifcopenshell.file() @@ -89,7 +89,7 @@ class TestCanContain(NewFile): element = ifc.createIfcTask() element_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure_obj, element_obj) is False + assert subject.can_contain(structure, element_obj) is False def test_other_non_elements_that_have_a_contained_in_structure_attribute_can_be_contained(self): ifc = ifcopenshell.file() @@ -100,7 +100,7 @@ class TestCanContain(NewFile): element = ifc.createIfcGrid() element_obj = bpy.data.objects.new("Object", None) tool.Ifc.link(element, element_obj) - assert subject.can_contain(structure_obj, element_obj) is True + assert subject.can_contain(structure, element_obj) is True class TestCanReference(NewFile): @@ -181,46 +181,6 @@ class TestGetRelativeObjectMatrix(NewFile): assert subject.get_relative_object_matrix(obj, relative_obj)[0][3] == -1 -class TestImportContainers(NewFile): - def test_run(self): - bpy.ops.bim.create_project() - subject.import_containers() - props = bpy.context.scene.BIMSpatialProperties - assert len(props.containers) == 1 - assert props.containers[0].name == "My Site" - assert props.containers[0].long_name == "" - assert props.containers[0].has_decomposition is True - assert props.containers[0].ifc_definition_id == tool.Ifc.get().by_type("IfcSite")[0].id() - assert props.active_container_id == tool.Ifc.get().by_type("IfcProject")[0].id() - - def test_importing_with_a_specified_parent(self): - bpy.ops.bim.create_project() - site = tool.Ifc.get().by_type("IfcSite")[0] - subject.import_containers(site) - props = bpy.context.scene.BIMSpatialProperties - assert len(props.containers) == 1 - assert props.containers[0].name == "My Building" - assert props.containers[0].long_name == "" - assert props.containers[0].has_decomposition is True - assert props.containers[0].ifc_definition_id == tool.Ifc.get().by_type("IfcBuilding")[0].id() - assert props.active_container_id == site.id() - - def test_importing_sorted_by_z_placement(self): - bpy.ops.bim.create_project() - building = tool.Ifc.get().by_type("IfcBuilding")[0] - storey1 = tool.Ifc.get().by_type("IfcBuildingStorey")[0] - storey1.Name = "Higher" - bpy.ops.bim.copy_class(obj=tool.Ifc.get_object(storey1).name) - storey2 = tool.Ifc.get().by_type("IfcBuildingStorey")[1] - storey2.Name = "Lower" - storey2.ObjectPlacement.RelativePlacement.Location.Coordinates = (0.0, 0.0, -100.0) - subject.import_containers(building) - props = bpy.context.scene.BIMSpatialProperties - assert len(props.containers) == 2 - assert props.containers[0].name == "Lower" - assert props.containers[1].name == "Higher" - - class TestRunRootCopyClass(NewFile): def test_nothing(self): pass diff --git a/src/blenderbim/test/tool/test_type.py b/src/blenderbim/test/tool/test_type.py index bf497e021c..4feac9d5a9 100644 --- a/src/blenderbim/test/tool/test_type.py +++ b/src/blenderbim/test/tool/test_type.py @@ -168,13 +168,6 @@ class TestHasMaterialUsage(NewFile): assert subject.has_material_usage(element) is True -class TestRemoveObject(NewFile): - def test_run(self): - obj = bpy.data.objects.new("Object", None) - subject.remove_object(obj) - assert not bpy.data.objects.get("Object") - - class TestRunGeometryAddRepresentation(NewFile): def test_nothing(self): pass diff --git a/src/bsdd/Makefile b/src/bsdd/Makefile index 8eaf0bbf83..c4cce9bbe1 100644 --- a/src/bsdd/Makefile +++ b/src/bsdd/Makefile @@ -22,4 +22,6 @@ include ../common.mk .PHONY: test test: - pytest -p no:pytest-blender test +# Using python -m pytest vs just using pytest is important here. +# As just using pytest does not add current directory to PATH. + python -m pytest -p no:pytest-blender tests diff --git a/src/bsdd/bsdd.py b/src/bsdd/bsdd.py index 31c87ecdcb..5cd2b3a613 100644 --- a/src/bsdd/bsdd.py +++ b/src/bsdd/bsdd.py @@ -31,6 +31,9 @@ __version__ = version = "0.0.0" Status = Literal["Preview", "Active", "Inactive"] ClassTypes = Literal["Class", "GroupOfProperties", "AlternativeUse", "Material"] +# NOTE: Some values in TypedDicts are actually not guaranteed to be provided +# and should be typed with NotRequired. + class DictionaryContractV1(TypedDict): uri: str @@ -725,8 +728,8 @@ class Client: def search_class( self, search_text: str, - dictionary_uris=None, - related_ifc_entities=None, + dictionary_uris: Optional[list[str]] = None, + related_ifc_entities: Optional[list[str]] = None, version: int = 1, offset: int = 0, limit: int = 100, diff --git a/src/bsdd/tests/test_bsdd.py b/src/bsdd/tests/test_bsdd.py index 2cc1fd3935..8f2421c9d5 100644 --- a/src/bsdd/tests/test_bsdd.py +++ b/src/bsdd/tests/test_bsdd.py @@ -3,39 +3,50 @@ from bsdd import Client client = Client() ifc4x3_uri = [l["uri"] for l in client.get_dictionary()["dictionaries"] if "4.3" in l["uri"]][0] -nbs_uri = [l["uri"] for l in client.get_dictionary()["dictionaries"] if 'Uniclass 2015' == l["name"]][0] +nbs_uri = [l["uri"] for l in client.get_dictionary()["dictionaries"] if "Uniclass 2015" == l["name"]][0] def get_ifc_classes(): - return client.get_classes(ifc4x3_uri, use_nested_classes= False, class_type="Class") + return client.get_classes(ifc4x3_uri, use_nested_classes=False, class_type="Class") + def get_nbs_classes(): - return client.get_classes(nbs_uri, use_nested_classes= False, class_type="Class", offset=0, limit=5) + return client.get_classes(nbs_uri, use_nested_classes=False, class_type="Class", offset=0, limit=5) + def test_get_dictionary(): li_names = [l["name"] for l in client.get_dictionary()["dictionaries"]] - assert 'Uniclass 2015' and 'IFC' in li_names + assert "Uniclass 2015" and "IFC" in li_names + def test_get_ifc_classes(): ifc4x3_classes = get_ifc_classes() assert "IfcBoiler" and "IfcLightFixture" in [l["code"] for l in ifc4x3_classes["classes"]] + def test_get_nbs_classes(): nbs_classes = get_nbs_classes() assert "Ac" in [l["code"] for l in nbs_classes["classes"]] + def test_get_class(): uri_light_fixture = [l for l in get_ifc_classes()["classes"] if "IfcLightFixture" == l["code"]][0]["uri"] ifc4x3_light_fixture = client.get_class(uri_light_fixture) - assert 'Maintenance Factor' and 'Light Fixture Mounting Type' in [l["name"] for l in ifc4x3_light_fixture["classProperties"]] - + assert "Maintenance Factor" and "Light Fixture Mounting Type" in [ + l["name"] for l in ifc4x3_light_fixture["classProperties"] + ] + + def test_search_class(): - ss_heat_pump_sys = client.search_class("Ss_60_40_36", [nbs_uri] ) + ss_heat_pump_sys = client.search_class("Ss_60_40_36", [nbs_uri]) li = [l + "source heat pump systems" for l in ["Air ", "Ground ", "Water "]] - assert len(li) < 8 # I think it should be 4 but just validating it isn't overfetching with some space for future change - for l in li: + assert ( + len(li) < 8 + ) # I think it should be 4 but just validating it isn't overfetching with some space for future change + for l in li: assert l in [_["name"] for _ in ss_heat_pump_sys["classes"]] - + + def test_get_properties(): pr = client.get_properties(ifc4x3_uri, offset=0, limit=5) assert len(pr["properties"]) == 5 diff --git a/src/ifc2ca/_deprecated/ca2ifc.py b/src/ifc2ca/_deprecated/ca2ifc.py index 599a94103f..917ad34eb0 100644 --- a/src/ifc2ca/_deprecated/ca2ifc.py +++ b/src/ifc2ca/_deprecated/ca2ifc.py @@ -1,4 +1,3 @@ - # Ifc2CA - IFC Code_Aster utility # Copyright (C) 2020, 2021 Ioannis P. Christovasilis # diff --git a/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py b/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py index 3264ca20b7..58a7267726 100644 --- a/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py +++ b/src/ifc2ca/_deprecated/scriptCodeAsterBonded.py @@ -55,9 +55,7 @@ class COMMANDFILE: conn["relatedElements"] = [] for el in elements: for rel in el["connections"]: - conn = [ - c for c in connections if c["referenceName"] == rel["relatedConnection"] - ][0] + conn = [c for c in connections if c["referenceName"] == rel["relatedConnection"]][0] conn["relatedElements"].append(rel) # End <-- @@ -65,27 +63,17 @@ class COMMANDFILE: profiles = data["db"]["profiles"] edgeGroupNames = tuple( - [ - self.getGroupName(el["referenceName"]) - for el in elements - if el["geometryType"] == "line" - ] + [self.getGroupName(el["referenceName"]) for el in elements if el["geometryType"] == "line"] ) faceGroupNames = tuple( - [ - self.getGroupName(el["referenceName"]) - for el in elements - if el["geometryType"] == "surface" - ] + [self.getGroupName(el["referenceName"]) for el in elements if el["geometryType"] == "surface"] ) rigidLinkGroupNames = [] for conn in connections: rigidLinkGroupNames.extend( [ - self.getGroupName(rel["relatingElement"]) - + "_1DR_" - + self.getGroupName(conn["referenceName"]) + self.getGroupName(rel["relatingElement"]) + "_1DR_" + self.getGroupName(conn["referenceName"]) for rel in conn["relatedElements"] if rel["eccentricity"] ] @@ -192,20 +180,16 @@ model = AFFE_MODELE( else: if "shearModulus" in material["mechProps"]: poissonRatio = ( - material["mechProps"]["youngModulus"] - / 2.0 - / material["mechProps"]["shearModulus"] + material["mechProps"]["youngModulus"] / 2.0 / material["mechProps"]["shearModulus"] ) - 1 else: poissonRatio = 0.0 context = { "matNameID": "mat" + "_%s" % i, - "youngModulus": float(material["mechProps"]["youngModulus"]) - * ScaleFactor ** 2, + "youngModulus": float(material["mechProps"]["youngModulus"]) * ScaleFactor**2, "poissonRatio": float(poissonRatio), - "massDensity": float(material["commonProps"]["massDensity"]) - * ScaleFactor ** 3, + "massDensity": float(material["commonProps"]["massDensity"]) * ScaleFactor**3, } f.write(template.format(**context)) @@ -225,9 +209,7 @@ material = AFFE_MATERIAU( ),""" context = { - "groupNames": tuple( - [self.getGroupName(rel) for rel in material["relatedElements"]] - ), + "groupNames": tuple([self.getGroupName(rel) for rel in material["relatedElements"]]), "matNameID": "mat" + "_%s" % i, } @@ -260,10 +242,7 @@ element = AFFE_CARA_ELEM( ) for profile in profiles: - if ( - profile["profileShape"] == "rectangular" - and profile["profileType"] == "AREA" - ): + if profile["profileShape"] == "rectangular" and profile["profileType"] == "AREA": template = """ _F( GROUP_MA = {groupNames}, @@ -273,9 +252,7 @@ element = AFFE_CARA_ELEM( ),""" context = { - "groupNames": tuple( - [self.getGroupName(rel) for rel in profile["relatedElements"]] - ), + "groupNames": tuple([self.getGroupName(rel) for rel in profile["relatedElements"]]), "profileDimensions": ( profile["xDim"] / ScaleFactor, profile["yDim"] / ScaleFactor, @@ -284,10 +261,7 @@ element = AFFE_CARA_ELEM( f.write(template.format(**context)) - elif ( - profile["profileShape"] == "iSymmetrical" - and profile["profileType"] == "AREA" - ): + elif profile["profileShape"] == "iSymmetrical" and profile["profileType"] == "AREA": template = """ _F( GROUP_MA = {groupNames}, @@ -297,14 +271,12 @@ element = AFFE_CARA_ELEM( ),""" context = { - "groupNames": tuple( - [self.getGroupName(rel) for rel in profile["relatedElements"]] - ), + "groupNames": tuple([self.getGroupName(rel) for rel in profile["relatedElements"]]), "profileProperties": ( - profile["mechProps"]["crossSectionArea"] / ScaleFactor ** 2, - profile["mechProps"]["momentOfInertiaY"] / ScaleFactor ** 4, - profile["mechProps"]["momentOfInertiaZ"] / ScaleFactor ** 4, - profile["mechProps"]["torsionalConstantX"] / ScaleFactor ** 4, + profile["mechProps"]["crossSectionArea"] / ScaleFactor**2, + profile["mechProps"]["momentOfInertiaY"] / ScaleFactor**4, + profile["mechProps"]["momentOfInertiaZ"] / ScaleFactor**4, + profile["mechProps"]["torsionalConstantX"] / ScaleFactor**4, ), } @@ -572,9 +544,7 @@ if __name__ == "__main__": files = fileNames for fileName in files: - BASE_PATH = Path( - "/home/jesusbill/Dev-Projects/github.com/IfcOpenShell/analysis-models/models/" - ) + BASE_PATH = Path("/home/jesusbill/Dev-Projects/github.com/IfcOpenShell/analysis-models/models/") DATAFILENAME = BASE_PATH / fileName / f"{fileName}.json" ASTERFILENAME = BASE_PATH / fileName / f"{fileName}.comm" COMMANDFILE(DATAFILENAME, ASTERFILENAME) diff --git a/src/ifc2ca/_deprecated/scriptSalomeBonded.py b/src/ifc2ca/_deprecated/scriptSalomeBonded.py index 2a68869ec9..4cde543334 100644 --- a/src/ifc2ca/_deprecated/scriptSalomeBonded.py +++ b/src/ifc2ca/_deprecated/scriptSalomeBonded.py @@ -30,6 +30,7 @@ from pathlib import Path flatten = itertools.chain.from_iterable + class MODEL: def __init__(self, dataFilename, medFilename, meshSize, zGround): self.dataFilename = dataFilename @@ -97,9 +98,7 @@ class MODEL: shapeType = "EDGE" if geometryType == "surface": shapeType = "FACE" - return self.geompy.MakePartition( - objects, [], [], [], self.geompy.ShapeType[shapeType], 0, [], 1 - ) + 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"]) @@ -195,29 +194,21 @@ class MODEL: el["linkObjs"] = [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["referenceName"] == rel["relatedConnection"]][0] if rel["eccentricity"]: rel["index"] = len(conn["relatedElements"]) + 1 - geometry = self.getLinkGeometry( - rel["eccentricity"], el["orientation"], conn["geometry"] - ) + geometry = self.getLinkGeometry(rel["eccentricity"], el["orientation"], conn["geometry"]) el["linkObjs"][j] = self.makeObject(geometry, "line") conn["relatedElements"].append(rel) # Make assemble of Building Object bldObjs = [] bldObjs.extend([el["elemObj"] 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])) # bldComp = geompy.MakeCompound(bldObjs) - bldComp = geompy.MakePartition( - bldObjs, [], [], [], self.geompy.ShapeType[buildingShapeType], 0, [], 1 - ) + bldComp = geompy.MakePartition(bldObjs, [], [], [], self.geompy.ShapeType[buildingShapeType], 0, [], 1) geompy.addToStudy(bldComp, "bldComp") elapsed_time = time.time() - init_time @@ -227,25 +218,19 @@ class MODEL: # Define and add groups for all curve, surface and rigid members if len([e for e in elements if e["geometryType"] == "line"]) > 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["geometryType"] == "line"]) # 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: # Make compound of requested group - compoundTemp = geompy.MakeCompound( - [e["elemObj"] for e in elements if e["geometryType"] == "surface"] - ) + compoundTemp = geompy.MakeCompound([e["elemObj"] for e in elements if e["geometryType"] == "surface"]) # Define group object and add to study surfaceCompound = geompy.GetInPlace(bldComp, compoundTemp, True) geompy.addToStudyInFather(bldComp, surfaceCompound, "SurfaceMembers") - linkObjs = list( - flatten([[obj for obj in el["linkObjs"] if obj] for el in elements]) - ) + linkObjs = list(flatten([[obj for obj in el["linkObjs"] if obj] for el in elements])) if len(linkObjs) > 0: # Make compound of requested group compoundTemp = geompy.MakeCompound(linkObjs) @@ -256,21 +241,15 @@ class MODEL: for el in elements: # el['partObj'] = geompy.RestoreGivenSubShapes(bldComp, [el['partObj']], GEOM.FSM_GetInPlace, False, False)[0] el["elemObj"] = geompy.GetInPlace(bldComp, el["elemObj"], True) - geompy.addToStudyInFather( - bldComp, el["elemObj"], self.getGroupName(el["referenceName"]) - ) + geompy.addToStudyInFather(bldComp, el["elemObj"], self.getGroupName(el["referenceName"])) for j, rel in enumerate(el["connections"]): if rel["eccentricity"]: # point geometry - el["linkObjs"][j] = geompy.GetInPlace( - bldComp, el["linkObjs"][j], True - ) + el["linkObjs"][j] = geompy.GetInPlace(bldComp, el["linkObjs"][j], True) geompy.addToStudyInFather( bldComp, el["linkObjs"][j], - self.getGroupName(el["referenceName"]) - + "_1DR_" - + self.getGroupName(rel["relatedConnection"]), + self.getGroupName(el["referenceName"]) + "_1DR_" + self.getGroupName(rel["relatedConnection"]), ) elapsed_time = time.time() - init_time @@ -307,9 +286,7 @@ class MODEL: NETGEN2D_Pars.SetFuseEdges(254) isDone = bldMesh.Compute() - coincident_nodes_on_part = bldMesh.FindCoincidentNodesOnPart( - [bldMesh], tolLoc, [], 0 - ) + coincident_nodes_on_part = bldMesh.FindCoincidentNodesOnPart([bldMesh], tolLoc, [], 0) if coincident_nodes_on_part: # bldMesh.MergeNodes(coincident_nodes_on_part, [], 0) # print(f'{len(coincident_nodes_on_part)} Sets of Coincident Nodes Found and Merged') @@ -349,25 +326,19 @@ class MODEL: shapeType = SMESH.EDGE if el["geometryType"] == "surface": shapeType = SMESH.FACE - tempgroup = bldMesh.GroupOnGeom( - el["elemObj"], self.getGroupName(el["referenceName"]), shapeType - ) + tempgroup = bldMesh.GroupOnGeom(el["elemObj"], self.getGroupName(el["referenceName"]), shapeType) smesh.SetName(tempgroup, self.getGroupName(el["referenceName"])) for j, rel in enumerate(el["connections"]): if rel["eccentricity"]: tempgroup = bldMesh.GroupOnGeom( el["linkObjs"][j], - self.getGroupName(el["referenceName"]) - + "_1DR_" - + self.getGroupName(rel["relatedConnection"]), + self.getGroupName(el["referenceName"]) + "_1DR_" + self.getGroupName(rel["relatedConnection"]), SMESH.EDGE, ) smesh.SetName( tempgroup, - self.getGroupName(el["referenceName"]) - + "_1DR_" - + self.getGroupName(rel["relatedConnection"]), + self.getGroupName(el["referenceName"]) + "_1DR_" + self.getGroupName(rel["relatedConnection"]), ) self.mesh = bldMesh @@ -420,9 +391,7 @@ if __name__ == "__main__": zGround = 0 for fileName in files: - BASE_PATH = Path( - "/home/jesusbill/Dev-Projects/github.com/IfcOpenShell/analysis-models/models/" - ) + 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, zGround) diff --git a/src/ifc2ca/ca2ifc.py b/src/ifc2ca/ca2ifc.py index 151edce378..f2be1bf0cc 100644 --- a/src/ifc2ca/ca2ifc.py +++ b/src/ifc2ca/ca2ifc.py @@ -1,4 +1,3 @@ - # Ifc2CA - IFC Code_Aster utility # Copyright (C) 2020, 2021, 2023, 2024 Ioannis P. Christovasilis # diff --git a/src/ifc2ca/ifc2ca.py b/src/ifc2ca/ifc2ca.py index 2dd4b04263..b380804dbb 100644 --- a/src/ifc2ca/ifc2ca.py +++ b/src/ifc2ca/ifc2ca.py @@ -24,10 +24,12 @@ from pathlib import Path from typing import Dict, List import ifcopenshell as ios + # import ifcopenshell.geom import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation + # import ifcopenshell.util.shape import numpy as np from jinja2 import Environment, FileSystemLoader diff --git a/src/ifc4d/__main__.py b/src/ifc4d/__main__.py index a4e8087a54..d7e2a58307 100644 --- a/src/ifc4d/__main__.py +++ b/src/ifc4d/__main__.py @@ -9,23 +9,26 @@ from ifc4d.pp2ifc import PP2Ifc import ifcopenshell -parser = argparse.ArgumentParser() -parser .add_argument('-f','--file', action='store', type=str, - required=True, help="schedule file name to be parsed") -parser .add_argument('-s','--schedule', action='store', required=True, - type=str, help='file format as xer, p6xml, mspxml, pp') -parser .add_argument('-i', '--ifcfile', action='store', required=False, - type=str, help='ifc file name as string e.g. \"file.ifc\"') -parser .add_argument('-o', '--output', action='store', required=True, - type=str, help='ifc file name as string e.g. \"file.ifc\"') -args = parser .parse_args() +parser = argparse.ArgumentParser() +parser.add_argument("-f", "--file", action="store", type=str, required=True, help="schedule file name to be parsed") +parser.add_argument( + "-s", "--schedule", action="store", required=True, type=str, help="file format as xer, p6xml, mspxml, pp" +) +parser.add_argument( + "-i", "--ifcfile", action="store", required=False, type=str, help='ifc file name as string e.g. "file.ifc"' +) +parser.add_argument( + "-o", "--output", action="store", required=True, type=str, help='ifc file name as string e.g. "file.ifc"' +) +args = parser.parse_args() + def get_file(): ifcfile = None if args.ifcfile: ifcfile = ifcopenshell.open(args.ifcfile) elif args.output: - ifc = ifcopenshell.file(schema='IFC4') + ifc = ifcopenshell.file(schema="IFC4") ifc.create_entity("IfcWorkPlan") ifc.create_entity("IfcProject") ifc.write(args.output) @@ -34,6 +37,7 @@ def get_file(): ifcfile = None return ifcfile + if not args.ifcfile: print("You need to provide an ifc file to add schedule") print("Examples python ifc4d -i model.ifc -s xer -f schedule.xer -o newifc.ifc") @@ -41,7 +45,7 @@ elif not args.output: print("an output file is required to save changes") print("python ifc4d -o newfile.ifc -s xer -f schedule.xer") -elif args.schedule== "xer": +elif args.schedule == "xer": p6xer = P6XER2Ifc() p6xer.xer = args.file p6xer.output = args.output @@ -49,7 +53,8 @@ elif args.schedule== "xer": if ifcfile: p6xer.file = ifcfile p6xer.execute() - else: raise Exception("No files provided for output") + else: + raise Exception("No files provided for output") elif args.schedule == "mspxml": msp = MSP2Ifc() msp.xml = args.file() @@ -76,4 +81,3 @@ elif args.schedule == "pp": pp.execute() else: print("schedule type you selected is not implemented at the moment") - diff --git a/src/ifc4d/ifc4d/common.py b/src/ifc4d/ifc4d/common.py index e12281dd5b..96ffe7a0af 100644 --- a/src/ifc4d/ifc4d/common.py +++ b/src/ifc4d/ifc4d/common.py @@ -246,12 +246,11 @@ class ScheduleIfcGenerator: "ScheduleStart": activity["StartDate"], "ScheduleFinish": activity["FinishDate"], "DurationType": "WORKTIME" if activity["PlannedDuration"] else None, - "ScheduleDuration": timedelta( - days=float(activity["PlannedDuration"]) / float(calendar["HoursPerDay"] or 8) - ) - or None - if activity["PlannedDuration"] - else None, + "ScheduleDuration": ( + timedelta(days=float(activity["PlannedDuration"]) / float(calendar["HoursPerDay"] or 8)) or None + if activity["PlannedDuration"] + else None + ), }, ) diff --git a/src/ifc4d/ifc4d/csv4d2ifc.py b/src/ifc4d/ifc4d/csv4d2ifc.py index 3204311d9d..19554614d2 100644 --- a/src/ifc4d/ifc4d/csv4d2ifc.py +++ b/src/ifc4d/ifc4d/csv4d2ifc.py @@ -86,12 +86,36 @@ class Csv2Ifc: task_relationships = self.parse_task_rel(row[self.headers["Relationships"]]) - scheduled_start_date = ifcopenshell.util.date.string_to_date(row[self.headers["ScheduleStart"]]) if row[self.headers["ScheduleStart"]] else None - scheduled_finish_date = ifcopenshell.util.date.string_to_date(row[self.headers["ScheduleFinish"]]) if row[self.headers["ScheduleFinish"]] else None - scheduled_duration = ifcopenshell.util.date.string_to_duration(row[self.headers["ScheduleDuration"]]) if row[self.headers["ScheduleDuration"]] else None - actual_start_date = ifcopenshell.util.date.string_to_date(row[self.headers["ActualStart"]]) if row[self.headers["ActualStart"]] else None - actual_finish_date = ifcopenshell.util.date.string_to_date(row[self.headers["ActualFinish"]]) if row[self.headers["ActualFinish"]] else None - actual_duration = ifcopenshell.util.date.string_to_duration(row[self.headers["ActualDuration"]]) if row[self.headers["ActualDuration"]] else None + scheduled_start_date = ( + ifcopenshell.util.date.string_to_date(row[self.headers["ScheduleStart"]]) + if row[self.headers["ScheduleStart"]] + else None + ) + scheduled_finish_date = ( + ifcopenshell.util.date.string_to_date(row[self.headers["ScheduleFinish"]]) + if row[self.headers["ScheduleFinish"]] + else None + ) + scheduled_duration = ( + ifcopenshell.util.date.string_to_duration(row[self.headers["ScheduleDuration"]]) + if row[self.headers["ScheduleDuration"]] + else None + ) + actual_start_date = ( + ifcopenshell.util.date.string_to_date(row[self.headers["ActualStart"]]) + if row[self.headers["ActualStart"]] + else None + ) + actual_finish_date = ( + ifcopenshell.util.date.string_to_date(row[self.headers["ActualFinish"]]) + if row[self.headers["ActualFinish"]] + else None + ) + actual_duration = ( + ifcopenshell.util.date.string_to_duration(row[self.headers["ActualDuration"]]) + if row[self.headers["ActualDuration"]] + else None + ) return { "Hierarchy": hierarchy, @@ -191,7 +215,7 @@ class Csv2Ifc: self.file, related_process=task_2, relating_process=task_1, - sequence_type = rel_type, + sequence_type=rel_type, ) if rel_type: ifcopenshell.api.run( diff --git a/src/ifc4d/ifc4d/ifc2p6.py b/src/ifc4d/ifc4d/ifc2p6.py index 7d3b9e4598..f0569d7873 100644 --- a/src/ifc4d/ifc4d/ifc2p6.py +++ b/src/ifc4d/ifc4d/ifc2p6.py @@ -46,9 +46,9 @@ class Ifc2P6: self.root = ET.Element("APIBusinessObjects") self.root.attrib["xmlns"] = "http://xmlns.oracle.com/Primavera/P6Professional/V18.8/API/BusinessObjects" self.root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance" - self.root.attrib[ - "xsi:schemaLocation" - ] = "http://xmlns.oracle.com/Primavera/P6Professional/V18.8/API/BusinessObjects http://xmlns.oracle.com/Primavera/P6Professional/V18.8/API/p6apibo.xsd" + self.root.attrib["xsi:schemaLocation"] = ( + "http://xmlns.oracle.com/Primavera/P6Professional/V18.8/API/BusinessObjects http://xmlns.oracle.com/Primavera/P6Professional/V18.8/API/p6apibo.xsd" + ) self.schedule = self.file.by_type("IfcWorkSchedule")[0] diff --git a/src/ifc4d/ifc4d/msp2ifc.py b/src/ifc4d/ifc4d/msp2ifc.py index face82e7e9..fb0f7acab0 100644 --- a/src/ifc4d/ifc4d/msp2ifc.py +++ b/src/ifc4d/ifc4d/msp2ifc.py @@ -112,13 +112,12 @@ class MSP2Ifc: # If first column = "all" then retrieve all columns if len(self.optionalColumns) and self.optionalColumns[0] == "all": self.optionalColumns = [child.tag.split("}")[1] for child in task] - + for column in self.optionalColumns: if not self.tasks[task_id].get(column): - self.tasks[task_id][column] = task.find(f"pr:{column}", self.ns).text if task.find(f"pr:{column}", self.ns) else None - - - + self.tasks[task_id][column] = ( + task.find(f"pr:{column}", self.ns).text if task.find(f"pr:{column}", self.ns) else None + ) def parse_calendar_xml(self, project): def parse_working_times(day): @@ -139,27 +138,37 @@ class MSP2Ifc: work_times = parse_working_times(exception) time_period = exception.find("pr:TimePeriod", self.ns) data = { - "Name": exception.find("pr:Name", self.ns).text - if exception.find("pr:Name", self.ns) is not None - else None, - "FromDate": datetime.datetime.fromisoformat(time_period.find("pr:FromDate", self.ns).text) - if time_period is not None - else None, - "ToDate": datetime.datetime.fromisoformat(time_period.find("pr:ToDate", self.ns).text) - if time_period is not None - else None, - "Occurrences": int(exception.find("pr:Occurrences", self.ns).text) - if exception.find("pr:Occurrences", self.ns) is not None - else None, - "Month": exception.find("pr:Month", self.ns).text - if exception.find("pr:Month", self.ns) is not None - else None, - "MonthDay": exception.find("pr:MonthDay", self.ns).text - if exception.find("pr:MonthDay", self.ns) is not None - else None, - "Type": exception.find("pr:Type", self.ns).text - if exception.find("pr:Type", self.ns) is not None - else None, + "Name": ( + exception.find("pr:Name", self.ns).text if exception.find("pr:Name", self.ns) is not None else None + ), + "FromDate": ( + datetime.datetime.fromisoformat(time_period.find("pr:FromDate", self.ns).text) + if time_period is not None + else None + ), + "ToDate": ( + datetime.datetime.fromisoformat(time_period.find("pr:ToDate", self.ns).text) + if time_period is not None + else None + ), + "Occurrences": ( + int(exception.find("pr:Occurrences", self.ns).text) + if exception.find("pr:Occurrences", self.ns) is not None + else None + ), + "Month": ( + exception.find("pr:Month", self.ns).text + if exception.find("pr:Month", self.ns) is not None + else None + ), + "MonthDay": ( + exception.find("pr:MonthDay", self.ns).text + if exception.find("pr:MonthDay", self.ns) is not None + else None + ), + "Type": ( + exception.find("pr:Type", self.ns).text if exception.find("pr:Type", self.ns) is not None else None + ), "WorkingTimes": work_times, "ifc": None, } @@ -283,16 +292,15 @@ class MSP2Ifc: for subtask_id in task["subtasks"]: self.create_task(self.tasks[subtask_id], parent_task=task) - # create pset for optional columns if len(self.optionalColumns): - pset = ifcopenshell.api.run("pset.add_pset", self.file, product=task["ifc"] , name="Pset_MSP_Task") + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=task["ifc"], name="Pset_MSP_Task") ifcopenshell.api.run( "pset.edit_pset", self.file, pset=pset, - properties= {name: str(task[name]) for name in self.optionalColumns if task[name]} + properties={name: str(task[name]) for name in self.optionalColumns if task[name]}, ) def process_working_week(self, week, calendar): diff --git a/src/ifc4d/ifc4d/wpattern.py b/src/ifc4d/ifc4d/wpattern.py index e69d8d0872..304f03777a 100644 --- a/src/ifc4d/ifc4d/wpattern.py +++ b/src/ifc4d/ifc4d/wpattern.py @@ -62,6 +62,7 @@ class AstaCalendarWorkPattern: if wp["DayOfWeek"] == days[lang][index]: wp["DayOfWeek"] = days["en"][index] return + translate_days(self.Days, self.dict_wp[-1]) for day in self.Days["en"]: diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 202f0158ff..25c547a76f 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -21,10 +21,12 @@ import json import ifcopenshell import ifcopenshell.api import ifcopenshell.api.pset +import ifcopenshell.util.element import ifcopenshell.util.unit import ifcopenshell.util.selector import multiprocessing from collections import namedtuple +from typing import Any Function = namedtuple("Function", ["measure", "name", "description"]) @@ -47,7 +49,7 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst return results -def edit_qtos(ifc_file, results) -> None: +def edit_qtos(ifc_file: ifcopenshell.file, results: dict[ifcopenshell.entity_instance, Any]) -> None: for element, qtos in results.items(): for name, quantities in qtos.items(): qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False) @@ -59,7 +61,7 @@ def edit_qtos(ifc_file, results) -> None: class SI2ProjectUnitConverter: - def __init__(self, ifc_file): + def __init__(self, ifc_file: ifcopenshell.file): self.project_units = { "IfcAreaMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "AREAUNIT"), "IfcLengthMeasure": ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT"), diff --git a/src/ifcbimtester/bimtester/features/steps/geolocation/en.py b/src/ifcbimtester/bimtester/features/steps/geolocation/en.py index 9d7105e64a..596aa858cf 100644 --- a/src/ifcbimtester/bimtester/features/steps/geolocation/en.py +++ b/src/ifcbimtester/bimtester/features/steps/geolocation/en.py @@ -30,7 +30,7 @@ from bimtester.ifc import IfcStore from bimtester.lang import _ -@step(u'There must be at least one "{ifc_class}" element') +@step('There must be at least one "{ifc_class}" element') def step_impl(context, ifc_class): assert len(IfcStore.file.by_type(ifc_class)) >= 1, _("An element of {} could not be found").format(ifc_class) @@ -68,7 +68,7 @@ def check_ifc4_geolocation(entity_name, prop_name=None, value=None, should_asser return actual_value -@step(u"The project must have coordinate reference system data") +@step("The project must have coordinate reference system data") def step_impl(context): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -77,7 +77,7 @@ def step_impl(context): check_ifc4_geolocation("IfcProjectedCRS") -@step(u'The name of the CRS must be "{coordinate_reference_name}"') +@step('The name of the CRS must be "{coordinate_reference_name}"') def step_impl(context, coordinate_reference_name): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -86,7 +86,7 @@ def step_impl(context, coordinate_reference_name): check_ifc4_geolocation("IfcProjectedCRS", "Name", coordinate_reference_name) -@step(u'The description of the CRS must be "{value}"') +@step('The description of the CRS must be "{value}"') def step_impl(context, value): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -95,7 +95,7 @@ def step_impl(context, value): check_ifc4_geolocation("IfcProjectedCRS", "Description", value) -@step(u'The geodetic datum must be "{coordinate_reference_name}"') +@step('The geodetic datum must be "{coordinate_reference_name}"') def step_impl(context, coordinate_reference_name): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -104,7 +104,7 @@ def step_impl(context, coordinate_reference_name): check_ifc4_geolocation("IfcProjectedCRS", "GeodeticDatum", coordinate_reference_name) -@step(u'The vertical datum must be "{coordinate_reference_name}"') +@step('The vertical datum must be "{coordinate_reference_name}"') def step_impl(context, coordinate_reference_name): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -113,7 +113,7 @@ def step_impl(context, coordinate_reference_name): check_ifc4_geolocation("IfcProjectedCRS", "VerticalDatum", coordinate_reference_name) -@step(u'The map projection must be "{coordinate_reference_name}"') +@step('The map projection must be "{coordinate_reference_name}"') def step_impl(context, coordinate_reference_name): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -122,7 +122,7 @@ def step_impl(context, coordinate_reference_name): check_ifc4_geolocation("IfcProjectedCRS", "MapProjection", coordinate_reference_name) -@step(u'The map zone must be "{coordinate_reference_name}"') +@step('The map zone must be "{coordinate_reference_name}"') def step_impl(context, coordinate_reference_name): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -131,7 +131,7 @@ def step_impl(context, coordinate_reference_name): check_ifc4_geolocation("IfcProjectedCRS", "MapZone", coordinate_reference_name) -@step(u'The map unit must be "{unit}"') +@step('The map unit must be "{unit}"') def step_impl(context, unit): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -148,7 +148,7 @@ def step_impl(context, unit): assert actual_value == unit, _('We expected a value of "{}" but instead got "{}"').format(unit, actual_value) -@step(u"The project must have coordinate transformations to convert from local to global coordinates") +@step("The project must have coordinate transformations to convert from local to global coordinates") def step_impl(context): if IfcStore.file.schema == "IFC2X3": for site in IfcStore.file.by_type("IfcSite"): @@ -156,7 +156,7 @@ def step_impl(context): check_ifc4_geolocation("IfcMapConversion") -@step(u'The eastings of the model must be offset by "{number}" to derive its global coordinates') +@step('The eastings of the model must be offset by "{number}" to derive its global coordinates') def step_impl(context, number): number = util.assert_number(number) if IfcStore.file.schema == "IFC2X3": @@ -166,7 +166,7 @@ def step_impl(context, number): check_ifc4_geolocation("IfcMapConversion", "Eastings", number) -@step(u'The northings of the model must be offset by "{number}" to derive its global coordinates') +@step('The northings of the model must be offset by "{number}" to derive its global coordinates') def step_impl(context, number): number = util.assert_number(number) if IfcStore.file.schema == "IFC2X3": @@ -176,7 +176,7 @@ def step_impl(context, number): check_ifc4_geolocation("IfcMapConversion", "Northings", number) -@step(u'The height of the model must be offset by "{number}" to derive its global coordinates') +@step('The height of the model must be offset by "{number}" to derive its global coordinates') def step_impl(context, number): number = util.assert_number(number) if IfcStore.file.schema == "IFC2X3": @@ -186,7 +186,7 @@ def step_impl(context, number): check_ifc4_geolocation("IfcMapConversion", "OrthogonalHeight", number) -@step(u'The model must be rotated clockwise by "{number}" to derive its global coordinates') +@step('The model must be rotated clockwise by "{number}" to derive its global coordinates') def step_impl(context, number): number = util.assert_number(number) if IfcStore.file.schema == "IFC2X3": @@ -198,7 +198,7 @@ def step_impl(context, number): assert actual_value == value, _('We expected a value of "{}" but instead got "{}"').format(value, actual_value) -@step(u'The model must be scaled along the horizontal axis by "{number}" to derive its global coordinates') +@step('The model must be scaled along the horizontal axis by "{number}" to derive its global coordinates') def step_impl(context, number): number = util.assert_number(number) if IfcStore.file.schema == "IFC2X3": @@ -208,7 +208,7 @@ def step_impl(context, number): check_ifc4_geolocation("IfcMapConversion", "Scale", number) -@step(u'The model must be rotated clockwise by "{number}" for true north to point up') +@step('The model must be rotated clockwise by "{number}" for true north to point up') def step_impl(context, number): number = util.assert_number(number) project = IfcStore.file.by_type("IfcProject")[0] @@ -228,7 +228,7 @@ def step_impl(context, number): assert False, _("True north is not defined in the file") -@step(u'The site "{guid}" has a longitude of "{number}"') +@step('The site "{guid}" has a longitude of "{number}"') def step_impl(context, guid, number): number = util.assert_number(number) site = util.assert_guid(IfcStore.file, guid) @@ -238,7 +238,7 @@ def step_impl(context, guid, number): util.assert_attribute(site, "RefLongitude", number) -@step(u'The site "{guid}" has a latitude of "{number}"') +@step('The site "{guid}" has a latitude of "{number}"') def step_impl(context, guid, number): number = util.assert_number(number) site = util.assert_guid(IfcStore.file, guid) @@ -248,7 +248,7 @@ def step_impl(context, guid, number): util.assert_attribute(site, "RefLatitude", number) -@step(u'The site "{guid}" has an elevation of "{number}"') +@step('The site "{guid}" has an elevation of "{number}"') def step_impl(context, guid, number): number = util.assert_number(number) site = util.assert_guid(IfcStore.file, guid) @@ -256,7 +256,7 @@ def step_impl(context, guid, number): util.assert_attribute(site, "RefElevation", number) -@step(u'The site "{guid}" must be coincident with the project origin') +@step('The site "{guid}" must be coincident with the project origin') def step_impl(context, guid): site = util.assert_guid(IfcStore.file, guid) util.assert_type(site, "IfcSite") diff --git a/src/ifcbimtester/bimtester/run.py b/src/ifcbimtester/bimtester/run.py index 725c208913..5f0d58bd18 100644 --- a/src/ifcbimtester/bimtester/run.py +++ b/src/ifcbimtester/bimtester/run.py @@ -38,7 +38,6 @@ from behave.__main__ import main as behave_main from logging import StreamHandler - class TestRunner: def __init__(self, ifc_path, schema_path=None, ifc=None): IfcStore.path = ifc_path diff --git a/src/ifcblender/io_import_scene_ifc/__init__.py b/src/ifcblender/io_import_scene_ifc/__init__.py index bc114da232..b005b15564 100644 --- a/src/ifcblender/io_import_scene_ifc/__init__.py +++ b/src/ifcblender/io_import_scene_ifc/__init__.py @@ -163,6 +163,7 @@ def import_ifc(filename, use_names, process_relations, blender_booleans): me.from_pydata(verts, [], faces) me.validate() + # MATERIAL CREATION def add_material(mname, props): if mname in bpy.data.materials: diff --git a/src/ifccityjson/ifccityjson/__main__.py b/src/ifccityjson/ifccityjson/__main__.py index 90361bb894..284560b288 100644 --- a/src/ifccityjson/ifccityjson/__main__.py +++ b/src/ifccityjson/ifccityjson/__main__.py @@ -20,6 +20,7 @@ import argparse from cjio import cityjson from .cityjson2ifc import Cityjson2ifc + def cmdline(): # Example: # python ifccityjson.py -i example/3DBAG_example.json -o example/output.ifc -n identificatie @@ -28,10 +29,10 @@ def cmdline(): parser.add_argument("-i", "--input", type=str, help="input CityJSON file", required=True) parser.add_argument("-o", "--output", type=str, help="output IFC file. Standard is output.ifc") parser.add_argument("-n", "--name", type=str, help="Attribute containing the name") - parser.add_argument('--split-lod', dest='split', action='store_true', - help="Split the file in multiple LoDs") - parser.add_argument('--no-split-lod', dest='split', action='store_false', - help="Do not split the file in multiple LoDs") + parser.add_argument("--split-lod", dest="split", action="store_true", help="Split the file in multiple LoDs") + parser.add_argument( + "--no-split-lod", dest="split", action="store_false", help="Do not split the file in multiple LoDs" + ) parser.add_argument("--lod", type=str, help="extract LOD value (example: 1.2)") parser.set_defaults(split=True) args = parser.parse_args() @@ -50,5 +51,6 @@ def cmdline(): converter.configuration(**data) converter.convert(city_model) -if __name__ == '__main__': + +if __name__ == "__main__": cmdline() diff --git a/src/ifccityjson/ifccityjson/cityjson2ifc/__init__.py b/src/ifccityjson/ifccityjson/cityjson2ifc/__init__.py index 71bcf23e7a..2bfb9dfce1 100644 --- a/src/ifccityjson/ifccityjson/cityjson2ifc/__init__.py +++ b/src/ifccityjson/ifccityjson/cityjson2ifc/__init__.py @@ -16,4 +16,4 @@ # You should have received a copy of the GNU Lesser General Public License # along with ifccityjson. If not, see . __version__ = "0.1.0" -from .cityjson2ifc import * \ No newline at end of file +from .cityjson2ifc import * diff --git a/src/ifcclash/bootstrap.py b/src/ifcclash/bootstrap.py index 15ec2f242a..dc9b028c23 100644 --- a/src/ifcclash/bootstrap.py +++ b/src/ifcclash/bootstrap.py @@ -1,4 +1,3 @@ - # IfcClash - IFC-based clash detection. # Copyright (C) 2020, 2021 Dion Moult # @@ -18,4 +17,3 @@ # along with IfcClash. If not, see . import ifcclash.__main__ - diff --git a/src/ifcclash/make.py b/src/ifcclash/make.py index 1e1f26ee70..b4436f6e14 100644 --- a/src/ifcclash/make.py +++ b/src/ifcclash/make.py @@ -24,4 +24,3 @@ import subprocess cmd = "pyinstaller ./bootstrap.py --name ifcclash --onefile --clean" subprocess.check_output(cmd, shell=True) - diff --git a/src/ifcfm/ifcfm/__main__.py b/src/ifcfm/ifcfm/__main__.py index 6b0fac9cb7..3a34dc245f 100644 --- a/src/ifcfm/ifcfm/__main__.py +++ b/src/ifcfm/ifcfm/__main__.py @@ -11,7 +11,13 @@ parser.add_argument( help="The FM standard to extract. Built-in preset standards include cobie24, cobie3, aohbsem, and basic.", ) parser.add_argument("-i", "--ifc", type=str, required=True, help="The IFC file") -parser.add_argument("-s", "--spreadsheet", type=str, default="output.ods", help="The spreadsheet file, or directory if the format is csv. Defaults to output.ods") +parser.add_argument( + "-s", + "--spreadsheet", + type=str, + default="output.ods", + help="The spreadsheet file, or directory if the format is csv. Defaults to output.ods", +) parser.add_argument( "-f", "--format", type=str, default="ods", help="The format, chosen from csv, ods, or xlsx. Defaults to ods." ) diff --git a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp index c1ec1805fd..2c7af56f61 100644 --- a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp +++ b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp @@ -391,6 +391,31 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity* return true; } +#include + +bool IfcGeom::OpenCascadeKernel::convert_impl(const taxonomy::revolve::ptr r, IfcGeom::ConversionResults& results) { + + + gp_Ax1 ax( + convert_xyz(*r->axis_origin), + convert_xyz(*r->direction)); + + TopoDS_Shape face; + if (!convert(taxonomy::cast(r->basis), face)) { + return false; + } + + TopoDS_Shape shape = BRepPrimAPI_MakeRevol(face, ax); + + results.emplace_back(ConversionResult( + r->instance->data().id(), + r->matrix, + new OpenCascadeShape(shape), + r->surface_style + )); + return true; +} + // IfcSchema::IfcRelVoidsElement::list::ptr IfcGeom::Kernel::find_openings(IfcSchema::IfcProduct* product) { // std::vector rs; // diff --git a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.h b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.h index 29becd032f..b5c6276488 100644 --- a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.h +++ b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.h @@ -128,7 +128,7 @@ public: {} bool convert(const ifcopenshell::geometry::taxonomy::extrusion::ptr, TopoDS_Shape&); - bool convert(const ifcopenshell::geometry::taxonomy::face::ptr, TopoDS_Shape&); + bool convert(const ifcopenshell::geometry::taxonomy::face::ptr, TopoDS_Shape&, bool reversed_surface = false); bool convert(const ifcopenshell::geometry::taxonomy::loop::ptr, TopoDS_Wire&); bool convert(const ifcopenshell::geometry::taxonomy::matrix4::ptr, gp_GTrsf&); bool convert(const ifcopenshell::geometry::taxonomy::shell::ptr, TopoDS_Shape&); @@ -143,6 +143,7 @@ public: virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::solid::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::shell::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::extrusion::ptr, IfcGeom::ConversionResults&); + virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::revolve::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::boolean_result::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::loft::ptr, IfcGeom::ConversionResults&); virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::sweep_along_curve::ptr, IfcGeom::ConversionResults&); diff --git a/src/ifcgeom/kernels/opencascade/face.cpp b/src/ifcgeom/kernels/opencascade/face.cpp index 86e386b3ed..c369e85387 100644 --- a/src/ifcgeom/kernels/opencascade/face.cpp +++ b/src/ifcgeom/kernels/opencascade/face.cpp @@ -229,7 +229,7 @@ Handle(Geom_Surface) OpenCascadeKernel::convert_surface(const taxonomy::ptr surf } } -bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& result) { +bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& result, bool reversed_surface) { #ifdef IFOPSH_DEBUG std::ostringstream oss; face->print(oss); @@ -382,7 +382,11 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re } } } else if (!fd.all_outer()) { - BRepBuilderAPI_MakeFace mf(fd.surface(), fd.outer_wire()); + auto surf = fd.surface(); + if (reversed_surface) { + surf = surf->UReversed(); + } + BRepBuilderAPI_MakeFace mf(surf, fd.outer_wire()); if (mf.IsDone()) { // Is this necessary @@ -434,10 +438,20 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re for (; jt.More(); jt.Next()) { Message_ListIteratorOfListOfMsg kt(jt.Value()); for (; kt.More(); kt.Next()) { - char* c = new char[kt.Value().Value().LengthOfCString() + 1]; - kt.Value().Value().ToUTF8CString(c); - Logger::Notice(c, face->instance); + char* c = new char[kt.Value().Original().LengthOfCString() + 1]; + kt.Value().Original().ToUTF8CString(c); + std::string message = c; delete[] c; +#if OCC_VERSION_MAJOR==7 && OCC_VERSION_MINOR == 7 + if (!reversed_surface && !fd.surface().IsNull() && fd.surface()->IsUPeriodic() && message == "Unknown message invoked with the keyword FixAdvFace.FixOrientation.MSG0") { + Logger::Notice("Detected reversed wire, reattempting with reversed basis surface"); + TopoDS_Face reversed_result; + convert(face, reversed_result, true); + result = reversed_result; + return true; + } else +#endif + Logger::Warning(message, face->instance); } } } diff --git a/src/ifcgeom/mapping/IfcCompositeCurve.cpp b/src/ifcgeom/mapping/IfcCompositeCurve.cpp index aed15a8f83..b9f2aaaf4d 100644 --- a/src/ifcgeom/mapping/IfcCompositeCurve.cpp +++ b/src/ifcgeom/mapping/IfcCompositeCurve.cpp @@ -92,7 +92,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCompositeCurve* inst) { return loop; } else { - auto pwf = taxonomy::make(pwfs,&settings_,inst); + auto pwf = taxonomy::make(0.0,pwfs,&settings_,inst); return pwf; } } diff --git a/src/ifcgeom/mapping/IfcCurveSegment.cpp b/src/ifcgeom/mapping/IfcCurveSegment.cpp index d50ab31281..d50112200a 100644 --- a/src/ifcgeom/mapping/IfcCurveSegment.cpp +++ b/src/ifcgeom/mapping/IfcCurveSegment.cpp @@ -888,9 +888,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCurveSegment* inst) { auto length = cse.length(); - taxonomy::piecewise_function::spans spans; + taxonomy::piecewise_function::spans_t spans; spans.emplace_back(fabs(length), fn); - auto pwf = taxonomy::make(spans,&settings_,inst); + auto pwf = taxonomy::make(0.0, spans,&settings_,inst); return pwf; } diff --git a/src/ifcgeom/mapping/IfcGradientCurve.cpp b/src/ifcgeom/mapping/IfcGradientCurve.cpp index c59286d3af..3c5b95635a 100644 --- a/src/ifcgeom/mapping/IfcGradientCurve.cpp +++ b/src/ifcgeom/mapping/IfcGradientCurve.cpp @@ -27,11 +27,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) { if (!inst->BaseCurve()->as()) Logger::Warning("Expected IfcGradientCurve.BaseCurve to be IfcCompositeCurve", inst); // CT 4.1.7.1.1.2 - auto horizontal = taxonomy::cast(map(inst->BaseCurve())); - auto segments = inst->Segments(); - std::vector pwfs; + std::vector pwfs; for (auto& segment : *segments) { if (segment->as()) { @@ -48,11 +46,33 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) { return nullptr; } } - - auto vertical = taxonomy::make(pwfs,&settings_); + // Get starting position of gradient curve, which is relative to the base curve + // The gradient curve can start before or after the start of the base curve + auto first_segment = *(segments->begin()); + auto p = taxonomy::cast(map(first_segment->as()->Placement())); + const Eigen::Matrix4d& m = p->ccomponents(); + double gradient_start = m(0, 3); // start of vertical (row 0, col 3) - "Distance Along" horizontal curve + + // create the vertical pwf + auto vertical = taxonomy::make(gradient_start, pwfs, &settings_); + + // Determine the valid domain of the PWF... the valid domain is where both + // the base curve and gradient curves are defined + auto horizontal = taxonomy::cast(map(inst->BaseCurve())); + double start = std::max(horizontal->start(),vertical->start()); + double end = std::min(horizontal->end(), vertical->end()); + double length = end - start; + + if (!(0 < length)) { + Logger::Error("IfcGradientCurve does not have a common domain with BaseCurve"); + } + + // define the callback function for the gradient curve auto composition = [horizontal, vertical](double u)->Eigen::Matrix4d { - auto xy = horizontal->evaluate(u); + // u is distance from start of gradient curve (vertical) + // add vertical->start() to u to get distance from start of horizontal + auto xy = horizontal->evaluate(u + vertical->start()); auto uz = vertical->evaluate(u); uz.col(3)(0) = 0.0; // x is distance along. zero it out so it doesn't add to the x from horizontal @@ -64,11 +84,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) { return m; }; - double min_length = std::min(horizontal->length(), vertical->length()); - - taxonomy::piecewise_function::spans spans; - spans.emplace_back(min_length, composition); - auto pwf = taxonomy::make(spans, &settings_, inst); + taxonomy::piecewise_function::spans_t spans; + spans.emplace_back(length, composition); + auto pwf = taxonomy::make(start, spans, &settings_, inst); return pwf; } diff --git a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp index 46ea9eee34..7af8b42bd8 100644 --- a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp +++ b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp @@ -38,11 +38,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst auto first_offset_value = *(offset_values->begin()); auto basis_curve = inst->BasisCurve(); - //auto pw_curve = ifcopenshell::geometry::piecewise_from_item(map(basis_curve)); auto pw_curve = taxonomy::dcast(map(basis_curve)); + + double start = pw_curve->start(); double basis_curve_length = pw_curve->length(); - taxonomy::piecewise_function::spans offset_spans; + taxonomy::piecewise_function::spans_t offset_spans; #if defined SCHEMA_HAS_IfcDistanceExpression double first_distance = first_offset_value->DistanceAlong(); @@ -143,7 +144,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst offset_spans.emplace_back(l, fn); } - auto offsets = taxonomy::make(offset_spans,&settings_); + auto offsets = taxonomy::make(start,offset_spans,&settings_); auto composition = [pw_curve, offsets](double u) -> Eigen::Matrix4d { auto p = pw_curve->evaluate(u); @@ -154,9 +155,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst // current implementation assumes that composition is equal to the full length of basis curve // this may change depending on decisions in the bSI-IF - taxonomy::piecewise_function::spans spans; + taxonomy::piecewise_function::spans_t spans; spans.emplace_back(basis_curve_length, composition); - auto pwf = taxonomy::make(spans,&settings_,inst); + auto pwf = taxonomy::make(start,spans,&settings_,inst); return pwf; } diff --git a/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp b/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp index bab1808601..caf8104c65 100644 --- a/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp +++ b/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp @@ -27,8 +27,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins if (!inst->BaseCurve()->as()) Logger::Warning("Expected IfcSegmentedReferenceCurve.BaseCurve to be IfcGradient", inst); // CT 4.1.7.1.1.3 - auto gradient = taxonomy::cast(map(inst->BaseCurve())); - auto segments = inst->Segments(); std::vector pwfs; @@ -46,11 +44,34 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins Logger::Error("Unsupported"); return nullptr; } - } - auto cant = taxonomy::make(pwfs,&settings_); + } + // Get starting position of cant curve, relative to the gradient curve. + // The cant curve can start before or after the start of the gradient curve + auto first_segment = *(segments->begin()); + auto p = taxonomy::cast(map(first_segment->as()->Placement())); + const Eigen::Matrix4d& m = p->ccomponents(); + double cant_start = m(0, 3); // start of cant curve + + auto cant = taxonomy::make(cant_start,pwfs,&settings_); + + // Determine the valid domain of the PWF... the valid domain is where + // horizontal, gradient and cant curves are defined + auto gradient = taxonomy::cast(map(inst->BaseCurve())); + auto horizontal = taxonomy::cast(map(inst->BaseCurve()->as()->BaseCurve())); + double start = std::max(std::max(cant->start(), gradient->start()), horizontal->start()); + double end = std::min(std::min(cant->end(), gradient->end()), horizontal->end()); + double length = end - start; + + if (!(0 < length)) { + Logger::Error("IfcSegmentedReferenceCurve does not have a common domain with BaseCurve"); + } + + // define the callback function for the segmented reference curve auto composition = [gradient, cant](double u)->Eigen::Matrix4d { - auto g = gradient->evaluate(u); + // u is distance from start of cant curve + // add cant->start() to u to get the distance from start of gradient curve + auto g = gradient->evaluate(u+cant->start()); auto c = cant->evaluate(u); c.col(3)(0) = 0.0; // x is distance along. zero it out so it doesn't add to the x from gradient curve @@ -62,11 +83,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins return m; }; - double min_length = std::min(gradient->length(), cant->length()); - - taxonomy::piecewise_function::spans spans; - spans.emplace_back(min_length, composition); - auto pwf = taxonomy::make(spans, &settings_, inst); + taxonomy::piecewise_function::spans_t spans; + spans.emplace_back(length, composition); + auto pwf = taxonomy::make(start, spans, &settings_, inst); return pwf; } diff --git a/src/ifcgeom/mapping/mapping.cpp b/src/ifcgeom/mapping/mapping.cpp index 3c297f03be..2c4c40ea1a 100644 --- a/src/ifcgeom/mapping/mapping.cpp +++ b/src/ifcgeom/mapping/mapping.cpp @@ -299,14 +299,45 @@ const IfcUtil::IfcBaseEntity* mapping::get_single_material_association(const Ifc single_material = associated_material->as(); // NB: Single-layer layersets are also considered, regardless of --enable-layerset-slicing, this // in accordance with other viewers. - if (!single_material && associated_material->as()) { - IfcSchema::IfcMaterialLayerSet* layerset = associated_material->as()->ForLayerSet(); - if (settings_.get().value ? layerset->MaterialLayers()->size() >= 1 : layerset->MaterialLayers()->size() == 1) { - IfcSchema::IfcMaterialLayer* layer = (*layerset->MaterialLayers()->begin()); - if (layer->Material()) { - single_material = layer->Material(); + if (!single_material) { + if (associated_material->as() || associated_material->as()) { + IfcSchema::IfcMaterialLayerSet* layerset; + if (auto *m = associated_material->as()) { + if (m->get("ForLayerSet")->isNull()) { + Logger::Warning("Missing ForLayerSet for:", m); + return nullptr; + } + layerset = m->ForLayerSet(); + } else { + layerset = associated_material->as(); + } + if (settings_.get().value ? layerset->MaterialLayers()->size() >= 1 : layerset->MaterialLayers()->size() == 1) { + IfcSchema::IfcMaterialLayer* layer = (*layerset->MaterialLayers()->begin()); + if (auto *m_ = layer->Material()) { + single_material = m_; + } } } +#ifdef SCHEMA_HAS_IfcMaterialProfileSet + if (associated_material->as() || associated_material->as()) { + IfcSchema::IfcMaterialProfileSet* profileset; + if (auto* m = associated_material->as()) { + if (m->get("ForProfileSet")->isNull()) { + Logger::Warning("Missing ForProfileSet for:", m); + return nullptr; + } + profileset = m->ForProfileSet(); + } else { + profileset = associated_material->as(); + } + if (settings_.get().value ? profileset->MaterialProfiles()->size() >= 1 : profileset->MaterialProfiles()->size() == 1) { + IfcSchema::IfcMaterialProfile* profile = (*profileset->MaterialProfiles()->begin()); + if (auto *m_ = profile->Material()) { + single_material = m_; + } + } + } +#endif } } } @@ -581,6 +612,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcStyledItem* inst) { taxonomy::ptr mapping::map(const IfcBaseInterface* inst) { auto iden = inst->as()->identity(); if (use_caching_) { + std::lock_guard guard(cache_guard_); auto it = cache_.find(iden); if (it != cache_.end()) { return it->second; @@ -598,7 +630,8 @@ taxonomy::ptr mapping::map(const IfcBaseInterface* inst) { if (item) { if (use_caching_) { - cache_.insert({ iden, item }); + std::lock_guard guard(cache_guard_); + cache_.insert({iden, item}); } } else if (!matched) { Logger::Message(Logger::LOG_ERROR, "No operation defined for:", inst); diff --git a/src/ifcgeom/mapping/mapping.h b/src/ifcgeom/mapping/mapping.h index 1111101bb6..ace92a2276 100644 --- a/src/ifcgeom/mapping/mapping.h +++ b/src/ifcgeom/mapping/mapping.h @@ -6,6 +6,8 @@ #include "../../ifcparse/IfcFile.h" #include "../../ifcparse/IfcLogger.h" +#include + #define INCLUDE_SCHEMA(x) STRINGIFY(../../ifcparse/x.h) #include INCLUDE_SCHEMA(IfcSchema) #undef INCLUDE_SCHEMA @@ -24,6 +26,7 @@ namespace geometry { std::string length_unit_name_; std::map cache_; + std::mutex cache_guard_; // provides mutually exclusive access to cache_ const IfcParse::declaration* placement_rel_to_type_; const IfcUtil::IfcBaseEntity* placement_rel_to_instance_; diff --git a/src/ifcgeom/piecewise_function_impl.cpp b/src/ifcgeom/piecewise_function_impl.cpp new file mode 100644 index 0000000000..ecd2762057 --- /dev/null +++ b/src/ifcgeom/piecewise_function_impl.cpp @@ -0,0 +1,106 @@ +#include "piecewise_function_impl.h" +#include "profile_helper.h" + +namespace ifcopenshell { + +namespace geometry { + +namespace taxonomy { + +std::vector ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluation_points() const { + if (!eval_points_.has_value()) { + double curve_length = length(); + + auto param_type = settings_ ? settings_->get().get() : ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE; + auto param = settings_ ? settings_->get().get() : 0.5; + unsigned num_steps = 0; + if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) { + // parameter is max step size + num_steps = (unsigned)std::ceil(curve_length / param); + } else { + // parameter is minimum number of steps + num_steps = (unsigned)std::ceil(param); + } + + eval_points_ = evaluation_points(start_, start_ + curve_length, num_steps); + } + return *eval_points_; +} + +std::vector ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluation_points(double ustart, double uend, unsigned nsteps) const { + double curve_length = length(); + ustart = std::max(start_, ustart); + uend = std::min(uend, start_ + curve_length); + + nsteps = std::max(1u, nsteps); // never have fewer than 1 step + + auto resolution = (uend - ustart) / nsteps; + + std::vector u_values; + u_values.reserve(nsteps); + + for (unsigned i = 0; i <= nsteps; ++i) { + auto u = resolution * i + ustart; + u_values.push_back(u); + } + + return u_values; +} + +ifcopenshell::geometry::taxonomy::item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate() const { + return evaluate(evaluation_points()); +} + +item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(double ustart, double uend, unsigned nsteps) const { + return evaluate(evaluation_points(ustart, uend, nsteps)); +} + +item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(const std::vector& dist) const { + std::vector polygon; + polygon.reserve(dist.size()); + for (auto& u : dist) { + Eigen::Matrix4d m = evaluate(u); + polygon.push_back(taxonomy::make(m.col(3)(0), m.col(3)(1), m.col(3)(2))); + } + + return polygon_from_points(polygon); +} + +Eigen::Matrix4d ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(double u) const { + // assume monotonic evaluation and store last evaluated segment + if (current_span_fn_ == nullptr || (u < current_span_start_ || current_span_end_ < u)) { + // there isn't a current span or u is outside the range of the current span + // get a new "current span" + std::tie(current_span_start_, current_span_end_, current_span_fn_) = get_span(u); + } + + u -= current_span_start_; // make u relative to start of span + return (*current_span_fn_)(u); +} + +std::tuple*> ifcopenshell::geometry::taxonomy::piecewise_function_impl::get_span(double u) const { + // force u to be within bounds of the curve + double s = start(); + double e = end(); + u = std::max(s, u); + u = std::min(u, e); + + double span_start = s; + for (auto& [length, fn] : spans_) { + double span_end = span_start + length; + auto tolerance = settings_ ? settings_->get().get() : 0.001; + if (span_start <= u && u < span_end + tolerance) { + return {span_start, span_end, &fn}; + } + span_start += length; + } + + Logger::Error("piecewise_function_impl::get_span span not found."); + return {0, 0, nullptr}; +} + +} // namespace taxonomy + +} // namespace geometry + +} // namespace ifcopenshell diff --git a/src/ifcgeom/piecewise_function_impl.h b/src/ifcgeom/piecewise_function_impl.h new file mode 100644 index 0000000000..0c2f306ac5 --- /dev/null +++ b/src/ifcgeom/piecewise_function_impl.h @@ -0,0 +1,93 @@ +#ifndef PIECEWISE_FUNCTION_IMPL +#define PIECEWISE_FUNCTION_IMPL + +#include "taxonomy.h" + +namespace ifcopenshell { + +namespace geometry { + +namespace taxonomy { + +struct piecewise_function_impl { + using spans_t = std::vector>>; + + piecewise_function_impl(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings = nullptr) : start_(start), + settings_(settings), + spans_(s){}; + piecewise_function_impl(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings = nullptr) : start_(start), + settings_(settings) { + for (auto& pwf : pwfs) { + spans_.insert(spans_.end(), pwf->spans().begin(), pwf->spans().end()); + } + }; + piecewise_function_impl(piecewise_function_impl&&) = default; + piecewise_function_impl(const piecewise_function_impl&) = default; + + const ifcopenshell::geometry::Settings* settings_ = nullptr; + + const spans_t& spans() const { return spans_; } + + bool is_empty() const { return spans_.empty(); } + + double start() const { + return start_; + } + + double end() const { + return start_ + length(); + } + + double length() const { + if (!length_.has_value()) { + length_ = std::accumulate(spans_.begin(), spans_.end(), 0.0, [](const auto& v, const auto& s) { return v + s.first; }); + } + return *length_; + } + + piecewise_function_impl* clone_() const { return new piecewise_function_impl(*this); } + + /// @brief returns a vector of "distance along" points where the evaluate function computes loop points + std::vector evaluation_points() const; + + /// @brief returns a vector of "distance along" points between ustart and uend + /// @param ustart starting location + /// @param uend ending location + /// @param nsteps number of steps to evaluate + std::vector evaluation_points(double ustart, double uend, unsigned nsteps) const; + + /// @brief evaluates the piecewise function between start and end + /// evaluation point step size is taken from the settings object + item::ptr evaluate() const; + + /// @brief evaluates the piecewise function between ustart and uend + /// if ustart and uend are out of range, the range of values evaluated + /// are constrained to start_ and start_+length_ + /// @param ustart starting location + /// @param uend ending location + /// @param nsteps number of steps to evaluate + /// @return taxonomy::loop::ptr + item::ptr evaluate(double ustart, double uend, unsigned nsteps) const; + + /// @brief evaluates the piecewise function at u + /// @param u u is constrained to be between start_ and start_+length + /// @return 4x4 placement matrix + Eigen::Matrix4d evaluate(double u) const; + + private: + item::ptr evaluate(const std::vector& dist) const; + std::tuple*> get_span(double u) const; + double start_ = 0.0; // starting value of the pwf + spans_t spans_; + + mutable double current_span_start_ = 0; + mutable double current_span_end_ = 0; + mutable const std::function* current_span_fn_ = nullptr; + mutable boost::optional length_; + mutable boost::optional> eval_points_; +}; + +} // namespace taxonomy +} // namespace geometry +} // namespace ifcopenshell +#endif PIECEWISE_FUNCTION_IMPL \ No newline at end of file diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index 3eacbe77e3..9b4f5043aa 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -1,6 +1,7 @@ #include "../ifcparse/IfcLogger.h" #include "taxonomy.h" #include "profile_helper.h" +#include "piecewise_function_impl.h" using namespace ifcopenshell::geometry::taxonomy; @@ -462,88 +463,34 @@ ifcopenshell::geometry::taxonomy::solid::ptr ifcopenshell::geometry::create_box( return solid; } -ifcopenshell::geometry::taxonomy::item::ptr ifcopenshell::geometry::taxonomy::piecewise_function::evaluate() const { - return evaluate2().first; +/////////////////// +piecewise_function::piecewise_function(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) { + impl_ = new piecewise_function_impl(start, s, settings); } -std::pair> ifcopenshell::geometry::taxonomy::piecewise_function::evaluate2() const { - double curve_length = length(); +piecewise_function::piecewise_function(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) { + impl_ = new piecewise_function_impl(start, pwfs, settings); +}; - std::vector polygon; - - auto param_type = settings_ ? settings_->get().get() : ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE; - auto param = settings_ ? settings_->get().get() : 0.5; - unsigned num_steps = 0; - if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) { - // parameter is max step size - num_steps = (unsigned)std::ceil(curve_length / param); - } else { - // parameter is minimum number of steps - num_steps = (unsigned)std::ceil(param); - } - - num_steps = std::max(1u, num_steps); // never have fewer than 1 step - - return evaluate2(0.0, curve_length, num_steps); +piecewise_function::piecewise_function(const piecewise_function& other) : implicit_item(other) { + impl_ = other.impl_->clone_(); } -item::ptr ifcopenshell::geometry::taxonomy::piecewise_function::evaluate(double ustart, double uend,unsigned nsteps) const { - return evaluate2(ustart, uend, nsteps).first; +piecewise_function::~piecewise_function() { + delete impl_; } -std::pair> ifcopenshell::geometry::taxonomy::piecewise_function::evaluate2(double ustart, double uend, unsigned nsteps) const { - double curve_length = length(); - ustart = std::max(0.0, ustart); - uend = std::min(uend, curve_length); +const piecewise_function::spans_t& piecewise_function::spans() const { return impl_->spans(); } +bool piecewise_function::is_empty() const { return impl_->is_empty(); } +double piecewise_function::start() const { return impl_->start(); } +double piecewise_function::end() const { return impl_->end(); } +double piecewise_function::length() const { return impl_->length(); } - auto resolution = (uend - ustart) / nsteps; - - std::vector polygon; - std::vector u_values; - polygon.reserve(nsteps); - u_values.reserve(nsteps); - - for (unsigned i = 0; i <= nsteps; ++i) { - auto u = resolution * i + ustart; - u_values.push_back(u); - Eigen::Matrix4d m = evaluate(u); - polygon.push_back(taxonomy::make(m.col(3)(0), m.col(3)(1), m.col(3)(2))); - } - - return {polygon_from_points(polygon), u_values}; -} - -Eigen::Matrix4d ifcopenshell::geometry::taxonomy::piecewise_function::evaluate(double u) const { - // assume monotonic evaluation and store last evaluated segment - if (current_span_fn_ == nullptr || (u < current_span_start_ || current_span_end_ < u)) { - // there isn't a current span or u is outside the range of the current span - // get a new "current span" - std::tie(current_span_start_,current_span_end_, current_span_fn_) = get_span(u); - } - - u -= current_span_start_; // make u relative to start of span - return (*current_span_fn_)(u); -} - -std::tuple*> ifcopenshell::geometry::taxonomy::piecewise_function::get_span(double u) const { - // force u to be within bounds of the curve - double curve_length = length(); - u = std::max(0.0, u); - u = std::min(u, curve_length); - - double start = 0; - for (auto& [length, fn] : spans_) { - auto tolerance = settings_ ? settings_->get().get() : 0.001; - if (u < length + tolerance) { - return {start, start+length, &fn} ; - } - start += length; - u -= length; - } - - Logger::Error("taxonomy::piecewise_function::get_span span not found."); - return {0, 0, nullptr}; -} +std::vector piecewise_function::evaluation_points() const { return impl_->evaluation_points(); } +std::vector piecewise_function::evaluation_points(double ustart, double uend, unsigned nsteps) const { return impl_->evaluation_points(ustart, uend, nsteps); } +item::ptr piecewise_function::evaluate() const { return impl_->evaluate(); } +item::ptr piecewise_function::evaluate(double ustart, double uend, unsigned nsteps) const { return impl_->evaluate(ustart, uend, nsteps); } +Eigen::Matrix4d piecewise_function::evaluate(double u) const { return impl_->evaluate(u); } ifcopenshell::geometry::taxonomy::collection::ptr ifcopenshell::geometry::flatten(const taxonomy::collection::ptr& deep) { auto flat = make(); diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index 4b227fd7ef..3b77273240 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -351,71 +351,66 @@ typedef item const* ptr; virtual item::ptr evaluate() const = 0; }; + struct piecewise_function_impl; // forward declaration struct piecewise_function : public implicit_item { DECLARE_PTR(piecewise_function) - using spans = std::vector>>; + using spans_t = std::vector>>; - piecewise_function(const spans& s, ifcopenshell::geometry::Settings* settings = nullptr, const IfcUtil::IfcBaseInterface* instance = nullptr) : - implicit_item(instance), settings_(settings), spans_(s){}; - piecewise_function(const std::vector& pwfs, ifcopenshell::geometry::Settings* settings = nullptr, const IfcUtil::IfcBaseInterface* instance = nullptr) : - implicit_item(instance), settings_(settings) - { - for (auto& pwf : pwfs) { - spans_.insert(spans_.end(), pwf->spans_.begin(), pwf->spans_.end()); - } - }; + piecewise_function(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings = nullptr, const IfcUtil::IfcBaseInterface* instance = nullptr); + piecewise_function(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings = nullptr, const IfcUtil::IfcBaseInterface* instance = nullptr); piecewise_function(piecewise_function&&) = default; - piecewise_function(const piecewise_function&) = default; + piecewise_function(const piecewise_function&); + virtual ~piecewise_function(); const ifcopenshell::geometry::Settings* settings_ = nullptr; - bool is_empty() const { return spans_.empty(); } - - double length() const { - if (!length_.has_value()) { - length_ = std::accumulate(spans_.begin(), spans_.end(), 0.0, [](const auto& v, const auto& s) { return v + s.first; }); - } - return *length_; - } + const spans_t& spans() const; + bool is_empty() const; + double start() const; + double end() const; + double length() const; virtual piecewise_function* clone_() const { return new piecewise_function(*this); } virtual kinds kind() const { return PIECEWISE_FUNCTION; } - virtual size_t calc_hash() const { + virtual size_t calc_hash() const { auto v = std::make_tuple(static_cast(PIECEWISE_FUNCTION), 0); return boost::hash{}(v); } - item::ptr evaluate() const override; - std::pair> evaluate2() const; + /// @brief returns a vector of "distance along" points where the evaluate function computes loop points + std::vector evaluation_points() const; + + /// @brief returns a vector of "distance along" points between ustart and uend + /// @param ustart starting location + /// @param uend ending location + /// @param nsteps number of steps to evaluate + std::vector evaluation_points(double ustart, double uend, unsigned nsteps) const; + + /// @brief evaluates the piecewise function between start and end + /// evaluation point step size is taken from the settings object + item::ptr evaluate() const override; /// @brief evaluates the piecewise function between ustart and uend - /// @param ustart starting location - taken as 0.0 if before start - /// @param uend ending location - taken as length if beyond end + /// if ustart and uend are out of range, the range of values evaluated + /// are constrained to start_ and start_+length_ + /// @param ustart starting location + /// @param uend ending location /// @param nsteps number of steps to evaluate /// @return taxonomy::loop::ptr item::ptr evaluate(double ustart, double uend, unsigned nsteps) const; - /// @brief evaluates the piecewise function between ustart and uend - /// @param ustart starting location - taken as 0.0 if before start - /// @param uend ending location - taken as length if beyond end - /// @param nsteps number of steps to evaluate - /// @return taxonomy::loop::ptr and vector of u values - std::pair> evaluate2(double ustart, double uend, unsigned nsteps) const; - /// @brief evaluates the piecewise function at u - /// @param u u is constrained to be between 0 and length + /// @param u u is constrained to be between start_ and start_+length /// @return 4x4 placement matrix Eigen::Matrix4d evaluate(double u) const; private: - std::tuple*> get_span(double u) const; - spans spans_; - mutable double current_span_start_ = 0; - mutable double current_span_end_ = 0; - mutable const std::function* current_span_fn_ = nullptr; - mutable boost::optional length_; + // note: it would be better if this were a std::unique_ptr, but that requires having the full definition + // of piecewise_function_impl in this header file, which defeats the purpose of the PIMPL idiom. + // if this is a std::unique_ptr, then the _ifcopenshell_wrapper library doesn't compile + piecewise_function_impl* impl_ = nullptr; }; #ifdef TAXONOMY_USE_SHARED_PTR @@ -1313,14 +1308,14 @@ typedef item const* ptr; boost::optional pwf_; public: - loop_to_piecewise_function_upgrade(taxonomy::ptr item) { + loop_to_piecewise_function_upgrade(taxonomy::ptr item) { if constexpr (std::is_same_v) { auto loop = taxonomy::dcast(item); if (loop) { if (loop->pwf.is_initialized()) { pwf_ = loop->pwf; } else { - taxonomy::piecewise_function::spans spans; + taxonomy::piecewise_function::spans_t spans; spans.reserve(loop->children.size()); for (auto& edge : loop->children) { // the edge could be an arc or trimmed circle in the case of IfcIndexPolyCurve - support for this isn't implemented yet @@ -1341,7 +1336,7 @@ typedef item const* ptr; }; spans.emplace_back(l, fn); } - pwf_ = taxonomy::make(spans); + pwf_ = taxonomy::make(0.0,spans); loop->pwf = pwf_; } } diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 4e125af5b5..c76a6fddb9 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -52,7 +52,8 @@ ifeq ($(PLATFORM), win64) PLATFORMTAG:=win_amd64 endif -IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.9-c18e4ea-$(PLATFORM).zip +IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.11-d51fa2c-$(PLATFORM).zip +IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-d51fa2c-$(PLATFORM).zip .PHONY: test test: @@ -89,6 +90,15 @@ coverage: clean: rm -rf htmlcov +.PHONY: zip-ifcconvert +zip-ifcconvert: +ifndef PLATFORM + $(error PLATFORM is not set) +endif + mkdir -p dist + cd dist && wget $(IFCCONVERT_URL) + cd dist && mv *.zip ifcconvert-$(VERSION)-$(PLATFORM).zip + .PHONY: zip zip: ifndef PLATFORM @@ -138,4 +148,4 @@ ifeq ($(IS_STABLE), TRUE) else cp build/dist/ifcopenshell-*.whl dist/ifcopenshell-$(VERSION)a$(VERSION_DATE)-$(PYVERSION)-none-$(PLATFORMTAG).whl endif - # rm -rf build + rm -rf build diff --git a/src/ifcopenshell-python/docs/conf.py b/src/ifcopenshell-python/docs/conf.py index 7a31cbb0e2..6dbb9d6b07 100644 --- a/src/ifcopenshell-python/docs/conf.py +++ b/src/ifcopenshell-python/docs/conf.py @@ -48,14 +48,21 @@ with open(os.path.join(cwd, "..", "..", "..", "VERSION"), "r") as f: from docutils import nodes -def versioned_link_role(name, rawtext, text, lineno, inliner, options={}, content=[]): +def ios_python_url(name, rawtext, text, lineno, inliner, options={}, content=[]): url = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/ifcopenshell-python-{release}/ifcopenshell-python-{release}-{text}.zip" node = nodes.reference(rawtext, text, refuri=url, **options) return [node], [] +def ifcconvert_url(name, rawtext, text, lineno, inliner, options={}, content=[]): + url = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/ifcconvert-{release}/ifcconvert-{release}-{text}.zip" + node = nodes.reference(rawtext, text, refuri=url, **options) + return [node], [] + + def setup(app): - app.add_role("ios_python_url", versioned_link_role) + app.add_role("ios_python_url", ios_python_url) + app.add_role("ifcconvert_url", ifcconvert_url) # -- General configuration --------------------------------------------------- @@ -81,13 +88,21 @@ autoapi_add_toctree_entry = True autoapi_type = "python" # autoapi works by reading source code instead of importing modules -autoapi_dirs = ['../ifcopenshell', '../../bcf/bcf', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester'] +autoapi_dirs = [ + "../ifcopenshell", + "../../bcf/bcf", + "../../bsdd", + "../../ifccsv", + "../../ifcdiff", + "../../ifcpatch/ifcpatch", + "../../ifctester/ifctester", +] # autoapi_dirs = ['../../ifcdiff'] # autoapi_dirs = ['../../ifcdiff', '../ifcopenshell/util'] # autoapi_dirs = ['../../bcf/bcf', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester'] # These are auto-generated based on the IFC schema, so exclude them -autoapi_ignore = ['*ifcopenshell/express/rules*'] +autoapi_ignore = ["*ifcopenshell/express/rules*"] # Custom autoapi templates to make it easier to read our docs autoapi_template_dir = "_autoapi_templates" diff --git a/src/ifcopenshell-python/docs/ifcconvert/installation.rst b/src/ifcopenshell-python/docs/ifcconvert/installation.rst index 963722856d..cd38d14231 100644 --- a/src/ifcopenshell-python/docs/ifcconvert/installation.rst +++ b/src/ifcopenshell-python/docs/ifcconvert/installation.rst @@ -14,17 +14,11 @@ Pre-built packages 1. Download the appropriate version for your operating system. - +----------------+----------------+----------------+----------------+------------------+ - | Linux 64bit | Windows 32bit | Windows 64bit | MacOS 64bit | MacOS M1 64bit | - +================+================+================+================+==================+ - | build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ | - +----------------+----------------+----------------+----------------+------------------+ - -.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.0-c18e4ea-linux64.zip -.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.0-c18e4ea-win32.zip -.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.0-c18e4ea-win64.zip -.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.0-c18e4ea-macos64.zip -.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.0-c18e4ea-macosm164.zip + +---------------------------+-------------------------+-------------------------+---------------------------+-----------------------------+ + | Linux 64bit | Windows 32bit | Windows 64bit | MacOS 64bit | MacOS M1 64bit | + +===========================+=========================+=========================+===========================+=============================+ + | :ifcconvert_url:`linux64` | :ifcconvert_url:`win32` | :ifcconvert_url:`win64` | :ifcconvert_url:`macos64` | :ifcconvert_url:`macosm164` | + +---------------------------+-------------------------+-------------------------+---------------------------+-----------------------------+ 2. Unzip the downloaded file and run IfcConvert using the command line. diff --git a/src/ifcopenshell-python/docs/ifcopenshell.rst b/src/ifcopenshell-python/docs/ifcopenshell.rst index e5bdd50069..d85eb74da7 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell.rst @@ -21,6 +21,7 @@ possible at compile-time when using C++ and at run-time when using Python. :caption: Contents: ifcopenshell/installation + ifcopenshell/getting_started ifcopenshell/geometry_iterator ifcopenshell/geometry_settings ifcopenshell/boolean_process diff --git a/src/ifcopenshell-python/docs/ifcopenshell/getting_started.rst b/src/ifcopenshell-python/docs/ifcopenshell/getting_started.rst new file mode 100644 index 0000000000..351dec6a61 --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcopenshell/getting_started.rst @@ -0,0 +1,115 @@ +Getting Started with IFC parsing +================================ + +The basis of all parsing and getting information from the IFC starts with obtaining a ``IfcParse::IfcFile`` object and validating that it is good for use. + +.. code-block:: c++ + + std::string input_file_path = "/path/to/my/model.ifc"; + IfcParse::IfcFile file(input_file_path); + if (!file.good()) { + std::cerr << "Unable to parse .ifc file" << std::endl; + return 1; + } + +Schema-agnostic parsing of IFCs +------------------------------- + +It is advisable to design your programme in a schema-agnostic fashion to be able to process all schema versions of input IFCs. +While different options are presented `here `__, the most comfortable approach for modern C++ developers +would be with the use of templates as shown below. + +.. code-block:: c++ + + // necessary includes + + // For BOOST_PP_SEQ_FOR_EACH and BOOST_PP_STRINGIZE preprocessor macro + #include + // Include all possible schema types that could be parsed + #include "ifcparse/Ifc2x3.h" + #include "ifcparse/Ifc4.h" + #include "ifcparse/Ifc4x3_add2.h" + + #define IFC_SCHEMA_SEQ (4x3_rc2)(4)(2x3) // TODO: Enumerate through all IFC schemas you want to be able to process + #define EXPAND_AND_CONCATENATE(elem) Ifc##elem + #define PROCESS_FOR_SCHEMA(r, data, elem) if (schema_version == BOOST_PP_STRINGIZE(elem)) { parseIfc(file); } else + + template + void parseIfc(IfcParse::IfcFile &file) { + const typename Schema::IfcProduct::list::ptr elements = file.instances_by_type(); + for (typename Schema::IfcProduct::list::it it = elements->begin(); + it != elements->end(); ++it) { + Schema::IfcProduct *ifcProduct = *it; + // TODO: Do something with ifcProduct + } + } + + void process(const std::string &schema_version, IfcParse::IfcFile &file) { + // Syntactic sugar for iterating through all IFC schemas and passing them to main processing method + BOOST_PP_SEQ_FOR_EACH(PROCESS_FOR_SCHEMA, ,IFC_SCHEMA_SEQ) + { // The final else to catch unhandled schema version + throw std::invalid_argument("IFC Schema " + schema_version + " not supported"); + } + } + + int main(int argc, char* argv[]) { + // file of IfcParse::IfcFile previously defined + auto schema_version = file.schema()->name(); + schema_version = schema_version.substr(3); + std::transform(schema_version.begin(), schema_version.end(), schema_version.begin(), [](unsigned char c) { return std::tolower(c); }); + process(schema_version, file); + // TODO: Handle more cases of IFC schema versions here + } + +Reading out attributes of an IfcProduct +--------------------------------------- + +The properties of an ``IfcProduct``, and by extension any derived class, can be read as by calling the method of the same name, e.g. ``GlobalId()``, ``Name()``. +Note that optional properties like name, long name, or description, among others, are wrapped with a ``boost::optional`` (`documentation `__) container object. Except for pointer types such as IfcRoot::OwnerHistory, here the developer is still responsible for performing comparison to `nullptr` before using the value. + +Navigating relationships in IFC +------------------------------- + +While it is natural to look at the IFC format as a representation of a physical building model where physical objects are related to each other in a tree structure (e.g. site > building > storey > slab), +the IFC format allows for the representation of information in a graph-like manner, joining physical objects to meta-information through relationships. +A detailed explanation of relationships in IFC is provided in this `blog post `__. + +The following function shows how property sets can be extracted from a given ``IfcObject``. + +.. code-block:: c++ + + template + void extractPropertySets(const typename Schema::IfcObject &obj, + typename std::vector &property_sets) { + const auto &is_defined_by = obj.IsDefinedBy(); + if (is_defined_by == nullptr) + return; + std::vector rels(is_defined_by->size()); + typename std::vector::iterator it = std::copy_if( + is_defined_by->begin(), is_defined_by->end(), rels.begin(), + [](const typename Schema::IfcRelDefines *x) { + if (x == nullptr) return false; + const typename Schema::IfcRelDefinesByProperties *defines_by_properties = + x->template as(); + if (defines_by_properties == nullptr) return false; + const auto *relating_property_definition = + defines_by_properties->RelatingPropertyDefinition(); + if (relating_property_definition == nullptr) return false; + return relating_property_definition->template as() != + nullptr; + }); + rels.resize(std::distance(rels.begin(), it)); + property_sets.resize(rels.size()); + std::transform(rels.begin(), rels.end(), property_sets.begin(), + [](const typename Schema::IfcRelDefines *x) { + const typename Schema::IfcRelDefinesByProperties *defines_by_properties = x->template as(); + return defines_by_properties->RelatingPropertyDefinition()->template as(); + }); + } + +Defensive programming with IfcOpenshell +--------------------------------------- + +The need for (down-)casting object pointers when accessing various properties in an IFC entity is evident from the previous code sample as the methods and properties usually +return the abstract class of the entity. It is hence important to check for ``nullptr`` when performing such casts. +The existence of certain fields and properties should also be checked. Also note that IfcOpenShell has as of now not been tested explicitly against malicious inputs. Schema validation (the correctness of attribute types and conformance to schema where rules) can currently only be assessed in Python (using `ifcopenshell.validate --rules`). diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py index bae2c32ea8..fcf8799707 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py +++ b/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py @@ -17,14 +17,15 @@ # along with IfcOpenShell. If not, see . import ifcopenshell -from typing import Optional, Literal +import ifcopenshell.util.representation +from typing import Optional def add_context( file: ifcopenshell.file, - context_type: Optional[Literal["Model", "Plan"]] = None, - context_identifier: Optional[str] = None, - target_view: Optional[str] = None, + context_type: Optional[ifcopenshell.util.representation.CONTEXT_TYPE] = None, + context_identifier: Optional[ifcopenshell.util.representation.REPRESENTATION_IDENTIFIER] = None, + target_view: Optional[ifcopenshell.util.representation.TARGET_VIEW] = None, parent: Optional[ifcopenshell.entity_instance] = None, ) -> ifcopenshell.entity_instance: """Adds a new geometric representation context @@ -106,7 +107,6 @@ def add_context( the common target views above or consult the IFC documentation (under the IfcShapeRepresentation page) for more details. Optional for contexts, but mandatory for subcontexts. - :type target_view: str, optional :param parent: the parent context. Must be left as None (the default) for contexts, and only set for subcontexts. Note that there are only contexts and subcontexts, a subcontext cannot have any children. diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py index 444f42a589..9008aa99b2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item.py @@ -74,7 +74,5 @@ def add_cost_item( }, ) elif settings["cost_item"]: - ifcopenshell.api.nest.assign_object( - file, related_objects=[cost_item], relating_object=settings["cost_item"] - ) + ifcopenshell.api.nest.assign_object(file, related_objects=[cost_item], relating_object=settings["cost_item"]) return cost_item diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py index 3e99728f37..87bc9e607d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py @@ -26,6 +26,7 @@ geometry extrusions). from .. import wrap_usecases from .add_axis_representation import add_axis_representation from .add_boolean import add_boolean + try: from .add_door_representation import add_door_representation except ModuleNotFoundError as e: @@ -33,6 +34,7 @@ except ModuleNotFoundError as e: from .add_footprint_representation import add_footprint_representation from .add_mesh_representation import add_mesh_representation from .add_profile_representation import add_profile_representation + try: from .add_railing_representation import add_railing_representation except ModuleNotFoundError as e: @@ -44,6 +46,7 @@ except ModuleNotFoundError as e: print(f"Note: API not available due to missing dependencies: geometry.add_representation - {e}") from .add_slab_representation import add_slab_representation from .add_wall_representation import add_wall_representation + try: from .add_window_representation import add_window_representation except ModuleNotFoundError as e: diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py index d3b12fa261..a06deca887 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py @@ -857,13 +857,19 @@ class Usecase: z = self.convert_si_to_unit(z) return self.file.createIfcCartesianPoint((x, y, z)) - def create_cartesian_point_list_from_vertices(self, vertices: list[bpy.types.MeshVertex], is_2d=False, is_model_coords=True): + def create_cartesian_point_list_from_vertices( + self, vertices: list[bpy.types.MeshVertex], is_2d=False, is_model_coords=True + ): if is_model_coords and self.settings["coordinate_offset"]: if is_2d: xy_offset = Vector((self.settings["coordinate_offset"][0:2])) - return self.file.createIfcCartesianPointList2D([self.convert_si_to_unit(v.co.xy + xy_offset) for v in vertices]) + return self.file.createIfcCartesianPointList2D( + [self.convert_si_to_unit(v.co.xy + xy_offset) for v in vertices] + ) xyz_offset = Vector((self.settings["coordinate_offset"][0:3])) - return self.file.createIfcCartesianPointList3D([self.convert_si_to_unit(v.co.xyz + xyz_offset) for v in vertices]) + return self.file.createIfcCartesianPointList3D( + [self.convert_si_to_unit(v.co.xyz + xyz_offset) for v in vertices] + ) if is_2d: return self.file.createIfcCartesianPointList2D([self.convert_si_to_unit(v.co.xy) for v in vertices]) return self.file.createIfcCartesianPointList3D([self.convert_si_to_unit(v.co) for v in vertices]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index 93163a4982..539aca12d7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -33,7 +33,7 @@ def edit_georeferencing( surveyor, and a third-party digital engineer with expertise in IFC to moderate. For more information, read the BlenderBIM Add-on documentation for Georeferencing: - https://docs.blenderbim.org/users/georeferencing.html + https://docs.blenderbim.org/users/advanced/georeferencing.html For more information about the attributes and data types of an IfcCoordinateOperation, consult the IFC documentation. diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_true_north.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_true_north.py index acffecb063..d4da8ecc3c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_true_north.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_true_north.py @@ -57,7 +57,7 @@ def edit_true_north(file: ifcopenshell.file, true_north: Optional[Union[tuple[fl for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.TrueNorth and true_north is None: - old_true_north = context.TrueNorth + old_true_north = context.TrueNorth context.TrueNorth = None if not file.get_total_inverses(old_true_north): ifcopenshell.util.element.remove_deep2(file, old_true_north) diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/grid/__init__.py index 3b01281460..5cd7199e63 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/__init__.py @@ -22,6 +22,7 @@ A grid in IFC may contain two or more axes running in two or more directions. """ from .. import wrap_usecases + try: from .create_axis_curve import create_axis_curve except ModuleNotFoundError as e: diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py index c18b54fbad..d938c75380 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py @@ -20,7 +20,10 @@ from typing import Optional def add_material( - file: ifcopenshell.file, name: Optional[str] = None, category: Optional[str] = None, description: Optional[str] = None + file: ifcopenshell.file, + name: Optional[str] = None, + category: Optional[str] = None, + description: Optional[str] = None, ) -> ifcopenshell.entity_instance: """Adds a new material @@ -50,8 +53,8 @@ def add_material( Note that categories are not available in IFC2X3. This shortcoming is one of the big reasons projects should upgrade to IFC4. - Additionally, a material's description provides more information beyond - its name or category. + Additionally, a material's description provides more information beyond + its name or category. :param name: The name of the material, typically tagged in a finishes drawing or schedule. @@ -78,7 +81,7 @@ def add_material( # "Style" has been specified. ifcopenshell.api.material.assign_material(model, products=[concrete_bench], material=concrete) """ - settings = {"name": name or "Unnamed", "category": category, "description": description } + settings = {"name": name or "Unnamed", "category": category, "description": description} material = file.create_entity("IfcMaterial", **{"Name": settings["name"] or "Unnamed"}) if settings["category"]: diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py index a9c9caf9f9..7f4259143a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_material.py @@ -74,6 +74,9 @@ def assign_material( :param material: The IfcMaterial or material set you are assigning here. If type is Usage then no need to provide `material`, it will be deduced from the element type automatically. + If IfcMaterial is provided as material and type is not IfcMaterial, + provided material will be ignored except for IfcMaterialList + where it will be used as part of the list. :type material: ifcopenshell.entity_instance, optional :return: IfcRelAssociatesMaterial entity or a list of IfcRelAssociatesMaterial entities diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py index 4d478d421a..7719fd86c8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -97,6 +97,7 @@ def assign_profile( class Usecase: file: ifcopenshell.file + def execute(self) -> None: # TODO: handle composite profiles old_profile = self.settings["material_profile"].Profile diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py index bd4ac85b7d..939e995a06 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py @@ -19,7 +19,9 @@ import ifcopenshell from typing import Any -def edit_assigned_material(file: ifcopenshell.file, element: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: +def edit_assigned_material( + file: ifcopenshell.file, element: ifcopenshell.entity_instance, attributes: dict[str, Any] +) -> None: """Edits the attributes of an IfcMaterial For more information about the attributes and data types of an diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py index f627a07146..7105e5e78d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py @@ -18,7 +18,9 @@ import ifcopenshell -def add_role(file: ifcopenshell.file, assigned_object: ifcopenshell.entity_instance, role: str = "ARCHITECT") -> ifcopenshell.entity_instance: +def add_role( + file: ifcopenshell.file, assigned_object: ifcopenshell.entity_instance, role: str = "ARCHITECT" +) -> ifcopenshell.entity_instance: """Adds and assigns a new role People and organisations must play one or more roles on a project. Roles diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py index 5e92a31e04..9cfbfff48d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py @@ -19,7 +19,9 @@ import ifcopenshell from typing import Any -def edit_organisation(file: ifcopenshell.file, organisation: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: +def edit_organisation( + file: ifcopenshell.file, organisation: ifcopenshell.entity_instance, attributes: dict[str, Any] +) -> None: """Edits the attributes of an IfcOrganization For more information about the attributes and data types of an diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py index 1d4f650029..776c611e78 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py @@ -95,7 +95,7 @@ def add_pset(file: ifcopenshell.file, product: ifcopenshell.entity_instance, nam "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), "Name": settings["name"], - } + }, ) file.create_entity( "IfcRelDefinesByProperties", @@ -104,7 +104,7 @@ def add_pset(file: ifcopenshell.file, product: ifcopenshell.entity_instance, nam "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), "RelatedObjects": [settings["product"]], "RelatingPropertyDefinition": pset, - } + }, ) return pset elif settings["product"].is_a("IfcTypeObject"): @@ -118,7 +118,7 @@ def add_pset(file: ifcopenshell.file, product: ifcopenshell.entity_instance, nam "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), "Name": settings["name"], - } + }, ) has_property_sets = list(settings["product"].HasPropertySets or []) has_property_sets.append(pset) @@ -142,7 +142,7 @@ def add_pset(file: ifcopenshell.file, product: ifcopenshell.entity_instance, nam **{ "Name": settings["name"], "Material": settings["product"], - } + }, ) elif settings["product"].is_a("IfcProfileDef"): # in IFC2X3 IfcProfileProperties doesn't have Name and we cannot identify them diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py index e5429b948c..1984506c30 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py @@ -50,7 +50,9 @@ def edit_prop_template( if enum_values := attributes.get("Enumerators", None): prop_name = attributes.get("Name", None) or getattr(prop_template, "Name", None) or "Unnamed" primary_measure_type = ( - attributes.get("PrimaryMeasureType", None) or getattr(prop_template, "PrimaryMeasureType", None) or "IfcLabel" + attributes.get("PrimaryMeasureType", None) + or getattr(prop_template, "PrimaryMeasureType", None) + or "IfcLabel" ) enum_values = [file.create_entity(primary_measure_type, v) for v in enum_values] if enumerators := prop_template.Enumerators: diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_load_case.py b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_load_case.py index d8823a11f1..56022446ec 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_load_case.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_load_case.py @@ -38,10 +38,7 @@ def add_structural_load_case( """ load_case = ifcopenshell.api.root.create_entity( - file, - ifc_class="IfcStructuralLoadCase", - predefined_type="LOAD_CASE", - name=name + file, ifc_class="IfcStructuralLoadCase", predefined_type="LOAD_CASE", name=name ) load_case.ActionType = action_type load_case.ActionSource = action_source diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py index 3c53a96cc1..1e826fc655 100644 --- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py +++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py @@ -83,11 +83,15 @@ def register_schema_attributes(schema: ifcopenshell_wrapper.schema_definition) - # resolve to actual functions in wrapper functions = [ - set_derived_attribute - if mname == "setArgumentAsDerived" - else set_unsupported_attribute - if mname == "setArgumentAsUnknown" - else getattr(ifcopenshell_wrapper.entity_instance, mname) + ( + set_derived_attribute + if mname == "setArgumentAsDerived" + else ( + set_unsupported_attribute + if mname == "setArgumentAsUnknown" + else getattr(ifcopenshell_wrapper.entity_instance, mname) + ) + ) for mname in fn_names ] @@ -203,15 +207,20 @@ class entity_instance: rules = importlib.import_module(f"ifcopenshell.express.rules.{schema_name}") except: import os - current_dir_files = {fn.lower(): fn for fn in os.listdir('.')} - exp_filename = schema_name.lower() + '.exp' + + current_dir_files = {fn.lower(): fn for fn in os.listdir(".")} + exp_filename = schema_name.lower() + ".exp" schema_path = current_dir_files.get(exp_filename) if schema_path is None: - raise Exception(f"Couldn't find express file '{schema_name.lower()}.exp' in the current folder: '{os.getcwd()}'.") - fn = schema_path[:-4] + '.py' + raise Exception( + f"Couldn't find express file '{schema_name.lower()}.exp' in the current folder: '{os.getcwd()}'." + ) + fn = schema_path[:-4] + ".py" if not os.path.exists(fn): - subprocess.run([sys.executable, "-m", "ifcopenshell.express.rule_compiler", schema_path, fn], check=True) - time.sleep(1.) + subprocess.run( + [sys.executable, "-m", "ifcopenshell.express.rule_compiler", schema_path, fn], check=True + ) + time.sleep(1.0) rules = importlib.import_module(schema_name) def yield_supertypes(): @@ -255,7 +264,7 @@ class entity_instance: # Define condition and transformation functions condition = lambda v: v == old transform = lambda v: new - + # Usage example attribute_value = element.RelatedElements print(old in attribute_value, new in attribute_value) # True, False diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index a7cc478011..953693ba44 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -24,7 +24,7 @@ import zipfile import functools import ifcopenshell from pathlib import Path -from typing import Optional, Any, Union, Callable, Generator +from typing import Optional, Any, Union, Callable, Generator, Literal from . import ifcopenshell_wrapper from .entity_instance import entity_instance @@ -382,10 +382,10 @@ class file: for (attr_index, _), attr_name in zip(kwargs_attrs, kwargs): if attr_index == 0xFFFFFFFF: invalid_attrs.append(attr_name) - raise ValueError( - "entity instance of type '%s' doesn't have the following attributes: %s." - % (e.is_a(True), ", ".join(invalid_attrs)) - ) + raise ValueError( + "entity instance of type '%s' doesn't have the following attributes: %s." + % (e.is_a(True), ", ".join(invalid_attrs)) + ) # Restore transaction status if attrs: @@ -406,7 +406,7 @@ class file: return e @property - def schema(self) -> str: + def schema(self) -> Literal["IFC2X3", "IFC4", "IFC4X3"]: """General IFC schema version: IFC2X3, IFC4, IFC4X3.""" prefixes = ("IFC", "X", "_ADD", "_TC") reg = "".join(f"(?P<{s}>{s}\d+)?" for s in prefixes) @@ -668,5 +668,5 @@ class file: return file(ifcopenshell_wrapper.read(s)) @staticmethod - def from_pointer(v): + def from_pointer(v) -> "file": return file_dict.get(v)() diff --git a/src/ifcopenshell-python/ifcopenshell/geom/app.py b/src/ifcopenshell-python/ifcopenshell/geom/app.py index 298b5b37ab..6c873597cd 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/app.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/app.py @@ -204,7 +204,6 @@ class application(QtWidgets.QApplication): with two tree views and a graphical 3d view""" class abstract_treeview(QtWidgets.QTreeWidget): - """Base class for the two treeview controls""" instanceSelected = QtCore.pyqtSignal([object]) @@ -255,7 +254,6 @@ class application(QtWidgets.QApplication): ) class decomposition_treeview(abstract_treeview): - """Treeview with typical IFC decomposition relationships""" ATTRIBUTES = ["Entity", "GlobalId", "Name"] @@ -301,7 +299,6 @@ class application(QtWidgets.QApplication): self.expandAll() class type_treeview(abstract_treeview): - """Treeview with typical IFC decomposition relationships""" ATTRIBUTES = ["Name"] diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 29ba26e92b..8fd68a4c62 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -33,6 +33,8 @@ from typing import TypeVar, Union, Optional, Generator, Any, Literal, overload, if TYPE_CHECKING: from OCC.Core import TopoDS + IteratorOutput = Union["ShapeElementType", "utils.shape_tuple"] + T = TypeVar("T") ShapeElementType = Union[ ifcopenshell_wrapper.BRepElement, ifcopenshell_wrapper.TriangulationElement, ifcopenshell_wrapper.SerializedElement @@ -273,7 +275,7 @@ class iterator(ifcopenshell_wrapper.Iterator): def get(self): return wrap_shape_creation(self.settings, ifcopenshell_wrapper.Iterator.get(self)) - def __iter__(self) -> Generator[ShapeElementType, None, None]: + def __iter__(self) -> Generator[IteratorOutput, None, None]: if self.initialize(): while True: yield self.get() @@ -412,7 +414,19 @@ def create_shape( ) -def consume_iterator(it, with_progress=False): +@overload +def consume_iterator(it: iterator, with_progress: Literal[False] = False) -> Generator[IteratorOutput, None, None]: ... +@overload +def consume_iterator( + it: iterator, with_progress: Literal[True] +) -> Generator[tuple[int, IteratorOutput], None, None]: ... +@overload +def consume_iterator( + it: iterator, with_progress: bool +) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ... +def consume_iterator( + it: iterator, with_progress: bool = False +) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: if it.initialize(): while True: if with_progress: @@ -423,16 +437,49 @@ def consume_iterator(it, with_progress=False): break +@overload def iterate( - settings, - file_or_filename, - num_threads=1, - include=None, - exclude=None, - with_progress=False, - cache=None, + settings: settings, + file_or_filename: Union[file, str], + num_threads: int = 1, + include: Optional[Union[list[entity_instance], list[str]]] = None, + exclude: Optional[Union[list[entity_instance], list[str]]] = None, + with_progress: Literal[False] = False, + cache: Optional[serializers.hdf5] = None, geometry_library: GEOMETRY_LIBRARY = "opencascade", -): +) -> Generator[IteratorOutput, None, None]: ... +@overload +def iterate( + settings: settings, + file_or_filename: Union[file, str], + num_threads: int = 1, + include: Optional[Union[list[entity_instance], list[str]]] = None, + exclude: Optional[Union[list[entity_instance], list[str]]] = None, + with_progress: Literal[True] = True, + cache: Optional[serializers.hdf5] = None, + geometry_library: GEOMETRY_LIBRARY = "opencascade", +) -> Generator[tuple[int, IteratorOutput], None, None]: ... +@overload +def iterate( + settings: settings, + file_or_filename: Union[file, str], + num_threads: int = 1, + include: Optional[Union[list[entity_instance], list[str]]] = None, + exclude: Optional[Union[list[entity_instance], list[str]]] = None, + with_progress: bool = False, + cache: Optional[serializers.hdf5] = None, + geometry_library: GEOMETRY_LIBRARY = "opencascade", +) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ... +def iterate( + settings: settings, + file_or_filename: Union[file, str], + num_threads: int = 1, + include: Optional[Union[list[entity_instance], list[str]]] = None, + exclude: Optional[Union[list[entity_instance], list[str]]] = None, + with_progress: bool = False, + cache: Optional[serializers.hdf5] = None, + geometry_library: GEOMETRY_LIBRARY = "opencascade", +) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: it = iterator(settings, file_or_filename, num_threads, include, exclude, geometry_library) if cache: hdf5_cache = serializers.hdf5(cache, settings) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py b/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py index b5e8b1ccae..f2c7dd8f11 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py @@ -40,6 +40,8 @@ except ImportError: class shape_tuple(NamedTuple): + """A tuple containing IfcOpenShell serialized element/shape and pythonOCC shape.""" + data: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization] geometry: TopoDS.TopoDS_Shape styles: tuple[tuple[float, float, float, float], ...] diff --git a/src/ifcopenshell-python/ifcopenshell/guid.py b/src/ifcopenshell-python/ifcopenshell/guid.py index eb11f31ba1..d31f744166 100644 --- a/src/ifcopenshell-python/ifcopenshell/guid.py +++ b/src/ifcopenshell-python/ifcopenshell/guid.py @@ -37,7 +37,7 @@ def compress(g): bs = [int(g[i : i + 2], 16) for i in range(0, len(g), 2)] def b64(v, l=4): - return "".join([chars[(v // (64 ** i)) % 64] for i in range(l)][::-1]) + return "".join([chars[(v // (64**i)) % 64] for i in range(l)][::-1]) return "".join([b64(bs[0], 2)] + [b64((bs[i] << 16) + (bs[i + 1] << 8) + bs[i + 2]) for i in range(1, 16, 3)]) diff --git a/src/ifcopenshell-python/ifcopenshell/settings.py b/src/ifcopenshell-python/ifcopenshell/settings.py index 7d4ae8bbd3..1364f59872 100644 --- a/src/ifcopenshell-python/ifcopenshell/settings.py +++ b/src/ifcopenshell-python/ifcopenshell/settings.py @@ -33,4 +33,4 @@ element or None. Example: #2=IfcRelAssignsToGroup($,$,$,$,$,$,#1) """ -unpack_non_aggregate_inverses = False \ No newline at end of file +unpack_non_aggregate_inverses = False diff --git a/src/ifcopenshell-python/ifcopenshell/stream.py b/src/ifcopenshell-python/ifcopenshell/stream.py index f221a6ffd4..01c5ce1ca9 100644 --- a/src/ifcopenshell-python/ifcopenshell/stream.py +++ b/src/ifcopenshell-python/ifcopenshell/stream.py @@ -11,19 +11,19 @@ try: class StreamTransformer(Transformer): def string(self, items): return str(items[0])[1:-1] - + def float(self, items): return float(items[0]) - + def ifcint(self, items): return int(items[0]) - + def null(self, items): return None - + def derived(self, items): return None - + def enum(self, items): if items[0] == ".T.": return True @@ -32,7 +32,7 @@ try: elif items[0] == ".U.": return "UNKNOWN" return str(items[0])[1:-1] - + def list(self, items): # List is always called twice, I think due to an ambiguity in the Lark # definition between a list and an arg, but I'm not quite sure. @@ -40,7 +40,7 @@ try: if items and isinstance(items[0], dict): return tuple(items[0]["list"]) return {"list": items} - + def inline_type(self, items): # inline_type is also always called twice. Why? if items and isinstance(items[0], dict): @@ -48,20 +48,19 @@ try: entity = ifcopenshell.create_entity(items[0]) entity[0] = items[1] return {"inline_type": entity} - + def reference(self, items): return self.file.by_id(int(items[0][1:])) - + def arg(self, items): return items[0] - + def args(self, items): return items - + def start(self, items): return (int(items[0]), str(items[1]), items[2]) - - + class stream(file): def __init__(self, filepath): self.wrapped_data = None @@ -69,9 +68,9 @@ try: self.history = [] self.future = [] self.transaction = None - + self.filepath = filepath - + self.file = open(filepath, "r") self.id_map = {} self.class_map = {} @@ -80,7 +79,7 @@ try: self.reference_pattern = re.compile(r"#(\d+)") self.entity_cache = {} self.inverses = {} - + # common.INT doesn't support negative integers. grammar = r""" start: "#" NUMBER "=" TYPE "(" args ")" ";" @@ -114,11 +113,11 @@ try: %import common.INT %import common.CNAME """ - + transformer = StreamTransformer() transformer.file = self self.parser = Lark(grammar, parser="lalr", transformer=transformer) - + exclude_classes = [ "IfcObjectPlacement", "IfcPresentationItem", @@ -128,9 +127,9 @@ try: "IfcRepresentationItem", ] exclude_classes = [] - + exclude = set() - + offset = 0 for line in self.file: line = line.strip() @@ -138,14 +137,14 @@ try: step_id, ifc_class = line.split("(")[0].split("=") step_id = int(step_id.strip()[1:]) ifc_class = ifc_class.strip() - + if ifc_class in exclude: offset += len(line) + 1 # +1 for the newline character continue - + for reference_id in self.reference_pattern.findall(line[1:]): self.inverses.setdefault(int(reference_id), []).append(step_id) - + self.id_map[step_id] = ifc_class self.class_map.setdefault(ifc_class, []).append(step_id) self.id_offset[step_id] = offset @@ -156,9 +155,9 @@ try: declaration = self.ifc_schema.declaration_by_name(ifc_class) exclude.update([st.name().upper() for st in ifcopenshell.util.schema.get_subtypes(declaration)]) offset += len(line) + 1 # +1 for the newline character - + self.preprocess_schema() - + def preprocess_schema(self): self.ifc_class_names = {} self.ifc_class_subtypes = {} @@ -166,32 +165,32 @@ try: self.ifc_class_inverse_attributes = {} self.ifc_class_references = {} self.ifc_class_inverses = {} - + for declaration in self.ifc_schema.entities(): self.ifc_class_names[declaration.name().upper()] = declaration.name() - + self.ifc_class_subtypes[declaration.name()] = ifcopenshell.util.schema.get_subtypes(declaration) self.ifc_class_attributes[declaration.name()] = {a.name(): a for a in declaration.all_attributes()} self.ifc_class_inverse_attributes[declaration.name()] = { a.name(): a for a in declaration.all_inverse_attributes() } - + entity = [] entity_list = [] for attribute in declaration.all_attributes(): primitive = ifcopenshell.util.attribute.get_primitive_type(attribute) if primitive == "entity": entity.append(attribute.name()) - + attribute_entity = attribute.type_of_attribute().declared_type() for subtype in ifcopenshell.util.schema.get_subtypes(attribute_entity): self.ifc_class_inverses.setdefault(subtype.name(), {}) self.ifc_class_inverses[subtype.name()].setdefault(declaration.name(), []) self.ifc_class_inverses[subtype.name()][declaration.name()].append(attribute.name()) - + elif self.is_entity_list(attribute): entity_list.append(attribute.name()) - + for entity_name in re.findall("", str(attribute)): attribute_entity = self.ifc_schema.declaration_by_name(entity_name) for subtype in ifcopenshell.util.schema.get_subtypes(attribute_entity): @@ -199,15 +198,15 @@ try: self.ifc_class_inverses.setdefault(subtype.name(), {}) self.ifc_class_inverses[subtype.name()].setdefault(declaration.name(), []) self.ifc_class_inverses[subtype.name()][declaration.name()].append(attribute.name()) - + self.ifc_class_references[declaration.name()] = {"entity": entity, "entity_list": entity_list} - + def clear_cache(self): self.entity_cache = {} - + def create_entity(self, type, *args, **kawrgs): assert False - + def by_id(self, id): entity = self.entity_cache.get(id, None) if entity: @@ -217,35 +216,35 @@ try: entity = stream_entity(id, self.ifc_class_names[ifc_class], self) self.entity_cache[id] = entity return entity - + def by_type(self, type, include_subtypes=True): results = [] subtypes = self.ifc_class_subtypes[type] if include_subtypes else self.ifc_class_subtypes[type][0:1] for subtype in subtypes: results.extend([self.by_id(i) for i in self.class_map.get(subtype.name().upper(), [])]) return results - + def traverse(self, inst, max_levels=None, breadth_first=False): results = [inst] queue = [inst] while queue: if max_levels is not None: max_levels -= 1 - + cur = queue.pop() level_results = set() - + for reference_id in self.reference_pattern.findall(str(cur)[1:]): result = self.by_id(int(reference_id)) results.append(result) if max_levels is None or max_levels: queue.append(result) - + return results - + def get_inverse(self, inst, allow_duplicate=False, with_attribute_indices=False): return {self.by_id(e) for e in self.inverses.get(inst.stream_wrapper.id, [])} - + def is_entity_list(self, attribute): attribute = str(attribute.type_of_attribute()) if (attribute.startswith(" Union[str, None]: md = _original_get_resource_path(resource, abort_on_error) if md and resource == "IfcURIReference": diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 61f72b55b5..f476df5792 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -74,8 +74,7 @@ def get_calendar(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entit calendar = [ rel.RelatingControl for rel in task.HasAssignments or [] - if rel.is_a("IfcRelAssignsToControl") - and rel.RelatingControl.is_a("IfcWorkCalendar") + if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkCalendar") ] if calendar: return calendar[0] @@ -88,11 +87,7 @@ def count_working_days(start, finish, calendar: ifcopenshell.entity_instance) -> current_date = datetime.date(start.year, start.month, start.day) finish_date = datetime.date(finish.year, finish.month, finish.day) while current_date <= finish_date: - if ( - calendar - and calendar.WorkingTimes - and is_working_day(current_date, calendar) - ): + if calendar and calendar.WorkingTimes and is_working_day(current_date, calendar): result += 1 elif not calendar or not is_calendar_applicable(current_date, calendar): result += 1 @@ -132,9 +127,7 @@ def offset_date(start, duration, duration_type: DURATION_TYPE, calendar: ifcopen abs_duration = abs((duration.days + months * 30 + years * 12 * 30)) date_offset = datetime.timedelta(days=1 if duration.days > 0 else -1) while abs_duration > 0: - if duration_type == "ELAPSEDTIME" or not is_calendar_applicable( - current_date, calendar - ): + if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(current_date, calendar): abs_duration -= 1 elif is_working_day(current_date, calendar): abs_duration -= 1 @@ -245,16 +238,13 @@ def is_work_time_applicable_to_day(work_time: ifcopenshell.entity_instance, day) return False # TODO elif recurrence_type == "MONTHLY_BY_POSITION": if not recurrence.Interval and not recurrence.Occurrences: - return (day.weekday() + 1) in recurrence.WeekdayComponent and floor( - day.day / 7 - ) + 1 == recurrence["Position"] + return (day.weekday() + 1) in recurrence.WeekdayComponent and floor(day.day / 7) + 1 == recurrence[ + "Position" + ] return False # TODO elif recurrence_type == "YEARLY_BY_DAY_OF_MONTH": if not recurrence.Interval and not recurrence.Occurrences: - return ( - day.month in recurrence.MonthComponent - and day.day in recurrence.DayComponent - ) + return day.month in recurrence.MonthComponent and day.day in recurrence.DayComponent return False # TODO elif recurrence_type == "YEARLY_BY_POSITION": if not recurrence.Interval and not recurrence.Occurrences: @@ -272,9 +262,7 @@ def get_task_work_schedule(task: ifcopenshell.entity_instance) -> Union[ifcopens return get_task_work_schedule(parent_task) or get_task_work_schedule(task) else: for rel in task.HasAssignments: - if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a( - "IfcWorkSchedule" - ): + if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule"): return rel.RelatingControl return None @@ -304,21 +292,11 @@ def get_work_schedule_tasks(work_schedule: ifcopenshell.entity_instance) -> list def get_root_tasks(work_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: - return [ - obj - for rel in work_schedule.Controls - for obj in rel.RelatedObjects - if obj.is_a("IfcTask") - ] + return [obj for rel in work_schedule.Controls for obj in rel.RelatedObjects if obj.is_a("IfcTask")] def get_root_tasks_ids(work_schedule: ifcopenshell.entity_instance) -> list[int]: - return [ - obj.id() - for rel in work_schedule.Controls - for obj in rel.RelatedObjects - if obj.is_a("IfcTask") - ] + return [obj.id() for rel in work_schedule.Controls for obj in rel.RelatedObjects if obj.is_a("IfcTask")] def guess_date_range(work_schedule: ifcopenshell.entity_instance): @@ -344,22 +322,14 @@ def guess_date_range(work_schedule: ifcopenshell.entity_instance): def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: - return [ - rel.RelatingProduct - for rel in task.HasAssignments - if rel.is_a("IfcRelAssignsToProduct") - ] + return [rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")] def get_task_outputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]: if not is_deep: return get_direct_task_outputs(task) else: - return [ - output - for nested_task in get_all_nested_tasks(task) - for output in get_direct_task_outputs(nested_task) - ] + return [output for nested_task in get_all_nested_tasks(task) for output in get_direct_task_outputs(nested_task)] def get_task_inputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]: @@ -434,8 +404,7 @@ def get_tasks_for_product( inputs = [ assignement.RelatingProcess for assignement in product.HasAssignments - if assignement.is_a("IfcRelAssignsToProcess") - and assignement.RelatingProcess.is_a("IfcTask") + if assignement.is_a("IfcRelAssignsToProcess") and assignement.RelatingProcess.is_a("IfcTask") ] outputs = [ obj @@ -446,16 +415,8 @@ def get_tasks_for_product( ] if schedule: - inputs = [ - task - for task in inputs - if get_task_work_schedule(task).id() == schedule.id() - ] - outputs = [ - task - for task in outputs - if get_task_work_schedule(task).id() == schedule.id() - ] + inputs = [task for task in inputs if get_task_work_schedule(task).id() == schedule.id()] + outputs = [task for task in outputs if get_task_work_schedule(task).id() == schedule.id()] return inputs, outputs diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_reference.py b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py index 3382883af8..6812805594 100644 --- a/src/ifcopenshell-python/test/api/classification/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py @@ -36,9 +36,7 @@ class TestRemoveReference(test.bootstrap.IFC4): name="Foobar", classification=result, ) - ifcopenshell.api.classification.remove_reference( - self.file, products=[element, element2], reference=reference - ) + ifcopenshell.api.classification.remove_reference(self.file, products=[element, element2], reference=reference) assert len(ifcopenshell.util.classification.get_references(element)) == 0 assert len(ifcopenshell.util.classification.get_references(element2)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 @@ -90,9 +88,7 @@ class TestRemoveReference(test.bootstrap.IFC4): classification=result, ) assert len(self.file.by_type("IfcClassificationReference")) == 1 - ifcopenshell.api.classification.remove_reference( - self.file, products=[element, element2], reference=reference - ) + ifcopenshell.api.classification.remove_reference(self.file, products=[element, element2], reference=reference) assert len(self.file.by_type("IfcClassificationReference")) == 1 ifcopenshell.api.classification.remove_reference(self.file, products=[element3], reference=reference) assert len(self.file.by_type("IfcClassificationReference")) == 0 diff --git a/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py index d8a24e48a2..41d594098e 100644 --- a/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py +++ b/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py @@ -27,9 +27,7 @@ class TestAssignConstraint(test.bootstrap.IFC4): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") constraint = ifcopenshell.api.constraint.add_objective(self.file) - ifcopenshell.api.constraint.assign_constraint( - self.file, products=[element, element2], constraint=constraint - ) + ifcopenshell.api.constraint.assign_constraint(self.file, products=[element, element2], constraint=constraint) assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element, element2} assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 1 @@ -37,13 +35,9 @@ class TestAssignConstraint(test.bootstrap.IFC4): constraint = ifcopenshell.api.constraint.add_objective(self.file) element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.constraint.assign_constraint( - self.file, products=[element, element2], constraint=constraint - ) + ifcopenshell.api.constraint.assign_constraint(self.file, products=[element, element2], constraint=constraint) total_elements = len([e for e in self.file]) - ifcopenshell.api.constraint.assign_constraint( - self.file, products=[element, element2], constraint=constraint - ) + ifcopenshell.api.constraint.assign_constraint(self.file, products=[element, element2], constraint=constraint) assert len([e for e in self.file]) == total_elements def test_that_old_relationships_are_updated_if_they_still_contain_elements(self): @@ -54,9 +48,7 @@ class TestAssignConstraint(test.bootstrap.IFC4): element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element3 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.constraint.assign_constraint( - self.file, products=[element2, element3], constraint=constraint - ) + ifcopenshell.api.constraint.assign_constraint(self.file, products=[element2, element3], constraint=constraint) assert len(rel.RelatedObjects) == 3 diff --git a/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py index 0114acfa0b..bf311f2870 100644 --- a/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py +++ b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py @@ -27,12 +27,8 @@ class TestUnassignConstraint(test.bootstrap.IFC4): constraint = ifcopenshell.api.constraint.add_objective(self.file) element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.constraint.assign_constraint( - self.file, products=[element, element2], constraint=constraint - ) - ifcopenshell.api.constraint.unassign_constraint( - self.file, products=[element, element2], constraint=constraint - ) + ifcopenshell.api.constraint.assign_constraint(self.file, products=[element, element2], constraint=constraint) + ifcopenshell.api.constraint.unassign_constraint(self.file, products=[element, element2], constraint=constraint) assert ifcopenshell.util.constraint.get_constrained_elements(element) == set() assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0 @@ -40,9 +36,7 @@ class TestUnassignConstraint(test.bootstrap.IFC4): constraint = ifcopenshell.api.constraint.add_objective(self.file) element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.constraint.unassign_constraint( - self.file, products=[element, element2], constraint=constraint - ) + ifcopenshell.api.constraint.unassign_constraint(self.file, products=[element, element2], constraint=constraint) assert ifcopenshell.util.constraint.get_constrained_elements(element) == set() assert ifcopenshell.util.constraint.get_constrained_elements(element2) == set() @@ -54,12 +48,8 @@ class TestUnassignConstraint(test.bootstrap.IFC4): ifcopenshell.api.constraint.assign_constraint(self.file, products=[element1], constraint=constraint) rel = self.file.by_type("IfcRelAssociatesConstraint")[0] - ifcopenshell.api.constraint.assign_constraint( - self.file, products=[element2, element3], constraint=constraint - ) - ifcopenshell.api.constraint.unassign_constraint( - self.file, products=[element1, element2], constraint=constraint - ) + ifcopenshell.api.constraint.assign_constraint(self.file, products=[element2, element3], constraint=constraint) + ifcopenshell.api.constraint.unassign_constraint(self.file, products=[element1, element2], constraint=constraint) assert rel.RelatedObjects == (element3,) diff --git a/src/ifcopenshell-python/test/api/control/test_assign_control.py b/src/ifcopenshell-python/test/api/control/test_assign_control.py index e607e53ef4..a0afc2c74a 100644 --- a/src/ifcopenshell-python/test/api/control/test_assign_control.py +++ b/src/ifcopenshell-python/test/api/control/test_assign_control.py @@ -27,24 +27,18 @@ class TestAssignControl(test.bootstrap.IFC4): control = ifcopenshell.api.cost.add_cost_schedule(self.file) # simple assignment - relation = ifcopenshell.api.control.assign_control( - self.file, relating_control=control, related_object=wall - ) + relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall) assert len(self.file.by_type("IfcRelAssignsToControl")) == 1 assert relation.RelatingControl == control assert relation.RelatedObjects == (wall,) # trying to establish existing relationship - relation = ifcopenshell.api.control.assign_control( - self.file, relating_control=control, related_object=wall - ) + relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall) assert relation is None # assigning same control to another object wall1 = self.file.createIfcWall() - relation = ifcopenshell.api.control.assign_control( - self.file, relating_control=control, related_object=wall1 - ) + relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall1) assert relation is not None assert len(self.file.by_type("IfcRelAssignsToControl")) == 1 assert relation.RelatingControl == control diff --git a/src/ifcopenshell-python/test/api/control/test_unassign_control.py b/src/ifcopenshell-python/test/api/control/test_unassign_control.py index 2037556a76..76302c06b5 100644 --- a/src/ifcopenshell-python/test/api/control/test_unassign_control.py +++ b/src/ifcopenshell-python/test/api/control/test_unassign_control.py @@ -27,17 +27,13 @@ class TestUnassignControl(test.bootstrap.IFC4): control = ifcopenshell.api.cost.add_cost_schedule(self.file) # assign and unassign - relation = ifcopenshell.api.control.assign_control( - self.file, relating_control=control, related_object=wall - ) + relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall) ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_object=wall) assert len(self.file.by_type("IfcRelAssignsToControl")) == 0 # 1 control 2 related objects wall1 = self.file.createIfcWall() - relation = ifcopenshell.api.control.assign_control( - self.file, relating_control=control, related_object=wall - ) + relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall) ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall1) ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_object=wall1) assert len(self.file.by_type("IfcRelAssignsToControl")) == 1 diff --git a/src/ifcopenshell-python/test/api/cost/test_add_cost_item_quantity.py b/src/ifcopenshell-python/test/api/cost/test_add_cost_item_quantity.py index 7b803a000b..e61a4dc2c5 100644 --- a/src/ifcopenshell-python/test/api/cost/test_add_cost_item_quantity.py +++ b/src/ifcopenshell-python/test/api/cost/test_add_cost_item_quantity.py @@ -33,9 +33,7 @@ class TestAddCostItemQuantity(test.bootstrap.IFC4): quantities = [] for quantity_type in quantity_types: - quantity = ifcopenshell.api.cost.add_cost_item_quantity( - self.file, cost_item=item, ifc_class=quantity_type - ) + quantity = ifcopenshell.api.cost.add_cost_item_quantity(self.file, cost_item=item, ifc_class=quantity_type) assert quantity.is_a(quantity_type) assert quantity.Name == "Unnamed" if quantity_type == "IfcQuantityCount": diff --git a/src/ifcopenshell-python/test/api/document/test_unassign_document.py b/src/ifcopenshell-python/test/api/document/test_unassign_document.py index a8fecd8ad2..a5671a2b89 100644 --- a/src/ifcopenshell-python/test/api/document/test_unassign_document.py +++ b/src/ifcopenshell-python/test/api/document/test_unassign_document.py @@ -36,9 +36,7 @@ class TestUnassignDocument(test.bootstrap.IFC4): element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element3 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") reference = ifcopenshell.api.document.add_reference(self.file, information=None) - ifcopenshell.api.document.assign_document( - self.file, products=[element, element2, element3], document=reference - ) + ifcopenshell.api.document.assign_document(self.file, products=[element, element2, element3], document=reference) ifcopenshell.api.document.unassign_document(self.file, products=[element, element2], document=reference) assert ifcopenshell.util.element.get_referenced_elements(reference) == {element3} diff --git a/src/ifcopenshell-python/test/api/geometry/test_unassign_representation.py b/src/ifcopenshell-python/test/api/geometry/test_unassign_representation.py index d43df03c6f..579cee589b 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_unassign_representation.py +++ b/src/ifcopenshell-python/test/api/geometry/test_unassign_representation.py @@ -30,9 +30,7 @@ class TestUnassignRepresentation(test.bootstrap.IFC4): ) ifcopenshell.api.geometry.unassign_representation(self.file, product=wall, representation=representation) assert representation not in wall.Representation.Representations - ifcopenshell.api.geometry.unassign_representation( - self.file, product=wall, representation=representation2 - ) + ifcopenshell.api.geometry.unassign_representation(self.file, product=wall, representation=representation2) assert not wall.Representation assert len(self.file.by_type("IfcShapeRepresentation")) == 2 assert len(self.file.by_type("IfcProductDefinitionShape")) == 0 @@ -42,9 +40,7 @@ class TestUnassignRepresentation(test.bootstrap.IFC4): origin = self.file.createIfcAxis2Placement3D() repmap = self.file.createIfcRepresentationMap(MappedRepresentation=representation, MappingOrigin=origin) walltype = self.file.createIfcWallType(RepresentationMaps=[repmap]) - ifcopenshell.api.geometry.unassign_representation( - self.file, product=walltype, representation=representation - ) + ifcopenshell.api.geometry.unassign_representation(self.file, product=walltype, representation=representation) assert not walltype.RepresentationMaps assert len(self.file.by_type("IfcAxis2Placement3D")) == 0 assert len(self.file.by_type("IfcRepresentationMap")) == 0 @@ -59,9 +55,7 @@ class TestUnassignRepresentation(test.bootstrap.IFC4): rep = self.file.createIfcShapeRepresentation(Items=[mapped_item]) prodrep = self.file.createIfcProductDefinitionShape(Representations=[rep]) wall = self.file.createIfcWall(Representation=prodrep) - ifcopenshell.api.geometry.unassign_representation( - self.file, product=walltype, representation=representation - ) + ifcopenshell.api.geometry.unassign_representation(self.file, product=walltype, representation=representation) assert not walltype.RepresentationMaps assert len(self.file.by_type("IfcAxis2Placement3D")) == 0 assert len(self.file.by_type("IfcRepresentationMap")) == 0 diff --git a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py index 480ddb6eec..e7e1b9cf63 100644 --- a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py @@ -33,5 +33,6 @@ class TestUnassignReference(test.bootstrap.IFC4): assert ifcopenshell.util.element.get_referenced_elements(reference) == set() assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 + class TestUnassignReferenceIFC2X3(test.bootstrap.IFC2X3, TestUnassignReference): pass diff --git a/src/ifcopenshell-python/test/api/material/test_remove_material.py b/src/ifcopenshell-python/test/api/material/test_remove_material.py index 2498a6dbdd..01f3621a81 100644 --- a/src/ifcopenshell-python/test/api/material/test_remove_material.py +++ b/src/ifcopenshell-python/test/api/material/test_remove_material.py @@ -111,9 +111,7 @@ class TestRemoveMaterialIFC4(test.bootstrap.IFC4, TestRemoveMaterialIFC2X3): def test_removing_material_in_constituent(self): wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") material = ifcopenshell.api.material.add_material(self.file) - material_set = ifcopenshell.api.material.add_material_set( - self.file, set_type="IfcMaterialConstituentSet" - ) + material_set = ifcopenshell.api.material.add_material_set(self.file, set_type="IfcMaterialConstituentSet") ifcopenshell.api.material.add_constituent(self.file, constituent_set=material_set, material=material) ifcopenshell.api.material.assign_material(self.file, products=[wall], material=material_set) assert len(self.file.by_type("IfcMaterialConstituentSet")[0].MaterialConstituents) == 1 diff --git a/src/ifcopenshell-python/test/api/nest/test_assign_object.py b/src/ifcopenshell-python/test/api/nest/test_assign_object.py index 73759a3bc7..a6abfde611 100644 --- a/src/ifcopenshell-python/test/api/nest/test_assign_object.py +++ b/src/ifcopenshell-python/test/api/nest/test_assign_object.py @@ -79,9 +79,7 @@ class TestAssignObject(test.bootstrap.IFC4): # maintain the order in the affected relationships too element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcTask") - ifcopenshell.api.nest.assign_object( - self.file, related_objects=subelements[2:3], relating_object=element2 - ) + ifcopenshell.api.nest.assign_object(self.file, related_objects=subelements[2:3], relating_object=element2) assert rel.RelatedObjects == tuple(subelements[:2] + subelements[3:]) diff --git a/src/ifcopenshell-python/test/api/owner/test_add_person_and_organisation.py b/src/ifcopenshell-python/test/api/owner/test_add_person_and_organisation.py index dd3518b167..ac97b7a317 100644 --- a/src/ifcopenshell-python/test/api/owner/test_add_person_and_organisation.py +++ b/src/ifcopenshell-python/test/api/owner/test_add_person_and_organisation.py @@ -24,9 +24,7 @@ class TestAddPersonAndOrganisation(test.bootstrap.IFC4): def test_adding(self): person = self.file.createIfcPerson() organisation = self.file.createIfcOrganization() - ifcopenshell.api.owner.add_person_and_organisation( - self.file, person=person, organisation=organisation - ) + ifcopenshell.api.owner.add_person_and_organisation(self.file, person=person, organisation=organisation) class TestAddPersonAndOrganisationIFC2X3(test.bootstrap.IFC2X3, TestAddPersonAndOrganisation): diff --git a/src/ifcopenshell-python/test/api/project/test_assign_declaration.py b/src/ifcopenshell-python/test/api/project/test_assign_declaration.py index aa2e48c638..8bbaf521cc 100644 --- a/src/ifcopenshell-python/test/api/project/test_assign_declaration.py +++ b/src/ifcopenshell-python/test/api/project/test_assign_declaration.py @@ -57,9 +57,7 @@ class TestAssignDeclaration(test.bootstrap.IFC4): def test_that_old_relationships_are_updated_if_they_still_contain_elements(self): element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") library = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProjectLibrary") - ifcopenshell.api.project.assign_declaration( - self.file, definitions=[element_type], relating_context=library - ) + ifcopenshell.api.project.assign_declaration(self.file, definitions=[element_type], relating_context=library) rel = self.file.by_type("IfcRelDeclares")[0] element_type2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") diff --git a/src/ifcopenshell-python/test/api/project/test_unassign_declaration.py b/src/ifcopenshell-python/test/api/project/test_unassign_declaration.py index 0b10c23f07..6a1683840b 100644 --- a/src/ifcopenshell-python/test/api/project/test_unassign_declaration.py +++ b/src/ifcopenshell-python/test/api/project/test_unassign_declaration.py @@ -61,9 +61,7 @@ class TestUnassignDeclaration(test.bootstrap.IFC4): element_type1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") element_type2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") element_type3 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") - ifcopenshell.api.project.assign_declaration( - self.file, definitions=[element_type1], relating_context=library - ) + ifcopenshell.api.project.assign_declaration(self.file, definitions=[element_type1], relating_context=library) rel = self.file.by_type("IfcRelDeclares")[0] ifcopenshell.api.project.assign_declaration( diff --git a/src/ifcopenshell-python/test/api/pset/test_edit_pset.py b/src/ifcopenshell-python/test/api/pset/test_edit_pset.py index 5a45ab489a..c7d5bae2e9 100644 --- a/src/ifcopenshell-python/test/api/pset/test_edit_pset.py +++ b/src/ifcopenshell-python/test/api/pset/test_edit_pset.py @@ -183,9 +183,7 @@ class TestEditPset(test.bootstrap.IFC4): assert pset.HasProperties[0].NominalValue.wrappedValue == 34 def test_editing_list_valued_properties(self): - cable = ifcopenshell.api.root.create_entity( - self.file, ifc_class="IfcDistributionPort", predefined_type="CABLE" - ) + cable = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDistributionPort", predefined_type="CABLE") pset = ifcopenshell.api.pset.add_pset(self.file, product=cable, name="Pset_DistributionPortTypeCable") ifcopenshell.api.pset.edit_pset( self.file, diff --git a/src/ifcopenshell-python/test/api/pset_template/test_edit_prop_template.py b/src/ifcopenshell-python/test/api/pset_template/test_edit_prop_template.py index 1ff555f6b4..ac076964c7 100644 --- a/src/ifcopenshell-python/test/api/pset_template/test_edit_prop_template.py +++ b/src/ifcopenshell-python/test/api/pset_template/test_edit_prop_template.py @@ -29,9 +29,7 @@ class TestEditPropTemplate(test.bootstrap.IFC4): prop_template=prop, attributes={"Name": "DemoA", "PrimaryMeasureType": "IfcLabel"}, ) - ifcopenshell.api.pset_template.edit_prop_template( - self.file, prop_template=prop, attributes={"Name": "DemoB"} - ) + ifcopenshell.api.pset_template.edit_prop_template(self.file, prop_template=prop, attributes={"Name": "DemoB"}) assert prop.Name == "DemoB" def test_editing_an_enumeration(self): diff --git a/src/ifcopenshell-python/test/api/root/test_reassign_class.py b/src/ifcopenshell-python/test/api/root/test_reassign_class.py index 87cf46656d..196a60f154 100644 --- a/src/ifcopenshell-python/test/api/root/test_reassign_class.py +++ b/src/ifcopenshell-python/test/api/root/test_reassign_class.py @@ -62,9 +62,7 @@ class TestReassignClass(test.bootstrap.IFC4): element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") ifcopenshell.api.type.assign_type(self.file, related_objects=[element2], relating_type=element_type) - element_type = ifcopenshell.api.root.reassign_class( - self.file, product=element_type, ifc_class="IfcSlabType" - ) + element_type = ifcopenshell.api.root.reassign_class(self.file, product=element_type, ifc_class="IfcSlabType") # type occurrences have reassigned classes occurrences = ifcopenshell.util.element.get_types(element_type) diff --git a/src/ifcopenshell-python/test/api/spatial/test_assign_container.py b/src/ifcopenshell-python/test/api/spatial/test_assign_container.py index e4dc44eef5..4e0ba57666 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_assign_container.py +++ b/src/ifcopenshell-python/test/api/spatial/test_assign_container.py @@ -93,12 +93,8 @@ class TestAssignContainer(test.bootstrap.IFC4): (0.0, 0.0, 0.0, 1.0), ) ) - ifcopenshell.api.geometry.edit_object_placement( - self.file, product=element1, matrix=matrix1.copy(), is_si=False - ) - ifcopenshell.api.geometry.edit_object_placement( - self.file, product=element2, matrix=matrix2.copy(), is_si=False - ) + ifcopenshell.api.geometry.edit_object_placement(self.file, product=element1, matrix=matrix1.copy(), is_si=False) + ifcopenshell.api.geometry.edit_object_placement(self.file, product=element2, matrix=matrix2.copy(), is_si=False) ifcopenshell.api.geometry.edit_object_placement( self.file, product=subelement, matrix=matrix1.copy(), is_si=False ) diff --git a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py index ccec00e697..9e618033e1 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py +++ b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py @@ -51,9 +51,7 @@ class TestDereferenceStructure(test.bootstrap.IFC4): subelement1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") subelement2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") subelement3 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.spatial.reference_structure( - self.file, products=[subelement1], relating_structure=element - ) + ifcopenshell.api.spatial.reference_structure(self.file, products=[subelement1], relating_structure=element) ifcopenshell.api.spatial.reference_structure( self.file, products=[subelement2, subelement3], relating_structure=element ) diff --git a/src/ifcopenshell-python/test/api/style/test_assign_material_style.py b/src/ifcopenshell-python/test/api/style/test_assign_material_style.py index ee18811a32..47f26ee6ac 100644 --- a/src/ifcopenshell-python/test/api/style/test_assign_material_style.py +++ b/src/ifcopenshell-python/test/api/style/test_assign_material_style.py @@ -82,9 +82,7 @@ class TestAssignMaterialStyleIFC4(test.bootstrap.IFC4, TestAssignMaterialStyleIF style = self.file.createIfcSurfaceStyle() material = ifcopenshell.api.material.add_material(self.file) - material_set = ifcopenshell.api.material.add_material_set( - self.file, set_type="IfcMaterialConstituentSet" - ) + material_set = ifcopenshell.api.material.add_material_set(self.file, set_type="IfcMaterialConstituentSet") constituent = ifcopenshell.api.material.add_constituent( self.file, constituent_set=material_set, material=material ) diff --git a/src/ifcopenshell-python/test/api/system/test_assign_port.py b/src/ifcopenshell-python/test/api/system/test_assign_port.py index 5366800cbb..d17f2775d8 100644 --- a/src/ifcopenshell-python/test/api/system/test_assign_port.py +++ b/src/ifcopenshell-python/test/api/system/test_assign_port.py @@ -61,9 +61,7 @@ class TestAssignPort(test.bootstrap.IFC4): (0.0, 0.0, 0.0, 1.0), ) ) - ifcopenshell.api.geometry.edit_object_placement( - self.file, product=element, matrix=matrix.copy(), is_si=False - ) + ifcopenshell.api.geometry.edit_object_placement(self.file, product=element, matrix=matrix.copy(), is_si=False) ifcopenshell.api.geometry.edit_object_placement( self.file, product=subelement, matrix=submatrix.copy(), is_si=False ) diff --git a/src/ifcopenshell-python/test/api/type/test_assign_type.py b/src/ifcopenshell-python/test/api/type/test_assign_type.py index 23fd6b9501..1237b000fe 100644 --- a/src/ifcopenshell-python/test/api/type/test_assign_type.py +++ b/src/ifcopenshell-python/test/api/type/test_assign_type.py @@ -51,9 +51,7 @@ class TestAssignType(test.bootstrap.IFC4): element_type2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.type.assign_type( - self.file, related_objects=[element1, element2], relating_type=element_type1 - ) + ifcopenshell.api.type.assign_type(self.file, related_objects=[element1, element2], relating_type=element_type1) rel = element1.IsDefinedBy[0] if self.file.schema == "IFC2X3" else element1.IsTypedBy[0] assert len(rel.RelatedObjects) == 2 ifcopenshell.api.type.assign_type(self.file, related_objects=[element1], relating_type=element_type2) @@ -110,9 +108,7 @@ class TestAssignType(test.bootstrap.IFC4): mapped_rep_id = mapped_rep.id() element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.type.assign_type( - self.file, related_objects=[element1, element2], relating_type=element_type - ) + ifcopenshell.api.type.assign_type(self.file, related_objects=[element1, element2], relating_type=element_type) assert (mapped_rep := ifcopenshell.util.representation.get_representation(element1, context=context)) assert mapped_rep.id() == mapped_rep_id @@ -139,9 +135,7 @@ class TestAssignType(test.bootstrap.IFC4): # use 2 elements to trigger material assignment code block element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.type.assign_type( - self.file, related_objects=[element1, element2], relating_type=element_type - ) + ifcopenshell.api.type.assign_type(self.file, related_objects=[element1, element2], relating_type=element_type) assert (material := ifcopenshell.util.element.get_material(element1)) assert material.id() == material_id diff --git a/src/ifcopenshell-python/test/api/type/test_unassign_type.py b/src/ifcopenshell-python/test/api/type/test_unassign_type.py index d4f3b0cfa8..c37ae7214d 100644 --- a/src/ifcopenshell-python/test/api/type/test_unassign_type.py +++ b/src/ifcopenshell-python/test/api/type/test_unassign_type.py @@ -29,9 +29,7 @@ class TestUnassignType(test.bootstrap.IFC4): element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - ifcopenshell.api.type.assign_type( - self.file, related_objects=[element1, element2], relating_type=element_type - ) + ifcopenshell.api.type.assign_type(self.file, related_objects=[element1, element2], relating_type=element_type) ifcopenshell.api.type.unassign_type(self.file, related_objects=[element1, element2]) assert ifcopenshell.util.element.get_type(element1) is None assert ifcopenshell.util.element.get_type(element2) is None diff --git a/src/ifcopenshell-python/test/api/void/test_add_opening.py b/src/ifcopenshell-python/test/api/void/test_add_opening.py index c36912d5b9..d1abe2aeea 100644 --- a/src/ifcopenshell-python/test/api/void/test_add_opening.py +++ b/src/ifcopenshell-python/test/api/void/test_add_opening.py @@ -61,12 +61,8 @@ class TestAddOpening(test.bootstrap.IFC4): (0.0, 0.0, 0.0, 1.0), ) ) - ifcopenshell.api.geometry.edit_object_placement( - self.file, product=wall, matrix=matrix1.copy(), is_si=False - ) - ifcopenshell.api.geometry.edit_object_placement( - self.file, product=opening, matrix=matrix1.copy(), is_si=False - ) + ifcopenshell.api.geometry.edit_object_placement(self.file, product=wall, matrix=matrix1.copy(), is_si=False) + ifcopenshell.api.geometry.edit_object_placement(self.file, product=opening, matrix=matrix1.copy(), is_si=False) ifcopenshell.api.void.add_opening(self.file, opening=opening, element=wall) assert opening.ObjectPlacement.PlacementRelTo.PlacesObject[0] == wall assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement), matrix1) diff --git a/src/ifcopenshell-python/test/fixtures/rules/fail-poly-curve-1-2-1-2-ifc4x3_add1.ifc b/src/ifcopenshell-python/test/fixtures/rules/fail-poly-curve-1-2-1-2-ifc4x3_add2.ifc similarity index 92% rename from src/ifcopenshell-python/test/fixtures/rules/fail-poly-curve-1-2-1-2-ifc4x3_add1.ifc rename to src/ifcopenshell-python/test/fixtures/rules/fail-poly-curve-1-2-1-2-ifc4x3_add2.ifc index ce4d05ee1a..12116122fb 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/fail-poly-curve-1-2-1-2-ifc4x3_add1.ifc +++ b/src/ifcopenshell-python/test/fixtures/rules/fail-poly-curve-1-2-1-2-ifc4x3_add2.ifc @@ -2,7 +2,7 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); FILE_NAME('','2023-03-11T14:53:24',(),(),'IfcOpenShell v0.7.0-c4b77c2a0','IfcOpenShell v0.7.0-c4b77c2a0',''); -FILE_SCHEMA(('IFC4X3_ADD1')); +FILE_SCHEMA(('IFC4X3_ADD2')); ENDSEC; DATA; #1=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,1.),(1.,1.)),$); diff --git a/src/ifcopenshell-python/test/fixtures/rules/fail-rigid-op-IfcPlaneAngleMeasure-IfcLengthMeasure-ifc4x3_add1.ifc b/src/ifcopenshell-python/test/fixtures/rules/fail-rigid-op-IfcPlaneAngleMeasure-IfcLengthMeasure-ifc4x3_add2.ifc similarity index 92% rename from src/ifcopenshell-python/test/fixtures/rules/fail-rigid-op-IfcPlaneAngleMeasure-IfcLengthMeasure-ifc4x3_add1.ifc rename to src/ifcopenshell-python/test/fixtures/rules/fail-rigid-op-IfcPlaneAngleMeasure-IfcLengthMeasure-ifc4x3_add2.ifc index 07d4da8efa..7e32acbb8a 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/fail-rigid-op-IfcPlaneAngleMeasure-IfcLengthMeasure-ifc4x3_add1.ifc +++ b/src/ifcopenshell-python/test/fixtures/rules/fail-rigid-op-IfcPlaneAngleMeasure-IfcLengthMeasure-ifc4x3_add2.ifc @@ -2,7 +2,7 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); FILE_NAME('','2023-01-11T09:47:45',(),(),'IfcOpenShell v0.7.0-8a108f37','IfcOpenShell v0.7.0-8a108f37',''); -FILE_SCHEMA(('IFC4X3_ADD1')); +FILE_SCHEMA(('IFC4X3_ADD2')); ENDSEC; DATA; #1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$); diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_00.py b/src/ifcopenshell-python/test/fixtures/rules/generate_00.py index 9938c40635..251f501bec 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_00.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_00.py @@ -1,18 +1,28 @@ import time import ifcopenshell + for i in range(3): - f = ifcopenshell.file(schema="IFC2X3") - p = f.createIfcPerson(Id="tfk", GivenName="Thomas") - o = f.createIfcOrganization(Name="AECgeeks") - pando = f.createIfcPersonAndOrganization(p, o) - appl = f.createIfcApplication(o, ifcopenshell.version, "IfcOpenShell", f"IfcOpenShell {ifcopenshell.version}") - units = f.createIfcUnitAssignment(Units=[f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")]) - ownerhist = f.createIfcOwnerHistory(pando, appl, ChangeAction="ADDED", CreationDate=int(time.time())) - for j in range(i): - f.createIfcProject(ifcopenshell.guid.new(), ownerhist, 'My Project', UnitsInContext=units, RepresentationContexts=[ - f.createIfcGeometricRepresentationContext( - None, None, 3, None, - f.create_entity(f'IfcAxis2Placement3D', f.createIfcCartesianPoint((0., 0., 0.))), - ) - ]) - f.write(f"{'fail' if i == 2 else 'pass'}-{i}-projects-ifc2x3.ifc") + f = ifcopenshell.file(schema="IFC2X3") + p = f.createIfcPerson(Id="tfk", GivenName="Thomas") + o = f.createIfcOrganization(Name="AECgeeks") + pando = f.createIfcPersonAndOrganization(p, o) + appl = f.createIfcApplication(o, ifcopenshell.version, "IfcOpenShell", f"IfcOpenShell {ifcopenshell.version}") + units = f.createIfcUnitAssignment(Units=[f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")]) + ownerhist = f.createIfcOwnerHistory(pando, appl, ChangeAction="ADDED", CreationDate=int(time.time())) + for j in range(i): + f.createIfcProject( + ifcopenshell.guid.new(), + ownerhist, + "My Project", + UnitsInContext=units, + RepresentationContexts=[ + f.createIfcGeometricRepresentationContext( + None, + None, + 3, + None, + f.create_entity(f"IfcAxis2Placement3D", f.createIfcCartesianPoint((0.0, 0.0, 0.0))), + ) + ], + ) + f.write(f"{'fail' if i == 2 else 'pass'}-{i}-projects-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_01.py b/src/ifcopenshell-python/test/fixtures/rules/generate_01.py index 5b2ed5dfc3..e6c92455f7 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_01.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_01.py @@ -1,13 +1,14 @@ import ifcopenshell -for i, box_alignment in enumerate(['top-left', 'center', 'invalid', 'CENTER']): + +for i, box_alignment in enumerate(["top-left", "center", "invalid", "CENTER"]): f = ifcopenshell.file(schema="IFC2X3") f.createIfcTextLiteralWithExtent( "My presentable text", - f.createIfcAxis2Placement2D(f.createIfcCartesianPoint((0., 0.))), - 'RIGHT', - f.createIfcPlanarExtent(10., 10.), - box_alignment + f.createIfcAxis2Placement2D(f.createIfcCartesianPoint((0.0, 0.0))), + "RIGHT", + f.createIfcPlanarExtent(10.0, 10.0), + box_alignment, ) f.write(f"{'pass' if i in (0,1) else 'fail'}-{i}-box-alignment-{box_alignment}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_02.py b/src/ifcopenshell-python/test/fixtures/rules/generate_02.py index 3f6cd94011..298fee7eba 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_02.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_02.py @@ -28,11 +28,20 @@ for i, (is_valid, lat) in enumerate(latitudes): ownerhist = f.createIfcOwnerHistory(pando, appl, ChangeAction="ADDED", CreationDate=int(time.time())) units = f.createIfcUnitAssignment(Units=[f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")]) site = f.createIfcSite(ifcopenshell.guid.new(), ownerhist, RefLatitude=lat) - proj = f.createIfcProject(ifcopenshell.guid.new(), ownerhist, 'My Project', UnitsInContext=units, RepresentationContexts=[ - f.createIfcGeometricRepresentationContext( - None, None, 3, None, - f.create_entity(f'IfcAxis2Placement3D', f.createIfcCartesianPoint((0., 0., 0.))), - ) - ]) + proj = f.createIfcProject( + ifcopenshell.guid.new(), + ownerhist, + "My Project", + UnitsInContext=units, + RepresentationContexts=[ + f.createIfcGeometricRepresentationContext( + None, + None, + 3, + None, + f.create_entity(f"IfcAxis2Placement3D", f.createIfcCartesianPoint((0.0, 0.0, 0.0))), + ) + ], + ) f.createIfcRelAggregates(ifcopenshell.guid.new(), ownerhist, None, None, proj, [site]) f.write(f"{'pass' if is_valid else 'fail'}-site-latitude-{i}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_03.py b/src/ifcopenshell-python/test/fixtures/rules/generate_03.py index 27888b3e31..38f83f85c6 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_03.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_03.py @@ -1,18 +1,13 @@ import ifcopenshell -for depth in (-1., 0., 1.): +for depth in (-1.0, 0.0, 1.0): f = ifcopenshell.file(schema="IFC2X3") f.createIfcExtrudedAreaSolid( f.createIfcRectangleProfileDef( - "AREA", None, - f.createIfcAxis2Placement2D( - f.createIfcCartesianPoint((0., 0.)) - ), 1., 1. + "AREA", None, f.createIfcAxis2Placement2D(f.createIfcCartesianPoint((0.0, 0.0))), 1.0, 1.0 ), - f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)) - ), - f.createIfcDirection((0., 0., 1.)), - depth + f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0.0, 0.0, 0.0))), + f.createIfcDirection((0.0, 0.0, 1.0)), + depth, ) f.write(f"{'pass' if depth > 0. else 'fail'}-extrusion-depth-{depth}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_04.py b/src/ifcopenshell-python/test/fixtures/rules/generate_04.py index 6d44e6a68d..aa62299073 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_04.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_04.py @@ -1,6 +1,8 @@ import ifcopenshell -for i, (r1, r2) in enumerate([("SUPPLIER", None), ("SUPPLIER", "Valid"), ("USERDEFINED", "Valid"), ("USERDEFINED", None)]): +for i, (r1, r2) in enumerate( + [("SUPPLIER", None), ("SUPPLIER", "Valid"), ("USERDEFINED", "Valid"), ("USERDEFINED", None)] +): f = ifcopenshell.file(schema="IFC2X3") f.createIfcActorRole(r1, r2) - f.write(f"{'fail' if i == 3 else 'pass'}-actor-role-{r1}-{r2}-ifc2x3.ifc") \ No newline at end of file + f.write(f"{'fail' if i == 3 else 'pass'}-actor-role-{r1}-{r2}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_05.py b/src/ifcopenshell-python/test/fixtures/rules/generate_05.py index 3982db43ea..b6b6fb9c50 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_05.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_05.py @@ -1,18 +1,22 @@ import ifcopenshell options = { - 'IfcPostalAddress': ({}, {'Country': 'The Netherlands'}, {'Country': 'The Netherlands', 'Town': 'Eindhoven'}), - 'IfcTelecomAddress': ({}, {'TelephoneNumbers': ['040-12345678']}, {'TelephoneNumbers': ['040-12345678'], 'PagerNumber': '12345'}) + "IfcPostalAddress": ({}, {"Country": "The Netherlands"}, {"Country": "The Netherlands", "Town": "Eindhoven"}), + "IfcTelecomAddress": ( + {}, + {"TelephoneNumbers": ["040-12345678"]}, + {"TelephoneNumbers": ["040-12345678"], "PagerNumber": "12345"}, + ), } -for ent in ('IfcPostalAddress', 'IfcTelecomAddress'): - for purpose in (None, 'USERDEFINED', 'HOME'): - for ud in (None, 'SomethingUserdefined'): +for ent in ("IfcPostalAddress", "IfcTelecomAddress"): + for purpose in (None, "USERDEFINED", "HOME"): + for ud in (None, "SomethingUserdefined"): f = ifcopenshell.file(schema="IFC2X3") f.create_entity(ent, purpose, None, ud, **options[ent][1]) - valid = not (purpose == 'USERDEFINED' and ud is None) + valid = not (purpose == "USERDEFINED" and ud is None) f.write(f"{'pass' if valid else 'fail'}-{ent}-{purpose}-{ud}-ifc2x3.ifc") @@ -21,5 +25,5 @@ for ent in ('IfcPostalAddress', 'IfcTelecomAddress'): f.create_entity(ent, **kwargs) valid = len(kwargs) >= 1 if not valid: - kwargs = {'all-unset': 1} + kwargs = {"all-unset": 1} f.write(f"{'pass' if valid else 'fail'}-{ent}-{'-'.join(kwargs.keys())}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_06.py b/src/ifcopenshell-python/test/fixtures/rules/generate_06.py index 9a5d0bc6f6..b36c5108cb 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_06.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_06.py @@ -2,13 +2,13 @@ import itertools import time import ifcopenshell -for pty, ety in itertools.product(('GRILLE', 'USERDEFINED'), (None, 'Something')): +for pty, ety in itertools.product(("GRILLE", "USERDEFINED"), (None, "Something")): f = ifcopenshell.file(schema="IFC2X3") p = f.createIfcPerson(Id="tfk", GivenName="Thomas") o = f.createIfcOrganization(Name="AECgeeks") pando = f.createIfcPersonAndOrganization(p, o) appl = f.createIfcApplication(o, ifcopenshell.version, "IfcOpenShell", f"IfcOpenShell {ifcopenshell.version}") ownerhist = f.createIfcOwnerHistory(pando, appl, ChangeAction="ADDED", CreationDate=int(time.time())) - f.createIfcAirTerminalType(ifcopenshell.guid.new(), ownerhist, 'My Type', ElementType=ety, PredefinedType=pty) - valid = pty != 'USERDEFINED' or ety is not None + f.createIfcAirTerminalType(ifcopenshell.guid.new(), ownerhist, "My Type", ElementType=ety, PredefinedType=pty) + valid = pty != "USERDEFINED" or ety is not None f.write(f"{'pass' if valid else 'fail'}-air-terminal-type-{pty}-{ety}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_07.py b/src/ifcopenshell-python/test/fixtures/rules/generate_07.py index a768700a71..c7fac5d49e 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_07.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_07.py @@ -1,10 +1,16 @@ import ifcopenshell create_none = lambda f: None -create_polyline = lambda f: f.createIfcPolyline((f.createIfcCartesianPoint((0., 0.)), f.createIfcCartesianPoint((1., 0.)))) -create_point = lambda f: f.createIfcCartesianPoint((0., 0.)) +create_polyline = lambda f: f.createIfcPolyline( + (f.createIfcCartesianPoint((0.0, 0.0)), f.createIfcCartesianPoint((1.0, 0.0))) +) +create_point = lambda f: f.createIfcCartesianPoint((0.0, 0.0)) for i, make_item in enumerate((create_none, create_polyline, create_point)): f = ifcopenshell.file(schema="IFC2X3") - inst = f.createIfcAnnotationCurveOccurrence(make_item(f), [f.createIfcPresentationStyleAssignment([f.createIfcCurveStyle()])]) - f.write(f"{'fail' if i == 2 else 'pass'}-annotation-curve-occurence-{'None' if inst.Item is None else inst.Item.is_a()}-ifc2x3.ifc") + inst = f.createIfcAnnotationCurveOccurrence( + make_item(f), [f.createIfcPresentationStyleAssignment([f.createIfcCurveStyle()])] + ) + f.write( + f"{'fail' if i == 2 else 'pass'}-annotation-curve-occurence-{'None' if inst.Item is None else inst.Item.is_a()}-ifc2x3.ifc" + ) diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_08.py b/src/ifcopenshell-python/test/fixtures/rules/generate_08.py index aa3f134fca..c616f75113 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_08.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_08.py @@ -1,7 +1,9 @@ import ifcopenshell -create_plane = lambda f: f.createIfcPlane(f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0., 0., 0.)))) -create_polyline = lambda f: f.createIfcPolyline((f.createIfcCartesianPoint((0., 0.)), f.createIfcCartesianPoint((1., 0.)))) +create_plane = lambda f: f.createIfcPlane(f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0.0, 0.0, 0.0)))) +create_polyline = lambda f: f.createIfcPolyline( + (f.createIfcCartesianPoint((0.0, 0.0)), f.createIfcCartesianPoint((1.0, 0.0))) +) for i, make_item in enumerate((create_plane, create_polyline)): f = ifcopenshell.file(schema="IFC2X3") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_09.py b/src/ifcopenshell-python/test/fixtures/rules/generate_09.py index 23ea530323..5ab9a2fe40 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_09.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_09.py @@ -1,22 +1,16 @@ import ifcopenshell -pts = [(0., 0.), (1., 0.), (1., 1.)] -dims = [ - (2,3,3), - (3,3,2), - (2,2,2), - (3,3,3) -] +pts = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)] +dims = [(2, 3, 3), (3, 3, 2), (2, 2, 2), (3, 3, 3)] + def make_point(xy, dim): - return f.createIfcCartesianPoint((xy + (0.,))[0:dim]) + return f.createIfcCartesianPoint((xy + (0.0,))[0:dim]) + for d in dims: f = ifcopenshell.file(schema="IFC2X3") - inst = f.createIfcBSplineCurve(1, - list(map(lambda t: make_point(*t), zip(pts, d))), - "POLYLINE_FORM", - False, - False + inst = f.createIfcBSplineCurve(1, list(map(lambda t: make_point(*t), zip(pts, d))), "POLYLINE_FORM", False, False) + f.write( + f"{'pass' if len(set(d)) == 1 else 'fail'}-bspline-curve-point-dimensions-{'-'.join(map(str, d))}-ifc2x3.ifc" ) - f.write(f"{'pass' if len(set(d)) == 1 else 'fail'}-bspline-curve-point-dimensions-{'-'.join(map(str, d))}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_10.py b/src/ifcopenshell-python/test/fixtures/rules/generate_10.py index 06b111bd17..674271c812 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_10.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_10.py @@ -1,8 +1,8 @@ import itertools import ifcopenshell -defaults = {'Girth': 1., 'WallThickness': 0.11} -depths = [2., 3.] +defaults = {"Girth": 1.0, "WallThickness": 0.11} +depths = [2.0, 3.0] widths = [0.2, 0.3] for d, w in itertools.product(depths, widths): @@ -12,9 +12,9 @@ for d, w in itertools.product(depths, widths): f = ifcopenshell.file(schema="IFC2X3") - valid = (Girth < (Depth / 2.)) and ((WallThickness < Width/2.) and (WallThickness < Depth/2.)) + valid = (Girth < (Depth / 2.0)) and ((WallThickness < Width / 2.0) and (WallThickness < Depth / 2.0)) inst = f.createIfcCShapeProfileDef( - 'AREA', None, f.createIfcAxis2Placement2D(f.createIfcCartesianPoint((0., 0.))), **D + "AREA", None, f.createIfcAxis2Placement2D(f.createIfcCartesianPoint((0.0, 0.0))), **D ) f.write(f"{'pass' if valid else 'fail'}-cshape-profile-width-{w}-depth-{d}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_11.py b/src/ifcopenshell-python/test/fixtures/rules/generate_11.py index 908a4297e4..b70f71d6e4 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_11.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_11.py @@ -1,34 +1,29 @@ import ifcopenshell depth = 1.0 -for (dir_x, dir_z) in ((0., -1.), (0., 0.), (1., 0.), (1., 0.001), (0., 1.)): +for dir_x, dir_z in ((0.0, -1.0), (0.0, 0.0), (1.0, 0.0), (1.0, 0.001), (0.0, 1.0)): - schemas = ['IFC2X3'] - if (dir_x, dir_z) == (0., 0.): - schemas.append('IFC4') + schemas = ["IFC2X3"] + if (dir_x, dir_z) == (0.0, 0.0): + schemas.append("IFC4") for schema in schemas: f = ifcopenshell.file(schema=schema) f.createIfcExtrudedAreaSolid( f.createIfcRectangleProfileDef( - "AREA", None, - f.createIfcAxis2Placement2D( - f.createIfcCartesianPoint((0., 0.)) - ), 1., 1. + "AREA", None, f.createIfcAxis2Placement2D(f.createIfcCartesianPoint((0.0, 0.0))), 1.0, 1.0 ), - f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)) - ), - f.createIfcDirection((dir_x, 0., dir_z)), - depth + f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0.0, 0.0, 0.0))), + f.createIfcDirection((dir_x, 0.0, dir_z)), + depth, ) # Due to the way IfcDotProduct and IfcNormalise interact, (0 0 0) actually # results into indeterminate. But in IFC4 onwars there is a rule in IfcDirection # for non-zero magnitude - valid = dir_z != 0. - if f.schema == 'IFC2X3' and (dir_x, dir_z) == (0., 0.): + valid = dir_z != 0.0 + if f.schema == "IFC2X3" and (dir_x, dir_z) == (0.0, 0.0): valid = True f.write(f"{'pass' if valid else 'fail'}-extrusion-dir-{dir_x}-{dir_z}-{f.schema.lower()}.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_12.py b/src/ifcopenshell-python/test/fixtures/rules/generate_12.py index 8569bef667..798385d498 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_12.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_12.py @@ -1,9 +1,9 @@ import ifcopenshell -coords = [(0., 0.), (10., 0.), (10., 10.), (0., 10.)] -make_3d = lambda cs: [c + (0.,) for c in cs] -inner_1 = [(1., 1.), (2., 1.), (2., 2.), (1., 2.)] -inner_2 = [(3., 3.), (4., 3.), (4., 4.), (3., 4.)] +coords = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)] +make_3d = lambda cs: [c + (0.0,) for c in cs] +inner_1 = [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0)] +inner_2 = [(3.0, 3.0), (4.0, 3.0), (4.0, 4.0), (3.0, 4.0)] old_map = map map = lambda fn, *args: list(old_map(fn, *args)) @@ -16,7 +16,7 @@ f.createIfcArbitraryProfileDefWithVoids( [ f.createIfcPolyline(map(f.createIfcCartesianPoint, inner_1)), f.createIfcPolyline(map(f.createIfcCartesianPoint, inner_2)), - ] + ], ) f.write(f"pass-arbitrary-profile-with-voids-ifc2x3.ifc") @@ -28,7 +28,7 @@ f.createIfcArbitraryProfileDefWithVoids( [ f.createIfcPolyline(map(f.createIfcCartesianPoint, inner_1)), f.createIfcPolyline(map(f.createIfcCartesianPoint, inner_2)), - ] + ], ) f.write(f"fail-arbitrary-profile-with-voids-curve-ifc2x3.ifc") @@ -40,7 +40,7 @@ f.createIfcArbitraryProfileDefWithVoids( [ f.createIfcPolyline(map(f.createIfcCartesianPoint, inner_1)), f.createIfcPolyline(map(f.createIfcCartesianPoint, inner_2)), - ] + ], ) f.write(f"fail-arbitrary-profile-with-voids-3d-outer-ifc2x3.ifc") @@ -52,7 +52,7 @@ f.createIfcArbitraryProfileDefWithVoids( [ f.createIfcPolyline(map(f.createIfcCartesianPoint, make_3d(inner_1))), f.createIfcPolyline(map(f.createIfcCartesianPoint, inner_2)), - ] + ], ) f.write(f"fail-arbitrary-profile-with-voids-3d-inner-ifc2x3.ifc") @@ -64,7 +64,7 @@ f.createIfcArbitraryProfileDefWithVoids( [ f.createIfcPolyline(map(f.createIfcCartesianPoint, make_3d(inner_1))), f.createIfcPolyline(map(f.createIfcCartesianPoint, make_3d(inner_2))), - ] + ], ) f.write(f"fail-arbitrary-profile-with-voids-3d-inner-2-ifc2x3.ifc") @@ -73,14 +73,6 @@ f.createIfcArbitraryProfileDefWithVoids( "AREA", None, f.createIfcPolyline(map(f.createIfcCartesianPoint, coords)), - [ - f.createIfcLine( - f.createIfcCartesianPoint((0., 0.)), - f.createIfcVector( - f.createIfcDirection((0., 0.)), - 1. - ) - ) - ] + [f.createIfcLine(f.createIfcCartesianPoint((0.0, 0.0)), f.createIfcVector(f.createIfcDirection((0.0, 0.0)), 1.0))], ) -f.write(f"fail-arbitrary-profile-with-voids-inner-line-ifc2x3.ifc") \ No newline at end of file +f.write(f"fail-arbitrary-profile-with-voids-inner-line-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_13.py b/src/ifcopenshell-python/test/fixtures/rules/generate_13.py index ea55e18575..05259e6ba0 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_13.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_13.py @@ -1,77 +1,75 @@ import ifcopenshell -coords = [(0., 0.), (10., 0.), (10., 10.), (0., 10.)] -make_3d = lambda cs: [c + (0.,) for c in cs] -inner_1 = [(1., 1.), (2., 1.), (2., 2.), (1., 2.)] -inner_2 = [(3., 3.), (4., 3.), (4., 4.), (3., 4.)] +coords = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)] +make_3d = lambda cs: [c + (0.0,) for c in cs] +inner_1 = [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0)] +inner_2 = [(3.0, 3.0), (4.0, 3.0), (4.0, 4.0), (3.0, 4.0)] old_map = map map = lambda fn, *args: list(old_map(fn, *args)) f = ifcopenshell.file(schema="IFC2X3") -f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)) -) +f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0.0, 0.0, 0.0))) f.write(f"pass-axis2-3d-pos-ifc2x3.ifc") f = ifcopenshell.file(schema="IFC2X3") f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)), - f.createIfcDirection((0., 0., 1.)), - f.createIfcDirection((1., 0., 0.)), + f.createIfcCartesianPoint((0.0, 0.0, 0.0)), + f.createIfcDirection((0.0, 0.0, 1.0)), + f.createIfcDirection((1.0, 0.0, 0.0)), ) f.write(f"pass-axis2-two-directions-ifc2x3.ifc") f = ifcopenshell.file(schema="IFC2X3") f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)), - Axis=f.createIfcDirection((0., 0., 1.)), + f.createIfcCartesianPoint((0.0, 0.0, 0.0)), + Axis=f.createIfcDirection((0.0, 0.0, 1.0)), RefDirection=None, ) f.write(f"fail-axis2-only-axis-ifc2x3.ifc") f = ifcopenshell.file(schema="IFC2X3") f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)), + f.createIfcCartesianPoint((0.0, 0.0, 0.0)), Axis=None, - RefDirection=f.createIfcDirection((1., 0., 0.)), + RefDirection=f.createIfcDirection((1.0, 0.0, 0.0)), ) f.write(f"fail-axis2-only-ref-ifc2x3.ifc") f = ifcopenshell.file(schema="IFC2X3") f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)), - f.createIfcDirection((1., 0.)), - f.createIfcDirection((0., 1., 0.)), + f.createIfcCartesianPoint((0.0, 0.0, 0.0)), + f.createIfcDirection((1.0, 0.0)), + f.createIfcDirection((0.0, 1.0, 0.0)), ) f.write(f"fail-axis2-2d-axis-ifc2x3.ifc") f = ifcopenshell.file(schema="IFC2X3") f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0.)), + f.createIfcCartesianPoint((0.0, 0.0)), ) f.write(f"fail-axis2-2d-pos-ifc2x3.ifc") f = ifcopenshell.file(schema="IFC2X3") f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)), - f.createIfcDirection((0., 0., 1.)), - f.createIfcDirection((1., 0.)), + f.createIfcCartesianPoint((0.0, 0.0, 0.0)), + f.createIfcDirection((0.0, 0.0, 1.0)), + f.createIfcDirection((1.0, 0.0)), ) f.write(f"fail-axis2-2d-ref-ifc2x3.ifc") f = ifcopenshell.file(schema="IFC2X3") f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)), - Axis=f.createIfcDirection((0., 0., 1.)), - RefDirection=f.createIfcDirection((0., 0., 1.)), + f.createIfcCartesianPoint((0.0, 0.0, 0.0)), + Axis=f.createIfcDirection((0.0, 0.0, 1.0)), + RefDirection=f.createIfcDirection((0.0, 0.0, 1.0)), ) f.write(f"fail-axis2-parallel-axes.ifc") f = ifcopenshell.file(schema="IFC2X3") f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)), - Axis=f.createIfcDirection((0., 0., 1.)), - RefDirection=f.createIfcDirection((0., 0., -1.)), + f.createIfcCartesianPoint((0.0, 0.0, 0.0)), + Axis=f.createIfcDirection((0.0, 0.0, 1.0)), + RefDirection=f.createIfcDirection((0.0, 0.0, -1.0)), ) f.write(f"fail-axis2-anti-parallel-axes.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_14.py b/src/ifcopenshell-python/test/fixtures/rules/generate_14.py index 4ff47c5c50..917a41797b 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_14.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_14.py @@ -1,26 +1,19 @@ import ifcopenshell dims = [ - (1,0,0,0,0,0,0), - (1,1,0,0,0,0,0), - (0,1,0,0,0,0,0), - (-1,0,0,0,0,0,0), - (2,0,0,0,0,0,0), + (1, 0, 0, 0, 0, 0, 0), + (1, 1, 0, 0, 0, 0, 0), + (0, 1, 0, 0, 0, 0, 0), + (-1, 0, 0, 0, 0, 0, 0), + (2, 0, 0, 0, 0, 0, 0), ] for i, d in enumerate(dims): f = ifcopenshell.file(schema="IFC2X3") f.createIfcConversionBasedUnit( f.createIfcDimensionalExponents(*d), - 'LENGTHUNIT', - 'beard-second', - f.createIfcMeasureWithUnit( - f.createIfcLengthMeasure(5.0), - f.createIfcSIUnit( - Prefix='NANO', - Name='METRE' - ) - ) - + "LENGTHUNIT", + "beard-second", + f.createIfcMeasureWithUnit(f.createIfcLengthMeasure(5.0), f.createIfcSIUnit(Prefix="NANO", Name="METRE")), ) f.write(f"{'pass' if i == 0 else 'fail'}-conv-unit-{i}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_15.py b/src/ifcopenshell-python/test/fixtures/rules/generate_15.py index 9c63bfeb9b..6d3bfa04c3 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_15.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_15.py @@ -1,72 +1,77 @@ import ifcopenshell -coords = [(0., 0.), (10., 0.)] -coords_2 = [(0., 0.), (10., 0.), (10., 10.), (0., 10.)] -make_3d = lambda cs: [c + (0.,) for c in cs] -make_nd = lambda d: lambda c: (c + (0.,)) if d == 3 else c +coords = [(0.0, 0.0), (10.0, 0.0)] +coords_2 = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)] +make_3d = lambda cs: [c + (0.0,) for c in cs] +make_nd = lambda d: lambda c: (c + (0.0,)) if d == 3 else c old_map = map map = lambda fn, *args: list(old_map(fn, *args)) poly_2d = lambda f: [f.createIfcPolyline(map(f.createIfcCartesianPoint, coords))] poly_3d = lambda f: [f.createIfcPolyline(map(f.createIfcCartesianPoint, make_3d(coords)))] -plane = lambda f: [f.createIfcPlane(f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0., 0., 0.))))] -fbsm = lambda f: [f.createIfcFaceBasedSurfaceModel( - FbsmFaces=[f.createIfcOpenShell( - CfsFaces=[f.createIfcFace( - Bounds=[f.createIfcFaceOuterBound( - Bound=f.createIfcPolyLoop( - Polygon=map(f.createIfcCartesianPoint, make_3d(coords_2)) - ) - )] - )] - )] -)] -extrusion = lambda f: [f.createIfcExtrudedAreaSolid( - f.createIfcRectangleProfileDef( - "AREA", None, - f.createIfcAxis2Placement2D( - f.createIfcCartesianPoint((0., 0.)) - ), 1., 1. - ), - f.createIfcAxis2Placement3D( - f.createIfcCartesianPoint((0., 0., 0.)) - ), - f.createIfcDirection((0., 0., 1.)), - 1. -)] -bbox = lambda f: [f.createIfcBoundingBox( - f.createIfcCartesianPoint((0., 0., 0.)), - 1., 1., 1. -)] +plane = lambda f: [f.createIfcPlane(f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0.0, 0.0, 0.0))))] +fbsm = lambda f: [ + f.createIfcFaceBasedSurfaceModel( + FbsmFaces=[ + f.createIfcOpenShell( + CfsFaces=[ + f.createIfcFace( + Bounds=[ + f.createIfcFaceOuterBound( + Bound=f.createIfcPolyLoop(Polygon=map(f.createIfcCartesianPoint, make_3d(coords_2))) + ) + ] + ) + ] + ) + ] + ) +] +extrusion = lambda f: [ + f.createIfcExtrudedAreaSolid( + f.createIfcRectangleProfileDef( + "AREA", None, f.createIfcAxis2Placement2D(f.createIfcCartesianPoint((0.0, 0.0))), 1.0, 1.0 + ), + f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0.0, 0.0, 0.0))), + f.createIfcDirection((0.0, 0.0, 1.0)), + 1.0, + ) +] +bbox = lambda f: [f.createIfcBoundingBox(f.createIfcCartesianPoint((0.0, 0.0, 0.0)), 1.0, 1.0, 1.0)] gcs = lambda fn: lambda f: [f.createIfcGeometricSet(fn(f))] repeat = lambda n, fn: lambda f: fn(f) * n options = [ - (True, 2, 'Curve2D', '2d-polyline', poly_2d), - (False, 3, 'Curve2D', '3d-polyline', poly_3d), - (True, 2, 'GeometricCurveSet', 'with-curve', gcs(poly_2d)), - (False, 3, 'GeometricCurveSet', 'with-surface', gcs(plane)), - (False, 3, 'SurfaceModel', '3d-polyline', poly_3d), - (True, 3, 'SurfaceModel', 'surface-model', fbsm), - (False, 3, 'SweptSolid', '3d-polyline', poly_3d), - (True, 3, 'SweptSolid', 'extrusion', extrusion), - (True, 3, 'BoundingBox', 'single-bbox', bbox), - (False, 3, 'BoundingBox', 'multiple-bbox', repeat(2, bbox)), + (True, 2, "Curve2D", "2d-polyline", poly_2d), + (False, 3, "Curve2D", "3d-polyline", poly_3d), + (True, 2, "GeometricCurveSet", "with-curve", gcs(poly_2d)), + (False, 3, "GeometricCurveSet", "with-surface", gcs(plane)), + (False, 3, "SurfaceModel", "3d-polyline", poly_3d), + (True, 3, "SurfaceModel", "surface-model", fbsm), + (False, 3, "SweptSolid", "3d-polyline", poly_3d), + (True, 3, "SweptSolid", "extrusion", extrusion), + (True, 3, "BoundingBox", "single-bbox", bbox), + (False, 3, "BoundingBox", "multiple-bbox", repeat(2, bbox)), ] for is_valid, dims, rep_type, name, fn in options: f = ifcopenshell.file(schema="IFC2X3") - f.createIfcProductDefinitionShape(Representations=[ - f.createIfcShapeRepresentation( - f.createIfcGeometricRepresentationContext( - None, None, dims, None, - f.create_entity(f'IfcAxis2Placement{dims}D', f.createIfcCartesianPoint(make_nd(dims)((0., 0.)))), - ), - 'Body', - rep_type, - fn(f) - ) - ]) + f.createIfcProductDefinitionShape( + Representations=[ + f.createIfcShapeRepresentation( + f.createIfcGeometricRepresentationContext( + None, + None, + dims, + None, + f.create_entity(f"IfcAxis2Placement{dims}D", f.createIfcCartesianPoint(make_nd(dims)((0.0, 0.0)))), + ), + "Body", + rep_type, + fn(f), + ) + ] + ) f.write(f"{'pass' if is_valid else 'fail'}-shaperep-{rep_type.lower()}-{name}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_16.py b/src/ifcopenshell-python/test/fixtures/rules/generate_16.py index c6b4f80dae..7b6a32dfb1 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_16.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_16.py @@ -1,10 +1,7 @@ import time import ifcopenshell -names = [ - ("Same", "Same"), - ("Different", "SomethingElse") -] +names = [("Same", "Same"), ("Different", "SomethingElse")] make_prop = lambda f: lambda nm: f.createIfcPropertySingleValue(Name=nm) @@ -19,11 +16,6 @@ for nms in names: appl = f.createIfcApplication(o, ifcopenshell.version, "IfcOpenShell", f"IfcOpenShell {ifcopenshell.version}") units = f.createIfcUnitAssignment(Units=[f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")]) ownerhist = f.createIfcOwnerHistory(pando, appl, ChangeAction="ADDED", CreationDate=int(time.time())) - f.createIfcPropertySet( - ifcopenshell.guid.new(), - ownerhist, - "MyPset", - HasProperties=map(make_prop(f), nms) - ) + f.createIfcPropertySet(ifcopenshell.guid.new(), ownerhist, "MyPset", HasProperties=map(make_prop(f), nms)) f.write(f"{'pass' if len(set(nms)) == len(nms) else 'fail'}-property-{'-'.join(map(str.lower, nms))}-ifc2x3.ifc") -15 \ No newline at end of file +15 diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_17.py b/src/ifcopenshell-python/test/fixtures/rules/generate_17.py index 028ea4cc3c..2daa8b3b38 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_17.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_17.py @@ -9,18 +9,12 @@ for i, nm in enumerate(("no-mls", "mls", "mlsu")): appl = f.createIfcApplication(o, ifcopenshell.version, "IfcOpenShell", f"IfcOpenShell {ifcopenshell.version}") units = f.createIfcUnitAssignment(Units=[f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")]) ownerhist = f.createIfcOwnerHistory(pando, appl, ChangeAction="ADDED", CreationDate=int(time.time())) - wall = f.createIfcWallStandardCase( - ifcopenshell.guid.new(), - ownerhist - ) - if i in (1,2): + wall = f.createIfcWallStandardCase(ifcopenshell.guid.new(), ownerhist) + if i in (1, 2): mls = f.createIfcMaterialLayerSet([f.createIfcMaterialLayer(None, 0.1)]) if i == 2: - mls = f.createIfcMaterialLayerSetUsage(mls, 'AXIS2', 'POSITIVE', 0.) + mls = f.createIfcMaterialLayerSetUsage(mls, "AXIS2", "POSITIVE", 0.0) f.createIfcRelAssociatesMaterial( - ifcopenshell.guid.new(), - ownerhist, - RelatedObjects=[wall], - RelatingMaterial=mls + ifcopenshell.guid.new(), ownerhist, RelatedObjects=[wall], RelatingMaterial=mls ) f.write(f"{'pass' if i == 2 else 'fail'}-wall-{nm}-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_18.py b/src/ifcopenshell-python/test/fixtures/rules/generate_18.py index 7f59b07b83..e551356b43 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_18.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_18.py @@ -1,12 +1,12 @@ import ifcopenshell -for i, type_decl in enumerate(('IfcLengthMeasure', 'IfcPlaneAngleMeasure')): +for i, type_decl in enumerate(("IfcLengthMeasure", "IfcPlaneAngleMeasure")): f = ifcopenshell.file(schema="IFC4X3_ADD1") f.createIfcRigidOperation( - SourceCRS=f.createIfcGeographicCRS('EPSG:4326'), - TargetCRS=f.createIfcGeographicCRS('EPSG:3857'), + SourceCRS=f.createIfcGeographicCRS("EPSG:4326"), + TargetCRS=f.createIfcGeographicCRS("EPSG:3857"), FirstCoordinate=f.create_entity(type_decl, 0.0), - SecondCoordinate=f.create_entity('IfcPlaneAngleMeasure', 0.0), - Height=0.0 + SecondCoordinate=f.create_entity("IfcPlaneAngleMeasure", 0.0), + Height=0.0, ) f.write(f"{'pass' if i else 'fail'}-rigid-op-IfcPlaneAngleMeasure-{type_decl}-ifc4x3_add1.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_19.py b/src/ifcopenshell-python/test/fixtures/rules/generate_19.py index b370364a03..ca4298d711 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_19.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_19.py @@ -1,18 +1,16 @@ import ifcopenshell -make_nd = lambda d: lambda *c: (c + (0.,)) if d == 3 else c +make_nd = lambda d: lambda *c: (c + (0.0,)) if d == 3 else c -for d1, d2 in ((2,3), (3,3), (3,2)): +for d1, d2 in ((2, 3), (3, 3), (3, 2)): f = ifcopenshell.file(schema="IFC2X3") f.createIfcLocalPlacement( PlacementRelTo=f.createIfcLocalPlacement( PlacementRelTo=None, - RelativePlacement=f.create_entity(f'IfcAxis2Placement{d1}D', - f.createIfcCartesianPoint(make_nd(d1)(0., 0.)) - ) + RelativePlacement=f.create_entity( + f"IfcAxis2Placement{d1}D", f.createIfcCartesianPoint(make_nd(d1)(0.0, 0.0)) + ), ), - RelativePlacement=f.create_entity(f'IfcAxis2Placement{d2}D', - f.createIfcCartesianPoint(make_nd(d2)(0., 0.)) - ) + RelativePlacement=f.create_entity(f"IfcAxis2Placement{d2}D", f.createIfcCartesianPoint(make_nd(d2)(0.0, 0.0))), ) f.write(f"{'pass' if d1 >= d2 else 'fail'}-placement-{d1}d-{d2}d-ifc2x3.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_20.py b/src/ifcopenshell-python/test/fixtures/rules/generate_20.py index 4fb5b30b48..0480373db5 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_20.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_20.py @@ -1,10 +1,10 @@ import ifcopenshell -task = lambda f: f.createIfcTask(ifcopenshell.guid.new(),None,'sleep',IsMilestone=True) +task = lambda f: f.createIfcTask(ifcopenshell.guid.new(), None, "sleep", IsMilestone=True) wall = lambda f: f.createIfcWall(ifcopenshell.guid.new()) -for i,fn in enumerate((task, wall)): +for i, fn in enumerate((task, wall)): f = ifcopenshell.file(schema="IFC4") elem = fn(f) - f.createIfcRelAssociatesMaterial(ifcopenshell.guid.new(), None, None, None, [elem], f.createIfcMaterial('brick')) + f.createIfcRelAssociatesMaterial(ifcopenshell.guid.new(), None, None, None, [elem], f.createIfcMaterial("brick")) f.write(f"{'pass' if i else 'fail'}-assoc-material-{elem.is_a()}-{f.schema}.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_21.py b/src/ifcopenshell-python/test/fixtures/rules/generate_21.py index d402f26520..f5fcc68b71 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_21.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_21.py @@ -1,13 +1,26 @@ import itertools import ifcopenshell -def EXISTS(v): return v is not None -def NOT(v): return not v -for LiningDepth, LiningThickness in itertools.product((None, 1.), (None, 1.)): +def EXISTS(v): + return v is not None + + +def NOT(v): + return not v + + +for LiningDepth, LiningThickness in itertools.product((None, 1.0), (None, 1.0)): f = ifcopenshell.file(schema="IFC4") valid = NOT(EXISTS(LiningDepth) and NOT(EXISTS(LiningThickness))) - f.createIfcWindowType(ifcopenshell.guid.new(), None, 'WindowType', HasPropertySets=[ - f.createIfcWindowLiningProperties(ifcopenshell.guid.new(), LiningDepth=LiningDepth, LiningThickness=LiningThickness) - ]) - f.write(f"{'pass' if valid else 'fail'}-lining-properties-{LiningDepth}-{LiningThickness}.ifc") \ No newline at end of file + f.createIfcWindowType( + ifcopenshell.guid.new(), + None, + "WindowType", + HasPropertySets=[ + f.createIfcWindowLiningProperties( + ifcopenshell.guid.new(), LiningDepth=LiningDepth, LiningThickness=LiningThickness + ) + ], + ) + f.write(f"{'pass' if valid else 'fail'}-lining-properties-{LiningDepth}-{LiningThickness}.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_22.py b/src/ifcopenshell-python/test/fixtures/rules/generate_22.py index f6940def97..6b129fec50 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_22.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_22.py @@ -1,21 +1,54 @@ import ifcopenshell -for cnt in range(0,3): +for cnt in range(0, 3): f = ifcopenshell.file(schema="IFC4") elem = f.createIfcWall(ifcopenshell.guid.new()) for i in range(cnt): - f.createIfcRelDefinesByProperties(ifcopenshell.guid.new(), None, None, None, [elem], - f.createIfcPropertySet(ifcopenshell.guid.new(), None, 'Pset_WallCommon', None, [f.createIfcPropertySingleValue('LoadBearing', None, f.createIfcBoolean(True))]) + f.createIfcRelDefinesByProperties( + ifcopenshell.guid.new(), + None, + None, + None, + [elem], + f.createIfcPropertySet( + ifcopenshell.guid.new(), + None, + "Pset_WallCommon", + None, + [f.createIfcPropertySingleValue("LoadBearing", None, f.createIfcBoolean(True))], + ), ) f.write(f"{'pass' if cnt < 2 else 'fail'}-wall-{cnt}-same-psets.ifc") f = ifcopenshell.file(schema="IFC4") elem = f.createIfcWall(ifcopenshell.guid.new()) -f.createIfcRelDefinesByProperties(ifcopenshell.guid.new(), None, None, None, [elem], - f.createIfcPropertySet(ifcopenshell.guid.new(), None, 'Pset_WallCommon', None, [f.createIfcPropertySingleValue('LoadBearing', None, f.createIfcBoolean(True))]) +f.createIfcRelDefinesByProperties( + ifcopenshell.guid.new(), + None, + None, + None, + [elem], + f.createIfcPropertySet( + ifcopenshell.guid.new(), + None, + "Pset_WallCommon", + None, + [f.createIfcPropertySingleValue("LoadBearing", None, f.createIfcBoolean(True))], + ), ) -f.createIfcRelDefinesByProperties(ifcopenshell.guid.new(), None, None, None, [elem], - f.createIfcPropertySet(ifcopenshell.guid.new(), None, 'Custom', None, [f.createIfcPropertySingleValue('IsBeautiful', None, f.createIfcBoolean(True))]) +f.createIfcRelDefinesByProperties( + ifcopenshell.guid.new(), + None, + None, + None, + [elem], + f.createIfcPropertySet( + ifcopenshell.guid.new(), + None, + "Custom", + None, + [f.createIfcPropertySingleValue("IsBeautiful", None, f.createIfcBoolean(True))], + ), ) -f.write(f"pass-wall-different-psets.ifc") \ No newline at end of file +f.write(f"pass-wall-different-psets.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_23.py b/src/ifcopenshell-python/test/fixtures/rules/generate_23.py index 22546ce436..723252acbb 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_23.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_23.py @@ -1,14 +1,14 @@ import ifcopenshell fns = [ - (False, lambda f: f.createIfcLengthMeasure(-1.)), - (True, lambda f: f.createIfcLengthMeasure(1.)), - (True, lambda f: f.createIfcPositiveLengthMeasure(1.)), - (False, lambda f: f.createIfcDescriptiveMeasure('large')) + (False, lambda f: f.createIfcLengthMeasure(-1.0)), + (True, lambda f: f.createIfcLengthMeasure(1.0)), + (True, lambda f: f.createIfcPositiveLengthMeasure(1.0)), + (False, lambda f: f.createIfcDescriptiveMeasure("large")), ] for valid, fn in fns: f = ifcopenshell.file(schema="IFC4") fs = fn(f) - f.createIfcTextStyleFontModel('Comic Sans', ('Comic Sans',), FontSize=fs) + f.createIfcTextStyleFontModel("Comic Sans", ("Comic Sans",), FontSize=fs) f.write(f"{'pass' if valid else 'fail'}-font-{fs.is_a()}-{fs[0]}.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_24.py b/src/ifcopenshell-python/test/fixtures/rules/generate_24.py index 312af62c94..c790a6103d 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_24.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_24.py @@ -1,7 +1,18 @@ import time import ifcopenshell -for ents in [('IfcWall',), ('IfcSpace',), ('IfcZone', 'IfcSpace',), ('IfcWall', 'IfcSpace',)]: +for ents in [ + ("IfcWall",), + ("IfcSpace",), + ( + "IfcZone", + "IfcSpace", + ), + ( + "IfcWall", + "IfcSpace", + ), +]: f = ifcopenshell.file(schema="IFC2X3") p = f.createIfcPerson(Id="tfk", GivenName="Thomas") o = f.createIfcOrganization(Name="AECgeeks") @@ -9,24 +20,38 @@ for ents in [('IfcWall',), ('IfcSpace',), ('IfcZone', 'IfcSpace',), ('IfcWall', appl = f.createIfcApplication(o, ifcopenshell.version, "IfcOpenShell", f"IfcOpenShell {ifcopenshell.version}") units = f.createIfcUnitAssignment(Units=[f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")]) ownerhist = f.createIfcOwnerHistory(pando, appl, ChangeAction="ADDED", CreationDate=int(time.time())) - proj = f.createIfcProject(ifcopenshell.guid.new(), ownerhist, 'My Project', UnitsInContext=units, RepresentationContexts=[ - f.createIfcGeometricRepresentationContext( - None, None, 3, None, - f.create_entity(f'IfcAxis2Placement3D', f.createIfcCartesianPoint((0., 0., 0.))), - ) - ]) + proj = f.createIfcProject( + ifcopenshell.guid.new(), + ownerhist, + "My Project", + UnitsInContext=units, + RepresentationContexts=[ + f.createIfcGeometricRepresentationContext( + None, + None, + 3, + None, + f.create_entity(f"IfcAxis2Placement3D", f.createIfcCartesianPoint((0.0, 0.0, 0.0))), + ) + ], + ) + def elems(): for ent in ents: - el = f.create_entity(ent, - ifcopenshell.guid.new(), - ownerhist - ) - if ent == 'IfcZone': - f.createIfcRelAssignsToGroup(ifcopenshell.guid.new(),ownerhist,RelatedObjects=[f.createIfcSpace(ifcopenshell.guid.new(), - ownerhist)],RelatingGroup=el) + el = f.create_entity(ent, ifcopenshell.guid.new(), ownerhist) + if ent == "IfcZone": + f.createIfcRelAssignsToGroup( + ifcopenshell.guid.new(), + ownerhist, + RelatedObjects=[f.createIfcSpace(ifcopenshell.guid.new(), ownerhist)], + RelatingGroup=el, + ) yield el - zone = f.createIfcZone(ifcopenshell.guid.new(),ownerhist) - f.createIfcRelAssignsToGroup(ifcopenshell.guid.new(),ownerhist,RelatedObjects=list(elems()),RelatingGroup=zone) - f.createIfcRelAggregates(ifcopenshell.guid.new(),ownerhist,RelatingObject=proj,RelatedObjects=f.by_type('IfcSpace')) - valid = not (set(ents) - {'IfcZone', 'IfcSpace'}) + + zone = f.createIfcZone(ifcopenshell.guid.new(), ownerhist) + f.createIfcRelAssignsToGroup(ifcopenshell.guid.new(), ownerhist, RelatedObjects=list(elems()), RelatingGroup=zone) + f.createIfcRelAggregates( + ifcopenshell.guid.new(), ownerhist, RelatingObject=proj, RelatedObjects=f.by_type("IfcSpace") + ) + valid = not (set(ents) - {"IfcZone", "IfcSpace"}) f.write(f"{'pass' if valid else 'fail'}-zone-with-{'-'.join(ents)}-{f.schema.lower()}.ifc") diff --git a/src/ifcopenshell-python/test/fixtures/rules/generate_25.py b/src/ifcopenshell-python/test/fixtures/rules/generate_25.py index 4df6537293..78b540aea2 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/generate_25.py +++ b/src/ifcopenshell-python/test/fixtures/rules/generate_25.py @@ -2,13 +2,15 @@ import ifcopenshell segs = [ lambda _: None, - lambda f: [f.createIfcLineIndex((1,2)),f.createIfcLineIndex((2,3))], - lambda f: [f.createIfcLineIndex((1,2)),f.createIfcLineIndex((1,2))] + lambda f: [f.createIfcLineIndex((1, 2)), f.createIfcLineIndex((2, 3))], + lambda f: [f.createIfcLineIndex((1, 2)), f.createIfcLineIndex((1, 2))], ] -for schema in ('ifc4', 'ifc4x3_add1'): +for schema in ("ifc4", "ifc4x3_add1"): for i, seg in enumerate(segs): f = ifcopenshell.file(schema=schema) s = seg(f) - f.createIfcIndexedPolyCurve(f.createIfcCartesianPointList2D([(0.0, 0.0),(0.0, 1.0),(1.0, 1.0)]), s) - f.write(f'{"fail" if i == 2 else "pass"}-poly-curve-{"no-segments" if s is None else "-".join(["-".join(map(str, x[0])) for x in s])}-{schema}.ifc') + f.createIfcIndexedPolyCurve(f.createIfcCartesianPointList2D([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]), s) + f.write( + f'{"fail" if i == 2 else "pass"}-poly-curve-{"no-segments" if s is None else "-".join(["-".join(map(str, x[0])) for x in s])}-{schema}.ifc' + ) diff --git a/src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-1-2-2-3-ifc4x3_add1.ifc b/src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-1-2-2-3-ifc4x3_add2.ifc similarity index 92% rename from src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-1-2-2-3-ifc4x3_add1.ifc rename to src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-1-2-2-3-ifc4x3_add2.ifc index bbf6bf9c6a..d2e4c6ceac 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-1-2-2-3-ifc4x3_add1.ifc +++ b/src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-1-2-2-3-ifc4x3_add2.ifc @@ -2,7 +2,7 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); FILE_NAME('','2023-03-11T14:53:24',(),(),'IfcOpenShell v0.7.0-c4b77c2a0','IfcOpenShell v0.7.0-c4b77c2a0',''); -FILE_SCHEMA(('IFC4X3_ADD1')); +FILE_SCHEMA(('IFC4X3_ADD2')); ENDSEC; DATA; #1=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,1.),(1.,1.)),$); diff --git a/src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-no-segments-ifc4x3_add1.ifc b/src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-no-segments-ifc4x3_add2.ifc similarity index 91% rename from src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-no-segments-ifc4x3_add1.ifc rename to src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-no-segments-ifc4x3_add2.ifc index 63ce19a8f8..3063714fd9 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-no-segments-ifc4x3_add1.ifc +++ b/src/ifcopenshell-python/test/fixtures/rules/pass-poly-curve-no-segments-ifc4x3_add2.ifc @@ -2,7 +2,7 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); FILE_NAME('','2023-03-11T14:53:24',(),(),'IfcOpenShell v0.7.0-c4b77c2a0','IfcOpenShell v0.7.0-c4b77c2a0',''); -FILE_SCHEMA(('IFC4X3_ADD1')); +FILE_SCHEMA(('IFC4X3_ADD2')); ENDSEC; DATA; #1=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.,1.),(1.,1.)),$); diff --git a/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add1.ifc b/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc similarity index 93% rename from src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add1.ifc rename to src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc index fce85eeb73..9ac949de12 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add1.ifc +++ b/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc @@ -2,7 +2,7 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); FILE_NAME('','2023-01-11T09:47:45',(),(),'IfcOpenShell v0.7.0-8a108f37','IfcOpenShell v0.7.0-8a108f37',''); -FILE_SCHEMA(('IFC4X3_ADD1')); +FILE_SCHEMA(('IFC4X3_ADD2')); ENDSEC; DATA; #1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$); diff --git a/src/ifcopenshell-python/test/global_id_updates.py b/src/ifcopenshell-python/test/global_id_updates.py index dcf9c442e9..40b548f8c6 100644 --- a/src/ifcopenshell-python/test/global_id_updates.py +++ b/src/ifcopenshell-python/test/global_id_updates.py @@ -6,21 +6,22 @@ import ifcopenshell.guid def test_global_id_updates(): g1, g2, g3 = (ifcopenshell.guid.new() for i in range(3)) f = ifcopenshell.file() - + f.createIfcWall(g1) f[g1].GlobalId = g2 with pytest.raises(RuntimeError): f[g1] assert f[g2] - + inst = f.createIfcWall() inst.GlobalId = g3 assert f[g3] - + # Non-unique guid, succeeds but logs an error ifcopenshell.get_log() inst = f.createIfcWall(g3) assert "Overwriting" in ifcopenshell.get_log() + if __name__ == "__main__": pytest.main(["-sx", __file__]) diff --git a/src/ifcopenshell-python/test/instance_string_formatting.py b/src/ifcopenshell-python/test/instance_string_formatting.py index 88ffcd2a02..2d85e99d26 100644 --- a/src/ifcopenshell-python/test/instance_string_formatting.py +++ b/src/ifcopenshell-python/test/instance_string_formatting.py @@ -2,17 +2,19 @@ import pytest import ifcopenshell import ifcopenshell.guid + def test_file_gc(): f = ifcopenshell.file() inst = f.createIfcWall(ifcopenshell.guid.new(), Name=chr(0x1F37A)) # 0x1F37A should be encoded using X4 - assert '\\X4\\' in inst.to_string() + assert "\\X4\\" in inst.to_string() # to_string() should use upper case entity names - assert 'IFCWALL' in inst.to_string() + assert "IFCWALL" in inst.to_string() # __str__ uses camel case entity names - assert 'IfcWall' in str(inst) + assert "IfcWall" in str(inst) # in fact, __str__ is equal to to_string(False) assert str(inst) == inst.to_string(False) + if __name__ == "__main__": pytest.main(["-sx", __file__]) diff --git a/src/ifcopenshell-python/test/test_file_inverse.py b/src/ifcopenshell-python/test/test_file_inverse.py index 40fd1062f4..9d385ff0f0 100644 --- a/src/ifcopenshell-python/test/test_file_inverse.py +++ b/src/ifcopenshell-python/test/test_file_inverse.py @@ -9,11 +9,11 @@ import ifcopenshell.api def test_inverse_indices(): - f = ifcopenshell.file() + f = ifcopenshell.file() - p0 = f.createIfcCartesianPoint((0.,0.,0.)) - p1 = f.createIfcCartesianPoint((1.,0.,0.)) - p2 = f.createIfcCartesianPoint((1.,1.,0.)) + p0 = f.createIfcCartesianPoint((0.0, 0.0, 0.0)) + p1 = f.createIfcCartesianPoint((1.0, 0.0, 0.0)) + p2 = f.createIfcCartesianPoint((1.0, 1.0, 0.0)) poly = f.createIfcPolyline((p0, p1, p2, p0)) place = f.createIfcAxis2Placement3D(p0) @@ -26,7 +26,7 @@ def test_inverse_indices(): # @nb this doesn't account for nested lists v = list(v) i = v.index(p0) - v[i:i+1] = [] + v[i : i + 1] = [] else: v = p1 inst[idx] = v diff --git a/src/ifcopenshell-python/test/test_open.py b/src/ifcopenshell-python/test/test_open.py index 02b2f32599..90ef8615df 100644 --- a/src/ifcopenshell-python/test/test_open.py +++ b/src/ifcopenshell-python/test/test_open.py @@ -32,24 +32,16 @@ class TestOpen: assert ifcopenshell.open(TEST_FILE_DIR / "wall-with-opening-and-window.ifcxml") def test_open_ifc_zip_ifcxml_format(self): - assert ifcopenshell.open( - TEST_FILE_DIR / "wall-with-opening-and-window_ifcxml_format.ifczip" - ) + assert ifcopenshell.open(TEST_FILE_DIR / "wall-with-opening-and-window_ifcxml_format.ifczip") def test_open_ifc_zip_ifcspf_format(self): - assert ifcopenshell.open( - TEST_FILE_DIR / "WallInstance_IFC4Add2_ifcspf_format.ifczip" - ) + assert ifcopenshell.open(TEST_FILE_DIR / "WallInstance_IFC4Add2_ifcspf_format.ifczip") def test_open_zip(self): - assert ifcopenshell.open( - TEST_FILE_DIR / "WallInstance_IFC4Add2_ifcspf_format.zip" - ) + assert ifcopenshell.open(TEST_FILE_DIR / "WallInstance_IFC4Add2_ifcspf_format.zip") def test_open_anyextension_ifcspf_format(self): - assert ifcopenshell.open( - TEST_FILE_DIR / "WallInstance_IFC4Add2_ifcspf_format.anyextension" - ) + assert ifcopenshell.open(TEST_FILE_DIR / "WallInstance_IFC4Add2_ifcspf_format.anyextension") def test_open_anyextension_ifczip_ifcspf_format(self): assert ifcopenshell.open( diff --git a/src/ifcopenshell-python/test/test_rules.py b/src/ifcopenshell-python/test/test_rules.py index c6dba38927..a54e98ed68 100644 --- a/src/ifcopenshell-python/test/test_rules.py +++ b/src/ifcopenshell-python/test/test_rules.py @@ -9,10 +9,13 @@ import ifcopenshell.validate import ifcopenshell.express.rule_executor - @pytest.mark.parametrize( "filename", - [fn for fn in glob.glob(os.path.join(os.path.dirname(__file__), "fixtures/rules/*.ifc")) if len(sys.argv) < 2 or sys.argv[1] in os.path.basename(fn)], + [ + fn + for fn in glob.glob(os.path.join(os.path.dirname(__file__), "fixtures/rules/*.ifc")) + if len(sys.argv) < 2 or sys.argv[1] in os.path.basename(fn) + ], ) def test_file(filename): base = os.path.basename(filename) @@ -27,12 +30,14 @@ def test_file(filename): print(f"{len(results)} errors") if results: - print(tabulate.tabulate( - [[c or '' for c in r.values()] for r in results], - maxcolwidths=[20,100,20], - tablefmt="simple_grid", - headers=results[0].keys() - )) + print( + tabulate.tabulate( + [[c or "" for c in r.values()] for r in results], + maxcolwidths=[20, 100, 20], + tablefmt="simple_grid", + headers=results[0].keys(), + ) + ) if base.startswith("fail-"): assert len(results) > 0 diff --git a/src/ifcopenshell-python/test/test_wall_opening.py b/src/ifcopenshell-python/test/test_wall_opening.py index 29aceda18a..0e8c0d42cf 100644 --- a/src/ifcopenshell-python/test/test_wall_opening.py +++ b/src/ifcopenshell-python/test/test_wall_opening.py @@ -230,7 +230,11 @@ class TestWallOpenings: if i == 0 and j == 0: for ln, st in cs: - assert len([l for l in log if l.startswith(st)]) == ln, f"\nOn file:\n - {fn}\nMessages:" + "".join(f'\n - "{l}"' for l in log) + f"\nExpected:\n - \"{st}\"" + assert len([l for l in log if l.startswith(st)]) == ln, ( + f"\nOn file:\n - {fn}\nMessages:" + + "".join(f'\n - "{l}"' for l in log) + + f'\nExpected:\n - "{st}"' + ) # breakpoint() diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 6c316d509e..864633cb15 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -379,23 +379,17 @@ class TestGetMaterial(test.bootstrap.IFC4): def test_getting_a_material_layer_set_of_a_product(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.material.assign_material( - self.file, products=[element], type="IfcMaterialLayerSet" - ) + rel = ifcopenshell.api.material.assign_material(self.file, products=[element], type="IfcMaterialLayerSet") assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_profile_set_of_a_product(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.material.assign_material( - self.file, products=[element], type="IfcMaterialProfileSet" - ) + rel = ifcopenshell.api.material.assign_material(self.file, products=[element], type="IfcMaterialProfileSet") assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_layer_set_usage_of_a_product(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.material.assign_material( - self.file, products=[element], type="IfcMaterialLayerSetUsage" - ) + rel = ifcopenshell.api.material.assign_material(self.file, products=[element], type="IfcMaterialLayerSetUsage") assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_profile_set_usage_of_a_product(self): @@ -407,9 +401,7 @@ class TestGetMaterial(test.bootstrap.IFC4): def test_getting_a_material_layer_set_indirectly_from_an_assigned_usage(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.material.assign_material( - self.file, products=[element], type="IfcMaterialLayerSetUsage" - ) + rel = ifcopenshell.api.material.assign_material(self.file, products=[element], type="IfcMaterialLayerSetUsage") assert subject.get_material(element, should_skip_usage=True) == rel.RelatingMaterial.ForLayerSet def test_getting_a_material_profile_set_indirectly_from_an_assigned_usage(self): @@ -501,9 +493,7 @@ class TestGetStyles(test.bootstrap.IFC4): self.file, context=body, length=5, height=3, thickness=0.118 ) - ifcopenshell.api.geometry.assign_representation( - self.file, product=element, representation=representation - ) + ifcopenshell.api.geometry.assign_representation(self.file, product=element, representation=representation) ifcopenshell.api.style.assign_representation_styles( self.file, shape_representation=representation, styles=[style2] ) @@ -540,9 +530,7 @@ class TestGetElementsByMaterial(test.bootstrap.IFC4): material_set = ifcopenshell.api.material.add_material_set(self.file, set_type="IfcMaterialProfileSet") ifcopenshell.api.material.add_profile(self.file, profile_set=material_set, material=material) ifcopenshell.api.material.assign_material(self.file, products=[element_type], material=material_set) - ifcopenshell.api.material.assign_material( - self.file, products=[element], type="IfcMaterialProfileSetUsage" - ) + ifcopenshell.api.material.assign_material(self.file, products=[element], type="IfcMaterialProfileSetUsage") usage = self.file.by_type("IfcMaterialProfileSetUsage")[0] assert subject.get_elements_by_material(self.file, material) == {element, element_type} assert subject.get_elements_by_material(self.file, material_set) == {element, element_type} @@ -551,9 +539,7 @@ class TestGetElementsByMaterial(test.bootstrap.IFC4): def test_getting_elements_of_a_material_constituent_set(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") material = ifcopenshell.api.material.add_material(self.file) - material_set = ifcopenshell.api.material.add_material_set( - self.file, set_type="IfcMaterialConstituentSet" - ) + material_set = ifcopenshell.api.material.add_material_set(self.file, set_type="IfcMaterialConstituentSet") ifcopenshell.api.material.add_constituent(self.file, constituent_set=material_set, material=material) ifcopenshell.api.material.assign_material(self.file, products=[element], material=material_set) assert subject.get_elements_by_material(self.file, material) == {element} diff --git a/src/ifcopenshell-python/test/util/test_pset.py b/src/ifcopenshell-python/test/util/test_pset.py index 2eb5a3da5c..de3b7b3bf2 100644 --- a/src/ifcopenshell-python/test/util/test_pset.py +++ b/src/ifcopenshell-python/test/util/test_pset.py @@ -57,7 +57,7 @@ class TestPsetQto: 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" ) + names = self.pset_qto.get_applicable_names("IfcFurnitureType") names2 = self.pset_qto.get_applicable_names("IfcFurnitureType", "CUSTOM") assert names == names2 diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 51ce7f3dea..a20372a9d7 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -86,9 +86,7 @@ class TestGetElementValue(test.bootstrap.IFC4): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") material = ifcopenshell.api.material.add_material(self.file, name="CON01") material2 = ifcopenshell.api.material.add_material(self.file, name="CON02") - material_set = ifcopenshell.api.material.add_material_set( - self.file, name="FOO", set_type="IfcMaterialLayerSet" - ) + material_set = ifcopenshell.api.material.add_material_set(self.file, name="FOO", set_type="IfcMaterialLayerSet") layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material) layer.Name = "L1" layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material) @@ -113,9 +111,7 @@ class TestGetElementValue(test.bootstrap.IFC4): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") material = ifcopenshell.api.material.add_material(self.file, name="CON01") material2 = ifcopenshell.api.material.add_material(self.file, name="CON02") - material_set = ifcopenshell.api.material.add_material_set( - self.file, name="FOO", set_type="IfcMaterialLayerSet" - ) + material_set = ifcopenshell.api.material.add_material_set(self.file, name="FOO", set_type="IfcMaterialLayerSet") layer = ifcopenshell.api.material.add_layer(self.file, layer_set=material_set, material=material) layer.Name = "L1" ifcopenshell.api.material.assign_material(self.file, products=[element], material=material_set) diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index 1ee5d8b293..a65a538f7e 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -151,6 +151,7 @@ class TestConvertFileLengthUnits(test.bootstrap.IFC4): unit_assignment = subject.get_unit_assignment(output) assert len(unit_assignment.Units) == 1 + class TestConvertFileLengthUnitsIFC2X3(test.bootstrap.IFC2X3): def test_converting_map_conversion_if_there_is_no_map_unit(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") @@ -171,7 +172,9 @@ class TestConvertFileLengthUnitsIFC2X3(test.bootstrap.IFC2X3): ifcopenshell.api.context.add_context(self.file, "Model") ifcopenshell.api.georeference.add_georeferencing(self.file) ifcopenshell.api.georeference.edit_georeferencing( - self.file, projected_crs={"MapUnit": subject.get_full_unit_name(meter)}, coordinate_operation={"Eastings": 10, "Scale": 0.001} + self.file, + projected_crs={"MapUnit": subject.get_full_unit_name(meter)}, + coordinate_operation={"Eastings": 10, "Scale": 0.001}, ) ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index 9ffc25ba6f..f5cb2902b0 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -26,6 +26,7 @@ import typing import inspect import collections import importlib +import importlib.util from typing import Union @@ -158,6 +159,9 @@ def _extract_docs(cls, method_name, boilerplate_args): continue if isinstance(type_hint, typing._UnionGenericAlias): inputs[input_name]["type"] = [t.__name__ for t in typing.get_args(type_hint)] + elif type_hint.__name__ == "Literal": + inputs[input_name]["type"] = "Literal" + inputs[input_name]["enum_items"] = list(typing.get_args(type_hint)) else: inputs[input_name]["type"] = type_hint.__name__ @@ -174,6 +178,10 @@ def _extract_docs(cls, method_name, boilerplate_args): param_name = line.split(":")[1].strip().replace("param ", "") if param_name in inputs: inputs[param_name]["description"] = line.split(":")[2].strip() + elif line.startswith(":filter_glob"): + param_name = line.split(":")[1].strip().replace("filter_glob ", "") + if param_name in inputs: + inputs[param_name]["filter_glob"] = line.split(":")[2].strip() elif i == 2: description += line elif i > 2: diff --git a/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py b/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py index dce1aaa60e..c555a2035e 100644 --- a/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py +++ b/src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py @@ -24,6 +24,31 @@ import ifcopenshell.util.element import ifcopenshell.util.unit from logging import Logger +import typing + +LengthUnit = typing.Literal[ + "ATTOMETER", + "FEMTOMETER", + "PICOMETER", + "NANOMETER", + "MICROMETER", + "MILLIMETER", + "CENTIMETER", + "DECIMETER", + "METER", + "DECAMETER", + "HECTOMETER", + "KILOMETER", + "MEGAMETER", + "GIGAMETER", + "TERAMETER", + "PETAMETER", + "EXAMETER", + "INCH", + "FOOT", + "MILE", +] + class Patcher: def __init__( @@ -31,7 +56,7 @@ class Patcher: src: str, file: ifcopenshell.file, logger: Logger, - unit: str = "METER", + unit: LengthUnit = "METER", ): """Converts the length unit of a model to the specified unit @@ -39,7 +64,7 @@ class Patcher: Allowed imperial units include INCH, FOOT, MILE. :param unit: The name of the desired unit, defaults to "METER" - :type unit: str + :type unit: LengthUnit Example: diff --git a/src/ifcpatch/ifcpatch/recipes/ConvertPropertiesToQuantities.py b/src/ifcpatch/ifcpatch/recipes/ConvertPropertiesToQuantities.py index 4483048e0c..71ec724cd3 100644 --- a/src/ifcpatch/ifcpatch/recipes/ConvertPropertiesToQuantities.py +++ b/src/ifcpatch/ifcpatch/recipes/ConvertPropertiesToQuantities.py @@ -17,12 +17,15 @@ # along with IfcPatch. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.pset import ifcopenshell.util.element +from logging import Logger +from typing import Union class Patcher: - def __init__(self, src, file, logger, property_name=None, quantity_name=None): + def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, property_name: str, quantity_name: str): """Converts a property to a standardised quantity IFC can store arbitrary key value metadata associated with a elements @@ -40,14 +43,12 @@ class Patcher: :param property_name: The name of the property to convert into a quantity. The name of the property set is not considered. - :type property_name: str :param quantity_name: The name of the quantity that this property should be stored in. This should be a standard name that is one of the quantity names of a buildingSMART quantity template. For example, it may be "NetSideArea" for walls, which exists in Qto_WallBaseQuantities. The quantity set name will be based on the standard buildingSMART quantity template. - :type quantity_name: str Example: @@ -63,7 +64,7 @@ class Patcher: self.source_property_name = property_name self.destination_quantity_name = quantity_name - def patch(self): + def patch(self) -> None: self.qto_template_cache = {} self.psetqto = ifcopenshell.util.pset.get_template("IFC4") @@ -76,7 +77,9 @@ class Patcher: [r.RelatingPropertyDefinition for r in product.IsDefinedBy if r.is_a("IfcRelDefinesByProperties")], ) - def process_product(self, product, definitions): + def process_product( + self, product: ifcopenshell.entity_instance, definitions: list[ifcopenshell.entity_instance] + ) -> None: value = None has_quantity = False qtos = {} @@ -98,12 +101,12 @@ class Patcher: "pset.edit_qto", self.file, qto=qto, Properties={self.destination_quantity_name: value} ) - def get_qto_name(self, ifc_class): + def get_qto_name(self, ifc_class: str) -> Union[str, None]: for template in self.get_qto_templates(ifc_class): if self.destination_quantity_name in [t.Name for t in template.HasPropertyTemplates]: return template.Name - def get_qto_templates(self, ifc_class): + def get_qto_templates(self, ifc_class: str) -> list[ifcopenshell.entity_instance]: if ifc_class not in self.qto_template_cache: self.qto_template_cache[ifc_class] = self.psetqto.get_applicable(ifc_class, qto_only=True) return self.qto_template_cache[ifc_class] diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py index 5f5c031bf6..a7bfb7fb36 100644 --- a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py +++ b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py @@ -100,7 +100,7 @@ class Patcher: if face.normal.z < 0.5: faces_to_delete.append(face) - bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES_ONLY') + bmesh.ops.delete(bm, geom=faces_to_delete, context="FACES_ONLY") bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.01) bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method="BEAUTY", ngon_method="BEAUTY") diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index b8664de93e..b3e18714d0 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -22,6 +22,7 @@ import re import json import time import tempfile +import typing import itertools import numpy as np import multiprocessing @@ -33,15 +34,19 @@ import ifcopenshell.util.schema import ifcopenshell.util.attribute import ifcopenshell.util.placement +SQLTypes = typing.Literal["SQLite", "MySQL"] + try: import sqlite3 except: print("No SQLite support") + SQLTypes = typing.Literal["MySQL"] try: import mysql.connector except: print("No MySQL support") + SQLTypes = typing.Literal["SQLite"] class Patcher: @@ -50,7 +55,7 @@ class Patcher: src, file, logger, - sql_type: str = "sqlite", + sql_type: SQLTypes = "SQLite", host: str = "localhost", username: str = "root", password: str = "pass", @@ -79,8 +84,8 @@ class Patcher: IfcRepresentation and IfcRepresentationItem classes. These tables are unnecessary if you are not interested in geometry. - :param sql_type: Choose between "sqlite" or "mysql" - :type sql_type: str + :param sql_type: Choose between "SQLite" or "MySQL" + :type sql_type: typing.Literal["SQLite", "MySQL"] Example: @@ -92,7 +97,7 @@ class Patcher: self.src = src self.file = file self.logger = logger - self.sql_type = sql_type + self.sql_type = sql_type.lower() self.host = host self.username = username self.password = password diff --git a/src/ifcpatch/ifcpatch/recipes/MergeProject.py b/src/ifcpatch/ifcpatch/recipes/MergeProject.py index 68f315945b..6e37bc104e 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeProject.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeProject.py @@ -40,6 +40,8 @@ class Patcher: :param filepath: The filepath of the second IFC model to merge into the first. The first model is already specified as the input to IfcPatch. + :type filepath: Union[str, ifcopenshell.file] + :filter_glob filepath: *.ifc;*.ifczip;*.ifcxml Example: @@ -53,11 +55,15 @@ class Patcher: self.filepath = filepath def patch(self): - if isinstance(self.filepath, ifcopenshell.file): - other = self.filepath - else: - other = ifcopenshell.open(self.filepath) + for filepath in self.filepath: + if isinstance(filepath, ifcopenshell.file): + other = filepath + else: + other = ifcopenshell.open(filepath) + self.merge(other) + + def merge(self, other): if (main_unit := self.get_unit_name(self.file)) != self.get_unit_name(other): other = ifcopenshell.util.unit.convert_file_length_units(other, main_unit) diff --git a/src/ifcpatch/ifcpatch/recipes/Migrate.py b/src/ifcpatch/ifcpatch/recipes/Migrate.py index ff5ed28e5e..4b3a1cf8c9 100644 --- a/src/ifcpatch/ifcpatch/recipes/Migrate.py +++ b/src/ifcpatch/ifcpatch/recipes/Migrate.py @@ -18,11 +18,18 @@ import ifcopenshell import ifcopenshell.util.schema +import typing from logging import Logger class Patcher: - def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, schema: str = "IFC4"): + def __init__( + self, + src: str, + file: ifcopenshell.file, + logger: Logger, + schema: typing.Literal["IFC2X3", "IFC4", "IFC4X3"] = "IFC4", + ): """Migrate from one IFC version to another Note that this is experimental and will try to preserve as much data as diff --git a/src/ifcpatch/ifcpatch/recipes/OffsetObjectPlacements.py b/src/ifcpatch/ifcpatch/recipes/OffsetObjectPlacements.py index 8582aea6e5..55059e5283 100644 --- a/src/ifcpatch/ifcpatch/recipes/OffsetObjectPlacements.py +++ b/src/ifcpatch/ifcpatch/recipes/OffsetObjectPlacements.py @@ -20,10 +20,23 @@ import math import numpy as np import ifcopenshell import ifcopenshell.util.placement +import typing class Patcher: - def __init__(self, src, file, logger, x=None, y=None, z=None, should_rotate_first=True, ax=None, ay=None, az=None): + def __init__( + self, + src, + file, + logger, + x: typing.Union[str, float] = "0", + y: typing.Union[str, float] = "0", + z: typing.Union[str, float] = "0", + should_rotate_first: bool = True, + ax: typing.Optional[typing.Union[str, float]] = "", + ay: typing.Optional[typing.Union[str, float]] = "", + az: typing.Optional[typing.Union[str, float]] = "", + ): """Offset and rotate all object placements in a model Every physical object in an IFC model has an object placement, a @@ -41,11 +54,11 @@ class Patcher: entire IFC model. :param x: The X coordinate to offset by in project length units. - :type x: float + :type x: typing.Union[str, float] :param y: The Y coordinate to offset by in project length units. - :type y: float + :type y: typing.Union[str, float] :param z: The Z coordinate to offset by in project length units. - :type z: float + :type z: typing.Union[str, float] :param should_rotate_first: Whether or not to rotate first and then translate, or to first translate and rotate afterwards. Defaults to rotate first then translate. @@ -55,13 +68,13 @@ class Patcher: around the Z axis). If all angle parameters are specified, then it is treated as the angle to rotate around the X axis. Angles are in decimal degrees and positive is anticlockwise. - :type ax: float,optional + :type ax: typing.Union[str, float],optional :param ay: An optional angle to rotate by for 3D rotations along the Y axis. Angles are in decimal degrees and positive is anticlockwise. - :type ay: float,optional + :type ay: typing.Union[str, float],optional :param az: An optional angle to rotate by for 3D rotations along the Z axis. Angles are in decimal degrees and positive is anticlockwise. - :type az: float,optional + :type az: typing.Union[str, float],optional Example: @@ -79,13 +92,21 @@ class Patcher: self.src = src self.file = file self.logger = logger - self.x = x - self.y = y - self.z = z + self.x = float(x) + self.y = float(y) + self.z = float(z) self.should_rotate_first = should_rotate_first - self.ax = ax - self.ay = ay - self.az = az + self.ax = None + self.ay = None + self.az = None + + try: + self.ax = float(ax) + self.ay = float(ay) + self.az = float(az) + except Exception: + print("At least one input angle is not specified.") + if self.ax is None: self.angle_type = None elif self.ay is None: diff --git a/src/ifcpatch/ifcpatch/recipes/OffsetStoreyElevations.py b/src/ifcpatch/ifcpatch/recipes/OffsetStoreyElevations.py index 24cc9f6f00..5611b4a126 100644 --- a/src/ifcpatch/ifcpatch/recipes/OffsetStoreyElevations.py +++ b/src/ifcpatch/ifcpatch/recipes/OffsetStoreyElevations.py @@ -16,15 +16,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcPatch. If not, see . +import typing + class Patcher: - def __init__(self, src, file, logger, z=None): + def __init__(self, src, file, logger, z: typing.Union[str, float] = "0"): """Offset building storeys by a particular Z value All objects placed relative to the storeys will also be shifted. :param z: The Z value in project length units to offset storeys by. - :type z: float + :type z: typing.Union[str, float] Example: @@ -36,7 +38,7 @@ class Patcher: self.src = src self.file = file self.logger = logger - self.z = z + self.z = float(z) def patch(self): project = self.file.by_type("IfcProject")[0] diff --git a/src/ifcpatch/ifcpatch/recipes/ResetAbsoluteCoordinates.py b/src/ifcpatch/ifcpatch/recipes/ResetAbsoluteCoordinates.py index 283c8f71e0..7f58e39694 100644 --- a/src/ifcpatch/ifcpatch/recipes/ResetAbsoluteCoordinates.py +++ b/src/ifcpatch/ifcpatch/recipes/ResetAbsoluteCoordinates.py @@ -30,14 +30,15 @@ class Patcher: file: ifcopenshell.file, logger: logging.Logger, mode: Literal[ - "geometry", - "placement", - "both", - ] = "geometry", - a: Optional[float] = None, - b: Optional[float] = None, - c: Optional[float] = None, - d: Optional[float] = None, + "Geometry", + "Placement", + "Both", + ] = "Geometry", + automatic_offset_point: bool = True, + threshold: float = 1000000, # Arbitrary default threshold based on experience + x: Union[str, float] = "0", + y: Union[str, float] = "0", + z: Union[str, float] = "0", ): """Reset any large coordinates to smaller coordinates based on a threshold @@ -53,47 +54,34 @@ class Patcher: ordinate is larger than a threshold) and offsets it back down to a small number. - You may either manually specify the offset to apply to any large - coordinate, or an offset will be automatically determined arbitrarily - based on the first large number we encounter. + You may either let Blender BIM determine the offset to apply to any large + coordinate automatically, arbitrarily based on the first large number we + encounter, or manually specify this offset. Note that if your model inconsistently mixes coordinates between large and small (such as if your model mixes both local and map coordinates) then the results of this function may be poor. Provide a bug report back to your BIM application to get it fixed. - You may specify up to 4 arguments, a, b, c, and d. - - If you specify no arguments, then the threshold is set to 1000000. The - offset is auto detected. - - If you only specify 1 parameter (i.e. a), then this is treated as the - threshold beyond which an ordinate is considered to be large. The offset - is auto detected. - - If you specify 3 parameters, (i.e. a, b, c) then your three numbers are - treated as the X, Y, Z offset to apply. Typically your numbers will be - negative to bring the numbers smaller. The threshold is set to 1000000. - - If you specify 4 parameters (i.e. a, b, c, d), then the first three - numbers are treated as the X, Y, Z offset to apply (a, b, c). The fourth - (d) will be treated as the threshold. - - :param mode: Choose from "geometry", "placement", or "both". Choosing - "geometry" will only replace cartesian points used in shape - representations. Choosing "placement" will only replace cartesian - points used in object placements. Choosing "both" will replace all + :param mode: Choose from "Geometry", "Placement", or "Both". Choosing + "Geometry" will only replace cartesian points used in shape + representations. Choosing "Placement" will only replace cartesian + points used in object placements. Choosing "Both" will replace all cartesian points regardless of use (useful if the model has both large placement offsets and large geometry offsets). :type mode: str - :param a: The first parameter - :type a: float,optional - :param b: The second parameter - :type b: float,optional - :param c: The third parameter - :type c: float,optional - :param d: The fourth parameter - :type d: float,optional + :param automatic_offset_point: Choose, whether the offset should be + determined automatically or specified manually. + :type automatic_offset_point: bool + :param threshold: The threshold for deciding, whether a coordinate is + treated as a large coordinate. + :type threshold: float + :param x: The x-ordinate of the manually specified offset point. + :type x: Union[str, float] + :param y: The y-ordinate of the manually specified offset point. + :type y: Union[str, float] + :param z: The z-ordinate of the manually specified offset point. + :type z: Union[str, float] Example: @@ -103,39 +91,33 @@ class Patcher: ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": []}) # Reset all coordinates with an ordinate larger than 1000 arbitrarily - ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [1000]}) + ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [True, 1000]}) # Reset all coordinates with an ordinate larger than 1000000 by -50000,-20000,0 - ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [-50000,-20000,0]}) + ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [False, 1000000, -50000,-20000,0]}) # Reset all coordinates with an ordinate larger than 1000 by -500,-200,0 - ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [-500,-200,0,1000]}) + ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ResetAbsoluteCoordinates", "arguments": [False, 1000, -500,-200,0]}) """ self.src = src self.file = file self.logger = logger - self.mode = mode - self.args = [x for x in [a, b, c, d] if x is not None] + self.mode = mode.lower() + self.threshold = threshold + if automatic_offset_point: + self.offset_point = None + else: + self.offset_point = (float(x), float(y), float(z)) def patch(self): placement_coord_ids = set() for placement in self.file.by_type("IfcObjectPlacement"): [placement_coord_ids.add(e.id()) for e in self.file.traverse(placement) if e.is_a("IfcCartesianPoint")] - # Arbitrary threshold based on experience - self.threshold = 1000000 - if self.args and len(self.args) == 1: - self.threshold = float(self.args[0]) - elif self.args and len(self.args) == 4: - self.threshold = float(self.args[3]) - # This method will not work all the time, but will catch most issues. It # assumes that absolute coordinates are easily recognisable based on # having a large absolute value above a threshold. This is not always # the case, but is very fast to run, and works for most cases. - offset_point = None - if self.args and len(self.args) >= 3: - offset_point = (float(self.args[0]), float(self.args[1]), float(self.args[2])) try: point_lists = self.file.by_type("IfcCartesianPointList3D") except: @@ -147,10 +129,14 @@ class Patcher: if len(point) == 2 or not self.is_point_far_away(point): coord_list[i] = point continue - if not offset_point: - offset_point = (-point[0], -point[1], -point[2]) + if not self.offset_point: + self.offset_point = (-point[0], -point[1], -point[2]) self.logger.info(f"Resetting absolute coordinates by {point}") - point = (point[0] + offset_point[0], point[1] + offset_point[1], point[2] + offset_point[2]) + point = ( + point[0] + self.offset_point[0], + point[1] + self.offset_point[1], + point[2] + self.offset_point[2], + ) coord_list[i] = point point_list.CoordList = coord_list for point in self.file.by_type("IfcCartesianPoint"): @@ -162,13 +148,13 @@ class Patcher: elif self.mode == "placement": if point.id() not in placement_coord_ids: continue - if not offset_point: - offset_point = (-point.Coordinates[0], -point.Coordinates[1], -point.Coordinates[2]) + if not self.offset_point: + self.offset_point = (-point.Coordinates[0], -point.Coordinates[1], -point.Coordinates[2]) self.logger.info(f"Resetting absolute coordinates by {point}") point.Coordinates = ( - point.Coordinates[0] + offset_point[0], - point.Coordinates[1] + offset_point[1], - point.Coordinates[2] + offset_point[2], + point.Coordinates[0] + self.offset_point[0], + point.Coordinates[1] + self.offset_point[1], + point.Coordinates[2] + self.offset_point[2], ) def is_point_far_away(self, point: Union[ifcopenshell.entity_instance, npt.NDArray[np.float64]]) -> bool: diff --git a/src/ifcpatch/ifcpatch/recipes/SetFalseOrigin.py b/src/ifcpatch/ifcpatch/recipes/SetFalseOrigin.py index a67463a586..8657890418 100644 --- a/src/ifcpatch/ifcpatch/recipes/SetFalseOrigin.py +++ b/src/ifcpatch/ifcpatch/recipes/SetFalseOrigin.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.georeference from ifcpatch.recipes import OffsetObjectPlacements, SetWorldCoordinateSystem +import typing class Patcher: @@ -28,14 +29,14 @@ class Patcher: file, logger, name: str = "EPSG:1234", - x: float = 0, - y: float = 0, - z: float = 0, - e: float = 0, - n: float = 0, - h: float = 0, - gn_angle: float = 0, - rotate_angle: float = 0, + x: typing.Union[str, float] = "0", + y: typing.Union[str, float] = "0", + z: typing.Union[str, float] = "0", + e: typing.Union[str, float] = "0", + n: typing.Union[str, float] = "0", + h: typing.Union[str, float] = "0", + gn_angle: typing.Union[str, float] = "0", + rotate_angle: typing.Union[str, float] = "0", ): """Sets a false origin with a map conversion in a model @@ -62,14 +63,14 @@ class Patcher: self.file = file self.logger = logger self.name = name - self.x = x - self.y = y - self.z = z - self.e = e - self.n = n - self.h = h - self.gn_angle = gn_angle - self.rotate_angle = rotate_angle + self.x = float(x) + self.y = float(y) + self.z = float(z) + self.e = float(e) + self.n = float(n) + self.h = float(h) + self.gn_angle = float(gn_angle) + self.rotate_angle = float(rotate_angle) def patch(self): SetWorldCoordinateSystem.Patcher(self.src, self.file, self.logger, x=0, y=0, z=0, ax=0, ay=0, az=0).patch() diff --git a/src/ifcpatch/ifcpatch/recipes/SetRefElevation.py b/src/ifcpatch/ifcpatch/recipes/SetRefElevation.py index fc83f37ccf..cc1a092aff 100644 --- a/src/ifcpatch/ifcpatch/recipes/SetRefElevation.py +++ b/src/ifcpatch/ifcpatch/recipes/SetRefElevation.py @@ -16,9 +16,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcPatch. If not, see . +import typing + class Patcher: - def __init__(self, src, file, logger, elevation=0): + def __init__(self, src, file, logger, elevation: typing.Union[str, float] = "0"): """Sets the reference elevation of all IfcSites To completely reference model coordinates, a reference elevation should @@ -30,7 +32,7 @@ class Patcher: The reference elevation is simply a numerical attribute. :param elevation: The elevation to set. - :type elevation: float + :type elevation: typing.Union[str, float] Example: @@ -42,7 +44,7 @@ class Patcher: self.src = src self.file = file self.logger = logger - self.elevation = elevation + self.elevation = float(elevation) def patch(self): project = self.file.by_type("IfcProject")[0] diff --git a/src/ifcpatch/ifcpatch/recipes/SetWorldCoordinateSystem.py b/src/ifcpatch/ifcpatch/recipes/SetWorldCoordinateSystem.py index 33624ef169..ff8a4a4043 100644 --- a/src/ifcpatch/ifcpatch/recipes/SetWorldCoordinateSystem.py +++ b/src/ifcpatch/ifcpatch/recipes/SetWorldCoordinateSystem.py @@ -18,29 +18,41 @@ import math import numpy as np +import typing class Patcher: - def __init__(self, src, file, logger, x=None, y=None, z=None, ax=None, ay=None, az=None): + def __init__( + self, + src, + file, + logger, + x: typing.Union[str, float] = "0", + y: typing.Union[str, float] = "0", + z: typing.Union[str, float] = "0", + ax: typing.Union[str, float] = "0", + ay: typing.Union[str, float] = "0", + az: typing.Union[str, float] = "0", + ): """Sets the world coordinate system of the geometric representation context Sets the world coordinate system to whatever you want. :param x: The X coordinate. - :type x: float + :type x: typing.Union[str, float] :param y: The Y coordinate. - :type y: float + :type y: typing.Union[str, float] :param z: The Z coordinate. - :type z: float + :type z: typing.Union[str, float] :param ax: An angle to rotate by for 3D rotations along the X axis. Angles are in decimal degrees and positive is anticlockwise. - :type ax: float + :type ax: typing.Union[str, float] :param ay: An angle to rotate by for 3D rotations along the Y axis. Angles are in decimal degrees and positive is anticlockwise. - :type ay: float + :type ay: typing.Union[str, float] :param az: An angle to rotate by for 3D rotations along the Z axis. Angles are in decimal degrees and positive is anticlockwise. - :type az: float + :type az: typing.Union[str, float] Example: @@ -52,17 +64,17 @@ class Patcher: self.src = src self.file = file self.logger = logger - self.x = x - self.y = y - self.z = z - self.ax = ax - self.ay = ay - self.az = az + self.x = float(x) + self.y = float(y) + self.z = float(z) + self.ax = float(ax) + self.ay = float(ay) + self.az = float(az) def patch(self): rotate = self.identity_matrix() - for arg in (("x", float(self.ax)), ("y", float(self.ay)), ("z", float(self.az))): + for arg in (("x", self.ax), ("y", self.ay), ("z", self.az)): if arg[1]: rotate = getattr(self, f"{arg[0]}_rotation_matrix")(math.radians(arg[1]), rotate) @@ -74,13 +86,13 @@ class Patcher: for context in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.ContextType == "Model": context.WorldCoordinateSystem = self.file.createIfcAxis2Placement3D( - self.file.createIfcCartesianPoint((float(self.x), float(self.y), float(self.z))), + self.file.createIfcCartesianPoint((self.x, self.y, self.z)), self.file.createIfcDirection(z.tolist()), self.file.createIfcDirection(x.tolist()), ) elif context.ContextType == "Plan": context.WorldCoordinateSystem = self.file.createIfcAxis2Placement2D( - self.file.createIfcCartesianPoint((float(self.x), float(self.y), float(self.z))), + self.file.createIfcCartesianPoint((self.x, self.y, self.z)), self.file.createIfcDirection(x.tolist()), ) diff --git a/src/ifcpatch/test/test_MergeProject.py b/src/ifcpatch/test/test_MergeProject.py index dabbf1ce1b..3c4e84c4f2 100644 --- a/src/ifcpatch/test/test_MergeProject.py +++ b/src/ifcpatch/test/test_MergeProject.py @@ -18,8 +18,14 @@ import ifcpatch import ifcopenshell +import ifcopenshell.api.context +import ifcopenshell.api.geometry import ifcopenshell.api.georeference +import ifcopenshell.geom +import ifcopenshell.util.geolocation import ifcopenshell.util.placement +import ifcopenshell.util.representation +import ifcopenshell.util.shape import ifcopenshell.util.shape_builder import test.bootstrap import tempfile @@ -119,8 +125,8 @@ class TestMergeProject(test.bootstrap.IFC4): ifcopenshell.api.geometry.assign_representation(self.file, product=wall1, representation=rep) shape = ifcopenshell.geom.create_shape(ifcopenshell.geom.settings(), wall1) verts = ifcopenshell.util.shape.get_shape_vertices(shape, shape.geometry) - assert np.any(np.all(np.isclose(np.array((1., 2., 3.)), verts), axis=1)) - assert np.any(np.all(np.isclose(np.array((3., 4., 5.)), verts), axis=1)) + assert np.any(np.all(np.isclose(np.array((1.0, 2.0, 3.0)), verts), axis=1)) + assert np.any(np.all(np.isclose(np.array((3.0, 4.0, 5.0)), verts), axis=1)) # Second file is in millimeters with a different false origin wall1 = second_file.by_type("IfcWall")[0] @@ -134,8 +140,8 @@ class TestMergeProject(test.bootstrap.IFC4): ifcopenshell.api.geometry.assign_representation(second_file, product=wall1, representation=rep) shape = ifcopenshell.geom.create_shape(ifcopenshell.geom.settings(), wall1) verts = ifcopenshell.util.shape.get_shape_vertices(shape, shape.geometry) - assert np.any(np.all(np.isclose(np.array((1., 2., 3.)), verts), axis=1)) - assert np.any(np.all(np.isclose(np.array((3., 4., 5.)), verts), axis=1)) + assert np.any(np.all(np.isclose(np.array((1.0, 2.0, 3.0)), verts), axis=1)) + assert np.any(np.all(np.isclose(np.array((3.0, 4.0, 5.0)), verts), axis=1)) output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [second_file]}) @@ -155,17 +161,17 @@ class TestMergeProject(test.bootstrap.IFC4): m2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) assert np.allclose(m1[:, 3], (1, 2, 3, 1)) # assert np.allclose(m2[:, 3], (8.321, 29.321, 3, 1), atol=1e-3) - assert np.allclose(m2[:, 3], (17.847, 24.707, 3., 1.), atol=1e-3) + assert np.allclose(m2[:, 3], (17.847, 24.707, 3.0, 1.0), atol=1e-3) shape = ifcopenshell.geom.create_shape(ifcopenshell.geom.settings(), wall1) verts = ifcopenshell.util.shape.get_shape_vertices(shape, shape.geometry) - assert np.any(np.all(np.isclose(np.array((1., 2., 3.)), verts), axis=1)) - assert np.any(np.all(np.isclose(np.array((3., 4., 5.)), verts), axis=1)) + assert np.any(np.all(np.isclose(np.array((1.0, 2.0, 3.0)), verts), axis=1)) + assert np.any(np.all(np.isclose(np.array((3.0, 4.0, 5.0)), verts), axis=1)) shape = ifcopenshell.geom.create_shape(ifcopenshell.geom.settings(), wall2) verts = ifcopenshell.util.shape.get_shape_vertices(shape, shape.geometry) - assert np.any(np.all(np.isclose(np.array((17.847, 24.707, 3.)), verts, atol=1e-3), axis=1)) - assert np.any(np.all(np.isclose(np.array((20.410, 25.902, 5.)), verts, atol=1e-3), axis=1)) + assert np.any(np.all(np.isclose(np.array((17.847, 24.707, 3.0)), verts, atol=1e-3), axis=1)) + assert np.any(np.all(np.isclose(np.array((20.410, 25.902, 5.0)), verts, atol=1e-3), axis=1)) class TestMergeProjectIFC2X3(test.bootstrap.IFC2X3, TestMergeProject): diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index 11fe67d736..e7d75036b8 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -30,15 +30,34 @@ bl_info = { import importlib import logging +import types +import bpy logger = logging.getLogger("sverchok.ifc") +def get_blender_addon_package_name(addon_name: str) -> types.ModuleType: + # Check for legacy addons. + if addon_name in bpy.context.preferences.addons: + return importlib.import_module(addon_name) + elif bpy.app.version < (4, 2, 0): + raise ModuleNotFoundError + + # Check for Blender extensions. + bbim_package_name = None + for package_name in bpy.context.preferences.addons.keys(): + if package_name.endswith(f".{addon_name}"): + bbim_package_name = package_name + if bbim_package_name is None: + raise ModuleNotFoundError + return importlib.import_module(bbim_package_name) + + def ensure_addons_are_enabled(*addon_names: str) -> None: errors = [] for addon_name in addon_names: try: - module = importlib.import_module(addon_name) + module = get_blender_addon_package_name(addon_name) # `__addon_enabled__` is not present if addon wasn't enabled before if not getattr(module, "__addon_enabled__", False): errors.append(f"- Addon {addon_name} appears to be disabled, it should be enabled before IFC Sverchok.") @@ -115,7 +134,6 @@ imported_modules = make_node_list() reload_event = False -import bpy from os.path import splitext import ifcopenshell import ifcopenshell.api diff --git a/src/ifcsverchok/nodes/ifc/add_pset.py b/src/ifcsverchok/nodes/ifc/add_pset.py index e71e5961af..73f5e24d37 100644 --- a/src/ifcsverchok/nodes/ifc/add_pset.py +++ b/src/ifcsverchok/nodes/ifc/add_pset.py @@ -55,9 +55,9 @@ class SvIfcAddPset(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIf self.outputs.new("SvStringsSocket", "Entity") def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Add a property set and corresponding properties to IfcElements." + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Add a property set and corresponding properties to IfcElements." + ) def process(self): if not any(socket.is_linked for socket in self.outputs): @@ -83,9 +83,7 @@ class SvIfcAddPset(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIf def create(self, name, properties, elements): results = [] for element in elements: - result = ifcopenshell.api.run( - "pset.add_pset", self.file, product=element, name=name - ) + result = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name=name) ifcopenshell.api.run( "pset.edit_pset", self.file, diff --git a/src/ifcsverchok/nodes/ifc/api.py b/src/ifcsverchok/nodes/ifc/api.py index cf9784c31b..7429c8ac58 100644 --- a/src/ifcsverchok/nodes/ifc/api.py +++ b/src/ifcsverchok/nodes/ifc/api.py @@ -24,7 +24,8 @@ from bpy.props import StringProperty, EnumProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode import logging -logger = logging.getLogger('sverchok.ifc') + +logger = logging.getLogger("sverchok.ifc") def update_usecase(self, context): diff --git a/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py b/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py index 054171ca46..0b8afcbeb3 100644 --- a/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py +++ b/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py @@ -42,9 +42,7 @@ from sverchok.core.socket_data import sv_get_socket from itertools import chain, cycle -class SvIfcBMeshToIfcRepr( - bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore -): +class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): """ Triggers: BMesh to Ifc Repr Tooltip: Blender mesh to Ifc Shape Representation @@ -61,9 +59,7 @@ class SvIfcBMeshToIfcRepr( self.process() self.refresh_local = False - refresh_local: BoolProperty( - name="Update Node", description="Update Node", update=refresh_node - ) + refresh_local: BoolProperty(name="Update Node", description="Update Node", update=refresh_node) context_types = [ ("Model", "Model", "Context type: Model", 0), @@ -113,20 +109,16 @@ class SvIfcBMeshToIfcRepr( def sv_init(self, context): self.inputs.new("SvStringsSocket", "context_type").prop_name = "context_type" - self.inputs.new( - "SvStringsSocket", "context_identifier" - ).prop_name = "context_identifier" + self.inputs.new("SvStringsSocket", "context_identifier").prop_name = "context_identifier" self.inputs.new("SvStringsSocket", "target_view").prop_name = "target_view" - self.inputs.new( - "SvObjectSocket", "blender_objects" - ).prop_name = "blender_objects" # no prop for now + self.inputs.new("SvObjectSocket", "blender_objects").prop_name = "blender_objects" # no prop for now self.outputs.new("SvVerticesSocket", "Representations") self.outputs.new("SvMatrixSocket", "Locations") def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Blender mesh to Ifc Shape Representation. \nTakes one or multiple geometries.\nDeconstructs joined geometries and creates a separate representation for each." + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Blender mesh to Ifc Shape Representation. \nTakes one or multiple geometries.\nDeconstructs joined geometries and creates a separate representation for each." + ) row = layout.row(align=True) row.prop(self, "is_interactive", icon="SCENE_DATA", icon_only=True) @@ -136,9 +128,7 @@ class SvIfcBMeshToIfcRepr( self.sv_input_names = [i.name for i in self.inputs] if hash(self) not in self.node_dict: - self.node_dict[ - hash(self) - ] = {} # happens if node is already on canvas when blender loads + self.node_dict[hash(self)] = {} # happens if node is already on canvas when blender loads if not self.node_dict[hash(self)]: self.node_dict[hash(self)].update(dict.fromkeys(self.sv_input_names, 0)) @@ -211,21 +201,17 @@ class SvIfcBMeshToIfcRepr( context=context, ) if not representation: - raise Exception( - "Couldn't create representation. Possibly wrong context." - ) + raise Exception("Couldn't create representation. Possibly wrong context.") representations_ids_obj.append([representation.id()]) locations_obj.append([obj.matrix_world]) representations_ids.append(representations_ids_obj) locations.append(locations_obj) - SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault( - "Representations", [] - ).append(representations_ids_obj) - SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault( - "Locations", [] - ).append(locations_obj) + SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault("Representations", []).append( + representations_ids_obj + ) + SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault("Locations", []).append(locations_obj) bpy.ops.object.select_all(action="DESELECT") return representations_ids, locations @@ -248,13 +234,9 @@ class SvIfcBMeshToIfcRepr( self.file, self.context_type, self.context_identifier, self.target_view ) if not context: - parent = ifcopenshell.util.representation.get_context( - self.file, self.context_type - ) + parent = ifcopenshell.util.representation.get_context(self.file, self.context_type) if not parent: - parent = ifcopenshell.api.run( - "context.add_context", self.file, context_type=self.context_type - ) + parent = ifcopenshell.api.run("context.add_context", self.file, context_type=self.context_type) context = ifcopenshell.api.run( "context.add_context", self.file, @@ -263,9 +245,7 @@ class SvIfcBMeshToIfcRepr( target_view=self.target_view, parent=parent, ) - SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault( - "Contexts", [] - ).append(context.id()) + SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault("Contexts", []).append(context.id()) return context def sv_free(self): @@ -286,14 +266,10 @@ class SvIfcBMeshToIfcRepr( if not self.file.get_inverse(context): if self.file.by_id(context_id).ParentContext: parent = self.file.by_id(context_id).ParentContext - ifcopenshell.api.run( - "context.remove_context", self.file, context=context - ) + ifcopenshell.api.run("context.remove_context", self.file, context=context) if parent: if not self.file.get_inverse(parent): - ifcopenshell.api.run( - "context.remove_context", self.file, context=parent - ) + ifcopenshell.api.run("context.remove_context", self.file, context=parent) SvIfcStore.id_map[self.node_id]["Contexts"].remove(context_id) del SvIfcStore.id_map[self.node_id] del self.node_dict[hash(self)] diff --git a/src/ifcsverchok/nodes/ifc/by_guid.py b/src/ifcsverchok/nodes/ifc/by_guid.py index aa8b0b2177..65c5196fad 100644 --- a/src/ifcsverchok/nodes/ifc/by_guid.py +++ b/src/ifcsverchok/nodes/ifc/by_guid.py @@ -39,9 +39,9 @@ class SvIfcByGuid(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfc self.outputs.new("SvStringsSocket", "Entities") def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Get IFC element by guid. Takes one or multiple guids." + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Get IFC element by guid. Takes one or multiple guids." + ) def process(self): self.guids = flatten_data(self.inputs["guid"].sv_get(), target_level=1) diff --git a/src/ifcsverchok/nodes/ifc/by_id.py b/src/ifcsverchok/nodes/ifc/by_id.py index edc40a19a1..b42060fcf5 100644 --- a/src/ifcsverchok/nodes/ifc/by_id.py +++ b/src/ifcsverchok/nodes/ifc/by_id.py @@ -38,9 +38,9 @@ class SvIfcById(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCo self.outputs.new("SvStringsSocket", "Entities") def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Get IFC element by step id. Takes one or multiple step ids." + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Get IFC element by step id. Takes one or multiple step ids." + ) def process(self): self.ids = flatten_data(self.inputs["id"].sv_get(), target_level=1) diff --git a/src/ifcsverchok/nodes/ifc/by_type.py b/src/ifcsverchok/nodes/ifc/by_type.py index 15c500142d..96c9d84942 100644 --- a/src/ifcsverchok/nodes/ifc/by_type.py +++ b/src/ifcsverchok/nodes/ifc/by_type.py @@ -108,17 +108,15 @@ class SvIfcByType(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfc def sv_init(self, context): self.inputs.new("SvStringsSocket", "ifc_product").prop_name = "ifc_product" self.inputs.new("SvStringsSocket", "ifc_class").prop_name = "ifc_class" - self.inputs.new( - "SvStringsSocket", "custom_ifc_class" - ).prop_name = "custom_ifc_class" + self.inputs.new("SvStringsSocket", "custom_ifc_class").prop_name = "custom_ifc_class" self.outputs.new("SvStringsSocket", "Entities") self.outputs.new("SvStringsSocket", "Entity Ids") self.width = 200 def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Get IFC element(s) in file by type. \nPick an IfcProduct and an IfcClass or give a custom IfcClass." + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Get IFC element(s) in file by type. \nPick an IfcProduct and an IfcClass or give a custom IfcClass." + ) def process(self): self.file = SvIfcStore.get_file() diff --git a/src/ifcsverchok/nodes/ifc/create_entity.py b/src/ifcsverchok/nodes/ifc/create_entity.py index 6843336c61..f17b02a3c0 100644 --- a/src/ifcsverchok/nodes/ifc/create_entity.py +++ b/src/ifcsverchok/nodes/ifc/create_entity.py @@ -32,9 +32,7 @@ from sverchok.data_structure import ( ) -class SvIfcCreateEntity( - bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore -): +class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcCreateEntity" bl_label = "IFC Create Entity" node_dict = {} @@ -46,9 +44,7 @@ class SvIfcCreateEntity( self.process() self.refresh_local = False - refresh_local: BoolProperty( - name="Update Node", description="Update Node", update=refresh_node - ) + refresh_local: BoolProperty(name="Update Node", description="Update Node", update=refresh_node) Names: StringProperty( name="Names", @@ -76,18 +72,16 @@ class SvIfcCreateEntity( self.inputs.new("SvStringsSocket", "Names").prop_name = "Names" self.inputs.new("SvStringsSocket", "Descriptions").prop_name = "Descriptions" self.inputs.new("SvStringsSocket", "IfcClass").prop_name = "IfcClass" - self.inputs.new( - "SvStringsSocket", "Representations" - ).prop_name = "Representations" + self.inputs.new("SvStringsSocket", "Representations").prop_name = "Representations" self.inputs.new("SvMatrixSocket", "Locations").is_mandatory = False # self.inputs.new("SvStringsSocket", "Properties").prop_name = "Properties" self.outputs.new("SvStringsSocket", "Entities") self.node_dict[hash(self)] = {} def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Create IFC Entity. Takes one or multiple inputs. \nIf 'Representation(s)' is given, that determines number of output entities. Otherwise, 'Names' is used." + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Create IFC Entity. Takes one or multiple inputs. \nIf 'Representation(s)' is given, that determines number of output entities. Otherwise, 'Names' is used." + ) row = layout.row(align=True) row.prop(self, "is_interactive", icon="SCENE_DATA", icon_only=True) @@ -95,26 +89,16 @@ class SvIfcCreateEntity( def process(self): self.names = flatten_data(self.inputs["Names"].sv_get(), target_level=1) - self.descriptions = flatten_data( - self.inputs["Descriptions"].sv_get(), target_level=1 - ) - self.ifc_class = flatten_data(self.inputs["IfcClass"].sv_get(), target_level=1)[ - 0 - ] - self.representations = ensure_min_nesting( - self.inputs["Representations"].sv_get(), 3 - ) + self.descriptions = flatten_data(self.inputs["Descriptions"].sv_get(), target_level=1) + self.ifc_class = flatten_data(self.inputs["IfcClass"].sv_get(), target_level=1)[0] + self.representations = ensure_min_nesting(self.inputs["Representations"].sv_get(), 3) self.representations = flatten_data(self.representations, target_level=3) - self.locations = ensure_min_nesting( - self.inputs["Locations"].sv_get(default=[]), 3 - ) + self.locations = ensure_min_nesting(self.inputs["Locations"].sv_get(default=[]), 3) self.locations = flatten_data(self.locations, target_level=3) self.sv_input_names = [i.name for i in self.inputs] if hash(self) not in self.node_dict: - self.node_dict[ - hash(self) - ] = {} # happens if node is already on canvas when blender loads + self.node_dict[hash(self)] = {} # happens if node is already on canvas when blender loads if not self.node_dict[hash(self)]: self.node_dict[hash(self)].update(dict.fromkeys(self.sv_input_names, 0)) if not self.inputs["IfcClass"].sv_get()[0][0]: @@ -122,9 +106,7 @@ class SvIfcCreateEntity( edit = False for i in range(len(self.inputs)): - input = self.inputs[self.sv_input_names[i]].sv_get( - deepcopy=True, default=[] - ) + input = self.inputs[self.sv_input_names[i]].sv_get(deepcopy=True, default=[]) if ( isinstance(self.node_dict[hash(self)][self.inputs[i].name], list) and input != self.node_dict[hash(self)][self.inputs[i].name] @@ -142,27 +124,18 @@ class SvIfcCreateEntity( for group in self.representations: try: group_representations = [ - [self.file.by_id(step_id) for step_id in representation] - for representation in group + [self.file.by_id(step_id) for step_id in representation] for representation in group ] representations.append(group_representations) except Exception as e: raise - names.append( - self.repeat_input_unique(self.names, len(group_representations)) - ) - descriptions.append( - self.repeat_input_unique( - self.descriptions, len(group_representations) - ) - ) + names.append(self.repeat_input_unique(self.names, len(group_representations))) + descriptions.append(self.repeat_input_unique(self.descriptions, len(group_representations))) self.representations = representations self.names = names self.descriptions = descriptions elif not self.representations[0][0][0]: - self.descriptions = self.repeat_input_unique( - self.descriptions, len(self.names) - ) + self.descriptions = self.repeat_input_unique(self.descriptions, len(self.names)) self.names = ensure_min_nesting(self.names, 2) self.descriptions = ensure_min_nesting(self.descriptions, 2) if self.node_id not in SvIfcStore.id_map: @@ -193,7 +166,7 @@ class SvIfcCreateEntity( self.file, ifc_class=self.ifc_class, name=self.names[i][j], - #description=self.descriptions[i][j], + # description=self.descriptions[i][j], ) try: for repr in self.representations[i][j]: @@ -266,9 +239,7 @@ class SvIfcCreateEntity( pass if entity.is_a() != self.ifc_class: SvIfcStore.id_map[self.node_id][i].remove(step_id) - entity = ifcopenshell.util.schema.reassign_class( - self.file, entity, self.ifc_class - ) + entity = ifcopenshell.util.schema.reassign_class(self.file, entity, self.ifc_class) group_entities_ids.append(entity.id()) entities_ids.append(group_entities_ids) @@ -280,12 +251,10 @@ class SvIfcCreateEntity( if input[0]: if flag: return [ - [a] if not (s := sum(j == a for j in input[:i])) else [f"{a}-{s+1}"] - for i, a in enumerate(input) + [a] if not (s := sum(j == a for j in input[:i])) else [f"{a}-{s+1}"] for i, a in enumerate(input) ] input = [ - a if not (s := sum(j == a for j in input[:i])) else f"{a}-{s+1}" - for i, a in enumerate(input) + a if not (s := sum(j == a for j in input[:i])) else f"{a}-{s+1}" for i, a in enumerate(input) ] # add number to duplicates return input diff --git a/src/ifcsverchok/nodes/ifc/create_project.py b/src/ifcsverchok/nodes/ifc/create_project.py index 3f8b323e51..3048f7f936 100644 --- a/src/ifcsverchok/nodes/ifc/create_project.py +++ b/src/ifcsverchok/nodes/ifc/create_project.py @@ -35,9 +35,9 @@ class SvIfcCreateProject(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helpe self.outputs.new("SvVerticesSocket", "file") def draw_buttons(self, context, layout): - op = layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Adds project, unit and context to IFC file" + op = layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Adds project, unit and context to IFC file" + ) # op.tooltip = self.tooltip def process(self): diff --git a/src/ifcsverchok/nodes/ifc/create_shape.py b/src/ifcsverchok/nodes/ifc/create_shape.py index bfcc971b51..8fe5b63d73 100644 --- a/src/ifcsverchok/nodes/ifc/create_shape.py +++ b/src/ifcsverchok/nodes/ifc/create_shape.py @@ -54,9 +54,9 @@ class SvIfcCreateShape(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper. def draw_buttons(self, context, layout): row = layout.row(align=True) - row.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Create Blender shape from IfcEntity Id. Takes one or multiple IfcEntity IDs." + row.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Create Blender shape from IfcEntity Id. Takes one or multiple IfcEntity IDs." + ) row.prop(self, "refresh_local", icon="FILE_REFRESH") def process(self): diff --git a/src/ifcsverchok/nodes/ifc/get_attribute.py b/src/ifcsverchok/nodes/ifc/get_attribute.py index 3e7a343c10..c4ec65e30e 100644 --- a/src/ifcsverchok/nodes/ifc/get_attribute.py +++ b/src/ifcsverchok/nodes/ifc/get_attribute.py @@ -26,9 +26,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, flatten_data -class SvIfcGetAttribute( - bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore -): +class SvIfcGetAttribute(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcGetAttribute" bl_label = "IFC Get Attribute" entity: StringProperty(name="Entity Ids", update=updateNode) @@ -40,30 +38,22 @@ class SvIfcGetAttribute( def sv_init(self, context): self.inputs.new("SvStringsSocket", "entity").prop_name = "entity" - self.inputs.new( - "SvStringsSocket", "attribute_name" - ).prop_name = "attribute_name" + self.inputs.new("SvStringsSocket", "attribute_name").prop_name = "attribute_name" self.outputs.new("SvStringsSocket", "value") def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = ( + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( "Get the value of an attribute of an IfcEntity. Can take multiple entities." ) def process(self): self.value_out = [] - entity_nested_input_ids = flatten_data( - self.inputs["entity"].sv_get(), target_level=1 - ) + entity_nested_input_ids = flatten_data(self.inputs["entity"].sv_get(), target_level=1) if not entity_nested_input_ids[0]: return self.file = SvIfcStore.get_file() try: - entity_nested_inputs = [ - self.file.by_id(int(step_id)) for step_id in entity_nested_input_ids - ] + entity_nested_inputs = [self.file.by_id(int(step_id)) for step_id in entity_nested_input_ids] except Exception as e: raise Exception("Instance ID not found", e) attribute_name = self.inputs["attribute_name"].sv_get()[0][0] diff --git a/src/ifcsverchok/nodes/ifc/get_property.py b/src/ifcsverchok/nodes/ifc/get_property.py index 47afde3862..a9a78e15b9 100644 --- a/src/ifcsverchok/nodes/ifc/get_property.py +++ b/src/ifcsverchok/nodes/ifc/get_property.py @@ -26,9 +26,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, flatten_data -class SvIfcGetProperty( - bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore -): +class SvIfcGetProperty(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcGetProperty" bl_label = "IFC Get Property" entity: StringProperty(name="Entity Ids", update=updateNode) @@ -50,9 +48,7 @@ class SvIfcGetProperty( self.outputs.new("SvStringsSocket", "value") def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = ( + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( "Get the value of a property of an IfcEntity. Can take multiple entity ids." ) @@ -72,9 +68,7 @@ class SvIfcGetProperty( self.value_out = [] for entity in self.entities: try: - self.value_out.append( - ifcopenshell.util.element.get_psets(entity)[pset_name][prop_name] - ) + self.value_out.append(ifcopenshell.util.element.get_psets(entity)[pset_name][prop_name]) except: pass self.outputs["value"].sv_set(self.value_out) diff --git a/src/ifcsverchok/nodes/ifc/quick_project_setup.py b/src/ifcsverchok/nodes/ifc/quick_project_setup.py index 435f8150bb..696d6d6515 100644 --- a/src/ifcsverchok/nodes/ifc/quick_project_setup.py +++ b/src/ifcsverchok/nodes/ifc/quick_project_setup.py @@ -60,9 +60,9 @@ class SvIfcQuickProjectSetup(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h self.outputs.new("SvVerticesSocket", "file") def draw_buttons(self, context, layout): - op = layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Quick Project Setup: creates Ifc file and sets up a basic project" + op = layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Quick Project Setup: creates Ifc file and sets up a basic project" + ) # op.tooltip = self.tooltip def process(self): diff --git a/src/ifcsverchok/nodes/ifc/read_entity.py b/src/ifcsverchok/nodes/ifc/read_entity.py index 919e740e73..fb581d1b63 100644 --- a/src/ifcsverchok/nodes/ifc/read_entity.py +++ b/src/ifcsverchok/nodes/ifc/read_entity.py @@ -25,9 +25,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, ensure_min_nesting, flatten_data -class SvIfcReadEntity( - bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore -): +class SvIfcReadEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcReadEntity" bl_label = "IFC Read Entity" entity: StringProperty(name="Entity Id", update=updateNode) @@ -39,9 +37,7 @@ class SvIfcReadEntity( self.outputs.new("SvStringsSocket", "is_a") def draw_buttons(self, context, layout): - layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = ( + layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( "Decompose an IfcEntity into its attributes. Takes one entity id as input" ) diff --git a/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py b/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py index 3cc528984b..dfedcf1f4a 100644 --- a/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py +++ b/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py @@ -30,9 +30,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, ensure_min_nesting -class SvIfcSverchokToIfcRepr( - bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore -): +class SvIfcSverchokToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): """ Triggers: Sv to Ifc Repr Tooltip: Sverchok geometry to Ifc Shape Representation @@ -85,9 +83,7 @@ class SvIfcSverchokToIfcRepr( def sv_init(self, context): self.inputs.new("SvStringsSocket", "context_type").prop_name = "context_type" - self.inputs.new( - "SvStringsSocket", "context_identifier" - ).prop_name = "context_identifier" + self.inputs.new("SvStringsSocket", "context_identifier").prop_name = "context_identifier" self.inputs.new("SvStringsSocket", "target_view").prop_name = "target_view" self.inputs.new("SvVerticesSocket", "Vertices") self.inputs.new("SvStringsSocket", "Edges") @@ -97,9 +93,9 @@ class SvIfcSverchokToIfcRepr( self.node_dict[hash(self)] = {} def draw_buttons(self, context, layout): - op = layout.operator( - "node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False - ).tooltip = "Sverchok geometry to Ifc Shape Representation. \nTakes one or multiple geometries." + op = layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( + "Sverchok geometry to Ifc Shape Representation. \nTakes one or multiple geometries." + ) def process(self): if not any(socket.is_linked for socket in self.inputs): @@ -108,9 +104,7 @@ class SvIfcSverchokToIfcRepr( self.sv_input_names = [i.name for i in self.inputs] if hash(self) not in self.node_dict: - self.node_dict[ - hash(self) - ] = {} # happens if node is already on canvas when blender loads + self.node_dict[hash(self)] = {} # happens if node is already on canvas when blender loads if not self.node_dict[hash(self)]: self.node_dict[hash(self)].update(dict.fromkeys(self.sv_input_names, 0)) @@ -124,9 +118,7 @@ class SvIfcSverchokToIfcRepr( edit = True self.node_dict[hash(self)][self.inputs[i].name] = input - self.vertices = ensure_min_nesting( - self.inputs["Vertices"].sv_get(deepcopy=False), 4 - ) + self.vertices = ensure_min_nesting(self.inputs["Vertices"].sv_get(deepcopy=False), 4) self.edges = ensure_min_nesting(self.inputs["Edges"].sv_get(deepcopy=False), 4) self.faces = ensure_min_nesting(self.inputs["Faces"].sv_get(deepcopy=False), 4) data = list(zip(self.vertices, self.edges, self.faces)) @@ -160,14 +152,12 @@ class SvIfcSverchokToIfcRepr( faces=[list(map(tuple, item[2]))], ) if not representation: - raise Exception( - "Couldn't create representation. Possibly wrong context." - ) + raise Exception("Couldn't create representation. Possibly wrong context.") representations_ids_obj.append([representation.id()]) representations_ids.append(representations_ids_obj) - SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault( - "Representations", [] - ).append(representations_ids_obj) + SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault("Representations", []).append( + representations_ids_obj + ) return representations_ids def edit(self): @@ -188,13 +178,9 @@ class SvIfcSverchokToIfcRepr( self.file, self.context_type, self.context_identifier, self.target_view ) if not context: - parent = ifcopenshell.util.representation.get_context( - self.file, self.context_type - ) + parent = ifcopenshell.util.representation.get_context(self.file, self.context_type) if not parent: - parent = ifcopenshell.api.run( - "context.add_context", self.file, context_type=self.context_type - ) + parent = ifcopenshell.api.run("context.add_context", self.file, context_type=self.context_type) context = ifcopenshell.api.run( "context.add_context", self.file, @@ -203,9 +189,7 @@ class SvIfcSverchokToIfcRepr( target_view=self.target_view, parent=parent, ) - SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault( - "Contexts", [] - ).append(context.id()) + SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault("Contexts", []).append(context.id()) return context def sv_free(self): @@ -225,14 +209,10 @@ class SvIfcSverchokToIfcRepr( if not self.file.get_inverse(context): if self.file.by_id(context_id).ParentContext: parent = self.file.by_id(context_id).ParentContext - ifcopenshell.api.run( - "context.remove_context", self.file, context=context - ) + ifcopenshell.api.run("context.remove_context", self.file, context=context) if parent: if not self.file.get_inverse(parent): - ifcopenshell.api.run( - "context.remove_context", self.file, context=parent - ) + ifcopenshell.api.run("context.remove_context", self.file, context=parent) # print("Removed context with step ID: ", context_id) SvIfcStore.id_map[self.node_id]["Contexts"].remove(context_id) del SvIfcStore.id_map[self.node_id] diff --git a/src/ifctester/ifctester/__init__.py b/src/ifctester/ifctester/__init__.py index 2cc1599de4..edb1db90e4 100644 --- a/src/ifctester/ifctester/__init__.py +++ b/src/ifctester/ifctester/__init__.py @@ -17,4 +17,5 @@ # along with IfcTester. If not, see . from .ids import open + __version__ = version = "0.0.0" diff --git a/src/ifctester/ifctester/__main__.py b/src/ifctester/ifctester/__main__.py index 30d1db517d..a61e2e0b7d 100644 --- a/src/ifctester/ifctester/__main__.py +++ b/src/ifctester/ifctester/__main__.py @@ -27,18 +27,10 @@ from . import reporter parser = argparse.ArgumentParser(description="Uses an IDS to audit an IFC") parser.add_argument("ids", type=str, help="Path to an IDS") parser.add_argument("ifc", type=str, help="Path to an IFC", nargs="?") -parser.add_argument( - "-r", "--reporter", type=str, help="The reporting method to view audit results", default="Console" -) -parser.add_argument( - "--no-color", help="Disable colour output (supported by Console reporting)", action="store_true" -) -parser.add_argument( - "--excel-safe", help="Make sure exported ODS is safely exported for Excel", action="store_true" -) -parser.add_argument( - "-o", "--output", help="Output file (supported for all types of reporting except Console)" -) +parser.add_argument("-r", "--reporter", type=str, help="The reporting method to view audit results", default="Console") +parser.add_argument("--no-color", help="Disable colour output (supported by Console reporting)", action="store_true") +parser.add_argument("--excel-safe", help="Make sure exported ODS is safely exported for Excel", action="store_true") +parser.add_argument("-o", "--output", help="Output file (supported for all types of reporting except Console)") args = parser.parse_args() specs = ids.open(args.ids) diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index db4b7b5e90..779d3376e7 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -444,7 +444,6 @@ class Html(Json): requirement["total_omitted_passes"] = total_passed_entities - entity_limit requirement["has_omitted_passes"] = total_passed_entities > entity_limit - def to_string(self) -> str: import pystache diff --git a/src/opencdeserver/api/app/api/bcf.py b/src/opencdeserver/api/app/api/bcf.py index 802bd51b70..30d0d5135c 100644 --- a/src/opencdeserver/api/app/api/bcf.py +++ b/src/opencdeserver/api/app/api/bcf.py @@ -1,5 +1,5 @@ from uuid import UUID -from fastapi import APIRouter, Depends, UploadFile, HTTPException +from fastapi import APIRouter, Depends, UploadFile, HTTPException from fastapi.responses import FileResponse from security.secure import get_current_active_user @@ -46,38 +46,40 @@ router = APIRouter(route_class=LoggingRoute) @router.get("/bcf/3.0/projects", tags=["projects_get"]) def projects_get(current_user: User = Depends(get_current_active_user)) -> List[ProjectGET]: projects_response = bcf_db.get_projects(current_user) - bcf_db.debug(endpoint='projects_get', - request={}, - response={count: value.dict() for count, value in enumerate(projects_response)}) + bcf_db.debug( + endpoint="projects_get", + request={}, + response={count: value.dict() for count, value in enumerate(projects_response)}, + ) return projects_response @router.get("/bcf/3.0/projects/{project_id}", tags=["project_get"]) def project_get(project_id: UUID, current_user: User = Depends(get_current_active_user)) -> ProjectGET: project_response = bcf_db.get_project(project_id, current_user) - bcf_db.debug(endpoint='project_get', - request={'project_id': project_id}, - response=project_response.dict()) + bcf_db.debug(endpoint="project_get", request={"project_id": project_id}, response=project_response.dict()) return project_response @router.put("/bcf/3.0/projects/{project_id}", tags=["project_put"], status_code=200) -def project_put(project_id: UUID, project_request: ProjectPUT, - current_user: User = Depends(get_current_active_user)) -> ProjectGET: +def project_put( + project_id: UUID, project_request: ProjectPUT, current_user: User = Depends(get_current_active_user) +) -> ProjectGET: project_response = bcf_db.put_project(project_id, project_request, current_user) - bcf_db.debug(endpoint='project_put', - request={'project_id': project_id, 'project_request': project_request}, - response=project_response.dict()) + bcf_db.debug( + endpoint="project_put", + request={"project_id": project_id, "project_request": project_request}, + response=project_response.dict(), + ) return project_response @router.get("/bcf/3.0/projects/{project_id}/extensions", tags=["project_extensions_get"]) -def project_extensions_get(project_id: UUID, - current_user: User = Depends(get_current_active_user)) -> ExtensionsGET: +def project_extensions_get(project_id: UUID, current_user: User = Depends(get_current_active_user)) -> ExtensionsGET: extensions_response = bcf_db.get_project_extensions(project_id, current_user) - bcf_db.debug(endpoint='project_extensions_get', - request={'project_id': project_id}, - response=extensions_response.dict()) + bcf_db.debug( + endpoint="project_extensions_get", request={"project_id": project_id}, response=extensions_response.dict() + ) return extensions_response @@ -86,62 +88,68 @@ def project_extensions_get(project_id: UUID, # # Topics + @router.get("/bcf/3.0/projects/{project_id}/topics", tags=["topics_get"]) -def topics_get(project_id: str, - current_user: User = Depends(get_current_active_user)) -> List[TopicGET]: +def topics_get(project_id: str, current_user: User = Depends(get_current_active_user)) -> List[TopicGET]: topics_response = bcf_db.get_topics(project_id, current_user) - bcf_db.debug(endpoint='topics_get', - request={'project_id': project_id}, - response={count: value.dict() for count, value in enumerate(topics_response)}) + bcf_db.debug( + endpoint="topics_get", + request={"project_id": project_id}, + response={count: value.dict() for count, value in enumerate(topics_response)}, + ) return topics_response @router.post("/bcf/3.0/projects/{project_id}/topics", tags=["topic_post"], status_code=201) -def topic_post(project_id: UUID, topic_request: TopicPOST, - current_user: User = Depends(get_current_active_user)) -> TopicGET: +def topic_post( + project_id: UUID, topic_request: TopicPOST, current_user: User = Depends(get_current_active_user) +) -> TopicGET: topic_response = bcf_db.post_topic(project_id, topic_request, current_user) if topic_response is None: raise HTTPException(status_code=400, detail="Could not create topic.") - bcf_db.debug(endpoint='topic_post', - request={'project_id': project_id, 'topic_request': topic_request.dict()}, - response=topic_response.dict()) + bcf_db.debug( + endpoint="topic_post", + request={"project_id": project_id, "topic_request": topic_request.dict()}, + response=topic_response.dict(), + ) return topic_response # Implemented @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}", tags=["topic_get"]) -def topic_get(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> TopicGET: +def topic_get(project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user)) -> TopicGET: topic_response = bcf_db.get_topic(project_id, topic_id, current_user) if topic_response is None: raise HTTPException(status_code=404, detail="Item not found.") - bcf_db.debug(endpoint='topic_get', - request={'project_id': project_id, 'topic_id': topic_id}, - response=topic_response.dict()) + bcf_db.debug( + endpoint="topic_get", request={"project_id": project_id, "topic_id": topic_id}, response=topic_response.dict() + ) return topic_response # Implemented @router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}", tags=["topic_put"], status_code=200) -def topic_put(project_id: UUID, topic_id: UUID, topic_request: TopicPUT, - current_user: User = Depends(get_current_active_user)) -> TopicGET: +def topic_put( + project_id: UUID, topic_id: UUID, topic_request: TopicPUT, current_user: User = Depends(get_current_active_user) +) -> TopicGET: topic_response = bcf_db.put_topic(project_id, topic_id, topic_request, current_user) - bcf_db.debug(endpoint='topic_put', - request={'project_id': project_id, 'topic_id': topic_id, 'topic_request': topic_request.dict()}, - response=topic_response.dict()) + bcf_db.debug( + endpoint="topic_put", + request={"project_id": project_id, "topic_id": topic_id, "topic_request": topic_request.dict()}, + response=topic_response.dict(), + ) return topic_response # Implemented @router.delete("/bcf/3.0/projects/{project_id}/topics/{topic_id}", tags=["topic_delete"], status_code=200) -def topic_delete(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> int: +def topic_delete(project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user)) -> int: topic_response = bcf_db.delete_topic(project_id, topic_id, current_user) if topic_response is None: raise HTTPException(status_code=404, detail="Item not found.") - bcf_db.debug(endpoint='topic_delete', - request={'project_id': project_id, 'topic_id': topic_id}, - response={topic_response}) + bcf_db.debug( + endpoint="topic_delete", request={"project_id": project_id, "topic_id": topic_id}, response={topic_response} + ) return topic_response @@ -153,25 +161,31 @@ def topic_delete(project_id: UUID, topic_id: UUID, # Implemented @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/snippet", tags=["bim_snippet_get"]) -def bim_snippet_get(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> BimSnippet: +def bim_snippet_get( + project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user) +) -> BimSnippet: bim_snippet_response = bcf_db.get_bim_snippet(project_id, topic_id, current_user) if bim_snippet_response is None: raise HTTPException(status_code=404, detail="Item not found.") - bcf_db.debug(endpoint='bim_snippet_get', - request={'project_id': project_id, 'topic_id': topic_id}, - response=bim_snippet_response.dict()) + bcf_db.debug( + endpoint="bim_snippet_get", + request={"project_id": project_id, "topic_id": topic_id}, + response=bim_snippet_response.dict(), + ) return bim_snippet_response # Implemented @router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/snippet", tags=["bim_snippet_put"], status_code=200) -def bim_snippet_put(project_id: UUID, topic_id: UUID, snippet: BimSnippet, - current_user: User = Depends(get_current_active_user)) -> BimSnippet: +def bim_snippet_put( + project_id: UUID, topic_id: UUID, snippet: BimSnippet, current_user: User = Depends(get_current_active_user) +) -> BimSnippet: bim_snippet_response = bcf_db.put_bim_snippet(project_id, topic_id, snippet, current_user) - bcf_db.debug(endpoint='bim_snippet_put', - request={'project_id': project_id, 'topic_id': topic_id, 'snippet': snippet.dict()}, - response=bim_snippet_response.dict()) + bcf_db.debug( + endpoint="bim_snippet_put", + request={"project_id": project_id, "topic_id": topic_id, "snippet": snippet.dict()}, + response=bim_snippet_response.dict(), + ) return bim_snippet_response @@ -182,36 +196,45 @@ def bim_snippet_put(project_id: UUID, topic_id: UUID, snippet: BimSnippet, @router.get("/bcf/3.0/projects/{project_id}/files_information", tags=["files_information_get"]) -def files_information_get(project_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[ProjectFileInformation]: +def files_information_get( + project_id: UUID, current_user: User = Depends(get_current_active_user) +) -> List[ProjectFileInformation]: files_information_response = bcf_db.get_files_information(project_id, current_user) - bcf_db.debug(endpoint='files_information_get', - request={'project_id': project_id}, - response={count: value.dict() for count, value in enumerate(files_information_response)}) + bcf_db.debug( + endpoint="files_information_get", + request={"project_id": project_id}, + response={count: value.dict() for count, value in enumerate(files_information_response)}, + ) return files_information_response # Implemented @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/files", tags=["files_get"]) -def files_get(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[FileGET]: +def files_get(project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user)) -> List[FileGET]: files_response = bcf_db.get_files(project_id, topic_id, current_user) - bcf_db.debug(endpoint='files_get', - request={'project_id': project_id, 'topic_id': topic_id}, - response={count: value.dict() for count, value in enumerate(files_response)}) + bcf_db.debug( + endpoint="files_get", + request={"project_id": project_id, "topic_id": topic_id}, + response={count: value.dict() for count, value in enumerate(files_response)}, + ) return files_response # request body file = FilePUT @router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/files", tags=["files_put"], status_code=200) -def files_put(project_id: UUID, topic_id: UUID, files: List[FilePUT], - current_user: User = Depends(get_current_active_user)) -> List[FileGET]: +def files_put( + project_id: UUID, topic_id: UUID, files: List[FilePUT], current_user: User = Depends(get_current_active_user) +) -> List[FileGET]: files_response = bcf_db.put_files(project_id, topic_id, files, current_user) - bcf_db.debug(endpoint='files_put', - request={'project_id': project_id, - 'topic_id': topic_id, - 'files': {count: value.dict() for count, value in enumerate(files)}}, - response={count: value.dict() for count, value in enumerate(files_response)}) + bcf_db.debug( + endpoint="files_put", + request={ + "project_id": project_id, + "topic_id": topic_id, + "files": {count: value.dict() for count, value in enumerate(files)}, + }, + response={count: value.dict() for count, value in enumerate(files_response)}, + ) return files_response @@ -222,58 +245,80 @@ def files_put(project_id: UUID, topic_id: UUID, files: List[FilePUT], @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments", tags=["comments_get"]) -def comments_get(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[CommentGET]: +def comments_get( + project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user) +) -> List[CommentGET]: comments_response = bcf_db.get_comments(project_id, topic_id, current_user) - bcf_db.debug(endpoint='comments_get', - request={'project_id': project_id, 'topic_id': topic_id}, - response={count: value.dict() for count, value in enumerate(comments_response)}) + bcf_db.debug( + endpoint="comments_get", + request={"project_id": project_id, "topic_id": topic_id}, + response={count: value.dict() for count, value in enumerate(comments_response)}, + ) return comments_response # request body comment = CommentPOST @router.post("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments", tags=["comment_post"], status_code=201) -def comment_post(project_id: UUID, topic_id: UUID, comment: CommentPOST, - current_user: User = Depends(get_current_active_user)) -> CommentGET: +def comment_post( + project_id: UUID, topic_id: UUID, comment: CommentPOST, current_user: User = Depends(get_current_active_user) +) -> CommentGET: comment_response = bcf_db.post_comment(project_id, topic_id, comment, current_user) - bcf_db.debug(endpoint='comment_post', - request={'project_id': project_id, 'topic_id': topic_id, 'comment': comment}, - response=comment_response.dict()) + bcf_db.debug( + endpoint="comment_post", + request={"project_id": project_id, "topic_id": topic_id, "comment": comment}, + response=comment_response.dict(), + ) return comment_response @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", tags=["comment_get"]) -def comment_get(project_id: UUID, topic_id: UUID, comment_id: UUID, - current_user: User = Depends(get_current_active_user)) -> CommentGET: +def comment_get( + project_id: UUID, topic_id: UUID, comment_id: UUID, current_user: User = Depends(get_current_active_user) +) -> CommentGET: comment_response = bcf_db.get_comment(project_id, topic_id, comment_id, current_user) - bcf_db.debug(endpoint='comment_get', - request={'project_id': project_id, 'topic_id': topic_id, 'comment_id': comment_id}, - response=comment_response.dict()) + bcf_db.debug( + endpoint="comment_get", + request={"project_id": project_id, "topic_id": topic_id, "comment_id": comment_id}, + response=comment_response.dict(), + ) return comment_response # request body comment = CommentPUT -@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", tags=["comment_put"], status_code=200) -def comment_put(project_id: UUID, topic_id: UUID, comment_id: UUID, comment: CommentPUT, - current_user: User = Depends(get_current_active_user)) -> CommentGET: +@router.put( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", tags=["comment_put"], status_code=200 +) +def comment_put( + project_id: UUID, + topic_id: UUID, + comment_id: UUID, + comment: CommentPUT, + current_user: User = Depends(get_current_active_user), +) -> CommentGET: comment_response = bcf_db.put_comment(project_id, topic_id, comment_id, comment, current_user) - bcf_db.debug(endpoint='comment_put', - request={'project_id': project_id, 'topic_id': topic_id, 'comment': comment}, - response=comment_response.dict()) + bcf_db.debug( + endpoint="comment_put", + request={"project_id": project_id, "topic_id": topic_id, "comment": comment}, + response=comment_response.dict(), + ) return comment_response # Implemented -@router.delete("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", - tags=["comment_delete"], status_code=200) -def comment_delete(project_id: UUID, topic_id: UUID, comment_id: UUID, - current_user: User = Depends(get_current_active_user)) -> int: +@router.delete( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", tags=["comment_delete"], status_code=200 +) +def comment_delete( + project_id: UUID, topic_id: UUID, comment_id: UUID, current_user: User = Depends(get_current_active_user) +) -> int: comment_response = bcf_db.delete_comment(project_id, topic_id, comment_id, current_user) if comment_response is None: raise HTTPException(status_code=404, detail="Item not found.") - bcf_db.debug(endpoint='comment_delete', - request={'project_id': project_id, 'topic_id': topic_id, 'comment_id': comment_id}, - response={comment_response}) + bcf_db.debug( + endpoint="comment_delete", + request={"project_id": project_id, "topic_id": topic_id, "comment_id": comment_id}, + response={comment_response}, + ) return comment_response @@ -284,113 +329,160 @@ def comment_delete(project_id: UUID, topic_id: UUID, comment_id: UUID, @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints", tags=["viewpoints_get"]) -def viewpoints_get(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[ViewpointGET]: +def viewpoints_get( + project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user) +) -> List[ViewpointGET]: viewpoints_response = bcf_db.get_viewpoints(project_id, topic_id, current_user) - bcf_db.debug(endpoint='viewpoints_get', - request={'project_id': project_id, 'topic_id': topic_id}, - response={count: value.dict() for count, value in enumerate(viewpoints_response)}) + bcf_db.debug( + endpoint="viewpoints_get", + request={"project_id": project_id, "topic_id": topic_id}, + response={count: value.dict() for count, value in enumerate(viewpoints_response)}, + ) return viewpoints_response # request body viewpoint = viewpointPOST @router.post("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints", tags=["viewpoint_post"], status_code=201) -def viewpoint_post(project_id: UUID, topic_id: UUID, viewpoint: ViewpointPOST, - current_user: User = Depends(get_current_active_user)) -> ViewpointGET: +def viewpoint_post( + project_id: UUID, topic_id: UUID, viewpoint: ViewpointPOST, current_user: User = Depends(get_current_active_user) +) -> ViewpointGET: viewpoint_response = bcf_db.post_viewpoint(project_id, topic_id, viewpoint, current_user) - bcf_db.debug(endpoint='viewpoint_post', - request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint': viewpoint.dict()}, - response=viewpoint_response.dict()) + bcf_db.debug( + endpoint="viewpoint_post", + request={"project_id": project_id, "topic_id": topic_id, "viewpoint": viewpoint.dict()}, + response=viewpoint_response.dict(), + ) return viewpoint_response @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}", tags=["viewpoint_get"]) -def viewpoint_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User = Depends(get_current_active_user)) -> ViewpointGET: +def viewpoint_get( + project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User = Depends(get_current_active_user) +) -> ViewpointGET: viewpoint_response = bcf_db.get_viewpoint(project_id, topic_id, viewpoint_id, current_user) - bcf_db.debug(endpoint='viewpoint_get', - request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id}, - response=viewpoint_response.dict()) + bcf_db.debug( + endpoint="viewpoint_get", + request={"project_id": project_id, "topic_id": topic_id, "viewpoint_id": viewpoint_id}, + response=viewpoint_response.dict(), + ) return viewpoint_response -@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot", - tags=["viewpoint_snapshot_get"]) -async def viewpoint_snapshot_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User = Depends(get_current_active_user)) -> FileResponse: +@router.get( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot", + tags=["viewpoint_snapshot_get"], +) +async def viewpoint_snapshot_get( + project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User = Depends(get_current_active_user) +) -> FileResponse: viewpoint_snapshot_response = bcf_db.get_viewpoint_snapshot(project_id, topic_id, viewpoint_id, current_user) - bcf_db.debug(endpoint='viewpoint_snapshot_get', - request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id}, - response=viewpoint_snapshot_response) - snapshot_name = 'snapshot_' + str(viewpoint_id) - file_ending = '.' + viewpoint_snapshot_response.split('/', 2)[1] - snapshot_path = 'data/snapshots/' + snapshot_name + file_ending + bcf_db.debug( + endpoint="viewpoint_snapshot_get", + request={"project_id": project_id, "topic_id": topic_id, "viewpoint_id": viewpoint_id}, + response=viewpoint_snapshot_response, + ) + snapshot_name = "snapshot_" + str(viewpoint_id) + file_ending = "." + viewpoint_snapshot_response.split("/", 2)[1] + snapshot_path = "data/snapshots/" + snapshot_name + file_ending snapshot_type = viewpoint_snapshot_response - return FileResponse(path=snapshot_path, - media_type=snapshot_type) + return FileResponse(path=snapshot_path, media_type=snapshot_type) -@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}", - tags=["viewpoint_bitmap_get"]) -async def viewpoint_bitmap_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, bitmap_id: UUID, - current_user: User = Depends(get_current_active_user)) -> FileResponse: +@router.get( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}", + tags=["viewpoint_bitmap_get"], +) +async def viewpoint_bitmap_get( + project_id: UUID, + topic_id: UUID, + viewpoint_id: UUID, + bitmap_id: UUID, + current_user: User = Depends(get_current_active_user), +) -> FileResponse: viewpoint_bitmap_response = bcf_db.get_viewpoint_bitmap(project_id, topic_id, viewpoint_id, bitmap_id, current_user) - bcf_db.debug(endpoint='viewpoint_bitmap_get', - request={'project_id': project_id, 'topic_id': topic_id, - 'viewpoint_id': viewpoint_id, 'bitmap_id': bitmap_id}, - response=viewpoint_bitmap_response.dict()) - bitmap_name = 'bitmap_' + str(viewpoint_id) - file_ending = '.' + viewpoint_bitmap_response['bitmap_type'].split('/', 2)[1] - bitmap_path = 'data/bitmaps/' + bitmap_name + file_ending - bitmap_type = viewpoint_bitmap_response['bitmap_type'] - return FileResponse(path=bitmap_path, - media_type=bitmap_type) + bcf_db.debug( + endpoint="viewpoint_bitmap_get", + request={"project_id": project_id, "topic_id": topic_id, "viewpoint_id": viewpoint_id, "bitmap_id": bitmap_id}, + response=viewpoint_bitmap_response.dict(), + ) + bitmap_name = "bitmap_" + str(viewpoint_id) + file_ending = "." + viewpoint_bitmap_response["bitmap_type"].split("/", 2)[1] + bitmap_path = "data/bitmaps/" + bitmap_name + file_ending + bitmap_type = viewpoint_bitmap_response["bitmap_type"] + return FileResponse(path=bitmap_path, media_type=bitmap_type) -@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring", - tags=["viewpoint_colored_components_get"]) -def viewpoint_colored_components_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User = Depends(get_current_active_user)) -> ColoringGET: - viewpoint_colored_components_response = bcf_db.get_viewpoint_colored_components(project_id, topic_id, viewpoint_id, current_user) - bcf_db.debug(endpoint='viewpoint_colored_components_get', - request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id}, - response=viewpoint_colored_components_response.dict()) +@router.get( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring", + tags=["viewpoint_colored_components_get"], +) +def viewpoint_colored_components_get( + project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User = Depends(get_current_active_user) +) -> ColoringGET: + viewpoint_colored_components_response = bcf_db.get_viewpoint_colored_components( + project_id, topic_id, viewpoint_id, current_user + ) + bcf_db.debug( + endpoint="viewpoint_colored_components_get", + request={"project_id": project_id, "topic_id": topic_id, "viewpoint_id": viewpoint_id}, + response=viewpoint_colored_components_response.dict(), + ) return viewpoint_colored_components_response -@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection", - tags=["viewpoint_selected_components_get"]) -def viewpoint_selected_components_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User = Depends(get_current_active_user)) -> SelectionGET: - viewpoint_selected_components_response = bcf_db.get_viewpoint_selected_components(project_id, topic_id, viewpoint_id, current_user) - bcf_db.debug(endpoint='viewpoint_selected_components_get', - request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id}, - response=viewpoint_selected_components_response.dict()) +@router.get( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection", + tags=["viewpoint_selected_components_get"], +) +def viewpoint_selected_components_get( + project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User = Depends(get_current_active_user) +) -> SelectionGET: + viewpoint_selected_components_response = bcf_db.get_viewpoint_selected_components( + project_id, topic_id, viewpoint_id, current_user + ) + bcf_db.debug( + endpoint="viewpoint_selected_components_get", + request={"project_id": project_id, "topic_id": topic_id, "viewpoint_id": viewpoint_id}, + response=viewpoint_selected_components_response.dict(), + ) return viewpoint_selected_components_response -@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility", - tags=["viewpoint_components_visibility_get"]) -def viewpoint_components_visibility_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User = Depends(get_current_active_user)) -> VisibilityGET: - viewpoint_components_visibility_response = bcf_db.get_viewpoint_components_visibility(project_id, topic_id, viewpoint_id, current_user) - bcf_db.debug(endpoint='viewpoint_components_visibility_get', - request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id}, - response=viewpoint_components_visibility_response.dict()) +@router.get( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility", + tags=["viewpoint_components_visibility_get"], +) +def viewpoint_components_visibility_get( + project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User = Depends(get_current_active_user) +) -> VisibilityGET: + viewpoint_components_visibility_response = bcf_db.get_viewpoint_components_visibility( + project_id, topic_id, viewpoint_id, current_user + ) + bcf_db.debug( + endpoint="viewpoint_components_visibility_get", + request={"project_id": project_id, "topic_id": topic_id, "viewpoint_id": viewpoint_id}, + response=viewpoint_components_visibility_response.dict(), + ) return viewpoint_components_visibility_response # Implemented -@router.delete("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}", - tags=["viewpoint_delete"], status_code=200) -def viewpoint_delete(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User = Depends(get_current_active_user)) -> int: +@router.delete( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}", + tags=["viewpoint_delete"], + status_code=200, +) +def viewpoint_delete( + project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User = Depends(get_current_active_user) +) -> int: viewpoint_response = bcf_db.delete_viewpoint(project_id, topic_id, viewpoint_id, current_user) if viewpoint_response is None: raise HTTPException(status_code=404, detail="Item not found.") - bcf_db.debug(endpoint='viewpoint_delete', - request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id}, - response={viewpoint_response}) + bcf_db.debug( + endpoint="viewpoint_delete", + request={"project_id": project_id, "topic_id": topic_id, "viewpoint_id": viewpoint_id}, + response={viewpoint_response}, + ) return viewpoint_response @@ -401,24 +493,38 @@ def viewpoint_delete(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/related_topics", tags=["related_topics_get"]) -def related_topics_get(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[RelatedTopicGET]: +def related_topics_get( + project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user) +) -> List[RelatedTopicGET]: related_topics_response = bcf_db.get_related_topics(project_id, topic_id, current_user) - bcf_db.debug(endpoint='related_topics_get', - request={'project_id': project_id, 'topic_id': topic_id}, - response={count: value.dict() for count, value in enumerate(related_topics_response)}) + bcf_db.debug( + endpoint="related_topics_get", + request={"project_id": project_id, "topic_id": topic_id}, + response={count: value.dict() for count, value in enumerate(related_topics_response)}, + ) return related_topics_response # request body related_topic = RelatedTopicPUT -@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/related_topics", tags=["related_topics_put"], status_code=200) -def related_topics_put(project_id: UUID, topic_id: UUID, related_topics: List[RelatedTopicPUT], - current_user: User = Depends(get_current_active_user)) -> List[RelatedTopicGET]: +@router.put( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/related_topics", tags=["related_topics_put"], status_code=200 +) +def related_topics_put( + project_id: UUID, + topic_id: UUID, + related_topics: List[RelatedTopicPUT], + current_user: User = Depends(get_current_active_user), +) -> List[RelatedTopicGET]: related_topics_response = bcf_db.put_related_topics(project_id, topic_id, related_topics, current_user) - bcf_db.debug(endpoint='related_topics_put', - request={'project_id': project_id, 'topic_id': topic_id, - 'related_topics': {count: value.dict() for count, value in enumerate(related_topics)}}, - response={count: value.dict() for count, value in enumerate(related_topics_response)}) + bcf_db.debug( + endpoint="related_topics_put", + request={ + "project_id": project_id, + "topic_id": topic_id, + "related_topics": {count: value.dict() for count, value in enumerate(related_topics)}, + }, + response={count: value.dict() for count, value in enumerate(related_topics_response)}, + ) return related_topics_response @@ -428,51 +534,70 @@ def related_topics_put(project_id: UUID, topic_id: UUID, related_topics: List[Re # Document references <- from topic -@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references", - tags=["topic_document_references_get"]) -def topic_document_references_get(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[DocumentReferenceGET]: +@router.get( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references", tags=["topic_document_references_get"] +) +def topic_document_references_get( + project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user) +) -> List[DocumentReferenceGET]: topic_document_references_response = bcf_db.get_topic_document_references(project_id, topic_id, current_user) - bcf_db.debug(endpoint='topic_document_references_get', - request={'project_id': project_id, 'topic_id': topic_id}, - response={count: value.dict() for count, value in enumerate(topic_document_references_response)}) + bcf_db.debug( + endpoint="topic_document_references_get", + request={"project_id": project_id, "topic_id": topic_id}, + response={count: value.dict() for count, value in enumerate(topic_document_references_response)}, + ) return topic_document_references_response # request body document_reference = DocumentReferencePOST -@router.post("/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references", - tags=["topic_document_references_post"], - status_code=201) -def topic_document_reference_post(project_id: UUID, topic_id: UUID, document_reference: DocumentReferencePOST, - current_user: User = Depends(get_current_active_user)) -> DocumentReferenceGET: - topic_document_references_response = bcf_db.post_topic_document_references(project_id, - topic_id, - document_reference, - current_user) - bcf_db.debug(endpoint='topic_document_references_post', - request={'project_id': project_id, 'topic_id': topic_id, 'document_reference': document_reference}, - response={count: value.dict() for count, value in enumerate(topic_document_references_response)}) +@router.post( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references", + tags=["topic_document_references_post"], + status_code=201, +) +def topic_document_reference_post( + project_id: UUID, + topic_id: UUID, + document_reference: DocumentReferencePOST, + current_user: User = Depends(get_current_active_user), +) -> DocumentReferenceGET: + topic_document_references_response = bcf_db.post_topic_document_references( + project_id, topic_id, document_reference, current_user + ) + bcf_db.debug( + endpoint="topic_document_references_post", + request={"project_id": project_id, "topic_id": topic_id, "document_reference": document_reference}, + response={count: value.dict() for count, value in enumerate(topic_document_references_response)}, + ) return topic_document_references_response # request body document_reference = DocumentReferencePUT -@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references/{reference_id}", - tags=["topic_document_references_put"], - status_code=200) -def topic_document_references_put(project_id: UUID, topic_id: UUID, reference_id: UUID, - document_reference: DocumentReferencePUT, - current_user: User = Depends(get_current_active_user)) -> DocumentReferenceGET: - topic_document_references_response = bcf_db.put_topic_document_references(project_id, - topic_id, - reference_id, - document_reference, - current_user) - bcf_db.debug(endpoint='topic_document_references_put', - request={'project_id': project_id, - 'topic_id': topic_id, - 'reference_id': reference_id, - 'document_reference': document_reference.dict()}, - response={count: value.dict() for count, value in enumerate(topic_document_references_response)}) +@router.put( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references/{reference_id}", + tags=["topic_document_references_put"], + status_code=200, +) +def topic_document_references_put( + project_id: UUID, + topic_id: UUID, + reference_id: UUID, + document_reference: DocumentReferencePUT, + current_user: User = Depends(get_current_active_user), +) -> DocumentReferenceGET: + topic_document_references_response = bcf_db.put_topic_document_references( + project_id, topic_id, reference_id, document_reference, current_user + ) + bcf_db.debug( + endpoint="topic_document_references_put", + request={ + "project_id": project_id, + "topic_id": topic_id, + "reference_id": reference_id, + "document_reference": document_reference.dict(), + }, + response={count: value.dict() for count, value in enumerate(topic_document_references_response)}, + ) return topic_document_references_response @@ -485,30 +610,34 @@ def topic_document_references_put(project_id: UUID, topic_id: UUID, reference_id @router.get("/bcf/3.0/projects/{project_id}/documents", tags=["documents_get"]) def documents_get(project_id: UUID, current_user: User = Depends(get_current_active_user)) -> List[DocumentGET]: documents_response = bcf_db.get_documents(project_id, current_user) - bcf_db.debug(endpoint='documents_get', - request={'project_id': project_id}, - response={count: value.dict() for count, value in enumerate(documents_response)}) + bcf_db.debug( + endpoint="documents_get", + request={"project_id": project_id}, + response={count: value.dict() for count, value in enumerate(documents_response)}, + ) return documents_response # request body file = UploadFile @router.post("/bcf/3.0/projects/{project_id}/documents", tags=["document_post"], status_code=201) -async def document_post(project_id: UUID, file: UploadFile, - current_user: User = Depends(get_current_active_user)) -> DocumentGET: +async def document_post( + project_id: UUID, file: UploadFile, current_user: User = Depends(get_current_active_user) +) -> DocumentGET: document_response = bcf_db.post_document(project_id, file, current_user) - bcf_db.debug(endpoint='document_post', - request={'project_id': project_id}, - response=document_response.dict()) + bcf_db.debug(endpoint="document_post", request={"project_id": project_id}, response=document_response.dict()) return document_response @router.get("/bcf/3.0/projects/{project_id}/documents/{document_id}", tags=["document_get"]) -def document_get(project_id: UUID, document_id: UUID, - current_user: User = Depends(get_current_active_user)) -> DocumentGET: +def document_get( + project_id: UUID, document_id: UUID, current_user: User = Depends(get_current_active_user) +) -> DocumentGET: document_response = bcf_db.get_document(project_id, document_id, current_user) - bcf_db.debug(endpoint='document_get', - request={'project_id': project_id, 'document_id': document_id}, - response=document_response.dict()) + bcf_db.debug( + endpoint="document_get", + request={"project_id": project_id, "document_id": document_id}, + response=document_response.dict(), + ) return document_response @@ -520,22 +649,26 @@ def document_get(project_id: UUID, document_id: UUID, # ... @router.get("/bcf/3.0/projects/{project_id}/topics/events", tags=["topics_events_get"]) -def topics_events_get(project_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[TopicEventGET]: +def topics_events_get(project_id: UUID, current_user: User = Depends(get_current_active_user)) -> List[TopicEventGET]: topic_events_response = bcf_db.get_topics_events(project_id, current_user) - bcf_db.debug(endpoint='topics_events_get', - request={'project_id': project_id}, - response={count: value.dict() for count, value in enumerate(topic_events_response)}) + bcf_db.debug( + endpoint="topics_events_get", + request={"project_id": project_id}, + response={count: value.dict() for count, value in enumerate(topic_events_response)}, + ) return topic_events_response @router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/events", tags=["topic_events_get"]) -def topic_events_get(project_id: UUID, topic_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[TopicEventGET]: +def topic_events_get( + project_id: UUID, topic_id: UUID, current_user: User = Depends(get_current_active_user) +) -> List[TopicEventGET]: topic_events_response = bcf_db.get_topic_events(project_id, topic_id, current_user) - bcf_db.debug(endpoint='topic_events_get', - request={'project_id': project_id, 'topic_id': topic_id}, - response={count: value.dict() for count, value in enumerate(topic_events_response)}) + bcf_db.debug( + endpoint="topic_events_get", + request={"project_id": project_id, "topic_id": topic_id}, + response={count: value.dict() for count, value in enumerate(topic_events_response)}, + ) return topic_events_response @@ -546,21 +679,28 @@ def topic_events_get(project_id: UUID, topic_id: UUID, @router.get("/bcf/3.0/projects/{project_id}/topics/comments/events", tags=["comments_events_get"]) -def comments_events_get(project_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[CommentEventGET]: +def comments_events_get( + project_id: UUID, current_user: User = Depends(get_current_active_user) +) -> List[CommentEventGET]: comments_events_response = bcf_db.get_comments_events(project_id, current_user) - bcf_db.debug(endpoint='comments_events_get', - request={'project_id': project_id}, - response={count: value.dict() for count, value in enumerate(comments_events_response)}) + bcf_db.debug( + endpoint="comments_events_get", + request={"project_id": project_id}, + response={count: value.dict() for count, value in enumerate(comments_events_response)}, + ) return comments_events_response -@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events", - tags=["comment_events_get"]) -def comment_events_get(project_id: UUID, topic_id: UUID, comment_id: UUID, - current_user: User = Depends(get_current_active_user)) -> List[CommentEventGET]: +@router.get( + "/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events", tags=["comment_events_get"] +) +def comment_events_get( + project_id: UUID, topic_id: UUID, comment_id: UUID, current_user: User = Depends(get_current_active_user) +) -> List[CommentEventGET]: comment_events_response = bcf_db.get_comment_events(project_id, topic_id, comment_id, current_user) - bcf_db.debug(endpoint='comment_events_get', - request={'project_id': project_id, 'topic_id': topic_id, 'comment_id': comment_id}, - response={count: value.dict() for count, value in enumerate(comment_events_response)}) + bcf_db.debug( + endpoint="comment_events_get", + request={"project_id": project_id, "topic_id": topic_id, "comment_id": comment_id}, + response={count: value.dict() for count, value in enumerate(comment_events_response)}, + ) return comment_events_response diff --git a/src/opencdeserver/api/app/api/documents.py b/src/opencdeserver/api/app/api/documents.py index 9db65bfacd..2f568fd9a3 100644 --- a/src/opencdeserver/api/app/api/documents.py +++ b/src/opencdeserver/api/app/api/documents.py @@ -1,4 +1,3 @@ - import collections import os import shutil @@ -6,7 +5,7 @@ import traceback import sys from fastapi import HTTPException, status, APIRouter, Request, Depends -from fastapi import UploadFile, Form +from fastapi import UploadFile, Form from fastapi.responses import FileResponse, HTMLResponse from fastapi.templating import Jinja2Templates from fastapi.encoders import jsonable_encoder @@ -87,12 +86,15 @@ templates = Jinja2Templates(directory="templates") @router.post("/documents/1.0/upload-documents", tags=[""]) -def upload_documents_post(upload_documents: UploadDocuments, - current_user: User = Depends(get_current_active_user)) -> DocumentUploadSessionInitialization: +def upload_documents_post( + upload_documents: UploadDocuments, current_user: User = Depends(get_current_active_user) +) -> DocumentUploadSessionInitialization: post_upload_documents_response = doc_db.post_upload_documents(upload_documents, current_user) - doc_db.debug(endpoint='upload_documents_post', - request={'upload_documents': upload_documents}, - response=post_upload_documents_response.dict()) + doc_db.debug( + endpoint="upload_documents_post", + request={"upload_documents": upload_documents}, + response=post_upload_documents_response.dict(), + ) return post_upload_documents_response @@ -101,20 +103,23 @@ def upload_documents_post(upload_documents: UploadDocuments, def upload_documents_get(request: Request, upload_session: UUID): data_for_upload_documents = doc_db.get_data_for_upload_documents(upload_session) - print('Data for site: ', data_for_upload_documents) + print("Data for site: ", data_for_upload_documents) return templates.TemplateResponse( - 'upload_files.html', - {'request': request, - 'upload_session': upload_session, - 'username': data_for_upload_documents.current_user.username, - 'email': data_for_upload_documents.current_user.email, - 'full_name': data_for_upload_documents.current_user.full_name, - 'server_context': data_for_upload_documents.server_context, - 'callback_url': data_for_upload_documents.callback.url, - 'callback_expires_in': data_for_upload_documents.callback.expires_in, - 'documents': data_for_upload_documents.documents, - 'projects': data_for_upload_documents.projects}) + "upload_files.html", + { + "request": request, + "upload_session": upload_session, + "username": data_for_upload_documents.current_user.username, + "email": data_for_upload_documents.current_user.email, + "full_name": data_for_upload_documents.current_user.full_name, + "server_context": data_for_upload_documents.server_context, + "callback_url": data_for_upload_documents.callback.url, + "callback_expires_in": data_for_upload_documents.callback.expires_in, + "documents": data_for_upload_documents.documents, + "projects": data_for_upload_documents.projects, + }, + ) @router.post("/documents/1.0/save-metadata-for-documents", tags=[""]) @@ -127,29 +132,29 @@ async def save_metadata_for_documents_post(request: Request) -> list: print(form_data_json) documents = collections.defaultdict(dict) - names = ('session_file_id', 'document', 'title', 'version_number', 'filename') + names = ("session_file_id", "document", "title", "version_number", "filename") for whole_form_key, value in form_data_json.items(): if whole_form_key.startswith(names): start_form_key, document_id = whole_form_key.split("@", 1) - print('New field: ', start_form_key, ' for document id: ', document_id) + print("New field: ", start_form_key, " for document id: ", document_id) documents[document_id][start_form_key] = value print("Documents: ") print(documents) - username = form_data_json['username'] - upload_session = form_data_json['upload_session'] - server_context = form_data_json['server_context'] - callback_url = form_data_json['callback_url'] - callback_expires_in = form_data_json['callback_expires_in'] - project = form_data_json['project'] + username = form_data_json["username"] + upload_session = form_data_json["upload_session"] + server_context = form_data_json["server_context"] + callback_url = form_data_json["callback_url"] + callback_expires_in = form_data_json["callback_expires_in"] + project = form_data_json["project"] documents_saved = list() for key in documents: try: - documents[key]['project'] = project + documents[key]["project"] = project document = DocumentMetadata(**documents[key]) save_metadata_response = doc_db.save_metadata_for_documents(document, upload_session, username) documents_saved.append(save_metadata_response) @@ -157,9 +162,11 @@ async def save_metadata_for_documents_post(request: Request) -> list: print(e) continue - doc_db.debug(endpoint='save_metadata_for_documents_post', - request={'documents': documents}, - response={'response': documents_saved}) + doc_db.debug( + endpoint="save_metadata_for_documents_post", + request={"documents": documents}, + response={"response": documents_saved}, + ) return documents_saved @@ -167,22 +174,30 @@ async def save_metadata_for_documents_post(request: Request) -> list: # http://localhost:8080/cde-callback-example?upload_documents_url= # https%3A%2F%2Fcde.example.com%2Fupload-instructions%3Fupload_session%3Dee56b8f3-8f93-4819-976e-46a45a5a996f + @router.post("/documents/1.0/upload-instructions", tags=[""]) -def upload_instructions(session_id: str, server_context: str, upload_files: UploadFileDetails, - current_user: User = Depends(get_current_active_user)) -> DocumentsToUpload: +def upload_instructions( + session_id: str, + server_context: str, + upload_files: UploadFileDetails, + current_user: User = Depends(get_current_active_user), +) -> DocumentsToUpload: documents_to_upload_model = DocumentsToUpload() documents_to_upload_model.server_context = server_context documents_to_upload_model.documents_to_upload = list() for upload_file in upload_files.files: - get_upload_instructions_response = doc_db.get_upload_instructions(session_id, server_context, upload_file, current_user) - doc_db.debug(endpoint='upload_instructions', - request={'session_id': session_id, - 'server_context': server_context, - 'document': upload_file}, - response=get_upload_instructions_response.dict()) + get_upload_instructions_response = doc_db.get_upload_instructions( + session_id, server_context, upload_file, current_user + ) + doc_db.debug( + endpoint="upload_instructions", + request={"session_id": session_id, "server_context": server_context, "document": upload_file}, + response=get_upload_instructions_response.dict(), + ) documents_to_upload_model.documents_to_upload.append(get_upload_instructions_response) return documents_to_upload_model + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- # summary: 'Upload a single file part' # @@ -206,8 +221,7 @@ def upload_instructions(session_id: str, server_context: str, upload_files: Uplo @router.post("/documents/1.0/upload-part/{part_id}", tags=[""]) -async def upload_part(part_id: str, request: Request, - current_user: User = Depends(get_current_active_user)): +async def upload_part(part_id: str, request: Request, current_user: User = Depends(get_current_active_user)): # file_name = doc_db.safe_path(part_id) file_name = part_id @@ -222,33 +236,31 @@ async def upload_part(part_id: str, request: Request, # try to receive the uploaded part try: - print('File contents: ', request_body) + print("File contents: ", request_body) # use document_id instead as dir_name # dir_name = doc_db.safe_path(document.document_id) dir_name = document.document_id - path = './data/document_parts/' + dir_name + '/' + path = "./data/document_parts/" + dir_name + "/" if not os.path.exists(path): os.makedirs(path) - with open(path + file_name, 'wb') as f: + with open(path + file_name, "wb") as f: f.write(request_body) except Exception: - print('Error uploading file') + print("Error uploading file") print(traceback.format_exc()) - print('Error uploading file') + print("Error uploading file") print(sys.exc_info()[2]) finally: # We will write to the database, information about part successfully uploaded. doc_db.mark_part_as_uploaded(part_id, current_user) - doc_db.debug(endpoint='upload-part', - request={'part_id': part_id}, - response={'uploaded': True}) + doc_db.debug(endpoint="upload-part", request={"part_id": part_id}, response={"uploaded": True}) return {"message": f"Successfully uploaded part {file_name}"} @@ -269,9 +281,11 @@ async def upload_part(part_id: str, request: Request, # /server-provided-path-document-upload-cancellation # description: This operation should be called to cancel the upload + @router.post("/documents/1.0/upload-completion", tags=[""]) -def upload_completion(upload_session: str, - current_user: User = Depends(get_current_active_user)) -> Union[DocumentVersion, bool]: +def upload_completion( + upload_session: str, current_user: User = Depends(get_current_active_user) +) -> Union[DocumentVersion, bool]: # check if all parts really are marked as uploaded in database # retrieve document_id, file_type, file_ending, and parts_id (in order) @@ -280,23 +294,23 @@ def upload_completion(upload_session: str, raise HTTPException(status_code=400, detail="All parts not uploaded.") parts = doc_db.retrieve_uploaded_parts(upload_session, current_user) - print('Number of parts: ' + str(len(parts))) + print("Number of parts: " + str(len(parts))) document = doc_db.get_document_from_session(upload_session, current_user) # check if all parts really are uploaded to document_id-dir document_name = doc_db.safe_path(document.document_id) - path = './data/document_parts/' + document_name + '/' + path = "./data/document_parts/" + document_name + "/" for part in parts: part = doc_db.safe_path(part) - print('Checking for part ' + part + ' in dir ' + path) + print("Checking for part " + part + " in dir " + path) if not os.path.isfile(path + part): - print(part + ' is not in dir ' + path) + print(part + " is not in dir " + path) raise HTTPException(status_code=400, detail="All parts not in dir.") else: - print(part + ' is in dir ' + path) + print(part + " is in dir " + path) # merge parts to a new temporary document temp_doc_path = path @@ -304,14 +318,14 @@ def upload_completion(upload_session: str, if not os.path.exists(temp_doc_path): os.makedirs(temp_doc_path) - new_doc_path = './data/documents/' + new_doc_path = "./data/documents/" new_doc_path_name = new_doc_path + document.file_description.name # Read parts and write to temp doc. - with open(temp_doc_file_name, 'ab') as temp_doc: + with open(temp_doc_file_name, "ab") as temp_doc: for part in parts: part = doc_db.safe_path(part) - with open(temp_doc_path + part, 'rb') as part_doc: + with open(temp_doc_path + part, "rb") as part_doc: temp_doc.write(part_doc.read()) # move document to new location in documents dir @@ -329,13 +343,12 @@ def upload_completion(upload_session: str, # get DocumentVersion document = doc_db.get_document_version(document.document_id, document.version_index, current_user) - doc_db.debug(endpoint='upload_completion', - request={'upload_session': upload_session}, - response=document.dict()) + doc_db.debug(endpoint="upload_completion", request={"upload_session": upload_session}, response=document.dict()) return document else: return False + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- # summary: 'Cancel the upload of a single file' # @@ -355,14 +368,14 @@ def upload_cancellation(upload_session: str, current_user: User = Depends(get_cu # clean temp dir document_name = doc_db.safe_path(document.document_id) - path = './data/document_parts/' + document_name + '/' + path = "./data/document_parts/" + document_name + "/" shutil.rmtree(path) except Exception as e: print(e) finally: - print('Upload cancellation complete.') + print("Upload cancellation complete.") return @@ -399,17 +412,17 @@ def upload_cancellation(upload_session: str, current_user: User = Depends(get_cu # that has been flagged to have a new version in the response. # + @router.post("/documents/1.0/document-versions", tags=[""]) -def document_versions_post(document_ids: List[UUID], - current_user: User = Depends(get_current_active_user)) -> List[DocumentVersion]: +def document_versions_post( + document_ids: List[UUID], current_user: User = Depends(get_current_active_user) +) -> List[DocumentVersion]: document_versions = list() for document_id in document_ids: document_versions.append(doc_db.get_document_version(document_id, 1, current_user)) - doc_db.debug(endpoint='document_versions_post', - request={document_ids}, - response={document_versions}) + doc_db.debug(endpoint="document_versions_post", request={document_ids}, response={document_versions}) return document_versions @@ -460,12 +473,15 @@ def document_versions_post(document_ids: List[UUID], @router.post("/documents/1.0/select-documents", tags=[""]) -def select_documents_post(select_documents: SelectDocuments, - current_user: User = Depends(get_current_active_user)) -> DocumentDiscoverySessionInitialization: +def select_documents_post( + select_documents: SelectDocuments, current_user: User = Depends(get_current_active_user) +) -> DocumentDiscoverySessionInitialization: post_select_documents_response = doc_db.post_select_documents(select_documents, current_user) - doc_db.debug(endpoint='select_documents_post', - request={'select_documents': select_documents}, - response=post_select_documents_response.dict()) + doc_db.debug( + endpoint="select_documents_post", + request={"select_documents": select_documents}, + response=post_select_documents_response.dict(), + ) print("Returns ", post_select_documents_response) return post_select_documents_response @@ -490,19 +506,22 @@ def select_documents_post(select_documents: SelectDocuments, # documents/1.0/document-selection?selection_session=7cf3dd70-c880-4fb1-9897-f60472959533 + @router.get("/documents/1.0/document-selection", tags=[""], response_class=HTMLResponse) -def selected_documents_get(request: Request, - selection_session: UUID): +def selected_documents_get(request: Request, selection_session: UUID): data_for_document_selection = doc_db.get_data_for_document_selection(selection_session) return templates.TemplateResponse( - 'select_files.html', - {'request': request, - 'selection_session': selection_session, - 'current_user': data_for_document_selection.current_user, - 'server_context': data_for_document_selection.server_context, - 'callback_url': data_for_document_selection.callback.url, - 'callback_expires_in': data_for_document_selection.callback.expires_in, - 'projects': data_for_document_selection.projects}) + "select_files.html", + { + "request": request, + "selection_session": selection_session, + "current_user": data_for_document_selection.current_user, + "server_context": data_for_document_selection.server_context, + "callback_url": data_for_document_selection.callback.url, + "callback_expires_in": data_for_document_selection.callback.expires_in, + "projects": data_for_document_selection.projects, + }, + ) @router.post("/documents/1.0/mark-documents-as-selected", tags=[""]) @@ -516,45 +535,52 @@ async def mark_documents_as_selected_post(request: Request) -> DocumentsMarkedAs documents = list() for key, value in form_data_json.items(): - if 'document_' in key: + if "document_" in key: document_id = key.split("ocument_", 1)[1] documents.append(document_id) print("Sends ", documents) - get_selected_response = doc_db.post_mark_documents_as_selected(documents, form_data_json['selection_session']) - doc_db.debug(endpoint='mark_some_documents_as_selected_post', - request={'documents': documents, - 'form_data_json[selection_session]': form_data_json['selection_session']}, - response=get_selected_response.dict()) + get_selected_response = doc_db.post_mark_documents_as_selected(documents, form_data_json["selection_session"]) + doc_db.debug( + endpoint="mark_some_documents_as_selected_post", + request={"documents": documents, "form_data_json[selection_session]": form_data_json["selection_session"]}, + response=get_selected_response.dict(), + ) return get_selected_response @router.get("/documents/1.0/download-instructions", tags=[""]) -def download_instructions(session_id: UUID, server_context: str, - current_user: User = Depends(get_current_active_user)) -> SelectedDocuments: +def download_instructions( + session_id: UUID, server_context: str, current_user: User = Depends(get_current_active_user) +) -> SelectedDocuments: get_download_instructions_response = doc_db.get_download_instructions(session_id, server_context, current_user) - doc_db.debug(endpoint='download_instructions', - request={'session_id': session_id, - 'server_context': server_context}, - response=get_download_instructions_response.dict()) + doc_db.debug( + endpoint="download_instructions", + request={"session_id": session_id, "server_context": server_context}, + response=get_download_instructions_response.dict(), + ) return get_download_instructions_response # download links + @router.get("/documents/1.0/document/{document_id}/version/{version_index}", tags=[""]) -def document_version(document_id: str, version_index: int, - current_user: User = Depends(get_current_active_user)) -> DocumentVersion: +def document_version( + document_id: str, version_index: int, current_user: User = Depends(get_current_active_user) +) -> DocumentVersion: # This endpoint returns the document version model itself. get_document_version = doc_db.get_document_version(document_id, version_index, current_user) - doc_db.debug(endpoint='document_version', - request={'document_id': document_id, - 'version_index': version_index}, - response=get_document_version.dict()) + doc_db.debug( + endpoint="document_version", + request={"document_id": document_id, "version_index": version_index}, + response=get_document_version.dict(), + ) return get_document_version + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- # summary: 'Get document metadata for a single document' # @@ -573,16 +599,21 @@ def document_version(document_id: str, version_index: int, @router.get("/documents/1.0/document/{document_id}/version/{version_index}/metadata", tags=[""]) -def document_version_metadata(document_id: str, version_index: int, - current_user: User = Depends(get_current_active_user)) -> DocumentMetadataEntries: +def document_version_metadata( + document_id: str, version_index: int, current_user: User = Depends(get_current_active_user) +) -> DocumentMetadataEntries: # The metadata for document versions is a list of key-value pairs - get_document_version_metadata_result = doc_db.get_document_version_metadata(document_id, version_index, current_user) - doc_db.debug(endpoint='document_version', - request={'document_id': document_id, - 'version_index': version_index}, - response=get_document_version_metadata_result.dict()) + get_document_version_metadata_result = doc_db.get_document_version_metadata( + document_id, version_index, current_user + ) + doc_db.debug( + endpoint="document_version", + request={"document_id": document_id, "version_index": version_index}, + response=get_document_version_metadata_result.dict(), + ) return get_document_version_metadata_result + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- # summary: 'Download the document' # @@ -601,47 +632,48 @@ def document_version_metadata(document_id: str, version_index: int, @router.get("/documents/1.0/document/{document_id}/version/{version_index}/download", tags=[""]) -def document_version_download(document_id: str, version_index: int, - current_user: User = Depends(get_current_active_user)) -> FileResponse: +def document_version_download( + document_id: str, version_index: int, current_user: User = Depends(get_current_active_user) +) -> FileResponse: # The url to download the binary content of this document version. # May either directly return the result or redirect to a storage provider - keep_characters = (' ', '.', '_', '-') + keep_characters = (" ", ".", "_", "-") document_id = "".join(c for c in document_id if c.isalnum() or c in keep_characters).rstrip() - file_location = './data/documents/' + document_id + '.ifc' - return FileResponse(file_location, - media_type='application/x-step', - filename='6dbd4d52-14db-11ee-be56-0242ac120002.ifc') + file_location = "./data/documents/" + document_id + ".ifc" + return FileResponse( + file_location, media_type="application/x-step", filename="6dbd4d52-14db-11ee-be56-0242ac120002.ifc" + ) @router.get("/documents/1.0/document/{document_id}/version/{version_index}/versions", tags=[""]) -def document_versions(document_id: str, version_index: int, - current_user: User = Depends(get_current_active_user)) -> DocumentVersions: +def document_versions( + document_id: str, version_index: int, current_user: User = Depends(get_current_active_user) +) -> DocumentVersions: # This url returns a list of all document versions for the parent document. # The client can use this URL to monitor for new document versions get_document_versions_result = doc_db.get_document_versions(document_id, current_user) - doc_db.debug(endpoint='document_version', - request={'document_id': document_id}, - response=get_document_versions_result.dict()) + doc_db.debug( + endpoint="document_version", request={"document_id": document_id}, response=get_document_versions_result.dict() + ) return get_document_versions_result -@router.get("/documents/1.0/document/{document_id}/version/{version_index}/details", tags=[""], - response_class=HTMLResponse) -def document_version_details(request: Request, - document_id: str, - version_index: int, - current_user: User = Depends(get_current_active_user)): +@router.get( + "/documents/1.0/document/{document_id}/version/{version_index}/details", tags=[""], response_class=HTMLResponse +) +def document_version_details( + request: Request, document_id: str, version_index: int, current_user: User = Depends(get_current_active_user) +): # This url returns a list of all document versions for the parent document. # The client can use this URL to monitor for new document versions details = doc_db.get_document_version(document_id, version_index, current_user) - doc_db.debug(endpoint='document_version', - request={'document_id': document_id, - 'version_index': version_index}, - response=details.dict()) - return templates.TemplateResponse( - 'document_details.html', - {'request': request, - 'details': details}) + doc_db.debug( + endpoint="document_version", + request={"document_id": document_id, "version_index": version_index}, + response=details.dict(), + ) + return templates.TemplateResponse("document_details.html", {"request": request, "details": details}) + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- # summary: 'Get the versions of a single document' @@ -699,7 +731,9 @@ def document_version_details(request: Request, @router.post("/documents/1.0/upload_file_to_project", tags=[""]) -async def upload_documents_post(file: UploadFile, project: str = Form(...), selection_session: str = Form(...)) -> Document: +async def upload_documents_post( + file: UploadFile, project: str = Form(...), selection_session: str = Form(...) +) -> Document: # Get the file size (in bytes) file.file.seek(0, 2) @@ -713,49 +747,46 @@ async def upload_documents_post(file: UploadFile, project: str = Form(...), sele # Find file name ending and create new storage file name if file.filename.lower().endswith(tuple(file_types)): - file_ending = file.filename.split('.')[-1].lower() - name = document_id + '.' + file_ending + file_ending = file.filename.split(".")[-1].lower() + name = document_id + "." + file_ending else: - file_ending = '' + file_ending = "" name = document_id # Get mime type and file type - mime_type = '' - file_type = '' + mime_type = "" + file_type = "" if hasattr(file_types, file_ending): - mime_type = file_types[file_ending]['mime_type'] - file_type = file_types[file_ending]['file_type'] + mime_type = file_types[file_ending]["mime_type"] + file_type = file_types[file_ending]["file_type"] # Create document data document_version_dict = { - 'document_id': document_id, - 'session_file_id': '', - 'version_index': 1, - 'version_number': '1', - 'creation_date': doc_db.timestamp(), - 'title': file.filename, - 'original_file_name': file.filename, - 'file_ending': file_ending, - 'mime_type': mime_type, - 'file_type': file_type, - 'project': project, - 'file_description': { - 'name': name, - 'size_in_bytes': file_size - } + "document_id": document_id, + "session_file_id": "", + "version_index": 1, + "version_number": "1", + "creation_date": doc_db.timestamp(), + "title": file.filename, + "original_file_name": file.filename, + "file_ending": file_ending, + "mime_type": mime_type, + "file_type": file_type, + "project": project, + "file_description": {"name": name, "size_in_bytes": file_size}, } document_version_model = Document(**document_version_dict) # Save file to disc - upload_directory = './data/documents/' + upload_directory = "./data/documents/" destination_path = os.path.join(upload_directory, name) - with open(destination_path, 'wb') as buffer: + with open(destination_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) # create database record inserted_document = doc_db.create_node_for_uploaded_file(selection_session, project, document_version_model) - print('Created node for document id: ' + str(inserted_document.document_id)) + print("Created node for document id: " + str(inserted_document.document_id)) doc_db.create_ifc_graph_for_document(inserted_document.document_id) # return document version of database record diff --git a/src/opencdeserver/api/app/api/foundation.py b/src/opencdeserver/api/app/api/foundation.py index 04a0baa9f4..25c6036ac8 100644 --- a/src/opencdeserver/api/app/api/foundation.py +++ b/src/opencdeserver/api/app/api/foundation.py @@ -28,11 +28,10 @@ authorization_code = None templates = Jinja2Templates(directory="templates") clients = { - os.environ['KONTROLL_CLIENT_ID']: - { - 'name': os.environ['KONTROLL_CLIENT_NAME'], - 'secret': secrets['kontroll_client_secret'] - } + os.environ["KONTROLL_CLIENT_ID"]: { + "name": os.environ["KONTROLL_CLIENT_NAME"], + "secret": secrets["kontroll_client_secret"], + } } # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- @@ -60,21 +59,25 @@ clients = { @router.get("/foundation/versions", tags=["api_versions_get"]) def api_versions_get(): return { - "versions": [{ - "api_id": "foundation", - "version_id": "1.0", - "detailed_version": "https://github.com/BuildingSMART/foundation-API/tree/release_1_0" - }, { - "api_id": "bcf", - "version_id": "3.0", - "detailed_version": "https://github.com/buildingSMART/BCF-API/tree/release_3_0", - "api_base_url": os.environ['KONTROLL_BASE_URL'] + "bcf/3.0" - }, { - "api_id": "documents", - "version_id": "1.0", - "detailed_version": "https://github.com/buildingSMART/documents-API/tree/release_1_0", - "api_base_url": os.environ['KONTROLL_BASE_URL'] + "documents/1.0" - }] + "versions": [ + { + "api_id": "foundation", + "version_id": "1.0", + "detailed_version": "https://github.com/BuildingSMART/foundation-API/tree/release_1_0", + }, + { + "api_id": "bcf", + "version_id": "3.0", + "detailed_version": "https://github.com/buildingSMART/BCF-API/tree/release_3_0", + "api_base_url": os.environ["KONTROLL_BASE_URL"] + "bcf/3.0", + }, + { + "api_id": "documents", + "version_id": "1.0", + "detailed_version": "https://github.com/buildingSMART/documents-API/tree/release_1_0", + "api_base_url": os.environ["KONTROLL_BASE_URL"] + "documents/1.0", + }, + ] } @@ -95,18 +98,15 @@ def api_versions_get(): # is not supported by the server. -@router.get("/foundation/1.0/auth", - tags=["foundation_auth_get"]) +@router.get("/foundation/1.0/auth", tags=["foundation_auth_get"]) def authentication_get(): return_variable = { - "oauth2_auth_url": os.environ['KONTROLL_BASE_URL'] + "foundation/oauth2/auth", - "oauth2_token_url": os.environ['KONTROLL_BASE_URL'] + "foundation/oauth2/token", + "oauth2_auth_url": os.environ["KONTROLL_BASE_URL"] + "foundation/oauth2/auth", + "oauth2_token_url": os.environ["KONTROLL_BASE_URL"] + "foundation/oauth2/token", # "oauth2_dynamic_client_reg_url": os.environ['KONTROLL_BASE_URL'] + "foundation/oauth2/reg", "http_basic_supported": True, - "supported_oauth2_flows": [ - "authorization_code_grant" - ] + "supported_oauth2_flows": ["authorization_code_grant"], } print(return_variable) return return_variable @@ -135,23 +135,25 @@ def authentication_get(): @router.get("/foundation/oauth2/auth", response_class=HTMLResponse) -def authorization(request: Request, - response_type: str, - client_id: str, - state: str, - scope: str, - redirect_uri: str, - ): +def authorization( + request: Request, + response_type: str, + client_id: str, + state: str, + scope: str, + redirect_uri: str, +): - client_name = clients[client_id]['name'] + client_name = clients[client_id]["name"] - print(f"Response type: {response_type}, " - f"Client_id: {client_id}, " - f"Client_name: {client_name}, " - f"State: {state}, " - f"Scope: {scope}," - f"Redirect_URI: {redirect_uri}." - ) + print( + f"Response type: {response_type}, " + f"Client_id: {client_id}, " + f"Client_name: {client_name}, " + f"State: {state}, " + f"Scope: {scope}," + f"Redirect_URI: {redirect_uri}." + ) # 3. Solibri sends the user to oauth2_auth_url with the following parameters: # response_type=code, client_id=solibri_test_001, state=..., redirect_uri=uri, scope=... @@ -159,38 +161,41 @@ def authorization(request: Request, return templates.TemplateResponse( "login.html", - {"request": request, - "response_type": response_type, - "client_id": client_id, - "client_name": client_name, - "state": state, - "scope": scope, - "redirect_uri": redirect_uri, - }) + { + "request": request, + "response_type": response_type, + "client_id": client_id, + "client_name": client_name, + "state": state, + "scope": scope, + "redirect_uri": redirect_uri, + }, + ) @router.get("/foundation/oauth2/code") -def code(username: str, - password: str, - response_type: str, - client_id, - client_name, - state: str, - redirect_uri: str, - scope: str = '' - ): +def code( + username: str, + password: str, + response_type: str, + client_id, + client_name, + state: str, + redirect_uri: str, + scope: str = "", +): global oauth2_state oauth2_state = state - print('Username: ' + username + '. Password: ' + password) + print("Username: " + username + ". Password: " + password) user = authenticate_user(username, password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"} + headers={"WWW-Authenticate": "Bearer"}, ) # The user is signed in, now the main purpose of this function is to generate the authorization code. @@ -242,36 +247,35 @@ def code(username: str, # POST https://example.com/foundation/oauth2/token?grant_type=authorization_code&code= -@router.post("/foundation/oauth2/token", - tags=["login_for_access_token_post"], - status_code=201) +@router.post("/foundation/oauth2/token", tags=["login_for_access_token_post"], status_code=201) def login_for_access_token( - grant_type: Optional[str] = Form(None), - refresh_token: Optional[str] = Form(None), - code: Optional[str] = Form(None), - credentials: HTTPBasicCredentials = Depends(http_basic)): + grant_type: Optional[str] = Form(None), + refresh_token: Optional[str] = Form(None), + code: Optional[str] = Form(None), + credentials: HTTPBasicCredentials = Depends(http_basic), +): - print('grant_type: ', grant_type) - print('refresh_token: ', refresh_token) - print('code: ', code) - print('credentials: ', credentials) + print("grant_type: ", grant_type) + print("refresh_token: ", refresh_token) + print("code: ", code) + print("credentials: ", credentials) # The API should check that the credentials (client_id and client_secret) are correct # credentials.username contains the client_id # credentials.password contains the client_secret - if credentials.username not in clients or credentials.password != clients[credentials.username]['secret']: + if credentials.username not in clients or credentials.password != clients[credentials.username]["secret"]: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect client_id or client_secret", ) - if grant_type == 'authorization_code': + if grant_type == "authorization_code": # use authorization code, # create access token and refresh token, # delete authorization code user_info = foundation_db.use_authorization_code(code) - elif grant_type == 'refresh_token': + elif grant_type == "refresh_token": # use refresh token to get access token # delete old access token and old refresh token # create new access token and a new refresh token @@ -279,8 +283,8 @@ def login_for_access_token( user_info = foundation_db.use_refresh_token(refresh_token) user_info.token_type = "Bearer" - user_info.expires_in = int(os.environ['SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS']) - print('user_info: ', user_info) + user_info.expires_in = int(os.environ["SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS"]) + print("user_info: ", user_info) return user_info diff --git a/src/opencdeserver/api/app/api/logging.py b/src/opencdeserver/api/app/api/logging.py index 2547ac66c2..8090ae9c7d 100644 --- a/src/opencdeserver/api/app/api/logging.py +++ b/src/opencdeserver/api/app/api/logging.py @@ -9,8 +9,8 @@ import httpx def log_info(req_body, res_body, route_url): - logging.info('request:' + route_url + ':' + str(req_body)) - logging.info('response:' + route_url + ':' + str(res_body)) + logging.info("request:" + route_url + ":" + str(req_body)) + logging.info("response:" + route_url + ":" + str(res_body)) class LoggingRoute(APIRoute): @@ -22,21 +22,26 @@ class LoggingRoute(APIRoute): response = await original_route_handler(request) route_url = str(request.url) if isinstance(response, StreamingResponse): - res_body = b'' + res_body = b"" async for item in response.body_iterator: res_body += item task = BackgroundTask(log_info, req_body, res_body, route_url) - return Response(content=res_body, status_code=response.status_code, - headers=dict(response.headers), media_type=response.media_type, background=task) + return Response( + content=res_body, + status_code=response.status_code, + headers=dict(response.headers), + media_type=response.media_type, + background=task, + ) else: - if hasattr(response, 'body'): + if hasattr(response, "body"): res_body = response.body else: - res_body = {'no response': True} + res_body = {"no response": True} response.background = BackgroundTask(log_info, req_body, res_body, route_url) return response return custom_route_handler -logging.basicConfig(filename='logs/info.log', level=logging.DEBUG) +logging.basicConfig(filename="logs/info.log", level=logging.DEBUG) diff --git a/src/opencdeserver/api/app/api/user.py b/src/opencdeserver/api/app/api/user.py index 28d3aa5fbb..5628a4cf05 100644 --- a/src/opencdeserver/api/app/api/user.py +++ b/src/opencdeserver/api/app/api/user.py @@ -1,10 +1,9 @@ - import collections import os import traceback import sys -from fastapi import APIRouter, Request, Depends +from fastapi import APIRouter, Request, Depends from fastapi.responses import FileResponse, HTMLResponse from fastapi.templating import Jinja2Templates from fastapi.encoders import jsonable_encoder @@ -30,13 +29,17 @@ templates = Jinja2Templates(directory="templates") # UPLOAD FLOW ################################################################ + @router.post("/user/1.0/upload-documents", tags=[""]) -def upload_documents_post(upload_documents: UploadDocuments, - current_user: User = Depends(get_current_active_user)) -> DocumentUploadSessionInitialization: +def upload_documents_post( + upload_documents: UploadDocuments, current_user: User = Depends(get_current_active_user) +) -> DocumentUploadSessionInitialization: post_upload_documents_response = doc_db.post_upload_documents(upload_documents, current_user) - doc_db.debug(endpoint='upload_documents_post', - request={'upload_documents': upload_documents}, - response=post_upload_documents_response.dict()) + doc_db.debug( + endpoint="upload_documents_post", + request={"upload_documents": upload_documents}, + response=post_upload_documents_response.dict(), + ) return post_upload_documents_response @@ -45,20 +48,23 @@ def upload_documents_post(upload_documents: UploadDocuments, def upload_documents_get(request: Request, upload_session: UUID): data_for_upload_documents = doc_db.get_data_for_upload_documents(upload_session) - print('Data for site: ', data_for_upload_documents) + print("Data for site: ", data_for_upload_documents) return templates.TemplateResponse( - 'upload_files.html', - {'request': request, - 'upload_session': upload_session, - 'username': data_for_upload_documents.current_user.username, - 'email': data_for_upload_documents.current_user.email, - 'full_name': data_for_upload_documents.current_user.full_name, - 'server_context': data_for_upload_documents.server_context, - 'callback_url': data_for_upload_documents.callback.url, - 'callback_expires_in': data_for_upload_documents.callback.expires_in, - 'documents': data_for_upload_documents.documents, - 'projects': data_for_upload_documents.projects}) + "upload_files.html", + { + "request": request, + "upload_session": upload_session, + "username": data_for_upload_documents.current_user.username, + "email": data_for_upload_documents.current_user.email, + "full_name": data_for_upload_documents.current_user.full_name, + "server_context": data_for_upload_documents.server_context, + "callback_url": data_for_upload_documents.callback.url, + "callback_expires_in": data_for_upload_documents.callback.expires_in, + "documents": data_for_upload_documents.documents, + "projects": data_for_upload_documents.projects, + }, + ) @router.post("/user/1.0/save-metadata-for-documents", tags=[""]) @@ -71,29 +77,29 @@ async def save_metadata_for_documents_post(request: Request) -> list: print(form_data_json) documents = collections.defaultdict(dict) - names = ('session_file_id', 'document', 'title', 'version_number', 'filename') + names = ("session_file_id", "document", "title", "version_number", "filename") for whole_form_key, value in form_data_json.items(): if whole_form_key.startswith(names): start_form_key, document_id = whole_form_key.split("@", 1) - print('New field: ', start_form_key, ' for document id: ', document_id) + print("New field: ", start_form_key, " for document id: ", document_id) documents[document_id][start_form_key] = value print("Documents: ") print(documents) - username = form_data_json['username'] - upload_session = form_data_json['upload_session'] - server_context = form_data_json['server_context'] - callback_url = form_data_json['callback_url'] - callback_expires_in = form_data_json['callback_expires_in'] - project = form_data_json['project'] + username = form_data_json["username"] + upload_session = form_data_json["upload_session"] + server_context = form_data_json["server_context"] + callback_url = form_data_json["callback_url"] + callback_expires_in = form_data_json["callback_expires_in"] + project = form_data_json["project"] documents_saved = list() for key in documents: try: - documents[key]['project'] = project + documents[key]["project"] = project document = DocumentMetadata(**documents[key]) save_metadata_response = doc_db.save_metadata_for_documents(document, upload_session, username) documents_saved.append(save_metadata_response) @@ -101,9 +107,11 @@ async def save_metadata_for_documents_post(request: Request) -> list: print(e) continue - doc_db.debug(endpoint='save_metadata_for_documents_post', - request={'documents': documents}, - response={'response': documents_saved}) + doc_db.debug( + endpoint="save_metadata_for_documents_post", + request={"documents": documents}, + response={"response": documents_saved}, + ) return documents_saved @@ -113,8 +121,7 @@ async def save_metadata_for_documents_post(request: Request) -> list: @router.post("/user/1.0/upload-part/{part_id}", tags=[""]) -async def upload_part(part_id: str, request: Request, - current_user: User = Depends(get_current_active_user)): +async def upload_part(part_id: str, request: Request, current_user: User = Depends(get_current_active_user)): # file_name = doc_db.safe_path(part_id) file_name = part_id @@ -129,33 +136,31 @@ async def upload_part(part_id: str, request: Request, # try to receive the uploaded part try: - print('File contents: ', request_body) + print("File contents: ", request_body) # use document_id instead as dir_name # dir_name = doc_db.safe_path(document.document_id) dir_name = document.document_id - path = './data/document_parts/' + dir_name + '/' + path = "./data/document_parts/" + dir_name + "/" if not os.path.exists(path): os.makedirs(path) - with open(path + file_name, 'wb') as f: + with open(path + file_name, "wb") as f: f.write(request_body) except Exception: - print('Error uploading file') + print("Error uploading file") print(traceback.format_exc()) - print('Error uploading file') + print("Error uploading file") print(sys.exc_info()[2]) finally: # We will write to the database, information about part successfully uploaded. doc_db.mark_part_as_uploaded(part_id, current_user) - doc_db.debug(endpoint='upload-part', - request={'part_id': part_id}, - response={'uploaded': True}) + doc_db.debug(endpoint="upload-part", request={"part_id": part_id}, response={"uploaded": True}) return {"message": f"Successfully uploaded part {file_name}"} @@ -166,13 +171,14 @@ async def upload_part(part_id: str, request: Request, @router.get("/user/1.0/document/{document_id}/version/{version_index}/download", tags=[""]) -def document_version_download(document_id: str, version_index: int, - current_user: User = Depends(get_current_active_user)) -> FileResponse: +def document_version_download( + document_id: str, version_index: int, current_user: User = Depends(get_current_active_user) +) -> FileResponse: # The url to download the binary content of this document version. # May either directly return the result or redirect to a storage provider - keep_characters = (' ', '.', '_', '-') + keep_characters = (" ", ".", "_", "-") document_id = "".join(c for c in document_id if c.isalnum() or c in keep_characters).rstrip() - file_location = './data/documents/' + document_id + '.ifc' - return FileResponse(file_location, - media_type='application/x-step', - filename='6dbd4d52-14db-11ee-be56-0242ac120002.ifc') \ No newline at end of file + file_location = "./data/documents/" + document_id + ".ifc" + return FileResponse( + file_location, media_type="application/x-step", filename="6dbd4d52-14db-11ee-be56-0242ac120002.ifc" + ) diff --git a/src/opencdeserver/api/app/database/neo4j.py b/src/opencdeserver/api/app/database/neo4j.py index a3770d23a7..b6172e7890 100644 --- a/src/opencdeserver/api/app/database/neo4j.py +++ b/src/opencdeserver/api/app/database/neo4j.py @@ -18,9 +18,9 @@ get_secrets() # otherwise the environment variable will have a value of neo4j://kontroll_neo4j:27687 # kontroll_neo4j is the docker-compose network. -driver = GraphDatabase.driver(os.environ['NEO4J_URI'], - auth=(os.environ['NEO4J_USER'], - os.environ['NEO4J_INITIAL_PASSWORD'])) +driver = GraphDatabase.driver( + os.environ["NEO4J_URI"], auth=(os.environ["NEO4J_USER"], os.environ["NEO4J_INITIAL_PASSWORD"]) +) # initial password should be changed to secret password @@ -33,33 +33,38 @@ class MyDB: def __init__(self, object_driver): self.driver = object_driver - self.database = 'neo4j' + self.database = "neo4j" @staticmethod def timestamp(): - return datetime.now(timezone.utc).isoformat(sep='T', timespec='milliseconds') + return datetime.now(timezone.utc).isoformat(sep="T", timespec="milliseconds") @staticmethod def bcf_time(any_datetime): - if isinstance(any_datetime, type('str')): + if isinstance(any_datetime, type("str")): datetime_any = parser.parse(any_datetime).astimezone(pytz.utc) elif isinstance(any_datetime, type(datetime.now())): datetime_any = any_datetime.astimezone(pytz.utc) else: return False - string_date = datetime_any.isoformat(sep='T', timespec='milliseconds') + string_date = datetime_any.isoformat(sep="T", timespec="milliseconds") return str(string_date) @staticmethod def safe_path(path_name): - safe_path_name = ''.join(x for x in path_name if x.isalnum() or '-') + safe_path_name = "".join(x for x in path_name if x.isalnum() or "-") return safe_path_name @staticmethod def debug(endpoint: str, request, response): - print("\n\n\nEndpoint: ", jsonpickle.dumps(endpoint), - "\nRequest: ", jsonpickle.dumps(request), - "\nResponse: ", jsonpickle.dumps(response)) + print( + "\n\n\nEndpoint: ", + jsonpickle.dumps(endpoint), + "\nRequest: ", + jsonpickle.dumps(request), + "\nResponse: ", + jsonpickle.dumps(response), + ) @staticmethod def node_to_json(node): @@ -89,11 +94,12 @@ class MyDB: cypher_file = open(cypher_file_path, "r") cypher_data = cypher_file.read() cypher_file.close() - cypher_statements = cypher_data.split(';') + cypher_statements = cypher_data.split(";") cypher_statements.pop() for cypher_statement in cypher_statements: tx.run(cypher_statement) return + with self.driver.session() as session: return session.execute_write(initialize_db_work) @@ -113,8 +119,10 @@ class MyDB: user_dict = self.node_to_json(user_node) user = UserInDB(**user_dict) return user + with self.driver.session() as session: return session.execute_read(get_user_work, username_work=username) + db = MyDB(driver) db.initialize_db() diff --git a/src/opencdeserver/api/app/ifcgraph/ifcgraph.py b/src/opencdeserver/api/app/ifcgraph/ifcgraph.py index d88359672a..74f27babd7 100644 --- a/src/opencdeserver/api/app/ifcgraph/ifcgraph.py +++ b/src/opencdeserver/api/app/ifcgraph/ifcgraph.py @@ -28,24 +28,24 @@ from py2neo import Graph def create_pure_node_from_ifc_entity(ifc_entity, ifc_file, hierarchy=True): node = Node() if ifc_entity.id() != 0: - node['id'] = ifc_entity.id() + node["id"] = ifc_entity.id() else: - node['id'] = str(uuid4()) - node['name'] = ifc_entity.is_a() + node["id"] = str(uuid4()) + node["name"] = ifc_entity.is_a() if hierarchy: for label in ifc_file.wrapped_data.types_with_super(): if ifc_entity.is_a(label): node.add_label(label) else: node.add_label(ifc_entity.is_a()) - attributes_type = ['ENTITY INSTANCE', 'AGGREGATE OF ENTITY INSTANCE', 'DERIVED'] + attributes_type = ["ENTITY INSTANCE", "AGGREGATE OF ENTITY INSTANCE", "DERIVED"] for i in range(ifc_entity.__len__()): if not ifc_entity.wrapped_data.get_argument_type(i) in attributes_type: name = ifc_entity.wrapped_data.get_argument_name(i) name_value = ifc_entity.wrapped_data.get_argument(i) - node[name]= name_value - node.__primarylabel__ = 'Root' - node.__primarykey__ = 'id' + node[name] = name_value + node.__primarylabel__ = "Root" + node.__primarykey__ = "id" return node @@ -55,14 +55,14 @@ def create_graph_from_ifc_entity_all(graph, ifc_entity, ifc_file): graph.merge(node) for i in range(ifc_entity.__len__()): if ifc_entity[i]: - if ifc_entity.wrapped_data.get_argument_type(i) == 'ENTITY INSTANCE': - if ifc_entity[i].is_a() in ['IfcOwnerHistory'] and ifc_entity.is_a() != 'IfcProject': + if ifc_entity.wrapped_data.get_argument_type(i) == "ENTITY INSTANCE": + if ifc_entity[i].is_a() in ["IfcOwnerHistory"] and ifc_entity.is_a() != "IfcProject": continue else: sub_node = create_pure_node_from_ifc_entity(ifc_entity[i], ifc_file) REL = Relationship(node, ifc_entity.wrapped_data.get_argument_name(i), sub_node) graph.merge(REL) - elif ifc_entity.wrapped_data.get_argument_type(i) == 'AGGREGATE OF ENTITY INSTANCE': + elif ifc_entity.wrapped_data.get_argument_type(i) == "AGGREGATE OF ENTITY INSTANCE": for sub_entity in ifc_entity[i]: sub_node = create_pure_node_from_ifc_entity(sub_entity, ifc_file) REL = Relationship(node, ifc_entity.wrapped_data.get_argument_name(i), sub_node) @@ -83,7 +83,7 @@ def create_full_graph(graph, ifc_file): length = len(ifc_file.wrapped_data.entity_names()) for entity_id in ifc_file.wrapped_data.entity_names(): entity = ifc_file.by_id(entity_id) - print(idx, '/', length, entity) + print(idx, "/", length, entity) create_graph_from_ifc_entity_all(graph, entity, ifc_file) idx += 1 return diff --git a/src/opencdeserver/api/app/main.py b/src/opencdeserver/api/app/main.py index 5240019f4b..4862106a55 100644 --- a/src/opencdeserver/api/app/main.py +++ b/src/opencdeserver/api/app/main.py @@ -13,79 +13,110 @@ endpoint_metadata = [ {"name": "authentication_get", "description": "/authentication"}, {"name": "login_for_access_token_post", "description": "/foundation/oauth2/token"}, {"name": "current_user_get", "description": "/foundation/1.0/current-user"}, - {"name": "projects_get", - "description": "Retrieve a collection of projects that the currently logged on user has access to."}, - {"name": "project_get", - "description": "Retrieve a specific project. The top level data container is known as the BCF project, " - "with a UUID and a project name attribute."}, - {"name": "project_put", - "description": "Modify a specific project. This operation is only possible when the server returns the update " - "flag in the Project authorization."}, - {"name": "project_extensions_get", - "description": "Retrieve a specific projects extensions. Project extensions are used to define possible values " - "that can be used in topics and comments, for example topic labels and priorities. They may " - "change during the course of a project. The most recent extensions state which values are valid " - "at a given moment for newly created topics and comments."}, - {"name": "topics_get", - "description": "Retrieve a collection of topics related to a project (default sort order is creation_date)."}, - {"name": "topic_post", - "description": "Add a new topic. The BCF project contains zero or more topics. Each topic represents a model " - "issue. A topic will have a UUID, a title, description, priority, stage, labels (similar to " - "tags), creation date / author, due date, and assigned to. If modified, it may contain the " - "modification date and author."}, + { + "name": "projects_get", + "description": "Retrieve a collection of projects that the currently logged on user has access to.", + }, + { + "name": "project_get", + "description": "Retrieve a specific project. The top level data container is known as the BCF project, " + "with a UUID and a project name attribute.", + }, + { + "name": "project_put", + "description": "Modify a specific project. This operation is only possible when the server returns the update " + "flag in the Project authorization.", + }, + { + "name": "project_extensions_get", + "description": "Retrieve a specific projects extensions. Project extensions are used to define possible values " + "that can be used in topics and comments, for example topic labels and priorities. They may " + "change during the course of a project. The most recent extensions state which values are valid " + "at a given moment for newly created topics and comments.", + }, + { + "name": "topics_get", + "description": "Retrieve a collection of topics related to a project (default sort order is creation_date).", + }, + { + "name": "topic_post", + "description": "Add a new topic. The BCF project contains zero or more topics. Each topic represents a model " + "issue. A topic will have a UUID, a title, description, priority, stage, labels (similar to " + "tags), creation date / author, due date, and assigned to. If modified, it may contain the " + "modification date and author.", + }, {"name": "topic_get", "description": "Retrieve a specific topic."}, {"name": "topic_put", "description": "Modify a specific topic, description similar to POST."}, - {"name": "bim_snippet_get", - "description": "Retrieves a topics BIM-Snippet as binary file. BIM snippet has been in BCF specification since " - "the very beginning, but is has never been used. Snippets have originally been added to provide " - "for 'changes in IFC'. To get this really working in a CDE environment changes to the IFC format " - "is necessary."}, - {"name": "bim_snippet_put", - "description": "Puts a new BIM Snippet binary file to a topic. If this is used, the parent topics BIM Snippet " - "property is_external must be set to false and the reference must be the file name with " - "extension."}, + { + "name": "bim_snippet_get", + "description": "Retrieves a topics BIM-Snippet as binary file. BIM snippet has been in BCF specification since " + "the very beginning, but is has never been used. Snippets have originally been added to provide " + "for 'changes in IFC'. To get this really working in a CDE environment changes to the IFC format " + "is necessary.", + }, + { + "name": "bim_snippet_put", + "description": "Puts a new BIM Snippet binary file to a topic. If this is used, the parent topics BIM Snippet " + "property is_external must be set to false and the reference must be the file name with " + "extension.", + }, {"name": "files_get", "description": "Retrieve a collection of file references as topic header."}, {"name": "files_put", "description": "Update a collection of file references on the topic header."}, - {"name": "comments_get", - "description": "Retrieve a collection of all comments related to a topic (default ordering is date)."}, + { + "name": "comments_get", + "description": "Retrieve a collection of all comments related to a topic (default ordering is date).", + }, {"name": "comment_post", "description": "Add a new comment to a topic."}, {"name": "comment_put", "description": "Update a single comment, description similar to POST."}, {"name": "comment_get", "description": "Get a single comment."}, {"name": "viewpoints_get", "description": "Retrieve a collection of all viewpoints related to a topic."}, - {"name": "viewpoint_post", - "description": "Add a new viewpoint. Viewpoints are immutable, meaning that they should never change. " - "Requirements for different visualizations should be handled by creating new viewpoint elements."}, + { + "name": "viewpoint_post", + "description": "Add a new viewpoint. Viewpoints are immutable, meaning that they should never change. " + "Requirements for different visualizations should be handled by creating new viewpoint elements.", + }, {"name": "viewpoint_get", "description": "Retrieve a specific viewpoint."}, - {"name": "viewpoint_selected_components_get", - "description": "Retrieve a collection of all selected components in a viewpoint."}, - {"name": "viewpoint_colored_components_get", - "description": "Retrieve a collection of all colored components in a viewpoint."}, + { + "name": "viewpoint_selected_components_get", + "description": "Retrieve a collection of all selected components in a viewpoint.", + }, + { + "name": "viewpoint_colored_components_get", + "description": "Retrieve a collection of all colored components in a viewpoint.", + }, {"name": "viewpoint_components_visibility_get", "description": "Retrieve visibility of components in a viewpoint."}, {"name": "viewpoint_snapshot_get", "description": "Retrieve a specific viewpoints bitmap image file (png or jpg)."}, {"name": "related_topics_get", "description": "Retrieve a collection of all related topics to a topic."}, {"name": "related_topics_put", "description": "Add or update a collection of all related topics to a topic."}, - {"name": "topic_document_references_get", - "description": "Retrieve a collection of all document references to a topic."}, + { + "name": "topic_document_references_get", + "description": "Retrieve a collection of all document references to a topic.", + }, {"name": "topic_document_references_post", "description": "Add or update document references to a topic."}, {"name": "topic_document_references_put", "description": "Add or update document references to a topic."}, {"name": "documents_get", "description": "Retrieve a collection of all documents uploaded to a project."}, {"name": "document_post", "description": "Upload a document (binary file) to a project."}, {"name": "document_get", "description": "Retrieves a document as binary file."}, - {"name": "topics_events_get", - "description": "Retrieve a collection of topic events related to a project (default sort order is date)."}, - {"name": "topic_events_get", - "description": "Retrieve a collection of topic events related to a project (default sort order is date)."}, - {"name": "comments_events_get", - "description": "Retrieve a collection of comment events related to a project (default sort order is date)."}, - {"name": "comment_events_get", - "description": "Retrieve a collection of comment events related to a comment (default sort order is date)."} + { + "name": "topics_events_get", + "description": "Retrieve a collection of topic events related to a project (default sort order is date).", + }, + { + "name": "topic_events_get", + "description": "Retrieve a collection of topic events related to a project (default sort order is date).", + }, + { + "name": "comments_events_get", + "description": "Retrieve a collection of comment events related to a project (default sort order is date).", + }, + { + "name": "comment_events_get", + "description": "Retrieve a collection of comment events related to a comment (default sort order is date).", + }, ] app = FastAPI( - title="Kontroll API", - description="Implementering av BCF API 3.0", - version="0.0.1", - openapi_tags=endpoint_metadata + title="Kontroll API", description="Implementering av BCF API 3.0", version="0.0.1", openapi_tags=endpoint_metadata ) # Configure app to accept requests from anywhere @@ -98,23 +129,21 @@ app.add_middleware( allow_headers=["*"], ) -app.include_router(foundation.router, prefix='') -app.include_router(bcf.router, prefix='') -app.include_router(documents.router, prefix='') +app.include_router(foundation.router, prefix="") +app.include_router(bcf.router, prefix="") +app.include_router(documents.router, prefix="") templates = Jinja2Templates(directory="templates") @app.get("/", response_class=HTMLResponse) def index(request: Request): - return templates.TemplateResponse( - "index.html", - {"request": request}) + return templates.TemplateResponse("index.html", {"request": request}) -favicon_path = 'favicon.ico' +favicon_path = "favicon.ico" -@app.get('/favicon.ico', include_in_schema=False) +@app.get("/favicon.ico", include_in_schema=False) async def favicon(): return FileResponse(favicon_path) diff --git a/src/opencdeserver/api/app/models/bcf_common.py b/src/opencdeserver/api/app/models/bcf_common.py index 1227b886c9..e729393506 100644 --- a/src/opencdeserver/api/app/models/bcf_common.py +++ b/src/opencdeserver/api/app/models/bcf_common.py @@ -55,13 +55,13 @@ class ClippingPlane(BaseModel): class SnapshotType(Enum): - jpg = 'jpg' - png = 'png' + jpg = "jpg" + png = "png" class BitmapType(Enum): - jpg = 'jpg' - png = 'png' + jpg = "jpg" + png = "png" class Component(BaseModel): diff --git a/src/opencdeserver/api/app/models/bcf_response.py b/src/opencdeserver/api/app/models/bcf_response.py index c4e39eed62..2b54ce014a 100644 --- a/src/opencdeserver/api/app/models/bcf_response.py +++ b/src/opencdeserver/api/app/models/bcf_response.py @@ -2,9 +2,9 @@ from models.bcf_common import * class ProjectAction(Enum): - update = 'update' - createTopic = 'createTopic' - createDocument = 'createDocument' + update = "update" + createTopic = "createTopic" + createDocument = "createDocument" class ProjectGETAuthorization(BaseModel): @@ -18,19 +18,19 @@ class ProjectGET(BaseModel): class TopicAction(Enum): - update = 'update' - updateBimSnippet = 'updateBimSnippet' - updateRelatedTopics = 'updateRelatedTopics' - updateDocumentReferences = 'updateDocumentReferences' - updateFiles = 'updateFiles' - createComment = 'createComment' - createViewpoint = 'createViewpoint' - delete = 'delete' + update = "update" + updateBimSnippet = "updateBimSnippet" + updateRelatedTopics = "updateRelatedTopics" + updateDocumentReferences = "updateDocumentReferences" + updateFiles = "updateFiles" + createComment = "createComment" + createViewpoint = "createViewpoint" + delete = "delete" class CommentAction(Enum): - update = 'update' - delete = 'delete' + update = "update" + delete = "delete" class ExtensionsGET(BaseModel): @@ -123,7 +123,7 @@ class SnapshotGET(BaseModel): class ViewpointAction(Enum): - delete = 'delete' + delete = "delete" class ViewpointGETAuthorization(BaseModel): @@ -174,6 +174,8 @@ class TopicEventGET(BaseModel): topic_guid: str date: str author: str + + # actions: Optional[List[EventAction]] = Field(None, min_items=1) @@ -182,6 +184,8 @@ class CommentEventGET(BaseModel): topic_guid: str date: str author: str + + # actions: Optional[List[EventAction]] = Field(None, min_items=1) diff --git a/src/opencdeserver/api/app/models/documents_common.py b/src/opencdeserver/api/app/models/documents_common.py index ac67e3f111..91db18849a 100644 --- a/src/opencdeserver/api/app/models/documents_common.py +++ b/src/opencdeserver/api/app/models/documents_common.py @@ -10,12 +10,10 @@ from typing import List, Optional class CallbackLink(BaseModel): url: constr(min_length=1) = Field( - description='The server will web-browser-redirect to this URL once the user has completed selecting ' - 'documents or entering document metadata on the CDE' - ) - expires_in: int = Field( - description='The expiry period for the URL, in seconds' + description="The server will web-browser-redirect to this URL once the user has completed selecting " + "documents or entering document metadata on the CDE" ) + expires_in: int = Field(description="The expiry period for the URL, in seconds") # ---- RESPONSE MODELS ---- # @@ -35,62 +33,52 @@ class DocumentVersionLinks(BaseModel): class FileDescription(BaseModel): name: constr(min_length=1) = Field( - description='The name of the document version file on the server. The files are named by ' - 'document_id.file_ending', - example='908e1cd4-2e09-11ee-be56-0242ac120002.ifc' - ) - size_in_bytes: int = Field( - description='The size of the file in bytes', - example='124563' + description="The name of the document version file on the server. The files are named by " + "document_id.file_ending", + example="908e1cd4-2e09-11ee-be56-0242ac120002.ifc", ) + size_in_bytes: int = Field(description="The size of the file in bytes", example="124563") class Document(BaseModel): document_id: constr(min_length=1) = Field( - description='A machine readable identifier that can be used to uniquely identify this version in future calls ' - 'UUID is used - see `Query` section', - example='908e1cd4-2e09-11ee-be56-0242ac120002' + description="A machine readable identifier that can be used to uniquely identify this version in future calls " + "UUID is used - see `Query` section", + example="908e1cd4-2e09-11ee-be56-0242ac120002", ) session_file_id: Optional[str] = Field( - description='A machine readable identifier that can be used to uniquely the file ' - 'during the upload session, UUID is used', - example='908e1cd4-2e09-11ee-be56-0242ac120002' + description="A machine readable identifier that can be used to uniquely the file " + "during the upload session, UUID is used", + example="908e1cd4-2e09-11ee-be56-0242ac120002", ) version_index: int = Field( - description='A machine readable sequence number of the version of the document. The sequence must be ordered, ' - 'so that newer versions have higher values than previous ones. Each version index must be unique ' - 'for that document, but there may be gaps in the sequence', - example='12' + description="A machine readable sequence number of the version of the document. The sequence must be ordered, " + "so that newer versions have higher values than previous ones. Each version index must be unique " + "for that document, but there may be gaps in the sequence", + example="12", ) version_number: Optional[constr(min_length=1)] = Field( - description='A human readable version number. This is not expected to be in any specific format across CDEs ' - 'and may hold any value', - example='V2.0-larger' + description="A human readable version number. This is not expected to be in any specific format across CDEs " + "and may hold any value", + example="V2.0-larger", ) creation_date: str = Field( - description='The creation date of the document revision', - example='2016-04-28T16:31:12.270+02:00' + description="The creation date of the document revision", example="2016-04-28T16:31:12.270+02:00" ) title: Optional[constr(min_length=1)] = Field( - description='A human readable code or identifier. Metadata entered by user in CDE.', - example='Large garage' + description="A human readable code or identifier. Metadata entered by user in CDE.", example="Large garage" ) original_file_name: Optional[str] = Field( - description='The full name of the file as sent to the API', - example='First_floor_vent.ifc') - file_ending: Optional[str] = Field( - description='The ending of the file name, including the dot', - example='.ifc') - mime_type: Optional[str] = Field( - description='The mime type identifier', - example='application/x-step') - file_type: Optional[str] = Field( - description='The full name of the file type', - example='STEP Physical File (SPF)') + description="The full name of the file as sent to the API", example="First_floor_vent.ifc" + ) + file_ending: Optional[str] = Field(description="The ending of the file name, including the dot", example=".ifc") + mime_type: Optional[str] = Field(description="The mime type identifier", example="application/x-step") + file_type: Optional[str] = Field(description="The full name of the file type", example="STEP Physical File (SPF)") project: Optional[str] = Field( - description='The project to which the document will belong once it has been uploaded,' - 'this information is added as metadata by the user in the CDE', - example='908e1cd4-2e09-11ee-be56-0242ac120003') + description="The project to which the document will belong once it has been uploaded," + "this information is added as metadata by the user in the CDE", + example="908e1cd4-2e09-11ee-be56-0242ac120003", + ) file_description: FileDescription parts: Optional[List[str]] diff --git a/src/opencdeserver/api/app/models/documents_other.py b/src/opencdeserver/api/app/models/documents_other.py index bc848dfa6b..bb721c6b27 100644 --- a/src/opencdeserver/api/app/models/documents_other.py +++ b/src/opencdeserver/api/app/models/documents_other.py @@ -21,8 +21,8 @@ class ProjectOnly(BaseModel): class DataForUploadDocuments(BaseModel): server_context: Optional[str] = Field( description="A CDE controlled identifier recording the user's context on the CDE. For example which project " - 'and folder the user was on. If the client provides the `server_context` in subsequent calls then ' - 'the CDE will attemp to load the UI at the same place.' + "and folder the user was on. If the client provides the `server_context` in subsequent calls then " + "the CDE will attemp to load the UI at the same place." ) documents: List[FileToUpload] callback: Optional[CallbackLink] @@ -32,25 +32,24 @@ class DataForUploadDocuments(BaseModel): # ---- DOWNLOAD MODELS ---- + class DocumentMetadataEntries(BaseModel): - metadata: List[DocumentMetadataEntry] = Field( - description='An array of metadata entries' - ) + metadata: List[DocumentMetadataEntry] = Field(description="An array of metadata entries") class Project(BaseModel): project_id: str name: str documents: Optional[List[Document]] = Field( - description='An array containing all the documents selected by the user' + description="An array containing all the documents selected by the user" ) class DataForDocumentSelection(BaseModel): server_context: Optional[str] = Field( description="A CDE controlled identifier recording the user's context on the CDE. For example which project " - 'and folder the user was on. If the client provides the `server_context` in subsequent calls then ' - 'the CDE will attemp to load the UI at the same place.' + "and folder the user was on. If the client provides the `server_context` in subsequent calls then " + "the CDE will attemp to load the UI at the same place." ) projects: List[Project] callback: Optional[CallbackLink] @@ -61,24 +60,8 @@ class DataForDocumentSelection(BaseModel): file_types = { - 'smc': { - 'file_type': 'Solibri Model Checker', - 'file_ending': '.smc', - 'mime_type': 'application/octet-stream' - }, - 'ifc': { - 'file_type': 'STEP Physical File', - 'file_ending': '.ifc', - 'mime_type': 'application/x-step' - }, - 'ifczip': { - 'file_type': 'ZIP of a STEP Physical File', - 'file_ending': '.ifcZIP', - 'mime_type': 'application/zip' - }, - 'pdf': { - 'file_type': 'Adobe Portable Document Format', - 'file_ending': '.pdf', - 'mime_type': 'application/pdf' - } + "smc": {"file_type": "Solibri Model Checker", "file_ending": ".smc", "mime_type": "application/octet-stream"}, + "ifc": {"file_type": "STEP Physical File", "file_ending": ".ifc", "mime_type": "application/x-step"}, + "ifczip": {"file_type": "ZIP of a STEP Physical File", "file_ending": ".ifcZIP", "mime_type": "application/zip"}, + "pdf": {"file_type": "Adobe Portable Document Format", "file_ending": ".pdf", "mime_type": "application/pdf"}, } diff --git a/src/opencdeserver/api/app/models/documents_request.py b/src/opencdeserver/api/app/models/documents_request.py index 91ff3c4fb9..0d9fb0551e 100644 --- a/src/opencdeserver/api/app/models/documents_request.py +++ b/src/opencdeserver/api/app/models/documents_request.py @@ -13,15 +13,15 @@ from models.documents_common import CallbackLink class FileToUpload(BaseModel): file_name: constr(min_length=1) = Field( - description='The CDE UI will display this value to the User when entering document metadata. This is the ' - 'original name of the file. This attribute is the same as the name attribute in a document model.' + description="The CDE UI will display this value to the User when entering document metadata. This is the " + "original name of the file. This attribute is the same as the name attribute in a document model." ) session_file_id: constr(min_length=1) = Field( - description='This is a client provided id to differentiate between multiple files that are being uploaded in ' - 'the same session' + description="This is a client provided id to differentiate between multiple files that are being uploaded in " + "the same session" ) document_id: Optional[constr(min_length=1)] = Field( - description='When present, indicates that this upload is a new version of an existing document' + description="When present, indicates that this upload is a new version of an existing document" ) @@ -29,19 +29,17 @@ class UploadDocuments(BaseModel): callback: CallbackLink server_context: Optional[str] = Field( description="A CDE controlled identifier recording the user's context on the CDE. For example which project " - 'and folder the user was on. If the client provides the `server_context` in subsequent calls then ' - 'the CDE will attemp to load the UI at the same place.' + "and folder the user was on. If the client provides the `server_context` in subsequent calls then " + "the CDE will attemp to load the UI at the same place." ) files: List[FileToUpload] class UploadFileDetail(BaseModel): - size_in_bytes: int = Field( - description='The uploaded file size' - ) + size_in_bytes: int = Field(description="The uploaded file size") session_file_id: constr(min_length=1) = Field( - description='This is a client provided id to differentiate between multiple files that are being uploaded in ' - 'the same session' + description="This is a client provided id to differentiate between multiple files that are being uploaded in " + "the same session" ) @@ -56,37 +54,31 @@ class SelectDocuments(BaseModel): callback: CallbackLink server_context: Optional[str] = Field( description="A CDE controlled identifier recording the user's context on the CDE. For example which project " - 'and folder the user was on. If the client provides the `server_context` in subsequent calls then ' - 'the CDE will attemp to load the UI at the same place.' + "and folder the user was on. If the client provides the `server_context` in subsequent calls then " + "the CDE will attemp to load the UI at the same place." ) supported_file_extensions: Optional[List[str]] = Field( - description='The client may optionally provide an array of accepted file extensions that should be opened ' - 'during this flow. The CDE server UI should make an attempt to only show files matching these ' - 'extensions to the user for the download selection or help the user in selecting the desired ' - 'files. However, the server does not have to guarantee that only files matching the extensions ' - 'will be selected. The extensions here must contain the dot separator.', - example=['.ifc', '.ifczip'] + description="The client may optionally provide an array of accepted file extensions that should be opened " + "during this flow. The CDE server UI should make an attempt to only show files matching these " + "extensions to the user for the download selection or help the user in selecting the desired " + "files. However, the server does not have to guarantee that only files matching the extensions " + "will be selected. The extensions here must contain the dot separator.", + example=[".ifc", ".ifczip"], ) class DataType(Enum): - string = 'string' - boolean = 'boolean' - date_time = 'date-time' - date = 'date' - integer32 = 'integer32' - integer64 = 'integer64' - number = 'number' - url = 'url' + string = "string" + boolean = "boolean" + date_time = "date-time" + date = "date" + integer32 = "integer32" + integer64 = "integer64" + number = "number" + url = "url" class DocumentMetadataEntry(BaseModel): - name: constr(min_length=1) = Field( - description='The name of the metadata property' - ) - value: List[constr(min_length=1)] = Field( - description='The value of the metadata property, can be a list' - ) - data_type: DataType = Field( - description='The data type of the items in the value array' - ) + name: constr(min_length=1) = Field(description="The name of the metadata property") + value: List[constr(min_length=1)] = Field(description="The value of the metadata property, can be a list") + data_type: DataType = Field(description="The data type of the items in the value array") diff --git a/src/opencdeserver/api/app/models/documents_response.py b/src/opencdeserver/api/app/models/documents_response.py index 3b60a47dfe..8b4bc308d7 100644 --- a/src/opencdeserver/api/app/models/documents_response.py +++ b/src/opencdeserver/api/app/models/documents_response.py @@ -13,20 +13,18 @@ from models.documents_common import DocumentVersion, LinkData class DocumentUploadSessionInitialization(BaseModel): upload_ui_url: constr(min_length=1) = Field( - description='A CDE UI URL for the client to open in a local browser. The user would enter document metadata ' - 'directly in the CDE' - ) - expires_in: int = Field( - description='`upload_ui_url` expiry in seconds' + description="A CDE UI URL for the client to open in a local browser. The user would enter document metadata " + "directly in the CDE" ) + expires_in: int = Field(description="`upload_ui_url` expiry in seconds") max_size_in_bytes: int = Field( - description='The maximum file size supported by the CDE. Attempts to upload a larger file will fail' + description="The maximum file size supported by the CDE. Attempts to upload a larger file will fail" ) class HttpMethod(Enum): - POST = 'POST' - PUT = 'PUT' + POST = "POST" + PUT = "PUT" class HeaderValue(BaseModel): @@ -40,12 +38,12 @@ class Headers(BaseModel): class MultipartFormData(BaseModel): prefix: str = Field( - description='This is a server provided value. Its value must be prefixed to the binary content body when ' - 'uploading this part' + description="This is a server provided value. Its value must be prefixed to the binary content body when " + "uploading this part" ) suffix: str = Field( - description='This is a server provided value. Its value must be suffixed to the binary content body when ' - 'uploading this part. Typically, this is the end boundary for a multipart/form-data request' + description="This is a server provided value. Its value must be suffixed to the binary content body when " + "uploading this part. Typically, this is the end boundary for a multipart/form-data request" ) @@ -54,26 +52,22 @@ class UploadFilePartInstruction(BaseModel): http_method: HttpMethod additional_headers: Optional[Headers] = None include_authorization: Optional[bool] = Field( - description='Whether or not to include the authorization request header in the file upload request. ' - 'Including the authorization header with some cloud storage providers might fail the request' + description="Whether or not to include the authorization request header in the file upload request. " + "Including the authorization header with some cloud storage providers might fail the request" ) multipart_form_data: Optional[MultipartFormData] = None - content_range_start: int = Field( - description='The inclusive, zero index based start for this part' - ) - content_range_end: int = Field( - description='The inclusive, zero index based end for this part' - ) + content_range_start: int = Field(description="The inclusive, zero index based start for this part") + content_range_end: int = Field(description="The inclusive, zero index based end for this part") class DocumentToUpload(BaseModel): session_file_id: constr(min_length=1) = Field( - description='A client-provided identifier that allows matching the specification with the correct file on the ' - "user's machine" + description="A client-provided identifier that allows matching the specification with the correct file on the " + "user's machine" ) upload_file_parts: List[UploadFilePartInstruction] = Field( - description='An array of request specifications detailing how to split the file to parts and upload each part ' - 'to the CDE' + description="An array of request specifications detailing how to split the file to parts and upload each part " + "to the CDE" # min_length=1, ) upload_completion: LinkData @@ -83,8 +77,8 @@ class DocumentToUpload(BaseModel): class DocumentsToUpload(BaseModel): server_context: Optional[str] = Field( description="A CDE controlled identifier recording the user's context on the CDE. For example which project " - 'and folder the user was on. If the client provides the `server_context` in subsequent calls then ' - 'the CDE will attemp to load the UI at the same place.' + "and folder the user was on. If the client provides the `server_context` in subsequent calls then " + "the CDE will attemp to load the UI at the same place." ) documents_to_upload: Optional[List[DocumentToUpload]] @@ -94,12 +88,10 @@ class DocumentsToUpload(BaseModel): class DocumentDiscoverySessionInitialization(BaseModel): select_documents_url: constr(min_length=1) = Field( - description='A CDE UI URL for the client to open in a local browser. The user would search and select ' - 'documents directly in the CDE' - ) - expires_in: int = Field( - description='`select_documents_url` expiry in seconds' + description="A CDE UI URL for the client to open in a local browser. The user would search and select " + "documents directly in the CDE" ) + expires_in: int = Field(description="`select_documents_url` expiry in seconds") class DocumentsMarkedAsSelected(BaseModel): @@ -109,29 +101,25 @@ class DocumentsMarkedAsSelected(BaseModel): class SelectedDocuments(BaseModel): server_context: Optional[str] = Field( description="A CDE controlled identifier recording the user's context on the CDE. For example which project " - 'and folder the user was on. If the client provides the `server_context` in subsequent calls then ' - 'the CDE will attemp to load the UI at the same place.' - ) - documents: List[DocumentVersion] = Field( - description='An array containing all the documents selected by the user' + "and folder the user was on. If the client provides the `server_context` in subsequent calls then " + "the CDE will attemp to load the UI at the same place." ) + documents: List[DocumentVersion] = Field(description="An array containing all the documents selected by the user") class DocumentMetadata(BaseModel): session_file_id: constr(min_length=1) = Field( - description='This is a client provided id to differentiate between multiple files that are being uploaded in ' - 'the same session' + description="This is a client provided id to differentiate between multiple files that are being uploaded in " + "the same session" ) document_id: Optional[constr(min_length=1)] = Field( - description='When present, indicates that this upload is a new version of an existing document' + description="When present, indicates that this upload is a new version of an existing document" ) version_number: constr(min_length=1) = Field( - description='A human readable version number. This is not expected to be in any specific format across CDEs ' - 'and may hold any value' - ) - title: constr(min_length=1) = Field( - description='A human readable code or identifier' + description="A human readable version number. This is not expected to be in any specific format across CDEs " + "and may hold any value" ) + title: constr(min_length=1) = Field(description="A human readable code or identifier") project: Optional[str] diff --git a/src/opencdeserver/api/app/models/response.py b/src/opencdeserver/api/app/models/response.py index a41e1d9302..22145a4945 100644 --- a/src/opencdeserver/api/app/models/response.py +++ b/src/opencdeserver/api/app/models/response.py @@ -1,4 +1 @@ from pydantic import BaseModel - - - diff --git a/src/opencdeserver/api/app/repository/bcf.py b/src/opencdeserver/api/app/repository/bcf.py index 8c881286ed..bce4ca1c1f 100644 --- a/src/opencdeserver/api/app/repository/bcf.py +++ b/src/opencdeserver/api/app/repository/bcf.py @@ -45,9 +45,7 @@ class BCFDB(MyDB): AND p.project_id = $project_id RETURN p AS project """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id)) + result = tx.run(cypher, username=current_user.username, project_id=str(project_id)) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") @@ -69,10 +67,9 @@ class BCFDB(MyDB): SET p.name = $project_name RETURN p AS project """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - project_name=project.name) + result = tx.run( + cypher, username=current_user.username, project_id=str(project_id), project_name=project.name + ) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") @@ -93,9 +90,7 @@ class BCFDB(MyDB): AND p.project_id = $project_id RETURN e AS extensions """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id)) + result = tx.run(cypher, username=current_user.username, project_id=str(project_id)) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") @@ -117,9 +112,7 @@ class BCFDB(MyDB): AND p.project_id = $project_id RETURN t AS topic, ID(t) AS server_assigned_id """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id)) + results = tx.run(cypher, username=current_user.username, project_id=str(project_id)) topic_model_list = list() for result in results: topic_json = self.node_to_json(result.get("topic")) @@ -142,10 +135,7 @@ class BCFDB(MyDB): AND t.guid = $topic_id RETURN t AS topic, ID(t) AS server_assigned_id """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id)) + result = tx.run(cypher, username=current_user.username, project_id=str(project_id), topic_id=str(topic_id)) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") @@ -181,24 +171,26 @@ class BCFDB(MyDB): t.due_date = $due_date RETURN t AS topic, ID(t) AS server_assigned_id """ - if not hasattr(topic, 'guid') or topic.guid is None: + if not hasattr(topic, "guid") or topic.guid is None: topic.guid = uuid4() creation_date = self.timestamp() print(cypher) - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - guid=str(topic.guid), - creation_date=creation_date, - topic_type=topic.topic_type, - topic_status=topic.topic_status, - title=topic.title, - priority=topic.priority, - index=topic.index, - assigned_to=topic.assigned_to, - stage=topic.stage, - description=topic.description, - due_date=topic.due_date) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + guid=str(topic.guid), + creation_date=creation_date, + topic_type=topic.topic_type, + topic_status=topic.topic_status, + title=topic.title, + priority=topic.priority, + index=topic.index, + assigned_to=topic.assigned_to, + stage=topic.stage, + description=topic.description, + due_date=topic.due_date, + ) summary = result.consume() if summary.counters.nodes_created < 1: raise HTTPException(status_code=400, detail="Item not added.") @@ -230,20 +222,22 @@ class BCFDB(MyDB): t.due_date = $due_date RETURN t AS topic, ID(t) AS server_assigned_id """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - topic_type=topic.topic_type, - topic_status=topic.topic_status, - title=topic.title, - priority=topic.priority, - index=topic.index, - modified_date=self.timestamp(), - assigned_to=topic.assigned_to, - stage=topic.stage, - description=topic.description, - due_date=topic.due_date) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + topic_type=topic.topic_type, + topic_status=topic.topic_status, + title=topic.title, + priority=topic.priority, + index=topic.index, + modified_date=self.timestamp(), + assigned_to=topic.assigned_to, + stage=topic.stage, + description=topic.description, + due_date=topic.due_date, + ) summary = result.consume() if summary.counters.properties_set < 1: raise HTTPException(status_code=404, detail="Item not found.") @@ -265,11 +259,13 @@ class BCFDB(MyDB): AND t.guid = $topic_id SET t.modified_date = $modified_date """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - modified_date=self.timestamp()) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + modified_date=self.timestamp(), + ) summary = result.consume() if summary.counters.properties_set < 1: raise HTTPException(status_code=404, detail="Item not found.") @@ -289,10 +285,7 @@ class BCFDB(MyDB): AND t.guid = $topic_id DETACH DELETE t """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id)) + result = tx.run(cypher, username=current_user.username, project_id=str(project_id), topic_id=str(topic_id)) result_summary = result.consume() if result_summary.counters.nodes_deleted < 1: raise HTTPException(status_code=404, detail="Item not found.") @@ -312,35 +305,29 @@ class BCFDB(MyDB): AND p.project_id = $project_id RETURN d AS document """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id)) + results = tx.run(cypher, username=current_user.username, project_id=str(project_id)) project_file_informations = list() for result in results: document_json = self.node_to_json(result.get("document")) display_information_list = list() - display_information_json = { - 'field_display_name': 'File', - 'field_value': document_json['title'] - } + display_information_json = {"field_display_name": "File", "field_value": document_json["title"]} display_information_list.append(display_information_json) - base = os.environ['KONTROLL_BASE_URL'] + "documents/1.0/document/" - base.replace('http://', 'https://') - base.replace('https://', 'open-cde-documents://') - document_id = document_json['document_id'] - version_index = document_json['version_index'] + base = os.environ["KONTROLL_BASE_URL"] + "documents/1.0/document/" + base.replace("http://", "https://") + base.replace("https://", "open-cde-documents://") + document_id = document_json["document_id"] + version_index = document_json["version_index"] extra_path = document_id + "/version/" + str(version_index) reference = base + extra_path file_get_json = { - 'ifc_project': document_json['ifc_project'], - 'filename': document_json['name'], - 'reference': reference + "ifc_project": document_json["ifc_project"], + "filename": document_json["name"], + "reference": reference, } - project_file_information = ProjectFileInformation(**{ - 'display_information': display_information_list, - 'file': file_get_json - }) + project_file_information = ProjectFileInformation( + **{"display_information": display_information_list, "file": file_get_json} + ) project_file_informations.append(project_file_information) return project_file_informations @@ -406,10 +393,7 @@ class BCFDB(MyDB): AND t.guid = $topic_id RETURN f AS file """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id)) + results = tx.run(cypher, username=current_user.username, project_id=str(project_id), topic_id=str(topic_id)) file_model_list = list() for result in results: file_json = self.node_to_json(result.get("file")) @@ -431,10 +415,12 @@ class BCFDB(MyDB): AND t.guid = $topic_id DELETE r3 """ - tx.run(cypher_delete_references, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id)) + tx.run( + cypher_delete_references, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + ) cypher_update_and_attach_references = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(f:Document:Model) MATCH (u)-[r1]->(p)-[r3:HAS]->(t:Topic) @@ -451,15 +437,17 @@ class BCFDB(MyDB): RETURN f """ for file in files: - result = tx.run(cypher_update_and_attach_references, - username=current_user.username, - project_id=str(project_id), - reference=file.reference, - topic_id=str(topic_id), - filename=file.filename, - date=file.date, - ifc_project=file.ifc_project, - ifc_spatial_structure_element=file.ifc_spatial_structure_element) + result = tx.run( + cypher_update_and_attach_references, + username=current_user.username, + project_id=str(project_id), + reference=file.reference, + topic_id=str(topic_id), + filename=file.filename, + date=file.date, + ifc_project=file.ifc_project, + ifc_spatial_structure_element=file.ifc_spatial_structure_element, + ) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") @@ -480,14 +468,11 @@ class BCFDB(MyDB): AND t.guid = $topic_id RETURN c AS comment """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id)) + results = tx.run(cypher, username=current_user.username, project_id=str(project_id), topic_id=str(topic_id)) comment_model_list = list() for result in results: comment_json = self.node_to_json(result.get("comment")) - comment_json['topic_guid'] = str(topic_id) + comment_json["topic_guid"] = str(topic_id) comment_model = CommentGET(**comment_json) comment_model_list.append(comment_model) return comment_model_list @@ -507,17 +492,19 @@ class BCFDB(MyDB): AND c.guid = $comment_id RETURN c AS comment """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - comment_id=str(comment_id)) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + comment_id=str(comment_id), + ) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") comment_node = first.get("comment") comment_dict = self.node_to_json(comment_node) - comment_dict['topic_guid'] = str(topic_id) + comment_dict["topic_guid"] = str(topic_id) comment = CommentGET(**comment_dict) return comment @@ -529,7 +516,7 @@ class BCFDB(MyDB): def post_comment(self, project_id: UUID, topic_id: UUID, comment: CommentPOST, current_user: User) -> CommentGET: def post_comment_work(tx) -> UUID: - if not hasattr(comment, 'guid') or comment.guid is None: + if not hasattr(comment, "guid") or comment.guid is None: this_comment_guid = uuid4() else: this_comment_guid = UUID(comment.guid) @@ -539,26 +526,26 @@ class BCFDB(MyDB): viewpoint_guids = [viewpoint.guid for viewpoint in viewpoints] if comment.viewpoint_guid not in viewpoint_guids: raise HTTPException(status_code=400, detail="Item not added.") - injection_viewpoint = '-[r3:HAS]->(v:Viewpoint)' - injection_viewpoint_selection = 'AND v.guid = $viewpoint_id' - injection_viewpoint_relation = 'MERGE (c2)-[r7:RELATED_TO]->(v)' + injection_viewpoint = "-[r3:HAS]->(v:Viewpoint)" + injection_viewpoint_selection = "AND v.guid = $viewpoint_id" + injection_viewpoint_relation = "MERGE (c2)-[r7:RELATED_TO]->(v)" else: - injection_viewpoint = '' - injection_viewpoint_selection = '' - injection_viewpoint_relation = '' - if hasattr(comment, 'reply_to_comment_guid'): + injection_viewpoint = "" + injection_viewpoint_selection = "" + injection_viewpoint_relation = "" + if hasattr(comment, "reply_to_comment_guid"): # make sure that given reply_to_comment_guid exists in database get_comments = self.get_comments(project_id, topic_id, current_user) comment_guids = [get_comment.guid for get_comment in get_comments] if comment.reply_to_comment_guid not in comment_guids: raise HTTPException(status_code=400, detail="Item not added.") - injection_existing_comment = 'MATCH (u)-[r1]->(p)-[r2]->(t)-[r4:HAS]->(c1:Comment)' - injection_existing_comment_selection = 'AND c1.guid = $reply_to_comment_id' - injection_existing_comment_relation = 'MERGE (c1)-[r5:THEN]->(c2)' + injection_existing_comment = "MATCH (u)-[r1]->(p)-[r2]->(t)-[r4:HAS]->(c1:Comment)" + injection_existing_comment_selection = "AND c1.guid = $reply_to_comment_id" + injection_existing_comment_relation = "MERGE (c1)-[r5:THEN]->(c2)" else: - injection_existing_comment = '' - injection_existing_comment_selection = '' - injection_existing_comment_relation = '' + injection_existing_comment = "" + injection_existing_comment_selection = "" + injection_existing_comment_relation = "" cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)%s %s @@ -576,21 +563,25 @@ class BCFDB(MyDB): c2.author = $username, c2.modified_author = $username, c2.comment = $comment - """ % (injection_viewpoint, - injection_existing_comment, - injection_viewpoint_selection, - injection_existing_comment_selection, - injection_viewpoint_relation, - injection_existing_comment_relation) - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(comment.viewpoint_guid), - reply_to_comment_id=str('comment.reply_to_comment_guid'), - comment_id=str(this_comment_guid), - creation_date=self.timestamp(), - comment=comment.comment) + """ % ( + injection_viewpoint, + injection_existing_comment, + injection_viewpoint_selection, + injection_existing_comment_selection, + injection_viewpoint_relation, + injection_existing_comment_relation, + ) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(comment.viewpoint_guid), + reply_to_comment_id=str("comment.reply_to_comment_guid"), + comment_id=str(this_comment_guid), + creation_date=self.timestamp(), + comment=comment.comment, + ) summary = result.consume() if summary.counters.nodes_created < 1: raise HTTPException(status_code=400, detail="Item not added.") @@ -600,21 +591,22 @@ class BCFDB(MyDB): comment_guid = session.execute_write(post_comment_work) return self.get_comment(project_id, topic_id, comment_guid, current_user) - def put_comment(self, project_id: UUID, topic_id: UUID, comment_id: UUID, comment: CommentPUT, - current_user: User) -> CommentGET: + def put_comment( + self, project_id: UUID, topic_id: UUID, comment_id: UUID, comment: CommentPUT, current_user: User + ) -> CommentGET: def put_comment_work(tx) -> bool: if comment.viewpoint_guid: viewpoints = self.get_viewpoints(project_id, topic_id, current_user) viewpoint_guids = [viewpoint.guid for viewpoint in viewpoints] if comment.viewpoint_guid not in viewpoint_guids: raise HTTPException(status_code=400, detail="Item not added.") - injection_new_viewpoint = 'MATCH (t)-[r5:HAS]->(v_new:Viewpoint)' - injection_new_viewpoint_selection = 'WHERE v_new.guid = $new_rtv_id' - injection_new_viewpoint_relation = 'MERGE (c)-[r5:RELATED_TO]->(v)' + injection_new_viewpoint = "MATCH (t)-[r5:HAS]->(v_new:Viewpoint)" + injection_new_viewpoint_selection = "WHERE v_new.guid = $new_rtv_id" + injection_new_viewpoint_relation = "MERGE (c)-[r5:RELATED_TO]->(v)" else: - injection_new_viewpoint = '' - injection_new_viewpoint_selection = '' - injection_new_viewpoint_relation = '' + injection_new_viewpoint = "" + injection_new_viewpoint_selection = "" + injection_new_viewpoint_relation = "" cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(c:Comment) WHERE u.username = $username @@ -630,17 +622,21 @@ class BCFDB(MyDB): SET c.modified_author = $username DELETE r4 %s - """ % (injection_new_viewpoint, - injection_new_viewpoint_selection, - injection_new_viewpoint_relation) - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - comment_id=str(comment_id), - new_rtv_id=str(comment.viewpoint_guid), - comment=comment.comment, - modified_date=self.timestamp()) + """ % ( + injection_new_viewpoint, + injection_new_viewpoint_selection, + injection_new_viewpoint_relation, + ) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + comment_id=str(comment_id), + new_rtv_id=str(comment.viewpoint_guid), + comment=comment.comment, + modified_date=self.timestamp(), + ) summary = result.consume() if summary.counters.properties_set < 1: raise HTTPException(status_code=400, detail="Item not added.") @@ -662,11 +658,13 @@ class BCFDB(MyDB): AND c.guid = $comment_id DETACH DELETE c """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - comment_id=str(comment_id)) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + comment_id=str(comment_id), + ) result_summary = result.consume() if result_summary.counters.nodes_deleted < 1: raise HTTPException(status_code=404, detail="Item not found.") @@ -678,27 +676,29 @@ class BCFDB(MyDB): @staticmethod def enum_to_mime(enum_type) -> str: - if str(enum_type) == 'png': - mime_type = 'image/png' - elif str(enum_type) == 'jpg' or str(enum_type) == 'jpeg': - mime_type = 'image/jpeg' + if str(enum_type) == "png": + mime_type = "image/png" + elif str(enum_type) == "jpg" or str(enum_type) == "jpeg": + mime_type = "image/jpeg" else: - mime_type = 'image/' + str(enum_type) + mime_type = "image/" + str(enum_type) return mime_type # implementing - def post_viewpoint(self, project_id: UUID, topic_id: UUID, viewpoint: ViewpointPOST, - current_user: User) -> ViewpointGET: + def post_viewpoint( + self, project_id: UUID, topic_id: UUID, viewpoint: ViewpointPOST, current_user: User + ) -> ViewpointGET: def post_viewpoint_work(tx) -> str: - if hasattr(viewpoint, 'snapshot'): + if hasattr(viewpoint, "snapshot"): snapshot_type = self.enum_to_mime(viewpoint.snapshot.snapshot_type.value) snapshot = True - set_snapshot = 'v.snapshot = $snapshot, v.snapshot_type = $snapshot_type,' + set_snapshot = "v.snapshot = $snapshot, v.snapshot_type = $snapshot_type," else: - snapshot_type = '' + snapshot_type = "" snapshot = False - set_snapshot = '' - cypher_viewpoint = """ + set_snapshot = "" + cypher_viewpoint = ( + """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) WHERE u.username = $username AND r1.createViewpoint = True @@ -716,41 +716,51 @@ class BCFDB(MyDB): v.spaces_visible = $spaces_visible, v.space_boundaries_visible = $space_boundaries_visible, v.openings_visible = $openings_visible - """ % set_snapshot + """ + % set_snapshot + ) if viewpoint.guid is None: viewpoint.guid = uuid4() if viewpoint.orthogonal_camera is None: - camera_definition = 'perspective_camera' + camera_definition = "perspective_camera" view_tws_or_fo_view = viewpoint.perspective_camera.field_of_view aspect_ratio = viewpoint.perspective_camera.aspect_ratio cvp = viewpoint.perspective_camera.camera_view_point cad = viewpoint.perspective_camera.camera_direction cuv = viewpoint.perspective_camera.camera_up_vector else: - camera_definition = 'orthogonal_camera' + camera_definition = "orthogonal_camera" view_tws_or_fo_view = viewpoint.orthogonal_camera.view_to_world_scale aspect_ratio = viewpoint.orthogonal_camera.aspect_ratio cvp = viewpoint.orthogonal_camera.camera_view_point cad = viewpoint.orthogonal_camera.camera_direction cuv = viewpoint.orthogonal_camera.camera_up_vector - result = tx.run(cypher_viewpoint, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - guid=str(viewpoint.guid), - index=viewpoint.index, - camera_definition=camera_definition, - cvp_x=cvp.x, cvp_y=cvp.y, cvp_z=cvp.z, - cad_x=cad.x, cad_y=cad.y, cad_z=cad.z, - cuv_x=cuv.x, cuv_y=cuv.y, cuv_z=cuv.z, - view_tws_or_fo_view=view_tws_or_fo_view, - aspect_ratio=aspect_ratio, - snapshot=snapshot, - snapshot_type=snapshot_type, - default_visibility=viewpoint.components.visibility.default_visibility, - spaces_visible=viewpoint.components.visibility.view_setup_hints.spaces_visible, - space_boundaries_visible=viewpoint.components.visibility.view_setup_hints.space_boundaries_visible, - openings_visible=viewpoint.components.visibility.view_setup_hints.openings_visible) + result = tx.run( + cypher_viewpoint, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + guid=str(viewpoint.guid), + index=viewpoint.index, + camera_definition=camera_definition, + cvp_x=cvp.x, + cvp_y=cvp.y, + cvp_z=cvp.z, + cad_x=cad.x, + cad_y=cad.y, + cad_z=cad.z, + cuv_x=cuv.x, + cuv_y=cuv.y, + cuv_z=cuv.z, + view_tws_or_fo_view=view_tws_or_fo_view, + aspect_ratio=aspect_ratio, + snapshot=snapshot, + snapshot_type=snapshot_type, + default_visibility=viewpoint.components.visibility.default_visibility, + spaces_visible=viewpoint.components.visibility.view_setup_hints.spaces_visible, + space_boundaries_visible=viewpoint.components.visibility.view_setup_hints.space_boundaries_visible, + openings_visible=viewpoint.components.visibility.view_setup_hints.openings_visible, + ) summary = result.consume() if summary.counters.nodes_created < 1: print(cypher_viewpoint) @@ -765,10 +775,16 @@ class BCFDB(MyDB): SET l.start_point = point({x: $lsp_x, y: $lsp_y, z: $lsp_z}), l.end_point = point({x: $lep_x, y: $lep_y, z: $lep_z}) """ - result = tx.run(cypher_line, - guid=str(viewpoint.guid), - lsp_x=line.start_point.x, lsp_y=line.start_point.y, lsp_z=line.start_point.z, - lep_x=line.end_point.x, lep_y=line.end_point.y, lep_z=line.end_point.z) + result = tx.run( + cypher_line, + guid=str(viewpoint.guid), + lsp_x=line.start_point.x, + lsp_y=line.start_point.y, + lsp_z=line.start_point.z, + lep_x=line.end_point.x, + lep_y=line.end_point.y, + lep_z=line.end_point.z, + ) summary = result.consume() if summary.counters.nodes_created < 1: print(cypher_line) @@ -783,12 +799,16 @@ class BCFDB(MyDB): SET cp.location = point({x: $cpl_x, y: $cpl_y, z: $cpl_z}), cp.direction = point({x: $cpd_x, y: $cpd_y, z: $cpd_z}) """ - result = tx.run(cypher_clipping_plane, - guid=str(viewpoint.guid), - lsp_x=clipping_plane.location.x, lsp_y=clipping_plane.location.y, - lsp_z=clipping_plane.location.z, - lep_x=clipping_plane.direction.x, lep_y=clipping_plane.direction.y, - lep_z=clipping_plane.direction.z) + result = tx.run( + cypher_clipping_plane, + guid=str(viewpoint.guid), + lsp_x=clipping_plane.location.x, + lsp_y=clipping_plane.location.y, + lsp_z=clipping_plane.location.z, + lep_x=clipping_plane.direction.x, + lep_y=clipping_plane.direction.y, + lep_z=clipping_plane.direction.z, + ) summary = result.consume() if summary.counters.nodes_created < 1: print(cypher_clipping_plane) @@ -807,13 +827,21 @@ class BCFDB(MyDB): b.height = $height """ bitmap_type = self.enum_to_mime(bitmap.bitmap_type) - result = tx.run(cypher_bitmap, - guid=str(viewpoint.guid), - type=bitmap_type, - bml_x=bitmap.location.x, bml_y=bitmap.location.y, bml_z=bitmap.location.z, - bmn_x=bitmap.direction.x, bmn_y=bitmap.direction.y, bmn_z=bitmap.direction.z, - bmu_x=bitmap.direction.x, bmu_y=bitmap.direction.y, bmu_z=bitmap.direction.z, - height=bitmap.height) + result = tx.run( + cypher_bitmap, + guid=str(viewpoint.guid), + type=bitmap_type, + bml_x=bitmap.location.x, + bml_y=bitmap.location.y, + bml_z=bitmap.location.z, + bmn_x=bitmap.direction.x, + bmn_y=bitmap.direction.y, + bmn_z=bitmap.direction.z, + bmu_x=bitmap.direction.x, + bmu_y=bitmap.direction.y, + bmu_z=bitmap.direction.z, + height=bitmap.height, + ) summary = result.consume() if summary.counters.nodes_created < 1: print(bitmap_type) @@ -824,44 +852,44 @@ class BCFDB(MyDB): for selected_component in viewpoint.components.selection: if selected_component.ifc_guid not in components: components[selected_component.ifc_guid] = {} - components[selected_component.ifc_guid]['selected'] = True - components[selected_component.ifc_guid]['originating_system'] = selected_component.originating_system - components[selected_component.ifc_guid]['authoring_tool_id'] = selected_component.authoring_tool_id + components[selected_component.ifc_guid]["selected"] = True + components[selected_component.ifc_guid]["originating_system"] = selected_component.originating_system + components[selected_component.ifc_guid]["authoring_tool_id"] = selected_component.authoring_tool_id for color in viewpoint.components.coloring: for colored_component in color.components: if colored_component.ifc_guid not in components: components[colored_component.ifc_guid] = {} - components[colored_component.ifc_guid]['color'] = color.color + components[colored_component.ifc_guid]["color"] = color.color for exception_component in viewpoint.components.visibility.exceptions: if exception_component.ifc_guid not in components: components[exception_component.ifc_guid] = {} - components[exception_component.ifc_guid]['visibility_exception'] = True + components[exception_component.ifc_guid]["visibility_exception"] = True set_list = list() for ifc_guid in components.keys(): - set_list.append('c.ifc_guid = $ifc_guid') - if 'selected' in components[ifc_guid]: + set_list.append("c.ifc_guid = $ifc_guid") + if "selected" in components[ifc_guid]: selected = True - originating_system = components[ifc_guid]['originating_system'] - authoring_tool_id = components[ifc_guid]['authoring_tool_id'] - set_list.append('c.selected = $selected') - set_list.append('c.originating_system = $originating_system') - set_list.append('c.authoring_tool_id = $authoring_tool_id') + originating_system = components[ifc_guid]["originating_system"] + authoring_tool_id = components[ifc_guid]["authoring_tool_id"] + set_list.append("c.selected = $selected") + set_list.append("c.originating_system = $originating_system") + set_list.append("c.authoring_tool_id = $authoring_tool_id") else: selected = False originating_system = False authoring_tool_id = False - if 'color' in components[ifc_guid]: - color = components[ifc_guid]['color'] - set_list.append('c.color = $color') + if "color" in components[ifc_guid]: + color = components[ifc_guid]["color"] + set_list.append("c.color = $color") else: color = False - if 'visibility_exception' in components[ifc_guid]: + if "visibility_exception" in components[ifc_guid]: visibility_exception = True - set_list.append('c.visibility_exception = $visibility_exception') + set_list.append("c.visibility_exception = $visibility_exception") else: visibility_exception = False - set_string = ', '.join(set_list) + set_string = ", ".join(set_list) cypher_component = """ MATCH (v:Viewpoint) WHERE v.guid = $viewpoint_id @@ -869,14 +897,16 @@ class BCFDB(MyDB): SET """ cypher_component += set_string - tx.run(cypher_component, - viewpoint_id=str(viewpoint.guid), - ifc_guid=str(ifc_guid), - selected=selected, - originating_system=originating_system, - authoring_tool_id=authoring_tool_id, - color=color, - visibility_exception=visibility_exception) + tx.run( + cypher_component, + viewpoint_id=str(viewpoint.guid), + ifc_guid=str(ifc_guid), + selected=selected, + originating_system=originating_system, + authoring_tool_id=authoring_tool_id, + color=color, + visibility_exception=visibility_exception, + ) summary = result.consume() # Look for component UUIDs in IFC-graph and relate them @@ -908,26 +938,26 @@ class BCFDB(MyDB): print(cypher_component) raise HTTPException(status_code=400, detail="Item not added.") else: - snapshot_path = '' - bitmap_path = '' + snapshot_path = "" + bitmap_path = "" try: - snapshot_name = 'snapshot_' + str(viewpoint.guid) - file_ending = '.' + str(viewpoint.snapshot.snapshot_type.value) - snapshot_path = 'data/snapshots/' + snapshot_name + file_ending + snapshot_name = "snapshot_" + str(viewpoint.guid) + file_ending = "." + str(viewpoint.snapshot.snapshot_type.value) + snapshot_path = "data/snapshots/" + snapshot_name + file_ending with open(snapshot_path, "wb") as snapshot_file: snapshot_image = base64.b64decode(viewpoint.snapshot.snapshot_data, validate=True) snapshot_file.write(snapshot_image) snapshot_file.close() for bitmap in viewpoint.bitmaps: - bitmap_name = 'bitmap_' + str(bitmap.guid) - file_ending = '.' + str(bitmap.bitmap_type.value) - bitmap_path = 'data/bitmaps/' + bitmap_name + file_ending + bitmap_name = "bitmap_" + str(bitmap.guid) + file_ending = "." + str(bitmap.bitmap_type.value) + bitmap_path = "data/bitmaps/" + bitmap_name + file_ending with open(bitmap_path, "wb") as bitmap_file: bitmap_image = base64.b64decode(bitmap.bitmap_data, validate=True) bitmap_file.write(bitmap_image) bitmap_file.close() except Exception as e: - print('could not create file ' + snapshot_path + ' ' + bitmap_path + ' ') + print("could not create file " + snapshot_path + " " + bitmap_path + " ") print(e) raise HTTPException(status_code=400, detail="Item not added.") return viewpoint.guid @@ -950,10 +980,7 @@ class BCFDB(MyDB): AND t.guid = $topic_id RETURN v.guid AS viewpoint_id """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id)) + results = tx.run(cypher, username=current_user.username, project_id=str(project_id), topic_id=str(topic_id)) viewpoint_model_list = list() for result in results: viewpoint_id = result.get("viewpoint_id") @@ -976,11 +1003,13 @@ class BCFDB(MyDB): AND v.guid = $viewpoint_id RETURN v AS viewpoint """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") @@ -991,32 +1020,30 @@ class BCFDB(MyDB): lines = self.get_viewpoint_lines(project_id, topic_id, viewpoint_id, current_user) clipping_planes = self.get_viewpoint_clipping_planes(project_id, topic_id, viewpoint_id, current_user) bitmaps = self.get_viewpoint_bitmaps(project_id, topic_id, viewpoint_id, current_user) - snapshot_get = { - 'snapshot_type': v['snapshot_type'].split('/', 2)[1] - } + snapshot_get = {"snapshot_type": v["snapshot_type"].split("/", 2)[1]} viewpoint_get = { - 'guid': v['guid'], - 'index': v['index'], - 'lines': lines, - 'clipping_planes': clipping_planes, - 'bitmaps': bitmaps, - 'snapshot': snapshot_get + "guid": v["guid"], + "index": v["index"], + "lines": lines, + "clipping_planes": clipping_planes, + "bitmaps": bitmaps, + "snapshot": snapshot_get, } - cvp = v['camera_view_point'] - cad = v['camera_direction'] - cuv = v['camera_up_vector'] + cvp = v["camera_view_point"] + cad = v["camera_direction"] + cuv = v["camera_up_vector"] camera = { - 'camera_view_point': {'x': cvp.x, 'y': cvp.y, 'z': cvp.z}, - 'camera_direction': {'x': cad.x, 'y': cad.y, 'z': cad.z}, - 'camera_up_vector': {'x': cuv.x, 'y': cuv.y, 'z': cuv.z}, - 'aspect_ratio': v['aspect_ratio'] + "camera_view_point": {"x": cvp.x, "y": cvp.y, "z": cvp.z}, + "camera_direction": {"x": cad.x, "y": cad.y, "z": cad.z}, + "camera_up_vector": {"x": cuv.x, "y": cuv.y, "z": cuv.z}, + "aspect_ratio": v["aspect_ratio"], } - if v['camera_definition'] == 'orthogonal_camera': - camera['view_to_world_scale'] = v['view_tws_or_fo_view'] - viewpoint_get['orthogonal_camera'] = camera - elif v['camera_definition'] == 'perspective_camera': - camera['field_of_view'] = v['view_tws_or_fo_view'] - viewpoint_get['perspective_camera'] = camera + if v["camera_definition"] == "orthogonal_camera": + camera["view_to_world_scale"] = v["view_tws_or_fo_view"] + viewpoint_get["orthogonal_camera"] = camera + elif v["camera_definition"] == "perspective_camera": + camera["field_of_view"] = v["view_tws_or_fo_view"] + viewpoint_get["perspective_camera"] = camera jsonpickle.dumps(v) jsonpickle.dumps(viewpoint_get) return ViewpointGET(**viewpoint_get) @@ -1024,8 +1051,9 @@ class BCFDB(MyDB): with self.driver.session() as session: return session.execute_read(get_viewpoint_work) - def get_viewpoint_lines(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User) -> List[Line]: + def get_viewpoint_lines( + self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User + ) -> List[Line]: def get_viewpoint_lines_work(tx) -> List[Line]: cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(l:Line) @@ -1036,18 +1064,17 @@ class BCFDB(MyDB): AND v.guid = $viewpoint_id RETURN l AS line """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + results = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) line_model_list = list() for result in results: line_node = result.get("line") - line_json = { - 'start_point': line_node.start_point, - 'end_point': line_node.end_point - } + line_json = {"start_point": line_node.start_point, "end_point": line_node.end_point} line_model = Line(**line_json) line_model_list.append(line_model) return line_model_list @@ -1055,8 +1082,9 @@ class BCFDB(MyDB): with self.driver.session() as session: return session.execute_read(get_viewpoint_lines_work) - def get_viewpoint_clipping_planes(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User) -> List[ClippingPlane]: + def get_viewpoint_clipping_planes( + self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User + ) -> List[ClippingPlane]: def get_viewpoint_clipping_planes_work(tx) -> List[ClippingPlane]: cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(cp:ClippingPlane) @@ -1067,11 +1095,13 @@ class BCFDB(MyDB): AND v.guid = $viewpoint_id RETURN cp AS clipping_plane """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + results = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) clipping_plane_model_list = list() for result in results: clipping_plane_json = self.node_to_json(result.get("clipping_plane")) @@ -1082,8 +1112,9 @@ class BCFDB(MyDB): with self.driver.session() as session: return session.execute_read(get_viewpoint_clipping_planes_work) - def get_viewpoint_bitmaps(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User) -> List[BitmapGET]: + def get_viewpoint_bitmaps( + self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User + ) -> List[BitmapGET]: def get_viewpoint_bitmaps_work(tx) -> List[BitmapGET]: cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(b:Bitmap) @@ -1094,11 +1125,13 @@ class BCFDB(MyDB): AND v.guid = $viewpoint_id RETURN b AS bitmap """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + results = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) bitmap_model_list = list() for result in results: bitmap_json = self.node_to_json(result.get("bitmap")) @@ -1109,8 +1142,9 @@ class BCFDB(MyDB): with self.driver.session() as session: return session.execute_read(get_viewpoint_bitmaps_work) - def get_viewpoint_bitmap(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, bitmap_id: UUID, - current_user: User) -> BitmapGET: + def get_viewpoint_bitmap( + self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, bitmap_id: UUID, current_user: User + ) -> BitmapGET: def get_viewpoint_bitmap_work(tx) -> BitmapGET: cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(b:Bitmap) @@ -1122,12 +1156,14 @@ class BCFDB(MyDB): AND b.guid = $bitmap_id RETURN b AS bitmap """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id), - bitmap_id=str(bitmap_id)) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + bitmap_id=str(bitmap_id), + ) bitmap_json = self.node_to_json(result.get("bitmap")) return BitmapGET(**bitmap_json) @@ -1146,11 +1182,13 @@ class BCFDB(MyDB): AND v.snapshot IS NOT NULL RETURN v.snapshot_type AS snapshot_type """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") @@ -1160,8 +1198,9 @@ class BCFDB(MyDB): with self.driver.session() as session: return session.execute_read(get_viewpoint_snapshot_work) - def get_viewpoint_colored_components(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User) -> ColoringGET: + def get_viewpoint_colored_components( + self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User + ) -> ColoringGET: def get_viewpoint_colored_components_work(tx) -> ColoringGET: cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(c:Component) @@ -1173,36 +1212,36 @@ class BCFDB(MyDB): AND c.color IS NOT NULL RETURN c AS component ORDER BY c.color """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + results = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) coloring_list = list() - color = '' + color = "" coloring = {} occasion = 0 for result in results: component_json = self.node_to_json(result.get("component")) - if component_json['color'] != color: + if component_json["color"] != color: if occasion > 0: coloring_list.append(coloring) occasion += 1 - color = component_json['color'] - coloring = { - 'color': color, - 'components': [] - } - coloring['components'].append(component_json) + color = component_json["color"] + coloring = {"color": color, "components": []} + coloring["components"].append(component_json) coloring_list.append(coloring) - coloring_get = {'coloring': coloring_list} + coloring_get = {"coloring": coloring_list} return ColoringGET(**coloring_get) with self.driver.session() as session: return session.execute_read(get_viewpoint_colored_components_work) - def get_viewpoint_selected_components(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User) -> SelectionGET: + def get_viewpoint_selected_components( + self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User + ) -> SelectionGET: def get_viewpoint_selected_components_work(tx) -> SelectionGET: cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(c:Component) @@ -1214,23 +1253,26 @@ class BCFDB(MyDB): AND c.selected IS NOT NULL RETURN c AS component """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + results = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) component_list = list() for result in results: component_json = self.node_to_json(result.get("component")) component_list.append(component_json) - selection_get = {'selection': component_list} + selection_get = {"selection": component_list} return SelectionGET(**selection_get) with self.driver.session() as session: return session.execute_read(get_viewpoint_selected_components_work) - def get_viewpoint_components_visibility(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, - current_user: User) -> VisibilityGET: + def get_viewpoint_components_visibility( + self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User + ) -> VisibilityGET: def get_viewpoint_components_visibility_work(tx) -> VisibilityGET: cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint) @@ -1241,23 +1283,25 @@ class BCFDB(MyDB): AND v.guid = $viewpoint_id RETURN v AS viewpoint """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Item not found.") viewpoint_json = self.node_to_json(first.get("viewpoint")) view_setup_hints = { - 'spaces_visible': viewpoint_json['spaces_visible'], - 'space_boundaries_visible': viewpoint_json['space_boundaries_visible'], - 'openings_visible': viewpoint_json['openings_visible'] + "spaces_visible": viewpoint_json["spaces_visible"], + "space_boundaries_visible": viewpoint_json["space_boundaries_visible"], + "openings_visible": viewpoint_json["openings_visible"], } visibility = { - 'default_visibility': viewpoint_json['default_visibility'], - 'view_setup_hints': view_setup_hints + "default_visibility": viewpoint_json["default_visibility"], + "view_setup_hints": view_setup_hints, } cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(c:Component) @@ -1269,17 +1313,19 @@ class BCFDB(MyDB): AND c.visibility_exception IS NOT NULL RETURN c AS component """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + results = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) component_list = list() for result in results: component_json = self.node_to_json(result.get("component")) component_list.append(component_json) - visibility['exceptions'] = component_list - visibility_get = {'visibility': visibility} + visibility["exceptions"] = component_list + visibility_get = {"visibility": visibility} return VisibilityGET(**visibility_get) with self.driver.session() as session: @@ -1297,11 +1343,13 @@ class BCFDB(MyDB): AND v.guid = $viewpoint_id DETACH DELETE v, n """ - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - viewpoint_id=str(viewpoint_id)) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + ) result_summary = result.consume() if result_summary.counters.nodes_deleted < 1: raise HTTPException(status_code=404, detail="Item not found.") @@ -1323,10 +1371,7 @@ class BCFDB(MyDB): AND t1.guid = $topic_id RETURN t2 AS topic, ID(t2) AS server_assigned_id """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id)) + results = tx.run(cypher, username=current_user.username, project_id=str(project_id), topic_id=str(topic_id)) topic_model_list = list() for result in results: topic_json = self.node_to_json(result.get("topic")) @@ -1338,8 +1383,9 @@ class BCFDB(MyDB): with self.driver.session() as session: return session.execute_read(get_related_topics_work) - def put_related_topics(self, project_id: UUID, topic_id: UUID, related_topics: List[RelatedTopicPUT], - current_user: User) -> List[TopicGET]: + def put_related_topics( + self, project_id: UUID, topic_id: UUID, related_topics: List[RelatedTopicPUT], current_user: User + ) -> List[TopicGET]: def put_related_topics_work(tx) -> bool: for related_topic in related_topics: cypher = """ @@ -1352,11 +1398,13 @@ class BCFDB(MyDB): AND t2.guid = $related_topic_id MERGE (t1)-[r3:RELATED_TO]->(t2:Topic) """ - tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=topic_id, - related_topic_id=related_topic.related_topic_guid) + tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=topic_id, + related_topic_id=related_topic.related_topic_guid, + ) return True with self.driver.session() as session: @@ -1365,8 +1413,9 @@ class BCFDB(MyDB): return self.get_related_topics(project_id, topic_id, current_user) # returns a collection - def get_topic_document_references(self, project_id: UUID, topic_id: UUID, - current_user: User) -> List[DocumentReferenceGET]: + def get_topic_document_references( + self, project_id: UUID, topic_id: UUID, current_user: User + ) -> List[DocumentReferenceGET]: def get_topic_document_references_work(tx) -> List[DocumentReferenceGET]: cypher = """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:REFERS_TO]->(d:Document) @@ -1380,10 +1429,7 @@ class BCFDB(MyDB): d.url AS document_url, d.description AS document_description """ - results = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id)) + results = tx.run(cypher, username=current_user.username, project_id=str(project_id), topic_id=str(topic_id)) document_reference_model_list = list() for result in results: document_reference_model = DocumentReferenceGET(**result) @@ -1393,19 +1439,22 @@ class BCFDB(MyDB): with self.driver.session() as session: return session.execute_read(get_topic_document_references_work) - def post_topic_document_reference(self, project_id: UUID, topic_id: UUID, document_reference: DocumentReferencePOST, - current_user: User) -> List[DocumentReferenceGET]: + def post_topic_document_reference( + self, project_id: UUID, topic_id: UUID, document_reference: DocumentReferencePOST, current_user: User + ) -> List[DocumentReferenceGET]: def post_topic_document_reference_work(tx) -> bool: if document_reference.guid is None: document_reference.guid = uuid4() if document_reference.document_guid is None or len(document_reference.url) > 4 > len( - document_reference.document_guid): + document_reference.document_guid + ): document_reference.document_guid = uuid4() - document_url = 'd.url = $url,' + document_url = "d.url = $url," else: - document_url = '' - document_reference.url = '' - cypher = """ + document_url = "" + document_reference.url = "" + cypher = ( + """ MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) WHERE u.username = $username AND r1.updateDocumentReferences = True @@ -1415,15 +1464,19 @@ class BCFDB(MyDB): SET r3.guid: $document_reference_id, %s d.description = $description - """ % document_url - result = tx.run(cypher, - username=current_user.username, - project_id=str(project_id), - topic_id=str(topic_id), - document_id=str(document_reference.document_guid), - document_reference_id=str(document_reference.guid), - url=document_reference.url, - description=document_reference.description) + """ + % document_url + ) + result = tx.run( + cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + document_id=str(document_reference.document_guid), + document_reference_id=str(document_reference.guid), + url=document_reference.url, + description=document_reference.description, + ) summary = result.consume() if summary.counters.nodes_created < 1: raise HTTPException(status_code=400, detail="Item not added.") @@ -1434,13 +1487,19 @@ class BCFDB(MyDB): self.set_topic_modified_date(project_id, topic_id, current_user) return self.get_topic_document_references(project_id, topic_id, current_user) - def put_topic_document_references(self, project_id: UUID, topic_id: UUID, reference_id: UUID, - document_reference: DocumentReferencePUT, - current_user: User) -> List[DocumentReferenceGET]: + def put_topic_document_references( + self, + project_id: UUID, + topic_id: UUID, + reference_id: UUID, + document_reference: DocumentReferencePUT, + current_user: User, + ) -> List[DocumentReferenceGET]: document_reference.guid = reference_id document_reference_post = DocumentReferencePOST(document_reference) - topic_document_references_response = self.post_topic_document_reference(project_id, topic_id, - document_reference_post, current_user) + topic_document_references_response = self.post_topic_document_reference( + project_id, topic_id, document_reference_post, current_user + ) self.set_topic_modified_date(project_id, topic_id, current_user) return topic_document_references_response diff --git a/src/opencdeserver/api/app/repository/documents.py b/src/opencdeserver/api/app/repository/documents.py index b48fb886a5..1f5a15440e 100644 --- a/src/opencdeserver/api/app/repository/documents.py +++ b/src/opencdeserver/api/app/repository/documents.py @@ -25,41 +25,42 @@ class DOCDB(MyDB): def document_node_to_model(self, document_node): document_json = self.node_to_json(document_node) - document_json['creation_date'] = self.bcf_time(document_json['creation_date']) - document_json['file_description'] = { - 'name': document_json.pop('name', ''), - 'size_in_bytes': document_json.pop('size_in_bytes', 0) + document_json["creation_date"] = self.bcf_time(document_json["creation_date"]) + document_json["file_description"] = { + "name": document_json.pop("name", ""), + "size_in_bytes": document_json.pop("size_in_bytes", 0), } - document_json['links'] = self.document_version_links(document_json) + document_json["links"] = self.document_version_links(document_json) return DocumentVersion(**document_json) def get_document(self, document_id, version_index=False): def get_document_work(tx) -> Union[Document, bool]: if version_index is None or version_index is False or not isinstance(version_index, int): - version_index_criteria = '' + version_index_criteria = "" else: - version_index_criteria = 'AND d.version_index = $version_index' + version_index_criteria = "AND d.version_index = $version_index" - cypher = """ + cypher = ( + """ MATCH (d:Document) WHERE d.document_id = $document_id %s RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ % version_index_criteria + """ + % version_index_criteria + ) - result = tx.run(cypher, - document_id=document_id, - version_index=version_index) + result = tx.run(cypher, document_id=document_id, version_index=version_index) first = result.single() if first is None: - print('There were no such document version.') + print("There were no such document version.") return False - document_node = first.get('document') + document_node = first.get("document") return self.document_node_to_model(document_node) with self.driver.session() as session: @@ -88,21 +89,23 @@ class DOCDB(MyDB): d.name = $name, d.size_in_bytes = $size_in_bytes """ - result = tx.run(cypher, - selection_session=selection_session, - project=project, - document_id=document_version.document_id, - session_file_id=document_version.session_file_id, - version_index=document_version.version_index, - version_number=document_version.version_number, - creation_date=document_version.creation_date, - title=document_version.title, - original_file_name=document_version.original_file_name, - file_ending=document_version.file_ending, - mime_type=document_version.mime_type, - file_type=document_version.file_type, - name=document_version.file_description.name, - size_in_bytes=document_version.file_description.size_in_bytes) + result = tx.run( + cypher, + selection_session=selection_session, + project=project, + document_id=document_version.document_id, + session_file_id=document_version.session_file_id, + version_index=document_version.version_index, + version_number=document_version.version_number, + creation_date=document_version.creation_date, + title=document_version.title, + original_file_name=document_version.original_file_name, + file_ending=document_version.file_ending, + mime_type=document_version.mime_type, + file_type=document_version.file_type, + name=document_version.file_description.name, + size_in_bytes=document_version.file_description.size_in_bytes, + ) summary = result.consume() if summary.counters.nodes_created < 1: @@ -122,10 +125,10 @@ class DOCDB(MyDB): first = result.single() if first is None: - print('There were no such document version.') + print("There were no such document version.") return False - document_node = first.get('document') + document_node = first.get("document") return self.document_node_to_model(document_node) with self.driver.session() as session: @@ -134,20 +137,21 @@ class DOCDB(MyDB): def create_ifc_graph_for_document(self, document_id): document = self.get_document(document_id) - my_ifc_file = ifcopenshell.open('./data/documents/' + document.file_description.name) - my_graph = Graph(os.environ['NEO4J_URI'], auth=(os.environ['NEO4J_USER'], os.environ['NEO4J_INITIAL_PASSWORD'])) + my_ifc_file = ifcopenshell.open("./data/documents/" + document.file_description.name) + my_graph = Graph(os.environ["NEO4J_URI"], auth=(os.environ["NEO4J_USER"], os.environ["NEO4J_INITIAL_PASSWORD"])) create_full_graph(my_graph, my_ifc_file) # ---- UPLOAD FUNCTIONS ---- - def post_upload_documents(self, upload_documents: UploadDocuments, - current_user: User) -> DocumentUploadSessionInitialization: + def post_upload_documents( + self, upload_documents: UploadDocuments, current_user: User + ) -> DocumentUploadSessionInitialization: def post_upload_documents_work(tx) -> DocumentUploadSessionInitialization: session_uuid = str(uuid4()) session_callback_timedelta = int(upload_documents.callback.expires_in) - session_url_validity_timedelta = 10 + int(os.environ['SESSION_URL_VALIDITY_SECONDS']) - if not hasattr(upload_documents, 'server_context') or not upload_documents.server_context: + session_url_validity_timedelta = 10 + int(os.environ["SESSION_URL_VALIDITY_SECONDS"]) + if not hasattr(upload_documents, "server_context") or not upload_documents.server_context: upload_documents.server_context = False cypher = """ @@ -165,13 +169,15 @@ class DOCDB(MyDB): s.session_callback_timedelta = $session_callback_timedelta """ - result = tx.run(cypher, - username=current_user.username, - session_callback_timedelta=session_callback_timedelta, - session_url_timedelta=session_url_validity_timedelta, - server_context=upload_documents.server_context, - upload_session=session_uuid, - callback=upload_documents.callback.url) + result = tx.run( + cypher, + username=current_user.username, + session_callback_timedelta=session_callback_timedelta, + session_url_timedelta=session_url_validity_timedelta, + server_context=upload_documents.server_context, + upload_session=session_uuid, + callback=upload_documents.callback.url, + ) summary = result.consume() if summary.counters.nodes_created < 2: @@ -197,54 +203,60 @@ class DOCDB(MyDB): d.file_type = $file_type """ - if not hasattr(file, 'document_id') or not file.document_id: + if not hasattr(file, "document_id") or not file.document_id: # When document_id is present, this indicates that # this upload is a new version of an existing document. # When not present, we create a new uuid as new document_id. file.document_id = doc_db.new_uuid() if file.file_name.lower().endswith(tuple(file_types)): - file_ending = file.file_name.split('.')[-1].lower() - name = file.document_id + '.' + file_ending + file_ending = file.file_name.split(".")[-1].lower() + name = file.document_id + "." + file_ending else: - file_ending = '' + file_ending = "" name = file.document_id - print('File ending: ', file_ending) + print("File ending: ", file_ending) - mime_type = '' - file_type = '' + mime_type = "" + file_type = "" if hasattr(file_types, file_ending): - print('We have this file ending in dict: ', file_ending) + print("We have this file ending in dict: ", file_ending) - mime_type = file_types[file_ending]['mime_type'] - file_type = file_types[file_ending]['file_type'] + mime_type = file_types[file_ending]["mime_type"] + file_type = file_types[file_ending]["file_type"] - result = tx.run(cypher, - username=current_user.username, - upload_session=session_uuid, - session_callback_timedelta=session_callback_timedelta, - original_file_name=file.file_name, - name=name, - session_file_id=file.session_file_id, - document_id=file.document_id, - creation_date=doc_db.timestamp(), - file_ending=file_ending, - mime_type=mime_type, - file_type=file_type) + result = tx.run( + cypher, + username=current_user.username, + upload_session=session_uuid, + session_callback_timedelta=session_callback_timedelta, + original_file_name=file.file_name, + name=name, + session_file_id=file.session_file_id, + document_id=file.document_id, + creation_date=doc_db.timestamp(), + file_ending=file_ending, + mime_type=mime_type, + file_type=file_type, + ) summary = result.consume() if summary.counters.nodes_created < 1: raise HTTPException(status_code=400, detail="Document node was not created.") - session_init = DocumentUploadSessionInitialization(**{ - 'upload_ui_url': os.environ['KONTROLL_BASE_URL'] + 'documents/1.0/' \ - + "document-upload?upload_session=" + session_uuid, - 'expires_in': os.environ['SESSION_URL_VALIDITY_SECONDS'], - 'max_size_in_bytes': os.environ['SESSION_MAX_FILE_SIZE_BYTES'], - }) + session_init = DocumentUploadSessionInitialization( + **{ + "upload_ui_url": os.environ["KONTROLL_BASE_URL"] + + "documents/1.0/" + + "document-upload?upload_session=" + + session_uuid, + "expires_in": os.environ["SESSION_URL_VALIDITY_SECONDS"], + "max_size_in_bytes": os.environ["SESSION_MAX_FILE_SIZE_BYTES"], + } + ) return session_init @@ -261,15 +273,14 @@ class DOCDB(MyDB): WHERE us.upload_session = $upload_session RETURN d AS document """ - results = tx.run(cypher, - upload_session=str(upload_session)) + results = tx.run(cypher, upload_session=str(upload_session)) document_list = list() for result in results: - document_json = self.node_to_json(result.get('document')) + document_json = self.node_to_json(result.get("document")) file_to_upload = { - 'file_name': document_json['original_file_name'], - 'session_file_id': document_json['session_file_id'], - 'document_id': document_json['document_id'] + "file_name": document_json["original_file_name"], + "session_file_id": document_json["session_file_id"], + "document_id": document_json["document_id"], } document_model = FileToUpload(**file_to_upload) document_list.append(document_model) @@ -280,16 +291,15 @@ class DOCDB(MyDB): WHERE us.upload_session = $upload_session RETURN p AS project """ - results = tx.run(cypher, - upload_session=str(upload_session)) + results = tx.run(cypher, upload_session=str(upload_session)) project_list = list() for result in results: - project_json = self.node_to_json(result.get('project')) + project_json = self.node_to_json(result.get("project")) project = { - 'project_id': project_json['project_id'], - 'name': project_json['name'], + "project_id": project_json["project_id"], + "name": project_json["name"], } - print('Project: ', project) + print("Project: ", project) project_list.append(project) # to get the user and some session data @@ -302,31 +312,30 @@ class DOCDB(MyDB): us.session_callback_timedelta AS session_callback_timedelta, u AS user """ - result = tx.run(cypher, - upload_session=str(upload_session)) + result = tx.run(cypher, upload_session=str(upload_session)) first = result.single() if first is None: raise HTTPException(status_code=401, detail="No session or link.") - user_node = first.get('user') + user_node = first.get("user") user_dict = self.node_to_json(user_node) for_upload_documents_dict = dict() - for_upload_documents_dict['server_context'] = first.get('server_context') + for_upload_documents_dict["server_context"] = first.get("server_context") callback_link = dict() - callback_link['url'] = first.get('callback') - callback_link['expires_in'] = first.get('session_callback_timedelta') + callback_link["url"] = first.get("callback") + callback_link["expires_in"] = first.get("session_callback_timedelta") - for_upload_documents_dict['callback'] = callback_link - for_upload_documents_dict['documents'] = document_list - print('Project list: ', project_list) + for_upload_documents_dict["callback"] = callback_link + for_upload_documents_dict["documents"] = document_list + print("Project list: ", project_list) - for_upload_documents_dict['projects'] = project_list - for_upload_documents_dict['current_user'] = user_dict + for_upload_documents_dict["projects"] = project_list + for_upload_documents_dict["current_user"] = user_dict - print('Documents list:', document_list) + print("Documents list:", document_list) for_upload_documents_model = DataForUploadDocuments(**for_upload_documents_dict) @@ -348,14 +357,16 @@ class DOCDB(MyDB): d.title = $title, d.project = $project """ - result = tx.run(cypher, - username=username, - upload_session=upload_session, - session_file_id=document.session_file_id, - version_number=document.version_number, - version_index=False, - title=document.title, - project=document.project) + result = tx.run( + cypher, + username=username, + upload_session=upload_session, + session_file_id=document.session_file_id, + version_number=document.version_number, + version_index=False, + title=document.title, + project=document.project, + ) summary = result.consume() if summary.counters.properties_set < 4: @@ -376,11 +387,13 @@ class DOCDB(MyDB): SET d.size_in_bytes = $size_in_bytes """ - result = tx.run(cypher, - username=user.username, - upload_session=upload_session, - session_file_id=document.session_file_id, - size_in_bytes=document.size_in_bytes) + result = tx.run( + cypher, + username=user.username, + upload_session=upload_session, + session_file_id=document.session_file_id, + size_in_bytes=document.size_in_bytes, + ) summary = result.consume() if summary.counters.properties_set < 1: raise HTTPException(status_code=400, detail="Property was not set.") @@ -390,18 +403,20 @@ class DOCDB(MyDB): session.execute_write(update_file_size_work) # link creation - base_url = os.environ['KONTROLL_BASE_URL'] + 'documents/1.0/' - upload_session_url = '?upload_session=' + upload_session - upload_complete_url = base_url + 'upload-completion' + upload_session_url - upload_cancellation_url = base_url + 'upload-cancellation' + upload_session_url - upload_completion = LinkData(**{'url': upload_complete_url}) - upload_cancellation = LinkData(**{'url': upload_cancellation_url}) - document_to_upload_model = DocumentToUpload(**{ - 'session_file_id': document.session_file_id, - 'upload_file_parts': list(), - 'upload_completion': upload_completion, - 'upload_cancellation': upload_cancellation - }) + base_url = os.environ["KONTROLL_BASE_URL"] + "documents/1.0/" + upload_session_url = "?upload_session=" + upload_session + upload_complete_url = base_url + "upload-completion" + upload_session_url + upload_cancellation_url = base_url + "upload-cancellation" + upload_session_url + upload_completion = LinkData(**{"url": upload_complete_url}) + upload_cancellation = LinkData(**{"url": upload_cancellation_url}) + document_to_upload_model = DocumentToUpload( + **{ + "session_file_id": document.session_file_id, + "upload_file_parts": list(), + "upload_completion": upload_completion, + "upload_cancellation": upload_cancellation, + } + ) def add_part_work(tx) -> UUID: cypher = """ @@ -416,15 +431,17 @@ class DOCDB(MyDB): p.content_range_end = $content_range_end, p.content_length = $content_length """ - result = tx.run(cypher, - username=user.username, - upload_session=upload_session, - session_file_id=document.session_file_id, - part_number=part_number, - part_uuid=str(upload_part_uuid), - content_range_start=content_range_start, - content_range_end=content_range_end, - content_length=content_length) + result = tx.run( + cypher, + username=user.username, + upload_session=upload_session, + session_file_id=document.session_file_id, + part_number=part_number, + part_uuid=str(upload_part_uuid), + content_range_start=content_range_start, + content_range_end=content_range_end, + content_length=content_length, + ) summary = result.consume() if summary.counters.nodes_created != 1: @@ -432,7 +449,7 @@ class DOCDB(MyDB): return upload_part_uuid # calculate the number of file parts to send - number_of_parts = math.ceil(int(document.size_in_bytes) / int(os.environ['SESSION_MAX_FILE_SIZE_BYTES'])) + number_of_parts = math.ceil(int(document.size_in_bytes) / int(os.environ["SESSION_MAX_FILE_SIZE_BYTES"])) part_length = math.ceil(int(document.size_in_bytes) / number_of_parts) for part_number in range(number_of_parts): content_range_start = part_number * part_length @@ -441,23 +458,18 @@ class DOCDB(MyDB): content_range_end = document.size_in_bytes - 1 content_length = content_range_end - content_range_start + 1 upload_part_uuid = uuid4() - upload_part_url = 'upload-part/' + str(upload_part_uuid) - additional_headers = { - 'values': [ - { - 'name': 'Content-Length', - 'value': content_length - } - ] - } - part_instruction = UploadFilePartInstruction(**{ - 'url': base_url + upload_part_url, - 'http_method': 'POST', - 'additional_headers': additional_headers, - 'include_authorization': True, - 'content_range_start': content_range_start, - 'content_range_end': content_range_end - }) + upload_part_url = "upload-part/" + str(upload_part_uuid) + additional_headers = {"values": [{"name": "Content-Length", "value": content_length}]} + part_instruction = UploadFilePartInstruction( + **{ + "url": base_url + upload_part_url, + "http_method": "POST", + "additional_headers": additional_headers, + "include_authorization": True, + "content_range_start": content_range_start, + "content_range_end": content_range_end, + } + ) with self.driver.session() as session: added_part = session.execute_write(add_part_work) @@ -476,19 +488,17 @@ class DOCDB(MyDB): RETURN d AS document """ - result = tx.run(cypher, - username=current_user.username, - part_id=part_id) + result = tx.run(cypher, username=current_user.username, part_id=part_id) first = result.single() if first is None: - print('There were no parts in graph!') + print("There were no parts in graph!") return False - document_node = first.get('document') + document_node = first.get("document") document = self.document_node_to_model(document_node) - print('User had this part ' + part_id + ' in document id: ' + document.document_id) + print("User had this part " + part_id + " in document id: " + document.document_id) return document with self.driver.session() as session: @@ -503,9 +513,7 @@ class DOCDB(MyDB): SET p.uploaded = True """ - result = tx.run(cypher, - username=current_user.username, - part_id=part_id) + result = tx.run(cypher, username=current_user.username, part_id=part_id) summary = result.consume() if summary.counters.properties_set < 1: @@ -526,26 +534,24 @@ class DOCDB(MyDB): RETURN count(p) as parts """ - result = tx.run(cypher, - username=current_user.username, - upload_session=upload_session) + result = tx.run(cypher, username=current_user.username, upload_session=upload_session) first = result.single() if first is None: return False - number_of_parts = first.get('parts') + number_of_parts = first.get("parts") - print('There are number of parts not uploaded: ', number_of_parts) + print("There are number of parts not uploaded: ", number_of_parts) if number_of_parts > 0: raise HTTPException(status_code=400, detail="All parts not uploaded.") else: return True + with self.driver.session() as session: return session.execute_read(check_uploaded_parts_work) - def retrieve_uploaded_parts(self, upload_session: str, current_user: User) -> list: def retrieve_uploaded_parts_work(tx) -> list: cypher = """ @@ -556,14 +562,12 @@ class DOCDB(MyDB): RETURN p as part ORDER BY part.number """ - results = tx.run(cypher, - username=current_user.username, - upload_session=upload_session) + results = tx.run(cypher, username=current_user.username, upload_session=upload_session) parts_list = list() for result in results: part_json = self.node_to_json(result.get("part")) - parts_list.append(part_json['uuid']) + parts_list.append(part_json["uuid"]) return parts_list with self.driver.session() as session: @@ -579,12 +583,10 @@ class DOCDB(MyDB): RETURN d AS document """ - result = tx.run(cypher, - username=current_user.username, - upload_session=upload_session) + result = tx.run(cypher, username=current_user.username, upload_session=upload_session) first = result.single() - document_node = first.get('document') + document_node = first.get("document") return self.document_node_to_model(document_node) with self.driver.session() as session: @@ -604,10 +606,7 @@ class DOCDB(MyDB): DELETE r2 CREATE (proj)-[r4:CONTAINS]->(d) """ - result = tx.run(cypher, - username=current_user.username, - upload_session=upload_session, - project=project) + result = tx.run(cypher, username=current_user.username, upload_session=upload_session, project=project) summary = result.consume() if summary.counters.nodes_deleted < 1: @@ -627,15 +626,13 @@ class DOCDB(MyDB): AND us.upload_session = $upload_session RETURN d as document """ - result = tx.run(cypher, - username=current_user.username, - upload_session=upload_session) + result = tx.run(cypher, username=current_user.username, upload_session=upload_session) first = result.single() if first is None: return False - document_node = first.get('document') + document_node = first.get("document") return self.document_node_to_model(document_node) def upload_cancellation_work(tx) -> bool: @@ -645,9 +642,7 @@ class DOCDB(MyDB): AND us.upload_session = $upload_session DETACH DELETE us, d, p """ - result = tx.run(cypher, - username=current_user.username, - upload_session=upload_session) + result = tx.run(cypher, username=current_user.username, upload_session=upload_session) summary = result.consume() if summary.counters.nodes_deleted < 1: @@ -662,14 +657,15 @@ class DOCDB(MyDB): # ---- DOWNLOAD FUNCTIONS ---- - def post_select_documents(self, select_documents: SelectDocuments, - current_user: User) -> DocumentDiscoverySessionInitialization: + def post_select_documents( + self, select_documents: SelectDocuments, current_user: User + ) -> DocumentDiscoverySessionInitialization: def post_select_documents_work(tx) -> DocumentDiscoverySessionInitialization: session_uuid = str(uuid4()) session_callback_timedelta = int(select_documents.callback.expires_in) - session_url_validity_timedelta = 10 + int(os.environ['SESSION_URL_VALIDITY_SECONDS']) - if not hasattr(select_documents, 'server_context') or not select_documents.server_context: + session_url_validity_timedelta = 10 + int(os.environ["SESSION_URL_VALIDITY_SECONDS"]) + if not hasattr(select_documents, "server_context") or not select_documents.server_context: select_documents.server_context = str(uuid4()) cypher = """ @@ -687,22 +683,28 @@ class DOCDB(MyDB): s.session_callback_timedelta = $session_callback_timedelta """ - result = tx.run(cypher, - username=current_user.username, - session_callback_timedelta=session_callback_timedelta, - session_url_timedelta=session_url_validity_timedelta, - server_context=select_documents.server_context, - selection_session=str(session_uuid), - callback=select_documents.callback.url) + result = tx.run( + cypher, + username=current_user.username, + session_callback_timedelta=session_callback_timedelta, + session_url_timedelta=session_url_validity_timedelta, + server_context=select_documents.server_context, + selection_session=str(session_uuid), + callback=select_documents.callback.url, + ) summary = result.consume() if summary.counters.nodes_created < 2: raise HTTPException(status_code=400, detail="Session or link node was not created.") session_init_dict = dict() - session_init_dict['select_documents_url'] = os.environ['KONTROLL_BASE_URL'] + 'documents/1.0/' \ - + "document-selection?selection_session=" + session_uuid - session_init_dict['expires_in'] = os.environ['SESSION_URL_VALIDITY_SECONDS'] + session_init_dict["select_documents_url"] = ( + os.environ["KONTROLL_BASE_URL"] + + "documents/1.0/" + + "document-selection?selection_session=" + + session_uuid + ) + session_init_dict["expires_in"] = os.environ["SESSION_URL_VALIDITY_SECONDS"] session_init_model = DocumentDiscoverySessionInitialization(**session_init_dict) return session_init_model @@ -719,13 +721,12 @@ class DOCDB(MyDB): RETURN p AS project ORDER BY project.name """ - project_results = tx.run(cypher, - selection_session=str(selection_session)) + project_results = tx.run(cypher, selection_session=str(selection_session)) project_list = list() for project_result in project_results: - project_json = self.node_to_json(project_result.get('project')) - project_json['documents'] = list() + project_json = self.node_to_json(project_result.get("project")) + project_json["documents"] = list() project_model = Project(**project_json) # to get the documents @@ -736,12 +737,12 @@ class DOCDB(MyDB): RETURN d AS document ORDER BY d.title, d.version_index """ - document_results = tx.run(cypher, - selection_session=str(selection_session), - project_id=str(project_model.project_id)) + document_results = tx.run( + cypher, selection_session=str(selection_session), project_id=str(project_model.project_id) + ) for document_result in document_results: - document_node = document_result.get('document') + document_node = document_result.get("document") document_model = self.document_node_to_model(document_node) project_model.documents.append(document_model) @@ -758,8 +759,7 @@ class DOCDB(MyDB): u AS user """ - result = tx.run(cypher, - selection_session=str(selection_session)) + result = tx.run(cypher, selection_session=str(selection_session)) first = result.single() if first is None: @@ -769,26 +769,28 @@ class DOCDB(MyDB): user_dict = self.node_to_json(user_node) for_document_selection_dict = dict() - for_document_selection_dict['server_context'] = first.get('server_context') + for_document_selection_dict["server_context"] = first.get("server_context") callback_link = dict() - callback_link['url'] = first.get('callback') - callback_link['expires_in'] = first.get('session_callback_timedelta') + callback_link["url"] = first.get("callback") + callback_link["expires_in"] = first.get("session_callback_timedelta") - for_document_selection_dict['callback'] = callback_link - for_document_selection_dict['projects'] = project_list - for_document_selection_dict['current_user'] = user_dict + for_document_selection_dict["callback"] = callback_link + for_document_selection_dict["projects"] = project_list + for_document_selection_dict["current_user"] = user_dict for_document_selection_model = DataForDocumentSelection(**for_document_selection_dict) - print('Data for document selection: ', for_document_selection_dict) + print("Data for document selection: ", for_document_selection_dict) return for_document_selection_model with self.driver.session() as session: return session.execute_read(get_data_for_document_selection_work) - def post_mark_documents_as_selected(self, all_documents: list, selection_session: UUID) -> DocumentsMarkedAsSelected: + def post_mark_documents_as_selected( + self, all_documents: list, selection_session: UUID + ) -> DocumentsMarkedAsSelected: def mark_documents_as_selected_work(tx) -> DocumentsMarkedAsSelected: cypher = """ @@ -798,12 +800,10 @@ class DOCDB(MyDB): MERGE (ss)-[r5:SELECTED]->(d) """ - selected_documents_model = DocumentsMarkedAsSelected(**{'documents': list()}) + selected_documents_model = DocumentsMarkedAsSelected(**{"documents": list()}) for document in all_documents: - result = tx.run(cypher, - selection_session=str(selection_session), - document_id=str(document)) + result = tx.run(cypher, selection_session=str(selection_session), document_id=str(document)) summary = result.consume() @@ -817,16 +817,20 @@ class DOCDB(MyDB): @staticmethod def document_version_links(document_json): - document_id = document_json['document_id'] - version_index = document_json['version_index'] - base = os.environ['KONTROLL_BASE_URL'] + "documents/1.0/document/" + document_id + "/version/" + str(version_index) - return DocumentVersionLinks(**{ - 'document_version': LinkData(**{'url': base}), - 'document_version_metadata': LinkData(**{'url': base + "/metadata"}), - 'document_version_download': LinkData(**{'url': base + "/download"}), - 'document_versions': LinkData(**{'url': base + "/versions"}), - 'document_details': LinkData(**{'url': base + "/details"}) - }) + document_id = document_json["document_id"] + version_index = document_json["version_index"] + base = ( + os.environ["KONTROLL_BASE_URL"] + "documents/1.0/document/" + document_id + "/version/" + str(version_index) + ) + return DocumentVersionLinks( + **{ + "document_version": LinkData(**{"url": base}), + "document_version_metadata": LinkData(**{"url": base + "/metadata"}), + "document_version_download": LinkData(**{"url": base + "/download"}), + "document_versions": LinkData(**{"url": base + "/versions"}), + "document_details": LinkData(**{"url": base + "/details"}), + } + ) def get_download_instructions(self, session_id: UUID, server_context: str, current_user: User) -> SelectedDocuments: def get_download_instructions_work(tx) -> SelectedDocuments: @@ -839,18 +843,16 @@ class DOCDB(MyDB): document_list = list() for result in results: - document_node = result.get('document') + document_node = result.get("document") document_model = self.document_node_to_model(document_node) document_list.append(document_model) - selected_documents = SelectedDocuments(**{'server_context': server_context, - 'documents': document_list}) + selected_documents = SelectedDocuments(**{"server_context": server_context, "documents": document_list}) return selected_documents with self.driver.session() as session: return session.execute_read(get_download_instructions_work) - def get_upload_documents(self, upload_session: UUID, current_user: User) -> UploadDocuments: def get_upload_documents_work(tx) -> UploadDocuments: cypher = """ @@ -860,12 +862,10 @@ class DOCDB(MyDB): AND us.upload_session = $upload_session RETURN d AS document """ - results = tx.run(cypher, - username=current_user.username, - upload_session=upload_session) + results = tx.run(cypher, username=current_user.username, upload_session=upload_session) file_list = list() for result in results: - file_json = self.node_to_json(result.get('document')) + file_json = self.node_to_json(result.get("document")) file_model = FileToUpload(**file_json) file_list.append(file_model) @@ -878,16 +878,14 @@ class DOCDB(MyDB): us.callback AS callback, us.session_callback_timedelta """ - result = tx.run(cypher, - username=current_user.username, - upload_session=upload_session) + result = tx.run(cypher, username=current_user.username, upload_session=upload_session) - callback = result.get('callback') - session_callback_timedelta = result.get('session_callback_timedelta') + callback = result.get("callback") + session_callback_timedelta = result.get("session_callback_timedelta") upload_documents = UploadDocuments() - upload_documents.server_context = result.get('server_context') - upload_documents.callback.url = result.get('callback') + upload_documents.server_context = result.get("server_context") + upload_documents.callback.url = result.get("callback") upload_documents.callback.expires_in = 3500 # difference between now and timedelta upload_documents.files = file_list @@ -896,16 +894,18 @@ class DOCDB(MyDB): with self.driver.session() as session: return session.execute_read(get_upload_documents_work) - def get_document_version(self, document_id: UUID, version_index: int, - current_user: User) -> Union[DocumentVersion, bool]: + def get_document_version( + self, document_id: UUID, version_index: int, current_user: User + ) -> Union[DocumentVersion, bool]: def get_document_version_work(tx) -> Union[DocumentVersion, bool]: if version_index is None or version_index is False or not isinstance(version_index, int): - version_index_criteria = 'AND d.version_index = $version_index' + version_index_criteria = "AND d.version_index = $version_index" else: - version_index_criteria = '' + version_index_criteria = "" - cypher = """ + cypher = ( + """ MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) WHERE u.username = $username AND d.document_id = $document_id @@ -913,19 +913,20 @@ class DOCDB(MyDB): RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ % version_index_criteria + """ + % version_index_criteria + ) - result = tx.run(cypher, - username=current_user.username, - document_id=document_id, - version_index=version_index) + result = tx.run( + cypher, username=current_user.username, document_id=document_id, version_index=version_index + ) first = result.single() if first is None: - print('There were no such document version.') + print("There were no such document version.") return False - document_node = first.get('document') + document_node = first.get("document") return self.document_node_to_model(document_node) with self.driver.session() as session: @@ -935,11 +936,12 @@ class DOCDB(MyDB): def get_document_version_metadata_work(tx) -> DocumentMetadataEntries: if version_index is None or version_index is False or not isinstance(version_index, int): - version_index_criteria = 'AND d.version_index = $version_index' + version_index_criteria = "AND d.version_index = $version_index" else: - version_index_criteria = '' + version_index_criteria = "" - cypher = """ + cypher = ( + """ MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) WHERE u.username = $username AND d.document_id = $document_id @@ -947,32 +949,34 @@ class DOCDB(MyDB): RETURN d AS document ORDER by d.version_index DESC LIMIT 1 - """ % version_index_criteria + """ + % version_index_criteria + ) - result = tx.run(cypher, - username=current_user.username, - document_id=document_id, - version_index=version_index) + result = tx.run( + cypher, username=current_user.username, document_id=document_id, version_index=version_index + ) first = result.single() if first is None: - print('There were no such document version.') + print("There were no such document version.") return False - document_json = self.node_to_json(first.get('document')) - document_json['creation_date'] = self.bcf_time(document_json['creation_date']) - metadata = ['title', 'version_number', 'creation_date'] + document_json = self.node_to_json(first.get("document")) + document_json["creation_date"] = self.bcf_time(document_json["creation_date"]) + metadata = ["title", "version_number", "creation_date"] entries = list() for each_metadata in metadata: - each_metadata_text = each_metadata.replace('_', ' ') + each_metadata_text = each_metadata.replace("_", " ") each_metadata_text = each_metadata_text.capitalize() entry = { - 'name': each_metadata_text, - 'value': [document_json[each_metadata]], - 'data_type': DataType.string + "name": each_metadata_text, + "value": [document_json[each_metadata]], + "data_type": DataType.string, } entries.append(entry) - return DocumentMetadataEntries(**{'metadata': entries}) + return DocumentMetadataEntries(**{"metadata": entries}) + with self.driver.session() as session: return session.execute_read(get_document_version_metadata_work) @@ -984,15 +988,14 @@ class DOCDB(MyDB): AND d.document_id = $document_id RETURN d AS document """ - results = tx.run(cypher, - username=current_user.username, - document_id=document_id) - document_versions = DocumentVersions({'documents': list()}) + results = tx.run(cypher, username=current_user.username, document_id=document_id) + document_versions = DocumentVersions({"documents": list()}) for result in results: - document_json = self.node_to_json(result.get('document')) - document_version = self.get_document_version(document_id, document_json['version_index'], current_user) + document_json = self.node_to_json(result.get("document")) + document_version = self.get_document_version(document_id, document_json["version_index"], current_user) document_versions.documents.append(document_version) return document_versions + with self.driver.session() as session: return session.execute_read(get_document_versions_work) diff --git a/src/opencdeserver/api/app/repository/foundation.py b/src/opencdeserver/api/app/repository/foundation.py index 803c70a9d1..2554bfee8d 100644 --- a/src/opencdeserver/api/app/repository/foundation.py +++ b/src/opencdeserver/api/app/repository/foundation.py @@ -36,11 +36,13 @@ class FoundationDB(MyDB): CALL apoc.ttl.expireIn(ac, $time_delta, 's') RETURN ac AS authorization_code """ - result = tx.run(cypher, - username=username, - authorization_code=authorization_code, - scope=scope, - time_delta=int(os.environ['SECURITY_AUTHORIZATION_CODE_EXPIRE_SECONDS'])) + result = tx.run( + cypher, + username=username, + authorization_code=authorization_code, + scope=scope, + time_delta=int(os.environ["SECURITY_AUTHORIZATION_CODE_EXPIRE_SECONDS"]), + ) summary = result.consume() if summary.counters.nodes_created < 1: raise HTTPException(status_code=400, detail="Authorization code was not created.") @@ -58,14 +60,13 @@ class FoundationDB(MyDB): RETURN username, scope """ - result = tx.run(cypher, - authorization_code=authorization_code) + result = tx.run(cypher, authorization_code=authorization_code) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Authorization code not found.") authorized_user = TokenData() authorized_user.username = first.get("username") - authorized_user.scopes = first.get("scope").split(' ') + authorized_user.scopes = first.get("scope").split(" ") return authorized_user with self.driver.session() as session: @@ -73,11 +74,11 @@ class FoundationDB(MyDB): token_info = TokenInfo() token_info.access_token = create_access_token( - user_info.dict(), - timedelta(seconds=int(os.environ['SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS']))) + user_info.dict(), timedelta(seconds=int(os.environ["SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS"])) + ) token_info.refresh_token = create_access_token( - user_info.dict(), - timedelta(seconds=int(os.environ['SECURITY_REFRESH_TOKEN_EXPIRE_SECONDS']))) + user_info.dict(), timedelta(seconds=int(os.environ["SECURITY_REFRESH_TOKEN_EXPIRE_SECONDS"])) + ) def add_tokens_and_delete_code_work(tx) -> bool: cypher = """ @@ -92,13 +93,15 @@ class FoundationDB(MyDB): SET t3.value = $refresh_token SET t3.hash = $refresh_token_hash """ - result = tx.run(cypher, - authorization_code=authorization_code, - username=user_info.username, - access_token=token_info.access_token, - access_token_hash=hashlib.md5(token_info.access_token.encode('utf-8')).hexdigest(), - refresh_token=token_info.refresh_token, - refresh_token_hash=hashlib.md5(token_info.refresh_token.encode('utf-8')).hexdigest()) + result = tx.run( + cypher, + authorization_code=authorization_code, + username=user_info.username, + access_token=token_info.access_token, + access_token_hash=hashlib.md5(token_info.access_token.encode("utf-8")).hexdigest(), + refresh_token=token_info.refresh_token, + refresh_token_hash=hashlib.md5(token_info.refresh_token.encode("utf-8")).hexdigest(), + ) summary = result.consume() if summary.counters.nodes_created < 1 or summary.counters.nodes_deleted < 1: @@ -119,24 +122,25 @@ class FoundationDB(MyDB): RETURN at.value AS access_token """ - refresh_token_payload = jwt.decode(refresh_token, - secrets['security_secret_key'], - algorithms=[os.environ['SECURITY_ALGORITHM']]) + refresh_token_payload = jwt.decode( + refresh_token, secrets["security_secret_key"], algorithms=[os.environ["SECURITY_ALGORITHM"]] + ) username_from_refresh_token: str = refresh_token_payload.get("username") - print('refresh_token_username: ', username_from_refresh_token) - result = tx.run(cypher, - username=username_from_refresh_token, - refresh_token_hash=hashlib.md5(refresh_token.encode('utf-8')).hexdigest() - ) + print("refresh_token_username: ", username_from_refresh_token) + result = tx.run( + cypher, + username=username_from_refresh_token, + refresh_token_hash=hashlib.md5(refresh_token.encode("utf-8")).hexdigest(), + ) first = result.single() if first is None: raise HTTPException(status_code=404, detail="Access token not found.") token_info = TokenInfo() token_info.access_token = first.get("access_token") token_info.refresh_token = refresh_token - access_token_payload = jwt.decode(token_info.access_token, - secrets['security_secret_key'], - algorithms=[os.environ['SECURITY_ALGORITHM']]) + access_token_payload = jwt.decode( + token_info.access_token, secrets["security_secret_key"], algorithms=[os.environ["SECURITY_ALGORITHM"]] + ) username_from_access_token: str = access_token_payload.get("username") if username_from_access_token is None: raise credentials_exception @@ -148,11 +152,11 @@ class FoundationDB(MyDB): got_token_data, got_token_info = session.execute_read(use_refresh_to_get_access_work) new_token_info = TokenInfo() new_token_info.access_token = create_access_token( - got_token_data.dict(), - timedelta(seconds=int(os.environ['SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS']))) + got_token_data.dict(), timedelta(seconds=int(os.environ["SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS"])) + ) new_token_info.refresh_token = create_access_token( - got_token_data.dict(), - timedelta(seconds=int(os.environ['SECURITY_REFRESH_TOKEN_EXPIRE_SECONDS']))) + got_token_data.dict(), timedelta(seconds=int(os.environ["SECURITY_REFRESH_TOKEN_EXPIRE_SECONDS"])) + ) def update_tokens_work(tx) -> bool: cypher = """ @@ -168,12 +172,14 @@ class FoundationDB(MyDB): rt.hash = $refresh_token_hash, at.hash = $access_token_hash """ - result = tx.run(cypher, - username=got_token_data.username, - access_token=new_token_info.access_token, - refresh_token=new_token_info.refresh_token, - access_token_hash=hashlib.md5(new_token_info.access_token.encode('utf-8')).hexdigest(), - refresh_token_hash=hashlib.md5(new_token_info.refresh_token.encode('utf-8')).hexdigest()) + result = tx.run( + cypher, + username=got_token_data.username, + access_token=new_token_info.access_token, + refresh_token=new_token_info.refresh_token, + access_token_hash=hashlib.md5(new_token_info.access_token.encode("utf-8")).hexdigest(), + refresh_token_hash=hashlib.md5(new_token_info.refresh_token.encode("utf-8")).hexdigest(), + ) summary = result.consume() if summary.counters.nodes_created < 2 or summary.counters.nodes_deleted < 2: raise HTTPException(status_code=400, detail="Tokens were not deleted and created.") @@ -183,4 +189,5 @@ class FoundationDB(MyDB): session.execute_write(update_tokens_work) return new_token_info + foundation_db = FoundationDB(driver) diff --git a/src/opencdeserver/api/app/security/secrets.py b/src/opencdeserver/api/app/security/secrets.py index 320d70f590..cf7aa1fbe2 100644 --- a/src/opencdeserver/api/app/security/secrets.py +++ b/src/opencdeserver/api/app/security/secrets.py @@ -3,8 +3,8 @@ from glob import glob def get_secrets(): secrets = dict() - for var in glob('/run/secrets/*'): - k = var.split('/')[-1] - v = open(var).read().rstrip('\n') + for var in glob("/run/secrets/*"): + k = var.split("/")[-1] + v = open(var).read().rstrip("\n") secrets[k] = v return secrets diff --git a/src/opencdeserver/api/app/security/secure.py b/src/opencdeserver/api/app/security/secure.py index 79d98e3c50..cc0027d152 100644 --- a/src/opencdeserver/api/app/security/secure.py +++ b/src/opencdeserver/api/app/security/secure.py @@ -16,25 +16,20 @@ from security.secrets import get_secrets secrets = get_secrets() # password context -crypt_context = CryptContext( - schemes=["bcrypt"], - deprecated="auto") +crypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2AuthorizationCodeBearer( - authorizationUrl='foundation/oauth2/auth', - tokenUrl='foundation/oauth2/token', - scopes={ - 'test': 'Full access, but only test data.', - 'user': 'Normal user access.', - 'admin': 'Full access to all.' - }) + authorizationUrl="foundation/oauth2/auth", + tokenUrl="foundation/oauth2/token", + scopes={"test": "Full access, but only test data.", "user": "Normal user access.", "admin": "Full access to all."}, +) credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, +) def create_access_token(data: dict, expires_delta: timedelta | None = None): @@ -44,7 +39,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): else: expire = datetime.utcnow() + timedelta(minutes=15) payload.update({"expires": str(expire)}) - encoded_jwt = jwt.encode(payload, secrets['security_secret_key'], algorithm=os.environ['SECURITY_ALGORITHM']) + encoded_jwt = jwt.encode(payload, secrets["security_secret_key"], algorithm=os.environ["SECURITY_ALGORITHM"]) return encoded_jwt @@ -79,20 +74,18 @@ async def get_current_user(security_scopes: SecurityScopes, token: str = Depends print(authenticate_value) try: - print('Token: ', token) - payload = jwt.decode(token, - secrets['security_secret_key'], - algorithms=[os.environ['SECURITY_ALGORITHM']]) + print("Token: ", token) + payload = jwt.decode(token, secrets["security_secret_key"], algorithms=[os.environ["SECURITY_ALGORITHM"]]) username_from_token: str = payload.get("username") - print('Token username: ', username_from_token) + print("Token username: ", username_from_token) if username_from_token is None: raise credentials_exception token_scopes = payload.get("scopes", []) - print('Token scopes: ', token_scopes) + print("Token scopes: ", token_scopes) token_data = TokenData(scopes=token_scopes, username=username_from_token) except JWTError: - print('JWTError') + print("JWTError") raise credentials_exception user = db.get_user(username=token_data.username) diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index e260c3e623..f0ebbb84b2 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -461,7 +461,7 @@ namespace { TopExp_Explorer it(shell, TopAbs_FACE); for (; it.More(); it.Next()) { - const auto& face = TopoDS::Face(it.Value()); + const auto& face = TopoDS::Face(it.Current()); auto surf = BRep_Tool::Surface(face); if (surf->DynamicType() != STANDARD_TYPE(Geom_Plane)) { return boost::none;