diff --git a/.gitignore b/.gitignore index 29a0f337d9..2d0f1d3b67 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,10 @@ __pycache__ # PyCharm files .idea +#Virtual Env Files +Pipfile +Pipfile.lock + # Docs /docs/output /docs/rst_files diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 0907cf9739..114671a4e6 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -280,7 +280,7 @@ ENDIF() # Use the found libTKernel as a template for all other OCC libraries # TODO Extract this into macro/function foreach(lib ${OPENCASCADE_LIBRARY_NAMES}) - # Make sure we'll handle the Windows/MSVC debug postfix convetion too. + # Make sure we'll handle the Windows/MSVC debug postfix convention too. string(REPLACE TKerneld "${lib}" lib_path "${libTKernel}") string(REPLACE TKernel "${lib}" lib_path "${lib_path}") list(APPEND OPENCASCADE_LIBRARIES "${lib_path}") @@ -355,7 +355,7 @@ IF(COLLADA_SUPPORT AND BUILD_CONVERT) # Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries foreach(lib ${OPENCOLLADA_LIBRARY_NAMES}) - # Make sure we'll handle the Windows/MSVC debug postfix convetion too. + # Make sure we'll handle the Windows/MSVC debug postfix convention too. string(REPLACE OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}") string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}") list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}") @@ -496,6 +496,10 @@ endfunction() set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2") +foreach(s ${SCHEMA_VERSIONS}) + add_definitions(-DHAS_SCHEMA_${s}) +endforeach() + if(COMPILE_SCHEMA) # @todo, this appears to be untested at the moment @@ -573,8 +577,35 @@ if (BUILD_CONVERT) endif() # IfcParse -file(GLOB IFCPARSE_H_FILES ../src/ifcparse/*.h) -file(GLOB IFCPARSE_CPP_FILES ../src/ifcparse/*.cpp) +file(GLOB IFCPARSE_H_FILES_ALL ../src/ifcparse/*.h) +file(GLOB IFCPARSE_CPP_FILES_ALL ../src/ifcparse/*.cpp) + +foreach(s ${IFCPARSE_H_FILES_ALL}) +get_filename_component(p "${s}" NAME) +if (NOT "${p}" MATCHES "[0-9]") +list(APPEND IFCPARSE_H_FILES "${s}") +endif() +endforeach() + +foreach(s ${IFCPARSE_CPP_FILES_ALL}) +get_filename_component(p "${s}" NAME) +if (NOT "${p}" MATCHES "[0-9]") +list(APPEND IFCPARSE_CPP_FILES "${s}") +endif() +endforeach() + +foreach(s ${SCHEMA_VERSIONS}) + list(APPEND IFCPARSE_H_FILES + ../src/ifcparse/Ifc${s}.h + ../src/ifcparse/Ifc${s}-definitions.h + ) + list(APPEND IFCPARSE_CPP_FILES + ../src/ifcparse/Ifc${s}.cpp + ../src/ifcparse/Ifc${s}-schema.cpp + ) +endforeach() + + set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES}) add_library(IfcParse ${IFCPARSE_FILES}) diff --git a/nix/build-all.py b/nix/build-all.py index 0871e52633..efd0983b4a 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -310,7 +310,7 @@ def run(cmds, cwd=None): BOOST_VERSION_UNDERSCORE=BOOST_VERSION.replace(".", "_") OCE_LOCATION="https://github.com/tpaviot/oce/archive/OCE-%s.tar.gz" % (OCE_VERSION,) -BOOST_LOCATION="https://dl.bintray.com/boostorg/release/%s/source/" % (BOOST_VERSION,) +BOOST_LOCATION="https://boostorg.jfrog.io/artifactory/main/release/%s/source/" % (BOOST_VERSION,) # Helper functions diff --git a/src/bcf/README.md b/src/bcf/README.md index 90c3fd71ca..1ce991d97f 100644 --- a/src/bcf/README.md +++ b/src/bcf/README.md @@ -4,23 +4,26 @@ A simple Python implementation of BCF. The data model is described in `data.py`. Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API is available via `bcfapi.py`. -Currently supports BCF version 2.1. + - BCF-XML version 2.1: Fully supported + - BCF-API version 2.1: Not supported, will probably tackle this after BCF-API v3.0 + - BCF-XML version 3.0: Almost fully supported, except for the documents module + - BCF-API version 3.0: Not supported, but work underway to support it ## bcfxml The `bcfxml` module lets you interact with the BCF-XML standard. ``` -from bcf.bcfxml import BcfXml +from bcf import bcfxml -bcfxml = BcfXml() # Load a project -project = bcfxml.get_project("/path/to/file.bcf") +bcfxml = bcfxml.load("/path/to/file.bcf") + # The project is also stored in the module # project == bcfxml.project - +project=bcfxml.get_project() print(project.name) # To edit a project, just modify the object directly diff --git a/src/bcf/bcf/__init__.py b/src/bcf/bcf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bcf/bcf/bcfxml.py b/src/bcf/bcf/bcfxml.py index 13885b7f7d..28b25a8db1 100644 --- a/src/bcf/bcf/bcfxml.py +++ b/src/bcf/bcf/bcfxml.py @@ -1,765 +1,39 @@ -import os -import uuid -import shutil +import os.path import zipfile -import logging import tempfile -import bcf.data -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__)) +def load(filepath): + filepath = extract_project(filepath) + if os.path.isfile(os.path.join(filepath, "bcf.version")): + version_path = os.path.join(filepath, "bcf.version") + version_id = get_version(version_path) + if version_id == "2.1": + from bcf.v2.bcfxml import BcfXml - -@contextmanager -def cd(newdir): - prevdir = os.getcwd() - os.chdir(os.path.expanduser(newdir)) - try: - yield - finally: - os.chdir(prevdir) - - -class BcfXml: - def __init__(self): - self.filepath = None - self.logger = logging.getLogger("bcfxml") - self.author = "john@doe.com" - self.project = bcf.data.Project() - self.version = "2.1" - self.topics = {} - - def new_project(self): - self.project.project_id = str(uuid.uuid4()) - self.project.name = "New Project" - self.topics = {} - if self.filepath: - self.close_project() - self.filepath = tempfile.mkdtemp() - self.edit_project() - self.edit_version() - - def get_project(self, filepath=None): - if not filepath: - return self.project - zip_file = zipfile.ZipFile(filepath) - self.filepath = tempfile.mkdtemp() - zip_file.extractall(self.filepath) - if os.path.isfile(os.path.join(self.filepath, "project.bcfp")): - data = self._read_xml("project.bcfp", "project.xsd") - self.project.extension_schema = data["ExtensionSchema"] - if "Project" in data: - self.project.project_id = data["Project"]["@ProjectId"] - self.project.name = data["Project"].get("Name") - return self.project - - def edit_project(self): - self.document = minidom.Document() - root = self._create_element(self.document, "ProjectExtension") - project = self._create_element(root, "Project", {"ProjectId": self.project.project_id}) - self._create_element(project, "Name", text=self.project.name) - self._create_element(root, "ExtensionSchema", text="extensions.xsd") - with open(os.path.join(self.filepath, "project.bcfp"), "wb") as f: - f.write(self.document.toprettyxml(encoding="utf-8")) - - def save_project(self, filepath): - with cd(self.filepath): - zip_file = zipfile.ZipFile(filepath, "w", zipfile.ZIP_DEFLATED) - for root, dirs, files in os.walk("./"): - for file in files: - zip_file.write(os.path.join(root, file)) - zip_file.close() - - def get_version(self): - data = self._read_xml("bcf.version", "version.xsd") - self.version = data["@VersionId"] - return self.version - - def edit_version(self): - self.document = minidom.Document() - root = self._create_element(self.document, "Version", {"VersionId": self.version}) - version = self._create_element(root, "DetailedVersion", text=self.version) - with open(os.path.join(self.filepath, "bcf.version"), "wb") as f: - f.write(self.document.toprettyxml(encoding="utf-8")) - - def get_topics(self): - self.topics = {} - topics = [] - subdirs = [] - for (dirpath, dirnames, filenames) in os.walk(self.filepath): - subdirs = dirnames - break - for subdir in subdirs: - self.topics[subdir] = self.get_topic(subdir) - return self.topics - - def get_header(self, guid): - data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") - if "Header" not in data: - return - header = bcf.data.Header() - for item in data["Header"]["File"]: - header_file = bcf.data.HeaderFile() - optional_keys = { - "filename": "Filename", - "date": "Date", - "reference": "Reference", - "ifc_project": "@IfcProject", - "ifc_spatial_structure_element": "@IfcSpatialStructureElement", - "is_external": "@isExternal", - } - for key, value in optional_keys.items(): - if value in item: - setattr(header_file, key, item[value]) - header.files.append(header_file) - self.topics[guid].header = header - return header - - def get_topic(self, guid): - if guid in self.topics: - return self.topics[guid] - data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") - topic = bcf.data.Topic() - self.topics[guid] = topic - - mandatory_keys = { - "guid": "@Guid", - "title": "Title", - "creation_date": "CreationDate", - "creation_author": "CreationAuthor", - } - for key, value in mandatory_keys.items(): - setattr(topic, key, data["Topic"][value]) - - optional_keys = { - "priority": "Priority", - "index": "Index", - "labels": "Labels", - "reference_links": "ReferenceLink", - "modified_date": "ModifiedDate", - "modified_author": "ModifiedAuthor", - "due_date": "DueDate", - "assigned_to": "AssignedTo", - "stage": "Stage", - "description": "Description", - "topic_status": "@TopicStatus", - "topic_type": "@TopicType", - } - for key, value in optional_keys.items(): - if value in data["Topic"]: - setattr(topic, key, data["Topic"][value]) - - if "BimSnippet" in data["Topic"]: - bim_snippet = bcf.data.BimSnippet() - keys = { - "snippet_type": "@SnippetType", - "is_external": "@IsExternal", - "reference": "Reference", - "reference_schema": "ReferenceSchema", - } - for key, value in keys.items(): - if value in data["Topic"]["BimSnippet"]: - setattr(bim_snippet, key, data["Topic"]["BimSnippet"][value]) - topic.bim_snippet = bim_snippet - - if "DocumentReference" in data["Topic"]: - for item in data["Topic"]["DocumentReference"]: - document_reference = bcf.data.DocumentReference() - keys = { - "referenced_document": "ReferencedDocument", - "is_external": "@IsExternal", - "guid": "@Guid", - "description": "Description", - } - for key, value in keys.items(): - if value in item: - setattr(document_reference, key, item[value]) - topic.document_references.append(document_reference) - - if "RelatedTopic" in data["Topic"]: - for item in data["Topic"]["RelatedTopic"]: - related_topic = bcf.data.RelatedTopic() - related_topic.guid = item["@Guid"] - topic.related_topics.append(related_topic) - return topic - - def add_topic(self, topic=None): - if topic is None: - topic = bcf.data.Topic() - if not topic.guid: - topic.guid = str(uuid.uuid4()) - if not topic.title: - topic.title = "New Topic" - os.mkdir(os.path.join(self.filepath, topic.guid)) - self.edit_topic(topic) - return topic - - def edit_topic(self, topic): - if not topic.creation_date: - topic.creation_date = datetime.utcnow().isoformat() - topic.creation_author = self.author + bcfxml = BcfXml() + bcfxml.filepath = filepath + return bcfxml else: - topic.modified_date = datetime.utcnow().isoformat() - topic.modified_author = self.author + from bcf.v3.bcfxml import BcfXml - self.document = minidom.Document() - root = self._create_element(self.document, "Markup") + bcfxml = BcfXml() + bcfxml.filepath = filepath + return bcfxml - self.write_header(topic.header, root) - topic_el = self._create_element( - root, - "Topic", - { - "Guid": topic.guid, - "TopicType": topic.topic_type, - "TopicStatus": topic.topic_status, - }, - ) +def get_version(version_path): + xmlparse = minidom.parse(version_path) + version_el = xmlparse.getElementsByTagName("Version")[0] + version = version_el.getAttribute("VersionId") + return version - 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, - "ModifiedAuthor": topic.modified_author, - "DueDate": topic.due_date, - "AssignedTo": topic.assigned_to, - "Stage": topic.stage, - "Description": topic.description, - } - for key, value in text_map.items(): - if value: - self._create_element(topic_el, key, text=value) - - if topic.bim_snippet: - bim_snippet = self._create_element( - topic_el, - "BimSnippet", - {"SnippetType": topic.bim_snippet.snippet_type, "isExternal": topic.bim_snippet.is_external}, - ) - self._create_element(bim_snippet, "Reference", text=topic.bim_snippet.reference) - self._create_element(bim_snippet, "ReferenceSchema", text=topic.bim_snippet.reference_schema) - for reference in topic.document_references: - reference_el = self._create_element( - topic_el, "DocumentReference", {"Guid": reference.guid, "isExternal": reference.is_external} - ) - self._create_element(reference_el, "ReferencedDocument", text=reference.referenced_document) - self._create_element(reference_el, "Description", text=reference.description) - for related_topic in topic.related_topics: - self._create_element(topic_el, "RelatedTopic", {"Guid": related_topic.guid}) - - self.write_comments(topic.comments, root) - self.write_viewpoints(topic.viewpoints, root, topic) - - with open(os.path.join(self.filepath, topic.guid, "markup.bcf"), "wb") as f: - f.write(self.document.toprettyxml(encoding="utf-8")) - - def write_header(self, header, root): - if not header or not header.files: - return - header_el = self._create_element(root, "Header") - for f in header.files: - file_el = self._create_element( - header_el, - "File", - { - "IfcProject": f.ifc_project, - "IfcSpatialStructureElement": f.ifc_spatial_structure_element, - "isExternal": f.is_external, - }, - ) - self._create_element(file_el, "Filename", text=f.filename) - self._create_element(file_el, "Date", text=f.date) - self._create_element(file_el, "Reference", text=f.reference) - - def write_comments(self, comments, root): - for comment in comments.values(): - comment_el = self._create_element(root, "Comment", {"Guid": comment.guid}) - text_map = { - "Date": comment.date, - "Author": comment.author, - "Comment": comment.comment, - "ModifiedDate": comment.modified_date, - "ModifiedAuthor": comment.modified_author, - } - for key, value in text_map.items(): - if value: - self._create_element(comment_el, key, text=value) - if comment.viewpoint: - self._create_element(comment_el, "Viewpoint", {"Guid": comment.viewpoint.guid}) - - 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'." - topic.comments[comment.guid] = comment - self.edit_comment(comment, topic) - - def edit_comment(self, comment, topic): - if not comment.date: - comment.date = datetime.utcnow().isoformat() - comment.author = self.author - else: - comment.modified_date = datetime.utcnow().isoformat() - comment.modified_author = self.author - self.edit_topic(topic) - - def delete_comment(self, guid, topic): - if guid in topic.comments: - del topic.comments[guid] - self.edit_topic(topic) - - def delete_topic(self, guid): - if guid in self.topics: - del self.topics[guid] - shutil.rmtree(os.path.join(self.filepath, guid)) - - def write_viewpoints(self, viewpoints, root, topic): - for viewpoint in viewpoints.values(): - viewpoint_el = self._create_element(root, "Viewpoints", {"Guid": viewpoint.guid}) - text_map = {"Viewpoint": viewpoint.viewpoint, "Snapshot": viewpoint.snapshot, "Index": viewpoint.index} - for key, value in text_map.items(): - if value: - self._create_element(viewpoint_el, key, text=value) - self.write_viewpoint(viewpoint, topic) - - def write_viewpoint(self, viewpoint, topic): - document = minidom.Document() - 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) - self.write_viewpoint_lines(viewpoint, root) - self.write_viewpoint_clipping_planes(viewpoint, root) - self.write_viewpoint_bitmaps(viewpoint, root) - with open(os.path.join(self.filepath, topic.guid, viewpoint.viewpoint), "wb") as f: - f.write(document.toprettyxml(encoding="utf-8")) - - def write_viewpoint_components(self, viewpoint, parent): - if not viewpoint.components: - return - components_el = self._create_element(parent, "Components") - if viewpoint.components.view_setup_hints: - view_setup_hints = self._create_element( - components_el, - "ViewSetupHints", - { - "SpacesVisible": viewpoint.components.view_setup_hints.spaces_visible, - "SpaceBoundariesVisible": viewpoint.components.view_setup_hints.space_boundaries_visible, - "OpeningsVisible": viewpoint.components.view_setup_hints.openings_visible, - }, - ) - if viewpoint.components.selection: - selection_el = self._create_element(components_el, "Selection") - for selection in viewpoint.components.selection: - self.write_component(selection, selection_el) - visibility = self._create_element( - components_el, "Visibility", {"DefaultVisibility": viewpoint.components.visibility.default_visibility} - ) - if viewpoint.components.visibility.exceptions: - exceptions_el = self._create_element(visibility, "Exceptions") - for exception in viewpoint.components.visibility.exceptions: - self.write_component(exception, exceptions_el) - if viewpoint.components.coloring: - coloring_el = self._create_element(components_el, "Coloring") - for color in viewpoint.components.coloring: - color_el = self._create_element(coloring_el, "Color", {"Color": color.color}) - for component in color.components: - self.write_component(component, color_el) - - def write_viewpoint_orthogonal_camera(self, viewpoint, parent): - if not viewpoint.orthogonal_camera: - return - camera = viewpoint.orthogonal_camera - camera_el = self._create_element(parent, "OrthogonalCamera") - camera_view_point = self._create_element(camera_el, "CameraViewPoint") - self.write_vector(camera_view_point, camera.camera_view_point) - camera_direction = self._create_element(camera_el, "CameraDirection") - self.write_vector(camera_direction, camera.camera_direction) - camera_up_vector = self._create_element(camera_el, "CameraUpVector") - self.write_vector(camera_up_vector, camera.camera_up_vector) - self._create_element(camera_el, "ViewToWorldScale", text=camera.view_to_world_scale) - - def write_viewpoint_perspective_camera(self, viewpoint, parent): - if not viewpoint.perspective_camera: - return - camera = viewpoint.perspective_camera - camera_el = self._create_element(parent, "PerspectiveCamera") - camera_view_point = self._create_element(camera_el, "CameraViewPoint") - self.write_vector(camera_view_point, camera.camera_view_point) - camera_direction = self._create_element(camera_el, "CameraDirection") - self.write_vector(camera_direction, camera.camera_direction) - camera_up_vector = self._create_element(camera_el, "CameraUpVector") - self.write_vector(camera_up_vector, camera.camera_up_vector) - self._create_element(camera_el, "FieldOfView", text=camera.field_of_view) - - def write_viewpoint_lines(self, viewpoint, parent): - if not viewpoint.lines: - return - lines_el = self._create_element(parent, "Lines") - for line in viewpoint.lines: - line_el = self._create_element(lines_el, "Line") - start_point_el = self._create_element(line_el, "StartPoint") - self.write_vector(start_point_el, line.start_point) - end_point_el = self._create_element(line_el, "EndPoint") - self.write_vector(end_point_el, line.end_point) - - def write_viewpoint_clipping_planes(self, viewpoint, parent): - if not viewpoint.clipping_planes: - return - planes_el = self._create_element(parent, "ClippingPlanes") - for plane in viewpoint.clipping_planes: - plane_el = self._create_element(planes_el, "ClippingPlane") - location_el = self._create_element(plane_el, "Location") - self.write_vector(location_el, plane.location) - direction_el = self._create_element(plane_el, "Direction") - self.write_vector(direction_el, plane.direction) - - def write_viewpoint_bitmaps(self, viewpoint, parent): - if not viewpoint.bitmaps: - return - for bitmap in viewpoint.bitmaps: - bitmap_el = self._create_element(parent, "Bitmap") - - 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") - self.write_vector(normal_el, bitmap.normal) - 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) - self._create_element(parent, "Z", text=from_obj.z) - - def write_component(self, data, parent): - component_el = self._create_element(parent, "Component", {"IfcGuid": data.ifc_guid}) - text_map = {"OriginatingSystem": data.originating_system, "AuthoringToolId": data.authoring_tool_id} - for key, value in text_map.items(): - if value: - self._create_element(component_el, key, text=value) - - def add_viewpoint(self, topic, viewpoint=None): - if not viewpoint: - viewpoint = bcf.data.Viewpoint() - if not viewpoint.guid: - viewpoint.guid = str(uuid.uuid4()) - if not viewpoint.viewpoint: - viewpoint.viewpoint = f"{viewpoint.guid}.bcfv" - 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): - filename = viewpoint.guid + os.path.splitext(viewpoint.snapshot)[-1] - 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): - if guid not in topic.viewpoints: - return - viewpoint = topic.viewpoints[guid] - if viewpoint.snapshot: - filepath = os.path.join(self.filepath, topic.guid, viewpoint.snapshot) - if os.path.exists(filepath): - os.remove(filepath) - if viewpoint.viewpoint: - filepath = os.path.join(self.filepath, topic.guid, viewpoint.viewpoint) - if os.path.exists(filepath): - os.remove(filepath) - for bitmap in viewpoint.bitmaps: - if not bitmap.reference: - continue - filepath = os.path.join(self.filepath, topic.guid, bitmap.reference) - if os.path.exists(filepath): - os.remove(filepath) - 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") - if "Comment" not in data: - return comments - for item in data["Comment"]: - comment = bcf.data.Comment() - mandatory_keys = {"guid": "@Guid", "date": "Date", "author": "Author", "comment": "Comment"} - for key, value in mandatory_keys.items(): - setattr(comment, key, item[value]) - optional_keys = {"modified_date": "ModifiedDate", "modified_author": "ModifiedAuthor"} - for key, value in optional_keys.items(): - if value in item: - setattr(comment, key, item[value]) - if "Viewpoint" in item: - viewpoint = bcf.data.Viewpoint() - viewpoint.guid = item["Viewpoint"]["@Guid"] - comment.viewpoint = viewpoint - comments[comment.guid] = comment - self.topics[guid].comments = comments - return comments - - def get_viewpoints(self, guid): - viewpoints = {} - data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") - if "Viewpoints" not in data: - return viewpoints - for item in data["Viewpoints"]: - viewpoint = self.get_viewpoint(item, guid) - viewpoints[viewpoint.guid] = viewpoint - self.topics[guid].viewpoints = viewpoints - return viewpoints - - def get_viewpoint(self, data, topic_guid): - viewpoint = bcf.data.Viewpoint() - viewpoint.guid = data["@Guid"] - optional_keys = {"viewpoint": "Viewpoint", "snapshot": "Snapshot", "index": "Index"} - for key, value in optional_keys.items(): - if value in data: - setattr(viewpoint, key, data[value]) - visinfo = self._read_xml(os.path.join(topic_guid, viewpoint.viewpoint), "visinfo.xsd") - viewpoint.components = self.get_viewpoint_components(visinfo) - viewpoint.orthogonal_camera = self.get_viewpoint_orthogonal_camera(visinfo) - viewpoint.perspective_camera = self.get_viewpoint_perspective_camera(visinfo) - viewpoint.lines = self.get_viewpoint_lines(visinfo) - viewpoint.clipping_planes = self.get_viewpoint_clipping_planes(visinfo) - viewpoint.bitmaps = self.get_viewpoint_bitmaps(visinfo) - return viewpoint - - def get_viewpoint_components(self, visinfo): - if "Components" not in visinfo: - return None - components = bcf.data.Components() - data = visinfo["Components"] - if "ViewSetupHints" in data: - view_setup_hints = bcf.data.ViewSetupHints() - optional_keys = { - "spaces_visible": "@SpacesVisible", - "space_boundaries_visible": "@SpaceBoundariesVisible", - "openings_visible": "@OpeningsVisible", - } - for key, value in optional_keys.items(): - if value in data["ViewSetupHints"]: - setattr(view_setup_hints, key, data["ViewSetupHints"][value]) - components.view_setup_hints = view_setup_hints - if "Selection" in data and "Component" in data["Selection"]: - for item in data["Selection"]["Component"]: - components.selection.append(self.get_component(item)) - if "Visibility" in data: - component_visibility = bcf.data.ComponentVisibility() - if "@DefaultVisibility" in data["Visibility"]: - component_visibility.default_visibility = data["Visibility"]["@DefaultVisibility"] - if "Exceptions" in data["Visibility"] and "Component" in data["Visibility"]["Exceptions"]: - for item in data["Visibility"]["Exceptions"]["Component"]: - component_visibility.exceptions.append(self.get_component(item)) - components.visibility = component_visibility - if "Coloring" in data and "Color" in data["Coloring"]: - for item in data["Coloring"]["Color"]: - color = bcf.data.Color() - color.color = item["@Color"] - for item2 in item["Component"]: - color.components.append(self.get_component(item2)) - components.coloring.append(color) - return components - - def get_viewpoint_orthogonal_camera(self, visinfo): - if "OrthogonalCamera" not in visinfo: - return None - camera = bcf.data.OrthogonalCamera() - data = visinfo["OrthogonalCamera"] - self.set_vector(camera.camera_view_point, data["CameraViewPoint"]) - self.set_vector(camera.camera_direction, data["CameraDirection"]) - self.set_vector(camera.camera_up_vector, data["CameraUpVector"]) - camera.view_to_world_scale = data["ViewToWorldScale"] - return camera - - def get_viewpoint_perspective_camera(self, visinfo): - if "PerspectiveCamera" not in visinfo: - return None - camera = bcf.data.PerspectiveCamera() - data = visinfo["PerspectiveCamera"] - self.set_vector(camera.camera_view_point, data["CameraViewPoint"]) - self.set_vector(camera.camera_direction, data["CameraDirection"]) - self.set_vector(camera.camera_up_vector, data["CameraUpVector"]) - camera.field_of_view = data["FieldOfView"] - return camera - - def get_viewpoint_lines(self, visinfo): - if "Lines" not in visinfo: - return [] - lines = [] - for item in visinfo["Lines"]["Line"]: - line = bcf.data.Line() - self.set_vector(line.start_point, item["StartPoint"]) - self.set_vector(line.end_point, item["EndPoint"]) - lines.append(line) - return lines - - def get_viewpoint_clipping_planes(self, visinfo): - if "ClippingPlanes" not in visinfo: - return [] - planes = [] - for item in visinfo["ClippingPlanes"]["ClippingPlane"]: - plane = bcf.data.ClippingPlane() - self.set_vector(plane.location, item["Location"]) - self.set_vector(plane.direction, item["Direction"]) - planes.append(plane) - return planes - - def get_viewpoint_bitmaps(self, visinfo): - if "Bitmap" not in visinfo: - return [] - bitmaps = [] - for item in visinfo["Bitmap"]: - bitmap = bcf.data.Bitmap() - bitmap.reference = item["Reference"] - 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"]) - bitmap.height = item["Height"] - bitmaps.append(bitmap) - return bitmaps - - def set_vector(self, to_obj, from_xml): - to_obj.x = from_xml["X"] - to_obj.y = from_xml["Y"] - to_obj.z = from_xml["Z"] - - def get_component(self, data): - component = bcf.data.Component() - optional_keys = { - "originating_system": "OriginatingSystem", - "authoring_tool_id": "AuthoringToolId", - "ifc_guid": "@IfcGuid", - } - for key, value in optional_keys.items(): - if value in data: - setattr(component, key, data[value]) - return component - - def close_project(self): - shutil.rmtree(self.filepath) - - def _read_xml(self, filename, xsd): - schema = XMLSchema(os.path.join(cwd, "xsd", xsd)) - filepath = os.path.join(self.filepath, filename) - (data, errors) = schema.to_dict(filepath, validation="lax") - for error in errors: - self.logger.error(error) - return data - - def _create_element(self, parent, name, attributes={}, text=None): - element = self.document.createElement(name) - for key, value in attributes.items(): - if isinstance(value, bool): - element.setAttribute(key, str(value).lower()) - elif value: - element.setAttribute(key, value) - if text is not None: - text = self.document.createTextNode(str(text)) - element.appendChild(text) - parent.appendChild(element) - return element - - def __del__(self): - self.close_project() +def extract_project(filepath): + if not filepath: + return + zip_file = zipfile.ZipFile(filepath) + filepath = tempfile.mkdtemp() + zip_file.extractall(filepath) + return filepath diff --git a/src/bcf/bcf/v2/bcfxml.py b/src/bcf/bcf/v2/bcfxml.py new file mode 100644 index 0000000000..3b4dd722b0 --- /dev/null +++ b/src/bcf/bcf/v2/bcfxml.py @@ -0,0 +1,771 @@ +import os +import uuid +import shutil +import zipfile +import logging +import tempfile +import bcf.v2.data +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__)) + + +@contextmanager +def cd(newdir): + prevdir = os.getcwd() + os.chdir(os.path.expanduser(newdir)) + try: + yield + finally: + os.chdir(prevdir) + + +class BcfXml: + def __init__(self): + self.filepath = None + self.logger = logging.getLogger("bcfxml") + self.author = "john@doe.com" + self.project = bcf.v2.data.Project() + self.version = "2.1" + self.topics = {} + + def new_project(self): + self.project.project_id = str(uuid.uuid4()) + self.project.name = "New Project" + self.topics = {} + if self.filepath: + self.close_project() + self.filepath = tempfile.mkdtemp() + self.edit_project() + self.edit_version() + + def get_project(self, filepath=None): + if not filepath: + return self.project + if os.path.isfile(os.path.join(self.filepath, "project.bcfp")): + data = self._read_xml("project.bcfp", "project.xsd") + self.project.extension_schema = data["ExtensionSchema"] + if "Project" in data: + self.project.project_id = data["Project"]["@ProjectId"] + self.project.name = data["Project"].get("Name") + return self.project + + def edit_project(self): + self.document = minidom.Document() + root = self._create_element(self.document, "ProjectExtension") + project = self._create_element(root, "Project", {"ProjectId": self.project.project_id}) + self._create_element(project, "Name", text=self.project.name) + self._create_element(root, "ExtensionSchema", text="extensions.xsd") + with open(os.path.join(self.filepath, "project.bcfp"), "wb") as f: + f.write(self.document.toprettyxml(encoding="utf-8")) + + def save_project(self, filepath): + with cd(self.filepath): + zip_file = zipfile.ZipFile(filepath, "w", zipfile.ZIP_DEFLATED) + for root, dirs, files in os.walk("./"): + for file in files: + zip_file.write(os.path.join(root, file)) + zip_file.close() + + def get_version(self): + data = self._read_xml("bcf.version", "version.xsd") + self.version = data["@VersionId"] + return self.version + + def edit_version(self): + self.document = minidom.Document() + root = self._create_element(self.document, "Version", {"VersionId": self.version}) + version = self._create_element(root, "DetailedVersion", text=self.version) + with open(os.path.join(self.filepath, "bcf.version"), "wb") as f: + f.write(self.document.toprettyxml(encoding="utf-8")) + + def get_topics(self): + self.topics = {} + topics = [] + subdirs = [] + for (dirpath, dirnames, filenames) in os.walk(self.filepath): + subdirs = dirnames + break + for subdir in subdirs: + try: + uuid.UUID(subdir) + except ValueError: + continue + if not os.path.exists(os.path.join(self.filepath, subdir, "markup.bcf")): + continue + self.topics[subdir] = self.get_topic(subdir) + return self.topics + + def get_header(self, guid): + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + if "Header" not in data: + return + header = bcf.v2.data.Header() + for item in data["Header"]["File"]: + header_file = bcf.v2.data.HeaderFile() + optional_keys = { + "filename": "Filename", + "date": "Date", + "reference": "Reference", + "ifc_project": "@IfcProject", + "ifc_spatial_structure_element": "@IfcSpatialStructureElement", + "is_external": "@isExternal", + } + for key, value in optional_keys.items(): + if value in item: + setattr(header_file, key, item[value]) + header.files.append(header_file) + self.topics[guid].header = header + return header + + def get_topic(self, guid): + if guid in self.topics: + return self.topics[guid] + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + topic = bcf.v2.data.Topic() + self.topics[guid] = topic + + mandatory_keys = { + "guid": "@Guid", + "title": "Title", + "creation_date": "CreationDate", + "creation_author": "CreationAuthor", + } + for key, value in mandatory_keys.items(): + setattr(topic, key, data["Topic"][value]) + + optional_keys = { + "priority": "Priority", + "index": "Index", + "labels": "Labels", + "reference_links": "ReferenceLink", + "modified_date": "ModifiedDate", + "modified_author": "ModifiedAuthor", + "due_date": "DueDate", + "assigned_to": "AssignedTo", + "stage": "Stage", + "description": "Description", + "topic_status": "@TopicStatus", + "topic_type": "@TopicType", + } + for key, value in optional_keys.items(): + if value in data["Topic"]: + setattr(topic, key, data["Topic"][value]) + + if "BimSnippet" in data["Topic"]: + bim_snippet = bcf.v2.data.BimSnippet() + keys = { + "snippet_type": "@SnippetType", + "is_external": "@IsExternal", + "reference": "Reference", + "reference_schema": "ReferenceSchema", + } + for key, value in keys.items(): + if value in data["Topic"]["BimSnippet"]: + setattr(bim_snippet, key, data["Topic"]["BimSnippet"][value]) + topic.bim_snippet = bim_snippet + + if "DocumentReference" in data["Topic"]: + for item in data["Topic"]["DocumentReference"]: + document_reference = bcf.v2.data.DocumentReference() + keys = { + "referenced_document": "ReferencedDocument", + "is_external": "@IsExternal", + "guid": "@Guid", + "description": "Description", + } + for key, value in keys.items(): + if value in item: + setattr(document_reference, key, item[value]) + topic.document_references.append(document_reference) + + if "RelatedTopic" in data["Topic"]: + for item in data["Topic"]["RelatedTopic"]: + related_topic = bcf.v2.data.RelatedTopic() + related_topic.guid = item["@Guid"] + topic.related_topics.append(related_topic) + return topic + + def add_topic(self, topic=None): + if topic is None: + topic = bcf.v2.data.Topic() + if not topic.guid: + topic.guid = str(uuid.uuid4()) + if not topic.title: + topic.title = "New Topic" + os.mkdir(os.path.join(self.filepath, topic.guid)) + self.edit_topic(topic) + return topic + + def edit_topic(self, topic): + if not topic.creation_date: + topic.creation_date = datetime.utcnow().isoformat() + topic.creation_author = self.author + else: + topic.modified_date = datetime.utcnow().isoformat() + topic.modified_author = self.author + + self.document = minidom.Document() + root = self._create_element(self.document, "Markup") + + self.write_header(topic.header, root) + + topic_el = self._create_element( + root, + "Topic", + { + "Guid": topic.guid, + "TopicType": topic.topic_type, + "TopicStatus": topic.topic_status, + }, + ) + + 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, + "ModifiedAuthor": topic.modified_author, + "DueDate": topic.due_date, + "AssignedTo": topic.assigned_to, + "Stage": topic.stage, + "Description": topic.description, + } + for key, value in text_map.items(): + if value: + self._create_element(topic_el, key, text=value) + + if topic.bim_snippet: + bim_snippet = self._create_element( + topic_el, + "BimSnippet", + {"SnippetType": topic.bim_snippet.snippet_type, "isExternal": topic.bim_snippet.is_external}, + ) + self._create_element(bim_snippet, "Reference", text=topic.bim_snippet.reference) + self._create_element(bim_snippet, "ReferenceSchema", text=topic.bim_snippet.reference_schema) + for reference in topic.document_references: + reference_el = self._create_element( + topic_el, "DocumentReference", {"Guid": reference.guid, "isExternal": reference.is_external} + ) + self._create_element(reference_el, "ReferencedDocument", text=reference.referenced_document) + self._create_element(reference_el, "Description", text=reference.description) + for related_topic in topic.related_topics: + self._create_element(topic_el, "RelatedTopic", {"Guid": related_topic.guid}) + + self.write_comments(topic.comments, root) + self.write_viewpoints(topic.viewpoints, root, topic) + + with open(os.path.join(self.filepath, topic.guid, "markup.bcf"), "wb") as f: + f.write(self.document.toprettyxml(encoding="utf-8")) + + def write_header(self, header, root): + if not header or not header.files: + return + header_el = self._create_element(root, "Header") + for f in header.files: + file_el = self._create_element( + header_el, + "File", + { + "IfcProject": f.ifc_project, + "IfcSpatialStructureElement": f.ifc_spatial_structure_element, + "isExternal": f.is_external, + }, + ) + if f.filename: + self._create_element(file_el, "Filename", text=f.filename) + if f.date: + self._create_element(file_el, "Date", text=f.date) + if f.reference: + self._create_element(file_el, "Reference", text=f.reference) + + def write_comments(self, comments, root): + for comment in comments.values(): + comment_el = self._create_element(root, "Comment", {"Guid": comment.guid}) + text_map = { + "Date": comment.date, + "Author": comment.author, + "Comment": comment.comment, + "ModifiedDate": comment.modified_date, + "ModifiedAuthor": comment.modified_author, + } + for key, value in text_map.items(): + if value: + self._create_element(comment_el, key, text=value) + if comment.viewpoint: + self._create_element(comment_el, "Viewpoint", {"Guid": comment.viewpoint.guid}) + + def add_comment(self, topic, comment=None): + if comment is None: + comment = bcf.v2.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'." + topic.comments[comment.guid] = comment + self.edit_comment(comment, topic) + + def edit_comment(self, comment, topic): + if not comment.date: + comment.date = datetime.utcnow().isoformat() + comment.author = self.author + else: + comment.modified_date = datetime.utcnow().isoformat() + comment.modified_author = self.author + self.edit_topic(topic) + + def delete_comment(self, guid, topic): + if guid in topic.comments: + del topic.comments[guid] + self.edit_topic(topic) + + def delete_topic(self, guid): + if guid in self.topics: + del self.topics[guid] + shutil.rmtree(os.path.join(self.filepath, guid)) + + def write_viewpoints(self, viewpoints, root, topic): + for viewpoint in viewpoints.values(): + viewpoint_el = self._create_element(root, "Viewpoints", {"Guid": viewpoint.guid}) + text_map = {"Viewpoint": viewpoint.viewpoint, "Snapshot": viewpoint.snapshot, "Index": viewpoint.index} + for key, value in text_map.items(): + if value: + self._create_element(viewpoint_el, key, text=value) + self.write_viewpoint(viewpoint, topic) + + def write_viewpoint(self, viewpoint, topic): + document = minidom.Document() + 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) + self.write_viewpoint_lines(viewpoint, root) + self.write_viewpoint_clipping_planes(viewpoint, root) + self.write_viewpoint_bitmaps(viewpoint, root) + with open(os.path.join(self.filepath, topic.guid, viewpoint.viewpoint), "wb") as f: + f.write(document.toprettyxml(encoding="utf-8")) + + def write_viewpoint_components(self, viewpoint, parent): + if not viewpoint.components: + return + components_el = self._create_element(parent, "Components") + if viewpoint.components.view_setup_hints: + view_setup_hints = self._create_element( + components_el, + "ViewSetupHints", + { + "SpacesVisible": viewpoint.components.view_setup_hints.spaces_visible, + "SpaceBoundariesVisible": viewpoint.components.view_setup_hints.space_boundaries_visible, + "OpeningsVisible": viewpoint.components.view_setup_hints.openings_visible, + }, + ) + if viewpoint.components.selection: + selection_el = self._create_element(components_el, "Selection") + for selection in viewpoint.components.selection: + self.write_component(selection, selection_el) + visibility = self._create_element( + components_el, "Visibility", {"DefaultVisibility": viewpoint.components.visibility.default_visibility} + ) + if viewpoint.components.visibility.exceptions: + exceptions_el = self._create_element(visibility, "Exceptions") + for exception in viewpoint.components.visibility.exceptions: + self.write_component(exception, exceptions_el) + if viewpoint.components.coloring: + coloring_el = self._create_element(components_el, "Coloring") + for color in viewpoint.components.coloring: + color_el = self._create_element(coloring_el, "Color", {"Color": color.color}) + for component in color.components: + self.write_component(component, color_el) + + def write_viewpoint_orthogonal_camera(self, viewpoint, parent): + if not viewpoint.orthogonal_camera: + return + camera = viewpoint.orthogonal_camera + camera_el = self._create_element(parent, "OrthogonalCamera") + camera_view_point = self._create_element(camera_el, "CameraViewPoint") + self.write_vector(camera_view_point, camera.camera_view_point) + camera_direction = self._create_element(camera_el, "CameraDirection") + self.write_vector(camera_direction, camera.camera_direction) + camera_up_vector = self._create_element(camera_el, "CameraUpVector") + self.write_vector(camera_up_vector, camera.camera_up_vector) + self._create_element(camera_el, "ViewToWorldScale", text=camera.view_to_world_scale) + + def write_viewpoint_perspective_camera(self, viewpoint, parent): + if not viewpoint.perspective_camera: + return + camera = viewpoint.perspective_camera + camera_el = self._create_element(parent, "PerspectiveCamera") + camera_view_point = self._create_element(camera_el, "CameraViewPoint") + self.write_vector(camera_view_point, camera.camera_view_point) + camera_direction = self._create_element(camera_el, "CameraDirection") + self.write_vector(camera_direction, camera.camera_direction) + camera_up_vector = self._create_element(camera_el, "CameraUpVector") + self.write_vector(camera_up_vector, camera.camera_up_vector) + self._create_element(camera_el, "FieldOfView", text=camera.field_of_view) + + def write_viewpoint_lines(self, viewpoint, parent): + if not viewpoint.lines: + return + lines_el = self._create_element(parent, "Lines") + for line in viewpoint.lines: + line_el = self._create_element(lines_el, "Line") + start_point_el = self._create_element(line_el, "StartPoint") + self.write_vector(start_point_el, line.start_point) + end_point_el = self._create_element(line_el, "EndPoint") + self.write_vector(end_point_el, line.end_point) + + def write_viewpoint_clipping_planes(self, viewpoint, parent): + if not viewpoint.clipping_planes: + return + planes_el = self._create_element(parent, "ClippingPlanes") + for plane in viewpoint.clipping_planes: + plane_el = self._create_element(planes_el, "ClippingPlane") + location_el = self._create_element(plane_el, "Location") + self.write_vector(location_el, plane.location) + direction_el = self._create_element(plane_el, "Direction") + self.write_vector(direction_el, plane.direction) + + def write_viewpoint_bitmaps(self, viewpoint, parent): + if not viewpoint.bitmaps: + return + for bitmap in viewpoint.bitmaps: + bitmap_el = self._create_element(parent, "Bitmap") + + text_map = {"Bitmap": bitmap.bitmap_format, "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") + self.write_vector(normal_el, bitmap.normal) + 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) + self._create_element(parent, "Z", text=from_obj.z) + + def write_component(self, data, parent): + component_el = self._create_element(parent, "Component", {"IfcGuid": data.ifc_guid}) + text_map = {"OriginatingSystem": data.originating_system, "AuthoringToolId": data.authoring_tool_id} + for key, value in text_map.items(): + if value: + self._create_element(component_el, key, text=value) + + def add_viewpoint(self, topic, viewpoint=None): + if not viewpoint: + viewpoint = bcf.v2.data.Viewpoint() + if not viewpoint.guid: + viewpoint.guid = str(uuid.uuid4()) + if not viewpoint.viewpoint: + viewpoint.viewpoint = f"{viewpoint.guid}.bcfv" + 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): + filename = viewpoint.guid + os.path.splitext(viewpoint.snapshot)[-1] + 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): + if guid not in topic.viewpoints: + return + viewpoint = topic.viewpoints[guid] + if viewpoint.snapshot: + filepath = os.path.join(self.filepath, topic.guid, viewpoint.snapshot) + if os.path.exists(filepath): + os.remove(filepath) + if viewpoint.viewpoint: + filepath = os.path.join(self.filepath, topic.guid, viewpoint.viewpoint) + if os.path.exists(filepath): + os.remove(filepath) + for bitmap in viewpoint.bitmaps: + if not bitmap.reference: + continue + filepath = os.path.join(self.filepath, topic.guid, bitmap.reference) + if os.path.exists(filepath): + os.remove(filepath) + 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.v2.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") + if "Comment" not in data: + return comments + for item in data["Comment"]: + comment = bcf.v2.data.Comment() + mandatory_keys = {"guid": "@Guid", "date": "Date", "author": "Author", "comment": "Comment"} + for key, value in mandatory_keys.items(): + setattr(comment, key, item[value]) + optional_keys = {"modified_date": "ModifiedDate", "modified_author": "ModifiedAuthor"} + for key, value in optional_keys.items(): + if value in item: + setattr(comment, key, item[value]) + if "Viewpoint" in item: + viewpoint = bcf.v2.data.Viewpoint() + viewpoint.guid = item["Viewpoint"]["@Guid"] + comment.viewpoint = viewpoint + comments[comment.guid] = comment + self.topics[guid].comments = comments + return comments + + def get_viewpoints(self, guid): + viewpoints = {} + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + if "Viewpoints" not in data: + return viewpoints + for item in data["Viewpoints"]: + viewpoint = self.get_viewpoint(item, guid) + viewpoints[viewpoint.guid] = viewpoint + self.topics[guid].viewpoints = viewpoints + return viewpoints + + def get_viewpoint(self, data, topic_guid): + viewpoint = bcf.v2.data.Viewpoint() + viewpoint.guid = data["@Guid"] + optional_keys = {"viewpoint": "Viewpoint", "snapshot": "Snapshot", "index": "Index"} + for key, value in optional_keys.items(): + if value in data: + setattr(viewpoint, key, data[value]) + visinfo = self._read_xml(os.path.join(topic_guid, viewpoint.viewpoint), "visinfo.xsd") + viewpoint.components = self.get_viewpoint_components(visinfo) + viewpoint.orthogonal_camera = self.get_viewpoint_orthogonal_camera(visinfo) + viewpoint.perspective_camera = self.get_viewpoint_perspective_camera(visinfo) + viewpoint.lines = self.get_viewpoint_lines(visinfo) + viewpoint.clipping_planes = self.get_viewpoint_clipping_planes(visinfo) + viewpoint.bitmaps = self.get_viewpoint_bitmaps(visinfo) + return viewpoint + + def get_viewpoint_components(self, visinfo): + if "Components" not in visinfo: + return None + components = bcf.v2.data.Components() + data = visinfo["Components"] + if "ViewSetupHints" in data: + view_setup_hints = bcf.v2.data.ViewSetupHints() + optional_keys = { + "spaces_visible": "@SpacesVisible", + "space_boundaries_visible": "@SpaceBoundariesVisible", + "openings_visible": "@OpeningsVisible", + } + for key, value in optional_keys.items(): + if value in data["ViewSetupHints"]: + setattr(view_setup_hints, key, data["ViewSetupHints"][value]) + components.view_setup_hints = view_setup_hints + if "Selection" in data and "Component" in data["Selection"]: + for item in data["Selection"]["Component"]: + components.selection.append(self.get_component(item)) + if "Visibility" in data: + component_visibility = bcf.v2.data.ComponentVisibility() + if "@DefaultVisibility" in data["Visibility"]: + component_visibility.default_visibility = data["Visibility"]["@DefaultVisibility"] + if "Exceptions" in data["Visibility"] and "Component" in data["Visibility"]["Exceptions"]: + for item in data["Visibility"]["Exceptions"]["Component"]: + component_visibility.exceptions.append(self.get_component(item)) + components.visibility = component_visibility + if "Coloring" in data and "Color" in data["Coloring"]: + for item in data["Coloring"]["Color"]: + color = bcf.v2.data.Color() + color.color = item["@Color"] + for item2 in item["Component"]: + color.components.append(self.get_component(item2)) + components.coloring.append(color) + return components + + def get_viewpoint_orthogonal_camera(self, visinfo): + if "OrthogonalCamera" not in visinfo: + return None + camera = bcf.v2.data.OrthogonalCamera() + data = visinfo["OrthogonalCamera"] + self.set_vector(camera.camera_view_point, data["CameraViewPoint"]) + self.set_vector(camera.camera_direction, data["CameraDirection"]) + self.set_vector(camera.camera_up_vector, data["CameraUpVector"]) + camera.view_to_world_scale = data["ViewToWorldScale"] + return camera + + def get_viewpoint_perspective_camera(self, visinfo): + if "PerspectiveCamera" not in visinfo: + return None + camera = bcf.v2.data.PerspectiveCamera() + data = visinfo["PerspectiveCamera"] + self.set_vector(camera.camera_view_point, data["CameraViewPoint"]) + self.set_vector(camera.camera_direction, data["CameraDirection"]) + self.set_vector(camera.camera_up_vector, data["CameraUpVector"]) + camera.field_of_view = data["FieldOfView"] + return camera + + def get_viewpoint_lines(self, visinfo): + if "Lines" not in visinfo: + return [] + lines = [] + for item in visinfo["Lines"]["Line"]: + line = bcf.v2.data.Line() + self.set_vector(line.start_point, item["StartPoint"]) + self.set_vector(line.end_point, item["EndPoint"]) + lines.append(line) + return lines + + def get_viewpoint_clipping_planes(self, visinfo): + if "ClippingPlanes" not in visinfo: + return [] + planes = [] + for item in visinfo["ClippingPlanes"]["ClippingPlane"]: + plane = bcf.v2.data.ClippingPlane() + self.set_vector(plane.location, item["Location"]) + self.set_vector(plane.direction, item["Direction"]) + planes.append(plane) + return planes + + def get_viewpoint_bitmaps(self, visinfo): + if "Bitmap" not in visinfo: + return [] + bitmaps = [] + for item in visinfo["Bitmap"]: + bitmap = bcf.v2.data.Bitmap() + bitmap.reference = item["Reference"] + bitmap.bitmap_format = item["Bitmap"].upper() + self.set_vector(bitmap.location, item["Location"]) + self.set_vector(bitmap.normal, item["Normal"]) + self.set_vector(bitmap.up, item["Up"]) + bitmap.height = item["Height"] + bitmaps.append(bitmap) + return bitmaps + + def set_vector(self, to_obj, from_xml): + to_obj.x = from_xml["X"] + to_obj.y = from_xml["Y"] + to_obj.z = from_xml["Z"] + + def get_component(self, data): + component = bcf.v2.data.Component() + optional_keys = { + "originating_system": "OriginatingSystem", + "authoring_tool_id": "AuthoringToolId", + "ifc_guid": "@IfcGuid", + } + for key, value in optional_keys.items(): + if value in data: + setattr(component, key, data[value]) + return component + + def close_project(self): + shutil.rmtree(self.filepath) + + def _read_xml(self, filename, xsd): + schema = XMLSchema(os.path.join(cwd, "xsd", xsd)) + filepath = os.path.join(self.filepath, filename) + (data, errors) = schema.to_dict(filepath, validation="lax") + for error in errors: + self.logger.error(error) + return data + + def _create_element(self, parent, name, attributes={}, text=None): + element = self.document.createElement(name) + for key, value in attributes.items(): + if isinstance(value, bool): + element.setAttribute(key, str(value).lower()) + elif value: + element.setAttribute(key, value) + if text is not None: + text = self.document.createTextNode(str(text)) + element.appendChild(text) + parent.appendChild(element) + return element + + def __del__(self): + self.close_project() diff --git a/src/bcf/bcf/data.py b/src/bcf/bcf/v2/data.py similarity index 98% rename from src/bcf/bcf/data.py rename to src/bcf/bcf/v2/data.py index ce4431da9a..534aacaffc 100644 --- a/src/bcf/bcf/data.py +++ b/src/bcf/bcf/v2/data.py @@ -157,7 +157,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_format = "PNG" # Enum of png or jpg self.location = Point() self.normal = Direction() self.up = Direction() diff --git a/src/bcf/bcf/xsd/markup.xsd b/src/bcf/bcf/v2/xsd/markup.xsd similarity index 100% rename from src/bcf/bcf/xsd/markup.xsd rename to src/bcf/bcf/v2/xsd/markup.xsd diff --git a/src/bcf/bcf/xsd/project.xsd b/src/bcf/bcf/v2/xsd/project.xsd similarity index 100% rename from src/bcf/bcf/xsd/project.xsd rename to src/bcf/bcf/v2/xsd/project.xsd diff --git a/src/bcf/bcf/xsd/version.xsd b/src/bcf/bcf/v2/xsd/version.xsd similarity index 100% rename from src/bcf/bcf/xsd/version.xsd rename to src/bcf/bcf/v2/xsd/version.xsd diff --git a/src/bcf/bcf/xsd/visinfo.xsd b/src/bcf/bcf/v2/xsd/visinfo.xsd similarity index 100% rename from src/bcf/bcf/xsd/visinfo.xsd rename to src/bcf/bcf/v2/xsd/visinfo.xsd diff --git a/src/bcf/bcf/v3/bcfapi.py b/src/bcf/bcf/v3/bcfapi.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/bcf/bcf/v3/bcfxml.py b/src/bcf/bcf/v3/bcfxml.py new file mode 100644 index 0000000000..3b3fcc8466 --- /dev/null +++ b/src/bcf/bcf/v3/bcfxml.py @@ -0,0 +1,827 @@ +import os +import uuid +import shutil +import zipfile +import logging +import tempfile +import bcf.v3.data +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__)) + + +@contextmanager +def cd(newdir): + prevdir = os.getcwd() + os.chdir(os.path.expanduser(newdir)) + try: + yield + finally: + os.chdir(prevdir) + + +class BcfXml: + def __init__(self): + self.filepath = None + self.logger = logging.getLogger("bcfxml") + self.author = "john@doe.com" + self.project = bcf.v3.data.Project() + self.version = "3.0" + self.topics = {} + + def new_project(self): + self.project.project_id = str(uuid.uuid4()) + self.project.name = "New Project" + self.topics = {} + if self.filepath: + self.close_project() + self.filepath = tempfile.mkdtemp() + self.edit_project() + self.edit_version() + + def get_project(self, filepath=None): + if os.path.isfile(os.path.join(self.filepath, "project.bcfp")): + data = self._read_xml("project.bcfp", "project.xsd") + self.project.project_id = data["Project"]["@ProjectId"] + self.project.name = data["Project"].get("Name") + return self.project + + def edit_project(self): + self.document = minidom.Document() + root = self._create_element(self.document, "ProjectInfo") + project = self._create_element(root, "Project", {"ProjectId": self.project.project_id}) + if self.project.name: + self._create_element(project, "Name", text=self.project.name) + with open(os.path.join(self.filepath, "project.bcfp"), "wb") as f: + f.write(self.document.toprettyxml(encoding="utf-8")) + + def save_project(self, filepath): + with cd(self.filepath): + zip_file = zipfile.ZipFile(filepath, "w", zipfile.ZIP_DEFLATED) + for root, dirs, files in os.walk("./"): + for file in files: + zip_file.write(os.path.join(root, file)) + zip_file.close() + + def get_version(self): + data = self._read_xml("bcf.version", "version.xsd") + self.version = data["@VersionId"] + return self.version + + def edit_version(self): + self.document = minidom.Document() + root = self._create_element(self.document, "Version", {"VersionId": self.version}) + with open(os.path.join(self.filepath, "bcf.version"), "wb") as f: + f.write(self.document.toprettyxml(encoding="utf-8")) + + def get_topics(self): + self.topics = {} + topics = [] + subdirs = [] + for (dirpath, dirnames, filenames) in os.walk(self.filepath): + subdirs = dirnames + break + for subdir in subdirs: + try: + uuid.UUID(subdir) + except ValueError: + continue + if not os.path.exists(os.path.join(self.filepath, subdir, "markup.bcf")): + continue + self.topics[subdir] = self.get_topic(subdir) + return self.topics + + def get_header(self, guid): + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + if "Header" not in data: + return + header = bcf.v3.data.Header() + if data["Header"].get("Files"): + for item in data["Header"]["Files"].get("File", []): + header_file = bcf.v3.data.HeaderFile() + optional_keys = { + "filename": "Filename", + "date": "Date", + "reference": "Reference", + "ifc_project": "@IfcProject", + "ifc_spatial_structure_element": "@IfcSpatialStructureElement", + "is_external": "@IsExternal", + } + for key, value in optional_keys.items(): + if value in item: + setattr(header_file, key, item[value]) + header.files.append(header_file) + self.topics[guid].header = header + return header + + def get_topic(self, guid): + if guid in self.topics: + return self.topics[guid] + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + topic = bcf.v3.data.Topic() + self.topics[guid] = topic + + mandatory_keys = { + "guid": "@Guid", + "title": "Title", + "creation_date": "CreationDate", + "creation_author": "CreationAuthor", + "topic_status": "@TopicStatus", + "topic_type": "@TopicType", + } + for key, value in mandatory_keys.items(): + setattr(topic, key, data["Topic"][value]) + + optional_keys = { + "priority": "Priority", + "index": "Index", + "modified_date": "ModifiedDate", + "modified_author": "ModifiedAuthor", + "due_date": "DueDate", + "assigned_to": "AssignedTo", + "stage": "Stage", + "description": "Description", + "server_assigned_id": "@ServerAssignedId", + } + for key, value in optional_keys.items(): + if value in data["Topic"]: + setattr(topic, key, data["Topic"][value]) + + if "ReferenceLinks" in data["Topic"]: + topic.reference_links.extend(data["Topic"]["ReferenceLinks"].get("ReferenceLink", [])) + + if "Labels" in data["Topic"]: + topic.labels.extend(data["Topic"]["Labels"].get("Label", [])) + + if "BimSnippet" in data["Topic"]: + bim_snippet = bcf.v3.data.BimSnippet() + keys = { + "snippet_type": "@SnippetType", + "is_external": "@IsExternal", + "reference": "Reference", + "reference_schema": "ReferenceSchema", + } + for key, value in keys.items(): + if value in data["Topic"]["BimSnippet"]: + setattr(bim_snippet, key, data["Topic"]["BimSnippet"][value]) + topic.bim_snippet = bim_snippet + + if data["Topic"].get("DocumentReferences"): + for item in data["Topic"]["DocumentReferences"].get("DocumentReference", []): + document_reference = bcf.v3.data.DocumentReference() + keys = { + "document_guid": "DocumentGuid", + "url": "Url", + "guid": "@Guid", + "description": "Description", + } + for key, value in keys.items(): + if value in item: + setattr(document_reference, key, item[value]) + topic.document_references.append(document_reference) + + if data["Topic"].get("RelatedTopics"): + for item in data["Topic"]["RelatedTopics"].get("RelatedTopic", []): + related_topic = bcf.v3.data.RelatedTopic() + related_topic.guid = item["@Guid"] + topic.related_topics.append(related_topic) + return topic + + def add_topic(self, topic=None): + if topic is None: + topic = bcf.v3.data.Topic() + if not topic.guid: + topic.guid = str(uuid.uuid4()) + if not topic.title: + topic.title = "New Topic" + os.mkdir(os.path.join(self.filepath, topic.guid)) + self.edit_topic(topic) + return topic + + def edit_topic(self, topic): + if not topic.creation_date: + topic.creation_date = datetime.utcnow().isoformat() + topic.creation_author = self.author + else: + topic.modified_date = datetime.utcnow().isoformat() + topic.modified_author = self.author + + self.document = minidom.Document() + root = self._create_element(self.document, "Markup") + if topic.header: + self.write_header(topic.header, root) + + topic_el = self._create_element( + root, + "Topic", + { + "Guid": topic.guid, + "ServerAssignedId": topic.server_assigned_id, + "TopicType": topic.topic_type, + "TopicStatus": topic.topic_status, + }, + ) + if topic.reference_links: + reference_Links_el = self._create_element(topic_el, "ReferenceLinks") + for reference_link in topic.reference_links: + self._create_element(reference_Links_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) + if topic.labels: + label_el = self._create_element(topic_el, "Labels") + for label in topic.labels: + self._create_element(label_el, "Label", text=label) + + text_map = { + "CreationDate": topic.creation_date, + "CreationAuthor": topic.creation_author, + "ModifiedDate": topic.modified_date, + "ModifiedAuthor": topic.modified_author, + "DueDate": topic.due_date, + "AssignedTo": topic.assigned_to, + "Stage": topic.stage, + "Description": topic.description, + } + for key, value in text_map.items(): + if value: + self._create_element(topic_el, key, text=value) + + if topic.bim_snippet: + bim_snippet = self._create_element( + topic_el, + "BimSnippet", + { + "SnippetType": topic.bim_snippet.snippet_type, + "IsExternal": topic.bim_snippet.is_external, + }, + ) + self._create_element(bim_snippet, "Reference", text=topic.bim_snippet.reference) + self._create_element( + bim_snippet, + "ReferenceSchema", + text=topic.bim_snippet.reference_schema, + ) + if topic.document_references: + reference_el = self._create_element(topic_el, "DocumentReferences") + self.write_document_references(topic.document_references, reference_el) + if topic.related_topics: + related_topic_el = self._create_element(topic_el, "RelatedTopics") + for related_topic in topic.related_topics: + self._create_element(related_topic_el, "RelatedTopic", {"Guid": related_topic.guid}) + if topic.comments: + comment_el = self._create_element(topic_el, "Comments") + self.write_comments(topic.comments, comment_el) + if topic.viewpoints: + viewpoint_el = self._create_element(topic_el, "Viewpoints") + self.write_viewpoints(topic.viewpoints, viewpoint_el, topic) + with open(os.path.join(self.filepath, topic.guid, "markup.bcf"), "wb") as f: + f.write(self.document.toprettyxml(encoding="utf-8")) + + def write_document_references(self, references, root): + for reference in references: + document_reference_el = self._create_element(root, "DocumentReference", {"Guid": reference.guid}) + if reference.document_guid: + self._create_element(document_reference_el, "DocumentGuid", text=reference.document_guid) + elif reference.url: + self._create_element(document_reference_el, "Url", text=reference.url) + if reference.description: + self._create_element(document_reference_el, "Description", text=reference.description) + + def write_header(self, header, root): + if not header or not header.files: + return + header_el = self._create_element(root, "Header") + files_el = self._create_element(header_el, "Files") + for f in header.files: + file_el = self._create_element( + files_el, + "File", + { + "IfcProject": f.ifc_project, + "IfcSpatialStructureElement": f.ifc_spatial_structure_element, + "IsExternal": f.is_external, + }, + ) + if f.filename: + self._create_element(file_el, "Filename", text=f.filename) + if f.date: + self._create_element(file_el, "Date", text=f.date) + if f.reference: + self._create_element(file_el, "Reference", text=f.reference) + + def write_comments(self, comments, root): + for comment in comments.values(): + comment_el = self._create_element(root, "Comment", {"Guid": comment.guid}) + text_map = { + "Date": comment.date, + "Author": comment.author, + "Comment": comment.comment, + "ModifiedDate": comment.modified_date, + "ModifiedAuthor": comment.modified_author, + } + for key, value in text_map.items(): + if value: + self._create_element(comment_el, key, text=value) + if comment.viewpoint: + self._create_element(comment_el, "Viewpoint", {"Guid": comment.viewpoint.guid}) + + def add_comment(self, topic, comment=None): + if comment is None: + comment = bcf.v3.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'." + topic.comments[comment.guid] = comment + self.edit_comment(comment, topic) + + def edit_comment(self, comment, topic): + if not comment.date: + comment.date = datetime.utcnow().isoformat() + comment.author = self.author + else: + comment.modified_date = datetime.utcnow().isoformat() + comment.modified_author = self.author + self.edit_topic(topic) + + def delete_comment(self, guid, topic): + if guid in topic.comments: + del topic.comments[guid] + self.edit_topic(topic) + + def delete_topic(self, guid): + if guid in self.topics: + del self.topics[guid] + shutil.rmtree(os.path.join(self.filepath, guid)) + + def write_viewpoints(self, viewpoints, root, topic): + for viewpoint in viewpoints.values(): + viewpoint_el = self._create_element(root, "ViewPoint", {"Guid": viewpoint.guid}) + text_map = { + "Viewpoint": viewpoint.viewpoint, + "Snapshot": viewpoint.snapshot, + "Index": viewpoint.index, + } + for key, value in text_map.items(): + if value: + self._create_element(viewpoint_el, key, text=value) + self.write_viewpoint(viewpoint, topic) + + def write_viewpoint(self, viewpoint, topic): + document = minidom.Document() + 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) + self.write_viewpoint_lines(viewpoint, root) + self.write_viewpoint_clipping_planes(viewpoint, root) + self.write_viewpoint_bitmaps(viewpoint, root) + with open(os.path.join(self.filepath, topic.guid, viewpoint.viewpoint), "wb") as f: + f.write(document.toprettyxml(encoding="utf-8")) + + def write_viewpoint_components(self, viewpoint, parent): + if not viewpoint.components: + return + components_el = self._create_element(parent, "Components") + if viewpoint.components.selection: + selection_el = self._create_element(components_el, "Selection") + for selection in viewpoint.components.selection: + self.write_component(selection, selection_el) + if viewpoint.components.visibility: + visibility = self._create_element( + components_el, + "Visibility", + {"DefaultVisibility": viewpoint.components.visibility.default_visibility}, + ) + if viewpoint.components.visibility.view_setup_hints: + view_setup_hints = self._create_element( + visibility, + "ViewSetupHints", + { + "SpacesVisible": viewpoint.components.visibility.view_setup_hints.spaces_visible, + "SpaceBoundariesVisible": viewpoint.components.visibility.view_setup_hints.space_boundaries_visible, + "OpeningsVisible": viewpoint.components.visibility.view_setup_hints.openings_visible, + }, + ) + if viewpoint.components.visibility.exceptions: + exceptions_el = self._create_element(visibility, "Exceptions") + for exception in viewpoint.components.visibility.exceptions: + self.write_component(exception, exceptions_el) + if viewpoint.components.coloring: + coloring_el = self._create_element(components_el, "Coloring") + for color in viewpoint.components.coloring: + color_el = self._create_element(coloring_el, "Color", {"Color": color.color}) + component_el = self._create_element(color_el, "Components") + for component in color.components: + self.write_component(component, component_el) + + def write_viewpoint_orthogonal_camera(self, viewpoint, parent): + if not viewpoint.orthogonal_camera: + return + camera = viewpoint.orthogonal_camera + camera_el = self._create_element(parent, "OrthogonalCamera") + camera_view_point = self._create_element(camera_el, "CameraViewPoint") + self.write_vector(camera_view_point, camera.camera_view_point) + camera_direction = self._create_element(camera_el, "CameraDirection") + self.write_vector(camera_direction, camera.camera_direction) + camera_up_vector = self._create_element(camera_el, "CameraUpVector") + self.write_vector(camera_up_vector, camera.camera_up_vector) + self._create_element(camera_el, "ViewToWorldScale", text=camera.view_to_world_scale) + self._create_element(camera_el, "AspectRatio", text=camera.aspect_ratio) + + def write_viewpoint_perspective_camera(self, viewpoint, parent): + if not viewpoint.perspective_camera: + return + camera = viewpoint.perspective_camera + camera_el = self._create_element(parent, "PerspectiveCamera") + camera_view_point = self._create_element(camera_el, "CameraViewPoint") + self.write_vector(camera_view_point, camera.camera_view_point) + camera_direction = self._create_element(camera_el, "CameraDirection") + self.write_vector(camera_direction, camera.camera_direction) + camera_up_vector = self._create_element(camera_el, "CameraUpVector") + self.write_vector(camera_up_vector, camera.camera_up_vector) + self._create_element(camera_el, "FieldOfView", text=camera.field_of_view) + self._create_element(camera_el, "AspectRatio", text=camera.aspect_ratio) + + def write_viewpoint_lines(self, viewpoint, parent): + if not viewpoint.lines: + return + lines_el = self._create_element(parent, "Lines") + for line in viewpoint.lines: + line_el = self._create_element(lines_el, "Line") + start_point_el = self._create_element(line_el, "StartPoint") + self.write_vector(start_point_el, line.start_point) + end_point_el = self._create_element(line_el, "EndPoint") + self.write_vector(end_point_el, line.end_point) + + def write_viewpoint_clipping_planes(self, viewpoint, parent): + if not viewpoint.clipping_planes: + return + planes_el = self._create_element(parent, "ClippingPlanes") + for plane in viewpoint.clipping_planes: + plane_el = self._create_element(planes_el, "ClippingPlane") + location_el = self._create_element(plane_el, "Location") + self.write_vector(location_el, plane.location) + direction_el = self._create_element(plane_el, "Direction") + self.write_vector(direction_el, plane.direction) + + def write_viewpoint_bitmaps(self, viewpoint, parent): + if not viewpoint.bitmaps: + return + bitmaps_el = self._create_element(parent, "Bitmaps") + for bitmap in viewpoint.bitmaps: + bitmap_el = self._create_element(bitmaps_el, "Bitmap") + + text_map = {"Format": bitmap.bitmap_format, "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") + self.write_vector(normal_el, bitmap.normal) + 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) + self._create_element(parent, "Z", text=from_obj.z) + + def write_component(self, data, parent): + component_el = self._create_element(parent, "Component", {"IfcGuid": data.ifc_guid}) + text_map = { + "OriginatingSystem": data.originating_system, + "AuthoringToolId": data.authoring_tool_id, + } + for key, value in text_map.items(): + if value: + self._create_element(component_el, key, text=value) + + def add_viewpoint(self, topic, viewpoint=None): + if not viewpoint: + viewpoint = bcf.v3.data.Viewpoint() + if not viewpoint.guid: + viewpoint.guid = str(uuid.uuid4()) + if not viewpoint.viewpoint: + viewpoint.viewpoint = f"{viewpoint.guid}.bcfv" + 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): + filename = viewpoint.guid + os.path.splitext(viewpoint.snapshot)[-1] + 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): + if guid not in topic.viewpoints: + return + viewpoint = topic.viewpoints[guid] + if viewpoint.snapshot: + filepath = os.path.join(self.filepath, topic.guid, viewpoint.snapshot) + if os.path.exists(filepath): + os.remove(filepath) + if viewpoint.viewpoint: + filepath = os.path.join(self.filepath, topic.guid, viewpoint.viewpoint) + if os.path.exists(filepath): + os.remove(filepath) + for bitmap in viewpoint.bitmaps: + if not bitmap.reference: + continue + filepath = os.path.join(self.filepath, topic.guid, bitmap.reference) + if os.path.exists(filepath): + os.remove(filepath) + 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.v3.data.Header() + topic.header.files.append(header_file) + self.edit_topic(topic) + + def get_comments(self, guid): + comments = {} + if "Comments" not in data["Topics"]: + return comments + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + for item in data["Topic"]["Comments"].get("Comment", []): + comment = bcf.v3.data.Comment() + mandatory_keys = { + "guid": "@Guid", + "date": "Date", + "author": "Author", + } + for key, value in mandatory_keys.items(): + setattr(comment, key, item[value]) + optional_keys = { + "comment": "Comment", + "modified_date": "ModifiedDate", + "modified_author": "ModifiedAuthor", + } + for key, value in optional_keys.items(): + if value in item: + setattr(comment, key, item[value]) + if "Viewpoint" in item: + viewpoint = bcf.v3.data.Viewpoint() + viewpoint.guid = item["Viewpoint"]["@Guid"] + comment.viewpoint = viewpoint + comments[comment.guid] = comment + self.topics[guid].comments = comments + return comments + + def get_viewpoints(self, guid): + viewpoints = {} + if "Viewpoints" not in data["Topic"]: + return viewpoints + data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd") + for item in data["Topic"]["Viewpoints"]: + viewpoint = self.get_viewpoint(item, guid) + viewpoints[viewpoint.guid] = viewpoint + self.topics[guid].viewpoints = viewpoints + return viewpoints + + def get_viewpoint(self, data, topic_guid): + viewpoint = bcf.v3.data.Viewpoint() + viewpoint.guid = data["@Guid"] + optional_keys = { + "viewpoint": "Viewpoint", + "snapshot": "Snapshot", + "index": "Index", + } + for key, value in optional_keys.items(): + if value in data: + setattr(viewpoint, key, data[value]) + visinfo = self._read_xml(os.path.join(topic_guid, viewpoint.viewpoint), "visinfo.xsd") + viewpoint.components = self.get_viewpoint_components(visinfo) + viewpoint.orthogonal_camera = self.get_viewpoint_orthogonal_camera(visinfo) + viewpoint.perspective_camera = self.get_viewpoint_perspective_camera(visinfo) + viewpoint.lines = self.get_viewpoint_lines(visinfo) + viewpoint.clipping_planes = self.get_viewpoint_clipping_planes(visinfo) + viewpoint.bitmaps = self.get_viewpoint_bitmaps(visinfo) + return viewpoint + + def get_viewpoint_components(self, visinfo): + if "Components" not in visinfo: + return None + components = bcf.v3.data.Components() + data = visinfo["Components"] + if "Selection" in data and "Component" in data["Selection"]: + for item in data["Selection"].get("Component", []): + components.selection.append(self.get_component(item)) + if "Visibility" in data: + component_visibility = bcf.v3.data.ComponentVisibility() + if "@DefaultVisibility" in data["Visibility"]: + component_visibility.default_visibility = data["Visibility"]["@DefaultVisibility"] + if "Exceptions" in data["Visibility"] and "Component" in data["Visibility"]["Exceptions"]: + for item in data["Visibility"]["Exceptions"]["Component"]: + component_visibility.exceptions.append(self.get_component(item)) + if "ViewSetupHints" in data["Visibility"]: + view_setup_hints = bcf.v3.data.ViewSetupHints() + optional_keys = { + "spaces_visible": "@SpacesVisible", + "space_boundaries_visible": "@SpaceBoundariesVisible", + "openings_visible": "@OpeningsVisible", + } + for key, value in optional_keys.items(): + if value in data["Visibility"]["ViewSetupHints"]: + setattr(view_setup_hints, key, data["Visibility"]["ViewSetupHints"][value]) + component_visibility.view_setup_hints = view_setup_hints + components.visibility = component_visibility + if "Coloring" in data and "Color" in data["Coloring"]: + for item in data["Coloring"]["Color"]: + color = bcf.v3.data.Color() + color.color = item["@Color"] + for item2 in item["Components"]["Component"]: + color.components.append(self.get_component(item2)) + components.coloring.append(color) + return components + + def get_viewpoint_orthogonal_camera(self, visinfo): + if "OrthogonalCamera" not in visinfo: + return None + camera = bcf.v3.data.OrthogonalCamera() + data = visinfo["OrthogonalCamera"] + self.set_vector(camera.camera_view_point, data["CameraViewPoint"]) + self.set_vector(camera.camera_direction, data["CameraDirection"]) + self.set_vector(camera.camera_up_vector, data["CameraUpVector"]) + camera.view_to_world_scale = data["ViewToWorldScale"] + camera.aspect_ratio = data["AspectRatio"] + return camera + + def get_viewpoint_perspective_camera(self, visinfo): + if "PerspectiveCamera" not in visinfo: + return None + camera = bcf.v3.data.PerspectiveCamera() + data = visinfo["PerspectiveCamera"] + self.set_vector(camera.camera_view_point, data["CameraViewPoint"]) + self.set_vector(camera.camera_direction, data["CameraDirection"]) + self.set_vector(camera.camera_up_vector, data["CameraUpVector"]) + camera.field_of_view = data["FieldOfView"] + camera.aspect_ratio = data["AspectRatio"] + return camera + + def get_viewpoint_lines(self, visinfo): + if "Lines" not in visinfo: + return [] + lines = [] + for item in visinfo["Lines"].get("Line", []): + line = bcf.v3.data.Line() + self.set_vector(line.start_point, item["StartPoint"]) + self.set_vector(line.end_point, item["EndPoint"]) + lines.append(line) + return lines + + def get_viewpoint_clipping_planes(self, visinfo): + if "ClippingPlanes" not in visinfo: + return [] + planes = [] + for item in visinfo["ClippingPlanes"]["ClippingPlane"]: + plane = bcf.v3.data.ClippingPlane() + self.set_vector(plane.location, item["Location"]) + self.set_vector(plane.direction, item["Direction"]) + planes.append(plane) + return planes + + def get_viewpoint_bitmaps(self, visinfo): + if "Bitmaps" not in visinfo: + return [] + bitmaps = [] + for item in visinfo["Bitmaps"].get("Bitmap"): + bitmap = bcf.v3.data.Bitmap() + bitmap.reference = item["Reference"] + bitmap.bitmap_format = item["Format"].upper() + self.set_vector(bitmap.location, item["Location"]) + self.set_vector(bitmap.normal, item["Normal"]) + self.set_vector(bitmap.up, item["Up"]) + bitmap.height = item["Height"] + bitmaps.append(bitmap) + return bitmaps + + def set_vector(self, to_obj, from_xml): + to_obj.x = from_xml["X"] + to_obj.y = from_xml["Y"] + to_obj.z = from_xml["Z"] + + def get_component(self, data): + component = bcf.v3.data.Component() + optional_keys = { + "originating_system": "OriginatingSystem", + "authoring_tool_id": "AuthoringToolId", + "ifc_guid": "@IfcGuid", + } + for key, value in optional_keys.items(): + if value in data: + setattr(component, key, data[value]) + return component + + def close_project(self): + shutil.rmtree(self.filepath) + + def _read_xml(self, filename, xsd): + schema = XMLSchema(os.path.join(cwd, "xsd", xsd)) + filepath = os.path.join(self.filepath, filename) + (data, errors) = schema.to_dict(filepath, validation="lax") + for error in errors: + self.logger.error(error) + return data + + def _create_element(self, parent, name, attributes={}, text=None): + element = self.document.createElement(name) + for key, value in attributes.items(): + if isinstance(value, bool): + element.setAttribute(key, str(value).lower()) + elif value: + element.setAttribute(key, value) + if text is not None: + text = self.document.createTextNode(str(text)) + element.appendChild(text) + parent.appendChild(element) + return element + + def __del__(self): + self.close_project() diff --git a/src/bcf/bcf/v3/data.py b/src/bcf/bcf/v3/data.py new file mode 100644 index 0000000000..b6f241e148 --- /dev/null +++ b/src/bcf/bcf/v3/data.py @@ -0,0 +1,181 @@ +class Project: + def __init__(self): + self.project_id = "" + self.name = "" + + +class BimSnippet: + def __init__(self): + self.snippet_type = None + self.is_external = False + self.reference = None + self.reference_schema = None + + +class DocumentReference: + def __init__(self): + self.description = None + self.document_guid = None + self.url = None + self.guid = None + + +class RelatedTopic: + def __init__(self): + self.guid = None + + +class HeaderFile: + def __init__(self): + self.file_name = "" + self.date = None + self.reference = "" + self.ifc_project = None + self.ifc_spatial_structure_element = None + self.is_external = True + + +class Header: + def __init__(self): + self.files = [] + + +class Topic: + def __init__(self): + self.reference_links = [] + self.title = "" + self.priority = None + self.index = None # Deprecated, stored, but ignored + self.labels = [] + self.creation_date = None + self.creation_author = None + self.modified_date = None + self.modified_author = None + self.due_date = None + self.assigned_to = None + self.stage = None + self.description = None + self.bim_snippet = None + self.document_references = [] + self.related_topics = [] + self.topic_status = None + self.topic_type = None + self.guid = None + + self.header = None + self.comments = {} + self.viewpoints = {} + self.server_assigned_id = "" + + +class Comment: + def __init__(self): + self.guid = None + self.date = None + self.author = "" + self.comment = "" + self.viewpoint = None + self.modified_date = None + self.modified_author = "" + + +class ViewSetupHints: + def __init__(self): + self.spaces_visible = False + self.space_boundaries_visible = False + self.openings_visible = False + + +class Component: + def __init__(self): + self.originating_system = None + self.authoring_tool_id = None + self.ifc_guid = None + + +class ComponentVisibility: + def __init__(self): + self.exceptions = [] + self.default_visibility = False + self.view_setup_hints = None + + +class Color: + def __init__(self): + self.color = None + self.components = [] + + +class Components: + def __init__(self): + + self.selection = [] + self.visibility = None + self.coloring = [] + + +class Point: + def __init__(self): + self.x = 0 + self.y = 0 + self.z = 0 + + +class Direction(Point): + pass + + +class OrthogonalCamera: + def __init__(self): + self.camera_view_point = Point() + self.camera_direction = Direction() + self.camera_up_vector = Direction() + self.view_to_world_scale = 1.0 + self.aspect_ratio = 1.0 + + +class PerspectiveCamera: + def __init__(self): + self.camera_view_point = Point() + self.camera_direction = Direction() + self.camera_up_vector = Direction() + self.field_of_view = 60.0 + self.aspect_ratio = 1.0 + + +class Line: + def __init__(self): + self.start_point = Point() + self.end_point = Point() + + +class ClippingPlane: + def __init__(self): + self.location = Point() + self.direction = Direction() + + +class Bitmap: + def __init__(self): + self.reference = "" # Only in BCF-XML + self.bitmap_data = None # Only in BCF-API + self.bitmap_format = "PNG" # Enum of png or jpg + self.location = Point() + self.normal = Direction() + self.up = Direction() + self.height = 1.0 + + +class Viewpoint: + def __init__(self): + self.guid = None + self.viewpoint = None + self.snapshot = None + self.index = None + + self.components = None # It's not a list, despite the plural name + self.orthogonal_camera = None + self.perspective_camera = None + self.lines = [] + self.clipping_planes = [] + self.bitmaps = [] diff --git a/src/bcf/bcf/v3/xsd/documents.xsd b/src/bcf/bcf/v3/xsd/documents.xsd new file mode 100644 index 0000000000..f02331f65c --- /dev/null +++ b/src/bcf/bcf/v3/xsd/documents.xsd @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/bcf/bcf/v3/xsd/extensions.xsd b/src/bcf/bcf/v3/xsd/extensions.xsd new file mode 100644 index 0000000000..f22f79d88d --- /dev/null +++ b/src/bcf/bcf/v3/xsd/extensions.xsd @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/bcf/bcf/v3/xsd/markup.xsd b/src/bcf/bcf/v3/xsd/markup.xsd new file mode 100644 index 0000000000..59185058a3 --- /dev/null +++ b/src/bcf/bcf/v3/xsd/markup.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/bcf/bcf/v3/xsd/project.xsd b/src/bcf/bcf/v3/xsd/project.xsd new file mode 100644 index 0000000000..9383d037df --- /dev/null +++ b/src/bcf/bcf/v3/xsd/project.xsd @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/bcf/bcf/v3/xsd/shared-types.xsd b/src/bcf/bcf/v3/xsd/shared-types.xsd new file mode 100644 index 0000000000..93dd4843fb --- /dev/null +++ b/src/bcf/bcf/v3/xsd/shared-types.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/bcf/bcf/v3/xsd/version.xsd b/src/bcf/bcf/v3/xsd/version.xsd new file mode 100644 index 0000000000..608b45fa71 --- /dev/null +++ b/src/bcf/bcf/v3/xsd/version.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/bcf/bcf/v3/xsd/visinfo.xsd b/src/bcf/bcf/v3/xsd/visinfo.xsd new file mode 100644 index 0000000000..49ef666712 --- /dev/null +++ b/src/bcf/bcf/v3/xsd/visinfo.xsd @@ -0,0 +1,220 @@ + + + + + + VisualizationInfo documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + view's visible vertical size in meters + + + + + + Proportional relationship between the width and the height of the view (w/h). + + + + + + + + + + + + + + Vertical field of view, in degrees. + It is currently limited to a value between 45 and 60 degrees. + This limitation will be dropped in the next release and viewers + should be expect values outside this range in current implementations. + + + + + + + Proportional relationship between the width and the height of the view (w/h). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 973b705fc2..23aa47d9f7 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -1,45 +1,6 @@ VERSION:=`date '+%y%m%d'` PYVERSION:=py37 -ifeq ($(PLATFORM), win) -ifeq ($(PYVERSION), py37) -PYTHONOCC_URL:=https://anaconda.org/DLR-SC/pythonocc-core/0.17.3/download/win-64/pythonocc-core-0.17.3-py37he980bc4_10.tar.bz2 -OCE_URL:=https://anaconda.org/DLR-SC/oce/0.17.2/download/win-64/oce-0.17.2-he980bc4_14.tar.bz2 -TBB_URL:=https://anaconda.org/DLR-SC/tbb/2019.5/download/win-64/tbb-2019.5-he980bc4_0.tar.bz2 -endif -ifeq ($(PYVERSION), py39) -PYTHONOCC_URL:=https://anaconda.org/conda-forge/pythonocc-core/7.4.1/download/win-64/pythonocc-core-7.4.1-py39h3d1c7c5_0.tar.bz2 -OCE_URL:=https://anaconda.org/conda-forge/occt/7.4.0/download/win-64/occt-7.4.0-h823b557_3.tar.bz2 -TBB_URL:=https://anaconda.org/conda-forge/tbb/2020.2/download/win-64/tbb-2020.2-h2d74725_4.tar.bz2 -endif -endif - -ifeq ($(PLATFORM), macos) -ifeq ($(PYVERSION), py37) -PYTHONOCC_URL:=https://anaconda.org/DLR-SC/pythonocc-core/0.17.3/download/osx-64/pythonocc-core-0.17.3-py37h04f5b5a_10.tar.bz2 -OCE_URL:=https://anaconda.org/DLR-SC/oce/0.17.2/download/osx-64/oce-0.17.2-h04f5b5a_12.tar.bz2 -TBB_URL:=https://anaconda.org/DLR-SC/tbb/4.3.6/download/osx-64/tbb-4.3.6-0.tar.bz2 -endif -ifeq ($(PYVERSION), py39) -PYTHONOCC_URL:=https://anaconda.org/conda-forge/pythonocc-core/7.4.1/download/osx-64/pythonocc-core-7.4.1-py39h4d29fe3_0.tar.bz2 -OCE_URL:=https://anaconda.org/conda-forge/occt/7.4.0/download/osx-64/occt-7.4.0-hb9b6dc7_3.tar.bz2 -TBB_URL:=https://anaconda.org/conda-forge/tbb/2020.2/download/osx-64/tbb-2020.2-h940c156_4.tar.bz2 -endif -endif - -ifeq ($(PLATFORM), linux) -ifeq ($(PYVERSION), py37) -PYTHONOCC_URL:=https://anaconda.org/DLR-SC/pythonocc-core/0.17.3/download/linux-64/pythonocc-core-0.17.3-py37h6bb024c_10.tar.bz2 -OCE_URL:=https://anaconda.org/DLR-SC/oce/0.17.2/download/linux-64/oce-0.17.2-h6bb024c_14.tar.bz2 -TBB_URL:=https://anaconda.org/DLR-SC/tbb/4.3.6/download/linux-64/tbb-4.3.6-0.tar.bz2 -endif -ifeq ($(PYVERSION), py39) -PYTHONOCC_URL:=https://anaconda.org/conda-forge/pythonocc-core/7.4.1/download/linux-64/pythonocc-core-7.4.1-py39h465cb30_0.tar.bz2 -OCE_URL:=https://anaconda.org/conda-forge/occt/7.4.0/download/linux-64/occt-7.4.0-h9121d39_3.tar.bz2 -TBB_URL:=https://anaconda.org/conda-forge/tbb/2020.2/download/linux-64/tbb-2020.2-h4bd325d_4.tar.bz2 -endif -endif - .PHONY: dist dist: ifndef PLATFORM @@ -52,10 +13,10 @@ endif # Provides IfcOpenShell Python functionality ifeq ($(PYVERSION), py37) - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-ff7219b-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-81ad689-$(PLATFORM)64.zip endif ifeq ($(PYVERSION), py39) - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-ff7219b-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-81ad689-$(PLATFORM)64.zip endif cd dist/working && unzip ifcblender* cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/ @@ -67,7 +28,7 @@ endif # Provides IfcConvert for construction documentation mkdir dist/working - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.6.0-517b819-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.6.0-81ad689-$(PLATFORM)64.zip cd dist/working && unzip IfcConvert* ifeq ($(PLATFORM), win) cp -r dist/working/IfcConvert.exe dist/blenderbim/libs/ @@ -76,66 +37,6 @@ else endif rm -rf dist/working - # Provides Python OCC functionality for cutting IFC geometry for construction documentation - mkdir dist/working - cd dist/working && wget $(PYTHONOCC_URL) - cd dist/working && tar -xjvf pythonocc-core* -ifeq ($(PLATFORM), win) - cd dist/working && cp -r Lib/site-packages/OCC ../blenderbim/libs/site/packages/ -else -ifeq ($(PYVERSION), py37) - cd dist/working && cp -r lib/python3.7/site-packages/OCC ../blenderbim/libs/site/packages/ -endif -ifeq ($(PYVERSION), py39) - cd dist/working && cp -r lib/python3.9/site-packages/OCC ../blenderbim/libs/site/packages/ -endif -endif - rm -rf dist/working - - # Required by Python OCC - mkdir dist/working - cd dist/working && wget $(OCE_URL) - cd dist/working && tar -xjvf oc* -ifeq ($(PLATFORM), win) -ifeq ($(PYVERSION), py37) - cd dist/working && cp -r Library/bin/* ../blenderbim/libs/site/packages/OCC/ -endif -ifeq ($(PYVERSION), py39) - cd dist/working && cp -r Library/bin/* ../blenderbim/libs/site/packages/OCC/Core/ -endif -else - # Unix Conda builds of PythonOCC expect OCE libs to have a RPATH of ../../../ - cd dist/working && cp -r lib/* ../blenderbim/libs/ -endif -ifeq ($(PLATFORM), linux) - rm -rf dist/blenderbim/libs/oce-0.17 -endif - rm -rf dist/working - - # Required by OpenCascade - mkdir dist/working - cd dist/working && wget $(TBB_URL) - cd dist/working && tar -xjvf tbb* -ifeq ($(PLATFORM), win) -ifeq ($(PYVERSION), py37) - cd dist/working && cp -r Library/bin/* ../blenderbim/libs/site/packages/OCC/ -endif -ifeq ($(PYVERSION), py39) - cd dist/working && cp -r Library/bin/* ../blenderbim/libs/site/packages/OCC/Core/ -endif -else - cd dist/working && cp -r lib/* ../blenderbim/libs/ -endif - rm -rf dist/working - -ifeq ($(PLATFORM), macos) - mkdir dist/working - cd dist/working && wget https://blenderbim.org/builds/patch-blender28-bim-macos.zip - cd dist/working && unzip patch* - cd dist/working && cp -r *.dylib ../blenderbim/libs/ - rm -rf dist/working -endif - # Provides dependencies that are part of IfcOpenShell mkdir dist/working cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.6.0.zip @@ -161,6 +62,8 @@ endif cp -r dist/working/IfcOpenShell-0.6.0/src/ifccsv/* dist/blenderbim/libs/site/packages/ # Provides IFCPatch functionality cp -r dist/working/IfcOpenShell-0.6.0/src/ifcpatch/ifcpatch dist/blenderbim/libs/site/packages/ + # Provides IFCP6 functionality + cp -r dist/working/IfcOpenShell-0.6.0/src/ifcp6/ifcp6 dist/blenderbim/libs/site/packages/ rm -rf dist/working # Provides Mustache templating in construction documentation @@ -184,6 +87,27 @@ endif cp -r dist/working/python-dateutil-2.8.1/dateutil dist/blenderbim/libs/site/packages/ rm -rf dist/working + # Provides duration parsing for construction sequencing + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/b1/80/fb8c13a4cd38eb5021dc3741a9e588e4d1de88d895c1910c6fc8a08b7a70/isodate-0.6.0.tar.gz + cd dist/working && tar -xzvf isodate* + cp -r dist/working/isodate-0.6.0/src/isodate dist/blenderbim/libs/site/packages/ + rm -rf dist/working + + # Provides networkx graph analysis for project dependency calculations + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/b0/21/adfbf6168631e28577e4af9eb9f26d75fe72b2bb1d33762a5f2c425e6c2a/networkx-2.5.1.tar.gz + cd dist/working && tar -xzvf networkx* + cp -r dist/working/networkx-2.5.1/networkx dist/blenderbim/libs/site/packages/ + rm -rf dist/working + + # Required by networkx + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/4f/51/15a4f6b8154d292e130e5e566c730d8ec6c9802563d58760666f1818ba58/decorator-5.0.9.tar.gz + cd dist/working && tar -xzvf decorator* + cp -r dist/working/decorator-5.0.9/src/decorator.py dist/blenderbim/libs/site/packages/ + rm -rf dist/working + # Provides jsgantt-improved supports for web-based construction sequencing gantt charts mkdir dist/working cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js diff --git a/src/blenderbim/blenderbim/__init__.py b/src/blenderbim/blenderbim/__init__.py index e26493b051..3251fdd873 100644 --- a/src/blenderbim/blenderbim/__init__.py +++ b/src/blenderbim/blenderbim/__init__.py @@ -15,6 +15,7 @@ import site # process *.pth in /libs/site/packages to setup globally importable modules # 3 levels deep required by occ static ../../ path +# TODO: 3 levels deep is no longer required as we no longer bundle OCC cwd = os.path.dirname(os.path.realpath(__file__)) site.addsitedir(os.path.join(cwd, "libs", "site", "packages")) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 65618371a3..c3f80faedc 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -6,14 +6,16 @@ bpy = sys.modules.get("bpy") if bpy is not None: import bpy import importlib - from . import handler, ui, prop, operator, gizmos + from . import handler, ui, prop, operator modules = { "project": None, + "parametric": None, "search": None, "bcf": None, "root": None, "unit": None, + "model": None, "georeference": None, "context": None, "drawing": None, @@ -29,10 +31,10 @@ if bpy is not None: "sequence": None, "group": None, "structural": None, + "boundary": None, "material": None, "style": None, "layer": None, - "model": None, "owner": None, "pset": None, "qto": None, @@ -61,97 +63,34 @@ if bpy is not None: operator.ExportIFC, operator.ImportIFC, operator.SelectExternalMaterialDir, - operator.AddSweptSolid, - operator.RemoveSweptSolid, - operator.AssignSweptSolidOuterCurve, - operator.SelectSweptSolidOuterCurve, - operator.AddSweptSolidInnerCurve, - operator.SelectSweptSolidInnerCurves, - operator.AssignSweptSolidExtrusion, - operator.SelectSweptSolidExtrusion, operator.FetchExternalMaterial, operator.FetchObjectPassport, - operator.CutSection, - operator.AddSheet, - operator.OpenSheet, - operator.AddDrawingToSheet, - operator.CreateSheets, - operator.OpenView, - operator.OpenViewCamera, - operator.ActivateView, operator.OpenUpstream, operator.AddSectionPlane, operator.RemoveSectionPlane, operator.ReloadIfcFile, operator.AddIfcFile, operator.RemoveIfcFile, - operator.SelectDocIfcFile, - operator.AddAnnotation, - operator.GenerateReferences, - operator.ResizeText, - operator.AddVariable, - operator.RemoveVariable, - operator.PropagateTextData, operator.SetOverrideColour, - operator.RemoveDrawing, - operator.AddDrawingStyle, - operator.RemoveDrawingStyle, - operator.SaveDrawingStyle, - operator.ActivateDrawingStyle, - operator.EditVectorStyle, - operator.RemoveSheet, - operator.AddSchedule, - operator.RemoveSchedule, - operator.SelectScheduleFile, - operator.BuildSchedule, - operator.AddScheduleToSheet, operator.SetViewportShadowFromSun, - operator.AddDrawingStyleAttribute, - operator.RemoveDrawingStyleAttribute, operator.CopyPropertyToSelection, operator.CopyAttributeToSelection, - operator.RefreshDrawingList, - operator.CleanWireframes, operator.LinkIfc, operator.SnapSpacesTogether, - operator.CopyGrid, - operator.AddSectionsAnnotations, prop.StrProperty, prop.Attribute, - prop.Variable, - prop.Drawing, - prop.Schedule, - prop.DrawingStyle, - prop.Sheet, prop.BIMProperties, - prop.DocProperties, prop.IfcParameter, prop.PsetQto, prop.GlobalId, - prop.RepresentationItem, prop.BIMObjectProperties, prop.BIMMaterialProperties, - prop.SweptSolid, prop.ItemSlotMap, prop.BIMMeshProperties, - prop.BIMCameraProperties, - prop.BIMTextProperties, ui.BIM_PT_section_plane, - ui.BIM_PT_drawings, - ui.BIM_PT_schedules, - ui.BIM_PT_sheets, - ui.BIM_PT_text, - ui.BIM_PT_annotation_utilities, - ui.BIM_PT_misc_utilities, ui.BIM_UL_generic, - ui.BIM_UL_drawinglist, ui.BIM_UL_topics, ui.BIM_ADDON_preferences, - gizmos.UglyDotGizmo, - gizmos.DotGizmo, - gizmos.DimensionLabelGizmo, - gizmos.ExtrusionGuidesGizmo, - gizmos.ExtrusionWidget ] for module in modules.values(): @@ -177,24 +116,21 @@ if bpy is not None: bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) - bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties) bpy.types.Object.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties) bpy.types.Material.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties) - bpy.types.Collection.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties) # Check if we need this + bpy.types.Collection.BIMObjectProperties = bpy.props.PointerProperty( + type=prop.BIMObjectProperties + ) # Check if we need this bpy.types.Material.BIMMaterialProperties = bpy.props.PointerProperty(type=prop.BIMMaterialProperties) bpy.types.Mesh.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Camera.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.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.SCENE_PT_unit.append(ui.ifc_units) for module in modules.values(): module.register() - bpy.app.handlers.depsgraph_update_pre.append(operator.depsgraph_update_pre_handler) - bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad) - def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) @@ -204,20 +140,15 @@ if bpy is not None: bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) del bpy.types.Scene.BIMProperties - del bpy.types.Scene.DocProperties del bpy.types.Object.BIMObjectProperties del bpy.types.Material.BIMObjectProperties - del bpy.types.Collection.BIMObjectProperties # Check if we need this + del bpy.types.Collection.BIMObjectProperties # Check if we need this del bpy.types.Material.BIMMaterialProperties del bpy.types.Mesh.BIMMeshProperties del bpy.types.Curve.BIMMeshProperties del bpy.types.Camera.BIMMeshProperties - del bpy.types.Camera.BIMCameraProperties - del bpy.types.TextCurve.BIMTextProperties + del bpy.types.PointLight.BIMMeshProperties bpy.types.SCENE_PT_unit.remove(ui.ifc_units) for module in reversed(list(modules.values())): module.unregister() - - bpy.app.handlers.depsgraph_update_pre.remove(operator.depsgraph_update_pre_handler) - bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad) diff --git a/src/blenderbim/blenderbim/bim/cut_ifc.py b/src/blenderbim/blenderbim/bim/cut_ifc.py deleted file mode 100644 index 2fbe91a29a..0000000000 --- a/src/blenderbim/blenderbim/bim/cut_ifc.py +++ /dev/null @@ -1,552 +0,0 @@ -import os -import re -import math -import time -import numpy -import pickle -import multiprocessing - - -def load_occ(): - # Don't import until we really need to, as a temporary step before we can purge OCC - try: - from OCC.Core import ( - gp, - Geom, - Bnd, - BRepBndLib, - BRep, - BRepPrimAPI, - BRepAlgoAPI, - BRepBuilderAPI, - TopOpeBRepTool, - TopOpeBRepBuild, - ShapeExtend, - GProp, - BRepGProp, - GC, - ShapeAnalysis, - TopTools, - TopExp, - TopAbs, - HLRAlgo, - HLRBRep, - TopLoc, - Bnd, - BRepBndLib, - BRepTools, - TopoDS, - GeomLProp, - IntCurvesFace, - ) - from OCC.Core.TopoDS import topods - except ImportError: - from OCC import ( - gp, - Geom, - Bnd, - BRepBndLib, - BRep, - BRepPrimAPI, - BRepAlgoAPI, - BRepBuilderAPI, - TopOpeBRepTool, - TopOpeBRepBuild, - ShapeExtend, - GProp, - BRepGProp, - GC, - ShapeAnalysis, - TopTools, - TopExp, - TopAbs, - HLRAlgo, - HLRBRep, - TopLoc, - Bnd, - BRepBndLib, - BRepTools, - TopoDS, - GeomLProp, - IntCurvesFace, - ) - from OCC.TopoDS import topods - -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.util.selector -import ifcopenshell.util.element - -cwd = os.path.dirname(os.path.realpath(__file__)) -this_file = os.path.join(cwd, "cut_ifc.py") - - -def get_booleaned_edges(shape): - load_occ() - edges = [] - exp = TopExp.TopExp_Explorer(shape, TopAbs.TopAbs_EDGE) - while exp.More(): - edges.append(topods.Edge(exp.Current())) - exp.Next() - return edges - - -def connect_edges_into_wires(unconnected_edges): - load_occ() - edges = TopTools.TopTools_HSequenceOfShape() - edges_handle = TopTools.Handle_TopTools_HSequenceOfShape(edges) - wires = TopTools.TopTools_HSequenceOfShape() - wires_handle = TopTools.Handle_TopTools_HSequenceOfShape(wires) - - for edge in unconnected_edges: - edges.Append(edge) - - ShapeAnalysis.ShapeAnalysis_FreeBounds.ConnectEdgesToWires(edges_handle, 1e-5, True, wires_handle) - return wires_handle.GetObject() - - -def do_cut(process_data): - load_occ() - global_id, shape, section, trsf_data = process_data - - axis = gp.gp_Ax2( - gp.gp_Pnt(trsf_data["top_left_corner"][0], trsf_data["top_left_corner"][1], trsf_data["top_left_corner"][2]), - gp.gp_Dir(trsf_data["projection"][0], trsf_data["projection"][1], trsf_data["projection"][2]), - gp.gp_Dir(trsf_data["x_axis"][0], trsf_data["x_axis"][1], trsf_data["x_axis"][2]), - ) - source = gp.gp_Ax3(axis) - destination = gp.gp_Ax3(gp.gp_Pnt(0, 0, 0), gp.gp_Dir(0, 0, -1), gp.gp_Dir(1, 0, 0)) - transformation = gp.gp_Trsf() - transformation.SetDisplacement(source, destination) - - cut_polygons = [] - section = BRepAlgoAPI.BRepAlgoAPI_Section(section, shape).Shape() - section_edges = get_booleaned_edges(section) - if len(section_edges) <= 0: - return cut_polygons - wires = connect_edges_into_wires(section_edges) - for i in range(wires.Length()): - wire_shape = wires.Value(i + 1) - - transformed_wire = BRepBuilderAPI.BRepBuilderAPI_Transform(wire_shape, transformation) - wire_shape = transformed_wire.Shape() - - wire = topods.Wire(wire_shape) - face = BRepBuilderAPI.BRepBuilderAPI_MakeFace(wire).Face() - - points = [] - exp = BRepTools.BRepTools_WireExplorer(wire) - while exp.More(): - point = BRep.BRep_Tool.Pnt(exp.CurrentVertex()) - points.append((point.X(), -point.Y())) - exp.Next() - cut_polygons.append({"global_id": global_id, "metadata": {}, "points": points}) - return cut_polygons - - -class IfcCutter: - def __init__(self): - self.time = None - self.selector = ifcopenshell.util.selector.Selector() - self.product_shapes = [] - self.background_elements = [] - self.cut_polygons = [] - self.template_variables = {} - self.metadata = {} - self.data_dir = "" - self.vector_style = "" - self.ifc_filenames = [] - self.ifc_files = {} - self.resolved_pixels = set() - self.text_pickle_file = "text.pickle" - self.metadata_pickle_file = "metadata.pickle" - self.cut_pickle_file = "cut.pickle" - self.should_recut = True - self.should_recut_selected = True - self.cut_objects = "" - self.selected_global_ids = [] - self.should_extract = True - self.diagram_name = None - self.background_image = None - self.section_box = { - "projection": (0, 1, 0), - "x_axis": (1, 0, 0), - "y_axis": (0, 0, -1), - "top_left_corner": (-2, 2, 8), - "x": 14, - "y": 9, - "z": 2, - "shape": None, - "face": None, - } - - def cut(self): - self.profile_code("Starting cut process") - self.load_ifc_files() - self.profile_code("Load IFC files") - self.get_template_variables() - self.profile_code("Get template variables") - self.get_product_shapes() - self.profile_code("Get product shapes") - self.create_section_box() - self.profile_code("Create section box") - self.get_cut_polygons() - self.profile_code("Get cut polygons") - self.get_annotation() - self.profile_code("Get annotation") - self.get_cut_polygon_metadata() - self.profile_code("Get cut polygon metadata") - - def profile_code(self, message): - if not self.time: - self.time = time.time() - print("{} :: {:.2f}".format(message, time.time() - self.time)) - self.time = time.time() - - def load_ifc_files(self): - if not self.should_recut and not self.should_extract: - return - - loaded_files = [] - for filename in self.ifc_filenames: - print("Loading file {} ...".format(filename)) - if filename: - self.ifc_files[filename] = ifcopenshell.open(filename) - - def get_template_variables(self): - if not self.should_extract: - if os.path.isfile(self.text_pickle_file): - with open(self.text_pickle_file, "rb") as text_file: - self.template_variables = pickle.load(text_file) - return - - data = {} - for text_obj in self.text_objs: - text_obj_data = self.get_text_variables(text_obj) - if text_obj_data: - data[text_obj.name] = text_obj_data - - with open(self.text_pickle_file, "wb") as text_file: - pickle.dump(data, text_file, protocol=pickle.HIGHEST_PROTOCOL) - - self.template_variables = data - - def get_text_variables(self, text_obj): - text_obj_data = {} - text_body = text_obj.data.body - related_element = text_obj.data.BIMTextProperties.related_element - if not related_element: - return - global_id = related_element.BIMObjectProperties.attributes.get("GlobalId") - if not global_id: - return - element = self.get_ifc_element(global_id.string_value) - for variable in text_obj.data.BIMTextProperties.variables: - if element: - if "{{" in variable.prop_key: - prop_key = variable.prop_key.split("{{")[1].split("}}")[0] - prop_value = self.selector.get_element_value(element, prop_key) - variable_value = eval(variable.prop_key.replace("{{" + prop_key + "}}", str(prop_value))) - else: - variable_value = self.selector.get_element_value(element, variable.prop_key) - text_obj_data[variable.name] = variable_value - return text_obj_data - - def get_product_shapes(self): - if not self.should_recut: - return - - settings = ifcopenshell.geom.settings() - settings.set(settings.USE_PYTHON_OPENCASCADE, True) - products = [] - - for filename, ifc_file in self.ifc_files.items(): - shape_pickle = os.path.join( - self.data_dir, "cache", "shapes", "{}.pickle".format(os.path.basename(filename)) - ) - shape_map = {} - if self.should_recut_selected and os.path.isfile(shape_pickle): - with open(shape_pickle, "rb") as shape_file: - shape_map = pickle.load(shape_file) - - products.extend(self.selector.parse(ifc_file, self.cut_objects)) - - selected_elements = [] - for i, product in enumerate(products): - if ( - product.is_a("IfcOpeningElement") - or product.is_a("IfcSite") - or product.Representation is None - or self.has_annotation(product) - ): - continue - try: - if self.should_recut_selected and product.GlobalId in self.selected_global_ids: - selected_elements.append(product) - elif product.GlobalId in shape_map: - shape = shape_map[product.GlobalId] - self.add_product_shape(product, shape) - else: - selected_elements.append(product) - except: - print("Failed to create shape for {}".format(product)) - - if selected_elements: - total = 0 - checkpoint = time.time() - iterator = ifcopenshell.geom.iterator( - settings, ifc_file, multiprocessing.cpu_count(), include=selected_elements - ) - valid_file = iterator.initialize() - if valid_file: - while True: - total += 1 - if total % 250 == 0: - print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint)) - checkpoint = time.time() - shape = iterator.get() - shape_map[shape.data.guid] = shape.geometry - self.add_product_shape(ifc_file.by_guid(shape.data.guid), shape.geometry) - if not iterator.next(): - break - - with open(shape_pickle, "wb") as shape_file: - pickle.dump(shape_map, shape_file, protocol=pickle.HIGHEST_PROTOCOL) - - def add_product_shape(self, product, shape): - self.product_shapes.append((product, shape)) - - def has_annotation(self, element): - for representation in element.Representation.Representations: - if ( - representation.ContextOfItems.ContextType == "Plan" - and representation.ContextOfItems.ContextIdentifier == "Annotation" - ): - return True - return False - - def create_section_box(self): - load_occ() - top_left_corner = gp.gp_Pnt( - self.section_box["top_left_corner"][0], - self.section_box["top_left_corner"][1], - self.section_box["top_left_corner"][2], - ) - axis = gp.gp_Ax2( - top_left_corner, - gp.gp_Dir( - self.section_box["projection"][0], self.section_box["projection"][1], self.section_box["projection"][2] - ), - gp.gp_Dir(self.section_box["x_axis"][0], self.section_box["x_axis"][1], self.section_box["x_axis"][2]), - ) - section_box = BRepPrimAPI.BRepPrimAPI_MakeBox( - axis, self.section_box["x"], self.section_box["y"], self.section_box["z"] - ) - self.section_box["shape"] = section_box.Shape() - self.section_box["face"] = section_box.BottomFace() - - source = gp.gp_Ax3(axis) - self.transformation_data = { - "top_left_corner": self.section_box["top_left_corner"], - "projection": self.section_box["projection"], - "x_axis": self.section_box["x_axis"], - } - destination = gp.gp_Ax3(gp.gp_Pnt(0, 0, 0), gp.gp_Dir(0, 0, -1), gp.gp_Dir(1, 0, 0)) - self.transformation_dest = destination - self.transformation = gp.gp_Trsf() - self.transformation.SetDisplacement(source, destination) - - def get_bbox(self, shape): - load_occ() - bbox = Bnd.Bnd_Box() - BRepBndLib.brepbndlib_Add(shape, bbox) - return bbox - - def calculate_face_zpos(self, face): - bbox = self.get_bbox(face) - xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() - zpos = zmin + ((zmax - zmin) / 2) - return zpos, zmax - - def get_booleaned_edges(self, shape): - load_occ() - edges = [] - exp = TopExp.TopExp_Explorer(shape, TopAbs.TopAbs_EDGE) - while exp.More(): - edges.append(topods.Edge(exp.Current())) - exp.Next() - return edges - - def get_cut_polygons(self): - if self.should_recut: - self.get_fresh_cut_polygons() - self.pickle_cut_polygons() - else: - self.get_pickled_cut_polygons() - - def get_annotation(self): - import mathutils - load_occ() - - self.annotation_objs = [] - settings_2d = ifcopenshell.geom.settings() - settings_2d.set(settings_2d.INCLUDE_CURVES, True) - settings_py = ifcopenshell.geom.settings() - settings_py.set(settings_py.USE_PYTHON_OPENCASCADE, True) - for ifc_file in self.ifc_files.values(): - for element in ifc_file.by_type("IfcElement"): - annotation_representation = None - box_representation = None - if not element.Representation: - continue # This can occur for aggregates - for representation in element.Representation.Representations: - if ( - representation.ContextOfItems.ContextType == "Plan" - and representation.ContextOfItems.ContextIdentifier == "Annotation" - ): - annotation_representation = representation - elif ( - representation.ContextOfItems.ContextType == "Model" - and representation.ContextOfItems.ContextIdentifier == "Box" - ): - box_representation = representation - if not annotation_representation or not box_representation: - continue - - # This is bad code. See bug #85 to make it slightly less bad. - # Effectively if the bbox does not intersect with the camera - # plane, then we should "continue" and not process the 2D - # wireframe. This approach works but is not very smart. - for subelement in ifc_file.traverse(box_representation): - if subelement.is_a("IfcBoundingBox"): - block = ifc_file.createIfcBlock( - ifc_file.createIfcAxis2Placement3D(subelement.Corner, None, None), - subelement.XDim, - subelement.YDim, - subelement.ZDim, - ) - for inverse in ifc_file.get_inverse(subelement): - ifcopenshell.util.element.replace_attribute(inverse, subelement, block) - element.Representation.Representations = [box_representation] - shape = ifcopenshell.geom.create_shape(settings_py, element) - - section = BRepAlgoAPI.BRepAlgoAPI_Section(self.section_box["face"], shape.geometry).Shape() - section_edges = get_booleaned_edges(section) - - if len(section_edges) <= 0: - # The bounding box of the annotation object does not - # intersect with the camera plane, so don't bother drawing - # the annotation - continue - - # Monkey patch - see bug #771. - element.Representation.Representations = [annotation_representation] - shape = ifcopenshell.geom.create_shape(settings_2d, element) - if hasattr(shape, "geometry"): - geometry = shape.geometry - else: - geometry = shape - e = geometry.edges - v = geometry.verts - m = shape.transformation.matrix.data - mat = mathutils.Matrix( - ([m[0], m[1], m[2], 0], [m[3], m[4], m[5], 0], [m[6], m[7], m[8], 0], [m[9], m[10], m[11], 1]) - ) - mat.transpose() - self.annotation_objs.append( - { - "raw": element, - "classes": self.get_classes(element, "annotation"), - "edges": [[e[i], e[i + 1]] for i in range(0, len(e), 2)], - "vertices": [mat @ mathutils.Vector((v[i], v[i + 1], v[i + 2])) for i in range(0, len(v), 3)], - } - ) - - def get_cut_polygon_metadata(self): - if not self.should_extract: - if os.path.isfile(self.metadata_pickle_file): - with open(self.metadata_pickle_file, "rb") as metadata_file: - self.metadata = pickle.load(metadata_file) - - for polygon in self.cut_polygons: - if polygon["global_id"] in self.metadata: - polygon["metadata"] = self.metadata[polygon["global_id"]] - return - - for polygon in self.cut_polygons: - metadata = {"classes": self.get_classes(self.get_ifc_element(polygon["global_id"]), "cut")} - self.metadata[polygon["global_id"]] = metadata - polygon["metadata"] = metadata - - with open(self.metadata_pickle_file, "wb") as metadata_file: - pickle.dump(self.metadata, metadata_file, protocol=pickle.HIGHEST_PROTOCOL) - - def pickle_cut_polygons(self): - with open(self.cut_pickle_file, "wb") as pickle_file: - pickle.dump(self.cut_polygons, pickle_file, protocol=pickle.HIGHEST_PROTOCOL) - - def get_fresh_cut_polygons(self): - process_data = [ - (p.GlobalId, s, self.section_box["face"], self.transformation_data) for p, s in self.product_shapes - ] - - import bpy - - if bpy.app.version > (2, 90, 0) and os.name == 'nt': - # See bug #1148 - for data in process_data: - results = do_cut(data) - polygons = [r for r in results if r["points"]] - self.cut_polygons.extend(polygons) - else: - multiprocessing.set_executable(bpy.app.binary_path_python) - - with multiprocessing.Pool(9) as p: - results = p.map(do_cut, process_data) - for result in results: - polygons = [p for p in result if p["points"]] - self.cut_polygons.extend(polygons) - - def get_polygon_metadata(self, polygon, position): - polygon["metadata"] = {"classes": self.get_classes(self.get_ifc_element(polygon["global_id"]), position)} - return polygon - - def get_ifc_element(self, global_id): - # TODO: make this less bad - element = None - for ifc_file in self.ifc_files.values(): - try: - element = ifc_file.by_id(global_id) - return element - except: - pass - - def get_classes(self, element, position): - classes = [position, element.is_a()] - material = ifcopenshell.util.element.get_material(element) - if material: - classes.append( - "material-{}".format( - re.sub("[^0-9a-zA-Z]+", "", self.get_material_name(material)) - ) - ) - classes.append("globalid-{}".format(element.GlobalId)) - for attribute in self.attributes: - result = self.selector.get_element_value(element, attribute) - if result: - classes.append( - "{}-{}".format(re.sub("[^0-9a-zA-Z]+", "", attribute), re.sub("[^0-9a-zA-Z]+", "", result)) - ) - return classes - - def get_material_name(self, element): - if hasattr(element, "Name") and element.Name: - return element.Name - elif hasattr(element, "LayerSetName") and element.LayerSetName: - return element.LayerSetName - return "mat-" + str(element.id()) - - def get_pickled_cut_polygons(self): - if os.path.isfile(self.cut_pickle_file): - with open(self.cut_pickle_file, "rb") as pickle_file: - self.cut_polygons = pickle.load(pickle_file) diff --git a/src/blenderbim/blenderbim/bim/data/gantt/index.mustache b/src/blenderbim/blenderbim/bim/data/gantt/index.mustache index 832f8e4a8b..b90f6a4cb1 100644 --- a/src/blenderbim/blenderbim/bim/data/gantt/index.mustache +++ b/src/blenderbim/blenderbim/bim/data/gantt/index.mustache @@ -3,6 +3,19 @@