diff --git a/.gitignore b/.gitignore index 902ea7f53b..4f60342ebf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ __pycache__ .vscode # PyCharm files .idea +# gettext binary translation files +*.mo +# Vim +*.swp diff --git a/nix/build-all.py b/nix/build-all.py index b9a100f672..536b7dba76 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -23,7 +23,7 @@ # This script builds IfcOpenShell and its dependencies # # # # Prerequisites for this script to function correctly: # -# * git * bzip2 * tar * c(++) compilers * yacc * autoconf # +# * cmake * git * bzip2 * tar * c(++) compilers * yacc * autoconf # # # # if building with USE_OCCT additionally: # # * freetype * glx.h # @@ -38,15 +38,15 @@ # * libffi(-dev[el]) # # # # on debian 7.8 these can be obtained with: # -# $ apt-get install git gcc g++ autoconf bison bzip2 # +# $ apt-get install git gcc g++ autoconf bison bzip2 cmake # # libfreetype6-dev mesa-common-dev libffi-dev libfontconfig1-dev # # # # on ubuntu 14.04: # -# $ apt-get install git gcc g++ autoconf bison make # +# $ apt-get install git gcc g++ autoconf bison make cmake # # libfreetype6-dev mesa-common-dev libffi-dev libfontconfig1-dev # # # # on OS X El Capitan with homebrew: # -# $ brew install git bison autoconf automake freetype libffi # +# $ brew install git bison autoconf automake freetype libffi cmake # # # ############################################################################### @@ -79,7 +79,7 @@ ch.setLevel(logging.INFO) logger.addHandler(ch) PROJECT_NAME="IfcOpenShell" -PYTHON_VERSIONS=["2.7.16", "3.2.6", "3.3.6", "3.4.6", "3.5.3", "3.6.2", "3.7.3", "3.8.2"] +PYTHON_VERSIONS=["2.7.16", "3.2.6", "3.3.6", "3.4.6", "3.5.3", "3.6.2", "3.7.3", "3.8.6", "3.9.1"] JSON_VERSION="v3.6.1" OCE_VERSION="0.18" # OCCT_VERSION="7.1.0" @@ -87,13 +87,11 @@ OCE_VERSION="0.18" # OCCT_VERSION="7.2.0" # OCCT_HASH="88af392" OCCT_VERSION="7.3.0p3" -BOOST_VERSION="1.59.0" +BOOST_VERSION="1.71.0" #PCRE_VERSION="8.39" PCRE_VERSION="8.41" #LIBXML2_VERSION="2.9.3" LIBXML2_VERSION="2.9.9" -CMAKE_VERSION="3.4.3" -#CMAKE_VERSION="3.14.5" SWIG_VERSION="3.0.12" #SWIG_VERSION="4.0.0" #OPENCOLLADA_VERSION="v1.6.63" @@ -159,6 +157,13 @@ TOOLSET = None if get_os() == "Darwin": # C++11 features used in OCCT 7+ need a more recent stdlib TOOLSET = "10.9" if USE_OCCT else "10.6" + +# python 3.4 doesn't seem to build anymore on recent versions of clang +if get_os() == "Darwin": + try: + PYTHON_VERSIONS.remove("3.4.6") + except ValueError as e: + pass try: IFCOS_NUM_BUILD_PROCS = os.environ["IFCOS_NUM_BUILD_PROCS"] @@ -259,7 +264,7 @@ print("Building:", *sorted(targets, key=lambda t: len(list(v(t))))) # Check that required tools are in PATH -for cmd in [git, bunzip2, tar, cc, cplusplus, autoconf, automake, yacc, make, "patch"]: +for cmd in [git, bunzip2, tar, cc, cplusplus, autoconf, automake, yacc, make, "patch", "cmake"]: if which(cmd) is None: raise ValueError("Required tool '%s' not installed or not added to PATH" % (cmd,)) @@ -303,10 +308,9 @@ def run(cmds, cwd=None): return stdout.strip() BOOST_VERSION_UNDERSCORE=BOOST_VERSION.replace(".", "_") -CMAKE_VERSION_2=CMAKE_VERSION[:CMAKE_VERSION.rindex('.')] OCE_LOCATION="https://github.com/tpaviot/oce/archive/OCE-%s.tar.gz" % (OCE_VERSION,) -BOOST_LOCATION="http://downloads.sourceforge.net/project/boost/boost/%s/boost_%s.tar.bz2" % (BOOST_VERSION, BOOST_VERSION_UNDERSCORE) +BOOST_LOCATION="https://dl.bintray.com/boostorg/release/%s/source/" % (BOOST_VERSION,) # Helper functions @@ -322,8 +326,7 @@ def run_cmake(arg1, cmake_args, cmake_dir=None, cwd=None): P=".." else: P=cmake_dir - cmake_path= os.path.join(DEPS_DIR, "install", "cmake-%s" % (CMAKE_VERSION,), "bin", "cmake") - run([cmake_path, P]+cmake_args+["-DCMAKE_BUILD_TYPE=%s" % (BUILD_CFG,)], cwd=cwd) + run(["cmake", P]+cmake_args+["-DCMAKE_BUILD_TYPE=%s" % (BUILD_CFG,)], cwd=cwd) def git_clone_or_pull_repository(clone_url, target_dir, revision=None): """Lazily clones the `git` repository denoted by `clone_url` into @@ -424,7 +427,7 @@ def build_dependency(name, mode, build_tool_args, download_url, download_name, d else: raise ValueError() logger.info("\rBuilding %s... " % (name,)) - run([make, "-j%s" % (IFCOS_NUM_BUILD_PROCS,)], cwd=extract_build_dir) + run([make, "-j%s" % (IFCOS_NUM_BUILD_PROCS,), "VERBOSE=1"], cwd=extract_build_dir) logger.info( "\rInstalling %s... " % (name,)) run([make, "install"], cwd=extract_build_dir) logger.info( "\rInstalled %s \n" % (name,)) @@ -445,7 +448,10 @@ cecho("Collecting dependencies:", GREEN) ADDITIONAL_ARGS=[] BOOST_ADDRESS_MODEL=[] if TARGET_ARCH == "i686" and run([uname, "-m"]).strip() == "x86_64": - ADDITIONAL_ARGS=["-m32", "-arch i386"] + if get_os() == "Darwin": + ADDITIONAL_ARGS=["-m32", "-arch i386"] + else: + ADDITIONAL_ARGS=["-m32"] BOOST_ADDRESS_MODEL=["architecture=x86", "address-model=32"] if get_os() == "Darwin": @@ -484,25 +490,8 @@ os.environ["CFLAGS"] = CFLAGS os.environ["LDFLAGS"] = LDFLAGS # Some dependencies need a more recent CMake version than most distros provide -build_dependency(name="cmake-%s" % (CMAKE_VERSION,), mode="autoconf", build_tool_args=[], download_url="https://cmake.org/files/v%s" % (CMAKE_VERSION_2,), download_name="cmake-%s.tar.gz" % (CMAKE_VERSION,)) - -# Extract compiler flags from CMake to harmonize settings with other autoconf dependencies -CMAKE_FLAG_EXTRACT_DIR="ifcopenshell_cmake_test_%s" % (time.time(),) -# was sp.check_output([bash, "-c", "cat /dev/urandom | env LC_CTYPE=C tr -dc 'a-zA-Z0-9' | head -c 32"]), in bash script, unclear what the exact required format is and whether it's needed -if os.path.exists(CMAKE_FLAG_EXTRACT_DIR): - shutil.rmtree(CMAKE_FLAG_EXTRACT_DIR) -os.makedirs(CMAKE_FLAG_EXTRACT_DIR) -BUILD_CFG_UPPER=BUILD_CFG.upper() -for FL in ["C", "CXX"]: - run([bash, "-c", """echo " - message(\"\${CMAKE_%s_FLAGS_%s}\") - " > CMakeLists.txt""" % (FL, BUILD_CFG_UPPER)], cwd=CMAKE_FLAG_EXTRACT_DIR) - FL="%sFLAGS" % (FL,) - FLM="%sFLAGS_MINIMAL" % (FL,) -# @TODO: bash code unclear -# exec("%sFLAGS=%s" % (FL, sp.check_output([os.path.join(DEPS_DIR, "install", "cmake-%s" % (CMAKE_VERSION,), "bin", "cmake"), "." -# declare ${FL}FLAGS_MINIMAL="`$DEPS_DIR/install/cmake-$CMAKE_VERSION/bin/cmake . 2>&1 >/dev/null` ${!FLM}" -shutil.rmtree(CMAKE_FLAG_EXTRACT_DIR) +# @tfk: this is no longer needed +# build_dependency(name="cmake-%s" % (CMAKE_VERSION,), mode="autoconf", build_tool_args=[], download_url="https://cmake.org/files/v%s" % (CMAKE_VERSION_2,), download_name="cmake-%s.tar.gz" % (CMAKE_VERSION,)) if "json" in targets: json_url = "https://github.com/nlohmann/json/releases/download/{JSON_VERSION}/json.hpp".format(**locals()) @@ -527,7 +516,7 @@ if "swig" in targets: build_dependency( name="swig", mode="autoconf", - build_tool_args=["--with-pcre-prefix={DEPS_DIR}/install/pcre-{PCRE_VERSION}".format(**locals())], + build_tool_args=["--disable-ccache", "--with-pcre-prefix={DEPS_DIR}/install/pcre-{PCRE_VERSION}".format(**locals())], download_url="https://github.com/swig/swig.git", download_name="swig", download_tool=download_tool_git, @@ -598,7 +587,7 @@ if "OpenCOLLADA" in targets: download_url="https://github.com/KhronosGroup/OpenCOLLADA.git", download_name="OpenCOLLADA", download_tool=download_tool_git, - patch="./patches/opencollada/pr622.patch", + patch="./patches/opencollada/pr622_and_disable_subdirs.patch", revision=OPENCOLLADA_VERSION ) @@ -662,7 +651,7 @@ if "boost" in targets: list(map(str_concat("cxxflags"), CXXFLAGS.strip().split(' '))) + \ list(map(str_concat("linkflags"), LDFLAGS.strip().split(' '))) + \ ["stage", "-s", "NO_BZIP2=1"], - download_url="http://downloads.sourceforge.net/project/boost/boost/{BOOST_VERSION}/".format(**locals()), + download_url=BOOST_LOCATION, download_name="boost_{BOOST_VERSION_UNDERSCORE}.tar.bz2".format(**locals()) ) diff --git a/nix/patches/opencollada/pr622_and_disable_subdirs.patch b/nix/patches/opencollada/pr622_and_disable_subdirs.patch new file mode 100644 index 0000000000..854b023af1 --- /dev/null +++ b/nix/patches/opencollada/pr622_and_disable_subdirs.patch @@ -0,0 +1,29 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 95abbe21..293c951c 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -284,10 +284,10 @@ add_subdirectory(COLLADASaxFrameworkLoader) + add_subdirectory(COLLADAStreamWriter) + + # building COLLADAValidator app +-add_subdirectory(COLLADAValidator) ++# add_subdirectory(COLLADAValidator) + + # DAE validator app +-add_subdirectory(DAEValidator) ++# add_subdirectory(DAEValidator) + + # Library export + install(EXPORT LibraryExport DESTINATION ${OPENCOLLADA_INST_CMAKECONFIG} FILE OpenCOLLADATargets.cmake) +diff --git a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp +index 1f9a3eef..dd6f5c59 100644 +--- a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp ++++ b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp +@@ -10,6 +10,7 @@ + + #include "GeneratedSaxParserUtils.h" + #include ++#include + #include + #include + #include diff --git a/src/bcf/bcf/bcfxml.py b/src/bcf/bcf/bcfxml.py index 40069ceb15..bc4eaadee0 100644 --- a/src/bcf/bcf/bcfxml.py +++ b/src/bcf/bcf/bcfxml.py @@ -9,6 +9,7 @@ from datetime import datetime from xml.dom import minidom from xmlschema import XMLSchema from contextlib import contextmanager +from shutil import copyfile cwd = os.path.dirname(os.path.realpath(__file__)) @@ -206,6 +207,8 @@ class BcfXml: self.document = minidom.Document() root = self._create_element(self.document, "Markup") + self.write_header(topic.header, root) + topic_el = self._create_element( root, "Topic", @@ -216,10 +219,22 @@ class BcfXml: }, ) + for reference_link in topic.reference_links: + self._create_element(topic_el, "ReferenceLink", text=reference_link) + text_map = { "Title": topic.title, "Priority": topic.priority, "Index": topic.index, + } + for key, value in text_map.items(): + if value: + self._create_element(topic_el, key, text=value) + + for label in topic.labels: + self._create_element(topic_el, "Labels", text=label) + + text_map = { "CreationDate": topic.creation_date, "CreationAuthor": topic.creation_author, "ModifiedDate": topic.modified_date, @@ -233,10 +248,6 @@ class BcfXml: if value: self._create_element(topic_el, key, text=value) - for reference_link in topic.reference_links: - self._create_element(topic_el, "ReferenceLink", text=reference_link) - for label in topic.labels: - self._create_element(topic_el, "Labels", text=label) if topic.bim_snippet: bim_snippet = self._create_element( topic_el, @@ -254,7 +265,6 @@ class BcfXml: for related_topic in topic.related_topics: self._create_element(topic_el, "RelatedTopic", {"Guid": related_topic.guid}) - self.write_header(topic.header, root) self.write_comments(topic.comments, root) self.write_viewpoints(topic.viewpoints, root, topic) @@ -262,7 +272,7 @@ class BcfXml: f.write(self.document.toprettyxml(encoding="utf-8")) def write_header(self, header, root): - if not header: + if not header or not header.files: return header_el = self._create_element(root, "Header") for f in header.files: @@ -295,14 +305,15 @@ class BcfXml: if comment.viewpoint: self._create_element(comment_el, "Viewpoint", {"Guid": comment.viewpoint.guid}) - def add_comment(self, comment=None): + def add_comment(self, topic, comment=None): if comment is None: comment = bcf.data.Comment() if not comment.guid: comment.guid = str(uuid.uuid4()) if not comment.comment: comment.comment = "'Free software' is a matter of liberty, not price. To understand the concept, you should think of 'free' as in 'free speech,' not as in 'free beer'." - self.edit_comment(comment) + topic.comments[comment.guid] = comment + self.edit_comment(comment, topic) def edit_comment(self, comment, topic): if not comment.date: @@ -334,7 +345,7 @@ class BcfXml: def write_viewpoint(self, viewpoint, topic): document = minidom.Document() - root = self._create_element(document, "VisualizationInfo") + root = self._create_element(document, "VisualizationInfo", {"Guid": viewpoint.guid}) self.write_viewpoint_components(viewpoint, root) self.write_viewpoint_orthogonal_camera(viewpoint, root) self.write_viewpoint_perspective_camera(viewpoint, root) @@ -354,7 +365,7 @@ class BcfXml: "ViewSetupHints", { "SpacesVisible": viewpoint.components.view_setup_hints.spaces_visible, - "SpaceBoundiresVisible": viewpoint.components.view_setup_hints.space_boundaries_visible, + "SpaceBoundariesVisible": viewpoint.components.view_setup_hints.space_boundaries_visible, "OpeningsVisible": viewpoint.components.view_setup_hints.openings_visible, }, ) @@ -429,9 +440,11 @@ class BcfXml: return for bitmap in viewpoint.bitmaps: bitmap_el = self._create_element(parent, "Bitmap") - text_map = {"Bitmap": bitmap.bitmap_type, "Reference": bitmap.reference, "Height": bitmap.height} + + text_map = {"Bitmap": bitmap.bitmap_type, "Reference": bitmap.reference} for key, value in text_map.items(): self._create_element(bitmap_el, key, text=value) + location_el = self._create_element(bitmap_el, "Location") self.write_vector(location_el, bitmap.location) normal_el = self._create_element(bitmap_el, "Normal") @@ -439,6 +452,8 @@ class BcfXml: up_el = self._create_element(bitmap_el, "Up") self.write_vector(up_el, bitmap.up) + self._create_element(bitmap_el, "Height", text=bitmap.height) + def write_vector(self, parent, from_obj): self._create_element(parent, "X", text=from_obj.x) self._create_element(parent, "Y", text=from_obj.y) @@ -461,8 +476,11 @@ class BcfXml: if viewpoint.snapshot: topic_filepath = os.path.join(self.filepath, topic.guid) filepath = os.path.join(topic_filepath, viewpoint.snapshot) - if not os.path.exists(filepath): - pass # TODO: handle uploading files + if not os.path.exists(filepath) or topic_filepath not in filepath: + filename = viewpoint.guid + "." + viewpoint.snapshot[-3:] + copyfile(viewpoint.snapshot, os.path.join(topic_filepath, filename)) + viewpoint.snapshot = filename + topic.viewpoints[viewpoint.guid] = viewpoint self.edit_topic(topic) def delete_viewpoint(self, guid, topic): @@ -486,6 +504,75 @@ class BcfXml: del topic.viewpoints[guid] self.edit_topic(topic) + def delete_file(self, topic, index): + if not topic.header: + return + f = topic.header.files.pop(index) + filepath = os.path.join(self.filepath, topic.guid, f.reference) + if not f.is_external and os.path.exists(filepath): + os.remove(filepath) + self.edit_topic(topic) + + def delete_bim_snippet(self, topic): + if not topic.bim_snippet: + return + if topic.bim_snippet.reference and not topic.bim_snippet.is_external: + filepath = os.path.join(self.filepath, topic.guid, topic.bim_snippet.reference) + if os.path.exists(filepath): + os.remove(filepath) + topic.bim_snippet = None + self.edit_topic(topic) + + def delete_document_reference(self, topic, index): + document_reference = topic.document_references[index] + if document_reference.referenced_document and not document_reference.is_external: + filepath = os.path.join(self.filepath, topic.guid, document_reference.referenced_document) + if os.path.exists(filepath): + os.remove(filepath) + del topic.document_references[index] + self.edit_topic(topic) + + def add_document_reference(self, topic, document_reference): + if os.path.exists(document_reference.referenced_document): + topic_filepath = os.path.join(self.filepath, topic.guid) + filename = os.path.basename(document_reference.referenced_document) + copyfile(document_reference.referenced_document, os.path.join(topic_filepath, filename)) + document_reference.referenced_document = filename + document_reference.is_external = False + else: + document_reference.is_external = True + if not document_reference.guid: + document_reference.guid = str(uuid.uuid4()) + topic.document_references.append(document_reference) + self.edit_topic(topic) + + def add_bim_snippet(self, topic, bim_snippet): + if topic.bim_snippet: + self.delete_bim_snippet(topic) + if os.path.exists(bim_snippet.reference): + topic_filepath = os.path.join(self.filepath, topic.guid) + filename = os.path.basename(bim_snippet.reference) + copyfile(bim_snippet.reference, os.path.join(topic_filepath, filename)) + bim_snippet.reference = filename + bim_snippet.is_external = False + else: + bim_snippet.is_external = True + topic.bim_snippet = bim_snippet + self.edit_topic(topic) + + def add_file(self, topic, header_file): + if os.path.exists(header_file.reference): + topic_filepath = os.path.join(self.filepath, topic.guid) + header_file.filename = os.path.basename(header_file.reference) + copyfile(header_file.reference, os.path.join(topic_filepath, header_file.filename)) + header_file.reference = header_file.filename + header_file.is_external = False + header_file.date = datetime.utcnow().isoformat() + if not topic.header: + topic.header = bcf.data.Header() + topic.header.files.append(header_file) + self.edit_topic(topic) + def get_comments(self, guid): comments = {} data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") @@ -622,7 +709,7 @@ class BcfXml: for item in visinfo["Bitmap"]: bitmap = bcf.data.Bitmap() bitmap.reference = item["Reference"] - bitmap.bitmap_type = item["Bitmap"].lower() + bitmap.bitmap_type = item["Bitmap"].upper() self.set_vector(bitmap.location, item["Location"]) self.set_vector(bitmap.normal, item["Normal"]) self.set_vector(bitmap.up, item["Up"]) diff --git a/src/bcf/bcf/data.py b/src/bcf/bcf/data.py index 0be7560e55..7fd23638be 100644 --- a/src/bcf/bcf/data.py +++ b/src/bcf/bcf/data.py @@ -156,7 +156,7 @@ class Bitmap: def __init__(self): self.reference = "" # Only in BCF-XML self.bitmap_data = None # Only in BCF-API - self.bitmap_type = "png" # Enum of png or jpg + self.bitmap_type = "PNG" # Enum of png or jpg self.location = Point() self.normal = Direction() self.up = Direction() diff --git a/src/bcf/bcf/xsd/visinfo.xsd b/src/bcf/bcf/xsd/visinfo.xsd index 8bec85b445..c54cbf8dd5 100644 --- a/src/bcf/bcf/xsd/visinfo.xsd +++ b/src/bcf/bcf/xsd/visinfo.xsd @@ -91,8 +91,8 @@ - - + + diff --git a/src/ifcbimtester/README.md b/src/ifcbimtester/README.md index a02d4bf267..162b355806 100644 --- a/src/ifcbimtester/README.md +++ b/src/ifcbimtester/README.md @@ -16,3 +16,14 @@ python3 ./bimtester.py -g `$ pyinstaller --onefile --clean --icon=icon.ico --add-data "features:features" bimtester.py` + Windows: `$ pyinstaller --onefile --clean --icon=icon.ico --add-data "features;features" bimtester.py` + + +### Translation files ++ the binary mo translation files are not part of the repo ++ they might be needed to be recreated ++ use the following commands on Linux +``` +cd YourIfcopenshellRepository/src/ifcbimtester/bimtester/locale +pybabel compile -d . + +``` diff --git a/src/ifcbimtester/bimtester/features/dummy.feature b/src/ifcbimtester/bimtester/features/dummy.feature new file mode 100644 index 0000000000..59da694397 --- /dev/null +++ b/src/ifcbimtester/bimtester/features/dummy.feature @@ -0,0 +1,3 @@ + # dummy file to be able to run the command to get a list of all steps + # the command has to be run in the parent directory of this one + # behave --steps-catalog diff --git a/src/ifcbimtester/bimtester/features/environment.py b/src/ifcbimtester/bimtester/features/environment.py index 9bce6c6fb4..5e5ef6303e 100644 --- a/src/ifcbimtester/bimtester/features/environment.py +++ b/src/ifcbimtester/bimtester/features/environment.py @@ -3,6 +3,10 @@ from behave.model import Scenario def before_all(context): userdata = context.config.userdata + context.ifcbasename = userdata["ifcbasename"] context.localedir = userdata.get("localedir") + + # do not break after a failed scenario + # https://community.osarch.org/discussion/comment/3328/#Comment_3328 continue_after_failed = userdata.getbool("runner.continue_after_failed_step", True) Scenario.continue_after_failed_step = continue_after_failed diff --git a/src/ifcbimtester/bimtester/features/steps/attributes_eleclasses.py b/src/ifcbimtester/bimtester/features/steps/attributes_eleclasses.py new file mode 100644 index 0000000000..b3b6b7d236 --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/attributes_eleclasses.py @@ -0,0 +1,97 @@ +from behave import step + +from attributes_eleclasses_methods import all_element_attribs_have_a_value +from attributes_eleclasses_methods import name_has_a_value +from attributes_eleclasses_methods import description_has_a_value +from attributes_eleclasses_methods import no_element_class_ele +from utils import assert_elements +from utils import IfcFile +from utils import switch_locale + + +@step("there are no {ifc_class} elements because {reason}") +def step_impl(context, ifc_class, reason): + switch_locale(context.localedir, "en") + no_element_class_ele(context, ifc_class, reason) + + +@step('all {ifc_class} elements class attributes have a value') +def step_impl(context, ifc_class): + switch_locale(context.localedir, "en") + all_element_attribs_have_a_value(context, ifc_class) + + +@step('all {ifc_class} elements have a name given') +def step_impl(context, ifc_class): + switch_locale(context.localedir, "en") + name_has_a_value(context, ifc_class) + + +@step('all {ifc_class} elements have a description given') +def step_impl(context, ifc_class): + switch_locale(context.localedir, "en") + description_has_a_value(context, ifc_class) + + +@step('all {ifc_class} elements have a name matching the pattern "{pattern}"') +def step_impl(context, ifc_class, pattern): + import re + + elements = IfcFile.get().by_type(ifc_class) + for element in elements: + if not re.search(pattern, element.Name): + assert False + + +@step('there is an {ifc_class} element with a {attribute_name} attribute with a value of "{attribute_value}"') +def step_impl(context, ifc_class, attribute_name, attribute_value): + elements = IfcFile.get().by_type(ifc_class) + for element in elements: + if hasattr(element, attribute_name) and getattr(element, attribute_name) == attribute_value: + return + assert False + + +# ------------------------------------------------------------------------ +# STEPS with Regular Expression Matcher ("re") +# ------------------------------------------------------------------------ +use_step_matcher("re") + + +@step("all (?P.*) elements have an? (?P.*) attribute") +def step_impl(context, ifc_class, attribute): + elements = IfcFile.get().by_type(ifc_class) + for element in elements: + if not getattr(element, attribute): + assert False + + +@step('all (?P.*) elements have an? (?P.*) matching the pattern "(?P.*)"') +def step_impl(context, ifc_class, attribute, pattern): + import re + + elements = IfcFile.get().by_type(ifc_class) + for element in elements: + value = getattr(element, attribute) + print(f'Checking value "{value}" for {element}') + assert re.search(pattern, value) + + +@step('all (?P.*) elements have an? (?P.*) taken from the list in "(?P.*)"') +def step_impl(context, ifc_class, attributes, list_file): + import csv + + values = [] + with open(list_file) as csvfile: + reader = csv.reader(csvfile) + for row in reader: + values.append(row) + elements = IfcFile.get().by_type(ifc_class) + for element in elements: + attribute_values = [] + for attribute in attributes.split(","): + if not hasattr(element, attribute): + assert False, f"Failed at element {element.GlobalId}" + attribute_values.append(getattr(element, attribute)) + if attribute_values not in values: + assert False, f"Failed at element {element.GlobalId}" diff --git a/src/ifcbimtester/bimtester/features/steps/attributes_eleclasses_methods.py b/src/ifcbimtester/bimtester/features/steps/attributes_eleclasses_methods.py new file mode 100644 index 0000000000..0588bd3d2f --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/attributes_eleclasses_methods.py @@ -0,0 +1,134 @@ +import gettext # noqa + +from utils import assert_elements +from utils import IfcFile + + +def no_element_class_ele(context, ifc_class, reason): + + context.falseelems = [] + context.falseguids = [] + + elements = IfcFile.get().by_type(ifc_class) + for elem in elements: + context.falseelems.append(str(elem)) + context.falseguids.append(elem.GlobalId) + + context.elemcount = len(elements) + context.falsecount = len(context.falseelems) + + if context.elemcount == 0 and context.falsecount == 0: + return # Test OK, thus we can not use the assert_elements method + elif context.falsecount == context.elemcount: + assert False, ( + _("All {elemcount} elements in the file are {ifc_class}.") + .format( + elemcount=context.elemcount, + ifc_class=ifc_class + ) + ) + elif context.falsecount > 0 and fcontext.alsecount < context.elemcount: + assert False, ( + _("{falsecount} of {elemcount} element are {ifc_class} elements: {falseelems}") + .format( + falsecount=context.falsecount, + elemcount=context.elemcount, + ifc_class=ifc_class, + falseelems=context.falseelems, + ) + ) + else: + assert False, _("Error in falsecount, something went wrong.") + + +def all_element_attribs_have_a_value(context, ifc_class): + + from ifcopenshell.ifcopenshell_wrapper import schema_by_name + # schema = schema_by_name("IFC2X3") + schema = schema_by_name(IfcFile.get().schema) + class_attributes = [] + for cl_attrib in schema.declaration_by_name(ifc_class).all_attributes(): + class_attributes.append(cl_attrib.name()) + # print(class_attributes) + + context.falseelems = [] + context.falseguids = [] + context.falseprops = {} + + elements = IfcFile.get().by_type(ifc_class) + for elem in elements: + failed_attribs = [] + elem_failed = False + for cl_attrib in class_attributes: + attrib_value = getattr(elem, cl_attrib) + if not attrib_value: + elem_failed = True + failed_attribs.append(cl_attrib) + # print(attrib_value) + if elem_failed is True: + context.falseelems.append(str(elem)) + context.falseguids.append(elem.GlobalId) + context.falseprops[elem.id()] = failed_attribs + + context.elemcount = len(elements) + context.falsecount = len(context.falseelems) + assert_elements( + ifc_class, + context.elemcount, + context.falsecount, + context.falseelems, + message_all_falseelems=_("For all {elemcount} {ifc_class} elements at least one of these class attributes {parameter} has no value."), + message_some_falseelems=_("For the following {falsecount} out of {elemcount} {ifc_class} elements at least one of these class attributes {parameter} has no value: {falseelems}"), + message_no_elems=_("There are no {ifc_class} elements in the IFC file."), + parameter=failed_attribs + ) + + +def name_has_a_value(context, ifc_class): + + context.falseelems = [] + context.falseguids = [] + + elements = IfcFile.get().by_type(ifc_class) + for elem in elements: + # print(elem.Name) + if not elem.Name: + context.falseelems.append(str(elem)) + context.falseguids.append(elem.GlobalId) + + context.elemcount = len(elements) + context.falsecount = len(context.falseelems) + assert_elements( + ifc_class, + context.elemcount, + context.falsecount, + context.falseelems, + message_all_falseelems=_("The name of all {elemcount} {elemcount} elements is not set."), + message_some_falseelems=_("The name of {falsecount} out of {elemcount} {ifc_class} elements is not set: {falseelems}"), + message_no_elems=_("There are no {ifc_class} elements in the IFC file."), + ) + + +def description_has_a_value(context, ifc_class): + + context.falseelems = [] + context.falseguids = [] + + elements = IfcFile.get().by_type(ifc_class) + for elem in elements: + # print(elem.Description) + if not elem.Description: + context.falseelems.append(str(elem)) + context.falseguids.append(elem.GlobalId) + + context.elemcount = len(elements) + context.falsecount = len(context.falseelems) + assert_elements( + ifc_class, + context.elemcount, + context.falsecount, + context.falseelems, + message_all_falseelems=_("The description of all {elemcount} {elemcount} elements is not set."), + message_some_falseelems=_("The description of {falsecount} out of {elemcount} {ifc_class} elements is not set: {falseelems}"), + message_no_elems=_("There are no {ifc_class} elements in the IFC file."), + ) diff --git a/src/ifcbimtester/bimtester/features/steps/attributes_psets.py b/src/ifcbimtester/bimtester/features/steps/attributes_psets.py new file mode 100644 index 0000000000..124c54467a --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/attributes_psets.py @@ -0,0 +1,45 @@ +from behave import step + +from attributes_psets_methods import all_eleclass_elements_have_prop_in_pset +from utils import assert_elements +from utils import IfcFile +from utils import switch_locale + + +@step("all {ifc_class} elements have an {aproperty} property in the {pset} pset") +def step_impl(context, ifc_class, aproperty, pset): + switch_locale(context.localedir, "en") + all_eleclass_elements_have_prop_in_pset(context, ifc_class, aproperty, pset) + + +# ------------------------------------------------------------------------ +# STEPS with Regular Expression Matcher ("re") +# ------------------------------------------------------------------------ +use_step_matcher("re") + + +@step("all (?P.*) elements have an? (?P.*\..*) property") +def step_impl(context, ifc_class, property_path): + pset_name, property_name = property_path.split(".") + elements = IfcFile.get().by_type(ifc_class) + for element in elements: + if not IfcFile.get_property(element, pset_name, property_name): + assert False + + +@step( + 'all (?P.*) elements have an? (?P.*\..*) property value matching the pattern "(?P.*)"' +) +def step_impl(context, ifc_class, property_path, pattern): + import re + + pset_name, property_name = property_path.split(".") + elements = IfcFile.get().by_type(ifc_class) + for element in elements: + prop = IfcFile.get_property(element, pset_name, property_name) + if not prop: + assert False + # For now, we only check single values + if prop.is_a("IfcPropertySingleValue"): + if not (prop.NominalValue and re.search(pattern, prop.NominalValue.wrappedValue)): + assert False diff --git a/src/ifcbimtester/bimtester/features/steps/attributes_psets_methods.py b/src/ifcbimtester/bimtester/features/steps/attributes_psets_methods.py new file mode 100644 index 0000000000..61c707b6da --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/attributes_psets_methods.py @@ -0,0 +1,34 @@ +import gettext # noqa + +from utils import assert_elements +from utils import IfcFile + + +def all_eleclass_elements_have_prop_in_pset(context, ifc_class, aproperty, pset): + + context.falseelems = [] + context.falseguids = [] + context.falseprops = {} + from ifcopenshell.util.element import get_psets + + elements = IfcFile.get().by_type(ifc_class) + for elem in elements: + psets = get_psets(elem) + if not (pset in psets and aproperty in psets[pset]): + context.falseelems.append(str(elem)) + context.falseguids.append(elem.GlobalId) + context.falseprops[elem.id()] = str(psets) + + context.elemcount = len(elements) + context.falsecount = len(context.falseelems) + assert_elements( + ifc_class, + context.elemcount, + context.falsecount, + context.falseelems, + message_all_falseelems=_("All {elemcount} {ifc_class} elements are missing the property {parameter} in the pset."), + message_some_falseelems=_("The following {falsecount} of {elemcount} {ifc_class} elements are missing the property {parameter} in the pset: {falseelems}"), + message_no_elems=_("There are no {ifc_class} elements in the IFC file."), + parameter=aproperty + ) + # the pset name is missing in the failing message, but it is in the step test name diff --git a/src/ifcbimtester/bimtester/features/steps/attributes_qsets.py b/src/ifcbimtester/bimtester/features/steps/attributes_qsets.py new file mode 100644 index 0000000000..85a2f3716c --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/attributes_qsets.py @@ -0,0 +1,19 @@ +from behave import step + +from utils import IfcFile + + +@step("all {ifc_class} elements have a {qto_name}.{quantity_name} quantity") +def step_impl(context, ifc_class, qto_name, quantity_name): + elements = IfcFile.get().by_type(ifc_class) + for element in elements: + is_successful = False + if not element.IsDefinedBy: + assert False + for relationship in element.IsDefinedBy: + if relationship.RelatingPropertyDefinition.Name == qto_name: + for quantity in relationship.RelatingPropertyDefinition.Quantities: + if quantity.Name == quantity_name: + is_successful = True + if not is_successful: + assert False diff --git a/src/ifcbimtester/bimtester/features/steps/element_classes.py b/src/ifcbimtester/bimtester/features/steps/element_classes_guid.py similarity index 100% rename from src/ifcbimtester/bimtester/features/steps/element_classes.py rename to src/ifcbimtester/bimtester/features/steps/element_classes_guid.py diff --git a/src/ifcbimtester/bimtester/features/steps/geometric_detail.py b/src/ifcbimtester/bimtester/features/steps/geometric_detail.py index 930e511d0e..594b42b4e6 100644 --- a/src/ifcbimtester/bimtester/features/steps/geometric_detail.py +++ b/src/ifcbimtester/bimtester/features/steps/geometric_detail.py @@ -1,6 +1,9 @@ from behave import step +from geometric_detail_methods import class_geometric_representation +from utils import assert_elements from utils import IfcFile +from utils import switch_locale @step("All elements must be under {number} polygons") @@ -29,3 +32,9 @@ def step_impl(context, number): for error in errors: message += "Polygons: {} - {}\n".format(error[0], error[1]) assert False, message + + +@step("all {ifc_class} elements have an {representation_class} representation") +def step_impl(context, ifc_class, representation_class): + switch_locale(context.localedir, "en") + class_geometric_representation(context, ifc_class, representation_class) diff --git a/src/ifcbimtester/bimtester/features/steps/geometric_detail_de.py b/src/ifcbimtester/bimtester/features/steps/geometric_detail_de.py new file mode 100644 index 0000000000..8b1977b42e --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/geometric_detail_de.py @@ -0,0 +1,10 @@ +from behave import step + +from geometric_detail_methods import class_geometric_representation +from utils import switch_locale + + +@step("Alle {ifc_class} Bauteile müssen eine geometrische Repräsentation der Klasse {representation_class} verwenden") +def step_impl(context, ifc_class, representation_class): + switch_locale(context.localedir, "de") + class_geometric_representation(context, ifc_class, representation_class) diff --git a/src/ifcbimtester/bimtester/features/steps/geometric_detail_methods.py b/src/ifcbimtester/bimtester/features/steps/geometric_detail_methods.py new file mode 100644 index 0000000000..5a321abc32 --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/geometric_detail_methods.py @@ -0,0 +1,54 @@ +import gettext # noqa +from utils import assert_elements +from utils import IfcFile + + +def class_geometric_representation(context, ifc_class, representation_class): + + def is_item_a_representation(item, representation): + if "/" in representation: + for cls in representation.split("/"): + if item.is_a(cls): + return True + elif item.is_a(representation): + return True + + context.falseelems = [] + context.falseguids = [] + context.falseprops = {} + rep = None + + elements = IfcFile.get().by_type(ifc_class) + for elem in elements: + if not elem.Representation: + continue + has_representation = False + for representation in elem.Representation.Representations: + for item in representation.Items: + if item.is_a("IfcMappedItem"): + # We only check one more level deep. + for item2 in item.MappingSource.MappedRepresentation.Items: + if is_item_a_representation(item2, representation_class): + has_representation = True + rep = item2 + else: + if is_item_a_representation(item, representation_class): + has_representation = True + rep = item + if not has_representation: + context.falseelems.append(str(elem)) + context.falseguids.append(elem.GlobalId) + context.falseprops[elem.id()] = str(rep) + + context.elemcount = len(elements) + context.falsecount = len(context.falseelems) + assert_elements( + ifc_class, + context.elemcount, + context.falsecount, + context.falseelems, + message_all_falseelems=_("All {elemcount} {ifc_class} elements are not a {parameter} representation."), + message_some_falseelems=_("The following {falsecount} of {elemcount} {ifc_class} elements are not a {parameter} representation: {falseelems}"), + message_no_elems=_("There are no {ifc_class} elements in the IFC file."), + parameter=representation_class + ) diff --git a/src/ifcbimtester/bimtester/features/steps/ifcdata.py b/src/ifcbimtester/bimtester/features/steps/ifcdata.py index 99096f0e6c..af7bba824c 100644 --- a/src/ifcbimtester/bimtester/features/steps/ifcdata.py +++ b/src/ifcbimtester/bimtester/features/steps/ifcdata.py @@ -1,6 +1,6 @@ -import gettext -import os -from behave import step, given +import gettext # noqa +from behave import given +from behave import step from ifcdata_methods import assert_schema from utils import IfcFile diff --git a/src/ifcbimtester/bimtester/features/steps/project_setup.py b/src/ifcbimtester/bimtester/features/steps/project_setup.py index 9cb8af8732..8fbf128d52 100644 --- a/src/ifcbimtester/bimtester/features/steps/project_setup.py +++ b/src/ifcbimtester/bimtester/features/steps/project_setup.py @@ -67,3 +67,9 @@ def get_subcontext(identifier, type, target_view): assert False, "The subcontext with identifier {}, type {}, and target view {} could not be found".format( identifier, type, target_view ) + + +@step('the project has a {attribute_name} attribute with a value of "{attribute_value}"') +def step_impl(context, attribute_name, attribute_value): + project = IfcFile.get().by_type("IfcProject")[0] + assert getattr(project, attribute_name) == attribute_value diff --git a/src/ifcbimtester/bimtester/features/steps/spatial_structure.py b/src/ifcbimtester/bimtester/features/steps/spatial_structure.py new file mode 100644 index 0000000000..ef96979639 --- /dev/null +++ b/src/ifcbimtester/bimtester/features/steps/spatial_structure.py @@ -0,0 +1,10 @@ +from behave import step + +from utils import IfcFile + + +@step("all buildings have an address") +def step_impl(context): + for building in IfcFile.get().by_type("IfcBuilding"): + if not building.BuildingAddress: + assert False, f'The building "{building.Name}" has no address.' diff --git a/src/ifcbimtester/bimtester/features/steps/steps.py b/src/ifcbimtester/bimtester/features/steps/steps.py deleted file mode 100644 index 8627efd419..0000000000 --- a/src/ifcbimtester/bimtester/features/steps/steps.py +++ /dev/null @@ -1,160 +0,0 @@ -from behave import step - -from utils import IfcFile - - -@step("there are no {ifc_class} elements because {reason}") -def step_impl(context, ifc_class, reason): - assert len(IfcFile.get().by_type(ifc_class)) == 0 - - -@step('all {ifc_class} elements have a name matching the pattern "{pattern}"') -def step_impl(context, ifc_class, pattern): - import re - - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - if not re.search(pattern, element.Name): - assert False - - -@step("all {ifc_class} elements have an {representation_class} representation") -def step_impl(context, ifc_class, representation_class): - def is_item_a_representation(item, representation): - if "/" in representation: - for cls in representation.split("/"): - if item.is_a(cls): - return True - elif item.is_a(representation): - return True - - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - if not element.Representation: - continue - has_representation = False - for representation in element.Representation.Representations: - for item in representation.Items: - if item.is_a("IfcMappedItem"): - # We only check one more level deep. - for item2 in item.MappingSource.MappedRepresentation.Items: - if is_item_a_representation(item2, representation_class): - has_representation = True - else: - if is_item_a_representation(item, representation_class): - has_representation = True - if not has_representation: - assert False - - -use_step_matcher("re") - - -@step("all (?P.*) elements have an? (?P.*) attribute") -def step_impl(context, ifc_class, attribute): - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - if not getattr(element, attribute): - assert False - - -@step("all (?P.*) elements have an? (?P.*\..*) property") -def step_impl(context, ifc_class, property_path): - pset_name, property_name = property_path.split(".") - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - if not IfcFile.get_property(element, pset_name, property_name): - assert False - - -@step( - 'all (?P.*) elements have an? (?P.*\..*) property value matching the pattern "(?P.*)"' -) -def step_impl(context, ifc_class, property_path, pattern): - import re - - pset_name, property_name = property_path.split(".") - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - prop = IfcFile.get_property(element, pset_name, property_name) - if not prop: - assert False - # For now, we only check single values - if prop.is_a("IfcPropertySingleValue"): - if not (prop.NominalValue and re.search(pattern, prop.NominalValue.wrappedValue)): - assert False - - -@step('all (?P.*) elements have an? (?P.*) matching the pattern "(?P.*)"') -def step_impl(context, ifc_class, attribute, pattern): - import re - - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - value = getattr(element, attribute) - print(f'Checking value "{value}" for {element}') - assert re.search(pattern, value) - - -@step('all (?P.*) elements have an? (?P.*) taken from the list in "(?P.*)"') -def step_impl(context, ifc_class, attributes, list_file): - import csv - - values = [] - with open(list_file) as csvfile: - reader = csv.reader(csvfile) - for row in reader: - values.append(row) - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - attribute_values = [] - for attribute in attributes.split(","): - if not hasattr(element, attribute): - assert False, f"Failed at element {element.GlobalId}" - attribute_values.append(getattr(element, attribute)) - if attribute_values not in values: - assert False, f"Failed at element {element.GlobalId}" - - -use_step_matcher("parse") - - -@step("all {ifc_class} elements have a {qto_name}.{quantity_name} quantity") -def step_impl(context, ifc_class, qto_name, quantity_name): - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - is_successful = False - if not element.IsDefinedBy: - assert False - for relationship in element.IsDefinedBy: - if relationship.RelatingPropertyDefinition.Name == qto_name: - for quantity in relationship.RelatingPropertyDefinition.Quantities: - if quantity.Name == quantity_name: - is_successful = True - if not is_successful: - assert False - - -use_step_matcher("parse") - - -@step('the project has a {attribute_name} attribute with a value of "{attribute_value}"') -def step_impl(context, attribute_name, attribute_value): - project = IfcFile.get().by_type("IfcProject")[0] - assert getattr(project, attribute_name) == attribute_value - - -@step('there is an {ifc_class} element with a {attribute_name} attribute with a value of "{attribute_value}"') -def step_impl(context, ifc_class, attribute_name, attribute_value): - elements = IfcFile.get().by_type(ifc_class) - for element in elements: - if hasattr(element, attribute_name) and getattr(element, attribute_name) == attribute_value: - return - assert False - - -@step("all buildings have an address") -def step_impl(context): - for building in IfcFile.get().by_type("IfcBuilding"): - if not building.BuildingAddress: - assert False, f'The building "{building.Name}" has no address.' diff --git a/src/ifcbimtester/bimtester/features/steps/utils.py b/src/ifcbimtester/bimtester/features/steps/utils.py index 30a79c5b60..f3078f8e61 100644 --- a/src/ifcbimtester/bimtester/features/steps/utils.py +++ b/src/ifcbimtester/bimtester/features/steps/utils.py @@ -1,3 +1,4 @@ +import gettext import ifcopenshell import ifcopenshell.express import ifcopenshell.util @@ -99,9 +100,66 @@ def assert_pset(element, pset_name, prop_name=None, value=None): ) +def assert_elements( + ifc_class, + elemcount, + falsecount, + falseelems, + message_all_falseelems, + message_some_falseelems, + message_no_elems, + parameter=None +): + if elemcount > 0 and falsecount == 0: + return # Test OK + elif elemcount == 0: + assert False, ( + message_no_elems.format( + ifc_class=ifc_class + ) + ) + elif falsecount == elemcount: + if parameter is None: + assert False, ( + message_all_falseelems.format( + elemcount=elemcount, + ifc_class=ifc_class + ) + ) + else: + assert False, ( + message_all_falseelems.format( + elemcount=elemcount, + ifc_class=ifc_class, + parameter=parameter + ) + ) + elif falsecount > 0 and falsecount < elemcount: + if parameter is None: + assert False, ( + message_some_falseelems.format( + falsecount=falsecount, + elemcount=elemcount, + ifc_class=ifc_class, + falseelems=falseelems, + ) + ) + else: + assert False, ( + message_some_falseelems.format( + falsecount=falsecount, + elemcount=elemcount, + ifc_class=ifc_class, + falseelems=falseelems, + parameter=parameter + ) + ) + else: + assert False, _("Error in falsecount, something went wrong.") + + def switch_locale(locale_dir, locale_id="en"): - from gettext import translation - newlang = translation( + newlang = gettext.translation( "messages", localedir=locale_dir, languages=[locale_id] diff --git a/src/ifcbimtester/bimtester/features/strings_template_de.json b/src/ifcbimtester/bimtester/features/strings_template_de.json deleted file mode 100644 index cf6e29d294..0000000000 --- a/src/ifcbimtester/bimtester/features/strings_template_de.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tr_lang": "de", - "tr_success": "Bestanden", - "tr_failure": "Durchgefallen", - "tr_tests_passed": "Erfolgreiche Tests", - "tr_duration": "Dauer", - "tr_auditing": "OpenBIM auditing ist eine Funktionalität von", - "tr_and": "und" -} diff --git a/src/ifcbimtester/bimtester/features/strings_template_en.json b/src/ifcbimtester/bimtester/features/strings_template_en.json deleted file mode 100644 index 745eb70a68..0000000000 --- a/src/ifcbimtester/bimtester/features/strings_template_en.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tr_lang": "en", - "tr_success": "Success", - "tr_failure": "Failure", - "tr_tests_passed": "Tests passed", - "tr_duration": "Duration", - "tr_auditing": "OpenBIM auditing is a feature of", - "tr_and": "and" -} diff --git a/src/ifcbimtester/bimtester/features/strings_template_fr.json b/src/ifcbimtester/bimtester/features/strings_template_fr.json deleted file mode 100644 index fa0a984bbb..0000000000 --- a/src/ifcbimtester/bimtester/features/strings_template_fr.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tr_lang": "fr", - "tr_success": "Succès", - "tr_failure": "Échec", - "tr_tests_passed": "Tests réussis", - "tr_duration": "Durée", - "tr_auditing": "L'audit OpenBIM auditing est une fonctionnalité de", - "tr_and": "et" -} diff --git a/src/ifcbimtester/bimtester/locale/de/LC_MESSAGES/messages.mo b/src/ifcbimtester/bimtester/locale/de/LC_MESSAGES/messages.mo deleted file mode 100644 index f95ef3bb99..0000000000 Binary files a/src/ifcbimtester/bimtester/locale/de/LC_MESSAGES/messages.mo and /dev/null differ diff --git a/src/ifcbimtester/bimtester/locale/de/LC_MESSAGES/messages.po b/src/ifcbimtester/bimtester/locale/de/LC_MESSAGES/messages.po index a1a86662aa..232eaf60eb 100644 --- a/src/ifcbimtester/bimtester/locale/de/LC_MESSAGES/messages.po +++ b/src/ifcbimtester/bimtester/locale/de/LC_MESSAGES/messages.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-12-17 15:14+0100\n" -"PO-Revision-Date: 2020-12-18 06:11+0100\n" +"POT-Creation-Date: 2020-12-23 11:03+0100\n" +"PO-Revision-Date: 2020-12-23 11:20+0100\n" "Last-Translator: \n" "Language: de\n" "Language-Team: de \n" @@ -19,6 +19,165 @@ msgstr "" "Generated-By: Babel 2.6.0\n" "X-Generator: Poedit 2.2.1\n" +#: reports.py:153 reports.py:160 +msgid "OpenBIM auditing is a feature of" +msgstr "OpenBIM auditing ist eine Funktionalität von" + +#: reports.py:155 +msgid "en" +msgstr "de" + +#: reports.py:156 +msgid "Success" +msgstr "Bestanden" + +#: reports.py:157 +msgid "Failure" +msgstr "Durchgefallen" + +#: reports.py:158 +msgid "Tests passed" +msgstr "Erfolgreiche Tests" + +#: reports.py:159 +msgid "Duration" +msgstr "Dauer" + +#: reports.py:161 +msgid "and" +msgstr "und" + +#: features/steps/attributes_eleclasses_methods.py:24 +msgid "All {elemcount} elements in the file are {ifc_class}." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:32 +msgid "{falsecount} of {elemcount} element are {ifc_class} elements: {falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:41 features/steps/utils.py:158 +msgid "Error in falsecount, something went wrong." +msgstr "Fehler in falsecount, es ist etwas falsch gelaufen." + +#: features/steps/attributes_eleclasses_methods.py:80 +msgid "" +"For all {elemcount} {ifc_class} elements at least one of these class attributes " +"{parameter} has no value." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:81 +msgid "" +"For the following {falsecount} out of {elemcount} {ifc_class} elements at least " +"one of these class attributes {parameter} has no value: {falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:82 +#: features/steps/attributes_eleclasses_methods.py:108 +#: features/steps/attributes_eleclasses_methods.py:133 +#: features/steps/attributes_psets_methods.py:31 +#: features/steps/geometric_detail_methods.py:52 +#: features/steps/geometric_detail_methods.py:127 +msgid "There are no {ifc_class} elements in the IFC file." +msgstr "Es sind keine {ifc_class} Bauteile in der IFC-Datei." + +#: features/steps/attributes_eleclasses_methods.py:106 +msgid "The name of all {elemcount} {elemcount} elements is not set." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:107 +msgid "" +"The name of {falsecount} out of {elemcount} {ifc_class} elements is not set: " +"{falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:131 +msgid "The description of all {elemcount} {elemcount} elements is not set." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:132 +msgid "" +"The description of {falsecount} out of {elemcount} {ifc_class} elements is not " +"set: {falseelems}" +msgstr "" + +#: features/steps/attributes_psets_methods.py:29 +msgid "" +"All {elemcount} {ifc_class} elements are missing the property {parameter} in " +"the pset." +msgstr "" + +#: features/steps/attributes_psets_methods.py:30 +msgid "" +"The following {falsecount} of {elemcount} {ifc_class} elements are missing the " +"property {parameter} in the pset: {falseelems}" +msgstr "" + +#: features/steps/geometric_detail_methods.py:50 +msgid "All {elemcount} {ifc_class} elements are not a {parameter} representation." +msgstr "" +"Alle {elemcount} {ifc_class} Bauteile haben keine geometrische Repräsentation " +"der Klasse {parameter}." + +#: features/steps/geometric_detail_methods.py:51 +msgid "" +"The following {falsecount} of {elemcount} {ifc_class} elements are not a " +"{parameter} representation: {falseelems}" +msgstr "" +"Die Anzahl Bauteile {falsecount} von allen {elemcount} {ifc_class} Bauteilen " +"haben keine geometrische Repräsentation der Klasse {parameter}: {falseelems}" + +#: features/steps/geometric_detail_methods.py:125 +msgid "The geometry of all {elemcount} {ifc_class} elements have errors." +msgstr "" +"Die geometrischen Repräsentationen von allen {elemcount} {ifc_class} Bauteilen " +"haben Fehler." + +#: features/steps/geometric_detail_methods.py:126 +msgid "" +"The geometry of {falsecount} out of all {elemcount} {ifc_class} elements have " +"errors: {falseelems}" +msgstr "" +"Die geometrischen Repräsentationen von {falsecount} von allen {elemcount} " +"{ifc_class} Bauteilen haben Fehler: {falseelems}" + #: features/steps/ifcdata_methods.py:7 msgid "We expected a schema of {} but instead got {}" msgstr "Wir haben das Schema {} erwartet, aber die Daten nutzen das Schema {}" + +#~ msgid "The geometry of all {} {} elements have errors." +#~ msgstr "" +#~ "Die geometrischen Repräsentationen von allen {} {} Bauteilen haben Fehler." + +#~ msgid "The geometry of {} out of all {} {} elements have errors: {}" +#~ msgstr "" +#~ "Die geometrischen Repräsentationen von {} von allen {} {} Bauteilen haben " +#~ "Fehler: {}" + +#~ msgid "There are no {} elements in the IFC file." +#~ msgstr "Es sind keine {} Bauteile in der IFC-Datei." + +#~ msgid "All {} {} elements are not a IfcFacetedBrep representation." +#~ msgstr "" +#~ "Alle {} {} Bauteile haben keine geometrische Repräsentation der Klasse " +#~ "IfcFacetedBrep." + +#~ msgid "" +#~ "The following {} of {} {} elements are not a IfcFacetedBrep representation: " +#~ "{}" +#~ msgstr "" +#~ "Die Anzahl Bauteile {} von allen {} {} Bauteilen haben keine geometrische " +#~ "Repräsentation der Klasse IfcFacetedBrep: {}" + +#~ msgid "" +#~ "All {elemcount} {ifc_class} elements are not a IfcFacetedBrep representation." +#~ msgstr "" +#~ "Alle {elemcount} {ifc_class} Bauteile haben keine geometrische " +#~ "Repräsentation der Klasse IfcFacetedBrep." + +#~ msgid "" +#~ "The following {falsecount} of {elemcount} {ifc_class} elements are not a " +#~ "IfcFacetedBrep representation: {falseelems}" +#~ msgstr "" +#~ "Die Anzahl Bauteile {falsecount} von allen {elemcount} {ifc_class} Bauteilen " +#~ "haben keine geometrische Repräsentation der Klasse IfcFacetedBrep: " +#~ "{falseelems}" diff --git a/src/ifcbimtester/bimtester/locale/en/LC_MESSAGES/messages.mo b/src/ifcbimtester/bimtester/locale/en/LC_MESSAGES/messages.mo deleted file mode 100644 index a8c96e9449..0000000000 Binary files a/src/ifcbimtester/bimtester/locale/en/LC_MESSAGES/messages.mo and /dev/null differ diff --git a/src/ifcbimtester/bimtester/locale/en/LC_MESSAGES/messages.po b/src/ifcbimtester/bimtester/locale/en/LC_MESSAGES/messages.po index 86be615289..bd6ad28f0b 100644 --- a/src/ifcbimtester/bimtester/locale/en/LC_MESSAGES/messages.po +++ b/src/ifcbimtester/bimtester/locale/en/LC_MESSAGES/messages.po @@ -7,18 +7,162 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-12-17 15:14+0100\n" -"PO-Revision-Date: 2020-12-18 06:12+0100\n" +"POT-Creation-Date: 2020-12-23 11:03+0100\n" +"PO-Revision-Date: 2020-12-18 11:41+0100\n" "Last-Translator: \n" "Language: en\n" "Language-Team: en \n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.6.0\n" -"X-Generator: Poedit 2.2.1\n" + +#: reports.py:153 reports.py:160 +msgid "OpenBIM auditing is a feature of" +msgstr "" + +#: reports.py:155 +msgid "en" +msgstr "" + +#: reports.py:156 +msgid "Success" +msgstr "" + +#: reports.py:157 +msgid "Failure" +msgstr "" + +#: reports.py:158 +msgid "Tests passed" +msgstr "" + +#: reports.py:159 +msgid "Duration" +msgstr "" + +#: reports.py:161 +msgid "and" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:24 +msgid "All {elemcount} elements in the file are {ifc_class}." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:32 +msgid "{falsecount} of {elemcount} element are {ifc_class} elements: {falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:41 +#: features/steps/utils.py:158 +msgid "Error in falsecount, something went wrong." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:80 +msgid "" +"For all {elemcount} {ifc_class} elements at least one of these class " +"attributes {parameter} has no value." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:81 +msgid "" +"For the following {falsecount} out of {elemcount} {ifc_class} elements at" +" least one of these class attributes {parameter} has no value: " +"{falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:82 +#: features/steps/attributes_eleclasses_methods.py:108 +#: features/steps/attributes_eleclasses_methods.py:133 +#: features/steps/attributes_psets_methods.py:31 +#: features/steps/geometric_detail_methods.py:52 +#: features/steps/geometric_detail_methods.py:127 +msgid "There are no {ifc_class} elements in the IFC file." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:106 +msgid "The name of all {elemcount} {elemcount} elements is not set." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:107 +msgid "" +"The name of {falsecount} out of {elemcount} {ifc_class} elements is not " +"set: {falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:131 +msgid "The description of all {elemcount} {elemcount} elements is not set." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:132 +msgid "" +"The description of {falsecount} out of {elemcount} {ifc_class} elements " +"is not set: {falseelems}" +msgstr "" + +#: features/steps/attributes_psets_methods.py:29 +msgid "" +"All {elemcount} {ifc_class} elements are missing the property {parameter}" +" in the pset." +msgstr "" + +#: features/steps/attributes_psets_methods.py:30 +msgid "" +"The following {falsecount} of {elemcount} {ifc_class} elements are " +"missing the property {parameter} in the pset: {falseelems}" +msgstr "" + +#: features/steps/geometric_detail_methods.py:50 +msgid "All {elemcount} {ifc_class} elements are not a {parameter} representation." +msgstr "" + +#: features/steps/geometric_detail_methods.py:51 +msgid "" +"The following {falsecount} of {elemcount} {ifc_class} elements are not a " +"{parameter} representation: {falseelems}" +msgstr "" + +#: features/steps/geometric_detail_methods.py:125 +msgid "The geometry of all {elemcount} {ifc_class} elements have errors." +msgstr "" + +#: features/steps/geometric_detail_methods.py:126 +msgid "" +"The geometry of {falsecount} out of all {elemcount} {ifc_class} elements " +"have errors: {falseelems}" +msgstr "" #: features/steps/ifcdata_methods.py:7 msgid "We expected a schema of {} but instead got {}" -msgstr "We expected a schema of {} but instead got {}" +msgstr "" + +#~ msgid "The geometry of all {} {} elements have errors." +#~ msgstr "" + +#~ msgid "The geometry of {} out of all {} {} elements have errors: {}" +#~ msgstr "" + +#~ msgid "There are no {} elements in the IFC file." +#~ msgstr "" + +#~ msgid "All {} {} elements are not a IfcFacetedBrep representation." +#~ msgstr "" + +#~ msgid "" +#~ "The following {} of {} {} elements" +#~ " are not a IfcFacetedBrep representation:" +#~ " {}" +#~ msgstr "" + +#~ msgid "" +#~ "All {elemcount} {ifc_class} elements are " +#~ "not a IfcFacetedBrep representation." +#~ msgstr "" + +#~ msgid "" +#~ "The following {falsecount} of {elemcount} " +#~ "{ifc_class} elements are not a " +#~ "IfcFacetedBrep representation: {falseelems}" +#~ msgstr "" + diff --git a/src/ifcbimtester/bimtester/locale/fr/LC_MESSAGES/messages.mo b/src/ifcbimtester/bimtester/locale/fr/LC_MESSAGES/messages.mo deleted file mode 100644 index 7f24af08f3..0000000000 Binary files a/src/ifcbimtester/bimtester/locale/fr/LC_MESSAGES/messages.mo and /dev/null differ diff --git a/src/ifcbimtester/bimtester/locale/fr/LC_MESSAGES/messages.po b/src/ifcbimtester/bimtester/locale/fr/LC_MESSAGES/messages.po index 0517d51973..cf837b8ebe 100644 --- a/src/ifcbimtester/bimtester/locale/fr/LC_MESSAGES/messages.po +++ b/src/ifcbimtester/bimtester/locale/fr/LC_MESSAGES/messages.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-12-17 15:14+0100\n" -"PO-Revision-Date: 2020-12-18 06:12+0100\n" +"POT-Creation-Date: 2020-12-23 11:03+0100\n" +"PO-Revision-Date: 2020-12-23 11:21+0100\n" "Last-Translator: \n" "Language: fr\n" "Language-Team: fr \n" @@ -19,6 +19,119 @@ msgstr "" "Generated-By: Babel 2.6.0\n" "X-Generator: Poedit 2.2.1\n" +#: reports.py:153 reports.py:160 +msgid "OpenBIM auditing is a feature of" +msgstr "L'audit OpenBIM auditing est une fonctionnalité de" + +#: reports.py:155 +msgid "en" +msgstr "fr" + +#: reports.py:156 +msgid "Success" +msgstr "Succès" + +#: reports.py:157 +msgid "Failure" +msgstr "Échec" + +#: reports.py:158 +msgid "Tests passed" +msgstr "Tests réussis" + +#: reports.py:159 +msgid "Duration" +msgstr "Durée" + +#: reports.py:161 +msgid "and" +msgstr "et" + +#: features/steps/attributes_eleclasses_methods.py:24 +msgid "All {elemcount} elements in the file are {ifc_class}." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:32 +msgid "{falsecount} of {elemcount} element are {ifc_class} elements: {falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:41 features/steps/utils.py:158 +msgid "Error in falsecount, something went wrong." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:80 +msgid "" +"For all {elemcount} {ifc_class} elements at least one of these class attributes " +"{parameter} has no value." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:81 +msgid "" +"For the following {falsecount} out of {elemcount} {ifc_class} elements at least " +"one of these class attributes {parameter} has no value: {falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:82 +#: features/steps/attributes_eleclasses_methods.py:108 +#: features/steps/attributes_eleclasses_methods.py:133 +#: features/steps/attributes_psets_methods.py:31 +#: features/steps/geometric_detail_methods.py:52 +#: features/steps/geometric_detail_methods.py:127 +msgid "There are no {ifc_class} elements in the IFC file." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:106 +msgid "The name of all {elemcount} {elemcount} elements is not set." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:107 +msgid "" +"The name of {falsecount} out of {elemcount} {ifc_class} elements is not set: " +"{falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:131 +msgid "The description of all {elemcount} {elemcount} elements is not set." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:132 +msgid "" +"The description of {falsecount} out of {elemcount} {ifc_class} elements is not " +"set: {falseelems}" +msgstr "" + +#: features/steps/attributes_psets_methods.py:29 +msgid "" +"All {elemcount} {ifc_class} elements are missing the property {parameter} in " +"the pset." +msgstr "" + +#: features/steps/attributes_psets_methods.py:30 +msgid "" +"The following {falsecount} of {elemcount} {ifc_class} elements are missing the " +"property {parameter} in the pset: {falseelems}" +msgstr "" + +#: features/steps/geometric_detail_methods.py:50 +msgid "All {elemcount} {ifc_class} elements are not a {parameter} representation." +msgstr "" + +#: features/steps/geometric_detail_methods.py:51 +msgid "" +"The following {falsecount} of {elemcount} {ifc_class} elements are not a " +"{parameter} representation: {falseelems}" +msgstr "" + +#: features/steps/geometric_detail_methods.py:125 +msgid "The geometry of all {elemcount} {ifc_class} elements have errors." +msgstr "" + +#: features/steps/geometric_detail_methods.py:126 +msgid "" +"The geometry of {falsecount} out of all {elemcount} {ifc_class} elements have " +"errors: {falseelems}" +msgstr "" + #: features/steps/ifcdata_methods.py:7 msgid "We expected a schema of {} but instead got {}" msgstr "" diff --git a/src/ifcbimtester/bimtester/locale/messages.pot b/src/ifcbimtester/bimtester/locale/messages.pot index ac5ec26174..5d7a7d9221 100644 --- a/src/ifcbimtester/bimtester/locale/messages.pot +++ b/src/ifcbimtester/bimtester/locale/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-12-17 15:14+0100\n" +"POT-Creation-Date: 2020-12-23 11:03+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,6 +17,121 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.6.0\n" +#: reports.py:153 reports.py:160 +msgid "OpenBIM auditing is a feature of" +msgstr "" + +#: reports.py:155 +msgid "en" +msgstr "" + +#: reports.py:156 +msgid "Success" +msgstr "" + +#: reports.py:157 +msgid "Failure" +msgstr "" + +#: reports.py:158 +msgid "Tests passed" +msgstr "" + +#: reports.py:159 +msgid "Duration" +msgstr "" + +#: reports.py:161 +msgid "and" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:24 +msgid "All {elemcount} elements in the file are {ifc_class}." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:32 +msgid "{falsecount} of {elemcount} element are {ifc_class} elements: {falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:41 +#: features/steps/utils.py:158 +msgid "Error in falsecount, something went wrong." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:80 +msgid "" +"For all {elemcount} {ifc_class} elements at least one of these class " +"attributes {parameter} has no value." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:81 +msgid "" +"For the following {falsecount} out of {elemcount} {ifc_class} elements at" +" least one of these class attributes {parameter} has no value: " +"{falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:82 +#: features/steps/attributes_eleclasses_methods.py:108 +#: features/steps/attributes_eleclasses_methods.py:133 +#: features/steps/attributes_psets_methods.py:31 +#: features/steps/geometric_detail_methods.py:52 +#: features/steps/geometric_detail_methods.py:127 +msgid "There are no {ifc_class} elements in the IFC file." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:106 +msgid "The name of all {elemcount} {elemcount} elements is not set." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:107 +msgid "" +"The name of {falsecount} out of {elemcount} {ifc_class} elements is not " +"set: {falseelems}" +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:131 +msgid "The description of all {elemcount} {elemcount} elements is not set." +msgstr "" + +#: features/steps/attributes_eleclasses_methods.py:132 +msgid "" +"The description of {falsecount} out of {elemcount} {ifc_class} elements " +"is not set: {falseelems}" +msgstr "" + +#: features/steps/attributes_psets_methods.py:29 +msgid "" +"All {elemcount} {ifc_class} elements are missing the property {parameter}" +" in the pset." +msgstr "" + +#: features/steps/attributes_psets_methods.py:30 +msgid "" +"The following {falsecount} of {elemcount} {ifc_class} elements are " +"missing the property {parameter} in the pset: {falseelems}" +msgstr "" + +#: features/steps/geometric_detail_methods.py:50 +msgid "All {elemcount} {ifc_class} elements are not a {parameter} representation." +msgstr "" + +#: features/steps/geometric_detail_methods.py:51 +msgid "" +"The following {falsecount} of {elemcount} {ifc_class} elements are not a " +"{parameter} representation: {falseelems}" +msgstr "" + +#: features/steps/geometric_detail_methods.py:125 +msgid "The geometry of all {elemcount} {ifc_class} elements have errors." +msgstr "" + +#: features/steps/geometric_detail_methods.py:126 +msgid "" +"The geometry of {falsecount} out of all {elemcount} {ifc_class} elements " +"have errors: {falseelems}" +msgstr "" + #: features/steps/ifcdata_methods.py:7 msgid "We expected a schema of {} but instead got {}" msgstr "" diff --git a/src/ifcbimtester/bimtester/reports.py b/src/ifcbimtester/bimtester/reports.py index 180cf7147e..d0672b8f9b 100644 --- a/src/ifcbimtester/bimtester/reports.py +++ b/src/ifcbimtester/bimtester/reports.py @@ -1,16 +1,32 @@ import datetime +import gettext # noqa import json import os import pystache +from .features.steps.utils import switch_locale -def generate_report(adir=".", use_report_folder=True, report_file_name="", html_template_file_path=""): - #print("# Generating HTML reports now.") + +def generate_report( + adir=".", + use_report_folder=True, + report_file_name="", + html_template_file_path="" +): + + # print("# Generating HTML reports now.") + + # get locale path + localedir = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "locale" + ) # get html template path report_template_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), - "features/" + "resources", + "reports" ) if html_template_file_path: @@ -24,9 +40,9 @@ def generate_report(adir=".", use_report_folder=True, report_file_name="", html_ return print("No report directory was found.") if report_file_name: - report_path = os.path.join(report_dir, report_file_name) + report_path = os.path.join(report_dir, report_file_name) else: - report_path = os.path.join(report_dir, "report.json") + report_path = os.path.join(report_dir, "report.json") # print(report_path) if not os.path.exists(report_path): return print("No report data was found.") @@ -113,29 +129,15 @@ def generate_report(adir=".", use_report_folder=True, report_file_name="", html_ # workaround for retrieving the feature file language print(feature["keyword"]) if feature["keyword"] == "Feature": - strings_file_report = os.path.join( - report_template_path, - "strings_template_en.json" - ) + switch_locale(localedir, "en") elif feature["keyword"] == "Funktionalität": - strings_file_report = os.path.join( - report_template_path, - "strings_template_de.json" - ) + switch_locale(localedir, "de") elif feature["keyword"] == "Fonctionnalité": - strings_file_report = os.path.join( - report_template_path, - "strings_template_fr.json" - ) + switch_locale(localedir, "fr") else: # standard English - strings_file_report = os.path.join( - report_template_path, - "strings_template_en.json" - ) - strings_report = json.loads( - open(strings_file_report, encoding="utf8").read() - ) + switch_locale(localedir, "en") + strings_report = get_html_template_strings() # print(strings_report) data.update(strings_report) # print(data) @@ -145,3 +147,16 @@ def generate_report(adir=".", use_report_folder=True, report_file_name="", html_ with open(html_report, "w", encoding="utf8") as out: with open(html_tmpl, encoding="utf8") as template: out.write(pystache.render(template.read(), data)) + + +def get_html_template_strings(): + print(_("OpenBIM auditing is a feature of")) + return { + "tr_lang": _("en"), + "tr_success": _("Success"), + "tr_failure": _("Failure"), + "tr_tests_passed": _("Tests passed"), + "tr_duration": _("Duration"), + "tr_auditing": _("OpenBIM auditing is a feature of"), + "tr_and": _("and") + } diff --git a/src/ifcbimtester/bimtester/features/template.html b/src/ifcbimtester/bimtester/resources/reports/template.html similarity index 100% rename from src/ifcbimtester/bimtester/features/template.html rename to src/ifcbimtester/bimtester/resources/reports/template.html diff --git a/src/ifcblenderexport/blenderbim/bim/__init__.py b/src/ifcblenderexport/blenderbim/bim/__init__.py index d2b38f8258..24cc944e8a 100644 --- a/src/ifcblenderexport/blenderbim/bim/__init__.py +++ b/src/ifcblenderexport/blenderbim/bim/__init__.py @@ -5,40 +5,18 @@ bpy = sys.modules.get("bpy") if bpy is not None: import bpy + import blenderbim.bim.module.covetool as module_covetool + import blenderbim.bim.module.model as module_model + import blenderbim.bim.module.bcf as module_bcf from . import ui, prop, operator - from .module.covetool import prop as covetool_prop - from .module.covetool import ui as covetool_ui - from .module.covetool import operator as covetool_operator - - from .module.model import grid as model_grid - from .module.model import wall as model_wall - from .module.model import stair as model_stair - from .module.model import door as model_door - from .module.model import window as model_window - from .module.model import slab as model_slab - from .module.model import opening as model_opening - - classes = ( + classes = [ operator.ReassignClass, operator.AssignClass, operator.UnassignClass, operator.SelectClass, operator.SelectType, - operator.NewBcfProject, - operator.LoadBcfProject, - operator.LoadBcfTopics, - operator.LoadBcfComments, - operator.EditBcfProjectName, - operator.EditBcfAuthor, - operator.EditBcfTopicName, - operator.EditBcfTopic, - operator.SaveBcfProject, - operator.AddBcfTopic, - operator.ViewBcfTopic, - operator.ActivateBcfViewpoint, operator.OpenUri, - operator.OpenBcfReferenceLink, operator.SelectFeaturesDir, operator.SelectDiffJsonFile, operator.SelectDiffNewFile, @@ -137,6 +115,7 @@ if bpy is not None: operator.AddDrawingToSheet, operator.CreateSheets, operator.OpenView, + operator.OpenViewCamera, operator.ActivateView, operator.ExecuteIfcDiff, operator.VisualiseDiff, @@ -238,6 +217,8 @@ if bpy is not None: operator.ExecuteBlenderClash, operator.CleanWireframes, operator.LinkIfc, + operator.SnapSpacesTogether, + operator.CopyGrid, prop.StrProperty, prop.Attribute, prop.MaterialLayer, @@ -264,15 +245,11 @@ if bpy is not None: prop.Schedule, prop.DrawingStyle, prop.Sheet, - prop.BcfBimSnippet, - prop.BcfDocumentReference, - prop.BcfComment, - prop.BcfTopic, prop.Subcontext, + prop.Representation, prop.PresentationLayer, prop.BIMProperties, prop.BIMDebugProperties, - prop.BCFProperties, prop.DocProperties, prop.BIMLibrary, prop.MapConversion, @@ -301,9 +278,6 @@ if bpy is not None: ui.BIM_PT_search, ui.BIM_PT_ifccsv, ui.BIM_PT_ifcclash, - ui.BIM_PT_bcf, - ui.BIM_PT_bcf_metadata, - ui.BIM_PT_bcf_comments, ui.BIM_PT_owner, ui.BIM_PT_people, ui.BIM_PT_organisations, @@ -337,6 +311,7 @@ if bpy is not None: ui.BIM_PT_clash_manager, ui.BIM_PT_misc_utilities, ui.BIM_UL_generic, + ui.BIM_UL_drawinglist, ui.BIM_UL_clash_sets, ui.BIM_UL_smart_groups, ui.BIM_UL_constraints, @@ -345,22 +320,11 @@ if bpy is not None: ui.BIM_UL_topics, ui.BIM_UL_classifications, ui.BIM_ADDON_preferences, - covetool_prop.CoveToolProject, - covetool_prop.CoveToolSimpleAnalysis, - covetool_prop.CoveToolProperties, - covetool_ui.BIM_UL_covetool_projects, - covetool_ui.BIM_PT_covetool, - covetool_operator.Login, - covetool_operator.RunSimpleAnalysis, - covetool_operator.RunAnalysis, - model_grid.BIM_OT_add_object, - model_wall.BIM_OT_add_object, - model_stair.BIM_OT_add_object, - model_door.BIM_OT_add_object, - model_window.BIM_OT_add_object, - model_slab.BIM_OT_add_object, - model_opening.BIM_OT_add_object, - ) + ] + + classes.extend(module_bcf.classes) + classes.extend(module_covetool.classes) + classes.extend(module_model.classes) def menu_func_export(self, context): self.layout.operator(operator.ExportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcjson)") @@ -381,7 +345,6 @@ if bpy is not None: bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) bpy.types.Scene.BIMDebugProperties = bpy.props.PointerProperty(type=prop.BIMDebugProperties) - bpy.types.Scene.BCFProperties = bpy.props.PointerProperty(type=prop.BCFProperties) bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties) bpy.types.Scene.BIMLibrary = bpy.props.PointerProperty(type=prop.BIMLibrary) bpy.types.Scene.MapConversion = bpy.props.PointerProperty(type=prop.MapConversion) @@ -392,15 +355,10 @@ if bpy is not None: bpy.types.Mesh.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties) bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties) - bpy.types.Scene.CoveToolProperties = bpy.props.PointerProperty(type=covetool_prop.CoveToolProperties) bpy.types.SCENE_PT_unit.append(ui.ifc_units) - bpy.types.VIEW3D_MT_mesh_add.append(model_grid.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.append(model_wall.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.append(model_stair.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.append(model_door.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.append(model_window.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.append(model_slab.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.append(model_opening.add_object_button) + module_bcf.register() + module_covetool.register() + module_model.register() bpy.app.handlers.depsgraph_update_pre.append(operator.depsgraph_update_pre_handler) def unregister(): @@ -411,7 +369,6 @@ if bpy is not None: bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) del bpy.types.Scene.BIMProperties del bpy.types.Scene.BIMDebugProperties - del bpy.types.Scene.BCFProperties del bpy.types.Scene.DocProperties del bpy.types.Scene.MapConversion del bpy.types.Scene.TargetCRS @@ -422,11 +379,7 @@ if bpy is not None: del bpy.types.Camera.BIMCameraProperties del bpy.types.TextCurve.BIMTextProperties bpy.types.SCENE_PT_unit.remove(ui.ifc_units) - bpy.types.VIEW3D_MT_mesh_add.remove(model_grid.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.remove(model_wall.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.remove(model_stair.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.remove(model_door.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.remove(model_window.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.remove(model_slab.add_object_button) - bpy.types.VIEW3D_MT_mesh_add.remove(model_opening.add_object_button) + module_model.unregister() + module_covetool.unregister() + module_bcf.unregister() bpy.app.handlers.depsgraph_update_pre.remove(operator.depsgraph_update_pre_handler) diff --git a/src/ifcblenderexport/blenderbim/bim/annotation.py b/src/ifcblenderexport/blenderbim/bim/annotation.py index 5376f37a39..381628877a 100644 --- a/src/ifcblenderexport/blenderbim/bim/annotation.py +++ b/src/ifcblenderexport/blenderbim/bim/annotation.py @@ -22,7 +22,7 @@ class Annotator: obj = bpy.data.objects.new("IfcAnnotation/Text", curve) obj.matrix_world = bpy.context.scene.camera.matrix_world if related_element is None: - location, co2 = Annotator.get_placeholder_coords() + location, _, _, _ = Annotator.get_placeholder_coords() else: obj.data.BIMTextProperties.related_element = related_element location = related_element.location @@ -66,7 +66,7 @@ class Annotator: @staticmethod def add_line_to_annotation(obj, co1=None, co2=None): if co1 is None: - co1, co2 = Annotator.get_placeholder_coords() + co1, co2, _, _ = Annotator.get_placeholder_coords() co1 = obj.matrix_world.inverted() @ co1 co2 = obj.matrix_world.inverted() @ co2 if isinstance(obj.data, bpy.types.Mesh): @@ -82,6 +82,55 @@ class Annotator: polyline.points[-1].co = list(co2) + [1] return obj + @staticmethod + def add_plane_to_annotation(obj): + co1, co2, co3, co4 = Annotator.get_placeholder_coords() + co1 = obj.matrix_world.inverted() @ co1 # bot left + co2 = obj.matrix_world.inverted() @ co2 # top left + co3 = obj.matrix_world.inverted() @ co3 # bot right + co4 = obj.matrix_world.inverted() @ co4 # top right + + obj.data.vertices.add(4) + v1 = obj.data.vertices[-4] + v2 = obj.data.vertices[-3] + v3 = obj.data.vertices[-2] + v4 = obj.data.vertices[-1] + v1.co = co4 + v2.co = co2 + v3.co = co1 + v4.co = co3 + + obj.data.edges.add(4) + e1 = obj.data.edges[-4] + e2 = obj.data.edges[-3] + e3 = obj.data.edges[-2] + e4 = obj.data.edges[-1] + e1.vertices = (v1.index, v2.index) + e2.vertices = (v2.index, v3.index) + e3.vertices = (v3.index, v4.index) + e4.vertices = (v4.index, v1.index) + + obj.data.loops.add(4) + l1 = obj.data.loops[-4] + l2 = obj.data.loops[-3] + l3 = obj.data.loops[-2] + l4 = obj.data.loops[-1] + l1.vertex_index = v1.index + l1.edge_index = e1.index + l2.vertex_index = v2.index + l2.edge_index = e2.index + l3.vertex_index = v3.index + l3.edge_index = e3.index + l4.vertex_index = v4.index + l4.edge_index = e4.index + + obj.data.polygons.add(1) + p1 = obj.data.polygons[-1] + p1.vertices = (v1.index, v2.index, v3.index, v4.index) + p1.loop_start = l1.index + p1.loop_total = 4 + return obj + @staticmethod def get_annotation_obj(name, data_type): collection = bpy.context.scene.camera.users_collection[0] @@ -111,4 +160,8 @@ class Annotator: else: y = camera.data.ortho_scale / 4 y_offset = camera.matrix_world.to_quaternion() @ Vector((0, y, 0)) - return (camera.location + z_offset, camera.location + z_offset + y_offset) + x_offset = camera.matrix_world.to_quaternion() @ Vector((y/2, 0, 0)) + return (camera.location + z_offset, + camera.location + z_offset + y_offset, + camera.location + z_offset + x_offset, + camera.location + z_offset + x_offset + y_offset) \ No newline at end of file diff --git a/src/ifcblenderexport/blenderbim/bim/decoration.py b/src/ifcblenderexport/blenderbim/bim/decoration.py index 3bfedc405f..41adc031f2 100644 --- a/src/ifcblenderexport/blenderbim/bim/decoration.py +++ b/src/ifcblenderexport/blenderbim/bim/decoration.py @@ -4,8 +4,9 @@ from functools import reduce from itertools import chain from bpy.types import SpaceView3D -from mathutils import Vector +from mathutils import Vector, Matrix import bpy +import bmesh import blf from bpy_extras.view3d_utils import location_3d_to_region_2d import gpu @@ -14,12 +15,104 @@ from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertForma from gpu_extras.batch import batch_for_shader -class ViewDecorator(object): +class BaseDecorator(): + # base name of objects to decorate + basename = "IfcAnnotation/Something" + + DEF_GLSL = """ + #define PI 3.141592653589793 + #define COLOR vec4({color[0]}, {color[1]}, {color[2]}, {color[3]}) + #define MAX_POINTS 64 + #define CIRCLE_SEGS 24 + """ + + LIB_GLSL = """ + #define matCLIP2WIN() vec4(winsize.x/2, winsize.y/2, 1, 1) + #define matWIN2CLIP() vec4(2/winsize.x, 2/winsize.y, 1, 1) + #define CLIP2WIN(v) (clip2win * v / v.w) + #define WIN2CLIP(v) (win2clip * v * v.w) + + void arrow_head(in vec4 dir, in float size, in float angle, out vec4 head[3]) { + vec4 nose = dir * size; + float c = cos(angle), s = sin(angle); + head[0] = nose; + head[1] = vec4(mat2(c, -s, +s, c) * nose.xy, 0, 0); + head[2] = vec4(mat2(c, +s, -s, c) * nose.xy, 0, 0); + } + + void circle_head(in float size, out vec4 head[CIRCLE_SEGS]) { + float angle_d = PI * 2 / CIRCLE_SEGS; + for(int i = 0; i 0: + bpy.context.scene.BCFProperties.topics.remove(0) + index = 0 + for topic_guid in bcfxml.topics.keys(): + new = bpy.context.scene.BCFProperties.topics.add() + bpy.ops.bim.load_bcf_topic(topic_guid = topic_guid, topic_index = index) + index += 1 + return {"FINISHED"} + + +class LoadBcfTopic(bpy.types.Operator): + bl_idname = "bim.load_bcf_topic" + bl_label = "Load BCF Topics" + topic_guid: bpy.props.StringProperty() + topic_index: bpy.props.IntProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + topic = bcfxml.get_topic(self.topic_guid) + new = bpy.context.scene.BCFProperties.topics[self.topic_index] + data_map = { + "name": topic.guid, + "title": topic.title, + "type": topic.topic_type, + "status": topic.topic_status, + "priority": topic.priority, + "stage": topic.stage, + "creation_date": topic.creation_date, + "creation_author": topic.creation_author, + "modified_date": topic.modified_date, + "modified_author": topic.modified_author, + "assigned_to": topic.assigned_to, + "due_date": topic.due_date, + "description": topic.description + } + for key, value in data_map.items(): + if value is not None: + setattr(new, key, str(value)) + while len(new.reference_links) > 0: + new.reference_links.remove(0) + for reference_link in topic.reference_links: + new2 = new.reference_links.add() + new2.name = reference_link + while len(new.labels) > 0: + new.labels.remove(0) + for label in topic.labels: + new2 = new.labels.add() + new2.name = label + if topic.bim_snippet: + data_map = { + "type": topic.bim_snippet.snippet_type, + "is_external": topic.bim_snippet.is_external, + "reference": topic.bim_snippet.reference, + "schema": topic.bim_snippet.reference_schema + } + for key, value in data_map.items(): + if value is not None: + setattr(new.bim_snippet, key, value) + while len(new.document_references) > 0: + new.document_references.remove(0) + for doc in topic.document_references: + new2 = new.document_references.add() + data_map = { + "reference": doc.referenced_document, + "description": doc.description, + "guid": doc.guid, + "is_external": doc.is_external + } + for key, value in data_map.items(): + if value is not None: + setattr(new2, key, value) + while len(new.related_topics) > 0: + new.related_topics.remove(0) + for related_topic in topic.related_topics: + new2 = new.related_topics.add() + new2.name = related_topic.guid + bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) + return {"FINISHED"} + + +class LoadBcfComments(bpy.types.Operator): + bl_idname = "bim.load_bcf_comments" + bl_label = "Load BCF Comments" + topic_guid: bpy.props.StringProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + bcfxml.get_comments(self.topic_guid) + blender_topic = bpy.context.scene.BCFProperties.topics.get(self.topic_guid) + while len(blender_topic.comments) > 0: + blender_topic.comments.remove(0) + for comment in bcfxml.topics[self.topic_guid].comments.values(): + new = blender_topic.comments.add() + data_map = { + "name": comment.guid, + "comment": comment.comment, + "viewpoint": comment.viewpoint.guid if comment.viewpoint else None, + "date": comment.date, + "author": comment.author, + "modified_date": comment.modified_date, + "modified_author": comment.modified_author, + } + for key, value in data_map.items(): + if value is not None: + setattr(new, key, str(value)) + return {"FINISHED"} + + +class EditBcfProjectName(bpy.types.Operator): + bl_idname = "bim.edit_bcf_project_name" + bl_label = "Edit BCF Project Name" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + bcfxml.project.name = bpy.context.scene.BCFProperties.name + bcfxml.edit_project() + return {"FINISHED"} + + +class EditBcfAuthor(bpy.types.Operator): + bl_idname = "bim.edit_bcf_author" + bl_label = "Edit BCF Author" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + bcfxml.author = bpy.context.scene.BCFProperties.author + return {"FINISHED"} + + +class EditBcfTopicName(bpy.types.Operator): + bl_idname = "bim.edit_bcf_topic_name" + bl_label = "Edit BCF Topic Name" + + def execute(self, context): + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + bcfxml = bcfstore.BcfStore.get_bcfxml() + topic = bcfxml.topics[blender_topic.name] + topic.title = blender_topic.title + bcfxml.edit_topic(topic) + return {"FINISHED"} + + +class EditBcfTopic(bpy.types.Operator): + bl_idname = "bim.edit_bcf_topic" + bl_label = "Edit BCF Topic" + + def execute(self, context): + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + bcfxml = bcfstore.BcfStore.get_bcfxml() + + topic = bcfxml.topics[blender_topic.name] + topic.title = blender_topic.title or None + topic.priority = blender_topic.priority or None + topic.due_date = blender_topic.due_date or None + topic.assigned_to = blender_topic.assigned_to or None + topic.stage = blender_topic.stage or None + topic.description = blender_topic.description or None + topic.topic_status = blender_topic.status or None + topic.topic_type = blender_topic.type or None + + bcfxml.edit_topic(topic) + props.active_topic_index = props.active_topic_index # Refreshes the BCF Topic + return {"FINISHED"} + + +class SaveBcfProject(bpy.types.Operator): + bl_idname = "bim.save_bcf_project" + bl_label = "Save BCF Project" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + bcfxml.save_project(self.filepath) + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class AddBcfTopic(bpy.types.Operator): + bl_idname = "bim.add_bcf_topic" + bl_label = "Add BCF Topic" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + bcfxml.add_topic() + new = bpy.context.scene.BCFProperties.topics.add() + new.name = "New Topic" + bpy.ops.bim.load_bcf_topics() + return {"FINISHED"} + + +class AddBcfBimSnippet(bpy.types.Operator): + bl_idname = "bim.add_bcf_bim_snippet" + bl_label = "Add BCF BIM Snippet" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + bim_snippet = bcf.data.BimSnippet() + bim_snippet.reference = blender_topic.bim_snippet_reference + bim_snippet.reference_schema = blender_topic.bim_snippet_schema + bim_snippet.snippet_type = blender_topic.bim_snippet_type + bcfxml.add_bim_snippet(topic, bim_snippet) + bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + return {"FINISHED"} + + +class AddBcfRelatedTopic(bpy.types.Operator): + bl_idname = "bim.add_bcf_related_topic" + bl_label = "Add BCF Related Topic" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + related_topic = None + for topic in bcfxml.topics.values(): + if topic.title == blender_topic.related_topic: + related_topic = bcf.data.RelatedTopic() + related_topic.guid = topic.guid + break + if not related_topic: + return {"FINISHED"} + topic = bcfxml.topics[blender_topic.name] + topic.related_topics.append(related_topic) + bcfxml.edit_topic(topic) + bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + return {"FINISHED"} + + +class AddBcfHeaderFile(bpy.types.Operator): + bl_idname = "bim.add_bcf_header_file" + bl_label = "Add BCF Header File" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + header_file = bcf.data.HeaderFile() + header_file.reference = blender_topic.file_reference + if not os.path.exists(header_file.reference): + header_file.filename = header_file.reference + if len(blender_topic.file_ifc_project) == 22: + header_file.ifc_project = blender_topic.file_ifc_project + if len(blender_topic.file_ifc_spatial_structure_element) == 22: + header_file.ifc_spatial_structure_element = blender_topic.file_ifc_spatial_structure_element + bcfxml.add_file(topic, header_file) + props.active_topic_index = props.active_topic_index # refreshes the BCF Topic + return {"FINISHED"} + + +class ViewBcfTopic(bpy.types.Operator): + bl_idname = "bim.view_bcf_topic" + bl_label = "Get BCF Topic" + topic_guid: bpy.props.StringProperty() + + def execute(self, context): + for index, topic in enumerate(bpy.context.scene.BCFProperties.topics): + if topic.guid.lower() == self.topic_guid.lower(): + bpy.context.scene.BCFProperties.active_topic_index = index + return {"FINISHED"} + + +class AddBcfViewpoint(bpy.types.Operator): + bl_idname = "bim.add_bcf_viewpoint" + bl_label = "Add BCF Viewpoint" + + def execute(self, context): + if not bpy.context.scene.camera: + return {"FINISHED"} + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + viewpoint = bcf.data.Viewpoint() + + if bpy.context.scene.camera.data.type == "ORTHO": + camera = bcf.data.OrthogonalCamera() + camera.view_to_world_scale = bpy.context.scene.camera.data.ortho_scale + viewpoint.orthogonal_camera = camera + elif bpy.context.scene.camera.data.type == "PERSP": + camera = bcf.data.PerspectiveCamera() + camera.field_of_view = degrees(bpy.context.scene.camera.data.angle) + viewpoint.perspective_camera = camera + camera.camera_view_point.x = bpy.context.scene.camera.location.x + camera.camera_view_point.y = bpy.context.scene.camera.location.y + camera.camera_view_point.z = bpy.context.scene.camera.location.z + direction = bpy.context.scene.camera.matrix_world.to_quaternion() @ Vector((0.0, 0.0, -1.0)) + camera.camera_direction.x = direction.x + camera.camera_direction.y = direction.y + camera.camera_direction.z = direction.z + up = bpy.context.scene.camera.matrix_world.to_quaternion() @ Vector((0.0, 1.0, 0.0)) + camera.camera_up_vector.x = up.x + camera.camera_up_vector.y = up.y + camera.camera_up_vector.z = up.z + + old_file_format = bpy.context.scene.render.image_settings.file_format + bpy.context.scene.render.image_settings.file_format = "PNG" + old_filepath = bpy.context.scene.render.filepath + bpy.context.scene.render.filepath = os.path.join(bpy.context.scene.BIMProperties.data_dir, "snapshot.png") + bpy.ops.render.opengl(write_still=True) + viewpoint.snapshot = bpy.context.scene.render.filepath + + bcfxml.add_viewpoint(topic, viewpoint) + bpy.context.scene.render.filepath = old_filepath + bpy.context.scene.render.image_settings.file_format = old_file_format + props.active_topic_index = props.active_topic_index # refreshes the BCF Topic + return {"FINISHED"} + + +class RemoveBcfViewpoint(bpy.types.Operator): + bl_idname = "bim.remove_bcf_viewpoint" + bl_label = "Remove BCF Viewpoint" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + viewpoint_guid = blender_topic.viewpoints + topic = bcfxml.topics[blender_topic.name] + bcfxml.delete_viewpoint(viewpoint_guid, topic) + props.active_topic_index = props.active_topic_index # Refreshes the BCF Topic + return {"FINISHED"} + + +class RemoveBcfFile(bpy.types.Operator): + bl_idname = "bim.remove_bcf_file" + bl_label = "Remove BCF File" + index: bpy.props.IntProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + bcfxml.delete_file(topic, self.index) + props.active_topic_index = props.active_topic_index # Refreshes the BCF Topic + return {"FINISHED"} + + +class AddBcfReferenceLink(bpy.types.Operator): + bl_idname = "bim.add_bcf_reference_link" + bl_label = "Add BCF Reference Link" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + if not blender_topic.reference_link: + return {"FINISHED"} + topic.reference_links.append(blender_topic.reference_link) + bcfxml.edit_topic(topic) + bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + blender_topic.reference_link = "" + return {"FINISHED"} + + +class AddBcfDocumentReference(bpy.types.Operator): + bl_idname = "bim.add_bcf_document_reference" + bl_label = "Add BCF Document Reference" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + if not blender_topic.document_reference: + return {"FINISHED"} + document_reference = bcf.data.DocumentReference() + document_reference.referenced_document = blender_topic.document_reference + document_reference.description = blender_topic.document_reference_description or None + bcfxml.add_document_reference(topic, document_reference) + bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + blender_topic.document_reference = "" + blender_topic.document_reference_description = "" + return {"FINISHED"} + + +class AddBcfLabel(bpy.types.Operator): + bl_idname = "bim.add_bcf_label" + bl_label = "Add BCF Label" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + if not blender_topic.label: + return {"FINISHED"} + new = blender_topic.labels.add() + new.name = blender_topic.label + topic.labels.append(blender_topic.label) + bcfxml.edit_topic(topic) + blender_topic.label = "" + return {"FINISHED"} + + +class EditBcfReferenceLinks(bpy.types.Operator): + bl_idname = "bim.edit_bcf_reference_links" + bl_label = "Edit BCF Reference Link" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + for index, reference_link in enumerate(topic.reference_links): + if reference_link == blender_topic.reference_links[index].name: + continue + topic.reference_links[index] = blender_topic.reference_links[index].name + bcfxml.edit_topic(topic) + return {"FINISHED"} + + +class EditBcfLabels(bpy.types.Operator): + bl_idname = "bim.edit_bcf_labels" + bl_label = "Edit BCF Labels" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + for index, label in enumerate(blender_topic.labels): + if label.name == topic.labels[index]: + continue + topic.labels[index] = blender_topic.labels[index].name + bcfxml.edit_topic(topic) + return {"FINISHED"} + + +class RemoveBcfReferenceLink(bpy.types.Operator): + bl_idname = "bim.remove_bcf_reference_link" + bl_label = "Remove BCF Reference Link" + index: bpy.props.IntProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + del topic.reference_links[self.index] + bcfxml.edit_topic(topic) + blender_topic.reference_links.remove(self.index) + return {"FINISHED"} + + +class RemoveBcfLabel(bpy.types.Operator): + bl_idname = "bim.remove_bcf_label" + bl_label = "Remove BCF Label" + index: bpy.props.IntProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + del topic.labels[self.index] + bcfxml.edit_topic(topic) + blender_topic.labels.remove(self.index) + return {"FINISHED"} + + +class RemoveBcfBimSnippet(bpy.types.Operator): + bl_idname = "bim.remove_bcf_bim_snippet" + bl_label = "Remove BCF BIM Snippet" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + bcfxml.delete_bim_snippet(topic) + blender_topic.bim_snippet.schema = "" + blender_topic.bim_snippet.reference = "" + blender_topic.bim_snippet.type = "" + return {"FINISHED"} + + +class RemoveBcfDocumentReference(bpy.types.Operator): + bl_idname = "bim.remove_bcf_document_reference" + bl_label = "Remove BCF Document Reference" + index: bpy.props.IntProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + bcfxml.delete_document_reference(topic, self.index) + bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + return {"FINISHED"} + + +class RemoveBcfRelatedTopic(bpy.types.Operator): + bl_idname = "bim.remove_bcf_related_topic" + bl_label = "Remove BCF Related Topic" + index: bpy.props.IntProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + del topic.related_topics[self.index] + bcfxml.edit_topic(topic) + bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + return {"FINISHED"} + + +class RemoveBcfComment(bpy.types.Operator): + bl_idname = "bim.remove_bcf_comment" + bl_label = "Remove BCF Comment" + comment_guid: bpy.props.StringProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + bcfxml.delete_comment(self.comment_guid, topic) + bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) + return {"FINISHED"} + + +class EditBcfComment(bpy.types.Operator): + bl_idname = "bim.edit_bcf_comment" + bl_label = "Edit BCF Comment" + comment_guid: bpy.props.StringProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + blender_comment = blender_topic.comments.get(self.comment_guid) + topic = bcfxml.topics[blender_topic.name] + comment = topic.comments[self.comment_guid] + comment.comment = blender_comment.comment + bcfxml.edit_comment(comment, topic) + bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) + return {"FINISHED"} + + +class AddBcfComment(bpy.types.Operator): + bl_idname = "bim.add_bcf_comment" + bl_label = "Add BCF Comment" + comment_guid: bpy.props.StringProperty() + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + if not blender_topic.comment: + return {"FINISHED"} + comment = bcf.data.Comment() + comment.comment = blender_topic.comment + if blender_topic.has_related_viewpoint and blender_topic.viewpoints: + comment.viewpoint = bcf.data.Viewpoint() + comment.viewpoint.guid = blender_topic.viewpoints + bcfxml.add_comment(topic, comment) + bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) + print(bcfxml.filepath) + return {"FINISHED"} + + +class ActivateBcfViewpoint(bpy.types.Operator): + bl_idname = "bim.activate_bcf_viewpoint" + bl_label = "Activate BCF Viewpoint" + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = bpy.context.scene.BCFProperties + blender_topic = props.topics[props.active_topic_index] + topic = bcfxml.topics[blender_topic.name] + if not topic.viewpoints: + return {"FINISHED"} + + viewpoint_guid = blender_topic.viewpoints + viewpoint = topic.viewpoints[viewpoint_guid] + obj = bpy.data.objects.get("Viewpoint") + if not obj: + obj = bpy.data.objects.new("Viewpoint", bpy.data.cameras.new("Viewpoint")) + bpy.context.scene.collection.objects.link(obj) + bpy.context.scene.camera = obj + + cam_width = bpy.context.scene.render.resolution_x + cam_height = bpy.context.scene.render.resolution_y + cam_aspect = cam_width / cam_height + + if viewpoint.snapshot: + obj.data.show_background_images = True + while len(obj.data.background_images) > 0: + obj.data.background_images.remove(obj.data.background_images[0]) + background = obj.data.background_images.new() + background.image = bpy.data.images.load( + os.path.join(bcfxml.filepath, topic.guid, viewpoint.snapshot) + ) + src_width = background.image.size[0] + src_height = background.image.size[1] + src_aspect = src_width / src_height + + if src_aspect > cam_aspect: + background.frame_method = "FIT" + else: + background.frame_method = "CROP" + background.display_depth = "FRONT" + area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area.spaces[0].region_3d.view_perspective = "CAMERA" + + if viewpoint.orthogonal_camera: + camera = viewpoint.orthogonal_camera + obj.data.type = "ORTHO" + obj.data.ortho_scale = viewpoint.orthogonal_camera.view_to_world_scale + elif viewpoint.perspective_camera: + camera = viewpoint.perspective_camera + obj.data.type = "PERSP" + if cam_aspect >= 1: + obj.data.angle = radians(camera.field_of_view) + else: + # https://blender.stackexchange.com/questions/23431/how-to-set-camera-horizontal-and-vertical-fov + obj.data.angle = 2 * atan((0.5 * cam_height) / (0.5 * cam_width / tan(radians(camera.field_of_view) / 2))) + + self.set_viewpoint_components(viewpoint) + + gp = bpy.data.grease_pencils.get("BCF") + if gp: + bpy.data.grease_pencils.remove(gp) + if viewpoint.lines: + self.draw_lines(viewpoint) + + self.delete_clipping_planes() + if viewpoint.clipping_planes: + self.create_clipping_planes(viewpoint) + + self.delete_bitmaps() + if viewpoint.bitmaps: + self.create_bitmaps(bcfxml, viewpoint, topic) + + z_axis = Vector((-camera.camera_direction.x, -camera.camera_direction.y, -camera.camera_direction.z)).normalized() + y_axis = Vector((camera.camera_up_vector.x, camera.camera_up_vector.y, camera.camera_up_vector.z)).normalized() + x_axis = y_axis.cross(z_axis).normalized() + rotation = Matrix((x_axis, y_axis, z_axis)) + rotation.invert() + location = Vector((camera.camera_view_point.x, camera.camera_view_point.y, camera.camera_view_point.z)) + obj.matrix_world = rotation.to_4x4() + obj.location = location + return {"FINISHED"} + + def set_viewpoint_components(self, viewpoint): + if not viewpoint.components: + return + selected_global_ids = [s.ifc_guid for s in viewpoint.components.selection] + exception_global_ids = [v.ifc_guid for v in viewpoint.components.visibility.exceptions] + global_id_colours = {} + for coloring in viewpoint.components.coloring: + for component in coloring.components: + global_id_colours.setdefault(component.ifc_guid, coloring.color) + + for obj in bpy.data.objects: + global_id = obj.BIMObjectProperties.attributes.get("GlobalId") + if not global_id: + continue + global_id = global_id.string_value + is_visible = viewpoint.components.visibility.default_visibility + if global_id in exception_global_ids: + is_visible = not is_visible + if not is_visible: + obj.hide_set(True) + continue + if "IfcSpace" in obj.name: + is_visible = viewpoint.components.viewSetuphints.spacesVisible + elif "IfcOpeningElement" in obj.name: + is_visible = viewpoint.components.viewSetuphints.openingsVisible + obj.hide_set(not is_visible) + if not is_visible: + continue + obj.select_set(global_id in selected_global_ids) + if global_id in global_id_colours: + obj.color = self.hex_to_rgb(global_id_colours[global_id]) + + def draw_lines(self, viewpoint): + gp = bpy.data.grease_pencils.new("BCF") + scene = bpy.context.scene + scene.grease_pencil = gp + scene.frame_set(1) + layer = gp.layers.new("BCF Annotation", set_active=True) + layer.thickness = 3 + layer.color = (1, 0, 0) + frame = layer.frames.new(1) + stroke = frame.strokes.new() + stroke.display_mode = "3DSPACE" + stroke.points.add(len(viewpoint.lines) * 2) + coords = [] + for l in viewpoint.lines: + coords.extend([l.start_point.x, l.start_point.y, l.start_point.z, l.end_point.x, l.end_point.y, l.end_point.z]) + stroke.points.foreach_set("co", coords) + + def create_clipping_planes(self, viewpoint): + n = 0 + for plane in viewpoint.clipping_planes: + bpy.ops.bim.add_section_plane() + if n == 0: + obj = bpy.data.objects["Section"] + else: + obj = bpy.data.objects["Section.{:03d}".format(n)] + obj.location = (plane.location.x, plane.location.y, plane.location.z) + obj.rotation_mode = "QUATERNION" + obj.rotation_quaternion = Vector((plane.direction.x, plane.direction.y, plane.direction.z)).to_track_quat( + "Z", "Y" + ) + n += 1 + + def delete_clipping_planes(self): + collection = bpy.data.collections.get("Sections") + if not collection: + return + for section in collection.objects: + bpy.context.view_layer.objects.active = section + bpy.ops.bim.remove_section_plane() + + def delete_bitmaps(self): + collection = bpy.data.collections.get("Bitmaps") + if not collection: + collection = bpy.data.collections.new("Bitmaps") + bpy.context.scene.collection.children.link(collection) + for bitmap in collection.objects: + bpy.data.objects.remove(bitmap) + + def create_bitmaps(self, bcfxml, viewpoint, topic): + collection = bpy.data.collections.get("Bitmaps") + if not collection: + collection = bpy.data.collections.new("Bitmaps") + for bitmap in viewpoint.bitmaps: + obj = bpy.data.objects.new("Bitmap", None) + obj.empty_display_type = "IMAGE" + image = bpy.data.images.load(os.path.join(bcfxml.filepath, topic.guid, bitmap.reference)) + src_width = image.size[0] + src_height = image.size[1] + if src_height > src_width: + obj.empty_display_size = bitmap.height + else: + obj.empty_display_size = bitmap.height * (src_width / src_height) + obj.data = image + y = Vector((bitmap.up.x, bitmap.up.y, bitmap.up.z)) + z = Vector((bitmap.normal.x, bitmap.normal.y, bitmap.normal.z)) + x = y.cross(z) + obj.matrix_world = Matrix( + [[x[0], y[0], z[0], 0], [x[1], y[1], z[1], 0], [x[2], y[2], z[2], 0], [0, 0, 0, 1]] + ) + obj.location = (bitmap.location.x, bitmap.location.y, bitmap.location.z) + collection.objects.link(obj) + + def hex_to_rgb(self, value): + value = value.lstrip("#") + lv = len(value) + t = tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)) + return [t[0] / 255.0, t[1] / 255.0, t[2] / 255.0, 1] + +class OpenBcfReferenceLink(bpy.types.Operator): + bl_idname = "bim.open_bcf_reference_link" + bl_label = "Open BCF Reference Link" + index: bpy.props.IntProperty() + + def execute(self, context): + webbrowser.open(bpy.context.scene.BCFProperties.topic_links[self.index].name) + return {"FINISHED"} + + +class SelectBcfHeaderFile(bpy.types.Operator): + bl_idname = "bim.select_bcf_header_file" + bl_label = "Select BCF Header File" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + if self.filepath: + props = bpy.context.scene.BCFProperties + topic = props.topics[props.active_topic_index] + topic.file_reference = self.filepath + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class SelectBcfBimSnippetReference(bpy.types.Operator): + bl_idname = "bim.select_bcf_bim_snippet_reference" + bl_label = "Select BCF BIM Snippet Reference" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + if self.filepath: + props = bpy.context.scene.BCFProperties + topic = props.topics[props.active_topic_index] + topic.bim_snippet_reference = self.filepath + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class SelectBcfDocumentReference(bpy.types.Operator): + bl_idname = "bim.select_bcf_document_reference" + bl_label = "Select BCF Document Reference" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + if self.filepath: + props = bpy.context.scene.BCFProperties + topic = props.topics[props.active_topic_index] + topic.document_reference = self.filepath + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} diff --git a/src/ifcblenderexport/blenderbim/bim/module/bcf/prop.py b/src/ifcblenderexport/blenderbim/bim/module/bcf/prop.py new file mode 100644 index 0000000000..82426afc42 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/bcf/prop.py @@ -0,0 +1,155 @@ +import bpy +from . import bcfstore +from blenderbim.bim.prop import StrProperty +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +bcfviewpoints_enum = None + + +def updateBcfReferenceLink(self, context): + if bpy.context.scene.BCFProperties.is_loaded: + bpy.ops.bim.edit_bcf_reference_links() + + +def updateBcfLabel(self, context): + if bpy.context.scene.BCFProperties.is_loaded: + bpy.ops.bim.edit_bcf_labels() + + +def updateBcfProjectName(self, context): + if bpy.context.scene.BCFProperties.is_loaded: + bpy.ops.bim.edit_bcf_project_name() + + +def updateBcfAuthor(self, context): + if bpy.context.scene.BCFProperties.is_loaded: + bpy.ops.bim.edit_bcf_author() + + +def updateBcfTopicName(self, context): + if bpy.context.scene.BCFProperties.is_loaded: + bpy.ops.bim.edit_bcf_topic_name() + + +def updateBcfTopicIsEditable(self, context): + if bpy.context.scene.BCFProperties.is_loaded and not self.is_editable: + bpy.ops.bim.edit_bcf_topic() + + +def updateBcfCommentIsEditable(self, context): + if bpy.context.scene.BCFProperties.is_loaded and not self.is_editable: + bpy.ops.bim.edit_bcf_comment(comment_guid = self.name) + + +def refreshBcfTopic(self, context): + global bcfviewpoints_enum + bcfviewpoints_enum = None + + props = bpy.context.scene.BCFProperties + bcfxml = bcfstore.BcfStore.get_bcfxml() + topic = props.topics[props.active_topic_index] + header = bcfxml.get_header(topic.name) + getBcfViewpoints(self, context) + + +class BcfReferenceLink(PropertyGroup): + name: StringProperty(name="Name", update=updateBcfReferenceLink) + + +class BcfLabel(PropertyGroup): + name: StringProperty(name="Name", update=updateBcfLabel) + + +def getBcfViewpoints(self, context): + global bcfviewpoints_enum + if bcfviewpoints_enum is None: + bcfviewpoints_enum = [] + props = bpy.context.scene.BCFProperties + bcfxml = bcfstore.BcfStore.get_bcfxml() + topic = props.topics[props.active_topic_index] + viewpoints = bcfxml.get_viewpoints(topic.name) + bcfviewpoints_enum.extend([(v, f"Viewpoint {i+1}", "") for i, v in enumerate(viewpoints.keys())]) + return bcfviewpoints_enum + + +class BcfBimSnippet(PropertyGroup): + schema: StringProperty(name="Schema") + reference: StringProperty(name="Reference") + type: StringProperty(name="Type") + is_external: BoolProperty(name="Is External") + + +class BcfDocumentReference(PropertyGroup): + reference: StringProperty(name="Reference") + description: StringProperty(name="Description") + guid: StringProperty(name="GUID") + is_external: BoolProperty(name="Is External") + + +class BcfComment(PropertyGroup): + name: StringProperty(name="GUID") + date: StringProperty(name="Date") + author: StringProperty(name="Author") + comment: StringProperty(name="Comment") + viewpoint: StringProperty(name="Viewpoint") + modified_date: StringProperty(name="Modified Date") + modified_author: StringProperty(name="Modified Author") + is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfCommentIsEditable) + + +class BcfTopic(PropertyGroup): + name: StringProperty(name="GUID") + title: StringProperty(default="", name="Title", update=updateBcfTopicName) + type: StringProperty(default="", name="Type") + status: StringProperty(default="", name="Status") + priority: StringProperty(default="", name="Priority") + stage: StringProperty(default="", name="Stage") + creation_date: StringProperty(default="", name="Date") + creation_author: StringProperty(default="", name="Author") + modified_date: StringProperty(default="", name="Modified Date") + modified_author: StringProperty(default="", name="Modified By") + assigned_to: StringProperty(default="", name="Assigned To") + due_date: StringProperty(default="", name="Due Date") + description: StringProperty(default="", name="Description") + viewpoints: EnumProperty(items=getBcfViewpoints, name="BCF Viewpoints") + files: CollectionProperty(name="Files", type=StrProperty) + file_reference: StringProperty(default="", name="Reference") + file_ifc_project: StringProperty(default="", name="IFC Project") + file_ifc_spatial_structure_element: StringProperty(default="", name="IFC Spatial Structure Element") + reference_links: CollectionProperty(name="Reference Links", type=BcfReferenceLink) + reference_link: StringProperty(default="", name="Reference Link") + labels: CollectionProperty(name="Labels", type=BcfLabel) + label: StringProperty(default="", name="Label") + bim_snippet: PointerProperty(type=BcfBimSnippet) + bim_snippet_type: StringProperty(default="", name="Type") + bim_snippet_reference: StringProperty(default="", name="Reference") + bim_snippet_schema: StringProperty(default="", name="Schema") + document_references: CollectionProperty(name="Document References", type=BcfDocumentReference) + document_reference: StringProperty(default="", name="Referenced Document") + document_reference_description: StringProperty(default="", name="Description") + related_topics: CollectionProperty(name="Related Topics", type=StrProperty) + related_topic: StringProperty(default="", name="Related Topic") + comments: CollectionProperty(name="Comments", type=BcfComment) + is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfTopicIsEditable) + comment: StringProperty(default="", name="Comment") + has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False) + + +class BCFProperties(PropertyGroup): + is_loaded: BoolProperty(name="Is Loaded", default=False) + comment_text_width: IntProperty(name="Comment Text Width", default=40) + name: StringProperty(default="", name="Project Name", update=updateBcfProjectName) + author: StringProperty(default="john@doe.com", name="Author Email", update=updateBcfAuthor) + topics: CollectionProperty(name="BCF Topics", type=BcfTopic) + active_topic_index: IntProperty(name="Active BCF Topic Index", update=refreshBcfTopic) diff --git a/src/ifcblenderexport/blenderbim/bim/module/bcf/ui.py b/src/ifcblenderexport/blenderbim/bim/module/bcf/ui.py new file mode 100644 index 0000000000..f2d22c08ee --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/bcf/ui.py @@ -0,0 +1,283 @@ +import os +import bpy +from . import bcfstore +from bpy.types import Panel + +class BIM_PT_bcf(Panel): + bl_label = "BIM Collaboration Format (BCF)" + bl_idname = "BIM_PT_bcf" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + scene = context.scene + props = bpy.context.scene.BCFProperties + + row = layout.row(align=True) + row.operator("bim.new_bcf_project", text="New Project") + row.operator("bim.load_bcf_project", text="Load Project") + + if not props.is_loaded: + return + + row.operator("bim.save_bcf_project", text="Save Project") + + row = layout.row() + row.prop(props, "name") + + row = layout.row() + row.prop(props, "author") + + props = bpy.context.scene.BCFProperties + row = layout.row() + row.template_list("BIM_UL_topics", "", props, "topics", props, "active_topic_index") + col = row.column(align=True) + col.operator("bim.add_bcf_topic", icon="ADD", text="") + if props.active_topic_index < len(props.topics): + topic = props.topics[props.active_topic_index] + col.prop(topic, "is_editable", icon="CHECKMARK" if topic.is_editable else "GREASEPENCIL", icon_only=True) + + if props.active_topic_index < len(props.topics): + topic = props.topics[props.active_topic_index] + row = layout.row() + row.enabled = topic.is_editable + row.prop(topic, "description", text="") + + row = layout.row() + row.prop(topic, "viewpoints") + row.operator("bim.activate_bcf_viewpoint", icon="SCENE", text="") + row.operator("bim.add_bcf_viewpoint", icon="ADD", text="") + row.operator("bim.remove_bcf_viewpoint", icon="X", text="") + + col = layout.column(align=True) + if topic.type: + col.prop(topic, "type", emboss=topic.is_editable) + if topic.status: + col.prop(topic, "status", emboss=topic.is_editable) + if topic.priority: + col.prop(topic, "priority", emboss=topic.is_editable) + if topic.stage: + col.prop(topic, "stage", emboss=topic.is_editable) + if topic.assigned_to: + col.prop(topic, "assigned_to", emboss=topic.is_editable) + if topic.due_date: + col.prop(topic, "due_date", emboss=topic.is_editable) + + col = layout.column(align=True) + if topic.modified_date: + col.prop(topic, "modified_date", emboss=False) + col.prop(topic, "modified_author", emboss=False) + else: + col.prop(topic, "creation_date", emboss=False) + col.prop(topic, "creation_author", emboss=False) + + +class BIM_PT_bcf_metadata(Panel): + bl_label = "BCF Metadata" + bl_idname = "BIM_PT_bcf_metadata" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_bcf" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + scene = context.scene + props = bpy.context.scene.BCFProperties + + if props.active_topic_index >= len(props.topics): + layout.label(text="No BCF project is loaded") + return + + topic = props.topics[props.active_topic_index] + bcfxml = bcfstore.BcfStore.get_bcfxml() + bcf_topic = bcfxml.topics[topic.name] + + layout.label(text="Header Files:") + + if bcf_topic.header: + for index, f in enumerate(bcf_topic.header.files): + box = self.layout.box() + row = box.row(align=True) + row.label(text=f.filename, icon="FILE_BLANK") + if f.is_external: + row.operator("bim.open_uri", icon="URL", text="").uri = f.reference + else: + op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="") + op.uri = os.path.join(bcfxml.filepath, topic.name, f.reference) + row.operator("bim.remove_bcf_file", icon="X", text="").index = index + + row = box.row() + row.label(text="Date") + row.label(text=f.date) + + if f.ifc_project: + row = box.row() + row.label(text="IFC Project") + row.label(text=f.ifc_project) + + if f.ifc_spatial_structure_element: + row = box.row() + row.label(text="IFC Spatial Structure Element") + row.label(text=f.ifc_spatial_structure_element) + + row = layout.row(align=True) + row.prop(topic, "file_reference") + row.operator("bim.select_bcf_header_file", icon="FILE_FOLDER", text="") + row = layout.row() + row.prop(topic, "file_ifc_project") + row = layout.row() + row.prop(topic, "file_ifc_spatial_structure_element") + + row = layout.row() + row.operator("bim.add_bcf_header_file") + + layout.label(text="Reference Links:") + for index, link in enumerate(topic.reference_links): + row = layout.row(align=True) + row.prop(link, "name") + row.operator("bim.open_uri", icon="URL", text="").uri = link.name + row.operator("bim.remove_bcf_reference_link", icon="X", text="").index = index + row = layout.row() + row.prop(topic, "reference_link") + row = layout.row() + row.operator("bim.add_bcf_reference_link") + + layout.label(text="Labels:") + for index, label in enumerate(topic.labels): + row = layout.row(align=True) + row.prop(label, "name", text="") + row.operator("bim.remove_bcf_label", icon="X", text="").index = index + row = layout.row() + row.prop(topic, "label") + row = layout.row() + row.operator("bim.add_bcf_label") + + layout.label(text="BIM Snippet:") + if topic.bim_snippet.schema: + row = layout.row(align=True) + row.prop(topic.bim_snippet, "type", emboss=False) + if topic.bim_snippet.schema: + row.operator("bim.open_uri", icon="URL", text="").uri = topic.bim_snippet.schema + + row = layout.row(align=True) + row.prop(topic.bim_snippet, "reference", emboss=False) + if topic.bim_snippet.is_external: + row.operator("bim.open_uri", icon="URL", text="").uri = topic.bim_snippet.reference + else: + op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="") + op.uri = os.path.join(bcfxml.filepath, topic.name, topic.bim_snippet.reference) + row.operator("bim.remove_bcf_bim_snippet", icon="X", text="") + else: + row = layout.row(align=True) + row.prop(topic, "bim_snippet_reference") + row.operator("bim.select_bcf_bim_snippet_reference", icon="FILE_FOLDER", text="") + row = layout.row() + row.prop(topic, "bim_snippet_type") + row = layout.row() + row.prop(topic, "bim_snippet_schema") + row = layout.row() + row.operator("bim.add_bcf_bim_snippet") + + layout.label(text="Document References:") + for index, doc in enumerate(topic.document_references): + box = self.layout.box() + row = box.row(align=True) + row.prop(doc, "reference") + if doc.is_external: + row.operator("bim.open_uri", icon="URL", text="").uri = doc.reference + else: + op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="") + op.uri = os.path.join(bcfxml.filepath, topic.name, doc.reference) + row.operator("bim.remove_bcf_document_reference", icon="X", text="").index = index + row = box.row(align=True) + row.prop(doc, "description") + row = layout.row(align=True) + row.prop(topic, "document_reference") + row.operator("bim.select_bcf_document_reference", icon="FILE_FOLDER", text="") + row = layout.row() + row.prop(topic, "document_reference_description") + row = layout.row() + row.operator("bim.add_bcf_document_reference") + + layout.label(text="Related Topics:") + for index, related_topic in enumerate(topic.related_topics): + row = layout.row(align=True) + row.operator("bim.view_bcf_topic", text=bcfxml.topics[related_topic.name.lower()].title).topic_guid = related_topic.name + row.operator("bim.remove_bcf_related_topic", icon="X", text="").index = index + row = layout.row() + row.prop(topic, "related_topic") + row = layout.row() + row.operator("bim.add_bcf_related_topic") + + +class BIM_PT_bcf_comments(Panel): + bl_label = "BCF Comments" + bl_idname = "BIM_PT_bcf_comments" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_bcf" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + scene = context.scene + props = bpy.context.scene.BCFProperties + + if props.active_topic_index >= len(props.topics): + layout.label(text="No BCF project is loaded") + return + + row = layout.row() + row.prop(props, "comment_text_width") + + topic = props.topics[props.active_topic_index] + for comment in topic.comments: + box = self.layout.box() + + author_text = "{} ({})".format(comment.author, comment.date) + if comment.modified_author: + author_text = "*{} ({})".format(comment.modified_author, comment.modified_date) + row = box.row(align=True) + row.label(text=author_text, icon="WORDWRAP_ON") + row.prop( + comment, "is_editable", icon="CHECKMARK" if comment.is_editable else "GREASEPENCIL", icon_only=True + ) + row.operator("bim.remove_bcf_comment", icon="X", text="").comment_guid = comment.name + + if comment.is_editable: + row = box.row() + row.prop(comment, "comment", text="") + else: + col = box.column(align=True) + col.scale_y = 0.8 + words = comment.comment.split() + while words: + total_line_chars = 0 + line_words = [] + while words and total_line_chars < props.comment_text_width: + word = words.pop(0) + line_words.append(word) + total_line_chars += len(word) + 1 # 1 is for the space + col.label(text=" ".join(line_words)) + + row = layout.row() + row.prop(topic, "comment") + row = layout.row() + row.prop(topic, "has_related_viewpoint") + row = layout.row() + row.operator("bim.add_bcf_comment") diff --git a/src/ifcblenderexport/blenderbim/bim/module/covetool/__init__.py b/src/ifcblenderexport/blenderbim/bim/module/covetool/__init__.py new file mode 100644 index 0000000000..0113965696 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/covetool/__init__.py @@ -0,0 +1,21 @@ +import bpy +from . import ui, prop, operator + +classes = ( + prop.CoveToolProject, + prop.CoveToolSimpleAnalysis, + prop.CoveToolProperties, + ui.BIM_UL_covetool_projects, + ui.BIM_PT_covetool, + operator.Login, + operator.RunSimpleAnalysis, + operator.RunAnalysis, +) + + +def register(): + bpy.types.Scene.CoveToolProperties = bpy.props.PointerProperty(type=prop.CoveToolProperties) + + +def unregister(): + del bpy.types.Scene.CoveToolProperties diff --git a/src/ifcblenderexport/blenderbim/bim/module/model/__init__.py b/src/ifcblenderexport/blenderbim/bim/module/model/__init__.py new file mode 100644 index 0000000000..09c610553b --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/model/__init__.py @@ -0,0 +1,32 @@ +import bpy +from . import grid, wall, stair, door, window, slab, opening + +classes = ( + grid.BIM_OT_add_object, + wall.BIM_OT_add_object, + stair.BIM_OT_add_object, + door.BIM_OT_add_object, + window.BIM_OT_add_object, + slab.BIM_OT_add_object, + opening.BIM_OT_add_object, +) + + +def register(): + bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.append(wall.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.append(window.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.append(slab.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.append(opening.add_object_button) + + +def unregister(): + bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.remove(wall.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.remove(slab.add_object_button) + bpy.types.VIEW3D_MT_mesh_add.remove(opening.add_object_button) diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index f6474d0d68..102b525102 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -1,3 +1,4 @@ +from itertools import chain import os import bpy import uuid @@ -20,14 +21,13 @@ from . import svgwriter from . import sheeter from . import scheduler from . import schema -from . import bcfstore from . import ifc from . import annotation from . import helper from bpy_extras.io_utils import ImportHelper from itertools import cycle from mathutils import Vector, Matrix, Euler, geometry -from math import radians, atan, tan, cos, sin +from math import radians, degrees, atan, tan, cos, sin from pathlib import Path from bpy.app.handlers import persistent @@ -520,415 +520,6 @@ class RejectElement(bpy.types.Operator): return {"FINISHED"} -class NewBcfProject(bpy.types.Operator): - bl_idname = "bim.new_bcf_project" - bl_label = "New BCF Project" - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.new_project() - bpy.ops.bim.load_bcf_project() - return {"FINISHED"} - - -class LoadBcfProject(bpy.types.Operator): - bl_idname = "bim.load_bcf_project" - bl_label = "Load BCF Project" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - if self.filepath: - bcfxml.get_project(self.filepath) - bpy.context.scene.BCFProperties.name = bcfxml.project.name - bpy.ops.bim.load_bcf_topics() - bpy.context.scene.BCFProperties.is_loaded = True - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class LoadBcfTopics(bpy.types.Operator): - bl_idname = "bim.load_bcf_topics" - bl_label = "Load BCF Topics" - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.get_topics() - while len(bpy.context.scene.BCFProperties.topics) > 0: - bpy.context.scene.BCFProperties.topics.remove(0) - for topic in bcfxml.topics.values(): - new = bpy.context.scene.BCFProperties.topics.add() - data_map = { - "name": topic.guid, - "title": topic.title, - "type": topic.topic_type, - "status": topic.topic_status, - "priority": topic.priority, - "stage": topic.stage, - "creation_date": topic.creation_date, - "creation_author": topic.creation_author, - "modified_date": topic.modified_date, - "modified_author": topic.modified_author, - "assigned_to": topic.assigned_to, - "due_date": topic.due_date, - "description": topic.description - } - for key, value in data_map.items(): - if value is not None: - setattr(new, key, str(value)) - for reference_link in topic.reference_links: - new2 = new.reference_links.add() - new2.name = reference_link - for label in topic.labels: - new2 = new.labels.add() - new2.name = label - if topic.bim_snippet: - data_map = { - "type": topic.bim_snippet.snippet_type, - "is_external": topic.bim_snippet.is_external, - "reference": topic.bim_snippet.reference, - "schema": topic.bim_snippet.reference_schema - } - for key, value in data_map.items(): - if value is not None: - setattr(new.bim_snippet, key, value) - for doc in topic.document_references: - new2 = new.document_references.add() - data_map = { - "reference": doc.referenced_document, - "description": doc.description, - "guid": doc.guid, - "is_external": doc.is_external - } - for key, value in data_map.items(): - if value is not None: - setattr(new2, key, value) - for related_topic in topic.related_topics: - new2 = new.related_topics.add() - new2.name = related_topic.guid - bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) - return {"FINISHED"} - - -class LoadBcfComments(bpy.types.Operator): - bl_idname = "bim.load_bcf_comments" - bl_label = "Load BCF Comments" - topic_guid: bpy.props.StringProperty() - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.get_comments(self.topic_guid) - blender_topic = bpy.context.scene.BCFProperties.topics.get(self.topic_guid) - for comment in bcfxml.topics[self.topic_guid].comments.values(): - new = blender_topic.comments.add() - data_map = { - "name": comment.guid, - "comment": comment.comment, - "viewpoint": comment.viewpoint.guid if comment.viewpoint else None, - "date": comment.date, - "author": comment.author, - "modified_date": comment.modified_date, - "modified_author": comment.modified_author, - } - for key, value in data_map.items(): - if value is not None: - setattr(new, key, str(value)) - return {"FINISHED"} - - -class EditBcfProjectName(bpy.types.Operator): - bl_idname = "bim.edit_bcf_project_name" - bl_label = "Edit BCF Project Name" - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.project.name = bpy.context.scene.BCFProperties.name - bcfxml.edit_project() - return {"FINISHED"} - - -class EditBcfAuthor(bpy.types.Operator): - bl_idname = "bim.edit_bcf_author" - bl_label = "Edit BCF Author" - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.author = bpy.context.scene.BCFProperties.author - return {"FINISHED"} - - -class EditBcfTopicName(bpy.types.Operator): - bl_idname = "bim.edit_bcf_topic_name" - bl_label = "Edit BCF Topic Name" - - def execute(self, context): - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] - bcfxml = bcfstore.BcfStore.get_bcfxml() - topic = bcfxml.topics[blender_topic.name] - topic.title = blender_topic.title - bcfxml.edit_topic(topic) - return {"FINISHED"} - - -class EditBcfTopic(bpy.types.Operator): - bl_idname = "bim.edit_bcf_topic" - bl_label = "Edit BCF Topic" - - def execute(self, context): - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] - bcfxml = bcfstore.BcfStore.get_bcfxml() - - topic = bcfxml.topics[blender_topic.name] - topic.title = blender_topic.title or None - topic.priority = blender_topic.priority or None - topic.due_date = blender_topic.due_date or None - topic.assigned_to = blender_topic.assigned_to or None - topic.stage = blender_topic.stage or None - topic.description = blender_topic.description or None - topic.topic_status = blender_topic.status or None - topic.topic_type = blender_topic.type or None - - bcfxml.edit_topic(topic) - return {"FINISHED"} - - -class SaveBcfProject(bpy.types.Operator): - bl_idname = "bim.save_bcf_project" - bl_label = "Save BCF Project" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.save_project(self.filepath) - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class AddBcfTopic(bpy.types.Operator): - bl_idname = "bim.add_bcf_topic" - bl_label = "Add BCF Topic" - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.add_topic() - new = bpy.context.scene.BCFProperties.topics.add() - new.name = "New Topic" - bpy.ops.bim.load_bcf_topics() - return {"FINISHED"} - - -class ViewBcfTopic(bpy.types.Operator): - bl_idname = "bim.view_bcf_topic" - bl_label = "Get BCF Topic" - topic_guid: bpy.props.StringProperty() - - def execute(self, context): - for index, topic in enumerate(bpy.context.scene.BCFProperties.topics): - if topic.guid.lower() == self.topic_guid.lower(): - bpy.context.scene.BCFProperties.active_topic_index = index - return {"FINISHED"} - - -class ActivateBcfViewpoint(bpy.types.Operator): - bl_idname = "bim.activate_bcf_viewpoint" - bl_label = "Activate BCF Viewpoint" - - def execute(self, context): - bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] - topic = bcfxml.topics[blender_topic.guid] - if not topic.viewpoints: - return {"FINISHED"} - - viewpoint_guid = blender_topic.viewpoints - viewpoint = topic.viewpoints[viewpoint_guid] - obj = bpy.data.objects.get("Viewpoint") - if not obj: - obj = bpy.data.objects.new("Viewpoint", bpy.data.cameras.new("Viewpoint")) - bpy.context.scene.collection.objects.link(obj) - bpy.context.scene.camera = obj - - cam_width = bpy.context.scene.render.resolution_x - cam_height = bpy.context.scene.render.resolution_y - cam_aspect = cam_width / cam_height - - if viewpoint.snapshot: - obj.data.show_background_images = True - while len(obj.data.background_images) > 0: - obj.data.background_images.remove(obj.data.background_images[0]) - background = obj.data.background_images.new() - background.image = bpy.data.images.load( - os.path.join(bcfxml.filepath, topic.guid, viewpoint.snapshot) - ) - src_width = background.image.size[0] - src_height = background.image.size[1] - src_aspect = src_width / src_height - - if src_aspect > cam_aspect: - background.frame_method = "FIT" - else: - background.frame_method = "CROP" - background.display_depth = "FRONT" - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") - area.spaces[0].region_3d.view_perspective = "CAMERA" - - if viewpoint.orthogonal_camera: - camera = viewpoint.orthogonal_camera - obj.data.type = "ORTHO" - obj.data.ortho_scale = viewpoint.orthogonal_camera.view_to_world_scale - elif viewpoint.perspective_camera: - camera = viewpoint.perspective_camera - obj.data.type = "PERSP" - if cam_aspect >= 1: - obj.data.angle = radians(camera.field_of_view) - else: - # https://blender.stackexchange.com/questions/23431/how-to-set-camera-horizontal-and-vertical-fov - obj.data.angle = 2 * atan((0.5 * cam_height) / (0.5 * cam_width / tan(radians(camera.field_of_view) / 2))) - - self.set_viewpoint_components(viewpoint) - - gp = bpy.data.grease_pencils.get("BCF") - if gp: - bpy.data.grease_pencils.remove(gp) - if viewpoint.lines: - self.draw_lines(viewpoint) - - self.delete_clipping_planes() - if viewpoint.clipping_planes: - self.create_clipping_planes(viewpoint) - - self.delete_bitmaps() - if viewpoint.bitmaps: - self.create_bitmaps(bcfxml, viewpoint, topic) - - z_axis = Vector((-camera.camera_direction.x, -camera.camera_direction.y, -camera.camera_direction.z)).normalized() - y_axis = Vector((camera.camera_up_vector.x, camera.camera_up_vector.y, camera.camera_up_vector.z)).normalized() - x_axis = y_axis.cross(z_axis).normalized() - rotation = Matrix((x_axis, y_axis, z_axis)) - rotation.invert() - location = Vector((camera.camera_view_point.x, camera.camera_view_point.y, camera.camera_view_point.z)) - obj.matrix_world = rotation.to_4x4() - obj.location = location - return {"FINISHED"} - - def set_viewpoint_components(self, viewpoint): - selected_global_ids = [s.ifc_guid for s in viewpoint.components.selection] - exception_global_ids = [v.ifc_guid for v in viewpoint.components.visibility.exceptions] - global_id_colours = {} - for coloring in viewpoint.components.coloring: - for component in coloring.components: - global_id_colours.setdefault(component.ifc_guid, coloring.color) - - for obj in bpy.data.objects: - global_id = obj.BIMObjectProperties.attributes.get("GlobalId") - if not global_id: - continue - global_id = global_id.string_value - is_visible = viewpoint.components.visibility.default_visibility - if global_id in exception_global_ids: - is_visible = not is_visible - if not is_visible: - obj.hide_set(True) - continue - if "IfcSpace" in obj.name: - is_visible = viewpoint.components.viewSetuphints.spacesVisible - elif "IfcOpeningElement" in obj.name: - is_visible = viewpoint.components.viewSetuphints.openingsVisible - obj.hide_set(not is_visible) - if not is_visible: - continue - obj.select_set(global_id in selected_global_ids) - if global_id in global_id_colours: - obj.color = self.hex_to_rgb(global_id_colours[global_id]) - - def draw_lines(self, viewpoint): - gp = bpy.data.grease_pencils.new("BCF") - scene = bpy.context.scene - scene.grease_pencil = gp - scene.frame_set(1) - layer = gp.layers.new("BCF Annotation", set_active=True) - layer.thickness = 3 - layer.color = (1, 0, 0) - frame = layer.frames.new(1) - stroke = frame.strokes.new() - stroke.display_mode = "3DSPACE" - stroke.points.add(len(viewpoint.lines) * 2) - coords = [] - for l in viewpoint.lines: - coords.extend([l.start_point.x, l.start_point.y, l.start_point.z, l.end_point.x, l.end_point.y, l.end_point.z]) - stroke.points.foreach_set("co", coords) - - def create_clipping_planes(self, viewpoint): - n = 0 - for plane in viewpoint.clipping_planes: - bpy.ops.bim.add_section_plane() - if n == 0: - obj = bpy.data.objects["Section"] - else: - obj = bpy.data.objects["Section.{:03d}".format(n)] - obj.location = (plane.location.x, plane.location.y, plane.location.z) - obj.rotation_mode = "QUATERNION" - obj.rotation_quaternion = Vector((plane.direction.x, plane.direction.y, plane.direction.z)).to_track_quat( - "Z", "Y" - ) - n += 1 - - def delete_clipping_planes(self): - collection = bpy.data.collections.get("Sections") - if not collection: - return - for section in collection.objects: - bpy.context.view_layer.objects.active = section - bpy.ops.bim.remove_section_plane() - - def delete_bitmaps(self): - collection = bpy.data.collections.get("Bitmaps") - if not collection: - collection = bpy.data.collections.new("Bitmaps") - bpy.context.scene.collection.children.link(collection) - for bitmap in collection.objects: - bpy.data.objects.remove(bitmap) - - def create_bitmaps(self, bcfxml, viewpoint, topic): - collection = bpy.data.collections.get("Bitmaps") - if not collection: - collection = bpy.data.collections.new("Bitmaps") - for bitmap in viewpoint.bitmaps: - obj = bpy.data.objects.new("Bitmap", None) - obj.empty_display_type = "IMAGE" - image = bpy.data.images.load(os.path.join(bcfxml.filepath, topic.guid, bitmap.reference)) - src_width = image.size[0] - src_height = image.size[1] - if src_height > src_width: - obj.empty_display_size = bitmap.height - else: - obj.empty_display_size = bitmap.height * (src_width / src_height) - obj.data = image - y = Vector((bitmap.up.x, bitmap.up.y, bitmap.up.z)) - z = Vector((bitmap.normal.x, bitmap.normal.y, bitmap.normal.z)) - x = y.cross(z) - obj.matrix_world = Matrix( - [[x[0], y[0], z[0], 0], [x[1], y[1], z[1], 0], [x[2], y[2], z[2], 0], [0, 0, 0, 1]] - ) - obj.location = (bitmap.location.x, bitmap.location.y, bitmap.location.z) - collection.objects.link(obj) - - def hex_to_rgb(self, value): - value = value.lstrip("#") - lv = len(value) - t = tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)) - return [t[0] / 255.0, t[1] / 255.0, t[2] / 255.0, 1] - - class OpenUri(bpy.types.Operator): bl_idname = "bim.open_uri" bl_label = "Open URI" @@ -939,16 +530,6 @@ class OpenUri(bpy.types.Operator): return {"FINISHED"} -class OpenBcfReferenceLink(bpy.types.Operator): - bl_idname = "bim.open_bcf_reference_link" - bl_label = "Open BCF Reference Link" - index: bpy.props.IntProperty() - - def execute(self, context): - webbrowser.open(bpy.context.scene.BCFProperties.topic_links[self.index].name) - return {"FINISHED"} - - class SelectAudited(bpy.types.Operator): bl_idname = "bim.select_audited" bl_label = "Select Audited" @@ -1987,11 +1568,12 @@ class SelectSmartGroupedClashesPath(bpy.types.Operator): class ExecuteIfcClash(bpy.types.Operator): bl_idname = "bim.execute_ifc_clash" bl_label = "Execute IFC Clash" - filename_ext = ".json" + filename_ext = ".bcf" filepath: bpy.props.StringProperty(subtype="FILE_PATH") def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") + if ".json" not in bpy.data.filepath: + self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf") WindowManager = context.window_manager WindowManager.fileselect_add(self) return {"RUNNING_MODAL"} @@ -2000,11 +1582,34 @@ class ExecuteIfcClash(bpy.types.Operator): import ifcclash settings = ifcclash.IfcClashSettings() - self.filepath = bpy.path.ensure_ext(self.filepath, ".json") + if ".json" not in self.filepath: + self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf") settings.output = self.filepath settings.logger = logging.getLogger("Clash") settings.logger.setLevel(logging.DEBUG) ifc_clasher = ifcclash.IfcClasher(settings) + + if bpy.context.scene.BIMProperties.should_create_clash_snapshots: + def get_viewpoint_snapshot(self, viewpoint, mat): + camera = bpy.data.objects.get('IFC Clash Camera') + if not camera: + camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera")) + bpy.context.scene.collection.objects.link(camera) + camera.matrix_world = Matrix(mat) + bpy.context.scene.camera = camera + camera.data.angle = radians(60) + area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area.spaces[0].region_3d.view_perspective = "CAMERA" + area.spaces[0].shading.show_xray = True + bpy.context.scene.render.resolution_x = 480 + bpy.context.scene.render.resolution_y = 270 + bpy.context.scene.render.image_settings.file_format = "PNG" + bpy.context.scene.render.filepath = os.path.join(bpy.context.scene.BIMProperties.data_dir, "snapshot.png") + bpy.ops.render.opengl(write_still=True) + return bpy.context.scene.render.filepath + + ifcclash.IfcClasher.get_viewpoint_snapshot = get_viewpoint_snapshot + ifc_clasher.clash_sets = [] for clash_set in bpy.context.scene.BIMProperties.clash_sets: self.a = [] @@ -2532,6 +2137,25 @@ class OpenView(bpy.types.Operator): return {"FINISHED"} +class OpenViewCamera(bpy.types.Operator): + bl_idname = "bim.open_view_camera" + bl_label = "Open View Camera" + view_name: bpy.props.StringProperty() + + def execute(self, context): + new_drawing_index = bpy.context.scene.DocProperties.drawings.find(self.view_name) + bpy.context.scene.DocProperties.active_drawing_index = new_drawing_index + drawing = bpy.context.scene.DocProperties.drawings[new_drawing_index] + bpy.context.view_layer.objects.active = drawing.camera + bpy.ops.object.select_all(action="DESELECT") + drawing.camera.select_set(True) + for area in bpy.context.screen.areas: + if area.ui_type == "PROPERTIES": + for space in area.spaces: + space.context = "DATA" + return {"FINISHED"} + + class CutSection(bpy.types.Operator): bl_idname = "bim.cut_section" bl_label = "Cut Section" @@ -3478,6 +3102,10 @@ class ExportIfcCsv(bpy.types.Operator): ifc_csv.output = self.filepath ifc_csv.attributes = [a.name for a in bpy.context.scene.BIMProperties.csv_attributes] ifc_csv.selector = selector + if bpy.context.scene.BIMProperties.csv_delimiter == "CUSTOM": + ifc_csv.delimiter = bpy.context.scene.BIMProperties.csv_custom_delimiter + else: + ifc_csv.delimiter = bpy.context.scene.BIMProperties.csv_delimiter ifc_csv.export(ifc_file, results) return {"FINISHED"} @@ -3617,7 +3245,10 @@ class AddAnnotation(bpy.types.Operator): obj = annotation.Annotator.add_text() else: obj = annotation.Annotator.get_annotation_obj(self.obj_name, self.data_type) - obj = annotation.Annotator.add_line_to_annotation(obj) + if self.obj_name == 'Break': + obj = annotation.Annotator.add_plane_to_annotation(obj) + else: + obj = annotation.Annotator.add_line_to_annotation(obj) bpy.ops.object.select_all(action="DESELECT") bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode="EDIT") @@ -5010,3 +4641,77 @@ class LinkIfc(bpy.types.Operator): def invoke(self, context, event): context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} + + +class SnapSpacesTogether(bpy.types.Operator): + bl_idname = "bim.snap_spaces_together" + bl_label = "Snap Spaces Together" + + def execute(self, context): + threshold = 0.5 + processed_polygons = set() + for obj in bpy.context.selected_objects: + if obj.type != "MESH": + continue + for polygon in obj.data.polygons: + center = obj.matrix_world @ polygon.center + distance = None + for obj2 in bpy.context.selected_objects: + if obj2 == obj or obj.type != "MESH": + continue + result = obj2.ray_cast( + obj2.matrix_world.inverted() @ center, + polygon.normal, + distance=threshold + ) + if not result[0]: + continue + hit = obj2.matrix_world @ result[1] + distance = (hit - center).length / 2 + if distance < 0.01: + distance = None + break + + if (obj2.name, result[3]) in processed_polygons: + distance *= 2 + continue + + offset = polygon.normal * distance * -1 + processed_polygons.add((obj2.name, result[3])) + for v in obj2.data.polygons[result[3]].vertices: + obj2.data.vertices[v].co += offset + break + if distance: + offset = polygon.normal * distance + processed_polygons.add((obj.name, polygon.index)) + for v in polygon.vertices: + obj.data.vertices[v].co += offset + return {"FINISHED"} + + +class CopyGrid(bpy.types.Operator): + bl_idname = "bim.add_grid" + bl_label = "Add Grid" + + def execute(self, context): + props = context.scene.DocProperties + if props.active_drawing_index is None or len(props.drawings) == 0: + return {"CANCELLED"} + drawing = props.drawings[props.active_drawing_index] + collection = bpy.data.collections.get("IfcGroup/" + drawing.name) + + existing = [obj for obj in collection.objects if obj.name.startswith('IfcGridAxis')] + for obj in existing: + collection.objects.unlink(obj) + + source = [obj + for coll in bpy.data.collections + if coll.name.startswith('IfcGrid') + for obj in coll.all_objects + if obj.type == 'MESH'] + for obj in source: + dstobj = obj.copy() + dstobj.data = dstobj.data.copy() + collection.objects.link(dstobj) + + return {"FINISHED"} diff --git a/src/ifcblenderexport/blenderbim/bim/prop.py b/src/ifcblenderexport/blenderbim/bim/prop.py index 7fd0c5e456..b80e6a159d 100644 --- a/src/ifcblenderexport/blenderbim/bim/prop.py +++ b/src/ifcblenderexport/blenderbim/bim/prop.py @@ -5,7 +5,6 @@ import ifcopenshell.util.pset from pathlib import Path from . import export_ifc from . import schema -from . import bcfstore from . import ifc from . import annotation from . import decoration @@ -52,7 +51,6 @@ persons_enum = [] organisations_enum = [] sheets_enum = [] vector_styles_enum = [] -bcfviewpoints_enum = None @persistent @@ -361,9 +359,11 @@ def refreshTitleblocks(self, context): def toggleDecorations(self, context): toggle = self.should_draw_decorations if toggle: - decoration.DimensionDecorator.install(self, context) + for dec in decoration.all_decorators: + dec.install(self, context) else: - decoration.DimensionDecorator.uninstall() + for dec in decoration.all_decorators: + dec.uninstall() def getScenarios(self, context): @@ -540,35 +540,6 @@ def refreshFontSize(self, context): annotation.Annotator.resize_text(context.active_object) -def updateBcfProjectName(self, context): - bpy.ops.bim.edit_bcf_project_name() - - -def updateBcfAuthor(self, context): - bpy.ops.bim.edit_bcf_author() - - -def updateBcfTopicName(self, context): - bpy.ops.bim.edit_bcf_topic_name() - - -def updateBcfTopicIsEditable(self, context): - if not self.is_editable: - print("EDITING!") - bpy.ops.bim.edit_bcf_topic() - - -def refreshBcfTopic(self, context): - global bcfviewpoints_enum - bcfviewpoints_enum = None - - props = bpy.context.scene.BCFProperties - bcfxml = bcfstore.BcfStore.get_bcfxml() - topic = props.topics[props.active_topic_index] - header = bcfxml.get_header(topic.name) - getBcfViewpoints(self, context) - - class StrProperty(PropertyGroup): pass @@ -594,6 +565,12 @@ class Subcontext(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") +class Representation(PropertyGroup): + name: StringProperty(name="Name") + type: StringProperty(name="Type") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + class MaterialLayer(PropertyGroup): name: StringProperty(name="Name") material: PointerProperty(name="Material", type=bpy.types.Material) @@ -707,6 +684,9 @@ class DocProperties(PropertyGroup): ifc_files: CollectionProperty(name="IFCs", type=StrProperty) drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle) should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations) + decorations_colour: FloatVectorProperty(name="Decorations Colour", subtype="COLOR", default=(1, 0, 0, 1), + min=0.0, max=1.0, size=4, + update=toggleDecorations) class BIMCameraProperties(PropertyGroup): @@ -939,67 +919,6 @@ class Constraint(PropertyGroup): user_defined_qualifier: StringProperty(name="Custom Qualifier") -def getBcfViewpoints(self, context): - global bcfviewpoints_enum - if bcfviewpoints_enum is None: - bcfviewpoints_enum = [] - props = bpy.context.scene.BCFProperties - bcfxml = bcfstore.BcfStore.get_bcfxml() - topic = props.topics[props.active_topic_index] - viewpoints = bcfxml.get_viewpoints(topic.name) - bcfviewpoints_enum.extend([(v, f"Viewpoint {i+1}", "") for i, v in enumerate(viewpoints.keys())]) - return bcfviewpoints_enum - - -class BcfBimSnippet(PropertyGroup): - schema: StringProperty(name="Schema") - reference: StringProperty(name="Reference") - type: StringProperty(name="Type") - is_external: BoolProperty(name="Is External") - - -class BcfDocumentReference(PropertyGroup): - reference: StringProperty(name="Reference") - description: StringProperty(name="Description") - guid: StringProperty(name="GUID") - is_external: BoolProperty(name="Is External") - - -class BcfComment(PropertyGroup): - name: StringProperty(name="GUID") - date: StringProperty(name="Date") - author: StringProperty(name="Author") - comment: StringProperty(name="Comment") - viewpoint: StringProperty(name="Viewpoint") - modified_date: StringProperty(name="Modified Date") - modified_author: StringProperty(name="Modified Author") - - -class BcfTopic(PropertyGroup): - name: StringProperty(name="GUID") - title: StringProperty(default="", name="Title", update=updateBcfTopicName) - type: StringProperty(default="", name="Type") - status: StringProperty(default="", name="Status") - priority: StringProperty(default="", name="Priority") - stage: StringProperty(default="", name="Stage") - creation_date: StringProperty(default="", name="Date") - creation_author: StringProperty(default="", name="Author") - modified_date: StringProperty(default="", name="Modified Date") - modified_author: StringProperty(default="", name="Modified By") - assigned_to: StringProperty(default="", name="Assigned To") - due_date: StringProperty(default="", name="Due Date") - description: StringProperty(default="", name="Description") - viewpoints: EnumProperty(items=getBcfViewpoints, name="BCF Viewpoints") - files: CollectionProperty(name="Files", type=StrProperty) - reference_links: CollectionProperty(name="Reference Links", type=StrProperty) - labels: CollectionProperty(name="Labels", type=StrProperty) - bim_snippet: PointerProperty(type=BcfBimSnippet) - document_references: CollectionProperty(name="Document References", type=BcfDocumentReference) - related_topics: CollectionProperty(name="Related Topics", type=StrProperty) - comments: CollectionProperty(name="Comments", type=BcfComment) - is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfTopicIsEditable) - - class PropertySetTemplate(PropertyGroup): global_id: StringProperty(name="Global ID") name: StringProperty(name="Name") @@ -1350,6 +1269,7 @@ class BIMProperties(PropertyGroup): ) export_should_force_faceted_brep: BoolProperty(name="Export with Faceted Breps", default=False) export_should_force_triangulation: BoolProperty(name="Export with Triangulation", default=False) + export_should_export_from_memory: BoolProperty(name="Export from Memory", default=False) import_should_ignore_site_coordinates: BoolProperty(name="Import Ignoring Site Coordinates", default=False) import_should_ignore_building_coordinates: BoolProperty(name="Import Ignoring Building Coordinates", default=False) import_should_reset_absolute_coordinates: BoolProperty(name="Import Resetting Absolute Coordinates", default=False) @@ -1432,6 +1352,12 @@ class BIMProperties(PropertyGroup): ) ifc_selector: StringProperty(default="", name="IFC Selector") csv_attributes: CollectionProperty(name="CSV Attributes", type=StrProperty) + csv_delimiter: EnumProperty( + items=[(";", ";", ""), (",", ",", ""), (".", ".", ""), ("CUSTOM", "Custom", ""),], + name="IFC CSV Delimiter", + default=";", + ) + csv_custom_delimiter: StringProperty(default="", name="Custom Delimiter") document_information: CollectionProperty(name="Document Information", type=DocumentInformation) active_document_information_index: IntProperty(name="Active Document Information Index") document_references: CollectionProperty(name="Document References", type=DocumentReference) @@ -1439,6 +1365,7 @@ class BIMProperties(PropertyGroup): blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty) clash_sets: CollectionProperty(name="Clash Sets", type=ClashSet) + should_create_clash_snapshots: BoolProperty(name="Create Snapshots", default=True) clash_results_path: StringProperty(name="Clash Results Path") smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path") active_clash_set_index: IntProperty(name="Active Clash Set Index") @@ -1505,15 +1432,6 @@ class BIMProperties(PropertyGroup): presentation_layers: CollectionProperty(name="Presentation Layers", type=PresentationLayer) -class BCFProperties(PropertyGroup): - is_loaded: BoolProperty(name="Is Loaded", default=False) - comment_text_width: IntProperty(name="Comment Text Width", default=40) - name: StringProperty(default="", name="Project Name", update=updateBcfProjectName) - author: StringProperty(default="john@doe.com", name="Author Email", update=updateBcfAuthor) - topics: CollectionProperty(name="BCF Topics", type=BcfTopic) - active_topic_index: IntProperty(name="Active BCF Topic Index", update=refreshBcfTopic) - - class MapConversion(PropertyGroup): eastings: StringProperty(name="Eastings") northings: StringProperty(name="Northings") @@ -1590,7 +1508,7 @@ class BIMObjectProperties(PropertyGroup): has_boundary_condition: BoolProperty(name="Has Boundary Condition") boundary_condition: PointerProperty(name="Boundary Condition", type=BoundaryCondition) structural_member_connection: PointerProperty(name="Structural Member Connection", type=bpy.types.Object) - representation_contexts: CollectionProperty(name="Representation Contexts", type=Subcontext) + representations: CollectionProperty(name="Representations", type=Representation) # Address applies to IfcSite's SiteAddress and IfcBuilding's BuildingAddress address: PointerProperty(name="Address", type=Address) diff --git a/src/ifcblenderexport/blenderbim/bim/ui.py b/src/ifcblenderexport/blenderbim/bim/ui.py index 91b23aa6cd..90ed6a32f4 100644 --- a/src/ifcblenderexport/blenderbim/bim/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/ui.py @@ -1,6 +1,6 @@ import os import bpy -from . import bcfstore +from . import ifc from bpy.types import Panel from bpy.props import StringProperty @@ -638,27 +638,34 @@ class BIM_PT_representations(Panel): layout = self.layout props = context.active_object.BIMObjectProperties - if not props.representation_contexts: + if not props.representations: layout.label(text="No representations found") row = layout.row(align=True) row.prop(bpy.context.scene.BIMProperties, "available_contexts", text="") row.prop(bpy.context.scene.BIMProperties, "available_subcontexts", text="") row.prop(bpy.context.scene.BIMProperties, "available_target_views", text="") - op = row.operator("bim.switch_context", icon="ADD", text="") - op.has_target_context = False + # TODO: reimplement with incremental editing + # op = row.operator("bim.switch_context", icon="ADD", text="") + # op.has_target_context = False - for index, subcontext in enumerate(props.representation_contexts): + self.file = ifc.IfcStore.get_file() + for index, representation in enumerate(props.representations): row = layout.row(align=True) - row.prop(subcontext, "context", text="") - row.prop(subcontext, "name", text="") - row.prop(subcontext, "target_view", text="") - op = row.operator("bim.switch_context", icon="OUTLINER_DATA_MESH", text="") - op.has_target_context = True - op.context_name = subcontext.context - op.subcontext_name = subcontext.name - op.target_view_name = subcontext.target_view - row.operator("bim.remove_context", icon="X", text="").index = index + row.prop(representation, "name", text="") + row.prop(representation, "type", text="") + if not representation.ifc_definition_id: + continue + subcontext = self.file.by_id(representation.ifc_definition_id).ContextOfItems + if subcontext.is_a() == "IfcGeometricRepresentationContext": + continue + # TODO: reimplement with incremental editing + # op = row.operator("bim.switch_context", icon="OUTLINER_DATA_MESH", text="") + # op.has_target_context = True + # op.context_name = subcontext.ContextType + # op.subcontext_name = subcontext.ContextIdentifier + # op.target_view_name = subcontext.TargetView + # row.operator("bim.remove_context", icon="X", text="").index = index class BIM_PT_classification_references(Panel): @@ -1643,216 +1650,13 @@ class BIM_PT_ifccsv(Panel): row.operator("bim.remove_csv_attribute", icon="X", text="").index = index row = layout.row(align=True) - row.operator("bim.export_ifccsv", icon="EXPORT") - row.operator("bim.import_ifccsv", icon="IMPORT") - - -class BIM_PT_bcf(Panel): - bl_label = "BIM Collaboration Format (BCF)" - bl_idname = "BIM_PT_bcf" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - - scene = context.scene - props = bpy.context.scene.BCFProperties + row.prop(props, "csv_delimiter") + row = layout.row(align=True) + row.prop(props, "csv_custom_delimiter") row = layout.row(align=True) - row.operator("bim.new_bcf_project", text="New Project") - row.operator("bim.load_bcf_project", text="Load Project") - - if not props.is_loaded: - return - - row.operator("bim.save_bcf_project", text="Save Project") - - row = layout.row() - row.prop(props, "name") - - row = layout.row() - row.prop(props, "author") - - props = bpy.context.scene.BCFProperties - row = layout.row() - row.template_list("BIM_UL_topics", "", props, "topics", props, "active_topic_index") - col = row.column(align=True) - col.operator("bim.add_bcf_topic", icon="ADD", text="") - if props.active_topic_index < len(props.topics): - topic = props.topics[props.active_topic_index] - col.prop(topic, "is_editable", icon="CHECKMARK" if topic.is_editable else "GREASEPENCIL", icon_only=True) - - if props.active_topic_index < len(props.topics): - topic = props.topics[props.active_topic_index] - row = layout.row() - row.enabled = topic.is_editable - row.prop(topic, "description", text="") - - row = layout.row() - row.prop(topic, "viewpoints") - row.operator("bim.activate_bcf_viewpoint", icon="SCENE", text="") - - col = layout.column(align=True) - if topic.type: - col.prop(topic, "type", emboss=topic.is_editable) - if topic.status: - col.prop(topic, "status", emboss=topic.is_editable) - if topic.priority: - col.prop(topic, "priority", emboss=topic.is_editable) - if topic.stage: - col.prop(topic, "stage", emboss=topic.is_editable) - if topic.assigned_to: - col.prop(topic, "assigned_to", emboss=topic.is_editable) - if topic.due_date: - col.prop(topic, "due_date", emboss=topic.is_editable) - - col = layout.column(align=True) - if topic.modified_date: - col.prop(topic, "modified_date", emboss=False) - col.prop(topic, "modified_author", emboss=False) - else: - col.prop(topic, "creation_date", emboss=False) - col.prop(topic, "creation_author", emboss=False) - - -class BIM_PT_bcf_metadata(Panel): - bl_label = "BCF Metadata" - bl_idname = "BIM_PT_bcf_metadata" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - bl_parent_id = "BIM_PT_bcf" - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - - scene = context.scene - props = bpy.context.scene.BCFProperties - - if props.active_topic_index >= len(props.topics): - layout.label(text="No BCF project is loaded") - return - - topic = props.topics[props.active_topic_index] - bcfxml = bcfstore.BcfStore.get_bcfxml() - bcf_topic = bcfxml.topics[topic.name] - - if bcf_topic.header: - layout.label(text="Header Files:") - for index, f in enumerate(bcf_topic.header.files): - box = self.layout.box() - row = box.row(align=True) - row.label(text=f.filename, icon="FILE_BLANK") - if f.is_external: - row.operator("bim.open_uri", icon="URL", text="").uri = f.reference - else: - op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="") - op.uri = os.path.join(bcfxml.filepath, topic.name, f.reference) - box.label(text=f.date) - #box.label(text=f.ifc_project) - #box.label(text=f.ifc_spatial_structure_element) - - if topic.reference_links: - layout.label(text="Reference Links:") - for index, link in enumerate(topic.reference_links): - row = layout.row(align=True) - row.prop(link, "name") - row.operator("bim.open_uri", icon="URL", text="").uri = link.name - - if topic.labels: - layout.label(text="Labels:") - for index, label in enumerate(topic.labels): - row = layout.row(align=True) - row.prop(label, "name", text="") - - if topic.bim_snippet.schema: - layout.label(text="BIM Snippet:") - row = layout.row(align=True) - row.prop(topic.bim_snippet, "type") - if topic.bim_snippet.schema: - row.operator("bim.open_uri", icon="URL", text="").uri = topic.bim_snippet.schema - - row = layout.row(align=True) - row.prop(topic.bim_snippet, "reference") - if topic.bim_snippet.is_external: - row.operator("bim.open_uri", icon="URL", text="").uri = topic.bim_snippet.reference - else: - op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="") - op.uri = os.path.join(bcfxml.filepath, topic.name, topic.bim_snippet.reference) - - if topic.document_references: - layout.label(text="Document References:") - for index, doc in enumerate(topic.document_references): - box = self.layout.box() - row = box.row(align=True) - row.prop(doc, "reference") - if doc.is_external: - row.operator("bim.open_uri", icon="URL", text="").uri = doc.reference - else: - op = row.operator("bim.open_uri", icon="FILE_FOLDER", text="") - op.uri = os.path.join(bcfxml.filepath, topic.name, doc.reference) - row = box.row(align=True) - row.prop(doc, "description") - - if topic.related_topics: - layout.label(text="Related Topics:") - for related_topic in topic.related_topics: - row = layout.row(align=True) - row.operator("bim.view_bcf_topic", text=related_topic.name).topic_guid = related_topic.name - - -class BIM_PT_bcf_comments(Panel): - bl_label = "BCF Comments" - bl_idname = "BIM_PT_bcf_comments" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - bl_parent_id = "BIM_PT_bcf" - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False - - scene = context.scene - props = bpy.context.scene.BCFProperties - - if props.active_topic_index >= len(props.topics): - layout.label(text="No BCF project is loaded") - return - - row = layout.row() - row.prop(props, "comment_text_width") - - topic = props.topics[props.active_topic_index] - for comment in topic.comments: - box = self.layout.box() - box.separator() - author_text = "{} ({})".format(comment.author, comment.date) - if comment.modified_author: - author_text = "*{} ({})".format(comment.modified_author, comment.modified_date) - box.label(text=author_text, icon="WORDWRAP_ON") - box.separator() - box.scale_y = 0.5 - words = comment.comment.split() - while words: - total_line_chars = 0 - line_words = [] - while words and total_line_chars < props.comment_text_width: - word = words.pop(0) - line_words.append(word) - total_line_chars += len(word) + 1 # 1 is for the space - box.label(text=" ".join(line_words)) - box.separator() + row.operator("bim.export_ifccsv", icon="EXPORT") + row.operator("bim.import_ifccsv", icon="IMPORT") class BIM_PT_qa(Panel): @@ -2092,6 +1896,8 @@ class BIM_PT_mvd(Panel): row = layout.row() row.prop(bim_properties, "import_export_should_roundtrip_native") row = layout.row() + row.prop(bim_properties, "export_should_export_from_memory") + row = layout.row() row.prop(bim_properties, "import_should_use_cpu_multiprocessing") row = layout.row() row.prop(bim_properties, "import_should_import_with_profiling") @@ -2182,6 +1988,17 @@ class BIM_UL_generic(bpy.types.UIList): layout.label(text="", translate=False) +class BIM_UL_drawinglist(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.prop(item, "name", text="", emboss=False) + op = row.operator("bim.open_view_camera", icon="OUTLINER_OB_CAMERA", text="") + op.view_name = item.name + else: + layout.label(text="", translate=False) + + class BIM_UL_topics(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): ob = data @@ -2199,14 +2016,16 @@ class BIM_UL_clash_sets(bpy.types.UIList): else: layout.label(text="", translate=False) + class BIM_UL_smart_groups(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): ob = data if item: - layout.label(text=str(item.number), translate=False, icon='NONE', icon_value=0) + layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0) else: layout.label(text="", translate=False) + class BIM_UL_constraints(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): ob = data @@ -2363,9 +2182,9 @@ class BIM_PT_ifcclash(Panel): row.prop(source, "selector", text="") row = layout.row() + row.prop(props, "should_create_clash_snapshots") + row = layout.row(align=True) row.operator("bim.execute_ifc_clash") - - row = layout.row() row.operator("bim.select_ifc_clash_results") @@ -2397,6 +2216,8 @@ class BIM_PT_annotation_utilities(Panel): row.operator("bim.clean_wireframes") row = layout.row(align=True) row.operator("bim.link_ifc") + row = layout.row(align=True) + row.operator("bim.add_grid") row = layout.row(align=True) op = row.operator("bim.add_annotation", text="Dim", icon="ARROW_LEFTRIGHT") @@ -2429,6 +2250,14 @@ class BIM_PT_annotation_utilities(Panel): op.obj_name = "Section Level" op.data_type = "curve" + row = layout.row(align=True) + op = row.operator("bim.add_annotation", text="Breakline", icon="FCURVE") + op.obj_name = "Break" + op.data_type = "mesh" + op = row.operator("bim.add_annotation", text="Misc", icon="MESH_MONKEY") + op.obj_name = "Misc" + op.data_type = "mesh" + props = bpy.context.scene.DocProperties row = layout.row(align=True) @@ -2440,10 +2269,10 @@ class BIM_PT_annotation_utilities(Panel): op = row.operator("bim.open_view", icon="URL", text="") op.view = props.drawings[props.active_drawing_index].name row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index - layout.template_list("BIM_UL_generic", "", props, "drawings", props, "active_drawing_index") + layout.template_list("BIM_UL_drawinglist", "", props, "drawings", props, "active_drawing_index") - row = layout.row() - row.prop(props, "should_draw_decorations") + layout.prop(props, "should_draw_decorations") + layout.prop(props, "decorations_colour") class BIM_PT_qto_utilities(Panel): @@ -2469,6 +2298,7 @@ class BIM_PT_qto_utilities(Panel): row = layout.row(align=True) row.operator("bim.calculate_object_volumes") + class BIM_PT_clash_manager(Panel): bl_idname = "BIM_PT_clash_manager" bl_label = "Clash Manager" @@ -2503,11 +2333,12 @@ class BIM_PT_clash_manager(Panel): row = layout.row(align=True) row.operator("bim.load_smart_groups_for_active_clash_set") - layout.template_list('BIM_UL_smart_groups', '', props, 'smart_clash_groups', props, 'active_smart_group_index') + layout.template_list("BIM_UL_smart_groups", "", props, "smart_clash_groups", props, "active_smart_group_index") row = layout.row(align=True) row.operator("bim.select_smart_group") + class BIM_PT_misc_utilities(Panel): bl_idname = "BIM_PT_misc_utilities" bl_label = "Miscellaneous" @@ -2525,6 +2356,8 @@ class BIM_PT_misc_utilities(Panel): row.operator("bim.set_override_colour") row = layout.row(align=True) row.operator("bim.set_viewport_shadow_from_sun") + row = layout.row(align=True) + row.operator("bim.snap_spaces_together") class BIM_PT_debug(Panel): diff --git a/src/ifcclash/ifcclash.py b/src/ifcclash/ifcclash.py index 449c881481..a17cc79d00 100644 --- a/src/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash.py @@ -11,6 +11,7 @@ import sys import argparse import logging + class Mesh: faces: [] vertices: [] @@ -153,6 +154,77 @@ class IfcClasher: return None def export(self): + if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": + return self.export_bcfxml() + self.export_json() + + def export_bcfxml(self): + import bcf + import bcf.bcfxml + + for i, clash_set in enumerate(self.clash_sets): + bcfxml = bcf.bcfxml.BcfXml() + bcfxml.new_project() + bcfxml.project.name = clash_set["name"] + bcfxml.edit_project() + for key, clash in clash_set["clashes"].items(): + topic = bcf.data.Topic() + topic.title = "{}/{} and {}/{}".format( + clash["a_ifc_class"], clash["a_name"], clash["b_ifc_class"], clash["b_name"] + ) + topic = bcfxml.add_topic(topic) + viewpoint = bcf.data.Viewpoint() + viewpoint.perspective_camera = bcf.data.PerspectiveCamera() + position = np.array(clash["position"]) + point = position + np.array((5, 5, 5)) # Dumb, but works! + viewpoint.perspective_camera.camera_view_point.x = point[0] + viewpoint.perspective_camera.camera_view_point.y = point[1] + viewpoint.perspective_camera.camera_view_point.z = point[2] + mat = self.get_track_to_matrix(point, position) + viewpoint.perspective_camera.camera_direction.x = mat[0][2] * -1 + viewpoint.perspective_camera.camera_direction.y = mat[1][2] * -1 + viewpoint.perspective_camera.camera_direction.z = mat[2][2] * -1 + viewpoint.perspective_camera.camera_up_vector.x = mat[0][1] + viewpoint.perspective_camera.camera_up_vector.y = mat[1][1] + viewpoint.perspective_camera.camera_up_vector.z = mat[2][1] + viewpoint.components = bcf.data.Components() + c1 = bcf.data.Component() + c1.ifc_guid = clash["a_global_id"] + c2 = bcf.data.Component() + c2.ifc_guid = clash["b_global_id"] + viewpoint.components.selection.append(c1) + viewpoint.components.selection.append(c2) + viewpoint.components.visibility = bcf.data.ComponentVisibility() + viewpoint.components.visibility.default_visibility = True + viewpoint.snapshot = self.get_viewpoint_snapshot(viewpoint, mat) + bcfxml.add_viewpoint(topic, viewpoint) + if i == 0: + bcfxml.save_project(self.settings.output) + else: + bcfxml.save_project(self.settings.output + f".{i}") + + def get_viewpoint_snapshot(self, viewpoint, mat): + return None # Possible to overload this function in a GUI application if used as a library + + # https://blender.stackexchange.com/questions/68834/recreate-to-track-quat-with-two-vectors-using-python/141706#141706 + def get_track_to_matrix(self, camera_position, target_position): + camera_direction = camera_position - target_position + camera_direction = camera_direction / np.linalg.norm(camera_direction) + camera_right = np.cross(np.array([0.0, 0.0, 1.0]), camera_direction) + camera_right = camera_right / np.linalg.norm(camera_right) + camera_up = np.cross(camera_direction, camera_right) + camera_up = camera_up / np.linalg.norm(camera_up) + rotation_transform = np.zeros((4, 4)) + rotation_transform[0, :3] = camera_right + rotation_transform[1, :3] = camera_up + rotation_transform[2, :3] = camera_direction + rotation_transform[-1, -1] = 1 + translation_transform = np.eye(4) + translation_transform[:3, -1] = - camera_position + look_at_transform = np.matmul(rotation_transform, translation_transform) + return np.linalg.inv(look_at_transform) + + def export_json(self): results = self.clash_sets.copy() for result in results: del result["a_cm"] @@ -269,14 +341,14 @@ class IfcClasher: def smart_group_clashes(self, clash_sets, max_clustering_distance): from sklearn.cluster import OPTICS from collections import defaultdict - + count_of_input_clashes = 0 count_of_clash_sets = 0 count_of_smart_groups = 0 count_of_final_clash_sets = 0 count_of_clash_sets = len(clash_sets) - + for clash_set in clash_sets: if not "clashes" in clash_set.keys(): print(f"Skipping clash set [{clash_set['name']}] since it contains no clash results.") @@ -324,14 +396,14 @@ class IfcClasher: if not "clashes" in clash_set.keys(): continue smart_groups = defaultdict(list) - for clash_id, content in clash_set['clashes'].items(): + for clash_id, content in clash_set["clashes"].items(): if "smart_group" in content: object_id_list = list() # Clash has been grouped, let's extract it. - object_id_list.append(content['a_global_id']) - object_id_list.append(content['b_global_id']) - smart_groups[content['smart_group']].append(object_id_list) - count_of_smart_groups += len(smart_groups) + object_id_list.append(content["a_global_id"]) + object_id_list.append(content["b_global_id"]) + smart_groups[content["smart_group"]].append(object_id_list) + count_of_smart_groups += len(smart_groups) output_clash_sets[clash_set["name"]].append(smart_groups) # Rename the clash groups to something more sensible @@ -346,8 +418,10 @@ class IfcClasher: i += 1 count_of_final_clash_sets = len(output_clash_sets) - print(f"Took {count_of_input_clashes} clashes in {count_of_clash_sets} clash sets and turned", - f"them into {count_of_smart_groups} smart groups in {count_of_final_clash_sets} clash sets") + print( + f"Took {count_of_input_clashes} clashes in {count_of_clash_sets} clash sets and turned", + f"them into {count_of_smart_groups} smart groups in {count_of_final_clash_sets} clash sets", + ) return output_clash_sets diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index 65399479b4..e5c048b71d 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -370,6 +370,12 @@ int main(int argc, char** argv) { "Element at which vertical cross sections should be created") ("elevation-ref", po::value(&elevation_ref), "Element at which vertical elevations should be created") + ("auto-section", + "Creates SVG cross section drawings automatically based on model extents") + ("auto-elevation", + "Creates SVG elevation drawings automatically based on model extents") + ("svg-xmlns", + "Stores name and guid in a separate namespace as opposed to data-name, data-guid") ("door-arcs", "Draw door openings arcs for IfcDoor elements") ("section-height", po::value(§ion_height), "Specifies the cut section height for SVG 2D geometry.") @@ -949,6 +955,13 @@ int main(int argc, char** argv) { if (vmap.count("elevation-ref")) { static_cast(serializer.get())->setElevationRef(elevation_ref); } + if (vmap.count("auto-section")) { + static_cast(serializer.get())->setAutoSection(true); + } + if (vmap.count("auto-elevation")) { + static_cast(serializer.get())->setAutoElevation(true); + } + static_cast(serializer.get())->setUseNamespace(vmap.count("svg-xmlns") > 0); if (relative_center_x && relative_center_y) { static_cast(serializer.get())->setDrawingCenter(*relative_center_x, *relative_center_y); } diff --git a/src/ifccsv/ifccsv.py b/src/ifccsv/ifccsv.py index 19aa94195a..26d66092a0 100755 --- a/src/ifccsv/ifccsv.py +++ b/src/ifccsv/ifccsv.py @@ -109,6 +109,7 @@ class IfcCsv: self.attributes = [] self.output = "" self.selector = None + self.delimiter = ";" def export(self, ifc_file, elements): self.ifc_file = ifc_file @@ -151,7 +152,7 @@ class IfcCsv: def Import(self, ifc): ifc_file = ifcopenshell.open(ifc) with open(self.output, newline="", encoding="utf-8") as f: - reader = csv.reader(f) + reader = csv.reader(f, delimiter=self.delimiter) headers = [] for row in reader: if not headers: diff --git a/src/ifcsverchok/nodes/ifc/get_attribute.py b/src/ifcsverchok/nodes/ifc/get_attribute.py index 8c7280fc8f..deb98421d2 100644 --- a/src/ifcsverchok/nodes/ifc/get_attribute.py +++ b/src/ifcsverchok/nodes/ifc/get_attribute.py @@ -19,9 +19,15 @@ class SvIfcGetAttribute(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper self.outputs.new("SvStringsSocket", "value") def process(self): - self.sv_input_names = ["entity", "attribute_name"] self.value_out = [] - super().process() + entity_nested_inputs = self.inputs["entity"].sv_get() + attribute_name = self.inputs["attribute_name"].sv_get()[0][0] + for entities in entity_nested_inputs: + if hasattr(entities, "__iter__"): + for entity in entities: + self.process_ifc(entity, attribute_name) + else: + self.process_ifc(entities, attribute_name) self.outputs["value"].sv_set(self.value_out) def process_ifc(self, entity, attribute_name): diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index 35c7259522..1d2381436d 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -397,7 +397,7 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) { double elev = p ? p->second : std::numeric_limits::quiet_NaN(); geometry_data data{ compound_local, trsf, brep_obj->product(), storey, elev, brep_obj->name(), nameElement(storey, brep_obj) }; - if (buffer_elements_) { + if (auto_section_ || auto_elevation_ || section_ref_ || elevation_ref_) { element_buffer_.push_back(data); } @@ -533,6 +533,7 @@ void SvgSerializer::write(const geometry_data& data) { std::pair range; gp_Vec projection_direction; + gp_Pln projection_plane; IfcUtil::IfcBaseEntity* storey = nullptr; std::string drawing_name; @@ -555,6 +556,7 @@ void SvgSerializer::write(const geometry_data& data) { } else if (variant.which() == 2) { const auto& section = boost::get(variant); projection_direction = section.plane.Axis().Direction(); + projection_plane = section.plane; drawing_name = section.name; use_hlr = section.with_projection; } @@ -562,7 +564,40 @@ void SvgSerializer::write(const geometry_data& data) { auto& compound_to_use = is_floor_plan_ ? compound : compound_unmirrored; if (use_hlr && hlr) { - hlr->Add(compound_to_use); + + // Check if any of the bounding box points is on the correct side of the plane + Bnd_Box bb; + try { + BRepBndLib::Add(compound_to_use, bb); + } + catch (const Standard_Failure&) {} + + if (bb.IsVoid()) { + continue; + } + + double xs[2], ys[2], zs[2]; + bb.Get(xs[0], ys[0], zs[0], xs[1], ys[1], zs[1]); + + bool any = false; + + for (int i = 0; i < 8; ++i) { + gp_Pnt p(xs[(i & 1) == 1], ys[(i & 2) == 2], zs[(i & 4) == 4]); + auto d = (p.XYZ() - projection_plane.Location().XYZ()).Dot(projection_plane.Axis().Direction().XYZ()); + int state; + if (std::abs(d) < 1.e-5) { + state = 0; + } else { + state = d < 0. ? -1 : 1; + } + if (state == 1) { + any = true; + } + } + + if (any) { + hlr->Add(compound_to_use); + } } TopoDS_Iterator it(compound_to_use); @@ -865,13 +900,16 @@ void SvgSerializer::resize() { sc = (*scale_) * 1000; cx = (xmax + xmin) / 2. * sc - width * center_x_.get_value_or(0.5); cy = (ymax + ymin) / 2. * sc - height * center_y_.get_value_or(0.5); - } else { + } + else { if (calculated_scale_) { sc = *calculated_scale_; - } else { + } + else { if (dx / width > dy / height) { sc = width / dx; - } else { + } + else { sc = height / dy; } calculated_scale_ = sc; @@ -893,8 +931,12 @@ void SvgSerializer::resize() { (*it)->value() *= sc; } } +} +void SvgSerializer::resetScale() { // reset the bounding box, as a subsequent drawing (elevation, section) will be centered, but use the same scale. + // this is a separate call now as we first need to read drawing extents for automatically positioning sections and + // elevations xmin = +std::numeric_limits::infinity(); ymin = +std::numeric_limits::infinity(); xmax = -std::numeric_limits::infinity(); @@ -904,6 +946,63 @@ void SvgSerializer::resize() { void SvgSerializer::finalize() { resize(); + if (!deferred_section_data_.is_initialized() && (auto_section_ || auto_elevation_)) { + deferred_section_data_.emplace(); + } + + // @nb keep in mind Y-axis is negated in these 6 definitions to account + // for coordinate system differences. + if (auto_section_) { + { + gp_Pln pln(gp_Ax3( + gp_Pnt((xmin + xmax) / 2., (ymin + ymax) / 2., 0.), + gp_Dir(-1, 0, 0), + gp_Dir(0, -1, 0))); + deferred_section_data_->push_back(vertical_section{ pln , "Section North South", false }); + } + { + gp_Pln pln(gp_Ax3( + gp_Pnt((xmin + xmax) / 2., (ymin + ymax) / -2., 0.), + gp_Dir(0, -1, 0), + gp_Dir(-1, 0, 0))); + deferred_section_data_->push_back(vertical_section{ pln , "Section East West", true }); + } + } + + if (auto_elevation_) { + { + gp_Pln pln(gp_Ax3( + gp_Pnt((xmin + xmax) / 2., -(ymin - 1.e-1), 0.), + gp_Dir(0, -1, 0), + gp_Dir(-1, 0, 0))); + deferred_section_data_->push_back(vertical_section{ pln , "Elevation South", true }); + } + { + gp_Pln pln(gp_Ax3( + gp_Pnt(xmax + 1.e-1, (ymin + ymax) / -2., 0.), + gp_Dir(-1, 0, 0), + gp_Dir(0, -1, 0))); + deferred_section_data_->push_back(vertical_section{ pln , "Elevation East", true }); + } + { + gp_Pln pln(gp_Ax3( + gp_Pnt((xmin + xmax) / 2., -(ymax + 1.e-1), 0.), + gp_Dir(0, -1, 0), + gp_Dir(1, 0, 0))); + deferred_section_data_->push_back(vertical_section{ pln , "Elevation North", true }); + } + { + gp_Pln pln(gp_Ax3( + gp_Pnt(xmin - 1.e-1, (ymin + ymax) / -2., 0.), + gp_Dir(-1, 0, 0), + gp_Dir(0, -1, 0))); + deferred_section_data_->push_back(vertical_section{ pln , "Elevation West", true }); + } + } + + resetScale(); + + if (deferred_section_data_ && deferred_section_data_->size() && element_buffer_.size()) { // Draw door arcs only on floor plans. @@ -922,7 +1021,7 @@ void SvgSerializer::finalize() { hlr = new HLRBRep_Algo; } - *section_data_ = { sd }; + section_data_ = std::vector{ sd }; for (auto& e : element_buffer_) { write(e); } @@ -939,35 +1038,40 @@ void SvgSerializer::finalize() { HLRBRep_HLRToShape hlr_shapes(hlr); auto hlr_compound_unmirrored = hlr_shapes.VCompound(); - // Compound 3D curves for mirroring to work - ShapeFix_Edge sfe; - TopExp_Explorer exp(hlr_compound_unmirrored, TopAbs_EDGE); - for (; exp.More(); exp.Next()) { - sfe.FixAddCurve3d(TopoDS::Edge(exp.Current())); + if (!hlr_compound_unmirrored.IsNull()) { + + // Compound 3D curves for mirroring to work + ShapeFix_Edge sfe; + TopExp_Explorer exp(hlr_compound_unmirrored, TopAbs_EDGE); + for (; exp.More(); exp.Next()) { + sfe.FixAddCurve3d(TopoDS::Edge(exp.Current())); + } + + // Mirror to match SVG coord system. + // @todo this is very wasteful. We better do the Y-mirror in the SVG writing and + // not on the TopoDS_Shape input. + + gp_Trsf trsf_mirror; + trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); + BRepBuilderAPI_Transform make_transform_mirror(hlr_compound_unmirrored, trsf_mirror, true); + make_transform_mirror.Build(); + auto hlr_compound = make_transform_mirror.Shape(); + + exp.Init(hlr_compound, TopAbs_EDGE); + BRep_Builder B; + auto& po = start_path(drawing_name, "class=\"projection\""); + for (; exp.More(); exp.Next()) { + TopoDS_Wire w; + B.MakeWire(w); + B.Add(w, exp.Current()); + write(po, w); + } + } - - // Mirror to match SVG coord system. - // @todo this is very wasteful. We better do the Y-mirror in the SVG writing and - // not on the TopoDS_Shape input. - - gp_Trsf trsf_mirror; - trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY())); - BRepBuilderAPI_Transform make_transform_mirror(hlr_compound_unmirrored, trsf_mirror, true); - make_transform_mirror.Build(); - auto hlr_compound = make_transform_mirror.Shape(); - - exp.Init(hlr_compound, TopAbs_EDGE); - BRep_Builder B; - auto& po = start_path(drawing_name, "class=\"projection\""); - for (; exp.More(); exp.Next()) { - TopoDS_Wire w; - B.MakeWire(w); - B.Add(w, exp.Current()); - write(po, w); - } } resize(); + resetScale(); if (use_hlr) { hlr.Nullify(); @@ -987,7 +1091,9 @@ void SvgSerializer::finalize() { if (it->first.first) { svg_file << " first.first) << ">\n"; } else { - svg_file << " first.second << "\" class=\"section\">\n"; + auto n = it->first.second; + IfcUtil::escape_xml(n); + svg_file << " \n"; } } svg_file << " second.first << ">\n"; @@ -1007,6 +1113,9 @@ void SvgSerializer::finalize() { void SvgSerializer::writeHeader() { svg_file << "* elem) { + auto n = elem->name(); + IfcUtil::escape_xml(n); + return nameElement_({ {"id", with_section_heights_from_storey_ ? object_id(storey, elem) : GeometrySerializer::object_id(elem)}, {"class", elem->type()}, - {"data-name", elem->name()}, - {"data-guid", elem->guid()} + {namespace_prefix_ + "name", n}, + {namespace_prefix_ + "guid", elem->guid()} }); } @@ -1099,13 +1211,14 @@ std::string SvgSerializer::nameElement(const IfcUtil::IfcBaseEntity* elem) { std::string ifc_name; if (!elem->get("Name")->isNull()) { ifc_name = (std::string) *elem->get("Name"); + IfcUtil::escape_xml(ifc_name); } return nameElement_({ {"id", idElement(elem)}, {"class", entity}, - {"data-name", ifc_name}, - {"data-guid", *elem->get("GlobalId")} + {namespace_prefix_ + "name", ifc_name}, + {namespace_prefix_ + "guid", *elem->get("GlobalId")} }); } diff --git a/src/serializers/SvgSerializer.h b/src/serializers/SvgSerializer.h index b69e1feb71..f483a69a47 100644 --- a/src/serializers/SvgSerializer.h +++ b/src/serializers/SvgSerializer.h @@ -119,7 +119,9 @@ protected: boost::optional scale_, calculated_scale_, center_x_, center_y_; bool with_section_heights_from_storey_, rescale, print_space_names_, print_space_areas_; - bool draw_door_arcs_, buffer_elements_, is_floor_plan_; + bool draw_door_arcs_, is_floor_plan_; + bool auto_section_, auto_elevation_; + bool use_namespace_; IfcParse::IfcFile* file; IfcUtil::IfcBaseEntity* storey_; @@ -133,6 +135,8 @@ protected: std::list element_buffer_; Handle(HLRBRep_Algo) hlr; + + std::string namespace_prefix_; public: SvgSerializer(const std::string& out_filename, const SerializerSettings& settings) : GeometrySerializer(settings) @@ -146,13 +150,16 @@ public: , print_space_names_(false) , print_space_areas_(false) , draw_door_arcs_(false) - , buffer_elements_(false) , is_floor_plan_(true) + , auto_section_(false) + , auto_elevation_(false) + , use_namespace_(false) , file(0) , storey_(0) , xcoords_begin(0) , ycoords_begin(0) , radii_begin(0) + , namespace_prefix_("data-") {} void addXCoordinate(const boost::shared_ptr& fi) { xcoords.push_back(fi); } void addYCoordinate(const boost::shared_ptr& fi) { ycoords.push_back(fi); } @@ -176,15 +183,30 @@ public: void setPrintSpaceNames(bool b) { print_space_names_ = b; } void setPrintSpaceAreas(bool b) { print_space_areas_ = b; } void setDrawDoorArcs(bool b) { draw_door_arcs_ = b; } + void resize(); + void resetScale(); + void setSectionRef(const boost::optional& s) { section_ref_ = s; - buffer_elements_ = true; } void setElevationRef(const boost::optional& s) { elevation_ref_ = s; - buffer_elements_ = true; } + + void setAutoSection(bool b) { + auto_section_ = b; + } + + void setAutoElevation(bool b) { + auto_elevation_ = b; + } + + void setUseNamespace(bool b) { + use_namespace_ = b; + namespace_prefix_ = use_namespace_ ? "ifc:" : "data-"; + } + void setScale(double s) { scale_ = s; } void setDrawingCenter(double x, double y) { center_x_ = x; center_y_ = y;