mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 15:08:51 +00:00
[BCF] New BCF XML handler with better performances (#2355)
BREAKING CHANGE: incompatible methods and objects with previous version. closes #2321
This commit is contained in:
committed by
Dion Moult
parent
ada8e1e923
commit
9838309fe3
@@ -19,20 +19,19 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.10'
|
||||||
- name: Build package
|
- name: Build package
|
||||||
run: |
|
run: |
|
||||||
cd src/bcf
|
cd src/bcf
|
||||||
pip install build
|
pip install build
|
||||||
python -m build
|
python -m build
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
cd src/bcf
|
||||||
|
make test
|
||||||
- name: Publish package
|
- name: Publish package
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
password: ${{ secrets.PYPI_TOKEN }}
|
password: ${{ secrets.PYPI_TOKEN }}
|
||||||
packages_dir: src/bcf/dist
|
packages_dir: src/bcf/dist
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -73,3 +73,7 @@ _build/
|
|||||||
|
|
||||||
# IDS Docs
|
# IDS Docs
|
||||||
src/ifcopenshell-python/test/build
|
src/ifcopenshell-python/test/build
|
||||||
|
|
||||||
|
# tox cache
|
||||||
|
.tox/
|
||||||
|
*.egg-info/
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
.tox
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
tox
|
||||||
|
|
||||||
|
.PHONY: qa
|
||||||
|
qa:
|
||||||
|
black .
|
||||||
|
isort .
|
||||||
|
pylint ./src --output-format=colorized
|
||||||
|
|
||||||
|
.PHONY: typecheck
|
||||||
|
typecheck:
|
||||||
|
mypy src/bcf
|
||||||
|
|
||||||
|
.PHONY: license
|
||||||
|
license:
|
||||||
|
copyright-header --license LGPL3 --copyright-holder "Andrea Ghensi <andrea.ghensi@gmail.com>" --copyright-year "2022" --copyright-software "IfcOpenShell" --copyright-software-description "BCF XML file handling" -a ./ -o ./
|
||||||
|
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage:
|
||||||
|
coverage run --source bcf -m pytest tests
|
||||||
|
coverage html
|
||||||
|
xdg-open htmlcov/index.html
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf htmlcov
|
||||||
|
|
||||||
|
# TODO: make this based on xsd file presence
|
||||||
|
.PHONY: models
|
||||||
|
models:
|
||||||
|
cd src && xsdata generate -p bcf.v2.model --unnest-classes --kw-only --slots -ds Google bcf/v2/xsd
|
||||||
|
cd src && xsdata generate -p bcf.v3.model --unnest-classes --kw-only --slots -ds Google bcf/v3/xsd
|
||||||
|
|
||||||
|
# .PHONY
|
||||||
|
# api:
|
||||||
|
# openapi-python-client generate --url https://api.swaggerhub.com/apis/buildingSMART/BCF/3.0
|
||||||
+31
-48
@@ -1,64 +1,54 @@
|
|||||||
# bcf
|
# bcf
|
||||||
|
|
||||||
A simple Python implementation of BCF. The data model is described in `data.py`.
|
A simple Python implementation of BCF.
|
||||||
Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API
|
Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API
|
||||||
is available via `bcfapi.py`.
|
is available via `bcfapi.py`.
|
||||||
|
|
||||||
- BCF-XML version 2.1: Fully supported
|
It tries to support BCF-XML version 2.1 and 3.0, and BCF-API 3.0.
|
||||||
- BCF-API version 2.1: Not supported, will probably tackle this after BCF-API v3.0
|
|
||||||
- BCF-XML version 3.0: Almost fully supported, except for the documents module
|
|
||||||
- BCF-API version 3.0: Almost fully supported, except for two requests.
|
|
||||||
|
|
||||||
## bcfxml
|
## bcfxml
|
||||||
|
|
||||||
The `bcfxml` module lets you interact with the BCF-XML standard.
|
The `bcfxml.load` function lets you read a BCF-XML file.
|
||||||
|
It takes care of using the right version based on the "bcf.version" file contained in the BCF package.
|
||||||
|
|
||||||
|
The BCF files are extracted and parsed on-demand, and edits are stored in memory until you call the `save` method.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from bcf import bcfxml
|
from bcf.bcfxml import load
|
||||||
|
|
||||||
|
|
||||||
# Load a project
|
# Load a project
|
||||||
bcfxml = bcfxml.load("/path/to/file.bcf")
|
with load("/path/to/file.bcf") as bcfxml:
|
||||||
|
project = bcfxml.project
|
||||||
|
print(project.name)
|
||||||
|
|
||||||
|
# To edit a project, just modify the object directly
|
||||||
|
bcfxml.project.name = "New name"
|
||||||
|
|
||||||
# The project is also stored in the module
|
# Get a dictionary of topics
|
||||||
# project == bcfxml.project
|
topics = bcfxml.topics
|
||||||
project=bcfxml.get_project()
|
|
||||||
print(project.name)
|
|
||||||
|
|
||||||
# To edit a project, just modify the object directly
|
for topic_handler in bcfxml.topics:
|
||||||
bcfxml.project.name = "New name"
|
topic = topic_handler.topic
|
||||||
bcfxml.edit_project()
|
print("Topic guid is", topic.guid)
|
||||||
|
print("Topic title is", topic.title)
|
||||||
|
|
||||||
# The BCF file is extracted to this temporary directory
|
# Fetch extra data about a topic
|
||||||
print(bcfxml.filepath)
|
header = topic_handler.header
|
||||||
|
comments = topic_handler.comments
|
||||||
|
viewpoints = topic_handler.viewpoints
|
||||||
|
|
||||||
# Get a dictionary of topics
|
for comment in comments:
|
||||||
topics = bcfxml.get_topics()
|
print(comment.guid)
|
||||||
|
print(comment.comment)
|
||||||
|
print(comment.author)
|
||||||
|
|
||||||
# Note: topics == bcfxml.topics
|
# Get a particular topic
|
||||||
for guid, topic in bcfxml.topics.items():
|
topic = bcfxml.get_topic(guid)
|
||||||
print("Topic guid is", guid)
|
|
||||||
print("Topic guid is", topic.guid)
|
|
||||||
print("Topic title is", topic.title)
|
|
||||||
|
|
||||||
# Fetch extra data about a topic
|
# Modify a topic
|
||||||
header = bcfxml.get_header(guid)
|
topic.title = "New title"
|
||||||
comments = bcfxml.get_comments(guid)
|
|
||||||
viewpoints = bcfxml.get_viewpoints(guid)
|
|
||||||
|
|
||||||
# Note: comments == topic.comments, and so on
|
bcfxml.save()
|
||||||
for comment_guid, comment in comments.items():
|
|
||||||
print(comment_guid)
|
|
||||||
print(comment.comment)
|
|
||||||
print(comment.author)
|
|
||||||
|
|
||||||
# Get a particular topic
|
|
||||||
topic = bcfxml.get_topic(guid)
|
|
||||||
|
|
||||||
# Modify a topic
|
|
||||||
topic.title = "New title"
|
|
||||||
bcfxml.edit_topic(topic)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## bcfapi
|
## bcfapi
|
||||||
@@ -92,10 +82,3 @@ print(data)
|
|||||||
data = bcf_client.get_extensions(project_id)
|
data = bcf_client.get_extensions(project_id)
|
||||||
print(data)
|
print(data)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Todo List
|
|
||||||
|
|
||||||
The remaining work that needs to be completed in `bcfxml.py` and `bcfapi.py`.
|
|
||||||
|
|
||||||
- For `bcfxml.py` two xsds support is remaining namely 'documents.xsd`and`extensions.xsd`.
|
|
||||||
- For `bcfapi.py` two requests that are `get_topics` and `get_comments` are remaining.
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
name: bcf-client
|
||||||
|
channels:
|
||||||
|
- conda-forge
|
||||||
|
dependencies:
|
||||||
|
- ifcopenshell
|
||||||
|
- xsdata
|
||||||
|
- numpy
|
||||||
+115
-1
@@ -1,12 +1,126 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = [
|
requires = [
|
||||||
"setuptools>=42",
|
"setuptools>=61",
|
||||||
"wheel"
|
"wheel"
|
||||||
]
|
]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "bcf-client"
|
||||||
|
# author = "IfcOpenShell"
|
||||||
|
description = "BCF-XML file handler."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
keywords = ["IFC", "BCF", "BIM", "eingineering"]
|
||||||
|
dependencies = [
|
||||||
|
"xsdata",
|
||||||
|
"numpy",
|
||||||
|
]
|
||||||
|
version = "0.0.1"
|
||||||
|
classifiers = [
|
||||||
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Topic :: Scientific/Engineering",
|
||||||
|
"Topic :: Utilities",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Source = "https://github.com/IfcOpenShell/IfcOpenShell"
|
||||||
|
Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
extend-exclude = "model"
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
extend_skip_glob = ["src/bcf/*/model/*"]
|
||||||
|
|
||||||
|
[tool.coverage.paths]
|
||||||
|
source = ["src"]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
branch = true
|
||||||
|
source = ["bcf"]
|
||||||
|
omit = ["*/model/*"]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
show_missing = true
|
||||||
|
fail_under = 65
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"if TYPE_CHECKING",
|
||||||
|
"if __name__ == .__main__.:",
|
||||||
|
"Protocol",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.tox]
|
||||||
|
legacy_tox_ini = """
|
||||||
|
[tox]
|
||||||
|
envlist = py310
|
||||||
|
isolated_build = true
|
||||||
|
skip_missing_interpreters = true
|
||||||
|
requires = tox-conda
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
conda_deps =
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
coverage
|
||||||
|
conda_channels =
|
||||||
|
conda-forge
|
||||||
|
conda_env = environment.yml
|
||||||
|
commands = pytest --cov --cov-report=term tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_any_generics = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
#no_implicit_reexport = true
|
||||||
|
show_column_numbers = true
|
||||||
|
show_error_codes = true
|
||||||
|
show_error_context = true
|
||||||
|
strict_equality = true
|
||||||
|
strict_optional = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
#warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
exclude= "src/bcf/v(2|3)/model"
|
||||||
|
plugins = "numpy.typing.mypy_plugin"
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "tests"
|
||||||
|
disallow_untyped_decorators = false
|
||||||
|
disallow_untyped_defs = false
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"pytest",
|
||||||
|
"pytest_mock",
|
||||||
|
"ifcopenshell",
|
||||||
|
"ifcopenshell.*",
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.pylint.main]
|
||||||
|
ignore = ["model"]
|
||||||
|
ignored-modules = ["bcf.v2.model", "bcf.v3.model", "xsdata"]
|
||||||
|
jobs = 0
|
||||||
|
disable="all"
|
||||||
|
enable="E" # B,B9,BLK,C,D,E,F,I,N,S,W
|
||||||
|
|
||||||
|
[tool.pylint.design]
|
||||||
|
max-args = 10
|
||||||
|
max-attributes = 10
|
||||||
|
|
||||||
|
[tool.pylint.format]
|
||||||
|
expected-line-ending-format = "LF"
|
||||||
|
max-line-length = 120
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
black
|
||||||
|
mypy
|
||||||
|
pylint
|
||||||
|
isort
|
||||||
|
xsdata
|
||||||
|
tox
|
||||||
|
tox-conda
|
||||||
@@ -1 +0,0 @@
|
|||||||
xmlschema
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
[metadata]
|
|
||||||
name=bcf-client
|
|
||||||
version=0.0.1
|
|
||||||
author = Ifcopenshell
|
|
||||||
description = A simple Python implementation of BCF
|
|
||||||
url = https://github.com/IfcOpenShell/IfcOpenShell
|
|
||||||
project_urls =
|
|
||||||
Code=https://github.com/IfcOpenShell/IfcOpenShell
|
|
||||||
Issues=https://github.com/IfcOpenShell/IfcOpenShell/issues
|
|
||||||
long_description = file: README.md
|
|
||||||
long_description_content_type = text/markdown
|
|
||||||
classifiers =
|
|
||||||
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
||||||
Operating System :: OS Independent
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Topic :: Scientific/Engineering
|
|
||||||
Topic :: Utilities
|
|
||||||
keywords =
|
|
||||||
Python
|
|
||||||
file formats
|
|
||||||
engineering
|
|
||||||
|
|
||||||
[options]
|
|
||||||
package_dir =
|
|
||||||
= src
|
|
||||||
packages = find:
|
|
||||||
python_requires = >=3
|
|
||||||
install_requires=
|
|
||||||
xmlschema
|
|
||||||
include_package_data = True
|
|
||||||
|
|
||||||
[options.packages.find]
|
|
||||||
where = src
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
max-line-length = 120
|
|
||||||
ignore = E24, E121, E123, E126, E203, E226, E704, E741, W503, W504
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup()
|
||||||
+58
-50
@@ -1,61 +1,69 @@
|
|||||||
# BCF - BCF Python library
|
"""
|
||||||
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
BCF - BCF Python library
|
||||||
#
|
Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
||||||
# This file is part of BCF.
|
Copyright (C) 2022 Andrea Ghensi <andrea.ghensi@gmail.com>
|
||||||
#
|
|
||||||
# BCF is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# BCF is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
|
||||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
This file is part of BCF.
|
||||||
|
|
||||||
import os.path
|
BCF is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
BCF is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
import zipfile
|
import zipfile
|
||||||
import tempfile
|
from pathlib import Path
|
||||||
from xml.dom import minidom
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from bcf.v2.bcfxml import BcfXml as BcfXml2
|
||||||
|
from bcf.v3.bcfxml import BcfXml as BcfXml3
|
||||||
|
from bcf.v3.model import Version
|
||||||
|
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
def load(filepath):
|
def load(
|
||||||
filepath = extract_project(filepath)
|
filepath: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None
|
||||||
if os.path.isfile(os.path.join(filepath, "bcf.version")):
|
) -> Optional[Union[BcfXml2, BcfXml3]]:
|
||||||
version_path = os.path.join(filepath, "bcf.version")
|
"""
|
||||||
version_id = get_version(version_path)
|
Load a BCF file.
|
||||||
# TODO: we actually coded it for 2.1, let's check the difference between 2.0 and 2.1
|
|
||||||
if version_id == "2.1" or version_id == "2.0":
|
|
||||||
from bcf.v2.bcfxml import BcfXml
|
|
||||||
|
|
||||||
bcfxml = BcfXml()
|
Args:
|
||||||
bcfxml.filepath = filepath
|
filepath: The path to the BCF file.
|
||||||
return bcfxml
|
|
||||||
elif version_id == "3.0":
|
|
||||||
from bcf.v3.bcfxml import BcfXml
|
|
||||||
|
|
||||||
bcfxml = BcfXml()
|
Returns:
|
||||||
bcfxml.filepath = filepath
|
The loaded BCF file.
|
||||||
return bcfxml
|
|
||||||
else:
|
Raises:
|
||||||
raise Exception(f"Version {version_id} not supported.")
|
ValueError: If the BCF version is not supported.
|
||||||
|
"""
|
||||||
|
xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
version_id = _get_version(filepath, xml_handler)
|
||||||
|
if version_id in {"2.1", "2.0"}:
|
||||||
|
return BcfXml2.load(filepath, xml_handler)
|
||||||
|
if version_id == "3.0":
|
||||||
|
return BcfXml3.load(filepath, xml_handler)
|
||||||
|
raise ValueError(f"Version {version_id} not supported.")
|
||||||
|
|
||||||
|
|
||||||
def get_version(version_path):
|
def _get_version(filepath: Union[str, Path], xml_handler: Optional[AbstractXmlParserSerializer] = None) -> str:
|
||||||
xmlparse = minidom.parse(version_path)
|
"""
|
||||||
version_el = xmlparse.getElementsByTagName("Version")[0]
|
Returns the version of the BCF file.
|
||||||
version = version_el.getAttribute("VersionId")
|
|
||||||
return version
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: The path to the BCF file.
|
||||||
|
xml_handler: The XML handler. If none is given, XmlParserSerializer is used.
|
||||||
|
|
||||||
def extract_project(filepath):
|
Returns:
|
||||||
if not filepath:
|
The version of the BCF file.
|
||||||
return
|
"""
|
||||||
zip_file = zipfile.ZipFile(filepath)
|
xml_handler = xml_handler or XmlParserSerializer()
|
||||||
filepath = tempfile.mkdtemp()
|
with zipfile.ZipFile(filepath) as bcf_zip:
|
||||||
zip_file.extractall(filepath)
|
version = xml_handler.parse(bcf_zip.read("bcf.version"), Version)
|
||||||
return filepath
|
return version.version_id
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import numpy as np
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
|
||||||
|
def calc_camera_vectors(
|
||||||
|
elem_placement: NDArray[np.float_],
|
||||||
|
) -> tuple[NDArray[np.float_], NDArray[np.float_], NDArray[np.float_]]:
|
||||||
|
"""
|
||||||
|
Calculate the vectors of a camera pointing to an element.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
elem_placement: Placement matrix of an element.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Camera position, direction and up vectors
|
||||||
|
"""
|
||||||
|
target_position = elem_placement[:3, 3]
|
||||||
|
camera_position = target_position + np.array((5, 5, 5))
|
||||||
|
camera_direction = unit_vector(camera_position - target_position)
|
||||||
|
camera_right = unit_vector(np.cross(np.array([0.0, 0.0, 1.0]), camera_direction))
|
||||||
|
camera_up = unit_vector(np.cross(camera_direction, camera_right))
|
||||||
|
rotation_transform = np.eye(4)
|
||||||
|
rotation_transform[0, :3] = camera_right
|
||||||
|
rotation_transform[1, :3] = camera_up
|
||||||
|
rotation_transform[2, :3] = camera_direction
|
||||||
|
translation_transform = np.eye(4)
|
||||||
|
translation_transform[:3, -1] = -camera_position
|
||||||
|
look_at_transform = np.matmul(rotation_transform, translation_transform)
|
||||||
|
mat = np.linalg.inv(look_at_transform)
|
||||||
|
return camera_position, -mat[:3, 2], mat[:3, 1]
|
||||||
|
|
||||||
|
|
||||||
|
def unit_vector(v: NDArray[np.float_]) -> NDArray[np.float_]:
|
||||||
|
"""
|
||||||
|
Return the unit vector of a vector.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
v: vector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
unit vector.
|
||||||
|
"""
|
||||||
|
norm = np.linalg.norm(v)
|
||||||
|
return v if norm == 0 else v / norm
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
In Memory Zip File management, taken from ruamel.std.zipfile
|
||||||
|
|
||||||
|
Copyright (c) 2017-2020 Anthon van der Neut, Ruamel bvba
|
||||||
|
|
||||||
|
original idea from https://stackoverflow.com/a/19722365/1307905
|
||||||
|
"""
|
||||||
|
import zipfile
|
||||||
|
from io import BytesIO
|
||||||
|
from os import PathLike
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional, Protocol
|
||||||
|
|
||||||
|
|
||||||
|
class ZipFileInterface(Protocol):
|
||||||
|
def writestr(self, filename_in_zip: str | zipfile.ZipInfo, file_contents: bytes | str) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryZipFile:
|
||||||
|
def __init__(
|
||||||
|
self, file_name: Optional[str | Path] = None, compression: int = zipfile.ZIP_DEFLATED, debug: int = 0
|
||||||
|
) -> None:
|
||||||
|
# Create the in-memory file-like object
|
||||||
|
self._file_name: Optional[str | Path] = str(file_name) if hasattr(file_name, "_from_parts") else file_name
|
||||||
|
self.in_memory_data = BytesIO()
|
||||||
|
# Create the in-memory zipfile
|
||||||
|
self.in_memory_zip = zipfile.ZipFile(self.in_memory_data, "w", compression, False)
|
||||||
|
self.in_memory_zip.debug = debug
|
||||||
|
|
||||||
|
def writestr(self, filename_in_zip: str | zipfile.ZipInfo, file_contents: bytes | str) -> None:
|
||||||
|
"""Appends a file with name filename_in_zip and contents of
|
||||||
|
file_contents to the in-memory zip."""
|
||||||
|
self.in_memory_zip.writestr(filename_in_zip, file_contents)
|
||||||
|
|
||||||
|
def write_to_file(self, filename: str | bytes | PathLike[str] | PathLike[bytes] | int) -> None:
|
||||||
|
"""Writes the in-memory zip to a file."""
|
||||||
|
# Mark the files as having been created on Windows so that
|
||||||
|
# Unix permissions are not inferred as 0000
|
||||||
|
for zfile in self.in_memory_zip.filelist:
|
||||||
|
zfile.create_system = 0
|
||||||
|
self.in_memory_zip.close()
|
||||||
|
with open(filename, "wb") as f:
|
||||||
|
f.write(self.data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> bytes:
|
||||||
|
return self.in_memory_data.getvalue()
|
||||||
|
|
||||||
|
def __enter__(self) -> "InMemoryZipFile":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
||||||
|
if self._file_name:
|
||||||
|
self.write_to_file(self._file_name)
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# BCF - BCF Python library
|
# BCF - BCF Python library
|
||||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||||
#
|
#
|
||||||
@@ -16,4 +15,3 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|||||||
+251
-768
File diff suppressed because it is too large
Load Diff
@@ -1,198 +0,0 @@
|
|||||||
|
|
||||||
# BCF - BCF Python library
|
|
||||||
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
|
||||||
#
|
|
||||||
# This file is part of BCF.
|
|
||||||
#
|
|
||||||
# BCF is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# BCF is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
|
||||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
class Project:
|
|
||||||
def __init__(self):
|
|
||||||
self.project_id = ""
|
|
||||||
self.name = ""
|
|
||||||
self.extension_schema = ""
|
|
||||||
|
|
||||||
|
|
||||||
class BimSnippet:
|
|
||||||
def __init__(self):
|
|
||||||
self.snippet_type = None
|
|
||||||
self.is_external = False
|
|
||||||
self.reference = None
|
|
||||||
self.reference_schema = None
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentReference:
|
|
||||||
def __init__(self):
|
|
||||||
self.referenced_document = None
|
|
||||||
self.description = None
|
|
||||||
self.guid = None
|
|
||||||
self.is_external = False
|
|
||||||
|
|
||||||
|
|
||||||
class RelatedTopic:
|
|
||||||
def __init__(self):
|
|
||||||
self.guid = None
|
|
||||||
|
|
||||||
|
|
||||||
class HeaderFile:
|
|
||||||
def __init__(self):
|
|
||||||
self.filename = None
|
|
||||||
self.date = None
|
|
||||||
self.reference = None
|
|
||||||
self.ifc_project = None
|
|
||||||
self.ifc_spatial_structure_element = None
|
|
||||||
self.is_external = True
|
|
||||||
|
|
||||||
|
|
||||||
class Header:
|
|
||||||
def __init__(self):
|
|
||||||
self.files = []
|
|
||||||
|
|
||||||
|
|
||||||
class Topic:
|
|
||||||
def __init__(self):
|
|
||||||
self.reference_links = []
|
|
||||||
self.title = ""
|
|
||||||
self.priority = None
|
|
||||||
self.index = None # Deprecated, stored, but ignored
|
|
||||||
self.labels = []
|
|
||||||
self.creation_date = None
|
|
||||||
self.creation_author = None
|
|
||||||
self.modified_date = None
|
|
||||||
self.modified_author = None
|
|
||||||
self.due_date = None
|
|
||||||
self.assigned_to = None
|
|
||||||
self.stage = None
|
|
||||||
self.description = None
|
|
||||||
self.bim_snippet = None
|
|
||||||
self.document_references = []
|
|
||||||
self.related_topics = []
|
|
||||||
self.topic_status = None
|
|
||||||
self.topic_type = None
|
|
||||||
self.guid = None
|
|
||||||
|
|
||||||
self.header = None
|
|
||||||
self.comments = {}
|
|
||||||
self.viewpoints = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Comment:
|
|
||||||
def __init__(self):
|
|
||||||
self.guid = None
|
|
||||||
self.date = None
|
|
||||||
self.author = None
|
|
||||||
self.comment = None
|
|
||||||
self.viewpoint = None
|
|
||||||
self.modified_date = None
|
|
||||||
self.modified_author = None
|
|
||||||
self.topic_guid = None # Part of BCF-API
|
|
||||||
|
|
||||||
|
|
||||||
class ViewSetupHints:
|
|
||||||
def __init__(self):
|
|
||||||
self.spaces_visible = False
|
|
||||||
self.space_boundaries_visible = False
|
|
||||||
self.openings_visible = False
|
|
||||||
|
|
||||||
|
|
||||||
class Component:
|
|
||||||
def __init__(self):
|
|
||||||
self.originating_system = None
|
|
||||||
self.authoring_tool_id = None
|
|
||||||
self.ifc_guid = None
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentVisibility:
|
|
||||||
def __init__(self):
|
|
||||||
self.exceptions = []
|
|
||||||
self.default_visibility = False
|
|
||||||
|
|
||||||
|
|
||||||
class Color:
|
|
||||||
def __init__(self):
|
|
||||||
self.color = None
|
|
||||||
self.components = []
|
|
||||||
|
|
||||||
|
|
||||||
class Components:
|
|
||||||
def __init__(self):
|
|
||||||
self.view_setup_hints = None
|
|
||||||
self.selection = []
|
|
||||||
self.visibility = None
|
|
||||||
self.coloring = []
|
|
||||||
|
|
||||||
|
|
||||||
class Point:
|
|
||||||
def __init__(self):
|
|
||||||
self.x = 0
|
|
||||||
self.y = 0
|
|
||||||
self.z = 0
|
|
||||||
|
|
||||||
|
|
||||||
class Direction(Point):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OrthogonalCamera:
|
|
||||||
def __init__(self):
|
|
||||||
self.camera_view_point = Point()
|
|
||||||
self.camera_direction = Direction()
|
|
||||||
self.camera_up_vector = Direction()
|
|
||||||
self.view_to_world_scale = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
class PerspectiveCamera:
|
|
||||||
def __init__(self):
|
|
||||||
self.camera_view_point = Point()
|
|
||||||
self.camera_direction = Direction()
|
|
||||||
self.camera_up_vector = Direction()
|
|
||||||
self.field_of_view = 60.0
|
|
||||||
|
|
||||||
|
|
||||||
class Line:
|
|
||||||
def __init__(self):
|
|
||||||
self.start_point = Point()
|
|
||||||
self.end_point = Point()
|
|
||||||
|
|
||||||
|
|
||||||
class ClippingPlane:
|
|
||||||
def __init__(self):
|
|
||||||
self.location = Point()
|
|
||||||
self.direction = Direction()
|
|
||||||
|
|
||||||
|
|
||||||
class Bitmap:
|
|
||||||
def __init__(self):
|
|
||||||
self.reference = "" # Only in BCF-XML
|
|
||||||
self.bitmap_data = None # Only in BCF-API
|
|
||||||
self.bitmap_format = "PNG" # Enum of png or jpg
|
|
||||||
self.location = Point()
|
|
||||||
self.normal = Direction()
|
|
||||||
self.up = Direction()
|
|
||||||
self.height = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
class Viewpoint:
|
|
||||||
def __init__(self):
|
|
||||||
self.guid = None
|
|
||||||
self.viewpoint = None
|
|
||||||
self.snapshot = None
|
|
||||||
self.index = None
|
|
||||||
|
|
||||||
self.components = None # It's not a list, despite the plural name
|
|
||||||
self.orthogonal_camera = None
|
|
||||||
self.perspective_camera = None
|
|
||||||
self.lines = []
|
|
||||||
self.clipping_planes = []
|
|
||||||
self.bitmaps = []
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
from bcf.v2.model.markup import (
|
||||||
|
BimSnippet,
|
||||||
|
Comment,
|
||||||
|
CommentViewpoint,
|
||||||
|
Header,
|
||||||
|
HeaderFile,
|
||||||
|
Markup,
|
||||||
|
Topic,
|
||||||
|
TopicDocumentReference,
|
||||||
|
TopicRelatedTopic,
|
||||||
|
ViewPoint,
|
||||||
|
)
|
||||||
|
from bcf.v2.model.project import Project, ProjectExtension
|
||||||
|
from bcf.v2.model.version import Version
|
||||||
|
from bcf.v2.model.visinfo import (
|
||||||
|
BitmapFormat,
|
||||||
|
ClippingPlane,
|
||||||
|
Component,
|
||||||
|
ComponentColoring,
|
||||||
|
ComponentColoringColor,
|
||||||
|
Components,
|
||||||
|
ComponentSelection,
|
||||||
|
ComponentVisibility,
|
||||||
|
ComponentVisibilityExceptions,
|
||||||
|
Direction,
|
||||||
|
Line,
|
||||||
|
OrthogonalCamera,
|
||||||
|
PerspectiveCamera,
|
||||||
|
Point,
|
||||||
|
ViewSetupHints,
|
||||||
|
VisualizationInfo,
|
||||||
|
VisualizationInfoBitmap,
|
||||||
|
VisualizationInfoClippingPlanes,
|
||||||
|
VisualizationInfoLines,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BimSnippet",
|
||||||
|
"Comment",
|
||||||
|
"CommentViewpoint",
|
||||||
|
"Header",
|
||||||
|
"HeaderFile",
|
||||||
|
"Markup",
|
||||||
|
"Topic",
|
||||||
|
"TopicDocumentReference",
|
||||||
|
"TopicRelatedTopic",
|
||||||
|
"ViewPoint",
|
||||||
|
"Project",
|
||||||
|
"ProjectExtension",
|
||||||
|
"Version",
|
||||||
|
"BitmapFormat",
|
||||||
|
"ClippingPlane",
|
||||||
|
"Component",
|
||||||
|
"ComponentColoring",
|
||||||
|
"ComponentColoringColor",
|
||||||
|
"ComponentSelection",
|
||||||
|
"ComponentVisibility",
|
||||||
|
"ComponentVisibilityExceptions",
|
||||||
|
"Components",
|
||||||
|
"Direction",
|
||||||
|
"Line",
|
||||||
|
"OrthogonalCamera",
|
||||||
|
"PerspectiveCamera",
|
||||||
|
"Point",
|
||||||
|
"ViewSetupHints",
|
||||||
|
"VisualizationInfo",
|
||||||
|
"VisualizationInfoBitmap",
|
||||||
|
"VisualizationInfoClippingPlanes",
|
||||||
|
"VisualizationInfoLines",
|
||||||
|
]
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from xsdata.models.datatype import XmlDateTime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class BimSnippet:
|
||||||
|
reference: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Reference",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reference_schema: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "ReferenceSchema",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
snippet_type: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "SnippetType",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is_external: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
"name": "isExternal",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class CommentViewpoint:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class HeaderFile:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
filename: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Filename",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
date: Optional[XmlDateTime] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Date",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reference: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Reference",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ifc_project: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "IfcProject",
|
||||||
|
"type": "Attribute",
|
||||||
|
"length": 22,
|
||||||
|
"pattern": r"[0-9,A-Z,a-z,_$]*",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ifc_spatial_structure_element: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "IfcSpatialStructureElement",
|
||||||
|
"type": "Attribute",
|
||||||
|
"length": 22,
|
||||||
|
"pattern": r"[0-9,A-Z,a-z,_$]*",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is_external: bool = field(
|
||||||
|
default=True,
|
||||||
|
metadata={
|
||||||
|
"name": "isExternal",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicDocumentReference:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
referenced_document: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ReferencedDocument",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
description: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Description",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is_external: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
"name": "isExternal",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicRelatedTopic:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ViewPoint:
|
||||||
|
viewpoint: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Viewpoint",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
snapshot: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Snapshot",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
index: Optional[int] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Index",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Comment:
|
||||||
|
date: XmlDateTime = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Date",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
author: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Author",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
comment: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Comment",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
viewpoint: Optional[CommentViewpoint] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Viewpoint",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modified_date: Optional[XmlDateTime] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ModifiedDate",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modified_author: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ModifiedAuthor",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Header:
|
||||||
|
file: List[HeaderFile] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "File",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_occurs": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Topic:
|
||||||
|
reference_link: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "ReferenceLink",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
title: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Title",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
priority: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Priority",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
index: Optional[int] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Index",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
labels: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Labels",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
creation_date: XmlDateTime = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CreationDate",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
creation_author: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CreationAuthor",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modified_date: Optional[XmlDateTime] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ModifiedDate",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modified_author: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ModifiedAuthor",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
due_date: Optional[XmlDateTime] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "DueDate",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assigned_to: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "AssignedTo",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
stage: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Stage",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
description: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Description",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
bim_snippet: Optional[BimSnippet] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "BimSnippet",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
document_reference: List[TopicDocumentReference] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "DocumentReference",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
related_topic: List[TopicRelatedTopic] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "RelatedTopic",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
topic_type: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "TopicType",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
topic_status: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "TopicStatus",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Markup:
|
||||||
|
header: Optional[Header] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Header",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
topic: Topic = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Topic",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
comment: List[Comment] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Comment",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
viewpoints: List[ViewPoint] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Viewpoints",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Project:
|
||||||
|
name: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Name",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
project_id: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "ProjectId",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ProjectExtension:
|
||||||
|
project: Optional[Project] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Project",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
extension_schema: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "ExtensionSchema",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Version:
|
||||||
|
detailed_version: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "DetailedVersion",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
version_id: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "VersionId",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class BitmapFormat(Enum):
|
||||||
|
PNG = "PNG"
|
||||||
|
JPG = "JPG"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Component:
|
||||||
|
originating_system: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "OriginatingSystem",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
authoring_tool_id: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "AuthoringToolId",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ifc_guid: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "IfcGuid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"length": 22,
|
||||||
|
"pattern": r"[0-9,A-Z,a-z,_$]*",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Direction:
|
||||||
|
x: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "X",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
y: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Y",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
z: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Z",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Point:
|
||||||
|
x: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "X",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
y: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Y",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
z: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Z",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ViewSetupHints:
|
||||||
|
spaces_visible: Optional[bool] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "SpacesVisible",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
space_boundaries_visible: Optional[bool] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "SpaceBoundariesVisible",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
openings_visible: Optional[bool] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "OpeningsVisible",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ClippingPlane:
|
||||||
|
location: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Location",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
direction: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Direction",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentColoringColor:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
component: List[Component] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Component",
|
||||||
|
"type": "Element",
|
||||||
|
"min_occurs": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
color: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Color",
|
||||||
|
"type": "Attribute",
|
||||||
|
"pattern": r"[0-9,a-f,A-F]{6}([0-9,a-f,A-F]{2})?",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentSelection:
|
||||||
|
component: List[Component] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Component",
|
||||||
|
"type": "Element",
|
||||||
|
"min_occurs": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentVisibilityExceptions:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
component: List[Component] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Component",
|
||||||
|
"type": "Element",
|
||||||
|
"min_occurs": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Line:
|
||||||
|
start_point: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "StartPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end_point: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "EndPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class OrthogonalCamera:
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
camera_view_point:
|
||||||
|
camera_direction:
|
||||||
|
camera_up_vector:
|
||||||
|
view_to_world_scale: view's visible size in meters
|
||||||
|
"""
|
||||||
|
camera_view_point: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraViewPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
camera_direction: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraDirection",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
camera_up_vector: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraUpVector",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
view_to_world_scale: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "ViewToWorldScale",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class PerspectiveCamera:
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
camera_view_point:
|
||||||
|
camera_direction:
|
||||||
|
camera_up_vector:
|
||||||
|
field_of_view: It is currently limited to a value between 45 and
|
||||||
|
60 degrees. This limitation will be dropped in the next
|
||||||
|
release and viewers should be expect values outside this
|
||||||
|
range in current implementations.
|
||||||
|
"""
|
||||||
|
camera_view_point: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraViewPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
camera_direction: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraDirection",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
camera_up_vector: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraUpVector",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
field_of_view: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "FieldOfView",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
"min_inclusive": 1.0,
|
||||||
|
"max_inclusive": 170.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class VisualizationInfoBitmap:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
bitmap: BitmapFormat = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Bitmap",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reference: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Reference",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
location: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Location",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
normal: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Normal",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
up: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Up",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
height: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Height",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentColoring:
|
||||||
|
color: List[ComponentColoringColor] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Color",
|
||||||
|
"type": "Element",
|
||||||
|
"min_occurs": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentVisibility:
|
||||||
|
exceptions: Optional[ComponentVisibilityExceptions] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Exceptions",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
default_visibility: Optional[bool] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "DefaultVisibility",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class VisualizationInfoClippingPlanes:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
clipping_plane: List[ClippingPlane] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "ClippingPlane",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class VisualizationInfoLines:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
line: List[Line] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Line",
|
||||||
|
"type": "Element",
|
||||||
|
"min_occurs": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Components:
|
||||||
|
view_setup_hints: Optional[ViewSetupHints] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ViewSetupHints",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
selection: Optional[ComponentSelection] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Selection",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
visibility: ComponentVisibility = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Visibility",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
coloring: Optional[ComponentColoring] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Coloring",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class VisualizationInfo:
|
||||||
|
"""
|
||||||
|
VisualizationInfo documentation.
|
||||||
|
"""
|
||||||
|
components: Optional[Components] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Components",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
orthogonal_camera: Optional[OrthogonalCamera] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "OrthogonalCamera",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
perspective_camera: Optional[PerspectiveCamera] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "PerspectiveCamera",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lines: Optional[VisualizationInfoLines] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Lines",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
clipping_planes: Optional[VisualizationInfoClippingPlanes] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ClippingPlanes",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
bitmap: List[VisualizationInfoBitmap] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Bitmap",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
"""BCF XML V2 Topic handler."""
|
||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from ifcopenshell import entity_instance
|
||||||
|
from xsdata.models.datatype import XmlDateTime
|
||||||
|
|
||||||
|
import bcf.v2.model as mdl
|
||||||
|
from bcf.inmemory_zipfile import ZipFileInterface
|
||||||
|
from bcf.v2.visinfo import VisualizationInfoHandler
|
||||||
|
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class TopicHandler:
|
||||||
|
"""BCF Topic and related objects handler."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
topic_dir: Optional[zipfile.Path] = None,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> None:
|
||||||
|
self._markup: Optional[mdl.Markup] = None
|
||||||
|
self._viewpoints: dict[str, VisualizationInfoHandler] = {}
|
||||||
|
self._reference_files: dict[str, bytes] = {}
|
||||||
|
self._document_references: dict[str, bytes] = {}
|
||||||
|
self._bim_snippet: Optional[bytes] = None
|
||||||
|
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
self._topic_dir = topic_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def markup(self) -> Optional[mdl.Markup]:
|
||||||
|
if not self._markup:
|
||||||
|
markup_path = self._topic_dir.joinpath("markup.bcf")
|
||||||
|
if markup_path.exists():
|
||||||
|
self._markup = self._xml_handler.parse(markup_path.read_bytes(), mdl.Markup)
|
||||||
|
return self._markup
|
||||||
|
|
||||||
|
@markup.setter
|
||||||
|
def markup(self, value: mdl.Markup) -> None:
|
||||||
|
self._markup = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def topic(self) -> mdl.Topic:
|
||||||
|
"""Return the Topic object."""
|
||||||
|
return self.markup.topic
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guid(self) -> str:
|
||||||
|
"""Return the GUID of the topic."""
|
||||||
|
if self._markup:
|
||||||
|
return self.topic.guid
|
||||||
|
return self._topic_dir.name if self._topic_dir else ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def header(self) -> Optional[mdl.Header]:
|
||||||
|
"""Return the header of the topic."""
|
||||||
|
return self.markup.header if self.markup else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comments(self) -> list[mdl.Comment]:
|
||||||
|
"""Return the comments of the topic."""
|
||||||
|
return self.markup.comment if self.markup else []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bim_snippet(self) -> Optional[bytes]:
|
||||||
|
if not self._bim_snippet and self._topic_dir:
|
||||||
|
self._bim_snippet = self._load_bim_snippet()
|
||||||
|
return self._bim_snippet
|
||||||
|
|
||||||
|
@bim_snippet.setter
|
||||||
|
def bim_snippet(self, value: bytes) -> None:
|
||||||
|
self._bim_snippet = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def viewpoints(self) -> dict[str, VisualizationInfoHandler]:
|
||||||
|
if not self._viewpoints and self._topic_dir:
|
||||||
|
self._viewpoints = self._load_viewpoints()
|
||||||
|
return self._viewpoints
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reference_files(self) -> dict[str, bytes]:
|
||||||
|
if self._reference_files or not self.header:
|
||||||
|
return self._reference_files
|
||||||
|
for ref in self.header.file:
|
||||||
|
if ref.is_external:
|
||||||
|
continue
|
||||||
|
real_path = self._topic_dir
|
||||||
|
for path_part in ref.reference.split("/"):
|
||||||
|
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
|
||||||
|
self._reference_files[ref.reference] = real_path.read_bytes()
|
||||||
|
return self._reference_files
|
||||||
|
|
||||||
|
@property
|
||||||
|
def document_references(self) -> dict[str, bytes]:
|
||||||
|
if self._document_references or not self.topic:
|
||||||
|
return self._document_references
|
||||||
|
for doc in self.topic.document_reference:
|
||||||
|
if doc.is_external or not doc.referenced_document:
|
||||||
|
continue
|
||||||
|
real_path = self._topic_dir
|
||||||
|
for path_part in doc.referenced_document.split("/"):
|
||||||
|
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
|
||||||
|
self._document_references[doc.referenced_document] = real_path.read_bytes()
|
||||||
|
return self._document_references
|
||||||
|
|
||||||
|
def _load_bim_snippet(self) -> Optional[bytes]:
|
||||||
|
bim_snippet_obj = self.topic.bim_snippet
|
||||||
|
if bim_snippet_obj and not bim_snippet_obj.is_external:
|
||||||
|
bim_snippet_path = self._topic_dir.joinpath(bim_snippet_obj.reference)
|
||||||
|
if bim_snippet_path.exists():
|
||||||
|
return bim_snippet_path.read_bytes()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_viewpoints(self) -> dict[str, VisualizationInfoHandler]:
|
||||||
|
if self.markup and (viewpoints := self.markup.viewpoints):
|
||||||
|
return VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_new(
|
||||||
|
cls,
|
||||||
|
title: str,
|
||||||
|
description: str,
|
||||||
|
author: str,
|
||||||
|
topic_type: str = "",
|
||||||
|
topic_status: str = "",
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> "TopicHandler":
|
||||||
|
"""
|
||||||
|
Create a new BCF topic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: The title of the topic.
|
||||||
|
description: The description of the topic.
|
||||||
|
author: The author of the topic.
|
||||||
|
topic_type: The type of the topic.
|
||||||
|
topic_status: The status of the topic.
|
||||||
|
xml_handler: The XML parser/serializer to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF topic definition.
|
||||||
|
"""
|
||||||
|
creation_date = XmlDateTime.from_datetime(datetime.datetime.now())
|
||||||
|
guid = str(uuid.uuid4())
|
||||||
|
topic = mdl.Topic(
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
creation_author=author,
|
||||||
|
creation_date=creation_date,
|
||||||
|
guid=guid,
|
||||||
|
topic_type=topic_type,
|
||||||
|
topic_status=topic_status,
|
||||||
|
)
|
||||||
|
markup = mdl.Markup(topic=topic)
|
||||||
|
obj = cls(topic_dir=Path(guid), xml_handler=xml_handler or XmlParserSerializer())
|
||||||
|
obj.markup = markup
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def save(self, destination_zip: ZipFileInterface) -> None:
|
||||||
|
"""
|
||||||
|
Save the topic to a BCF zip file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bcf_zip: The BCF zip file to save to.
|
||||||
|
"""
|
||||||
|
topic_dir = self.guid
|
||||||
|
self._save_xml(destination_zip, self._markup, "markup.bcf")
|
||||||
|
self._save_viewpoints(destination_zip, topic_dir)
|
||||||
|
self._save_bim_snippet(destination_zip)
|
||||||
|
self._save_reference_files(destination_zip)
|
||||||
|
self._save_document_references(destination_zip)
|
||||||
|
|
||||||
|
def _save_viewpoints(self, destination_zip: ZipFileInterface, topic_dir: str) -> None:
|
||||||
|
if not self.markup or not (viewpoints := self.markup.viewpoints):
|
||||||
|
return
|
||||||
|
for vpt in viewpoints:
|
||||||
|
if vpt.viewpoint:
|
||||||
|
self.viewpoints[vpt.viewpoint].save(destination_zip, topic_dir, vpt)
|
||||||
|
|
||||||
|
def _save_xml(self, destination_zip: ZipFileInterface, item: Any, target: str) -> None:
|
||||||
|
if self._topic_dir is None:
|
||||||
|
return
|
||||||
|
to_write = self._xml_handler.serialize(item) if item else self._topic_dir.joinpath(target).read_bytes()
|
||||||
|
destination_zip.writestr(f"{self._topic_dir.name}/{target}", to_write)
|
||||||
|
|
||||||
|
def _save_bim_snippet(self, destination_zip: ZipFileInterface) -> None:
|
||||||
|
snippet = self.topic.bim_snippet
|
||||||
|
if not snippet or snippet.is_external:
|
||||||
|
return
|
||||||
|
ref_filename = Path(snippet.reference).name
|
||||||
|
if self.bim_snippet:
|
||||||
|
destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet)
|
||||||
|
|
||||||
|
def _save_reference_files(self, destination_zip: ZipFileInterface) -> None:
|
||||||
|
if not self.header:
|
||||||
|
return
|
||||||
|
for ref in self.header.file:
|
||||||
|
if ref.is_external or not ref.reference:
|
||||||
|
continue
|
||||||
|
real_path = self._topic_dir
|
||||||
|
for path_part in ref.reference.split("/"):
|
||||||
|
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
|
||||||
|
destination_zip.writestr(real_path.at, self.reference_files[ref.reference])
|
||||||
|
|
||||||
|
def _save_document_references(self, destination_zip: ZipFileInterface) -> None:
|
||||||
|
if not self.topic:
|
||||||
|
return
|
||||||
|
for doc in self.topic.document_reference:
|
||||||
|
if doc.is_external or not doc.referenced_document:
|
||||||
|
continue
|
||||||
|
real_path = self._topic_dir
|
||||||
|
for path_part in doc.referenced_document.split("/"):
|
||||||
|
real_path = real_path.parent if path_part == ".." else real_path.joinpath(path_part)
|
||||||
|
destination_zip.writestr(real_path.at, self.document_references[doc.referenced_document])
|
||||||
|
|
||||||
|
def add_viewpoint(self, element: entity_instance) -> None:
|
||||||
|
"""
|
||||||
|
Add a viewpoint tergeting an IFC element to the topic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: The IFC element.
|
||||||
|
"""
|
||||||
|
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
|
||||||
|
self.add_visinfo_handler(new_viewpoint)
|
||||||
|
|
||||||
|
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
|
||||||
|
self.viewpoints[new_viewpoint.guid] = new_viewpoint
|
||||||
|
self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, TopicHandler):
|
||||||
|
raise TypeError("Equality needs a BcfXml object.")
|
||||||
|
return (
|
||||||
|
self.markup == other.markup
|
||||||
|
and self.viewpoints == other.viewpoints
|
||||||
|
and self.bim_snippet == other.bim_snippet
|
||||||
|
)
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Any, Iterable, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from ifcopenshell import entity_instance
|
||||||
|
from ifcopenshell.util import placement
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
import bcf.v2.model as mdl
|
||||||
|
from bcf.geometry import calc_camera_vectors
|
||||||
|
from bcf.inmemory_zipfile import ZipFileInterface
|
||||||
|
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class VisualizationInfoHandler:
|
||||||
|
"""Handle the VisualizationInfo and related objects."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
visualization_info: mdl.VisualizationInfo,
|
||||||
|
snapshot: Optional[bytes] = None,
|
||||||
|
bitmaps: Optional[dict[str, bytes]] = None,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> None:
|
||||||
|
self.visualization_info = visualization_info
|
||||||
|
self.snapshot = snapshot
|
||||||
|
self.bitmaps = bitmaps or {}
|
||||||
|
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guid(self) -> str:
|
||||||
|
"""Return the GUID of the visualization info."""
|
||||||
|
return self.visualization_info.guid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_topic_viewpoints(
|
||||||
|
cls,
|
||||||
|
topic_dir: zipfile.Path,
|
||||||
|
vps: Iterable[mdl.ViewPoint],
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> dict[str, "VisualizationInfoHandler"]:
|
||||||
|
"""Create VisualizationInfoHandler objects of a Topic's ViewPoints."""
|
||||||
|
viewpoints = {}
|
||||||
|
for vpt in vps:
|
||||||
|
visinfo = cls.load(topic_dir, vpt, xml_handler)
|
||||||
|
if visinfo and vpt.viewpoint:
|
||||||
|
viewpoints[vpt.viewpoint] = visinfo
|
||||||
|
return viewpoints
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(
|
||||||
|
cls,
|
||||||
|
topic_dir: zipfile.Path,
|
||||||
|
vpt: mdl.ViewPoint,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> Optional["VisualizationInfoHandler"]:
|
||||||
|
"""
|
||||||
|
Load the VisualizationInfo and related objects from a BCF zip file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic_dir: The directory in the BCF zip file to load from.
|
||||||
|
vpt: The ViewPoint to load.
|
||||||
|
xml_handler: The XML handler to use to parse the VisualizationInfo.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The VisualizationInfoHandler object.
|
||||||
|
"""
|
||||||
|
visinfo = cls._load_visinfo(topic_dir, vpt.viewpoint, xml_handler)
|
||||||
|
if not visinfo:
|
||||||
|
return None
|
||||||
|
snapshot = cls._load_snapshot(topic_dir, vpt.snapshot)
|
||||||
|
bitmaps = cls._load_bitmaps(topic_dir, visinfo)
|
||||||
|
return cls(visinfo, snapshot, bitmaps, xml_handler)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_visinfo(
|
||||||
|
topic_dir: zipfile.Path,
|
||||||
|
vp_name: Optional[str],
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> Optional[mdl.VisualizationInfo]:
|
||||||
|
if not vp_name:
|
||||||
|
return None
|
||||||
|
vp_path = topic_dir.joinpath(vp_name)
|
||||||
|
if vp_path.exists():
|
||||||
|
xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
return xml_handler.parse(vp_path.read_bytes(), mdl.VisualizationInfo)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_snapshot(topic_dir: zipfile.Path, vp_snapshot: Optional[str]) -> Optional[bytes]:
|
||||||
|
if vp_snapshot:
|
||||||
|
snapshot_path = topic_dir.joinpath(vp_snapshot)
|
||||||
|
if snapshot_path.exists():
|
||||||
|
return snapshot_path.read_bytes()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_bitmaps(topic_dir: zipfile.Path, visinfo: Optional[mdl.VisualizationInfo]) -> dict[str, bytes]:
|
||||||
|
if not visinfo or not (bitmaps := visinfo.bitmap):
|
||||||
|
return {}
|
||||||
|
bitmaps_dict = {}
|
||||||
|
for bitmap in bitmaps:
|
||||||
|
if not bitmap.reference:
|
||||||
|
continue
|
||||||
|
bitmap_path = topic_dir.joinpath(bitmap.reference)
|
||||||
|
if bitmap_path.exists():
|
||||||
|
bitmaps_dict[bitmap.reference] = bitmap_path.read_bytes()
|
||||||
|
return bitmaps_dict
|
||||||
|
|
||||||
|
def save(
|
||||||
|
self,
|
||||||
|
bcf_zip: ZipFileInterface,
|
||||||
|
topic_dir: str,
|
||||||
|
vpt: mdl.ViewPoint,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Save the VisualizationInfo and related objects to a BCF zip file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bcf_zip: The BCF zip file to save to.
|
||||||
|
topic_dir: The directory in the BCF zip file to save to.
|
||||||
|
vpt: The ViewPoint to save.
|
||||||
|
"""
|
||||||
|
if not (vp_name := vpt.viewpoint):
|
||||||
|
return
|
||||||
|
self._save_visinfo(bcf_zip, topic_dir, vp_name)
|
||||||
|
self._save_snapshot(bcf_zip, topic_dir, vpt.snapshot)
|
||||||
|
self._save_bitmaps(bcf_zip, topic_dir)
|
||||||
|
|
||||||
|
def _save_snapshot(self, bcf_zip: ZipFileInterface, topic_dir: str, filename: Optional[str]) -> None:
|
||||||
|
if self.snapshot and filename:
|
||||||
|
bcf_zip.writestr(f"{topic_dir}/{filename}", self.snapshot)
|
||||||
|
|
||||||
|
def _save_visinfo(self, bcf_zip: ZipFileInterface, topic_dir: str, vp_name: str) -> None:
|
||||||
|
bcf_zip.writestr(
|
||||||
|
f"{topic_dir}/{vp_name}",
|
||||||
|
self._xml_handler.serialize(self.visualization_info),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _save_bitmaps(self, bcf_zip: ZipFileInterface, topic_dir: str) -> None:
|
||||||
|
if not self.bitmaps:
|
||||||
|
return
|
||||||
|
if not (bitmaps_defs := self.visualization_info.bitmap):
|
||||||
|
return
|
||||||
|
for bitmap_def in bitmaps_defs:
|
||||||
|
if not (bitmap_name := bitmap_def.reference):
|
||||||
|
continue
|
||||||
|
if bitmap_name in self.bitmaps:
|
||||||
|
bcf_zip.writestr(f"{topic_dir}/{bitmap_name}", self.bitmaps[bitmap_name])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_new(
|
||||||
|
cls,
|
||||||
|
element: entity_instance,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> "VisualizationInfoHandler":
|
||||||
|
"""
|
||||||
|
Create a new VisualizationInfoHandler object from an IFC element.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: The IFC element to point at.
|
||||||
|
xml_handler: The XML handler to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The VisualizationInfoHandler object.
|
||||||
|
"""
|
||||||
|
xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
|
||||||
|
"""
|
||||||
|
Return a BCF viewpoint of an IFC element.
|
||||||
|
|
||||||
|
This function is cached to speedudp the creation of multiple BCF topics regarding the same element.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: The IFC element to point at.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF viewpoint definition.
|
||||||
|
"""
|
||||||
|
elem_placement = placement.get_local_placement(element.ObjectPlacement)
|
||||||
|
|
||||||
|
return mdl.VisualizationInfo(
|
||||||
|
guid=str(uuid.uuid4()),
|
||||||
|
components=build_components(element.GlobalId),
|
||||||
|
perspective_camera=build_camera(elem_placement),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_components(guid: str) -> mdl.Components:
|
||||||
|
"""
|
||||||
|
Return the BCF components from an IFC element GUID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guid: The IFC element GUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF components definition.
|
||||||
|
"""
|
||||||
|
return mdl.Components(
|
||||||
|
selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]),
|
||||||
|
visibility=mdl.ComponentVisibility(default_visibility=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera:
|
||||||
|
"""
|
||||||
|
Return a BCF camera for an IFC element placement matrix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
elem_placement: The IFC element placement as a rototranslation matrix.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF camera definition.
|
||||||
|
"""
|
||||||
|
return build_camera_from_vectors(*calc_camera_vectors(elem_placement))
|
||||||
|
|
||||||
|
|
||||||
|
def build_camera_from_vectors(
|
||||||
|
camera_position: NDArray[np.float_], camera_dir: NDArray[np.float_], camera_up: NDArray[np.float_]
|
||||||
|
) -> mdl.PerspectiveCamera:
|
||||||
|
"""
|
||||||
|
Return a BCF camera for an IFC element placement matrix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_position: camera position array
|
||||||
|
camera_dir: camera direction versor
|
||||||
|
camera_up_vector: camera up versor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF camera definition.
|
||||||
|
"""
|
||||||
|
camera_viewpoint = mdl.Point(x=camera_position[0], y=camera_position[1], z=camera_position[2])
|
||||||
|
camera_direction = mdl.Direction(x=camera_dir[0], y=camera_dir[1], z=camera_dir[2])
|
||||||
|
camera_up_vector = mdl.Direction(x=camera_up[0], y=camera_up[1], z=camera_up[2])
|
||||||
|
return mdl.PerspectiveCamera(
|
||||||
|
camera_view_point=camera_viewpoint,
|
||||||
|
camera_direction=camera_direction,
|
||||||
|
camera_up_vector=camera_up_vector,
|
||||||
|
field_of_view=60.0,
|
||||||
|
)
|
||||||
@@ -1,19 +1 @@
|
|||||||
|
"""BCF XML v3 handler."""
|
||||||
# BCF - BCF Python library
|
|
||||||
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
|
||||||
#
|
|
||||||
# This file is part of BCF.
|
|
||||||
#
|
|
||||||
# BCF is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# BCF is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
|
||||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|||||||
+172
-184
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# BCF - BCF Python library
|
# BCF - BCF Python library
|
||||||
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
||||||
#
|
#
|
||||||
@@ -17,25 +16,27 @@
|
|||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import uuid
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import urllib
|
|
||||||
import requests
|
|
||||||
import webbrowser
|
|
||||||
import http.server
|
|
||||||
import base64
|
import base64
|
||||||
import tempfile
|
import http.server
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import urllib
|
||||||
|
import uuid
|
||||||
|
import webbrowser
|
||||||
|
from re import A
|
||||||
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
client_id, client_secret = "", ""
|
client_id, client_secret = "", ""
|
||||||
|
|
||||||
|
|
||||||
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self) -> None:
|
||||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||||
self.server.auth_code = query.get("code", [""])[0]
|
self.server.auth_code = query.get("code", [""])[0] # type:ignore
|
||||||
self.server.auth_state = query.get("state", [""])[0]
|
self.server.auth_state = query.get("state", [""])[0] # type:ignore
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/plain")
|
self.send_header("Content-type", "text/plain")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -43,20 +44,21 @@ class OAuthReceiver(http.server.BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class FoundationClient:
|
class FoundationClient:
|
||||||
def __init__(self, client_id, client_secret, base_url=None, redirect_subdir=None):
|
def __init__(
|
||||||
|
self, client_id: str, client_secret: str, base_url: Optional[str] = None, redirect_subdir: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
self.baseurl = base_url
|
self.baseurl = base_url
|
||||||
self.access_token = ""
|
self.access_token = ""
|
||||||
self.refresh_token = ""
|
self.refresh_token = ""
|
||||||
self.access_token_expires_on = time.time()
|
self.access_token_expires_on = time.time()
|
||||||
self.refresh_token_expires_on = float("inf")
|
self.refresh_token_expires_on = float("inf")
|
||||||
self.auth_endpoint = None
|
self.token_endpoint = ""
|
||||||
self.token_endpoint = None
|
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
self.auth_method = None
|
self.auth_method: Optional[str] = None
|
||||||
self.redirect_subdir = redirect_subdir
|
self.redirect_subdir = redirect_subdir
|
||||||
|
|
||||||
def get_access_token(self):
|
def get_access_token(self) -> str:
|
||||||
if self.access_token and self.access_token_expires_on > time.time():
|
if self.access_token and self.access_token_expires_on > time.time():
|
||||||
return self.access_token
|
return self.access_token
|
||||||
elif self.refresh_token and self.refresh_token_expires_on > time.time():
|
elif self.refresh_token and self.refresh_token_expires_on > time.time():
|
||||||
@@ -65,18 +67,18 @@ class FoundationClient:
|
|||||||
self.login()
|
self.login()
|
||||||
return self.access_token
|
return self.access_token
|
||||||
|
|
||||||
def get_auth_methods(self):
|
def get_auth_methods(self) -> list[Any]:
|
||||||
resp = requests.get(f"{self.baseurl}foundation/1.0/auth")
|
resp = requests.get(f"{self.baseurl}foundation/1.0/auth")
|
||||||
return resp.json()["supported_oauth2_flows"]
|
return resp.json()["supported_oauth2_flows"]
|
||||||
|
|
||||||
def get_versions(self):
|
def get_versions(self) -> list[Any]:
|
||||||
resp = requests.get(f"{self.baseurl}foundation/versions")
|
resp = requests.get(f"{self.baseurl}foundation/versions")
|
||||||
return resp.json()["versions"]
|
return resp.json()["versions"]
|
||||||
|
|
||||||
def login(self):
|
def login(self) -> None:
|
||||||
resp = requests.get(f"{self.baseurl}foundation/1.0/auth")
|
resp = requests.get(f"{self.baseurl}foundation/1.0/auth")
|
||||||
values = resp.json()
|
values = resp.json()
|
||||||
self.auth_endpoint = values["oauth2_auth_url"]
|
auth_endpoint = values["oauth2_auth_url"]
|
||||||
self.token_endpoint = values["oauth2_token_url"]
|
self.token_endpoint = values["oauth2_token_url"]
|
||||||
|
|
||||||
with http.server.HTTPServer(("", 8080), OAuthReceiver) as server:
|
with http.server.HTTPServer(("", 8080), OAuthReceiver) as server:
|
||||||
@@ -89,25 +91,22 @@ class FoundationClient:
|
|||||||
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
|
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if "?" in self.auth_endpoint:
|
if "?" in auth_endpoint:
|
||||||
webbrowser.open(f"{self.auth_endpoint}&{query}")
|
webbrowser.open(f"{auth_endpoint}&{query}")
|
||||||
else:
|
else:
|
||||||
webbrowser.open(f"{self.auth_endpoint}?{query}")
|
webbrowser.open(f"{auth_endpoint}?{query}")
|
||||||
server.timeout = 100
|
server.timeout = 100
|
||||||
server.state = state
|
|
||||||
server.handle_request()
|
server.handle_request()
|
||||||
if server.auth_code and server.auth_state == state:
|
if server.auth_code and server.auth_state == state: # pylint: disable=E1101
|
||||||
data = {
|
data = {
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"code": server.auth_code,
|
"code": server.auth_code, # pylint: disable=E1101 type:ignore
|
||||||
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
|
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
|
||||||
}
|
}
|
||||||
auth_string = f"{self.client_id}:{self.client_secret}"
|
headers = self._get_access_token_headers()
|
||||||
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
|
||||||
headers = {"Authorization": f"Basic {header_string}"}
|
|
||||||
self.set_tokens_from_response(requests.post(self.token_endpoint, data=data, headers=headers))
|
self.set_tokens_from_response(requests.post(self.token_endpoint, data=data, headers=headers))
|
||||||
|
|
||||||
def get_refresh_token(self):
|
def get_refresh_token(self) -> None:
|
||||||
self.set_tokens_from_response(
|
self.set_tokens_from_response(
|
||||||
requests.post(
|
requests.post(
|
||||||
self.token_endpoint,
|
self.token_endpoint,
|
||||||
@@ -118,10 +117,8 @@ class FoundationClient:
|
|||||||
).json()
|
).json()
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_new_access_token(self):
|
def get_new_access_token(self) -> None:
|
||||||
auth_string = f"{self.client_id}:{self.client_secret}"
|
headers = self._get_access_token_headers()
|
||||||
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
|
||||||
headers = {"Authorization": f"Basic {header_string}"}
|
|
||||||
self.set_tokens_from_response(
|
self.set_tokens_from_response(
|
||||||
requests.post(
|
requests.post(
|
||||||
self.token_endpoint,
|
self.token_endpoint,
|
||||||
@@ -133,35 +130,41 @@ class FoundationClient:
|
|||||||
).json()
|
).json()
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_auth_method(self, method="authorization_code_grant"):
|
def _get_access_token_headers(self) -> dict[str, str]:
|
||||||
|
auth_string = f"{self.client_id}:{self.client_secret}"
|
||||||
|
header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
|
||||||
|
return {"Authorization": f"Basic {header_string}"}
|
||||||
|
|
||||||
|
def set_auth_method(self, method: str = "authorization_code_grant") -> None:
|
||||||
if method != "authorization_code_grant":
|
if method != "authorization_code_grant":
|
||||||
raise NotImplementedError(f"{method} not supported")
|
raise NotImplementedError(f"{method} not supported")
|
||||||
else:
|
else:
|
||||||
self.auth_method = method
|
self.auth_method = method
|
||||||
|
|
||||||
def set_tokens_from_response(self, response):
|
def set_tokens_from_response(self, response: requests.Response) -> None:
|
||||||
response = response.json()
|
response_dict = response.json()
|
||||||
self.access_token = response["access_token"]
|
self.access_token = response_dict["access_token"]
|
||||||
self.refresh_token = response["refresh_token"]
|
self.refresh_token = response_dict["refresh_token"]
|
||||||
self.access_token_expires_on = time.time() + response["expires_in"]
|
self.access_token_expires_on = time.time() + response_dict["expires_in"]
|
||||||
if "refresh_token_expires_in" in response:
|
if "refresh_token_expires_in" in response_dict:
|
||||||
self.refresh_token_expires_on = time.time() + response["refresh_token_expires_in"]
|
self.refresh_token_expires_on = time.time() + response_dict["refresh_token_expires_in"]
|
||||||
|
|
||||||
|
|
||||||
class BcfClient:
|
class BcfClient:
|
||||||
def __init__(self, foundation_client):
|
def __init__(self, foundation_client: FoundationClient) -> None:
|
||||||
self.foundation_client = foundation_client
|
self.foundation_client = foundation_client
|
||||||
self.version_id = None
|
self.version_id: Optional[str] = None
|
||||||
self.baseurl = None
|
self.baseurl: Optional[str] = None
|
||||||
self.filepath = tempfile.mkdtemp()
|
self.filepath = tempfile.mkdtemp()
|
||||||
|
|
||||||
def set_version(self, version):
|
def set_version(self, version: dict[str, str]) -> None:
|
||||||
self.version_id = version["version_id"]
|
self.version_id = version["version_id"]
|
||||||
self.baseurl = version["api_base_url"]
|
self.baseurl = version["api_base_url"]
|
||||||
|
|
||||||
def get(self, endpoint, params=None, is_auth_required=False):
|
def get(self, endpoint: str, params: Any = None, is_auth_required: bool = False) -> Any:
|
||||||
# TODO: handle error http status codes and raise exception. Follow error.json standard.
|
# TODO: handle error http status codes and raise exception. Follow error.json standard.
|
||||||
headers = {"Authorization": "Bearer " + self.foundation_client.get_access_token()}
|
headers = {"Authorization": f"Bearer {self.foundation_client.get_access_token()}"}
|
||||||
|
|
||||||
response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
|
response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
|
||||||
try:
|
try:
|
||||||
response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
|
response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
|
||||||
@@ -171,68 +174,62 @@ class BcfClient:
|
|||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
print(f"message: {response.reason}' '{response.status_code}' '{ e }")
|
print(f"message: {response.reason}' '{response.status_code}' '{ e }")
|
||||||
|
|
||||||
def post(self, endpoint, data=None, params=None):
|
def post(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.baseurl}{endpoint}",
|
f"{self.baseurl}{endpoint}", headers=headers, params=params or None, data=data or None
|
||||||
headers=headers,
|
|
||||||
params=params or None,
|
|
||||||
data=data or None,
|
|
||||||
)
|
)
|
||||||
if response.status_code == 201:
|
|
||||||
return response.status_code, response.text
|
if response.status_code != 201:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
return response.status_code, response.text
|
||||||
except requests.exceptions.HTTPError as errh:
|
except requests.exceptions.HTTPError as errh:
|
||||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||||
|
return response.status_code, response.reason
|
||||||
|
|
||||||
def put(self, endpoint, data=None, params=None):
|
def put(self, endpoint: str, data: Any = None, params: Any = None) -> Tuple[int, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.put(
|
response = requests.put(
|
||||||
f"{self.baseurl}{endpoint}",
|
f"{self.baseurl}{endpoint}", headers=headers, params=params or None, data=data or None
|
||||||
headers=headers,
|
|
||||||
params=params or None,
|
|
||||||
data=data or None,
|
|
||||||
)
|
)
|
||||||
if response.status_code == 200:
|
|
||||||
return response.status_code, response.text
|
if response.status_code != 200:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
return response.status_code, response.text
|
||||||
except requests.exceptions.HTTPError as errh:
|
except requests.exceptions.HTTPError as errh:
|
||||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||||
|
return response.status_code, response.reason
|
||||||
|
|
||||||
def delete(self, endpoint, params=None):
|
def delete(self, endpoint: str, params: Any = None) -> Tuple[int, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.delete(
|
response = requests.delete(f"{self.baseurl}{endpoint}", headers=headers, params=params or None)
|
||||||
f"{self.baseurl}{endpoint}",
|
|
||||||
headers=headers,
|
if response.status_code != 200:
|
||||||
params=params or None,
|
response.raise_for_status()
|
||||||
)
|
return response.status_code, response.text
|
||||||
if response.status_code == 200:
|
|
||||||
return response.status_code, response.text
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.exceptions.HTTPError as errh:
|
except requests.exceptions.HTTPError as errh:
|
||||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||||
|
return response.status_code, response.reason
|
||||||
|
|
||||||
def get_projects(self) -> list:
|
def get_projects(self) -> list[Any]:
|
||||||
return self.get(
|
return self.get("/projects")
|
||||||
f"/projects",
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_project(
|
def get_project(self, project_id: str = "") -> dict[str, Any]:
|
||||||
self,
|
|
||||||
project_id="",
|
|
||||||
) -> dict:
|
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}",
|
f"/projects/{project_id}",
|
||||||
{
|
{
|
||||||
@@ -240,16 +237,13 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_project(self, project_id="", data=None) -> dict:
|
def update_project(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
|
||||||
url = f"{self.baseurl}/projects/{project_id}"
|
url = f"{self.baseurl}/projects/{project_id}"
|
||||||
headers = {"Authorization": "Bearer " + self.foundation_client.get_access_token()}
|
headers = {"Authorization": f"Bearer {self.foundation_client.get_access_token()}"}
|
||||||
resp = requests.put(url, headers=headers, data=data)
|
resp = requests.put(url, headers=headers, data=data)
|
||||||
return resp.status_code, resp.text
|
return resp.status_code, resp.text
|
||||||
|
|
||||||
def get_extensions(
|
def get_extensions(self, project_id: str = "") -> dict[str, Any]:
|
||||||
self,
|
|
||||||
project_id="",
|
|
||||||
) -> dict:
|
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/extensions",
|
f"/projects/{project_id}/extensions",
|
||||||
{
|
{
|
||||||
@@ -259,10 +253,10 @@ class BcfClient:
|
|||||||
|
|
||||||
def get_topics(
|
def get_topics(
|
||||||
self,
|
self,
|
||||||
project_id="",
|
project_id: str = "",
|
||||||
topics="",
|
topics: str = "",
|
||||||
query_string=None,
|
query_string: Optional[str] = None,
|
||||||
) -> list:
|
) -> list[Any]:
|
||||||
# return self.get(
|
# return self.get(
|
||||||
# f"/projects/{project_id}/topics",
|
# f"/projects/{project_id}/topics",
|
||||||
# {
|
# {
|
||||||
@@ -273,7 +267,7 @@ class BcfClient:
|
|||||||
# )
|
# )
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_topic(self, project_id="", topic_id="") -> dict:
|
def get_topic(self, project_id: str = "", topic_id: str = "") -> dict[str, Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}",
|
f"/projects/{project_id}/topics/{topic_id}",
|
||||||
{
|
{
|
||||||
@@ -282,42 +276,40 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_topic(self, project_id="", data=None):
|
def create_topic(self, project_id: str = "", data: Any = None) -> Tuple[int, str]:
|
||||||
return self.post(f"/projects/{project_id}/topics", data=data)
|
return self.post(f"/projects/{project_id}/topics", data=data)
|
||||||
|
|
||||||
def update_topic(self, project_id="", topic_id="", data=None) -> dict:
|
def update_topic(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
|
||||||
return self.put(f"/projects/{project_id}/topics/{topic_id}", data=data)
|
return self.put(f"/projects/{project_id}/topics/{topic_id}", data=data)
|
||||||
|
|
||||||
def delete_topic(self, project_id="", topic_id=""):
|
def delete_topic(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
|
||||||
return self.delete(f"/projects/{project_id}/topics/{topic_id}")
|
return self.delete(f"/projects/{project_id}/topics/{topic_id}")
|
||||||
|
|
||||||
def get_snippet(self, project_id="", topic_id="") -> str:
|
def get_snippet(self, project_id: str = "", topic_id: str = "") -> Tuple[int, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||||
"Content-type": "application/octet-stream",
|
"Content-type": "application/octet-stream",
|
||||||
}
|
}
|
||||||
response = requests.get(
|
|
||||||
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
# TODO: write to tmpdir
|
|
||||||
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_snippet.txt"), "wb") as f:
|
|
||||||
f.write(response.content.decode("utf-8"))
|
|
||||||
return response.status_code, response.content
|
|
||||||
|
|
||||||
def update_snippet(self, project_id="", topic_id="", files=None, data=None):
|
response = requests.get(f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet", headers=headers)
|
||||||
|
content = response.content.decode("utf-8")
|
||||||
|
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_snippet.txt"), "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
return response.status_code, content
|
||||||
|
|
||||||
|
def update_snippet(self, project_id: str = "", topic_id: str = "", files: Any = None, data: Any = None) -> int:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||||
"Content-type": "application/octet-stream",
|
"Content-type": "application/octet-stream",
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.put(
|
response = requests.put(
|
||||||
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet",
|
f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet", headers=headers, files=files
|
||||||
headers=headers,
|
|
||||||
files=files,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return response.status_code
|
return response.status_code
|
||||||
|
|
||||||
def get_files_information(self, project_id="") -> list:
|
def get_files_information(self, project_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/files_information",
|
f"/projects/{project_id}/files_information",
|
||||||
{
|
{
|
||||||
@@ -325,7 +317,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_files(self, project_id="", topic_id="") -> list:
|
def get_files(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/files",
|
f"/projects/{project_id}/topics/{topic_id}/files",
|
||||||
{
|
{
|
||||||
@@ -336,32 +328,32 @@ class BcfClient:
|
|||||||
|
|
||||||
def update_files(
|
def update_files(
|
||||||
self,
|
self,
|
||||||
project_id="",
|
project_id: str = "",
|
||||||
topic_id="",
|
topic_id: str = "",
|
||||||
data=None,
|
data: Any = None,
|
||||||
params=None,
|
params: Any = None,
|
||||||
):
|
) -> Tuple[int, str]:
|
||||||
return self.put(
|
return self.put(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/files",
|
f"/projects/{project_id}/topics/{topic_id}/files",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_comments(self, project_id="", topic_id="") -> list:
|
def get_comments(self, project_id: str = "", topic_id: str = "") -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create_comments(
|
def create_comments(
|
||||||
self,
|
self,
|
||||||
project_id="",
|
project_id: str = "",
|
||||||
topic_id="",
|
topic_id: str = "",
|
||||||
data=None,
|
data: Any = None,
|
||||||
params=None,
|
params: Any = None,
|
||||||
):
|
) -> Tuple[int, str]:
|
||||||
return self.post(
|
return self.post(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/comments",
|
f"/projects/{project_id}/topics/{topic_id}/comments",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_comment(self, project_id="", topic_id="", comment_id="") -> dict:
|
def get_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> dict[str, Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
|
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
|
||||||
{
|
{
|
||||||
@@ -371,22 +363,22 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_comment(self, project_id="", topic_id="", comment_id=""):
|
def delete_comment(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> Tuple[int, str]:
|
||||||
return self.delete(f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}")
|
return self.delete(f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}")
|
||||||
|
|
||||||
def update_comment(
|
def update_comment(
|
||||||
self,
|
self,
|
||||||
project_id="",
|
project_id: str = "",
|
||||||
topic_id="",
|
topic_id: str = "",
|
||||||
comment_id="",
|
comment_id: str = "",
|
||||||
data=None,
|
data: Any = None,
|
||||||
):
|
) -> Tuple[int, str]:
|
||||||
return self.put(
|
return self.put(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
|
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_viewpoints(self, project_id="", topic_id="") -> list:
|
def get_viewpoints(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
|
||||||
{
|
{
|
||||||
@@ -395,13 +387,13 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_viewpoints(self, project_id="", topic_id="", data=None):
|
def create_viewpoints(self, project_id: str = "", topic_id: str = "", data: Any = None) -> Tuple[int, str]:
|
||||||
return self.post(
|
return self.post(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_viewpoint(self, project_id="", topic_id="", viewpoint_id="") -> dict:
|
def get_viewpoint(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
|
||||||
{
|
{
|
||||||
@@ -413,15 +405,15 @@ class BcfClient:
|
|||||||
|
|
||||||
def delete_viewpoint(
|
def delete_viewpoint(
|
||||||
self,
|
self,
|
||||||
project_id="",
|
project_id: str = "",
|
||||||
topic_id="",
|
topic_id: str = "",
|
||||||
viewpoint_id="",
|
viewpoint_id: str = "",
|
||||||
):
|
) -> Tuple[int, str]:
|
||||||
return self.delete(
|
return self.delete(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_snapshot(self, project_id="", topic_id="", viewpoint_id="") -> str:
|
def get_snapshot(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> str:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot",
|
||||||
{
|
{
|
||||||
@@ -431,7 +423,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_bitmap(self, project_id="", topic_id="", viewpoint_id="", bitmap_id="") -> str:
|
def get_bitmap(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "", bitmap_id: str = "") -> str:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}",
|
||||||
{
|
{
|
||||||
@@ -442,7 +434,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_selection(self, project_id="", topic_id="", viewpoint_id="") -> dict:
|
def get_selection(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection",
|
||||||
{
|
{
|
||||||
@@ -452,7 +444,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_coloring(self, project_id="", topic_id="", viewpoint_id="") -> dict:
|
def get_coloring(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring",
|
||||||
{
|
{
|
||||||
@@ -462,7 +454,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_visibility(self, project_id="", topic_id="", viewpoint_id="") -> dict:
|
def get_visibility(self, project_id: str = "", topic_id: str = "", viewpoint_id: str = "") -> dict[str, Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility",
|
f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility",
|
||||||
{
|
{
|
||||||
@@ -472,7 +464,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_related_topics(self, project_id="", topic_id="") -> list:
|
def get_related_topics(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/related_topics",
|
f"/projects/{project_id}/topics/{topic_id}/related_topics",
|
||||||
{
|
{
|
||||||
@@ -483,16 +475,16 @@ class BcfClient:
|
|||||||
|
|
||||||
def update_related_topics(
|
def update_related_topics(
|
||||||
self,
|
self,
|
||||||
project_id="",
|
project_id: str = "",
|
||||||
topic_id="",
|
topic_id: str = "",
|
||||||
data=None,
|
data: Any = None,
|
||||||
):
|
) -> Tuple[int, str]:
|
||||||
return self.put(
|
return self.put(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/related_topics",
|
f"/projects/{project_id}/topics/{topic_id}/related_topics",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_document_references(self, project_id="", topic_id="") -> list:
|
def get_document_references(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/document_references",
|
f"/projects/{project_id}/topics/{topic_id}/document_references",
|
||||||
{
|
{
|
||||||
@@ -503,10 +495,10 @@ class BcfClient:
|
|||||||
|
|
||||||
def create_document_reference(
|
def create_document_reference(
|
||||||
self,
|
self,
|
||||||
project_id="",
|
project_id: str = "",
|
||||||
topic_id="",
|
topic_id: str = "",
|
||||||
data=None,
|
data: Any = None,
|
||||||
):
|
) -> Tuple[int, str]:
|
||||||
return self.post(
|
return self.post(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/document_references",
|
f"/projects/{project_id}/topics/{topic_id}/document_references",
|
||||||
data=data,
|
data=data,
|
||||||
@@ -514,17 +506,17 @@ class BcfClient:
|
|||||||
|
|
||||||
def update_document_references(
|
def update_document_references(
|
||||||
self,
|
self,
|
||||||
project_id="",
|
project_id: str = "",
|
||||||
topic_id="",
|
topic_id: str = "",
|
||||||
document_reference_id="",
|
document_reference_id: str = "",
|
||||||
data=None,
|
data: Any = None,
|
||||||
):
|
) -> Tuple[int, str]:
|
||||||
return self.put(
|
return self.put(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/document_references/{document_reference_id}",
|
f"/projects/{project_id}/topics/{topic_id}/document_references/{document_reference_id}",
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_documents(self, project_id="", topic_id="") -> list:
|
def get_documents(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/documents",
|
f"/projects/{project_id}/topics/{topic_id}/documents",
|
||||||
{
|
{
|
||||||
@@ -534,40 +526,36 @@ class BcfClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create_document(
|
def create_document(
|
||||||
self,
|
self, project_id: str = "", topic_id: str = "", guid: Optional[str] = None, files: Any = None, data: Any = None
|
||||||
project_id="",
|
) -> int:
|
||||||
topic_id="",
|
|
||||||
guid=None,
|
|
||||||
files=None,
|
|
||||||
data=None,
|
|
||||||
):
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||||
"Content-type": "application/octet-stream",
|
"Content-type": "application/octet-stream",
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/documents",
|
f"/projects/{project_id}/topics/{topic_id}/documents",
|
||||||
data=data,
|
data=data,
|
||||||
params={guid},
|
params={"guid": guid},
|
||||||
files=files,
|
files=files,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
return response.status_code
|
return response.status_code
|
||||||
|
|
||||||
def get_document(self, project_id="", topic_id="", document_id="") -> str:
|
def get_document(self, project_id: str = "", topic_id: str = "", document_id: str = "") -> Tuple[int, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.foundation_client.get_access_token(),
|
"Authorization": f"Bearer {self.foundation_client.get_access_token()}",
|
||||||
"Content-type": "application/octet-stream",
|
"Content-type": "application/octet-stream",
|
||||||
}
|
}
|
||||||
response = requests.get(
|
|
||||||
f"{self.baseurl}/projects/{project_id}/topics/documents/{document_id}",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_{document_id}_document.txt"), "wb") as f:
|
|
||||||
f.write(response.content.decode("utf-8"))
|
|
||||||
return response.status_code, response.content
|
|
||||||
|
|
||||||
def get_topics_events(self, project_id="") -> list:
|
response = requests.get(f"{self.baseurl}/projects/{project_id}/topics/documents/{document_id}", headers=headers)
|
||||||
|
content = response.content.decode("utf-8")
|
||||||
|
with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_{document_id}_document.txt"), "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
return response.status_code, content
|
||||||
|
|
||||||
|
def get_topics_events(self, project_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/events",
|
f"/projects/{project_id}/topics/events",
|
||||||
{
|
{
|
||||||
@@ -575,7 +563,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_topic_events(self, project_id="", topic_id="") -> list:
|
def get_topic_events(self, project_id: str = "", topic_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/events",
|
f"/projects/{project_id}/topics/{topic_id}/events",
|
||||||
{
|
{
|
||||||
@@ -584,7 +572,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_comments_events(self, project_id="") -> list:
|
def get_comments_events(self, project_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/comments/events",
|
f"/projects/{project_id}/topics/comments/events",
|
||||||
{
|
{
|
||||||
@@ -592,7 +580,7 @@ class BcfClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_comment_events(self, project_id="", topic_id="", comment_id="") -> list:
|
def get_comment_events(self, project_id: str = "", topic_id: str = "", comment_id: str = "") -> list[Any]:
|
||||||
return self.get(
|
return self.get(
|
||||||
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events",
|
f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events",
|
||||||
{
|
{
|
||||||
|
|||||||
+269
-825
File diff suppressed because it is too large
Load Diff
@@ -1,200 +0,0 @@
|
|||||||
|
|
||||||
# BCF - BCF Python library
|
|
||||||
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
|
|
||||||
#
|
|
||||||
# This file is part of BCF.
|
|
||||||
#
|
|
||||||
# BCF is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# BCF is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
|
||||||
# along with BCF. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
class Project:
|
|
||||||
def __init__(self):
|
|
||||||
self.project_id = ""
|
|
||||||
self.name = ""
|
|
||||||
|
|
||||||
|
|
||||||
class BimSnippet:
|
|
||||||
def __init__(self):
|
|
||||||
self.snippet_type = None
|
|
||||||
self.is_external = False
|
|
||||||
self.reference = None
|
|
||||||
self.reference_schema = None
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentReference:
|
|
||||||
def __init__(self):
|
|
||||||
self.description = None
|
|
||||||
self.document_guid = None
|
|
||||||
self.url = None
|
|
||||||
self.guid = None
|
|
||||||
|
|
||||||
|
|
||||||
class RelatedTopic:
|
|
||||||
def __init__(self):
|
|
||||||
self.guid = None
|
|
||||||
|
|
||||||
|
|
||||||
class HeaderFile:
|
|
||||||
def __init__(self):
|
|
||||||
self.filename = ""
|
|
||||||
self.date = None
|
|
||||||
self.reference = ""
|
|
||||||
self.ifc_project = None
|
|
||||||
self.ifc_spatial_structure_element = None
|
|
||||||
self.is_external = True
|
|
||||||
|
|
||||||
|
|
||||||
class Header:
|
|
||||||
def __init__(self):
|
|
||||||
self.files = []
|
|
||||||
|
|
||||||
|
|
||||||
class Topic:
|
|
||||||
def __init__(self):
|
|
||||||
self.reference_links = []
|
|
||||||
self.title = ""
|
|
||||||
self.priority = None
|
|
||||||
self.index = None # Deprecated, stored, but ignored
|
|
||||||
self.labels = []
|
|
||||||
self.creation_date = None
|
|
||||||
self.creation_author = None
|
|
||||||
self.modified_date = None
|
|
||||||
self.modified_author = None
|
|
||||||
self.due_date = None
|
|
||||||
self.assigned_to = None
|
|
||||||
self.stage = None
|
|
||||||
self.description = None
|
|
||||||
self.bim_snippet = None
|
|
||||||
self.document_references = []
|
|
||||||
self.related_topics = []
|
|
||||||
self.topic_status = None
|
|
||||||
self.topic_type = None
|
|
||||||
self.guid = None
|
|
||||||
|
|
||||||
self.header = None
|
|
||||||
self.comments = {}
|
|
||||||
self.viewpoints = {}
|
|
||||||
self.server_assigned_id = ""
|
|
||||||
|
|
||||||
|
|
||||||
class Comment:
|
|
||||||
def __init__(self):
|
|
||||||
self.guid = None
|
|
||||||
self.date = None
|
|
||||||
self.author = ""
|
|
||||||
self.comment = ""
|
|
||||||
self.viewpoint = None
|
|
||||||
self.modified_date = None
|
|
||||||
self.modified_author = ""
|
|
||||||
|
|
||||||
|
|
||||||
class ViewSetupHints:
|
|
||||||
def __init__(self):
|
|
||||||
self.spaces_visible = False
|
|
||||||
self.space_boundaries_visible = False
|
|
||||||
self.openings_visible = False
|
|
||||||
|
|
||||||
|
|
||||||
class Component:
|
|
||||||
def __init__(self):
|
|
||||||
self.originating_system = None
|
|
||||||
self.authoring_tool_id = None
|
|
||||||
self.ifc_guid = None
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentVisibility:
|
|
||||||
def __init__(self):
|
|
||||||
self.exceptions = []
|
|
||||||
self.default_visibility = False
|
|
||||||
self.view_setup_hints = None
|
|
||||||
|
|
||||||
|
|
||||||
class Color:
|
|
||||||
def __init__(self):
|
|
||||||
self.color = None
|
|
||||||
self.components = []
|
|
||||||
|
|
||||||
|
|
||||||
class Components:
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
self.selection = []
|
|
||||||
self.visibility = None
|
|
||||||
self.coloring = []
|
|
||||||
|
|
||||||
|
|
||||||
class Point:
|
|
||||||
def __init__(self):
|
|
||||||
self.x = 0
|
|
||||||
self.y = 0
|
|
||||||
self.z = 0
|
|
||||||
|
|
||||||
|
|
||||||
class Direction(Point):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OrthogonalCamera:
|
|
||||||
def __init__(self):
|
|
||||||
self.camera_view_point = Point()
|
|
||||||
self.camera_direction = Direction()
|
|
||||||
self.camera_up_vector = Direction()
|
|
||||||
self.view_to_world_scale = 1.0
|
|
||||||
self.aspect_ratio = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
class PerspectiveCamera:
|
|
||||||
def __init__(self):
|
|
||||||
self.camera_view_point = Point()
|
|
||||||
self.camera_direction = Direction()
|
|
||||||
self.camera_up_vector = Direction()
|
|
||||||
self.field_of_view = 60.0
|
|
||||||
self.aspect_ratio = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
class Line:
|
|
||||||
def __init__(self):
|
|
||||||
self.start_point = Point()
|
|
||||||
self.end_point = Point()
|
|
||||||
|
|
||||||
|
|
||||||
class ClippingPlane:
|
|
||||||
def __init__(self):
|
|
||||||
self.location = Point()
|
|
||||||
self.direction = Direction()
|
|
||||||
|
|
||||||
|
|
||||||
class Bitmap:
|
|
||||||
def __init__(self):
|
|
||||||
self.reference = "" # Only in BCF-XML
|
|
||||||
self.bitmap_data = None # Only in BCF-API
|
|
||||||
self.bitmap_format = "PNG" # Enum of png or jpg
|
|
||||||
self.location = Point()
|
|
||||||
self.normal = Direction()
|
|
||||||
self.up = Direction()
|
|
||||||
self.height = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
class Viewpoint:
|
|
||||||
def __init__(self):
|
|
||||||
self.guid = None
|
|
||||||
self.viewpoint = None
|
|
||||||
self.snapshot = None
|
|
||||||
self.index = None
|
|
||||||
|
|
||||||
self.components = None # It's not a list, despite the plural name
|
|
||||||
self.orthogonal_camera = None
|
|
||||||
self.perspective_camera = None
|
|
||||||
self.lines = []
|
|
||||||
self.clipping_planes = []
|
|
||||||
self.bitmaps = []
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""BCF XML V3 Documents handler."""
|
||||||
|
import zipfile
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import bcf.v3.model as mdl
|
||||||
|
from bcf.inmemory_zipfile import ZipFileInterface
|
||||||
|
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentsHandler:
|
||||||
|
"""BCF documents handler."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
definition: mdl.DocumentInfo,
|
||||||
|
documents: Optional[dict[str, bytes]] = None,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> None:
|
||||||
|
self.definition = definition
|
||||||
|
self.documents = documents or {}
|
||||||
|
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(
|
||||||
|
cls,
|
||||||
|
zip_file: zipfile.ZipFile,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> Optional["DocumentsHandler"]:
|
||||||
|
"""
|
||||||
|
Loads the documents from the given zip file directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
zip_path: The directory path inside the zip file.
|
||||||
|
xml_handler: The xml parser/serializer to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The documents handler.
|
||||||
|
"""
|
||||||
|
xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
file_to_open = zipfile.Path(zip_file, "documents.xml")
|
||||||
|
if not file_to_open.exists():
|
||||||
|
return None
|
||||||
|
definition = xml_handler.parse(file_to_open.read_bytes(), mdl.DocumentInfo)
|
||||||
|
documents = {}
|
||||||
|
if def_docs := definition.documents:
|
||||||
|
for document in def_docs.document:
|
||||||
|
document_path = zipfile.Path(zip_file, f"documents/{document.guid}")
|
||||||
|
if document_path.exists():
|
||||||
|
documents[document.filename] = document_path.read_bytes()
|
||||||
|
return cls(definition, documents=documents)
|
||||||
|
|
||||||
|
def save(self, bcf_zip: ZipFileInterface) -> None:
|
||||||
|
"""Save the documents to the zip file."""
|
||||||
|
bcf_zip.writestr("documents.xml", self._xml_handler.serialize(self.definition))
|
||||||
|
if documents := self.definition.documents:
|
||||||
|
for doc in documents.document:
|
||||||
|
if doc.filename in self.documents:
|
||||||
|
bcf_zip.writestr(
|
||||||
|
f"documents/{doc.guid}",
|
||||||
|
self.documents[doc.filename],
|
||||||
|
)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
from bcf.v3.model.documents import Document, DocumentInfo, DocumentInfoDocuments
|
||||||
|
from bcf.v3.model.extensions import (
|
||||||
|
Extensions,
|
||||||
|
ExtensionsPriorities,
|
||||||
|
ExtensionsSnippetTypes,
|
||||||
|
ExtensionsStages,
|
||||||
|
ExtensionsTopicLabels,
|
||||||
|
ExtensionsTopicStatuses,
|
||||||
|
ExtensionsTopicTypes,
|
||||||
|
ExtensionsUsers,
|
||||||
|
)
|
||||||
|
from bcf.v3.model.markup import (
|
||||||
|
BimSnippet,
|
||||||
|
Comment,
|
||||||
|
CommentViewpoint,
|
||||||
|
DocumentReference,
|
||||||
|
File,
|
||||||
|
Header,
|
||||||
|
HeaderFiles,
|
||||||
|
Markup,
|
||||||
|
Topic,
|
||||||
|
TopicComments,
|
||||||
|
TopicDocumentReferences,
|
||||||
|
TopicLabels,
|
||||||
|
TopicReferenceLinks,
|
||||||
|
TopicRelatedTopics,
|
||||||
|
TopicRelatedTopicsRelatedTopic,
|
||||||
|
TopicViewpoints,
|
||||||
|
ViewPoint,
|
||||||
|
)
|
||||||
|
from bcf.v3.model.project import Project, ProjectInfo
|
||||||
|
from bcf.v3.model.version import Version
|
||||||
|
from bcf.v3.model.visinfo import (
|
||||||
|
Bitmap,
|
||||||
|
BitmapFormat,
|
||||||
|
ClippingPlane,
|
||||||
|
Component,
|
||||||
|
ComponentColoring,
|
||||||
|
ComponentColoringColor,
|
||||||
|
ComponentColoringColorComponents,
|
||||||
|
Components,
|
||||||
|
ComponentSelection,
|
||||||
|
ComponentVisibility,
|
||||||
|
ComponentVisibilityExceptions,
|
||||||
|
Direction,
|
||||||
|
Line,
|
||||||
|
OrthogonalCamera,
|
||||||
|
PerspectiveCamera,
|
||||||
|
Point,
|
||||||
|
ViewSetupHints,
|
||||||
|
VisualizationInfo,
|
||||||
|
VisualizationInfoBitmaps,
|
||||||
|
VisualizationInfoClippingPlanes,
|
||||||
|
VisualizationInfoLines,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Document",
|
||||||
|
"DocumentInfo",
|
||||||
|
"DocumentInfoDocuments",
|
||||||
|
"Extensions",
|
||||||
|
"ExtensionsPriorities",
|
||||||
|
"ExtensionsSnippetTypes",
|
||||||
|
"ExtensionsStages",
|
||||||
|
"ExtensionsTopicLabels",
|
||||||
|
"ExtensionsTopicStatuses",
|
||||||
|
"ExtensionsTopicTypes",
|
||||||
|
"ExtensionsUsers",
|
||||||
|
"BimSnippet",
|
||||||
|
"Comment",
|
||||||
|
"CommentViewpoint",
|
||||||
|
"DocumentReference",
|
||||||
|
"File",
|
||||||
|
"Header",
|
||||||
|
"HeaderFiles",
|
||||||
|
"Markup",
|
||||||
|
"Topic",
|
||||||
|
"TopicComments",
|
||||||
|
"TopicDocumentReferences",
|
||||||
|
"TopicLabels",
|
||||||
|
"TopicReferenceLinks",
|
||||||
|
"TopicRelatedTopics",
|
||||||
|
"TopicRelatedTopicsRelatedTopic",
|
||||||
|
"TopicViewpoints",
|
||||||
|
"ViewPoint",
|
||||||
|
"Project",
|
||||||
|
"ProjectInfo",
|
||||||
|
"Version",
|
||||||
|
"Bitmap",
|
||||||
|
"BitmapFormat",
|
||||||
|
"ClippingPlane",
|
||||||
|
"Component",
|
||||||
|
"ComponentColoring",
|
||||||
|
"ComponentColoringColor",
|
||||||
|
"ComponentColoringColorComponents",
|
||||||
|
"ComponentSelection",
|
||||||
|
"ComponentVisibility",
|
||||||
|
"ComponentVisibilityExceptions",
|
||||||
|
"Components",
|
||||||
|
"Direction",
|
||||||
|
"Line",
|
||||||
|
"OrthogonalCamera",
|
||||||
|
"PerspectiveCamera",
|
||||||
|
"Point",
|
||||||
|
"ViewSetupHints",
|
||||||
|
"VisualizationInfo",
|
||||||
|
"VisualizationInfoBitmaps",
|
||||||
|
"VisualizationInfoClippingPlanes",
|
||||||
|
"VisualizationInfoLines",
|
||||||
|
]
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Document:
|
||||||
|
filename: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Filename",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
description: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Description",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class DocumentInfoDocuments:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
document: List[Document] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Document",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class DocumentInfo:
|
||||||
|
documents: Optional[DocumentInfoDocuments] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Documents",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ExtensionsPriorities:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
priority: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Priority",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ExtensionsSnippetTypes:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
snippet_type: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "SnippetType",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ExtensionsStages:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
stage: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Stage",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ExtensionsTopicLabels:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
topic_label: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "TopicLabel",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ExtensionsTopicStatuses:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
topic_status: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "TopicStatus",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ExtensionsTopicTypes:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
topic_type: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "TopicType",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ExtensionsUsers:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
user: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "User",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Extensions:
|
||||||
|
topic_types: Optional[ExtensionsTopicTypes] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "TopicTypes",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
topic_statuses: Optional[ExtensionsTopicStatuses] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "TopicStatuses",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
priorities: Optional[ExtensionsPriorities] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Priorities",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
topic_labels: Optional[ExtensionsTopicLabels] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "TopicLabels",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
users: Optional[ExtensionsUsers] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Users",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
snippet_types: Optional[ExtensionsSnippetTypes] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "SnippetTypes",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
stages: Optional[ExtensionsStages] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Stages",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,616 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from xsdata.models.datatype import XmlDateTime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class BimSnippet:
|
||||||
|
reference: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Reference",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reference_schema: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "ReferenceSchema",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
snippet_type: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "SnippetType",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is_external: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
"name": "IsExternal",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class CommentViewpoint:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class DocumentReference:
|
||||||
|
document_guid: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "DocumentGuid",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
url: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Url",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
description: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Description",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class File:
|
||||||
|
filename: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Filename",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
date: Optional[XmlDateTime] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Date",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reference: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Reference",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ifc_project: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "IfcProject",
|
||||||
|
"type": "Attribute",
|
||||||
|
"length": 22,
|
||||||
|
"pattern": r"[0-9A-Za-z_$]*",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ifc_spatial_structure_element: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "IfcSpatialStructureElement",
|
||||||
|
"type": "Attribute",
|
||||||
|
"length": 22,
|
||||||
|
"pattern": r"[0-9A-Za-z_$]*",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is_external: bool = field(
|
||||||
|
default=True,
|
||||||
|
metadata={
|
||||||
|
"name": "IsExternal",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicLabels:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
label: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Label",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicReferenceLinks:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
reference_link: List[str] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "ReferenceLink",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicRelatedTopicsRelatedTopic:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ViewPoint:
|
||||||
|
viewpoint: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Viewpoint",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
snapshot: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Snapshot",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
index: Optional[int] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Index",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Comment:
|
||||||
|
date: XmlDateTime = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Date",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
author: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Author",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
comment: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Comment",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
viewpoint: Optional[CommentViewpoint] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Viewpoint",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modified_date: Optional[XmlDateTime] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ModifiedDate",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modified_author: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ModifiedAuthor",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class HeaderFiles:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
file: List[File] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "File",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicDocumentReferences:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
document_reference: List[DocumentReference] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "DocumentReference",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicRelatedTopics:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
related_topic: List[TopicRelatedTopicsRelatedTopic] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "RelatedTopic",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicViewpoints:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
view_point: List[ViewPoint] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "ViewPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Header:
|
||||||
|
files: Optional[HeaderFiles] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Files",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class TopicComments:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
comment: List[Comment] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Comment",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Topic:
|
||||||
|
reference_links: Optional[TopicReferenceLinks] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ReferenceLinks",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
title: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Title",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
priority: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Priority",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
index: Optional[int] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Index",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
labels: Optional[TopicLabels] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Labels",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
creation_date: XmlDateTime = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CreationDate",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
creation_author: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CreationAuthor",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modified_date: Optional[XmlDateTime] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ModifiedDate",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modified_author: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ModifiedAuthor",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
due_date: Optional[XmlDateTime] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "DueDate",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assigned_to: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "AssignedTo",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
stage: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Stage",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
description: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Description",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
bim_snippet: Optional[BimSnippet] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "BimSnippet",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
document_references: Optional[TopicDocumentReferences] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "DocumentReferences",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
related_topics: Optional[TopicRelatedTopics] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "RelatedTopics",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
comments: Optional[TopicComments] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Comments",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
viewpoints: Optional[TopicViewpoints] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Viewpoints",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
server_assigned_id: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ServerAssignedId",
|
||||||
|
"type": "Attribute",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
topic_type: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "TopicType",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
topic_status: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "TopicStatus",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Markup:
|
||||||
|
header: Optional[Header] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Header",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
topic: Topic = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Topic",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Project:
|
||||||
|
name: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Name",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
project_id: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "ProjectId",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ProjectInfo:
|
||||||
|
project: Project = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Project",
|
||||||
|
"type": "Element",
|
||||||
|
"namespace": "",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Version:
|
||||||
|
version_id: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "VersionId",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class BitmapFormat(Enum):
|
||||||
|
PNG = "png"
|
||||||
|
JPG = "jpg"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Component:
|
||||||
|
originating_system: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "OriginatingSystem",
|
||||||
|
"type": "Element",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
authoring_tool_id: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "AuthoringToolId",
|
||||||
|
"type": "Element",
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ifc_guid: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "IfcGuid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"length": 22,
|
||||||
|
"pattern": r"[0-9A-Za-z_$]*",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Direction:
|
||||||
|
x: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "X",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
y: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Y",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
z: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Z",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Point:
|
||||||
|
x: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "X",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
y: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Y",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
z: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Z",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ViewSetupHints:
|
||||||
|
spaces_visible: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
"name": "SpacesVisible",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
space_boundaries_visible: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
"name": "SpaceBoundariesVisible",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
openings_visible: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
"name": "OpeningsVisible",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Bitmap:
|
||||||
|
format: BitmapFormat = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Format",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reference: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Reference",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
"min_length": 1,
|
||||||
|
"white_space": "collapse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
location: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Location",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
normal: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Normal",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
up: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Up",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
height: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Height",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ClippingPlane:
|
||||||
|
location: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Location",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
direction: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Direction",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentColoringColorComponents:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
component: List[Component] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Component",
|
||||||
|
"type": "Element",
|
||||||
|
"min_occurs": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentSelection:
|
||||||
|
component: List[Component] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Component",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentVisibilityExceptions:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
component: List[Component] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Component",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Line:
|
||||||
|
start_point: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "StartPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end_point: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "EndPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class OrthogonalCamera:
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
camera_view_point:
|
||||||
|
camera_direction:
|
||||||
|
camera_up_vector:
|
||||||
|
view_to_world_scale: view's visible vertical size in meters
|
||||||
|
aspect_ratio: Proportional relationship between the width and
|
||||||
|
the height of the view (w/h).
|
||||||
|
"""
|
||||||
|
camera_view_point: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraViewPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
camera_direction: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraDirection",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
camera_up_vector: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraUpVector",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
view_to_world_scale: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "ViewToWorldScale",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
aspect_ratio: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "AspectRatio",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
"min_exclusive": 0.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class PerspectiveCamera:
|
||||||
|
"""
|
||||||
|
Attributes
|
||||||
|
camera_view_point:
|
||||||
|
camera_direction:
|
||||||
|
camera_up_vector:
|
||||||
|
field_of_view: Vertical field of view, in degrees. It is
|
||||||
|
currently limited to a value between 45 and 60 degrees. This
|
||||||
|
limitation will be dropped in the next release and viewers
|
||||||
|
should be expect values outside this range in current
|
||||||
|
implementations.
|
||||||
|
aspect_ratio: Proportional relationship between the width and
|
||||||
|
the height of the view (w/h).
|
||||||
|
"""
|
||||||
|
camera_view_point: Point = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraViewPoint",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
camera_direction: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraDirection",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
camera_up_vector: Direction = field(
|
||||||
|
metadata={
|
||||||
|
"name": "CameraUpVector",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
field_of_view: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "FieldOfView",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
"min_exclusive": 0.0,
|
||||||
|
"max_exclusive": 180.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
aspect_ratio: float = field(
|
||||||
|
metadata={
|
||||||
|
"name": "AspectRatio",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
"min_exclusive": 0.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentColoringColor:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
components: ComponentColoringColorComponents = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Components",
|
||||||
|
"type": "Element",
|
||||||
|
"required": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
color: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Color",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentVisibility:
|
||||||
|
view_setup_hints: Optional[ViewSetupHints] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ViewSetupHints",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
exceptions: Optional[ComponentVisibilityExceptions] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Exceptions",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
default_visibility: bool = field(
|
||||||
|
default=False,
|
||||||
|
metadata={
|
||||||
|
"name": "DefaultVisibility",
|
||||||
|
"type": "Attribute",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class VisualizationInfoBitmaps:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
bitmap: List[Bitmap] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Bitmap",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class VisualizationInfoClippingPlanes:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
clipping_plane: List[ClippingPlane] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "ClippingPlane",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class VisualizationInfoLines:
|
||||||
|
class Meta:
|
||||||
|
global_type = False
|
||||||
|
|
||||||
|
line: List[Line] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Line",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class ComponentColoring:
|
||||||
|
color: List[ComponentColoringColor] = field(
|
||||||
|
default_factory=list,
|
||||||
|
metadata={
|
||||||
|
"name": "Color",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class Components:
|
||||||
|
selection: Optional[ComponentSelection] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Selection",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
visibility: Optional[ComponentVisibility] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Visibility",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
coloring: Optional[ComponentColoring] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Coloring",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class VisualizationInfo:
|
||||||
|
"""
|
||||||
|
VisualizationInfo documentation.
|
||||||
|
"""
|
||||||
|
components: Optional[Components] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Components",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
orthogonal_camera: Optional[OrthogonalCamera] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "OrthogonalCamera",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
perspective_camera: Optional[PerspectiveCamera] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "PerspectiveCamera",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lines: Optional[VisualizationInfoLines] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Lines",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
clipping_planes: Optional[VisualizationInfoClippingPlanes] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "ClippingPlanes",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
bitmaps: Optional[VisualizationInfoBitmaps] = field(
|
||||||
|
default=None,
|
||||||
|
metadata={
|
||||||
|
"name": "Bitmaps",
|
||||||
|
"type": "Element",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
guid: str = field(
|
||||||
|
metadata={
|
||||||
|
"name": "Guid",
|
||||||
|
"type": "Attribute",
|
||||||
|
"required": True,
|
||||||
|
"pattern": r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
"""BCF XML V3 Topic handler."""
|
||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from ifcopenshell import entity_instance
|
||||||
|
from xsdata.models.datatype import XmlDateTime
|
||||||
|
|
||||||
|
import bcf.v3.model as mdl
|
||||||
|
from bcf.inmemory_zipfile import ZipFileInterface
|
||||||
|
from bcf.v3.visinfo import VisualizationInfoHandler
|
||||||
|
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class TopicHandler:
|
||||||
|
"""BCF Topic and related objects handler."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
topic_dir: Optional[zipfile.Path] = None,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> None:
|
||||||
|
self._markup: Optional[mdl.Markup] = None
|
||||||
|
self._viewpoints: dict[str, VisualizationInfoHandler] = {}
|
||||||
|
self._bim_snippet: Optional[bytes] = None
|
||||||
|
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
self._topic_dir = topic_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def markup(self) -> Optional[mdl.Markup]:
|
||||||
|
if not self._markup and self._topic_dir:
|
||||||
|
markup_path = self._topic_dir.joinpath("markup.bcf")
|
||||||
|
if markup_path.exists():
|
||||||
|
self._markup = self._xml_handler.parse(markup_path.read_bytes(), mdl.Markup)
|
||||||
|
return self._markup
|
||||||
|
|
||||||
|
@markup.setter
|
||||||
|
def markup(self, value: mdl.Markup) -> None:
|
||||||
|
self._markup = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def topic(self) -> mdl.Topic:
|
||||||
|
"""Return the Topic object."""
|
||||||
|
return self.markup.topic
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guid(self) -> Optional[str]:
|
||||||
|
"""Return the GUID of the topic."""
|
||||||
|
if self._markup:
|
||||||
|
return self.topic.guid
|
||||||
|
return self._topic_dir.name if self._topic_dir else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def header(self) -> Optional[mdl.Header]:
|
||||||
|
"""Return the header of the topic."""
|
||||||
|
return self.markup.header
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comments(self) -> list[mdl.Comment]:
|
||||||
|
"""Return the comments of the topic."""
|
||||||
|
return self.topic.comments.comment if self.topic.comments else []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bim_snippet(self) -> Optional[bytes]:
|
||||||
|
if not self._bim_snippet and self._topic_dir:
|
||||||
|
self._bim_snippet = self._load_bim_snippet()
|
||||||
|
return self._bim_snippet
|
||||||
|
|
||||||
|
@bim_snippet.setter
|
||||||
|
def bim_snippet(self, value: bytes) -> None:
|
||||||
|
self._bim_snippet = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def viewpoints(self) -> Optional[VisualizationInfoHandler]:
|
||||||
|
if (
|
||||||
|
not self._viewpoints
|
||||||
|
and self._topic_dir
|
||||||
|
and self.topic.viewpoints
|
||||||
|
and (viewpoints := self.topic.viewpoints.view_point)
|
||||||
|
):
|
||||||
|
self._viewpoints = VisualizationInfoHandler.from_topic_viewpoints(self._topic_dir, viewpoints)
|
||||||
|
return self._viewpoints
|
||||||
|
|
||||||
|
def _load_bim_snippet(self) -> Optional[bytes]:
|
||||||
|
bim_snippet_obj = self.topic.bim_snippet
|
||||||
|
if bim_snippet_obj and not bim_snippet_obj.is_external and self._topic_dir:
|
||||||
|
bim_snippet_path = self._topic_dir.joinpath(bim_snippet_obj.reference)
|
||||||
|
if bim_snippet_path.exists():
|
||||||
|
return bim_snippet_path.read_bytes()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_new(
|
||||||
|
cls,
|
||||||
|
title: str,
|
||||||
|
description: str,
|
||||||
|
author: str,
|
||||||
|
topic_type: str = "",
|
||||||
|
topic_status: str = "",
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> "TopicHandler":
|
||||||
|
"""
|
||||||
|
Create a new BCF topic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: The title of the topic.
|
||||||
|
description: The description of the topic.
|
||||||
|
author: The author of the topic.
|
||||||
|
topic_type: The type of the topic.
|
||||||
|
topic_status: The status of the topic.
|
||||||
|
xml_handler: The XML parser/serializer to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF topic definition.
|
||||||
|
"""
|
||||||
|
creation_date = XmlDateTime.from_datetime(datetime.datetime.now())
|
||||||
|
guid = str(uuid.uuid4())
|
||||||
|
topic = mdl.Topic(
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
creation_author=author,
|
||||||
|
creation_date=creation_date,
|
||||||
|
guid=guid,
|
||||||
|
topic_type=topic_type,
|
||||||
|
topic_status=topic_status,
|
||||||
|
)
|
||||||
|
markup = mdl.Markup(topic=topic)
|
||||||
|
obj = cls(topic_dir=Path(guid), xml_handler=xml_handler or XmlParserSerializer())
|
||||||
|
obj.markup = markup
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def save(self, destination_zip: ZipFileInterface) -> None:
|
||||||
|
"""
|
||||||
|
Save the topic to a BCF zip file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bcf_zip: The BCF zip file to save to.
|
||||||
|
"""
|
||||||
|
topic_dir = self.guid
|
||||||
|
self._save_xml(destination_zip, self._markup, "markup.bcf")
|
||||||
|
self._save_viewpoints(destination_zip, topic_dir)
|
||||||
|
self._save_bim_snippet(destination_zip)
|
||||||
|
|
||||||
|
def _save_viewpoints(self, destination_zip, topic_dir) -> None:
|
||||||
|
if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point):
|
||||||
|
return
|
||||||
|
for vpt in viewpoints:
|
||||||
|
if vpt.viewpoint:
|
||||||
|
self.viewpoints[vpt.viewpoint].save(destination_zip, topic_dir, vpt)
|
||||||
|
|
||||||
|
def _save_xml(self, destination_zip: ZipFileInterface, item: Any, target: str) -> None:
|
||||||
|
to_write = self._xml_handler.serialize(item) if item else self._topic_dir.joinpath(target).read_bytes()
|
||||||
|
destination_zip.writestr(f"{self._topic_dir.name}/{target}", to_write)
|
||||||
|
|
||||||
|
def _save_bim_snippet(self, destination_zip: ZipFileInterface) -> None:
|
||||||
|
snippet = self.topic.bim_snippet
|
||||||
|
if not snippet or snippet.is_external:
|
||||||
|
return
|
||||||
|
ref_filename = Path(snippet.reference).name
|
||||||
|
if self.bim_snippet:
|
||||||
|
destination_zip.writestr(f"{self.topic.guid}/{ref_filename}", self.bim_snippet)
|
||||||
|
|
||||||
|
def add_viewpoint(self, element: entity_instance) -> None:
|
||||||
|
"""
|
||||||
|
Add a viewpoint tergeting an IFC element to the topic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: The IFC element.
|
||||||
|
"""
|
||||||
|
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
|
||||||
|
self.add_visinfo_handler(new_viewpoint)
|
||||||
|
|
||||||
|
def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
|
||||||
|
self.viewpoints[new_viewpoint.guid] = new_viewpoint
|
||||||
|
if self.topic.viewpoints is None:
|
||||||
|
self.topic.viewpoints = mdl.TopicViewpoints()
|
||||||
|
self.topic.viewpoints.view_point.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, TopicHandler):
|
||||||
|
raise TypeError("Equality needs a BcfXml object.")
|
||||||
|
return (
|
||||||
|
self.markup == other.markup
|
||||||
|
and self.viewpoints == other.viewpoints
|
||||||
|
and self.bim_snippet == other.bim_snippet
|
||||||
|
)
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import uuid
|
||||||
|
import zipfile
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Any, Iterable, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from ifcopenshell import entity_instance
|
||||||
|
from ifcopenshell.util import placement
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
import bcf.v3.model as mdl
|
||||||
|
from bcf.geometry import calc_camera_vectors
|
||||||
|
from bcf.inmemory_zipfile import ZipFileInterface
|
||||||
|
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class VisualizationInfoHandler:
|
||||||
|
"""Handle the VisualizationInfo and related objects."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
visualization_info: mdl.VisualizationInfo,
|
||||||
|
snapshot: Optional[bytes] = None,
|
||||||
|
bitmaps: Optional[dict[str, bytes]] = None,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> None:
|
||||||
|
self.visualization_info = visualization_info
|
||||||
|
self.snapshot = snapshot
|
||||||
|
self.bitmaps = bitmaps or {}
|
||||||
|
self._xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guid(self) -> str:
|
||||||
|
"""Return the GUID of the visualization info."""
|
||||||
|
return self.visualization_info.guid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_topic_viewpoints(
|
||||||
|
cls,
|
||||||
|
topic_dir: zipfile.Path,
|
||||||
|
vps: Iterable[mdl.ViewPoint],
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> dict[str, "VisualizationInfoHandler"]:
|
||||||
|
"""Create VisualizationInfoHandler objects of a Topic's ViewPoints."""
|
||||||
|
viewpoints = {}
|
||||||
|
for vpt in vps:
|
||||||
|
visinfo = cls.load(topic_dir, vpt, xml_handler)
|
||||||
|
if visinfo and vpt.viewpoint:
|
||||||
|
viewpoints[vpt.viewpoint] = visinfo
|
||||||
|
return viewpoints
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(
|
||||||
|
cls,
|
||||||
|
topic_dir: zipfile.Path,
|
||||||
|
vpt: mdl.ViewPoint,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> Optional["VisualizationInfoHandler"]:
|
||||||
|
"""
|
||||||
|
Load the VisualizationInfo and related objects from a BCF zip file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic_dir: The directory in the BCF zip file to load from.
|
||||||
|
vpt: The ViewPoint to load.
|
||||||
|
xml_handler: The XML handler to use to parse the VisualizationInfo.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The VisualizationInfoHandler object.
|
||||||
|
"""
|
||||||
|
visinfo = cls._load_visinfo(topic_dir, vpt.viewpoint, xml_handler)
|
||||||
|
if not visinfo:
|
||||||
|
return None
|
||||||
|
snapshot = cls._load_snapshot(topic_dir, vpt.snapshot)
|
||||||
|
bitmaps = cls._load_bitmaps(topic_dir, visinfo)
|
||||||
|
return cls(visinfo, snapshot, bitmaps, xml_handler)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_visinfo(
|
||||||
|
topic_dir: zipfile.Path,
|
||||||
|
vp_name: Optional[str],
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> Optional[mdl.VisualizationInfo]:
|
||||||
|
if not vp_name:
|
||||||
|
return None
|
||||||
|
vp_path = topic_dir.joinpath(vp_name)
|
||||||
|
if vp_path.exists():
|
||||||
|
xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
return xml_handler.parse(vp_path.read_bytes(), mdl.VisualizationInfo)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_snapshot(topic_dir: zipfile.Path, vp_snapshot: Optional[str]) -> Optional[bytes]:
|
||||||
|
if vp_snapshot:
|
||||||
|
snapshot_path = topic_dir.joinpath(vp_snapshot)
|
||||||
|
if snapshot_path.exists():
|
||||||
|
return snapshot_path.read_bytes()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_bitmaps(topic_dir: zipfile.Path, visinfo: Optional[mdl.VisualizationInfo]) -> dict[str, bytes]:
|
||||||
|
if not visinfo or not (bitmaps := visinfo.bitmaps):
|
||||||
|
return {}
|
||||||
|
bitmaps_dict = {}
|
||||||
|
for bitmap in bitmaps.bitmap:
|
||||||
|
if not bitmap.reference:
|
||||||
|
continue
|
||||||
|
bitmap_path = topic_dir.joinpath(bitmap.reference)
|
||||||
|
if bitmap_path.exists():
|
||||||
|
bitmaps_dict[bitmap.reference] = bitmap_path.read_bytes()
|
||||||
|
return bitmaps_dict
|
||||||
|
|
||||||
|
def save(
|
||||||
|
self,
|
||||||
|
bcf_zip: ZipFileInterface,
|
||||||
|
topic_dir: str,
|
||||||
|
vpt: mdl.ViewPoint,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Save the VisualizationInfo and related objects to a BCF zip file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bcf_zip: The BCF zip file to save to.
|
||||||
|
topic_dir: The directory in the BCF zip file to save to.
|
||||||
|
vpt: The ViewPoint to save.
|
||||||
|
"""
|
||||||
|
if not (vp_name := vpt.viewpoint):
|
||||||
|
return
|
||||||
|
self._save_visinfo(bcf_zip, topic_dir, vp_name)
|
||||||
|
self._save_snapshot(bcf_zip, topic_dir, vpt.snapshot)
|
||||||
|
self._save_bitmaps(bcf_zip, topic_dir)
|
||||||
|
|
||||||
|
def _save_snapshot(self, bcf_zip: ZipFileInterface, topic_dir: str, filename: Optional[str]) -> None:
|
||||||
|
if self.snapshot and filename:
|
||||||
|
bcf_zip.writestr(f"{topic_dir}/{filename}", self.snapshot)
|
||||||
|
|
||||||
|
def _save_visinfo(self, bcf_zip: ZipFileInterface, topic_dir: str, vp_name: str) -> None:
|
||||||
|
bcf_zip.writestr(
|
||||||
|
f"{topic_dir}/{vp_name}",
|
||||||
|
self._xml_handler.serialize(self.visualization_info),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _save_bitmaps(self, bcf_zip: ZipFileInterface, topic_dir: str) -> None:
|
||||||
|
if not self.bitmaps:
|
||||||
|
return
|
||||||
|
if not (bitmaps_defs := self.visualization_info.bitmaps):
|
||||||
|
return
|
||||||
|
for bitmap_def in bitmaps_defs.bitmap:
|
||||||
|
if not (bitmap_name := bitmap_def.reference):
|
||||||
|
continue
|
||||||
|
if bitmap_name in self.bitmaps:
|
||||||
|
bcf_zip.writestr(f"{topic_dir}/{bitmap_name}", self.bitmaps[bitmap_name])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_new(
|
||||||
|
cls,
|
||||||
|
element: entity_instance,
|
||||||
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
||||||
|
) -> "VisualizationInfoHandler":
|
||||||
|
"""
|
||||||
|
Create a new VisualizationInfoHandler object from an IFC element.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: The IFC element to point at.
|
||||||
|
xml_handler: The XML handler to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The VisualizationInfoHandler object.
|
||||||
|
"""
|
||||||
|
xml_handler = xml_handler or XmlParserSerializer()
|
||||||
|
return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
|
||||||
|
"""
|
||||||
|
Return a BCF viewpoint of an IFC element.
|
||||||
|
|
||||||
|
This function is cached to speedudp the creation of multiple BCF topics regarding the same element.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: The IFC element to point at.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF viewpoint definition.
|
||||||
|
"""
|
||||||
|
elem_placement = placement.get_local_placement(element.ObjectPlacement)
|
||||||
|
|
||||||
|
return mdl.VisualizationInfo(
|
||||||
|
guid=str(uuid.uuid4()),
|
||||||
|
components=build_components(element.GlobalId),
|
||||||
|
perspective_camera=build_camera(elem_placement),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_components(guid: str) -> mdl.Components:
|
||||||
|
"""
|
||||||
|
Return the BCF components from an IFC element GUID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guid: The IFC element GUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF components definition.
|
||||||
|
"""
|
||||||
|
return mdl.Components(
|
||||||
|
selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]),
|
||||||
|
visibility=mdl.ComponentVisibility(default_visibility=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera:
|
||||||
|
"""
|
||||||
|
Return a BCF camera for an IFC element placement matrix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
elem_placement: The IFC element placement as a rototranslation matrix.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF camera definition.
|
||||||
|
"""
|
||||||
|
return build_camera_from_vectors(*calc_camera_vectors(elem_placement))
|
||||||
|
|
||||||
|
|
||||||
|
def build_camera_from_vectors(
|
||||||
|
camera_position: NDArray[np.float_], camera_dir: NDArray[np.float_], camera_up: NDArray[np.float_]
|
||||||
|
) -> mdl.PerspectiveCamera:
|
||||||
|
"""
|
||||||
|
Return a BCF camera for an IFC element placement matrix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_position: camera position array
|
||||||
|
camera_dir: camera direction versor
|
||||||
|
camera_up_vector: camera up versor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The BCF camera definition.
|
||||||
|
"""
|
||||||
|
camera_viewpoint = mdl.Point(x=camera_position[0], y=camera_position[1], z=camera_position[2])
|
||||||
|
camera_direction = mdl.Direction(x=camera_dir[0], y=camera_dir[1], z=camera_dir[2])
|
||||||
|
camera_up_vector = mdl.Direction(x=camera_up[0], y=camera_up[1], z=camera_up[2])
|
||||||
|
return mdl.PerspectiveCamera(
|
||||||
|
camera_view_point=camera_viewpoint,
|
||||||
|
camera_direction=camera_direction,
|
||||||
|
camera_up_vector=camera_up_vector,
|
||||||
|
aspect_ratio=1.0,
|
||||||
|
field_of_view=60.0,
|
||||||
|
)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""XML Parser and Serializer factories."""
|
||||||
|
from typing import Optional, Protocol, Type, TypeVar
|
||||||
|
|
||||||
|
from xsdata.formats.dataclass.context import XmlContext
|
||||||
|
from xsdata.formats.dataclass.parsers import XmlParser
|
||||||
|
from xsdata.formats.dataclass.serializers import XmlSerializer
|
||||||
|
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||||
|
|
||||||
|
|
||||||
|
def build_xml_parser(context: Optional[XmlContext] = None) -> XmlParser:
|
||||||
|
"""Return a parser for an XML file."""
|
||||||
|
parser = XmlParser(context=context or XmlContext())
|
||||||
|
parser.register_namespace("xs", "http://www.w3.org/2001/XMLSchema")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def build_serializer(context: Optional[XmlContext] = None) -> XmlSerializer:
|
||||||
|
"""Return a serializer for an XML file."""
|
||||||
|
return XmlSerializer(
|
||||||
|
config=SerializerConfig(pretty_print=True),
|
||||||
|
context=context or XmlContext(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractXmlParserSerializer(Protocol):
|
||||||
|
"""XML Parser and serializer wrapper."""
|
||||||
|
|
||||||
|
def parse(self, xml: bytes, clazz: Type[T]) -> T:
|
||||||
|
"""
|
||||||
|
Parse an XML file to an object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xml: The XML file as bytes.
|
||||||
|
clazz: The class to parse to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
|
||||||
|
"""
|
||||||
|
Serialize an object to XML.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj: The object to serialize.
|
||||||
|
ns_map: The namespace map to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The XML as string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class XmlParserSerializer:
|
||||||
|
"""XML Parser and serializer wrapper."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.context = XmlContext()
|
||||||
|
self.parser = build_xml_parser(self.context)
|
||||||
|
self.serializer = build_serializer(self.context)
|
||||||
|
|
||||||
|
def parse(self, xml: bytes, clazz: Type[T]) -> T:
|
||||||
|
"""
|
||||||
|
Parse an XML file to an object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xml: The XML file as bytes.
|
||||||
|
clazz: The class to parse to.
|
||||||
|
"""
|
||||||
|
return self.parser.from_bytes(xml, clazz)
|
||||||
|
|
||||||
|
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
|
||||||
|
"""
|
||||||
|
Serialize an object to XML.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj: The object to serialize.
|
||||||
|
ns_map: The namespace map to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The XML as string.
|
||||||
|
"""
|
||||||
|
ns_map = ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
|
||||||
|
return self.serializer.render(obj, ns_map)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""BCF tests."""
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from bcf.xml_parser import XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def xml_handler() -> XmlParserSerializer:
|
||||||
|
return XmlParserSerializer()
|
||||||
Binary file not shown.
@@ -0,0 +1,122 @@
|
|||||||
|
"""BCF XML tests."""
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import bcf.v2.model as mdl
|
||||||
|
from bcf.v2.bcfxml import BcfXml
|
||||||
|
from bcf.v2.topic import TopicHandler
|
||||||
|
from bcf.v2.visinfo import (
|
||||||
|
VisualizationInfoHandler,
|
||||||
|
build_camera_from_vectors,
|
||||||
|
build_components,
|
||||||
|
)
|
||||||
|
from bcf.xml_parser import XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def build_sample(xml_handler: XmlParserSerializer) -> tuple[BcfXml, TopicHandler]:
|
||||||
|
bcf = BcfXml.create_new("Test project", xml_handler=xml_handler)
|
||||||
|
orig_th = bcf.add_topic("Test topic", "Test message", "Test author", "Test type")
|
||||||
|
return bcf, orig_th
|
||||||
|
|
||||||
|
|
||||||
|
def test_bcf_roundtrip(xml_handler, build_sample) -> None:
|
||||||
|
"""Saving and loading a bcf xml project returns the same objects."""
|
||||||
|
bcf, orig_th = build_sample
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path)
|
||||||
|
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||||
|
assert parsed == bcf
|
||||||
|
parsed_th = parsed.topics[orig_th.guid]
|
||||||
|
assert parsed_th == orig_th
|
||||||
|
|
||||||
|
|
||||||
|
def test_bcf_edit_saveas(xml_handler, build_sample) -> None:
|
||||||
|
"""Saving and loading a bcf xml project returns the same objects."""
|
||||||
|
bcf, orig_th = build_sample
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path)
|
||||||
|
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||||
|
for th in parsed.topics.values():
|
||||||
|
th.topic.title = "New Topic Title"
|
||||||
|
modified_path = Path(tmp_dir) / "edited.bcf"
|
||||||
|
parsed.save(modified_path)
|
||||||
|
with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed:
|
||||||
|
assert modified_parsed == bcf
|
||||||
|
parsed_th = modified_parsed.topics[orig_th.guid]
|
||||||
|
assert parsed_th.markup != orig_th.markup
|
||||||
|
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
|
||||||
|
assert parsed_th.viewpoints == orig_th.viewpoints
|
||||||
|
assert parsed_th.bim_snippet == orig_th.bim_snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_bcf_edit(xml_handler, build_sample) -> None:
|
||||||
|
"""Saving and loading a bcf xml project returns the same objects."""
|
||||||
|
bcf, orig_th = build_sample
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path)
|
||||||
|
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||||
|
for th in parsed.topics.values():
|
||||||
|
th.topic.title = "New Topic Title"
|
||||||
|
parsed.save()
|
||||||
|
with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed:
|
||||||
|
assert modified_parsed == bcf
|
||||||
|
assert len(modified_parsed.topics) == 1
|
||||||
|
parsed_th = modified_parsed.topics[orig_th.guid]
|
||||||
|
assert parsed_th.markup != orig_th.markup
|
||||||
|
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
|
||||||
|
assert parsed_th.viewpoints == orig_th.viewpoints
|
||||||
|
assert parsed_th.bim_snippet == orig_th.bim_snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_no_filename(build_sample) -> None:
|
||||||
|
bcf, _ = build_sample
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
bcf.save()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_no_filename() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
BcfXml.load("")
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_keep_open(build_sample) -> None:
|
||||||
|
bcf, _ = build_sample
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path, keep_open=True)
|
||||||
|
assert bcf._zip_file is not None
|
||||||
|
bcf._zip_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_massive_bcf(xml_handler) -> None:
|
||||||
|
bcf = BcfXml.create_new("Test project", xml_handler=xml_handler)
|
||||||
|
for i in range(100):
|
||||||
|
th = bcf.add_topic(f"Topic {i:04}", f"Message {i:04}", "Test author", "Test type")
|
||||||
|
vi = mdl.VisualizationInfo(
|
||||||
|
guid=str(uuid.uuid4()),
|
||||||
|
components=build_components(str(uuid.uuid4())),
|
||||||
|
perspective_camera=build_camera_from_vectors([i, 0, 0], [0, 1, 0], [0, 0, 1]),
|
||||||
|
)
|
||||||
|
vh = VisualizationInfoHandler(visualization_info=vi, xml_handler=xml_handler)
|
||||||
|
th.add_visinfo_handler(vh)
|
||||||
|
assert len(bcf.topics) == 100
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_equality_with_wrong_object() -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
build_sample[0] == "Wrong object"
|
||||||
|
|
||||||
|
|
||||||
|
def test_topic_equality_with_wrong_object() -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
build_sample[1] == "Wrong object"
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from xsdata.models.datatype import XmlDateTime
|
||||||
|
|
||||||
|
import bcf.v2.model as mdl
|
||||||
|
from bcf.v2.bcfxml import BcfXml
|
||||||
|
from bcf.v2.topic import TopicHandler
|
||||||
|
|
||||||
|
|
||||||
|
def test_maximum_information() -> None:
|
||||||
|
"""
|
||||||
|
All the info in a BCF is parsed correctly.
|
||||||
|
|
||||||
|
Uses sample file from https://github.com/buildingSMART/BCF-XML/
|
||||||
|
"""
|
||||||
|
bcf_path = Path(__file__).parent / "MaximumInformation.bcf"
|
||||||
|
with BcfXml.load(bcf_path) as bcf:
|
||||||
|
assert_everything_in_place(bcf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_maximum_information() -> None:
|
||||||
|
base_dir = Path(__file__).parent
|
||||||
|
bcf_path = base_dir / "MaximumInformation.bcf"
|
||||||
|
target_path = base_dir / "MaximumInformationSaved.bcf"
|
||||||
|
with BcfXml.load(bcf_path) as bcf:
|
||||||
|
bcf.save(target_path)
|
||||||
|
assert target_path.exists()
|
||||||
|
with BcfXml.load(target_path) as bcf2:
|
||||||
|
assert_everything_in_place(bcf2)
|
||||||
|
|
||||||
|
expected_files = [
|
||||||
|
"bcf.version",
|
||||||
|
"project.bcfp",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/markup.bcf",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/bitmap.png",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/tux.png",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/JsonElement.json",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_4ab7514b-b216-4d56-98d2-45cf8500ff5a.png",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_9a4a1878-ecbd-4916-83a8-dad82e560231.png",
|
||||||
|
"7ddc3ef0-0ab7-43f1-918a-45e38b42369c/Snapshot_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.png",
|
||||||
|
"d1068c81-af04-4546-b63c-348810f6c716/markup.bcf",
|
||||||
|
"extensions.xsd",
|
||||||
|
"IfcPile_01.ifc",
|
||||||
|
"markup.xsd",
|
||||||
|
]
|
||||||
|
assert_files_present(target_path, expected_files)
|
||||||
|
os.unlink(target_path)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_everything_in_place(bcf: BcfXml):
|
||||||
|
assert bcf.version.version_id == "2.1"
|
||||||
|
assert bcf.project.name == "BCF API Implementation"
|
||||||
|
assert bcf.project_info.extension_schema == "extensions.xsd"
|
||||||
|
|
||||||
|
assert len(bcf.topics) == 2
|
||||||
|
assert_first_topic_handler(bcf.topics["7ddc3ef0-0ab7-43f1-918a-45e38b42369c"])
|
||||||
|
second_th = bcf.topics["d1068c81-af04-4546-b63c-348810f6c716"]
|
||||||
|
assert second_th.topic == mdl.Topic(
|
||||||
|
title="Referenced topic",
|
||||||
|
creation_date=XmlDateTime(2017, 5, 22, 7, 51, 0, 42987900),
|
||||||
|
creation_author="dangl@iabi.eu",
|
||||||
|
description="This is just an empty topic that acts as a referenced topic.",
|
||||||
|
guid="d1068c81-af04-4546-b63c-348810f6c716",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_first_topic_handler(topic_handler: TopicHandler):
|
||||||
|
assert topic_handler.guid == "7ddc3ef0-0ab7-43f1-918a-45e38b42369c"
|
||||||
|
|
||||||
|
expected_bs1 = mdl.BimSnippet(
|
||||||
|
reference="JsonElement.json", reference_schema="http://json-schema.org", snippet_type="JSON"
|
||||||
|
)
|
||||||
|
expected_t1 = mdl.Topic(
|
||||||
|
reference_link=["https://bim--it.net"],
|
||||||
|
title="Maximum Content",
|
||||||
|
priority="High",
|
||||||
|
index=0,
|
||||||
|
labels=["Structural", "IT Development"],
|
||||||
|
creation_date=XmlDateTime(2015, 6, 21, 12, 0, 0),
|
||||||
|
creation_author="dangl@iabi.eu",
|
||||||
|
modified_date=XmlDateTime(2015, 6, 21, 14, 22, 47),
|
||||||
|
modified_author="dangl@iabi.eu",
|
||||||
|
due_date=XmlDateTime(2016, 10, 2, 14, 22, 47),
|
||||||
|
assigned_to="linhard@iabi.eu",
|
||||||
|
stage="Construction Start",
|
||||||
|
description="This is a topic with all informations present.",
|
||||||
|
bim_snippet=expected_bs1,
|
||||||
|
document_reference=[
|
||||||
|
mdl.TopicDocumentReference(
|
||||||
|
referenced_document="https://github.com/BuildingSMART/BCF-XML",
|
||||||
|
description="GitHub BCF Specification",
|
||||||
|
is_external=True,
|
||||||
|
),
|
||||||
|
mdl.TopicDocumentReference(
|
||||||
|
referenced_document="../markup.xsd",
|
||||||
|
description="Markup.xsd Schema",
|
||||||
|
is_external=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
related_topic=[mdl.TopicRelatedTopic(guid="d1068c81-af04-4546-b63c-348810f6c716")],
|
||||||
|
guid="7ddc3ef0-0ab7-43f1-918a-45e38b42369c",
|
||||||
|
topic_type="Structural",
|
||||||
|
topic_status="Open",
|
||||||
|
)
|
||||||
|
assert topic_handler.topic == expected_t1
|
||||||
|
expected_h1 = mdl.Header(
|
||||||
|
file=[
|
||||||
|
mdl.HeaderFile(
|
||||||
|
filename="IfcPile_01.ifc",
|
||||||
|
date=XmlDateTime(2014, 10, 27, 16, 27, 27),
|
||||||
|
reference="../IfcPile_01.ifc",
|
||||||
|
ifc_project="0M6o7Znnv7hxsbWgeu7oQq",
|
||||||
|
ifc_spatial_structure_element="23B$bNeGHFQuMYJzvUX0FD",
|
||||||
|
is_external=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert topic_handler.header == expected_h1
|
||||||
|
|
||||||
|
expected_th1_comments = [
|
||||||
|
mdl.Comment(
|
||||||
|
date=XmlDateTime(2015, 8, 31, 12, 40, 17),
|
||||||
|
author="dangl@iabi.eu",
|
||||||
|
comment="This is an unmodified topic at the uppermost hierarchical level.\nAll times in the XML are marked as UTC times.",
|
||||||
|
guid="07ccdba0-1736-47e1-807d-67dc6f3addaa",
|
||||||
|
),
|
||||||
|
mdl.Comment(
|
||||||
|
date=XmlDateTime(2015, 8, 31, 14, 0, 1),
|
||||||
|
author="dangl@iabi.eu",
|
||||||
|
comment="This comment was a reply to the first comment in BCF v2.0. This is a no longer supported functionality and therefore is to be treated as a regular comment in v2.1.",
|
||||||
|
guid="a12766c2-61bc-40b4-ab19-e9f45fd0b0bf",
|
||||||
|
),
|
||||||
|
mdl.Comment(
|
||||||
|
date=XmlDateTime(2015, 8, 31, 13, 7, 11),
|
||||||
|
author="dangl@iabi.eu",
|
||||||
|
comment="This comment again is in the highest hierarchy level.\nIt references a viewpoint.",
|
||||||
|
viewpoint=mdl.CommentViewpoint(guid="4ab7514b-b216-4d56-98d2-45cf8500ff5a"),
|
||||||
|
guid="c2bb5bb0-773d-45dd-bdaa-19a216439ed3",
|
||||||
|
),
|
||||||
|
mdl.Comment(
|
||||||
|
date=XmlDateTime(2015, 8, 31, 15, 42, 58),
|
||||||
|
author="dangl@iabi.eu",
|
||||||
|
comment="This comment contained some spllng errs.\nHopefully, the modifier did catch them all.",
|
||||||
|
modified_date=XmlDateTime(2015, 8, 31, 16, 7, 11),
|
||||||
|
modified_author="dangl@iabi.eu",
|
||||||
|
guid="0b843a5c-c3bf-41ef-be98-52a9f7bd9790",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
assert topic_handler.comments == expected_th1_comments
|
||||||
|
|
||||||
|
expected_th1_viewpoints = [
|
||||||
|
mdl.ViewPoint(
|
||||||
|
viewpoint="Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv",
|
||||||
|
snapshot="Snapshot_4ab7514b-b216-4d56-98d2-45cf8500ff5a.png",
|
||||||
|
index=2,
|
||||||
|
guid="4ab7514b-b216-4d56-98d2-45cf8500ff5a",
|
||||||
|
),
|
||||||
|
mdl.ViewPoint(
|
||||||
|
viewpoint="Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv",
|
||||||
|
snapshot="Snapshot_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.png",
|
||||||
|
index=0,
|
||||||
|
guid="fc4019d7-365e-47f3-b6d0-b39fc48f15fc",
|
||||||
|
),
|
||||||
|
mdl.ViewPoint(
|
||||||
|
viewpoint="Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv",
|
||||||
|
snapshot="Snapshot_9a4a1878-ecbd-4916-83a8-dad82e560231.png",
|
||||||
|
index=1,
|
||||||
|
guid="9a4a1878-ecbd-4916-83a8-dad82e560231",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_m1 = mdl.Markup(
|
||||||
|
header=expected_h1,
|
||||||
|
topic=expected_t1,
|
||||||
|
comment=expected_th1_comments,
|
||||||
|
viewpoints=expected_th1_viewpoints,
|
||||||
|
)
|
||||||
|
assert topic_handler.markup == expected_m1
|
||||||
|
|
||||||
|
assert json.loads(topic_handler.bim_snippet) == {"Material": "Concrete", "Temperatures": ["Cold", "Hot", "Hotter"]}
|
||||||
|
|
||||||
|
assert topic_handler.reference_files["../IfcPile_01.ifc"] is not None
|
||||||
|
|
||||||
|
assert_viewpoints(topic_handler.viewpoints)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_viewpoints(viewpoints):
|
||||||
|
assert len(viewpoints) == 3
|
||||||
|
|
||||||
|
expected_selection = mdl.ComponentSelection(
|
||||||
|
component=[
|
||||||
|
mdl.Component(ifc_guid="0cSRUx$EX1NRjqiKcYQ$a0"),
|
||||||
|
mdl.Component(ifc_guid="1jQQiGIAnFzxOUzrdmJYDS"),
|
||||||
|
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
|
||||||
|
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
|
||||||
|
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_exception = mdl.ComponentVisibilityExceptions(
|
||||||
|
component=[
|
||||||
|
mdl.Component(ifc_guid="0Gl71cVurFn8bxAOox6M4X"),
|
||||||
|
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
|
||||||
|
mdl.Component(ifc_guid="3DvyPxGIn8qR0KDwbL_9r1"),
|
||||||
|
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
|
||||||
|
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_coloring = mdl.ComponentColoring(
|
||||||
|
color=[
|
||||||
|
mdl.ComponentColoringColor(
|
||||||
|
component=[
|
||||||
|
mdl.Component(ifc_guid="0fdpeZZEX3FwJ7x0ox5kzF"),
|
||||||
|
mdl.Component(ifc_guid="23Zwlpd71EyvHlH6OZ77nK"),
|
||||||
|
mdl.Component(ifc_guid="1OpjQ1Nlv4sQuTxfUC_8zS"),
|
||||||
|
mdl.Component(ifc_guid="0cSRUx$EX1NRjqiKcYQ$a0"),
|
||||||
|
],
|
||||||
|
color="3498DB",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_first_viewpoint(
|
||||||
|
viewpoints["Viewpoint_4ab7514b-b216-4d56-98d2-45cf8500ff5a.bcfv"],
|
||||||
|
expected_selection,
|
||||||
|
expected_exception,
|
||||||
|
expected_coloring,
|
||||||
|
)
|
||||||
|
assert_second_viewpoint(
|
||||||
|
viewpoints["Viewpoint_fc4019d7-365e-47f3-b6d0-b39fc48f15fc.bcfv"],
|
||||||
|
expected_selection,
|
||||||
|
expected_exception,
|
||||||
|
expected_coloring,
|
||||||
|
)
|
||||||
|
assert_third_viewpoint(
|
||||||
|
viewpoints["Viewpoint_9a4a1878-ecbd-4916-83a8-dad82e560231.bcfv"],
|
||||||
|
expected_selection,
|
||||||
|
expected_exception,
|
||||||
|
expected_coloring,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_first_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
|
expected_vp = mdl.VisualizationInfo(
|
||||||
|
components=mdl.Components(
|
||||||
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
|
spaces_visible=True,
|
||||||
|
space_boundaries_visible=True,
|
||||||
|
openings_visible=True,
|
||||||
|
),
|
||||||
|
selection=expected_selection,
|
||||||
|
visibility=mdl.ComponentVisibility(
|
||||||
|
exceptions=expected_exception,
|
||||||
|
default_visibility=True,
|
||||||
|
),
|
||||||
|
coloring=expected_coloring,
|
||||||
|
),
|
||||||
|
perspective_camera=mdl.PerspectiveCamera(
|
||||||
|
camera_view_point=mdl.Point(x=0.43079984188079834, y=69.52057647705078, z=10.666350364685059),
|
||||||
|
camera_direction=mdl.Direction(x=0.09159398823976517, y=-0.9375035166740417, z=-0.3357048034667969),
|
||||||
|
camera_up_vector=mdl.Direction(x=0.01938679628074169, y=-0.3353792130947113, z=0.9418837428092957),
|
||||||
|
field_of_view=60,
|
||||||
|
),
|
||||||
|
lines=mdl.VisualizationInfoLines(
|
||||||
|
line=[
|
||||||
|
mdl.Line(start_point=mdl.Point(x=0, y=0, z=0), end_point=mdl.Point(x=0, y=0, z=1)),
|
||||||
|
mdl.Line(start_point=mdl.Point(x=0, y=0, z=1), end_point=mdl.Point(x=0, y=1, z=1)),
|
||||||
|
mdl.Line(start_point=mdl.Point(x=0, y=1, z=1), end_point=mdl.Point(x=1, y=1, z=1)),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
clipping_planes=mdl.VisualizationInfoClippingPlanes(
|
||||||
|
clipping_plane=[
|
||||||
|
mdl.ClippingPlane(location=mdl.Point(x=0, y=0, z=0), direction=mdl.Direction(x=0, y=0, z=1)),
|
||||||
|
mdl.ClippingPlane(location=mdl.Point(x=0, y=0, z=0), direction=mdl.Direction(x=0, y=1, z=0)),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
bitmap=[
|
||||||
|
mdl.VisualizationInfoBitmap(
|
||||||
|
bitmap=mdl.BitmapFormat.PNG,
|
||||||
|
reference="bitmap.png",
|
||||||
|
location=mdl.Point(x=10, y=-10, z=7),
|
||||||
|
normal=mdl.Direction(x=0, y=1, z=0),
|
||||||
|
up=mdl.Direction(x=0, y=0, z=1),
|
||||||
|
height=5.0,
|
||||||
|
),
|
||||||
|
mdl.VisualizationInfoBitmap(
|
||||||
|
bitmap=mdl.BitmapFormat.PNG,
|
||||||
|
reference="tux.png",
|
||||||
|
location=mdl.Point(x=20, y=-10, z=7),
|
||||||
|
normal=mdl.Direction(x=0, y=1, z=0),
|
||||||
|
up=mdl.Direction(x=0, y=0, z=1),
|
||||||
|
height=5.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
guid="8dc86298-9737-40b4-a448-98a9e953293a",
|
||||||
|
)
|
||||||
|
assert viewpoint.visualization_info == expected_vp
|
||||||
|
assert viewpoint.snapshot is not None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
|
expected_vp = mdl.VisualizationInfo(
|
||||||
|
components=mdl.Components(
|
||||||
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
|
spaces_visible=False,
|
||||||
|
space_boundaries_visible=False,
|
||||||
|
openings_visible=False,
|
||||||
|
),
|
||||||
|
selection=expected_selection,
|
||||||
|
visibility=mdl.ComponentVisibility(
|
||||||
|
exceptions=expected_exception,
|
||||||
|
default_visibility=False,
|
||||||
|
),
|
||||||
|
coloring=expected_coloring,
|
||||||
|
),
|
||||||
|
perspective_camera=mdl.PerspectiveCamera(
|
||||||
|
camera_view_point=mdl.Point(x=-47.18794250488281, y=43.829200744628906, z=10.666350364685059),
|
||||||
|
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
|
||||||
|
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
||||||
|
field_of_view=60,
|
||||||
|
),
|
||||||
|
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
||||||
|
)
|
||||||
|
assert viewpoint.visualization_info == expected_vp
|
||||||
|
assert viewpoint.snapshot is not None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
|
expected_vp = mdl.VisualizationInfo(
|
||||||
|
components=mdl.Components(
|
||||||
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
|
spaces_visible=False,
|
||||||
|
space_boundaries_visible=False,
|
||||||
|
openings_visible=True,
|
||||||
|
),
|
||||||
|
selection=expected_selection,
|
||||||
|
visibility=mdl.ComponentVisibility(
|
||||||
|
exceptions=expected_exception,
|
||||||
|
default_visibility=True,
|
||||||
|
),
|
||||||
|
coloring=expected_coloring,
|
||||||
|
),
|
||||||
|
perspective_camera=mdl.PerspectiveCamera(
|
||||||
|
camera_view_point=mdl.Point(x=-48.974571228027344, y=-64.20051574707031, z=10.666350364685059),
|
||||||
|
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
|
||||||
|
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
||||||
|
field_of_view=60,
|
||||||
|
),
|
||||||
|
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
||||||
|
)
|
||||||
|
assert viewpoint.visualization_info == expected_vp
|
||||||
|
assert viewpoint.snapshot is not None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_files_present(saved_bcf_path, expected_files):
|
||||||
|
with zipfile.ZipFile(saved_bcf_path) as bcf_zip:
|
||||||
|
for file_path in expected_files:
|
||||||
|
assert zipfile.Path(bcf_zip, file_path).exists()
|
||||||
Binary file not shown.
@@ -0,0 +1,129 @@
|
|||||||
|
"""BCF XML tests."""
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import bcf.v3.model as mdl
|
||||||
|
from bcf.v3.bcfxml import BcfXml
|
||||||
|
from bcf.v3.topic import TopicHandler
|
||||||
|
from bcf.v3.visinfo import (
|
||||||
|
VisualizationInfoHandler,
|
||||||
|
build_camera_from_vectors,
|
||||||
|
build_components,
|
||||||
|
)
|
||||||
|
from bcf.xml_parser import XmlParserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def build_sample(xml_handler: XmlParserSerializer) -> tuple[BcfXml, TopicHandler]:
|
||||||
|
ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"]))
|
||||||
|
bcf = BcfXml.create_new("Test project", extensions=ext, xml_handler=xml_handler)
|
||||||
|
orig_th = bcf.add_topic("Test topic", "Test message", "Test author", "Test type")
|
||||||
|
return bcf, orig_th
|
||||||
|
|
||||||
|
|
||||||
|
def test_bcf_roundtrip(xml_handler, build_sample) -> None:
|
||||||
|
"""Saving and loading a bcf xml project returns the same objects."""
|
||||||
|
bcf, orig_th = build_sample
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path)
|
||||||
|
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||||
|
assert parsed == bcf
|
||||||
|
parsed_th = parsed.topics[orig_th.guid]
|
||||||
|
assert parsed_th == orig_th
|
||||||
|
|
||||||
|
|
||||||
|
def test_bcf_edit_saveas(xml_handler, build_sample) -> None:
|
||||||
|
"""Saving and loading a bcf xml project returns the same objects."""
|
||||||
|
bcf, orig_th = build_sample
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path)
|
||||||
|
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||||
|
for th in parsed.topics.values():
|
||||||
|
th.topic.title = "New Topic Title"
|
||||||
|
modified_path = Path(tmp_dir) / "edited.bcf"
|
||||||
|
parsed.save(modified_path)
|
||||||
|
with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed:
|
||||||
|
assert modified_parsed == bcf
|
||||||
|
parsed_th = modified_parsed.topics[orig_th.guid]
|
||||||
|
assert parsed_th.markup != orig_th.markup
|
||||||
|
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
|
||||||
|
assert parsed_th.viewpoints == orig_th.viewpoints
|
||||||
|
assert parsed_th.bim_snippet == orig_th.bim_snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_bcf_edit(xml_handler, build_sample) -> None:
|
||||||
|
"""Saving and loading a bcf xml project returns the same objects."""
|
||||||
|
bcf, orig_th = build_sample
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path)
|
||||||
|
with BcfXml.load(file_path, xml_handler=xml_handler) as parsed:
|
||||||
|
for th in parsed.topics.values():
|
||||||
|
th.topic.title = "New Topic Title"
|
||||||
|
parsed.save()
|
||||||
|
with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed:
|
||||||
|
assert modified_parsed == bcf
|
||||||
|
assert len(modified_parsed.topics) == 1
|
||||||
|
parsed_th = modified_parsed.topics[orig_th.guid]
|
||||||
|
assert parsed_th.markup != orig_th.markup
|
||||||
|
assert parsed_th.markup == parsed.topics[orig_th.guid].markup
|
||||||
|
assert parsed_th.viewpoints == orig_th.viewpoints
|
||||||
|
assert parsed_th.bim_snippet == orig_th.bim_snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_no_filename(build_sample) -> None:
|
||||||
|
bcf, _ = build_sample
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
bcf.save()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_no_filename() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
BcfXml.load("")
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_keep_open(build_sample) -> None:
|
||||||
|
bcf, orig_th = build_sample
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path, keep_open=True)
|
||||||
|
assert bcf._zip_file is not None
|
||||||
|
bcf._zip_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
# image = PIL.Image.new('RGB', size=(100, 100))
|
||||||
|
# file = BinaryIO()
|
||||||
|
# image.save(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_massive_bcf(xml_handler) -> None:
|
||||||
|
ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"]))
|
||||||
|
bcf = BcfXml.create_new("Test project", extensions=ext, xml_handler=xml_handler)
|
||||||
|
for i in range(100):
|
||||||
|
th = bcf.add_topic(f"Topic {i:04}", f"Message {i:04}", "Test author", "Test type")
|
||||||
|
vi = mdl.VisualizationInfo(
|
||||||
|
guid=str(uuid.uuid4()),
|
||||||
|
components=build_components(str(uuid.uuid4())),
|
||||||
|
perspective_camera=build_camera_from_vectors([i, 0, 0], [0, 1, 0], [0, 0, 1]),
|
||||||
|
)
|
||||||
|
vh = VisualizationInfoHandler(visualization_info=vi, xml_handler=xml_handler)
|
||||||
|
th.add_visinfo_handler(vh)
|
||||||
|
assert len(bcf.topics) == 100
|
||||||
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
file_path = Path(tmp_dir) / "test.bcf"
|
||||||
|
bcf.save(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_equality_with_wrong_object() -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
build_sample[0] == "Wrong object"
|
||||||
|
|
||||||
|
|
||||||
|
def test_topic_equality_with_wrong_object() -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
build_sample[1] == "Wrong object"
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from xsdata.models.datatype import XmlDateTime
|
||||||
|
|
||||||
|
import bcf.v3.model as mdl
|
||||||
|
from bcf.v3.bcfxml import BcfXml
|
||||||
|
from bcf.v3.topic import TopicHandler
|
||||||
|
|
||||||
|
|
||||||
|
def test_doc_ref_internal() -> None:
|
||||||
|
"""
|
||||||
|
All the info in a BCF is parsed correctly.
|
||||||
|
|
||||||
|
Uses sample file from https://github.com/buildingSMART/BCF-XML/
|
||||||
|
"""
|
||||||
|
bcf_path = Path(__file__).parent / "Document reference internal.bcf"
|
||||||
|
with BcfXml.load(bcf_path) as bcf:
|
||||||
|
assert_everything_in_place(bcf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_doc_ref_internal_save() -> None:
|
||||||
|
base_dir = Path(__file__).parent
|
||||||
|
bcf_path = base_dir / "Document reference internal.bcf"
|
||||||
|
target_path = base_dir / "Document reference internal Saved.bcf"
|
||||||
|
with BcfXml.load(bcf_path) as bcf:
|
||||||
|
bcf.save(target_path)
|
||||||
|
assert target_path.exists()
|
||||||
|
with BcfXml.load(target_path) as bcf2:
|
||||||
|
assert_everything_in_place(bcf2)
|
||||||
|
assert_files_present(target_path)
|
||||||
|
os.unlink(target_path)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_everything_in_place(bcf: BcfXml):
|
||||||
|
assert bcf.version.version_id == "3.0"
|
||||||
|
assert bcf.project.name == "BCF 3.0 test cases"
|
||||||
|
assert bcf.project.project_id == "de894a86-3a08-4ea0-b2d1-6c222b5602d1"
|
||||||
|
|
||||||
|
assert len(bcf.topics) == 1
|
||||||
|
topic_handler = bcf.topics["8ac9822a-761a-4deb-9f39-f61286acbf6a"]
|
||||||
|
|
||||||
|
expected_h1 = mdl.Header(
|
||||||
|
files=mdl.HeaderFiles(
|
||||||
|
file=[
|
||||||
|
mdl.File(
|
||||||
|
filename="BCF-ARK",
|
||||||
|
date=XmlDateTime(2021, 1, 4, 9, 37, 45),
|
||||||
|
reference="https://bimsync.com/project/f5fbb3c695274d1890036bf64f77eb71/revisions/007afab57f264d2296aae0a452486ae1",
|
||||||
|
is_external=True,
|
||||||
|
),
|
||||||
|
mdl.File(
|
||||||
|
filename="BCF-MEP",
|
||||||
|
date=XmlDateTime(2017, 8, 7, 9, 51, 34),
|
||||||
|
reference="https://bimsync.com/project/f5fbb3c695274d1890036bf64f77eb71/revisions/a21ed391f9e046a2bb2bc879c48f1d48",
|
||||||
|
is_external=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert topic_handler.header == expected_h1
|
||||||
|
|
||||||
|
expected_comments = [
|
||||||
|
mdl.Comment(
|
||||||
|
date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
|
||||||
|
author="Architect@example.com",
|
||||||
|
comment="A comment",
|
||||||
|
viewpoint=mdl.CommentViewpoint(guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182"),
|
||||||
|
guid="c045ef04-324d-4c36-9ac8-831f2a15c6d6",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
assert topic_handler.comments == expected_comments
|
||||||
|
|
||||||
|
expected_viewpoints = [
|
||||||
|
mdl.ViewPoint(
|
||||||
|
viewpoint="Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv",
|
||||||
|
snapshot="Snapshot_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.png",
|
||||||
|
guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_topic = mdl.Topic(
|
||||||
|
reference_links=mdl.TopicReferenceLinks(),
|
||||||
|
title="Document Reference Internal",
|
||||||
|
labels=mdl.TopicLabels(),
|
||||||
|
creation_date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
|
||||||
|
creation_author="Architect@example.com",
|
||||||
|
modified_date=XmlDateTime(2021, 2, 17, 9, 16, 4, 160_000_000),
|
||||||
|
assigned_to="OtherUser@doe.com",
|
||||||
|
document_references=mdl.TopicDocumentReferences(
|
||||||
|
document_reference=[
|
||||||
|
mdl.DocumentReference(
|
||||||
|
document_guid="b1d1b7f0-60b9-457d-ad12-16e0fb997bc5",
|
||||||
|
description="ThisIsADocument.txt",
|
||||||
|
guid="048e898f-555f-47c3-a273-6c664fb2ef69",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
related_topics=mdl.TopicRelatedTopics(),
|
||||||
|
comments=mdl.TopicComments(comment=expected_comments),
|
||||||
|
viewpoints=mdl.TopicViewpoints(view_point=expected_viewpoints),
|
||||||
|
guid="8ac9822a-761a-4deb-9f39-f61286acbf6a",
|
||||||
|
server_assigned_id="6",
|
||||||
|
topic_type="ERROR",
|
||||||
|
topic_status="OPEN",
|
||||||
|
)
|
||||||
|
assert topic_handler.topic == expected_topic
|
||||||
|
|
||||||
|
expected_m1 = mdl.Markup(
|
||||||
|
header=expected_h1,
|
||||||
|
topic=expected_topic,
|
||||||
|
)
|
||||||
|
assert topic_handler.markup == expected_m1
|
||||||
|
|
||||||
|
assert topic_handler.bim_snippet is None
|
||||||
|
|
||||||
|
assert_viewpoints(topic_handler.viewpoints)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_viewpoints(viewpoints):
|
||||||
|
assert len(viewpoints) == 1
|
||||||
|
|
||||||
|
expected_vp = mdl.VisualizationInfo(
|
||||||
|
components=mdl.Components(
|
||||||
|
selection=mdl.ComponentSelection(),
|
||||||
|
visibility=mdl.ComponentVisibility(
|
||||||
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
|
spaces_visible=False,
|
||||||
|
space_boundaries_visible=False,
|
||||||
|
openings_visible=False,
|
||||||
|
),
|
||||||
|
exceptions=mdl.ComponentVisibilityExceptions(
|
||||||
|
component=[
|
||||||
|
mdl.Component(ifc_guid="1m5wAJelDFdhn6qBdOGjos", originating_system="https://bimsync.com"),
|
||||||
|
mdl.Component(ifc_guid="1bbI761TbBCOoIa5Kt6PXt", originating_system="https://bimsync.com"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
default_visibility=True,
|
||||||
|
),
|
||||||
|
coloring=mdl.ComponentColoring(),
|
||||||
|
),
|
||||||
|
perspective_camera=mdl.PerspectiveCamera(
|
||||||
|
camera_view_point=mdl.Point(x=23.112083480037754, y=-25.45574897560043, z=18.52519828443092),
|
||||||
|
camera_direction=mdl.Direction(x=-0.6289237666876837, y=0.5933487270610425, z=-0.5023864884632315),
|
||||||
|
camera_up_vector=mdl.Direction(x=-0.36542566067664123, y=0.3447553774917179, z=0.8646431727652648),
|
||||||
|
field_of_view=60,
|
||||||
|
aspect_ratio=1,
|
||||||
|
),
|
||||||
|
lines=mdl.VisualizationInfoLines(),
|
||||||
|
clipping_planes=mdl.VisualizationInfoClippingPlanes(),
|
||||||
|
bitmaps=mdl.VisualizationInfoBitmaps(),
|
||||||
|
guid="20ad2ff7-ceac-4d4b-b288-ad92a0f65182",
|
||||||
|
)
|
||||||
|
|
||||||
|
viewpoint = viewpoints["Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv"]
|
||||||
|
assert viewpoint.visualization_info == expected_vp
|
||||||
|
assert viewpoint.snapshot is not None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_second_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
|
expected_vp = mdl.VisualizationInfo(
|
||||||
|
components=mdl.Components(
|
||||||
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
|
spaces_visible=False,
|
||||||
|
space_boundaries_visible=False,
|
||||||
|
openings_visible=False,
|
||||||
|
),
|
||||||
|
selection=expected_selection,
|
||||||
|
visibility=mdl.ComponentVisibility(
|
||||||
|
exceptions=expected_exception,
|
||||||
|
default_visibility=False,
|
||||||
|
),
|
||||||
|
coloring=expected_coloring,
|
||||||
|
),
|
||||||
|
perspective_camera=mdl.PerspectiveCamera(
|
||||||
|
camera_view_point=mdl.Point(x=-47.18794250488281, y=43.829200744628906, z=10.666350364685059),
|
||||||
|
camera_direction=mdl.Direction(x=0.6745243072509766, y=-0.6599355936050415, z=-0.33091068267822266),
|
||||||
|
camera_up_vector=mdl.Direction(x=0.2271970510482788, y=-0.24091780185699463, z=0.9435783624649048),
|
||||||
|
field_of_view=60,
|
||||||
|
),
|
||||||
|
guid="21dd4807-e9af-439e-a980-04d913a6b1ce",
|
||||||
|
)
|
||||||
|
assert viewpoint.visualization_info == expected_vp
|
||||||
|
assert viewpoint.snapshot is not None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_third_viewpoint(viewpoint, expected_selection, expected_exception, expected_coloring) -> None:
|
||||||
|
expected_vp = mdl.VisualizationInfo(
|
||||||
|
components=mdl.Components(
|
||||||
|
view_setup_hints=mdl.ViewSetupHints(
|
||||||
|
spaces_visible=False,
|
||||||
|
space_boundaries_visible=False,
|
||||||
|
openings_visible=True,
|
||||||
|
),
|
||||||
|
selection=expected_selection,
|
||||||
|
visibility=mdl.ComponentVisibility(
|
||||||
|
exceptions=expected_exception,
|
||||||
|
default_visibility=True,
|
||||||
|
),
|
||||||
|
coloring=expected_coloring,
|
||||||
|
),
|
||||||
|
perspective_camera=mdl.PerspectiveCamera(
|
||||||
|
camera_view_point=mdl.Point(x=-48.974571228027344, y=-64.20051574707031, z=10.666350364685059),
|
||||||
|
camera_direction=mdl.Direction(x=0.7232745289802551, y=0.5967116951942444, z=-0.3475759029388428),
|
||||||
|
camera_up_vector=mdl.Direction(x=0.27662187814712524, y=0.21082592010498047, z=0.937567412853241),
|
||||||
|
field_of_view=60,
|
||||||
|
),
|
||||||
|
guid="81daa431-bf01-4a49-80a2-1ab07c177717",
|
||||||
|
)
|
||||||
|
assert viewpoint.visualization_info == expected_vp
|
||||||
|
assert viewpoint.snapshot is not None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_files_present(saved_bcf_path):
|
||||||
|
expected_files = [
|
||||||
|
"bcf.version",
|
||||||
|
"project.bcfp",
|
||||||
|
"documents.xml",
|
||||||
|
"extensions.xml",
|
||||||
|
"documents/b1d1b7f0-60b9-457d-ad12-16e0fb997bc5",
|
||||||
|
"8ac9822a-761a-4deb-9f39-f61286acbf6a/markup.bcf",
|
||||||
|
"8ac9822a-761a-4deb-9f39-f61286acbf6a/Viewpoint_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.bcfv",
|
||||||
|
"8ac9822a-761a-4deb-9f39-f61286acbf6a/Snapshot_20ad2ff7-ceac-4d4b-b288-ad92a0f65182.png",
|
||||||
|
]
|
||||||
|
|
||||||
|
with zipfile.ZipFile(saved_bcf_path) as bcf_zip:
|
||||||
|
for file_path in expected_files:
|
||||||
|
assert zipfile.Path(bcf_zip, file_path).exists()
|
||||||
Reference in New Issue
Block a user