Merge branch 'v0.6.0' into GSoC#45-IDS-checking

This commit is contained in:
ArturTomczak
2021-06-21 08:48:46 +02:00
committed by GitHub
244 changed files with 15113 additions and 5939 deletions
+4
View File
@@ -16,6 +16,10 @@ __pycache__
# PyCharm files # PyCharm files
.idea .idea
#Virtual Env Files
Pipfile
Pipfile.lock
# Docs # Docs
/docs/output /docs/output
/docs/rst_files /docs/rst_files
+35 -4
View File
@@ -280,7 +280,7 @@ ENDIF()
# Use the found libTKernel as a template for all other OCC libraries # Use the found libTKernel as a template for all other OCC libraries
# TODO Extract this into macro/function # TODO Extract this into macro/function
foreach(lib ${OPENCASCADE_LIBRARY_NAMES}) 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 TKerneld "${lib}" lib_path "${libTKernel}")
string(REPLACE TKernel "${lib}" lib_path "${lib_path}") string(REPLACE TKernel "${lib}" lib_path "${lib_path}")
list(APPEND OPENCASCADE_LIBRARIES "${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 # Use the found OpenCOLLADAFramework as a template for all other OpenCOLLADA libraries
foreach(lib ${OPENCOLLADA_LIBRARY_NAMES}) 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 OpenCOLLADAFrameworkd "${lib}" lib_path "${OpenCOLLADAFramework}")
string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}") string(REPLACE OpenCOLLADAFramework "${lib}" lib_path "${lib_path}")
list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}") list(APPEND OPENCOLLADA_LIBRARIES "${lib_path}")
@@ -496,6 +496,10 @@ endfunction()
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2") set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2")
foreach(s ${SCHEMA_VERSIONS})
add_definitions(-DHAS_SCHEMA_${s})
endforeach()
if(COMPILE_SCHEMA) if(COMPILE_SCHEMA)
# @todo, this appears to be untested at the moment # @todo, this appears to be untested at the moment
@@ -573,8 +577,35 @@ if (BUILD_CONVERT)
endif() endif()
# IfcParse # IfcParse
file(GLOB IFCPARSE_H_FILES ../src/ifcparse/*.h) file(GLOB IFCPARSE_H_FILES_ALL ../src/ifcparse/*.h)
file(GLOB IFCPARSE_CPP_FILES ../src/ifcparse/*.cpp) 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}) set(IFCPARSE_FILES ${IFCPARSE_CPP_FILES} ${IFCPARSE_H_FILES})
add_library(IfcParse ${IFCPARSE_FILES}) add_library(IfcParse ${IFCPARSE_FILES})
+1 -1
View File
@@ -310,7 +310,7 @@ def run(cmds, cwd=None):
BOOST_VERSION_UNDERSCORE=BOOST_VERSION.replace(".", "_") BOOST_VERSION_UNDERSCORE=BOOST_VERSION.replace(".", "_")
OCE_LOCATION="https://github.com/tpaviot/oce/archive/OCE-%s.tar.gz" % (OCE_VERSION,) 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 # Helper functions
+8 -5
View File
@@ -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 Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API
is available via `bcfapi.py`. 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 ## bcfxml
The `bcfxml` module lets you interact with the BCF-XML standard. 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 # 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 # The project is also stored in the module
# project == bcfxml.project # project == bcfxml.project
project=bcfxml.get_project()
print(project.name) print(project.name)
# To edit a project, just modify the object directly # To edit a project, just modify the object directly
View File
+28 -754
View File
@@ -1,765 +1,39 @@
import os import os.path
import uuid
import shutil
import zipfile import zipfile
import logging
import tempfile import tempfile
import bcf.data
from datetime import datetime
from xml.dom import minidom 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
bcfxml = BcfXml()
bcfxml.filepath = filepath
return bcfxml
else:
from bcf.v3.bcfxml import BcfXml
bcfxml = BcfXml()
bcfxml.filepath = filepath
return bcfxml
@contextmanager def get_version(version_path):
def cd(newdir): xmlparse = minidom.parse(version_path)
prevdir = os.getcwd() version_el = xmlparse.getElementsByTagName("Version")[0]
os.chdir(os.path.expanduser(newdir)) version = version_el.getAttribute("VersionId")
try: return version
yield
finally:
os.chdir(prevdir)
class BcfXml: def extract_project(filepath):
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: if not filepath:
return self.project return
zip_file = zipfile.ZipFile(filepath) zip_file = zipfile.ZipFile(filepath)
self.filepath = tempfile.mkdtemp() filepath = tempfile.mkdtemp()
zip_file.extractall(self.filepath) zip_file.extractall(filepath)
if os.path.isfile(os.path.join(self.filepath, "project.bcfp")): return filepath
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
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,
},
)
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()
+771
View File
@@ -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()
@@ -157,7 +157,7 @@ class Bitmap:
def __init__(self): def __init__(self):
self.reference = "" # Only in BCF-XML self.reference = "" # Only in BCF-XML
self.bitmap_data = None # Only in BCF-API 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.location = Point()
self.normal = Direction() self.normal = Direction()
self.up = Direction() self.up = Direction()
View File
+827
View File
@@ -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()
+181
View File
@@ -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 = []
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:include schemaLocation="shared-types.xsd"/>
<xs:element name="DocumentInfo">
<xs:complexType>
<xs:sequence>
<xs:element name="Documents" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Document" type="Document" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="Document">
<xs:sequence>
<!-- Filename of the document with the file extension. Not used to store the file in the BCF -->
<xs:element name="Filename" type="NonEmptyOrBlankString"/>
<!-- Human readable description of the document -->
<xs:element name="Description" type="NonEmptyOrBlankString" minOccurs="0"/>
</xs:sequence>
<xs:attributeGroup ref="DocumentAttributes"/>
</xs:complexType>
<xs:attributeGroup name="DocumentAttributes">
<!-- Guid of the document. Must match the filename in the BCF -->
<xs:attribute name="Guid" type="Guid" use="required"/>
</xs:attributeGroup>
</xs:schema>
+60
View File
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:include schemaLocation="shared-types.xsd"/>
<xs:element name="Extensions">
<xs:complexType>
<xs:sequence>
<xs:element name="TopicTypes" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="TopicType" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="TopicStatuses" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="TopicStatus" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Priorities" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Priority" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="TopicLabels" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="TopicLabel" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Users" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="User" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="SnippetTypes" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="SnippetType" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Stages" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Stage" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
+174
View File
@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:include schemaLocation="shared-types.xsd"/>
<xs:element name="Markup">
<xs:complexType>
<xs:sequence>
<xs:element name="Header" type="Header" minOccurs="0"/>
<xs:element name="Topic" type="Topic"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="Header">
<xs:sequence>
<xs:element name="Files" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="File" type="File" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<!-- ISG Jira issue BCF-9. Add support for several viewpoints and snapshots per issue -->
<xs:complexType name="ViewPoint">
<xs:sequence>
<!-- viewpoint file (xml) -->
<xs:element name="Viewpoint" type="NonEmptyOrBlankString" minOccurs="0"/>
<!-- the snapshot png -->
<xs:element name="Snapshot" type="NonEmptyOrBlankString" minOccurs="0"/>
<!-- the viewpoint index (sort order) -->
<xs:element name="Index" type="xs:int" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="Guid" type="Guid" use="required"/>
<!-- Guid of the viewpoint -->
</xs:complexType>
<!-- BimSnippet -->
<xs:complexType name="BimSnippet">
<xs:sequence>
<!--
Name of the file in the topic folder containing the snippet or a URL.
E.G.- Expresscode containing p.e Issue, Request
// Maybe some header infos ?? // IfcEntites // Geometry
-->
<!-- Reference (name) to the snippet file -->
<xs:element name="Reference" type="NonEmptyOrBlankString"/>
<xs:element name="ReferenceSchema" type="NonEmptyOrBlankString"/>
</xs:sequence>
<xs:attribute name="SnippetType" type="NonEmptyOrBlankString" use="required"/>
<xs:attribute name="IsExternal" type="xs:boolean" default="false"/>
<!-- This flag is true when the reference is a URL pointing outside of the BCF file-->
</xs:complexType>
<xs:complexType name="Topic">
<xs:sequence>
<xs:element name="ReferenceLinks" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="ReferenceLink" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Title" type="NonEmptyOrBlankString"/>
<xs:element name="Priority" type="NonEmptyOrBlankString" minOccurs="0"/>
<!-- ISG Jira issue BCF-8 Add a way save order the topics -->
<!-- This property is deprecated and will be removed in a future release -->
<xs:element name="Index" type="xs:int" minOccurs="0"/>
<xs:element name="Labels" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Label" type="NonEmptyOrBlankString" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="CreationDate" type="xs:dateTime"/>
<xs:element name="CreationAuthor" type="NonEmptyOrBlankString"/>
<xs:element name="ModifiedDate" type="xs:dateTime" minOccurs="0"/>
<xs:element name="ModifiedAuthor" type="NonEmptyOrBlankString" minOccurs="0"/>
<xs:element name="DueDate" type="xs:dateTime" minOccurs="0"/>
<xs:element name="AssignedTo" type="NonEmptyOrBlankString" minOccurs="0"/>
<xs:element name="Stage" type="NonEmptyOrBlankString" minOccurs="0"/>
<xs:element name="Description" type="NonEmptyOrBlankString" minOccurs="0"/>
<xs:element name="BimSnippet" type="BimSnippet" minOccurs="0"/>
<xs:element name="DocumentReferences" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="DocumentReference" type="DocumentReference" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="RelatedTopics" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="RelatedTopic" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="Guid" type="Guid" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Comments" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Comment" type="Comment" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- ISG Jira issue BCF-9. Add support for several viewpoints and snapshots per issue -->
<xs:element name="Viewpoints" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="ViewPoint" type="ViewPoint" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="Guid" type="Guid" use="required"/>
<xs:attribute name="ServerAssignedId" type="NonEmptyOrBlankString" use="optional"/>
<xs:attribute name="TopicType" type="NonEmptyOrBlankString" use="required"/>
<xs:attribute name="TopicStatus" type="NonEmptyOrBlankString" use="required"/>
</xs:complexType>
<xs:complexType name="File">
<xs:sequence>
<xs:element name="Filename" type="NonEmptyOrBlankString" minOccurs="0"/>
<xs:element name="Date" type="xs:dateTime" minOccurs="0"/>
<!-- Reference (URL) of the file -->
<xs:element name="Reference" type="NonEmptyOrBlankString" minOccurs="0"/>
</xs:sequence>
<xs:attributeGroup ref="FileAttributes"/>
</xs:complexType>
<xs:attributeGroup name="FileAttributes">
<xs:attribute name="IfcProject" type="IfcGuid" use="optional"/>
<xs:attribute name="IfcSpatialStructureElement" type="IfcGuid" use="optional"/>
<xs:attribute name="IsExternal" type="xs:boolean" default="true"/>
</xs:attributeGroup>
<!-- Reference to a document inside of the topic folder or a url pointing to the web -->
<xs:complexType name="DocumentReference">
<xs:sequence>
<xs:choice>
<!-- Guid of the document: If pointing to an internal document -->
<xs:element name="DocumentGuid" type="Guid" minOccurs="0"/>
<!-- Url of the reference. If pointing to an external document -->
<xs:element name="Url" type="NonEmptyOrBlankString" minOccurs="0"/>
</xs:choice>
<!-- Human readable description of the document reference -->
<xs:element name="Description" type="NonEmptyOrBlankString" minOccurs="0"/>
</xs:sequence>
<xs:attributeGroup ref="DocumentReferenceAttributes"/>
</xs:complexType>
<xs:attributeGroup name="DocumentReferenceAttributes">
<!-- Guid of the document reference -->
<xs:attribute name="Guid" type="Guid" use="required"/>
</xs:attributeGroup>
<xs:complexType name="Comment">
<xs:sequence>
<xs:element name="Date" type="xs:dateTime"/>
<xs:element name="Author" type="NonEmptyOrBlankString"/>
<xs:element name="Comment" minOccurs="0" type="NonEmptyOrBlankString"/>
<xs:element name="Viewpoint" minOccurs="0">
<xs:complexType>
<xs:attribute name="Guid" type="Guid" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="ModifiedDate" type="xs:dateTime" minOccurs="0"/>
<xs:element name="ModifiedAuthor" type="NonEmptyOrBlankString" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="Guid" type="Guid" use="required"/>
</xs:complexType>
<xs:simpleType name="IfcGuid">
<xs:restriction base="xs:string">
<xs:length value="22"/>
<xs:pattern value="[0-9A-Za-z_$]*"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:include schemaLocation="shared-types.xsd"/>
<xs:element name="ProjectInfo">
<xs:complexType>
<xs:sequence>
<xs:element name="Project" type="Project"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="Project">
<xs:sequence>
<xs:element name="Name" type="NonEmptyOrBlankString" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="ProjectId" type="NonEmptyOrBlankString" use="required"/>
</xs:complexType>
</xs:schema>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:simpleType name="Guid">
<xs:restriction base="xs:string">
<xs:pattern value="[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="NonEmptyOrBlankString">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
<xs:whiteSpace value="collapse"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Version">
<xs:complexType>
<xs:attribute name="VersionId" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
</xs:schema>
+220
View File
@@ -0,0 +1,220 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:include schemaLocation="shared-types.xsd"/>
<xs:element name="VisualizationInfo">
<xs:annotation>
<xs:documentation>VisualizationInfo documentation</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<!--
Although plural, 'Components' is not a collection
-->
<xs:element name="Components" type="Components" minOccurs="0"/>
<xs:choice>
<xs:element name="OrthogonalCamera" type="OrthogonalCamera"/>
<xs:element name="PerspectiveCamera" type="PerspectiveCamera"/>
</xs:choice>
<xs:element name="Lines" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Line" type="Line" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="ClippingPlanes" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="ClippingPlane" type="ClippingPlane" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Bitmaps" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Bitmap" type="Bitmap" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<!-- Guid of the viewpoint -->
<xs:attribute name="Guid" type="Guid" use="required"/>
</xs:complexType>
</xs:element>
<xs:complexType name="OrthogonalCamera">
<xs:sequence>
<xs:element name="CameraViewPoint" type="Point"/>
<xs:element name="CameraDirection" type="Direction"/>
<xs:element name="CameraUpVector" type="Direction"/>
<xs:element name="ViewToWorldScale" type="xs:double">
<xs:annotation>
<xs:documentation>view's visible vertical size in meters</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="AspectRatio" type="PositiveDouble">
<xs:annotation>
<xs:documentation>
Proportional relationship between the width and the height of the view (w/h).
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="PerspectiveCamera">
<xs:sequence>
<xs:element name="CameraViewPoint" type="Point"/>
<xs:element name="CameraDirection" type="Direction"/>
<xs:element name="CameraUpVector" type="Direction"/>
<xs:element name="FieldOfView" type="FieldOfView">
<xs:annotation>
<xs:documentation>
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.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="AspectRatio" type="PositiveDouble">
<xs:annotation>
<xs:documentation>
Proportional relationship between the width and the height of the view (w/h).
</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Point">
<xs:sequence>
<xs:element name="X" type="xs:double"/>
<xs:element name="Y" type="xs:double"/>
<xs:element name="Z" type="xs:double"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Direction">
<xs:sequence>
<xs:element name="X" type="xs:double"/>
<xs:element name="Y" type="xs:double"/>
<xs:element name="Z" type="xs:double"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="PositiveDouble">
<xs:restriction base="xs:double">
<xs:minExclusive value="0"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="FieldOfView">
<xs:restriction base="xs:double">
<xs:minExclusive value="0"/>
<xs:maxExclusive value="180"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="Components">
<xs:sequence>
<!-- Components with relevance to the viewpoint. They should be displayed highlighted or selected in a viewer -->
<xs:element name="Selection" type="ComponentSelection" minOccurs="0"/>
<xs:element name="Visibility" type="ComponentVisibility" minOccurs="0"/>
<xs:element name="Coloring" type="ComponentColoring" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ComponentSelection">
<xs:sequence>
<xs:element name="Component" type="Component" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ComponentVisibility">
<xs:sequence>
<xs:element name="ViewSetupHints" type="ViewSetupHints" minOccurs="0"/>
<xs:element name="Exceptions" minOccurs="0">
<!-- List Components that are different than the DefaultVisibility. E.g. if DefaultVisibility = false then list
Components that should be visible -->
<xs:complexType>
<xs:sequence>
<xs:element name="Component" type="Component" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="DefaultVisibility" type="xs:boolean" default="false"/>
</xs:complexType>
<xs:complexType name="ViewSetupHints">
<xs:attribute name="SpacesVisible" type="xs:boolean" default="false"/>
<xs:attribute name="SpaceBoundariesVisible" type="xs:boolean" default="false"/>
<xs:attribute name="OpeningsVisible" type="xs:boolean" default="false"/>
</xs:complexType>
<xs:complexType name="ComponentColoring">
<xs:sequence>
<xs:element name="Color" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<!-- At least one component is required for a Color. -->
<xs:element name="Components">
<xs:complexType>
<xs:sequence>
<xs:element name="Component" type="Component" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute ref="Color" use="required"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Component">
<xs:sequence>
<xs:element name="OriginatingSystem" type="NonEmptyOrBlankString" minOccurs="0"/>
<xs:element name="AuthoringToolId" type="NonEmptyOrBlankString" minOccurs="0"/>
</xs:sequence>
<xs:attribute ref="IfcGuid"/>
</xs:complexType>
<xs:attribute name="Color">
<xs:simpleType>
<xs:restriction base="xs:normalizedString">
<!-- Should either match 3 or 4 hex bytes , e.g. "FF00FF" or "FF00FF99" -->
<xs:pattern value="[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="IfcGuid">
<xs:simpleType>
<xs:restriction base="xs:normalizedString">
<xs:length value="22"/>
<xs:pattern value="[0-9A-Za-z_$]*"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:complexType name="Line">
<xs:sequence>
<xs:element name="StartPoint" type="Point"/>
<xs:element name="EndPoint" type="Point"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ClippingPlane">
<xs:sequence>
<xs:element name="Location" type="Point"/>
<xs:element name="Direction" type="Direction"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Bitmap">
<xs:sequence>
<xs:element name="Format" type="BitmapFormat"/>
<!-- Name of the bitmap file in the topic folder -->
<xs:element name="Reference" type="NonEmptyOrBlankString"/>
<!-- Location of the center of the bitmap -->
<xs:element name="Location" type="Point"/>
<!-- Normal of the bitmap -->
<xs:element name="Normal" type="Direction"/>
<!-- Upvector of the bitmap -->
<xs:element name="Up" type="Direction"/>
<!-- Height of the bitmap -->
<xs:element name="Height" type="xs:double"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="BitmapFormat">
<xs:restriction base="xs:string">
<xs:enumeration value="png"/>
<xs:enumeration value="jpg"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
+26 -102
View File
@@ -1,45 +1,6 @@
VERSION:=`date '+%y%m%d'` VERSION:=`date '+%y%m%d'`
PYVERSION:=py37 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 .PHONY: dist
dist: dist:
ifndef PLATFORM ifndef PLATFORM
@@ -52,10 +13,10 @@ endif
# Provides IfcOpenShell Python functionality # Provides IfcOpenShell Python functionality
ifeq ($(PYVERSION), py37) 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 endif
ifeq ($(PYVERSION), py39) 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 endif
cd dist/working && unzip ifcblender* cd dist/working && unzip ifcblender*
cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/ cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -67,7 +28,7 @@ endif
# Provides IfcConvert for construction documentation # Provides IfcConvert for construction documentation
mkdir dist/working 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* cd dist/working && unzip IfcConvert*
ifeq ($(PLATFORM), win) ifeq ($(PLATFORM), win)
cp -r dist/working/IfcConvert.exe dist/blenderbim/libs/ cp -r dist/working/IfcConvert.exe dist/blenderbim/libs/
@@ -76,66 +37,6 @@ else
endif endif
rm -rf dist/working 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 # Provides dependencies that are part of IfcOpenShell
mkdir dist/working mkdir dist/working
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.6.0.zip 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/ cp -r dist/working/IfcOpenShell-0.6.0/src/ifccsv/* dist/blenderbim/libs/site/packages/
# Provides IFCPatch functionality # Provides IFCPatch functionality
cp -r dist/working/IfcOpenShell-0.6.0/src/ifcpatch/ifcpatch dist/blenderbim/libs/site/packages/ 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 rm -rf dist/working
# Provides Mustache templating in construction documentation # 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/ cp -r dist/working/python-dateutil-2.8.1/dateutil dist/blenderbim/libs/site/packages/
rm -rf dist/working 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 # Provides jsgantt-improved supports for web-based construction sequencing gantt charts
mkdir dist/working mkdir dist/working
cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js
+1
View File
@@ -15,6 +15,7 @@ import site
# process *.pth in /libs/site/packages to setup globally importable modules # process *.pth in /libs/site/packages to setup globally importable modules
# 3 levels deep required by occ static ../../ path # 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__)) cwd = os.path.dirname(os.path.realpath(__file__))
site.addsitedir(os.path.join(cwd, "libs", "site", "packages")) site.addsitedir(os.path.join(cwd, "libs", "site", "packages"))
+9 -78
View File
@@ -6,14 +6,16 @@ bpy = sys.modules.get("bpy")
if bpy is not None: if bpy is not None:
import bpy import bpy
import importlib import importlib
from . import handler, ui, prop, operator, gizmos from . import handler, ui, prop, operator
modules = { modules = {
"project": None, "project": None,
"parametric": None,
"search": None, "search": None,
"bcf": None, "bcf": None,
"root": None, "root": None,
"unit": None, "unit": None,
"model": None,
"georeference": None, "georeference": None,
"context": None, "context": None,
"drawing": None, "drawing": None,
@@ -29,10 +31,10 @@ if bpy is not None:
"sequence": None, "sequence": None,
"group": None, "group": None,
"structural": None, "structural": None,
"boundary": None,
"material": None, "material": None,
"style": None, "style": None,
"layer": None, "layer": None,
"model": None,
"owner": None, "owner": None,
"pset": None, "pset": None,
"qto": None, "qto": None,
@@ -61,97 +63,34 @@ if bpy is not None:
operator.ExportIFC, operator.ExportIFC,
operator.ImportIFC, operator.ImportIFC,
operator.SelectExternalMaterialDir, operator.SelectExternalMaterialDir,
operator.AddSweptSolid,
operator.RemoveSweptSolid,
operator.AssignSweptSolidOuterCurve,
operator.SelectSweptSolidOuterCurve,
operator.AddSweptSolidInnerCurve,
operator.SelectSweptSolidInnerCurves,
operator.AssignSweptSolidExtrusion,
operator.SelectSweptSolidExtrusion,
operator.FetchExternalMaterial, operator.FetchExternalMaterial,
operator.FetchObjectPassport, operator.FetchObjectPassport,
operator.CutSection,
operator.AddSheet,
operator.OpenSheet,
operator.AddDrawingToSheet,
operator.CreateSheets,
operator.OpenView,
operator.OpenViewCamera,
operator.ActivateView,
operator.OpenUpstream, operator.OpenUpstream,
operator.AddSectionPlane, operator.AddSectionPlane,
operator.RemoveSectionPlane, operator.RemoveSectionPlane,
operator.ReloadIfcFile, operator.ReloadIfcFile,
operator.AddIfcFile, operator.AddIfcFile,
operator.RemoveIfcFile, operator.RemoveIfcFile,
operator.SelectDocIfcFile,
operator.AddAnnotation,
operator.GenerateReferences,
operator.ResizeText,
operator.AddVariable,
operator.RemoveVariable,
operator.PropagateTextData,
operator.SetOverrideColour, 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.SetViewportShadowFromSun,
operator.AddDrawingStyleAttribute,
operator.RemoveDrawingStyleAttribute,
operator.CopyPropertyToSelection, operator.CopyPropertyToSelection,
operator.CopyAttributeToSelection, operator.CopyAttributeToSelection,
operator.RefreshDrawingList,
operator.CleanWireframes,
operator.LinkIfc, operator.LinkIfc,
operator.SnapSpacesTogether, operator.SnapSpacesTogether,
operator.CopyGrid,
operator.AddSectionsAnnotations,
prop.StrProperty, prop.StrProperty,
prop.Attribute, prop.Attribute,
prop.Variable,
prop.Drawing,
prop.Schedule,
prop.DrawingStyle,
prop.Sheet,
prop.BIMProperties, prop.BIMProperties,
prop.DocProperties,
prop.IfcParameter, prop.IfcParameter,
prop.PsetQto, prop.PsetQto,
prop.GlobalId, prop.GlobalId,
prop.RepresentationItem,
prop.BIMObjectProperties, prop.BIMObjectProperties,
prop.BIMMaterialProperties, prop.BIMMaterialProperties,
prop.SweptSolid,
prop.ItemSlotMap, prop.ItemSlotMap,
prop.BIMMeshProperties, prop.BIMMeshProperties,
prop.BIMCameraProperties,
prop.BIMTextProperties,
ui.BIM_PT_section_plane, 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_generic,
ui.BIM_UL_drawinglist,
ui.BIM_UL_topics, ui.BIM_UL_topics,
ui.BIM_ADDON_preferences, ui.BIM_ADDON_preferences,
gizmos.UglyDotGizmo,
gizmos.DotGizmo,
gizmos.DimensionLabelGizmo,
gizmos.ExtrusionGuidesGizmo,
gizmos.ExtrusionWidget
] ]
for module in modules.values(): 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_export.append(menu_func_export)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) 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.Object.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties)
bpy.types.Material.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.Material.BIMMaterialProperties = bpy.props.PointerProperty(type=prop.BIMMaterialProperties)
bpy.types.Mesh.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Mesh.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.Curve.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.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties) bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.types.SCENE_PT_unit.append(ui.ifc_units) bpy.types.SCENE_PT_unit.append(ui.ifc_units)
for module in modules.values(): for module in modules.values():
module.register() module.register()
bpy.app.handlers.depsgraph_update_pre.append(operator.depsgraph_update_pre_handler)
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
def unregister(): def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
@@ -204,7 +140,6 @@ if bpy is not None:
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
del bpy.types.Scene.BIMProperties del bpy.types.Scene.BIMProperties
del bpy.types.Scene.DocProperties
del bpy.types.Object.BIMObjectProperties del bpy.types.Object.BIMObjectProperties
del bpy.types.Material.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
@@ -212,12 +147,8 @@ if bpy is not None:
del bpy.types.Mesh.BIMMeshProperties del bpy.types.Mesh.BIMMeshProperties
del bpy.types.Curve.BIMMeshProperties del bpy.types.Curve.BIMMeshProperties
del bpy.types.Camera.BIMMeshProperties del bpy.types.Camera.BIMMeshProperties
del bpy.types.Camera.BIMCameraProperties del bpy.types.PointLight.BIMMeshProperties
del bpy.types.TextCurve.BIMTextProperties
bpy.types.SCENE_PT_unit.remove(ui.ifc_units) bpy.types.SCENE_PT_unit.remove(ui.ifc_units)
for module in reversed(list(modules.values())): for module in reversed(list(modules.values())):
module.unregister() module.unregister()
bpy.app.handlers.depsgraph_update_pre.remove(operator.depsgraph_update_pre_handler)
bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad)
-552
View File
@@ -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)
@@ -3,6 +3,19 @@
<div style="position:relative" class="gantt" id="GanttChartDIV"></div> <div style="position:relative" class="gantt" id="GanttChartDIV"></div>
<script type="text/javascript"> <script type="text/javascript">
var g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day'); var g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day');
g.setOptions({
vCaptionType: 'Complete', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
vQuarterColWidth: 36,
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
vWeekMinorDateDisplayFormat: 'dd mon', // Set format to display dates in the "Minor" header of the "Week" view
vLang: 'en',
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
vTotalHeight: 1000,
});
var json_data = ` var json_data = `
{{{json_data}}} {{{json_data}}}
`; `;
@@ -1,6 +1,6 @@
* { stroke-linecap: round; } * { stroke-linecap: round; stroke-linejoin: round; }
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; } *[id] { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; }
.background { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; } .annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.hidden { stroke-dasharray: 3, 2; } .hidden { stroke-dasharray: 3, 2; }
.solid {} .solid {}
+11 -17
View File
@@ -49,7 +49,6 @@ class IfcExporter:
json.dump(jsonData, outfile, indent=None if self.ifc_export_settings.json_compact else 4) json.dump(jsonData, outfile, indent=None if self.ifc_export_settings.json_compact else 4)
def set_header(self): def set_header(self):
# TODO: add all metadata, pending bug #747
self.file.wrapped_data.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file) self.file.wrapped_data.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
self.file.wrapped_data.header.file_name.time_stamp = ( self.file.wrapped_data.header.file_name.time_stamp = (
datetime.datetime.utcnow() datetime.datetime.utcnow()
@@ -62,16 +61,6 @@ class IfcExporter:
self.file.wrapped_data.header.file_name.originating_system = "{} {}".format( self.file.wrapped_data.header.file_name.originating_system = "{} {}".format(
self.get_application_name(), self.get_application_version() self.get_application_name(), self.get_application_version()
) )
# TODO: reimplement. See #1222.
# if self.owner_history:
# if self.schema_version == "IFC2X3":
# self.file.wrapped_data.header.file_name.authorization = self.owner_history.OwningUser.ThePerson.Id
# else:
# self.file.wrapped_data.header.file_name.authorization = (
# self.owner_history.OwningUser.ThePerson.Identification
# )
# else:
# self.file.wrapped_data.header.file_name.authorization = "Nobody"
def sync_object_placements_and_deletions(self): def sync_object_placements_and_deletions(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -94,13 +83,13 @@ class IfcExporter:
ifcopenshell.api.run("root.remove_product", self.file, **{"product": product}) ifcopenshell.api.run("root.remove_product", self.file, **{"product": product})
def sync_edited_objects(self): def sync_edited_objects(self):
for obj_name in IfcStore.edited_objs.copy(): for obj in IfcStore.edited_objs.copy():
obj = bpy.data.objects.get(obj_name)
if not obj: if not obj:
continue continue
representation = IfcStore.get_file().by_id(obj.data.BIMMeshProperties.ifc_definition_id) try:
if representation.RepresentationType == "Tessellation" or representation.RepresentationType == "Brep": bpy.ops.bim.update_representation(obj=obj.name)
bpy.ops.bim.update_mesh_representation(obj=obj.name) except ReferenceError:
pass # The object is likely deleted
IfcStore.edited_objs.clear() IfcStore.edited_objs.clear()
def sync_object_placement(self, obj): def sync_object_placement(self, obj):
@@ -136,7 +125,11 @@ class IfcExporter:
elif element.is_a("IfcContext"): elif element.is_a("IfcContext"):
return return
if (element.is_a("IfcElement") and element_collection) or element.is_a("IfcSpatialStructureElement"): if (
(element.is_a("IfcElement") and element_collection)
or element.is_a("IfcSpatialStructureElement")
or element.is_a("IfcGrid")
):
try: try:
parent_collection = [c for c in bpy.data.collections if c.children.get(element_collection.name)][0] parent_collection = [c for c in bpy.data.collections if c.children.get(element_collection.name)][0]
except: except:
@@ -144,6 +137,7 @@ class IfcExporter:
else: else:
parent_collection = obj.users_collection[0] parent_collection = obj.users_collection[0]
parent_obj = bpy.data.objects.get(parent_collection.name) parent_obj = bpy.data.objects.get(parent_collection.name)
if not parent_obj or not parent_obj.BIMObjectProperties.ifc_definition_id: if not parent_obj or not parent_obj.BIMObjectProperties.ifc_definition_id:
return return
+38 -15
View File
@@ -1,7 +1,6 @@
import bpy import bpy
import json import json
import addon_utils import addon_utils
import blenderbim.bim.decoration as decoration
import ifcopenshell.api.owner.settings import ifcopenshell.api.owner.settings
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
@@ -9,19 +8,25 @@ from ifcopenshell.api.attribute.data import Data as AttributeData
from ifcopenshell.api.type.data import Data as TypeData from ifcopenshell.api.type.data import Data as TypeData
global_subscription_owner = object()
def mode_callback(obj, data): def mode_callback(obj, data):
for obj in bpy.context.selected_objects: for obj in bpy.context.selected_objects + [bpy.context.active_object]:
if ( if (
obj.mode != "EDIT" obj.mode != "EDIT"
or not obj.data or not obj.data
or not isinstance(obj.data, bpy.types.Mesh) or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.data.BIMMeshProperties.ifc_definition_id or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring or not bpy.context.scene.BIMProjectProperties.is_authoring
): ):
return return
if obj.data.BIMMeshProperties.ifc_definition_id:
representation = IfcStore.get_file().by_id(obj.data.BIMMeshProperties.ifc_definition_id) representation = IfcStore.get_file().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if representation.RepresentationType == "Tessellation" or representation.RepresentationType == "Brep": if representation.RepresentationType in ["Tessellation", "Brep", "Annotation2D"]:
IfcStore.edited_objs.add(obj.name) IfcStore.edited_objs.add(obj)
elif IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id).is_a("IfcGridAxis"):
IfcStore.edited_objs.add(obj)
def name_callback(obj, data): def name_callback(obj, data):
@@ -34,12 +39,32 @@ def name_callback(obj, data):
if element.is_a("IfcSpatialStructureElement") or (hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy): if element.is_a("IfcSpatialStructureElement") or (hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy):
collection = obj.users_collection[0] collection = obj.users_collection[0]
collection.name = obj.name collection.name = obj.name
if element.is_a("IfcGrid"):
axis_obj = IfcStore.id_map[element.UAxes[0].id()]
axis_collection = axis_obj.users_collection[0]
grid_collection = None
for collection in bpy.data.collections:
if axis_collection.name in collection.children.keys():
grid_collection = collection
break
if grid_collection:
grid_collection.name = obj.name
if element.is_a("IfcTypeProduct"): if element.is_a("IfcTypeProduct"):
TypeData.purge() TypeData.purge()
element.Name = "/".join(obj.name.split("/")[1:]) element.Name = "/".join(obj.name.split("/")[1:])
AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
def active_object_callback():
obj = bpy.context.active_object
for obj in bpy.context.selected_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
stored_obj = IfcStore.get_element(obj.BIMObjectProperties.ifc_definition_id)
if stored_obj and stored_obj != obj:
bpy.ops.bim.copy_class(obj=obj.name)
def subscribe_to(object, data_path, callback): def subscribe_to(object, data_path, callback):
subscribe_to = object.path_resolve(data_path, False) subscribe_to = object.path_resolve(data_path, False)
bpy.msgbus.subscribe_rna( bpy.msgbus.subscribe_rna(
@@ -75,6 +100,8 @@ def purge_module_data():
def loadIfcStore(scene): def loadIfcStore(scene):
IfcStore.purge() IfcStore.purge()
ifc_file = IfcStore.get_file() ifc_file = IfcStore.get_file()
if not ifc_file:
return
IfcStore.get_schema() IfcStore.get_schema()
[ [
IfcStore.link_element(ifc_file.by_id(o.BIMObjectProperties.ifc_definition_id), o) IfcStore.link_element(ifc_file.by_id(o.BIMObjectProperties.ifc_definition_id), o)
@@ -161,6 +188,11 @@ def create_application_organisation(ifc):
@persistent @persistent
def setDefaultProperties(scene): def setDefaultProperties(scene):
global global_subscription_owner
active_object_key = bpy.types.LayerObjects, "active"
bpy.msgbus.subscribe_rna(
key=active_object_key, owner=global_subscription_owner, args=(), notify=active_object_callback
)
ifcopenshell.api.owner.settings.get_person = ( ifcopenshell.api.owner.settings.get_person = (
lambda ifc: ifc.by_id(int(bpy.context.scene.BIMOwnerProperties.user_person)) lambda ifc: ifc.by_id(int(bpy.context.scene.BIMOwnerProperties.user_person))
if bpy.context.scene.BIMOwnerProperties.user_person if bpy.context.scene.BIMOwnerProperties.user_person
@@ -239,12 +271,3 @@ def setDefaultProperties(scene):
drawing_style.name = "Blender Default" drawing_style.name = "Blender Default"
drawing_style.render_type = "DEFAULT" drawing_style.render_type = "DEFAULT"
bpy.ops.bim.save_drawing_style(index="2") bpy.ops.bim.save_drawing_style(index="2")
@persistent
def toggleDecorationsOnLoad(*args):
toggle = bpy.context.scene.DocProperties.should_draw_decorations
if toggle:
decoration.DecorationsHandler.install(bpy.context)
else:
decoration.DecorationsHandler.uninstall()
+75 -402
View File
@@ -1,411 +1,84 @@
import bpy
import json
import math import math
import ifcopenshell
import ifcopenshell.util.attribute
from mathutils import geometry from mathutils import geometry
from mathutils import Vector from mathutils import Vector
import bpy from blenderbim.bim.ifc import IfcStore
# TODO: Deprecate this in favour of ifcopenshell.util.unit def draw_attributes(props, layout, copy_operator=None):
for attribute in props:
row = layout.row(align=True)
value = None
if attribute.data_type == "string":
row.prop(attribute, "string_value", text=attribute.name)
value = attribute.string_value
elif attribute.data_type == "boolean":
row.prop(attribute, "bool_value", text=attribute.name)
value = attribute.bool_value
elif attribute.data_type == "integer":
row.prop(attribute, "int_value", text=attribute.name)
value = attribute.int_value
elif attribute.data_type == "float":
row.prop(attribute, "float_value", text=attribute.name)
value = attribute.float_value
elif attribute.data_type == "enum":
row.prop(attribute, "enum_value", text=attribute.name)
value = attribute.enum_value
if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if copy_operator:
op = row.operator(f"{copy_operator}", text="", icon="COPYDOWN")
op.data = json.dumps({"name": attribute.name, "value": value, "is_null": attribute.is_null})
class SIUnitHelper: def import_attributes(ifc_class, props, data, callback=None):
prefixes = { for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
"EXA": 1e18, data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
"PETA": 1e15, if data_type == "entity" or (isinstance(data_type, tuple) and "entity" in ".".join(data_type)):
"TERA": 1e12, continue
"GIGA": 1e9, new = props.add()
"MEGA": 1e6, new.name = attribute.name()
"KILO": 1e3, new.is_null = data[attribute.name()] is None
"HECTO": 1e2, new.is_optional = attribute.optional()
"DECA": 1e1, new.data_type = data_type if isinstance(data_type, str) else ""
"DECI": 1e-1, is_handled_by_callback = callback(attribute.name(), new, data) if callback else None
"CENTI": 1e-2, if is_handled_by_callback:
"MILLI": 1e-3, pass # Our job is done
"MICRO": 1e-6, elif is_handled_by_callback is False:
"NANO": 1e-9, props.remove(len(props) - 1)
"PICO": 1e-12, elif data_type == "string":
"FEMTO": 1e-15, new.string_value = "" if new.is_null else data[attribute.name()]
"ATTO": 1e-18, elif data_type == "boolean":
} new.bool_value = False if new.is_null else data[attribute.name()]
unit_names = [ elif data_type == "integer":
"AMPERE", new.int_value = 0 if new.is_null else data[attribute.name()]
"BECQUEREL", elif data_type == "float":
"CANDELA", new.float_value = 0.0 if new.is_null else data[attribute.name()]
"COULOMB", elif data_type == "enum":
"CUBIC_METRE", new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
"DEGREE CELSIUS", if data[attribute.name()]:
"FARAD", new.enum_value = data[attribute.name()]
"GRAM",
"GRAY",
"HENRY",
"HERTZ",
"JOULE",
"KELVIN",
"LUMEN",
"LUX",
"MOLE",
"NEWTON",
"OHM",
"PASCAL",
"RADIAN",
"SECOND",
"SIEMENS",
"SIEVERT",
"SQUARE METRE",
"METRE",
"STERADIAN",
"TESLA",
"VOLT",
"WATT",
"WEBER",
]
si_conversions = {
"inch": 0.0254,
"foot": 0.3048,
"yard": 0.914,
"mile": 1609,
"square inch": 0.0006452,
"square foot": 0.09290304,
"square yard": 0.83612736,
"acre": 4046.86,
"square mile": 2588881,
"cubic inch": 0.00001639,
"cubic foot": 0.02831684671168849,
"cubic yard": 0.7636,
"litre": 0.001,
"fluid ounce UK": 0.0000284130625,
"fluid ounce US": 0.00002957353,
"pint UK": 0.000568,
"pint US": 0.000473,
"gallon UK": 0.004546,
"gallon US": 0.003785,
"degree": math.pi / 180,
"ounce": 0.02835,
"pound": 0.454,
"ton UK": 1016.0469088,
"ton US": 907.18474,
"lbf": 4.4482216153,
"kip": 4448.2216153,
"psi": 6894.7572932,
"ksi": 6894757.2932,
"minute": 60,
"hour": 3600,
"day": 86400,
"btu": 1055.056,
}
@staticmethod
def get_prefix(text):
for prefix in SIUnitHelper.prefixes.keys():
if prefix in text.upper():
return prefix
@staticmethod
def get_prefix_multiplier(text):
if not text:
return 1
prefix = SIUnitHelper.get_prefix(text)
if prefix:
return SIUnitHelper.prefixes[prefix]
return 1
@staticmethod
def get_unit_name(text):
for name in SIUnitHelper.unit_names:
if name in text.upper().replace("METER", "METRE"):
return name
@staticmethod
def convert(value, from_prefix, from_unit, to_prefix, to_unit):
"""Converts between length, area, and volume units
:param value: The numeric value you want to convert
:type value: float
:param from_prefix: A prefix from IfcSIPrefix. Can be None.
:type from_prefix: string
:param from_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
:type from_unit: string
:param to_prefix: A prefix from IfcSIPrefix. Can be None.
:type to_prefix: string
:param to_unit: IfcSIUnitName or IfcConversionBasedUnit.Name
:type to_unit: string
"""
if from_unit in SIUnitHelper.si_conversions:
value *= SIUnitHelper.si_conversions[from_unit]
elif from_prefix:
value *= SIUnitHelper.get_prefix_multiplier(from_prefix)
if "SQUARE" in from_unit:
value *= SIUnitHelper.get_prefix_multiplier(from_prefix)
elif "CUBIC" in from_unit:
value *= SIUnitHelper.get_prefix_multiplier(from_prefix)
value *= SIUnitHelper.get_prefix_multiplier(from_prefix)
if to_unit in SIUnitHelper.si_conversions:
return value * (1 / SIUnitHelper.si_conversions[to_unit])
elif to_prefix:
value *= 1 / SIUnitHelper.get_prefix_multiplier(to_prefix)
if "SQUARE" in from_unit:
value *= 1 / SIUnitHelper.get_prefix_multiplier(to_prefix)
elif "CUBIC" in from_unit:
value *= 1 / SIUnitHelper.get_prefix_multiplier(to_prefix)
value *= 1 / SIUnitHelper.get_prefix_multiplier(to_prefix)
return value
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py def export_attributes(props, callback=None):
# MeasureIt-ARCH is GPL-v3 attributes = {}
# In the future I will need to rewrite this to allow the user to have custom for attribute in props:
# settings for each annotation object, not read from Blender. is_handled_by_callback = callback(attributes, attribute) if callback else False
def format_distance(value, isArea=False, hide_units=True): if attribute.is_null:
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented) attributes[attribute.name] = None
elif is_handled_by_callback:
# Get Scene Unit Settings pass # Our job is done
scaleFactor = bpy.context.scene.unit_settings.scale_length elif attribute.data_type == "string":
unit_system = bpy.context.scene.unit_settings.system attributes[attribute.name] = attribute.string_value
unit_length = bpy.context.scene.unit_settings.length_unit elif attribute.data_type == "boolean":
attributes[attribute.name] = attribute.bool_value
toInches = 39.3700787401574887 elif attribute.data_type == "integer":
inPerFoot = 11.999 attributes[attribute.name] = attribute.int_value
elif attribute.data_type == "float":
if isArea: attributes[attribute.name] = attribute.float_value
toInches = 1550 elif attribute.data_type == "enum":
inPerFoot = 143.999 attributes[attribute.name] = attribute.enum_value
return attributes
value *= scaleFactor
# Imperial Formating
if unit_system == "IMPERIAL":
precision = bpy.context.scene.BIMProperties.imperial_precision
if precision == "NONE":
precision = 256
elif precision == "1":
precision = 1
elif "/" in precision:
precision = int(precision.split("/")[1])
base = int(precision)
decInches = value * toInches
# Seperate ft and inches
# Unless Inches are the specified Length Unit
if unit_length != "INCHES":
feet = math.floor(decInches / inPerFoot)
decInches -= feet * inPerFoot
else:
feet = 0
# Seperate Fractional Inches
inches = math.floor(decInches)
if inches != 0:
frac = round(base * (decInches - inches))
else:
frac = round(base * (decInches))
# Set proper numerator and denominator
if frac != base:
numcycles = int(math.log2(base))
for i in range(numcycles):
if frac % 2 == 0:
frac = int(frac / 2)
base = int(base / 2)
else:
break
else:
frac = 0
inches += 1
# Check values and compose string
if inches == 12:
feet += 1
inches = 0
if not isArea:
tx_dist = ""
if feet:
tx_dist += str(feet) + "'"
if feet and inches:
tx_dist += " - "
if inches:
tx_dist += str(inches)
if inches and frac:
tx_dist += " "
if frac:
tx_dist += str(frac) + "/" + str(base)
if inches or frac:
tx_dist += '"'
else:
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
# METRIC FORMATING
elif unit_system == "METRIC":
precision = bpy.context.scene.BIMProperties.metric_precision
if precision != 0:
value = precision * round(float(value) / precision)
# Meters
if unit_length == "METERS":
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
# Centimeters
elif unit_length == "CENTIMETERS":
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
# Millimeters
elif unit_length == "MILLIMETERS":
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
d_mm = value * (1000)
tx_dist = fmt % d_mm
# Otherwise Use Adaptive Units
else:
if round(value, 2) >= 1.0:
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
else:
if round(value, 2) >= 0.01:
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
else:
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
d_mm = value * (1000)
tx_dist = fmt % d_mm
if isArea:
tx_dist += s_code
else:
tx_dist = fmt % value
return tx_dist
def parse_diagram_scale(camera):
"""Returns numeric value of scale"""
if camera.BIMCameraProperties.diagram_scale == "CUSTOM":
_, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|")
else:
_, fraction = camera.BIMCameraProperties.diagram_scale.split("|")
numerator, denominator = fraction.split("/")
return float(numerator) / float(denominator)
def get_project_collection(scene):
"""Get main project collection"""
colls = [c for c in scene.collection.children if c.name.startswith('IfcProject')]
if len(colls) != 1:
raise RuntimeError("project collection missing or not unique")
return colls[0]
def get_active_drawing(scene):
"""Get active drawing collection and camera"""
props = scene.DocProperties
if props.active_drawing_index is None or len(props.drawings) == 0:
return None, None
try:
drawing = props.drawings[props.active_drawing_index]
return scene.collection.children['Views'].children[f"IfcGroup/{drawing.name}"], drawing.camera
except (KeyError, IndexError):
raise RuntimeError("missing drawing collection")
def ortho_view_frame(camera, margin=0.015):
"""Calculates 2d bounding box of camera view area.
Similar to `bpy.types.Camera.view_frame`
:arg camera: camera of drawing
:type camera: bpy.types.Camera + BIMCameraProperties
:arg margin: margins, in scene units
:type margin: float
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x
size = camera.ortho_scale
hwidth = size * .5
hheight = size * .5 * aspect
scale = parse_diagram_scale(camera)
xmarg = margin * scale
ymarg = margin * scale * aspect
return (-hwidth + xmarg, hwidth - xmarg, -hheight + ymarg, hheight - ymarg, -camera.clip_start, -camera.clip_end)
def almost_zero(v):
return abs(v) < 1e-5
def clip_segment(bounds, segm):
"""Clipping line segment to bounds
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
# LiangBarsky algorithm
xmin, xmax, ymin, ymax, _, _ = bounds
p1, p2 = segm
def clip_side(p, q):
if almost_zero(p): # ~= 0, parallel to the side
if q < 0:
return None # outside
else:
return 0, 1 # inside
t = q / p # the intersection point
if p < 0: # entering
return t, 1
else: # leaving
return 0, t
dlt = p2 - p1
tt = (
clip_side(-dlt.x, p1.x - xmin), # left
clip_side(+dlt.x, xmax - p1.x), # right
clip_side(-dlt.y, p1.y - ymin), # bottom
clip_side(+dlt.y, ymax - p1.y), # top
)
if None in tt:
return None
t1 = max(0, max(t[0] for t in tt))
t2 = min(1, min(t[1] for t in tt))
if t1 >= t2:
return None
p1c = p1 + dlt * t1
p2c = p1 + dlt * t2
return p1c, p2c
def elevate_segment(bounds, segm):
"""Elevate line xy-perpendicular segment vertically
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
_, _, ymin, ymax, zmin, _ = bounds
p1, p2 = segm
dlt = p2 - p1
if not (almost_zero(dlt.x) and almost_zero(dlt.y)):
return None
x = p1.x
return [Vector((x, ymin, zmin)), Vector((x, ymax, zmin))]
+39 -2
View File
@@ -14,6 +14,7 @@ class IfcStore:
pset_template_file = None pset_template_file = None
library_path = "" library_path = ""
library_file = None library_file = None
element_listeners = set()
@staticmethod @staticmethod
def purge(): def purge():
@@ -47,6 +48,23 @@ class IfcStore:
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema) IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema)
return IfcStore.schema return IfcStore.schema
@staticmethod
def get_element(id_or_guid):
if isinstance(id_or_guid, int):
map_object = IfcStore.id_map
else:
map_object = IfcStore.guid_map
try:
obj = map_object[id_or_guid]
obj.type # In case the object has been deleted, this triggers an exception
except:
return
return obj
@staticmethod
def add_element_listener(callback):
IfcStore.element_listeners.add(callback)
@staticmethod @staticmethod
def link_element(element, obj): def link_element(element, obj):
IfcStore.id_map[element.id()] = obj IfcStore.id_map[element.id()] = obj
@@ -55,11 +73,30 @@ class IfcStore:
obj.BIMObjectProperties.ifc_definition_id = element.id() obj.BIMObjectProperties.ifc_definition_id = element.id()
blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback)
blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback) blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback)
for listener in IfcStore.element_listeners:
listener(element, obj)
@staticmethod @staticmethod
def unlink_element(element, obj=None): def unlink_element(element=None, obj=None):
if element is None:
try:
element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
except:
pass
try:
if element:
del IfcStore.id_map[element.id()] del IfcStore.id_map[element.id()]
if hasattr(element, "GlobalId"): else:
del IfcStore.id_map[obj.BIMObjectProperties.ifc_definition_id]
except:
pass
try:
if element and hasattr(element, "GlobalId"):
del IfcStore.guid_map[element.GlobalId] del IfcStore.guid_map[element.GlobalId]
except:
pass
if obj: if obj:
obj.BIMObjectProperties.ifc_definition_id = 0 obj.BIMObjectProperties.ifc_definition_id = 0
+71 -5
View File
@@ -18,11 +18,11 @@ import multiprocessing
import zipfile import zipfile
import tempfile import tempfile
import numpy as np import numpy as np
from blenderbim.bim.module.drawing.prop import getDiagramScales
from pathlib import Path from pathlib import Path
from itertools import cycle from itertools import cycle
from datetime import datetime from datetime import datetime
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from . import schema
class FileCopy(threading.Thread): class FileCopy(threading.Thread):
@@ -346,6 +346,8 @@ class IfcImporter:
self.profile_code("Create native products") self.profile_code("Create native products")
self.create_products() self.create_products()
self.profile_code("Create products") self.profile_code("Create products")
self.create_empty_products()
self.profile_code("Create empty products")
self.create_type_products() self.create_type_products()
self.profile_code("Create type products") self.profile_code("Create type products")
self.create_annotation() self.create_annotation()
@@ -442,9 +444,9 @@ class IfcImporter:
def is_native_swept_disk_solid(self, representations): def is_native_swept_disk_solid(self, representations):
for representation in representations: for representation in representations:
if len(representation["raw"].Items) > 1 or not representation["raw"].Items[0].is_a("IfcSweptDiskSolid"): if len(representation["raw"].Items) == 1 and representation["raw"].Items[0].is_a("IfcSweptDiskSolid"):
return False
return True return True
return False
def is_native_faceted_brep(self, representations): def is_native_faceted_brep(self, representations):
for representation in representations: for representation in representations:
@@ -702,15 +704,26 @@ class IfcImporter:
checkpoint = time.time() checkpoint = time.time()
shape = iterator.get() shape = iterator.get()
if shape: if shape:
product = self.file.by_id(shape.guid)
if shape.context != "Body" and shape.guid in IfcStore.guid_map: if shape.context != "Body" and shape.guid in IfcStore.guid_map:
# We only load a single context, and we prioritise the Body context. See #1290. # We only load a single context, and we prioritise the Body context. See #1290.
pass pass
elif product.is_a("IfcAnnotation") and product.ObjectType == "DRAWING":
# We have already processed this during the create_annotation step
pass
else: else:
self.create_product(self.file.by_id(shape.guid), shape) self.create_product(product, shape)
if not iterator.next(): if not iterator.next():
break break
print("Done creating geometry") print("Done creating geometry")
def create_empty_products(self):
for element in self.file.by_type("IfcProduct"):
if element.GlobalId in self.added_data:
continue
if not element.Representation:
self.create_product(element)
def create_annotation(self): def create_annotation(self):
self.create_curve_products(self.file.by_type("IfcAnnotation")) self.create_curve_products(self.file.by_type("IfcAnnotation"))
@@ -718,7 +731,7 @@ class IfcImporter:
# Create structural collections # Create structural collections
self.structural_member_collection = bpy.data.collections.new("Members") self.structural_member_collection = bpy.data.collections.new("Members")
self.structural_connection_collection = bpy.data.collections.new("Connections") self.structural_connection_collection = bpy.data.collections.new("Connections")
self.structural_collection = bpy.data.collections.new("StructuralEntities") self.structural_collection = bpy.data.collections.new("StructuralItems")
self.structural_collection.children.link(self.structural_member_collection) self.structural_collection.children.link(self.structural_member_collection)
self.structural_collection.children.link(self.structural_connection_collection) self.structural_collection.children.link(self.structural_connection_collection)
self.project["blender"].children.link(self.structural_collection) self.project["blender"].children.link(self.structural_collection)
@@ -784,6 +797,8 @@ class IfcImporter:
if mesh: if mesh:
pass pass
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
mesh = self.create_camera(element, shape)
elif shape: elif shape:
mesh_name = self.get_mesh_name(shape.geometry) mesh_name = self.get_mesh_name(shape.geometry)
mesh = self.meshes.get(mesh_name) mesh = self.meshes.get(mesh_name)
@@ -1206,6 +1221,15 @@ class IfcImporter:
self.structural_member_collection.objects.link(obj) self.structural_member_collection.objects.link(obj)
elif element.is_a("IfcStructuralConnection"): elif element.is_a("IfcStructuralConnection"):
self.structural_connection_collection.objects.link(obj) self.structural_connection_collection.objects.link(obj)
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
view_collection = bpy.data.collections.get("Views")
if not view_collection:
view_collection = bpy.data.collections.new("Views")
bpy.context.scene.collection.children.link(view_collection)
group = [r for r in element.HasAssignments if r.is_a("IfcRelAssignsToGroup")][0].RelatingGroup
drawing_collection = bpy.data.collections.new("IfcGroup/" + group.Name)
view_collection.children.link(drawing_collection)
drawing_collection.objects.link(obj)
else: else:
self.ifc_import_settings.logger.warning("Warning: this object is outside the spatial hierarchy %s", element) self.ifc_import_settings.logger.warning("Warning: this object is outside the spatial hierarchy %s", element)
bpy.context.scene.collection.objects.link(obj) bpy.context.scene.collection.objects.link(obj)
@@ -1294,6 +1318,48 @@ class IfcImporter:
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0 context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
return "{}/{}".format(context_id, representation_id) return "{}/{}".format(context_id, representation_id)
def create_camera(self, element, shape):
if hasattr(shape, "geometry"):
geometry = shape.geometry
else:
geometry = shape
v = geometry.verts
x = [v[i] for i in range(0, len(v), 3)]
y = [v[i + 1] for i in range(0, len(v), 3)]
z = [v[i + 2] for i in range(0, len(v), 3)]
width = max(x) - min(x)
height = max(y) - min(y)
depth = max(z) - min(z)
camera = bpy.data.cameras.new(self.get_mesh_name(geometry))
camera.type = "ORTHO"
camera.ortho_scale = width if width > height else height
camera.clip_end = depth
if width > height:
camera.BIMCameraProperties.raster_x = 1000
camera.BIMCameraProperties.raster_y = round(1000 * (height / width))
else:
camera.BIMCameraProperties.raster_x = round(1000 * (width / height))
camera.BIMCameraProperties.raster_y = 1000
psets = ifcopenshell.util.element.get_psets(element)
pset = psets.get("EPset_Drawing")
if pset:
if "TargetView" in pset:
camera.BIMCameraProperties.target_view = pset["TargetView"]
if "Scale" in pset:
valid_scales = [
i[0] for i in getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1]
]
if valid_scales:
camera.BIMCameraProperties.diagram_scale = valid_scales[0]
else:
camera.BIMCameraProperties.diagram_scale = "CUSTOM"
camera.BIMCameraProperties.custom_diagram_scale = pset["Scale"]
return camera
def create_mesh(self, element, shape): def create_mesh(self, element, shape):
try: try:
if hasattr(shape, "geometry"): if hasattr(shape, "geometry"):
@@ -45,6 +45,7 @@ class AssignObject(bpy.types.Operator):
self.remove_collection(collection, spatial_collection) self.remove_collection(collection, spatial_collection)
else: else:
for collection in related_object.users_collection: for collection in related_object.users_collection:
if collection.name.startswith("Ifc"):
collection.objects.unlink(related_object) collection.objects.unlink(related_object)
relating_collection.objects.link(related_object) relating_collection.objects.link(related_object)
return {"FINISHED"} return {"FINISHED"}
@@ -16,6 +16,8 @@ class BIM_PT_aggregate(Panel):
props = context.active_object.BIMObjectProperties props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id: if not props.ifc_definition_id:
return False return False
if not IfcStore.get_element(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False return False
if props.ifc_definition_id not in Data.products: if props.ifc_definition_id not in Data.products:
@@ -24,7 +24,7 @@ class EnableEditingAttributes(bpy.types.Operator):
props.attributes.remove(0) props.attributes.remove(0)
for attribute in Data.products[oprops.ifc_definition_id]: for attribute in Data.products[oprops.ifc_definition_id]:
new = props.attributes.add() new = props.attributes.add()
if attribute["type"] == "entity": if attribute["type"] == "entity" or (attribute["type"] == "list" and attribute["list_type"] == "entity"):
continue continue
new.name = attribute["name"] new.name = attribute["name"]
new.is_null = attribute["is_null"] new.is_null = attribute["is_null"]
@@ -1,5 +1,4 @@
import bpy import bpy
import blenderbim.bim.schema # refactor
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty, Attribute from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
@@ -15,7 +15,7 @@ def draw_ui(context, layout, obj_type):
op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes") op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
op.obj_type = obj_type op.obj_type = obj_type
op.obj = obj.name op.obj = obj.name
op = row.operator("bim.disable_editing_attributes", icon="X", text="") op = row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
op.obj_type = obj_type op.obj_type = obj_type
op.obj = obj.name op.obj = obj.name
@@ -78,6 +78,8 @@ class BIM_PT_object_attributes(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id) return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
def draw(self, context): def draw(self, context):
@@ -1,5 +1,5 @@
import bcf import bcf
import bcf.bcfxml import bcf.v2.bcfxml
class BcfStore: class BcfStore:
bcfxml = None bcfxml = None
@@ -7,5 +7,5 @@ class BcfStore:
@staticmethod @staticmethod
def get_bcfxml(): def get_bcfxml():
if not BcfStore.bcfxml: if not BcfStore.bcfxml:
BcfStore.bcfxml = bcf.bcfxml.BcfXml() BcfStore.bcfxml = bcf.v2.bcfxml.BcfXml()
return BcfStore.bcfxml return BcfStore.bcfxml
@@ -1,6 +1,8 @@
import os import os
import bpy import bpy
import bcf import bcf
import bcf.bcfxml
import bcf.v2.data
from . import bcfstore from . import bcfstore
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from math import radians, degrees, atan, tan, cos, sin from math import radians, degrees, atan, tan, cos, sin
@@ -26,9 +28,10 @@ class LoadBcfProject(bpy.types.Operator):
def execute(self, context): def execute(self, context):
bpy.context.scene.BCFProperties.is_loaded = False bpy.context.scene.BCFProperties.is_loaded = False
bcfxml = bcfstore.BcfStore.get_bcfxml()
if self.filepath: if self.filepath:
bcfxml.get_project(self.filepath) bcfstore.BcfStore.bcfxml = bcf.bcfxml.load(self.filepath)
bcfxml = bcfstore.BcfStore.get_bcfxml()
bcfxml.get_project()
bpy.context.scene.BCFProperties.name = bcfxml.project.name bpy.context.scene.BCFProperties.name = bcfxml.project.name
bpy.ops.bim.load_bcf_topics() bpy.ops.bim.load_bcf_topics()
bpy.context.scene.BCFProperties.is_loaded = True bpy.context.scene.BCFProperties.is_loaded = True
@@ -250,7 +253,7 @@ class AddBcfBimSnippet(bpy.types.Operator):
props = bpy.context.scene.BCFProperties props = bpy.context.scene.BCFProperties
blender_topic = props.topics[props.active_topic_index] blender_topic = props.topics[props.active_topic_index]
topic = bcfxml.topics[blender_topic.name] topic = bcfxml.topics[blender_topic.name]
bim_snippet = bcf.data.BimSnippet() bim_snippet = bcf.v2.data.BimSnippet()
bim_snippet.reference = blender_topic.bim_snippet_reference bim_snippet.reference = blender_topic.bim_snippet_reference
bim_snippet.reference_schema = blender_topic.bim_snippet_schema bim_snippet.reference_schema = blender_topic.bim_snippet_schema
bim_snippet.snippet_type = blender_topic.bim_snippet_type bim_snippet.snippet_type = blender_topic.bim_snippet_type
@@ -270,7 +273,7 @@ class AddBcfRelatedTopic(bpy.types.Operator):
related_topic = None related_topic = None
for topic in bcfxml.topics.values(): for topic in bcfxml.topics.values():
if topic.title == blender_topic.related_topic: if topic.title == blender_topic.related_topic:
related_topic = bcf.data.RelatedTopic() related_topic = bcf.v2.data.RelatedTopic()
related_topic.guid = topic.guid related_topic.guid = topic.guid
break break
if not related_topic: if not related_topic:
@@ -291,7 +294,7 @@ class AddBcfHeaderFile(bpy.types.Operator):
props = bpy.context.scene.BCFProperties props = bpy.context.scene.BCFProperties
blender_topic = props.topics[props.active_topic_index] blender_topic = props.topics[props.active_topic_index]
topic = bcfxml.topics[blender_topic.name] topic = bcfxml.topics[blender_topic.name]
header_file = bcf.data.HeaderFile() header_file = bcf.v2.data.HeaderFile()
header_file.reference = blender_topic.file_reference header_file.reference = blender_topic.file_reference
if not os.path.exists(header_file.reference): if not os.path.exists(header_file.reference):
header_file.filename = header_file.reference header_file.filename = header_file.reference
@@ -327,14 +330,14 @@ class AddBcfViewpoint(bpy.types.Operator):
props = bpy.context.scene.BCFProperties props = bpy.context.scene.BCFProperties
blender_topic = props.topics[props.active_topic_index] blender_topic = props.topics[props.active_topic_index]
topic = bcfxml.topics[blender_topic.name] topic = bcfxml.topics[blender_topic.name]
viewpoint = bcf.data.Viewpoint() viewpoint = bcf.v2.data.Viewpoint()
if bpy.context.scene.camera.data.type == "ORTHO": if bpy.context.scene.camera.data.type == "ORTHO":
camera = bcf.data.OrthogonalCamera() camera = bcf.v2.data.OrthogonalCamera()
camera.view_to_world_scale = bpy.context.scene.camera.data.ortho_scale camera.view_to_world_scale = bpy.context.scene.camera.data.ortho_scale
viewpoint.orthogonal_camera = camera viewpoint.orthogonal_camera = camera
elif bpy.context.scene.camera.data.type == "PERSP": elif bpy.context.scene.camera.data.type == "PERSP":
camera = bcf.data.PerspectiveCamera() camera = bcf.v2.data.PerspectiveCamera()
camera.field_of_view = degrees(bpy.context.scene.camera.data.angle) camera.field_of_view = degrees(bpy.context.scene.camera.data.angle)
viewpoint.perspective_camera = camera viewpoint.perspective_camera = camera
camera.camera_view_point.x = bpy.context.scene.camera.location.x camera.camera_view_point.x = bpy.context.scene.camera.location.x
@@ -422,7 +425,7 @@ class AddBcfDocumentReference(bpy.types.Operator):
topic = bcfxml.topics[blender_topic.name] topic = bcfxml.topics[blender_topic.name]
if not blender_topic.document_reference: if not blender_topic.document_reference:
return {"FINISHED"} return {"FINISHED"}
document_reference = bcf.data.DocumentReference() document_reference = bcf.v2.data.DocumentReference()
document_reference.referenced_document = blender_topic.document_reference document_reference.referenced_document = blender_topic.document_reference
document_reference.description = blender_topic.document_reference_description or None document_reference.description = blender_topic.document_reference_description or None
bcfxml.add_document_reference(topic, document_reference) bcfxml.add_document_reference(topic, document_reference)
@@ -609,10 +612,10 @@ class AddBcfComment(bpy.types.Operator):
topic = bcfxml.topics[blender_topic.name] topic = bcfxml.topics[blender_topic.name]
if not blender_topic.comment: if not blender_topic.comment:
return {"FINISHED"} return {"FINISHED"}
comment = bcf.data.Comment() comment = bcf.v2.data.Comment()
comment.comment = blender_topic.comment comment.comment = blender_topic.comment
if blender_topic.has_related_viewpoint and blender_topic.viewpoints: if blender_topic.has_related_viewpoint and blender_topic.viewpoints:
comment.viewpoint = bcf.data.Viewpoint() comment.viewpoint = bcf.v2.data.Viewpoint()
comment.viewpoint.guid = blender_topic.viewpoints comment.viewpoint.guid = blender_topic.viewpoints
bcfxml.add_comment(topic, comment) bcfxml.add_comment(topic, comment)
bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid)
@@ -0,0 +1,14 @@
import bpy
from . import ui
classes = (
ui.BIM_PT_boundary,
)
def register():
pass
def unregister():
pass
@@ -0,0 +1,36 @@
import bpy
import blenderbim.bim.helper
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.boundary.data import Data
class BIM_PT_boundary(Panel):
bl_label = "IFC Space Boundaries"
bl_idname = "BIM_PT_boundary"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
@classmethod
def poll(cls, context):
if not context.active_object:
return False
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if not IfcStore.get_element(props.ifc_definition_id):
return False
if IfcStore.get_file().by_id(props.ifc_definition_id).is_a() not in ["IfcSpace", "IfcExternalSpatialElement"]:
return False
return True
def draw(self, context):
self.oprops = context.active_object.BIMObjectProperties
if not Data.is_loaded:
Data.load(IfcStore.get_file())
for boundary_id in Data.spaces.get(self.oprops.ifc_definition_id, []):
boundary = Data.boundaries[boundary_id]
row = self.layout.row()
row.label(text=f"{boundary_id}", icon="GHOST_ENABLED")
@@ -93,6 +93,7 @@ class BIM_PT_ifcclash(Panel):
class BIM_PT_clash_manager(Panel): class BIM_PT_clash_manager(Panel):
bl_idname = "BIM_PT_clash_manager" bl_idname = "BIM_PT_clash_manager"
bl_label = "Clash Manager" bl_label = "Clash Manager"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "BlenderBIM" bl_category = "BlenderBIM"
@@ -70,6 +70,8 @@ class BIM_PT_classification_references(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id) return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
def draw(self, context): def draw(self, context):
@@ -64,6 +64,8 @@ class BIM_PT_object_constraints(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id) return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
def draw(self, context): def draw(self, context):
@@ -7,7 +7,7 @@ classes = (
operator.EditCostSchedule, operator.EditCostSchedule,
operator.EditCostItem, operator.EditCostItem,
operator.EditCostItemQuantity, operator.EditCostItemQuantity,
operator.EditCostItemValue, operator.EditCostValue,
operator.EnableEditingCostSchedule, operator.EnableEditingCostSchedule,
operator.EnableEditingCostItems, operator.EnableEditingCostItems,
operator.EnableEditingCostItem, operator.EnableEditingCostItem,
@@ -24,12 +24,15 @@ classes = (
operator.ExpandCostItem, operator.ExpandCostItem,
operator.ContractCostItem, operator.ContractCostItem,
operator.RemoveCostItem, operator.RemoveCostItem,
operator.AssignControl, operator.AssignCostItemProduct,
operator.UnassignControl, operator.UnassignCostItemProduct,
operator.AddCostItemQuantity, operator.AddCostItemQuantity,
operator.RemoveCostItemQuantity, operator.RemoveCostItemQuantity,
operator.AddCostItemValue, operator.AddCostValue,
operator.RemoveCostItemValue, operator.RemoveCostItemValue,
operator.CopyCostItemValues,
operator.SelectCostItemProducts,
operator.SelectCostScheduleProducts,
prop.CostItem, prop.CostItem,
prop.BIMCostProperties, prop.BIMCostProperties,
ui.BIM_PT_cost_schedules, ui.BIM_PT_cost_schedules,
@@ -2,6 +2,8 @@ import os
import bpy import bpy
import json import json
import ifcopenshell.api import ifcopenshell.api
import blenderbim.bim.helper
from blenderbim.bim.module.cost.prop import purge
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.cost.data import Data from ifcopenshell.api.cost.data import Data
@@ -22,15 +24,7 @@ class EditCostSchedule(bpy.types.Operator):
def execute(self, context): def execute(self, context):
props = context.scene.BIMCostProperties props = context.scene.BIMCostProperties
attributes = {} attributes = blenderbim.bim.helper.export_attributes(props.cost_schedule_attributes)
for attribute in props.cost_schedule_attributes:
if attribute.is_null:
attributes[attribute.name] = None
else:
if attribute.data_type == "string":
attributes[attribute.name] = attribute.string_value
elif attribute.data_type == "enum":
attributes[attribute.name] = attribute.enum_value
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run(
"cost.edit_cost_schedule", "cost.edit_cost_schedule",
@@ -73,24 +67,14 @@ class EnableEditingCostSchedule(bpy.types.Operator):
def enable_editing_cost_schedule(self): def enable_editing_cost_schedule(self):
data = Data.cost_schedules[self.cost_schedule] data = Data.cost_schedules[self.cost_schedule]
blenderbim.bim.helper.import_attributes(
"IfcCostSchedule", self.props.cost_schedule_attributes, data, self.import_attributes
)
for attribute in IfcStore.get_schema().declaration_by_name("IfcCostSchedule").all_attributes(): def import_attributes(self, name, prop, data):
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) if name in ["SubmittedOn", "UpdateDate"]:
if data_type == "entity": prop.string_value = "" if prop.is_null else data[name].isoformat()
continue return True
new = self.props.cost_schedule_attributes.add()
new.name = attribute.name()
new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = data_type
if attribute.name() in ["SubmittedOn", "UpdateDate"]:
new.string_value = "" if new.is_null else data[attribute.name()].isoformat()
elif data_type == "string":
new.string_value = "" if new.is_null else data[attribute.name()]
elif data_type == "enum":
new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
if data[attribute.name()]:
new.enum_value = data[attribute.name()]
class EnableEditingCostItems(bpy.types.Operator): class EnableEditingCostItems(bpy.types.Operator):
@@ -242,22 +226,7 @@ class EnableEditingCostItem(bpy.types.Operator):
props.cost_item_attributes.remove(0) props.cost_item_attributes.remove(0)
data = Data.cost_items[self.cost_item] data = Data.cost_items[self.cost_item]
blenderbim.bim.helper.import_attributes("IfcCostItem", props.cost_item_attributes, data)
for attribute in IfcStore.get_schema().declaration_by_name("IfcCostItem").all_attributes():
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
if data_type == "entity" or isinstance(data_type, tuple):
continue
new = props.cost_item_attributes.add()
new.name = attribute.name()
new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = data_type
if data_type == "string":
new.string_value = "" if new.is_null else data[attribute.name()]
elif data_type == "enum":
new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
if data[attribute.name()]:
new.enum_value = data[attribute.name()]
props.active_cost_item_id = self.cost_item props.active_cost_item_id = self.cost_item
props.cost_item_editing_type = "ATTRIBUTES" props.cost_item_editing_type = "ATTRIBUTES"
return {"FINISHED"} return {"FINISHED"}
@@ -278,19 +247,7 @@ class EditCostItem(bpy.types.Operator):
def execute(self, context): def execute(self, context):
props = context.scene.BIMCostProperties props = context.scene.BIMCostProperties
attributes = {} attributes = blenderbim.bim.helper.export_attributes(props.cost_item_attributes)
for attribute in props.cost_item_attributes:
if attribute.is_null:
attributes[attribute.name] = None
else:
if attribute.data_type == "string":
attributes[attribute.name] = attribute.string_value
elif attribute.data_type == "boolean":
attributes[attribute.name] = attribute.bool_value
elif attribute.data_type == "integer":
attributes[attribute.name] = attribute.int_value
elif attribute.data_type == "enum":
attributes[attribute.name] = attribute.enum_value
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run(
"cost.edit_cost_item", "cost.edit_cost_item",
@@ -303,8 +260,8 @@ class EditCostItem(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class AssignControl(bpy.types.Operator): class AssignCostItemProduct(bpy.types.Operator):
bl_idname = "bim.assign_control" bl_idname = "bim.assign_cost_item_product"
bl_label = "Assign Control" bl_label = "Assign Control"
cost_item: bpy.props.IntProperty() cost_item: bpy.props.IntProperty()
related_object: bpy.props.StringProperty() related_object: bpy.props.StringProperty()
@@ -313,20 +270,23 @@ class AssignControl(bpy.types.Operator):
related_objects = ( related_objects = (
[bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects
) )
for related_object in related_objects:
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run(
"control.assign_control", "cost.assign_cost_item_product",
self.file, self.file,
related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id), cost_item=self.file.by_id(self.cost_item),
relating_control=self.file.by_id(self.cost_item), products=[
self.file.by_id(o.BIMObjectProperties.ifc_definition_id)
for o in related_objects
if o.BIMObjectProperties.ifc_definition_id
],
) )
Data.load(self.file) Data.load(self.file)
return {"FINISHED"} return {"FINISHED"}
class UnassignControl(bpy.types.Operator): class UnassignCostItemProduct(bpy.types.Operator):
bl_idname = "bim.unassign_control" bl_idname = "bim.unassign_cost_item_product"
bl_label = "Unassign Control" bl_label = "Unassign Control"
cost_item: bpy.props.IntProperty() cost_item: bpy.props.IntProperty()
related_object: bpy.props.StringProperty() related_object: bpy.props.StringProperty()
@@ -335,13 +295,16 @@ class UnassignControl(bpy.types.Operator):
related_objects = ( related_objects = (
[bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects
) )
for related_object in related_objects:
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run(
"control.unassign_control", "cost.unassign_cost_item_product",
self.file, self.file,
related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id), cost_item=self.file.by_id(self.cost_item),
relating_control=self.file.by_id(self.cost_item), products=[
self.file.by_id(o.BIMObjectProperties.ifc_definition_id)
for o in related_objects
if o.BIMObjectProperties.ifc_definition_id
],
) )
Data.load(self.file) Data.load(self.file)
return {"FINISHED"} return {"FINISHED"}
@@ -356,6 +319,7 @@ class EnableEditingCostItemQuantities(bpy.types.Operator):
props = context.scene.BIMCostProperties props = context.scene.BIMCostProperties
props.active_cost_item_id = self.cost_item props.active_cost_item_id = self.cost_item
props.cost_item_editing_type = "QUANTITIES" props.cost_item_editing_type = "QUANTITIES"
purge()
return {"FINISHED"} return {"FINISHED"}
@@ -368,6 +332,7 @@ class EnableEditingCostItemValues(bpy.types.Operator):
props = context.scene.BIMCostProperties props = context.scene.BIMCostProperties
props.active_cost_item_id = self.cost_item props.active_cost_item_id = self.cost_item
props.cost_item_editing_type = "VALUES" props.cost_item_editing_type = "VALUES"
bpy.ops.bim.disable_editing_cost_item_value()
return {"FINISHED"} return {"FINISHED"}
@@ -392,8 +357,7 @@ class AddCostItemQuantity(bpy.types.Operator):
"cost.assign_cost_item_product_quantities", "cost.assign_cost_item_product_quantities",
self.file, self.file,
cost_item=self.file.by_id(self.cost_item), cost_item=self.file.by_id(self.cost_item),
qto_name=self.props.qto_name, prop_name=self.props.quantity_names,
prop_name=self.props.prop_name
) )
def add_manual_quantity(self): def add_manual_quantity(self):
@@ -434,22 +398,7 @@ class EnableEditingCostItemQuantity(bpy.types.Operator):
self.props.quantity_attributes.remove(0) self.props.quantity_attributes.remove(0)
self.props.active_cost_item_quantity_id = self.physical_quantity self.props.active_cost_item_quantity_id = self.physical_quantity
data = Data.physical_quantities[self.physical_quantity] data = Data.physical_quantities[self.physical_quantity]
blenderbim.bim.helper.import_attributes(data["type"], self.props.quantity_attributes, data)
for attribute in IfcStore.get_schema().declaration_by_name(data["type"]).all_attributes():
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
if data_type == "entity":
continue
new = self.props.quantity_attributes.add()
new.name = attribute.name()
new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = data_type
if data_type == "string":
new.string_value = "" if new.is_null else data[attribute.name()]
elif data_type == "float":
new.float_value = 0.0 if new.is_null else data[attribute.name()]
elif data_type == "integer":
new.int_value = 0 if new.is_null else data[attribute.name()]
return {"FINISHED"} return {"FINISHED"}
@@ -470,17 +419,7 @@ class EditCostItemQuantity(bpy.types.Operator):
def execute(self, context): def execute(self, context):
props = context.scene.BIMCostProperties props = context.scene.BIMCostProperties
attributes = {} attributes = blenderbim.bim.helper.export_attributes(props.quantity_attributes)
for attribute in props.quantity_attributes:
if attribute.is_null:
attributes[attribute.name] = None
else:
if attribute.data_type == "string":
attributes[attribute.name] = attribute.string_value
if attribute.data_type == "float":
attributes[attribute.name] = attribute.float_value
if attribute.data_type == "integer":
attributes[attribute.name] = attribute.int_value
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run(
"cost.edit_cost_item_quantity", "cost.edit_cost_item_quantity",
@@ -492,10 +431,10 @@ class EditCostItemQuantity(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class AddCostItemValue(bpy.types.Operator): class AddCostValue(bpy.types.Operator):
bl_idname = "bim.add_cost_item_value" bl_idname = "bim.add_cost_value"
bl_label = "Add Cost Item Value" bl_label = "Add Cost Value"
cost_item: bpy.props.IntProperty() parent: bpy.props.IntProperty()
cost_type: bpy.props.StringProperty() cost_type: bpy.props.StringProperty()
cost_category: bpy.props.StringProperty() cost_category: bpy.props.StringProperty()
@@ -507,10 +446,8 @@ class AddCostItemValue(bpy.types.Operator):
category = "*" category = "*"
elif self.cost_type == "CATEGORY": elif self.cost_type == "CATEGORY":
category = self.cost_category category = self.cost_category
value = ifcopenshell.api.run("cost.add_cost_item_value", self.file, cost_item=self.file.by_id(self.cost_item)) value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=self.file.by_id(self.parent))
ifcopenshell.api.run( ifcopenshell.api.run("cost.edit_cost_value", self.file, cost_value=value, attributes={"Category": category})
"cost.edit_cost_item_value", self.file, cost_value=value, attributes={"Category": category}
)
Data.load(self.file) Data.load(self.file)
return {"FINISHED"} return {"FINISHED"}
@@ -522,11 +459,7 @@ class RemoveCostItemValue(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run("cost.remove_cost_item_value", self.file, cost_value=self.file.by_id(self.cost_value))
"cost.remove_cost_item_value",
self.file,
cost_value=self.file.by_id(self.cost_value),
)
Data.load(self.file) Data.load(self.file)
return {"FINISHED"} return {"FINISHED"}
@@ -543,31 +476,18 @@ class EnableEditingCostItemValue(bpy.types.Operator):
self.props.active_cost_item_value_id = self.cost_value self.props.active_cost_item_value_id = self.cost_value
data = Data.cost_values[self.cost_value] data = Data.cost_values[self.cost_value]
for attribute in IfcStore.get_schema().declaration_by_name(data["type"]).all_attributes(): blenderbim.bim.helper.import_attributes(
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) data["type"], self.props.cost_value_attributes, data, self.import_attributes
if data_type == "entity" or isinstance(data_type, tuple): )
continue
new = self.props.cost_value_attributes.add()
new.name = attribute.name()
new.is_null = data[attribute.name()] is None
new.is_optional = attribute.optional()
new.data_type = data_type
if attribute.name() == "AppliedValue":
# TODO: for now, only support simple values
new.data_type = "float"
new.float_value = 0.0 if new.is_null else data[attribute.name()]
if data_type == "string":
new.string_value = "" if new.is_null else data[attribute.name()]
elif data_type == "float":
new.float_value = 0.0 if new.is_null else data[attribute.name()]
elif data_type == "integer":
new.int_value = 0 if new.is_null else data[attribute.name()]
elif data_type == "enum":
new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute))
if data[attribute.name()]:
new.enum_value = data[attribute.name()]
return {"FINISHED"} return {"FINISHED"}
def import_attributes(self, name, prop, data):
if name == "AppliedValue":
# TODO: for now, only support simple values
prop.data_type = "float"
prop.float_value = 0.0 if prop.is_null else data[name]
return True
class DisableEditingCostItemValue(bpy.types.Operator): class DisableEditingCostItemValue(bpy.types.Operator):
bl_idname = "bim.disable_editing_cost_item_value" bl_idname = "bim.disable_editing_cost_item_value"
@@ -579,30 +499,75 @@ class DisableEditingCostItemValue(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class EditCostItemValue(bpy.types.Operator): class EditCostValue(bpy.types.Operator):
bl_idname = "bim.edit_cost_item_value" bl_idname = "bim.edit_cost_value"
bl_label = "Edit Cost Item Value" bl_label = "Edit Cost Item Value"
cost_value: bpy.props.IntProperty() cost_value: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
props = context.scene.BIMCostProperties props = context.scene.BIMCostProperties
attributes = {} attributes = blenderbim.bim.helper.export_attributes(props.cost_value_attributes)
for attribute in props.cost_value_attributes:
if attribute.is_null:
attributes[attribute.name] = None
else:
if attribute.data_type == "string":
attributes[attribute.name] = attribute.string_value
if attribute.data_type == "float":
attributes[attribute.name] = attribute.float_value
if attribute.data_type == "integer":
attributes[attribute.name] = attribute.int_value
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run(
"cost.edit_cost_item_value", "cost.edit_cost_value",
self.file, self.file,
**{"cost_value": self.file.by_id(self.cost_value), "attributes": attributes}, **{"cost_value": self.file.by_id(self.cost_value), "attributes": attributes},
) )
Data.load(IfcStore.get_file()) Data.load(IfcStore.get_file())
bpy.ops.bim.disable_editing_cost_item_value() bpy.ops.bim.disable_editing_cost_item_value()
return {"FINISHED"} return {"FINISHED"}
class CopyCostItemValues(bpy.types.Operator):
bl_idname = "bim.copy_cost_item_values"
bl_label = "Copy Cost Item Values"
source: bpy.props.IntProperty()
destination: bpy.props.IntProperty()
def execute(self, context):
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"cost.copy_cost_item_values",
self.file,
**{"source": self.file.by_id(self.source), "destination": self.file.by_id(self.destination)},
)
Data.load(IfcStore.get_file())
return {"FINISHED"}
class SelectCostItemProducts(bpy.types.Operator):
bl_idname = "bim.select_cost_item_products"
bl_label = "Select Cost Item Products"
cost_item: bpy.props.IntProperty()
def execute(self, context):
self.file = IfcStore.get_file()
related_products = Data.cost_items[self.cost_item]["Controls"]
for obj in bpy.context.visible_objects:
obj.select_set(False)
if obj.BIMObjectProperties.ifc_definition_id in related_products:
obj.select_set(True)
return {"FINISHED"}
class SelectCostScheduleProducts(bpy.types.Operator):
bl_idname = "bim.select_cost_schedule_products"
bl_label = "Select Cost Schedule Products"
cost_schedule: bpy.props.IntProperty()
def execute(self, context):
self.file = IfcStore.get_file()
self.related_products = []
for cost_item_id in Data.cost_schedules[self.cost_schedule]["Controls"]:
self.get_related_products(Data.cost_items[cost_item_id])
self.related_products = set(self.related_products)
for obj in bpy.context.visible_objects:
obj.select_set(False)
if obj.BIMObjectProperties.ifc_definition_id in self.related_products:
obj.select_set(True)
return {"FINISHED"}
def get_related_products(self, cost_item):
self.related_products.extend(cost_item["Controls"])
for child_id in cost_item["IsNestedBy"]:
self.get_related_products(Data.cost_items[child_id])
@@ -2,6 +2,7 @@ import bpy
import ifcopenshell.api import ifcopenshell.api
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.cost.data import Data from ifcopenshell.api.cost.data import Data
from ifcopenshell.api.pset.data import Data as PsetData
from blenderbim.bim.prop import StrProperty, Attribute from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bpy.props import ( from bpy.props import (
@@ -17,17 +18,19 @@ from bpy.props import (
quantitytypes_enum = [] quantitytypes_enum = []
quantitynames_enum = []
def purge(): def purge():
global quantitytypes_enum global quantitytypes_enum
global quantitynames_enum
quantitytypes_enum = [] quantitytypes_enum = []
quantitynames_enum = []
def getQuantityTypes(self, context): def getQuantityTypes(self, context):
global quantitytypes_enum global quantitytypes_enum
if len(quantitytypes_enum) == 0 and IfcStore.get_schema(): if len(quantitytypes_enum) == 0 and IfcStore.get_schema():
quantitytypes_enum.clear()
quantitytypes_enum = [("QTO", "Qto", "Derive quantities from IFC quantity sets")] quantitytypes_enum = [("QTO", "Qto", "Derive quantities from IFC quantity sets")]
quantitytypes_enum.extend( quantitytypes_enum.extend(
[ [
@@ -38,6 +41,21 @@ def getQuantityTypes(self, context):
return quantitytypes_enum return quantitytypes_enum
def getQuantityNames(self, context):
global quantitynames_enum
ifc_file = IfcStore.get_file()
if len(quantitynames_enum) == 0 and ifc_file:
names = set()
for element_id in Data.cost_items[self.active_cost_item_id]["Controls"]:
if element_id not in PsetData.products:
PsetData.load(IfcStore.get_file(), element_id)
for qto_id in PsetData.products[element_id]["qtos"]:
qto = PsetData.qtos[qto_id]
[names.add(PsetData.properties[p]["Name"]) for p in qto["Properties"]]
quantitynames_enum.extend([(n, n, "") for n in names])
return quantitynames_enum
def updateCostItemName(self, context): def updateCostItemName(self, context):
if self.name == "Unnamed": if self.name == "Unnamed":
return return
@@ -73,8 +91,7 @@ class BIMCostProperties(PropertyGroup):
cost_item_attributes: CollectionProperty(name="Task Attributes", type=Attribute) cost_item_attributes: CollectionProperty(name="Task Attributes", type=Attribute)
contracted_cost_items: StringProperty(name="Contracted Cost Items", default="[]") contracted_cost_items: StringProperty(name="Contracted Cost Items", default="[]")
quantity_types: EnumProperty(items=getQuantityTypes, name="Quantity Types") quantity_types: EnumProperty(items=getQuantityTypes, name="Quantity Types")
qto_name: StringProperty(name="Qto Name") quantity_names: EnumProperty(items=getQuantityNames, name="Quantity Names")
prop_name: StringProperty(name="Prop Name")
active_cost_item_quantity_id: IntProperty(name="Active Cost Item Quantity Id") active_cost_item_quantity_id: IntProperty(name="Active Cost Item Quantity Id")
quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute) quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute)
cost_types: EnumProperty( cost_types: EnumProperty(
+76 -25
View File
@@ -32,6 +32,8 @@ class BIM_PT_cost_schedules(Panel):
row.label(text=cost_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON") row.label(text=cost_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON")
if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule_id: if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule_id:
op = row.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="")
op.cost_schedule = cost_schedule_id
if self.props.is_editing == "COST_SCHEDULE": if self.props.is_editing == "COST_SCHEDULE":
row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK") row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK")
elif self.props.is_editing == "COST_ITEMS": elif self.props.is_editing == "COST_ITEMS":
@@ -97,8 +99,7 @@ class BIM_PT_cost_schedules(Panel):
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.prop(self.props, "quantity_types", text="") row.prop(self.props, "quantity_types", text="")
if self.props.quantity_types == "QTO": if self.props.quantity_types == "QTO":
row.prop(self.props, "qto_name", text="") row.prop(self.props, "quantity_names", text="")
row.prop(self.props, "prop_name", text="")
op = row.operator("bim.add_cost_item_quantity", text="", icon="ADD") op = row.operator("bim.add_cost_item_quantity", text="", icon="ADD")
op.cost_item = self.props.active_cost_item_id op.cost_item = self.props.active_cost_item_id
op.ifc_class = self.props.quantity_types op.ifc_class = self.props.quantity_types
@@ -108,7 +109,7 @@ class BIM_PT_cost_schedules(Panel):
value = quantity[[k for k in quantity.keys() if "Value" in k][0]] value = quantity[[k for k in quantity.keys() if "Value" in k][0]]
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text=quantity["Name"]) row.label(text=quantity["Name"])
row.label(text=str(value)) row.label(text="{0:.2f}".format(value))
if self.props.active_cost_item_quantity_id and self.props.active_cost_item_quantity_id == quantity_id: if self.props.active_cost_item_quantity_id and self.props.active_cost_item_quantity_id == quantity_id:
op = row.operator("bim.edit_cost_item_quantity", text="", icon="CHECKMARK") op = row.operator("bim.edit_cost_item_quantity", text="", icon="CHECKMARK")
op.physical_quantity = quantity_id op.physical_quantity = quantity_id
@@ -149,35 +150,75 @@ class BIM_PT_cost_schedules(Panel):
row.prop(self.props, "cost_types", text="") row.prop(self.props, "cost_types", text="")
if self.props.cost_types == "CATEGORY": if self.props.cost_types == "CATEGORY":
row.prop(self.props, "cost_category", text="") row.prop(self.props, "cost_category", text="")
op = row.operator("bim.add_cost_item_value", text="", icon="ADD") op = row.operator("bim.add_cost_value", text="", icon="ADD")
op.cost_item = self.props.active_cost_item_id op.parent = self.props.active_cost_item_id
op.cost_type = self.props.cost_types op.cost_type = self.props.cost_types
if self.props.cost_types == "CATEGORY": if self.props.cost_types == "CATEGORY":
op.cost_category = self.props.cost_category op.cost_category = self.props.cost_category
for cost_value_id in Data.cost_items[self.props.active_cost_item_id]["CostValues"]: for cost_value_id in Data.cost_items[self.props.active_cost_item_id]["CostValues"]:
cost_value = Data.cost_values[cost_value_id]
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text=str(cost_value["Category"])) self.draw_readonly_cost_value_ui(row, cost_value_id)
row.label(text=str(cost_value["AppliedValue"]))
if self.props.active_cost_item_value_id:
box = self.layout.box()
self.draw_editable_cost_value_ui(box, Data.cost_values[self.props.active_cost_item_value_id])
def draw_readonly_cost_value_ui(self, layout, cost_value_id):
cost_value = Data.cost_values[cost_value_id]
cost_value_label = "{0:.2f}".format(cost_value["AppliedValue"])
if cost_value["Category"]:
cost_value_label += " ({})".format(cost_value["Category"])
layout.label(text="", icon="DISC")
self.draw_cost_value_operator_ui(layout, cost_value_id)
layout.label(text=cost_value_label)
for component_id in cost_value["Components"] or []:
self.draw_readonly_component_cost_value_ui(layout, component_id)
def draw_readonly_component_cost_value_ui(self, layout, cost_value_id, level=1):
self.draw_cost_value_operator_ui(layout, cost_value_id)
cost_value = Data.cost_values[cost_value_id]
cost_value_label = ">" * level
cost_value_label += "{0:.2f}".format(cost_value["AppliedValue"])
if cost_value["Category"]:
cost_value_label += " ({})".format(cost_value["Category"])
layout.label(text=cost_value_label)
for component_id in cost_value["Components"] or []:
self.draw_readonly_component_cost_value_ui(layout, component_id, level + 1)
def draw_cost_value_operator_ui(self, layout, cost_value_id):
if self.props.active_cost_item_value_id and self.props.active_cost_item_value_id == cost_value_id: if self.props.active_cost_item_value_id and self.props.active_cost_item_value_id == cost_value_id:
op = row.operator("bim.edit_cost_item_value", text="", icon="CHECKMARK") op = layout.operator("bim.edit_cost_value", text="", icon="CHECKMARK")
op.cost_value = cost_value_id op.cost_value = cost_value_id
row.operator("bim.disable_editing_cost_item_value", text="", icon="CANCEL") op = layout.operator("bim.add_cost_value", text="", icon="ADD")
op.parent = cost_value_id
op.cost_type = self.props.cost_types
if self.props.cost_types == "CATEGORY":
op.cost_category = self.props.cost_category
layout.operator("bim.disable_editing_cost_item_value", text="", icon="CANCEL")
elif self.props.active_cost_item_value_id: elif self.props.active_cost_item_value_id:
op = row.operator("bim.remove_cost_item_value", text="", icon="X") op = layout.operator("bim.add_cost_value", text="", icon="ADD")
op.parent = cost_value_id
op.cost_type = self.props.cost_types
if self.props.cost_types == "CATEGORY":
op.cost_category = self.props.cost_category
op = layout.operator("bim.remove_cost_item_value", text="", icon="X")
op.cost_value = cost_value_id op.cost_value = cost_value_id
else: else:
op = row.operator("bim.enable_editing_cost_item_value", text="", icon="GREASEPENCIL") op = layout.operator("bim.enable_editing_cost_item_value", text="", icon="GREASEPENCIL")
op.cost_value = cost_value_id op.cost_value = cost_value_id
op = row.operator("bim.remove_cost_item_value", text="", icon="X") op = layout.operator("bim.add_cost_value", text="", icon="ADD")
op.parent = cost_value_id
op.cost_type = self.props.cost_types
if self.props.cost_types == "CATEGORY":
op.cost_category = self.props.cost_category
op = layout.operator("bim.remove_cost_item_value", text="", icon="X")
op.cost_value = cost_value_id op.cost_value = cost_value_id
if self.props.active_cost_item_value_id and self.props.active_cost_item_value_id == cost_value_id: def draw_editable_cost_value_ui(self, layout, cost_value):
box = self.layout.box()
self.draw_editable_cost_item_value_ui(box)
def draw_editable_cost_item_value_ui(self, layout):
for attribute in self.props.cost_value_attributes: for attribute in self.props.cost_value_attributes:
row = layout.row(align=True) row = layout.row(align=True)
if attribute.data_type == "string": if attribute.data_type == "string":
@@ -194,12 +235,14 @@ class BIM_PT_cost_schedules(Panel):
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
class BIM_UL_cost_items(UIList): class BIM_UL_cost_items(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item: if item:
props = context.scene.BIMCostProperties props = context.scene.BIMCostProperties
cost_item = Data.cost_items[item.ifc_definition_id] cost_item = Data.cost_items[item.ifc_definition_id]
row = layout.row(align=True) row = layout.row(align=True)
for i in range(0, item.level_index): for i in range(0, item.level_index):
row.label(text="", icon="BLANK1") row.label(text="", icon="BLANK1")
if item.has_children: if item.has_children:
@@ -213,35 +256,43 @@ class BIM_UL_cost_items(UIList):
).cost_item = item.ifc_definition_id ).cost_item = item.ifc_definition_id
else: else:
row.label(text="", icon="DOT") row.label(text="", icon="DOT")
row.prop(item, "name", emboss=False, text="")
row.label(text="M3") split1 = row.split(factor=0.7)
split1.prop(item, "name", emboss=False, text="")
op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES") op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES")
op.cost_item = item.ifc_definition_id op.cost_item = item.ifc_definition_id
row.label(text=str(cost_item["TotalCostQuantity"])) row.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + " (M3)")
op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC") op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC")
op.cost_item = item.ifc_definition_id op.cost_item = item.ifc_definition_id
row.label(text=str(cost_item["TotalAppliedValue"])) row.label(text="{0:.2f}".format(cost_item["TotalAppliedValue"]))
row.label(text=str(cost_item["TotalCostValue"]), icon="CON_TRANSLIKE") row.label(text="{0:.2f}".format(cost_item["TotalCostValue"]), icon="CON_TRANSLIKE")
if context.active_object: if context.active_object:
oprops = context.active_object.BIMObjectProperties oprops = context.active_object.BIMObjectProperties
row = layout.row(align=True) row = layout.row(align=True)
if oprops.ifc_definition_id in cost_item["Controls"]: if oprops.ifc_definition_id in cost_item["Controls"]:
op = row.operator("bim.unassign_control", text="", icon="KEYFRAME_HLT", emboss=False) op = row.operator("bim.unassign_cost_item_product", text="", icon="KEYFRAME_HLT", emboss=False)
op.cost_item = item.ifc_definition_id op.cost_item = item.ifc_definition_id
else: else:
op = row.operator("bim.assign_control", text="", icon="KEYFRAME", emboss=False) op = row.operator("bim.assign_cost_item_product", text="", icon="KEYFRAME", emboss=False)
op.cost_item = item.ifc_definition_id op.cost_item = item.ifc_definition_id
if props.active_cost_item_id == item.ifc_definition_id: if props.active_cost_item_id == item.ifc_definition_id:
if props.cost_item_editing_type == "ATTRIBUTES":
row.operator("bim.edit_cost_item", text="", icon="CHECKMARK") row.operator("bim.edit_cost_item", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL") row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL")
elif props.active_cost_item_id: elif props.active_cost_item_id:
if props.cost_item_editing_type == "VALUES":
op = row.operator("bim.copy_cost_item_values", text="", icon="COPYDOWN")
op.source = props.active_cost_item_id
op.destination = item.ifc_definition_id
row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id
row.operator("bim.remove_cost_item", text="", icon="X").cost_item = item.ifc_definition_id row.operator("bim.remove_cost_item", text="", icon="X").cost_item = item.ifc_definition_id
else: else:
op = row.operator("bim.select_cost_item_products", icon="RESTRICT_SELECT_OFF", text="")
op.cost_item = item.ifc_definition_id
row.operator( row.operator(
"bim.enable_editing_cost_item", text="", icon="GREASEPENCIL" "bim.enable_editing_cost_item", text="", icon="GREASEPENCIL"
).cost_item = item.ifc_definition_id ).cost_item = item.ifc_definition_id
@@ -68,6 +68,8 @@ class BIM_PT_object_documents(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id) return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
def draw(self, context): def draw(self, context):
@@ -1,16 +1,76 @@
import bpy import bpy
from . import ui, operator from . import ui, prop, operator, handler, gizmos
classes = ( classes = (
operator.AddDrawing, operator.AddDrawing,
operator.CreateDrawing, operator.CreateDrawing,
operator.AddAnnotation,
operator.AddSheet,
operator.OpenSheet,
operator.AddDrawingToSheet,
operator.CreateSheets,
operator.OpenView,
operator.OpenViewCamera,
operator.ActivateView,
operator.SelectDocIfcFile,
operator.GenerateReferences,
operator.ResizeText,
operator.AddVariable,
operator.RemoveVariable,
operator.PropagateTextData,
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.AddDrawingStyleAttribute,
operator.RemoveDrawingStyleAttribute,
operator.RefreshDrawingList,
operator.CleanWireframes,
operator.CopyGrid,
operator.AddSectionsAnnotations,
prop.Variable,
prop.Drawing,
prop.Schedule,
prop.DrawingStyle,
prop.Sheet,
prop.DocProperties,
prop.BIMCameraProperties,
prop.BIMTextProperties,
ui.BIM_PT_camera, ui.BIM_PT_camera,
ui.BIM_PT_drawing_underlay,
ui.BIM_PT_drawings,
ui.BIM_PT_schedules,
ui.BIM_PT_sheets,
ui.BIM_PT_text,
ui.BIM_PT_annotation_utilities,
ui.BIM_UL_drawinglist,
gizmos.UglyDotGizmo,
gizmos.DotGizmo,
gizmos.DimensionLabelGizmo,
gizmos.ExtrusionGuidesGizmo,
gizmos.ExtrusionWidget,
) )
def register(): def register():
pass bpy.types.Scene.DocProperties = bpy.props.PointerProperty(type=prop.DocProperties)
bpy.types.Camera.BIMCameraProperties = bpy.props.PointerProperty(type=prop.BIMCameraProperties)
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.app.handlers.load_post.append(handler.toggleDecorationsOnLoad)
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
def unregister(): def unregister():
pass del bpy.types.Scene.DocProperties
del bpy.types.Camera.BIMCameraProperties
del bpy.types.TextCurve.BIMTextProperties
bpy.app.handlers.load_post.remove(handler.toggleDecorationsOnLoad)
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
@@ -17,9 +17,9 @@ class Annotator:
@staticmethod @staticmethod
def add_text(related_element=None): def add_text(related_element=None):
curve = bpy.data.curves.new(type="FONT", name="Plan/Annotation/PLAN_VIEW/Text") curve = bpy.data.curves.new(type="FONT", name="Text")
curve.body = "TEXT" curve.body = "TEXT"
obj = bpy.data.objects.new("IfcAnnotation/Text", curve) obj = bpy.data.objects.new("Text", curve)
obj.matrix_world = bpy.context.scene.camera.matrix_world obj.matrix_world = bpy.context.scene.camera.matrix_world
if related_element is None: if related_element is None:
location, _, _, _ = Annotator.get_placeholder_coords() location, _, _, _ = Annotator.get_placeholder_coords()
@@ -138,12 +138,12 @@ class Annotator:
if name in obj.name: if name in obj.name:
return obj return obj
if data_type == "mesh": if data_type == "mesh":
data = bpy.data.meshes.new("Plan/Annotation/PLAN_VIEW/" + name) data = bpy.data.meshes.new(name)
elif data_type == "curve": elif data_type == "curve":
data = bpy.data.curves.new("Plan/Annotation/PLAN_VIEW/" + name, type="CURVE") data = bpy.data.curves.new(name, type="CURVE")
data.dimensions = "3D" data.dimensions = "3D"
data.resolution_u = 2 data.resolution_u = 2
obj = bpy.data.objects.new("IfcAnnotation/" + name, data) obj = bpy.data.objects.new(name, data)
collection.objects.link(obj) collection.objects.link(obj)
return obj return obj
@@ -13,7 +13,7 @@ import gpu
import bgl import bgl
from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat from gpu.types import GPUShader, GPUBatch, GPUIndexBuf, GPUVertBuf, GPUVertFormat
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from . import helper import blenderbim.bim.module.drawing.helper as helper
class BaseDecorator(): class BaseDecorator():
@@ -992,7 +992,7 @@ class GridDecorator(BaseDecorator):
p0 = location_3d_to_region_2d(region, region3d, v0) p0 = location_3d_to_region_2d(region, region3d, v0)
p1 = location_3d_to_region_2d(region, region3d, v1) p1 = location_3d_to_region_2d(region, region3d, v1)
dir = Vector((1, 0)) dir = Vector((1, 0))
text = obj.BIMObjectProperties.attributes['AxisTag'].string_value text = obj.name.split("/")[1].split(".")[0]
self.draw_label(context, text, p0, dir, vcenter=True, gap=0) self.draw_label(context, text, p0, dir, vcenter=True, gap=0)
self.draw_label(context, text, p1, dir, vcenter=True, gap=0) self.draw_label(context, text, p1, dir, vcenter=True, gap=0)
@@ -1,14 +1,12 @@
import bpy import bpy
import blf import blf
from bpy import types
import math import math
import gpu, bgl
from bpy import types
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from mathutils import geometry from mathutils import geometry
import gpu, bgl
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader, BaseLinesShader
from .shaders import DotsGizmoShader, ExtrusionGuidesShader, BaseLinesShader
"""Gizmos under the hood """Gizmos under the hood
@@ -43,76 +41,196 @@ draw_select -- fake-draw of selection geometry for gpu-side cursor tracking
# some geometries for Gizmo.custom_shape shaders # some geometries for Gizmo.custom_shape shaders
CUBE = ( CUBE = (
(+1, +1, +1), (-1, +1, +1), (+1, -1, +1), # top (+1, +1, +1),
(+1, -1, +1), (-1, +1, +1), (-1, -1, +1), (-1, +1, +1),
(+1, +1, +1), (+1, -1, +1), (+1, +1, -1), # right (+1, -1, +1), # top
(+1, +1, -1), (+1, -1, +1), (+1, -1, -1), (+1, -1, +1),
(+1, +1, +1), (+1, +1, -1), (-1, +1, +1), # back (-1, +1, +1),
(-1, +1, +1), (+1, +1, -1), (-1, +1, -1), (-1, -1, +1),
(-1, -1, -1), (-1, +1, -1), (+1, -1, -1), # bot (+1, +1, +1),
(+1, -1, -1), (-1, +1, -1), (+1, +1, -1), (+1, -1, +1),
(-1, -1, -1), (-1, -1, +1), (-1, +1, -1), # left (+1, +1, -1), # right
(-1, +1, -1), (-1, -1, +1), (-1, +1, +1), (+1, +1, -1),
(-1, -1, -1), (+1, -1, -1), (-1, -1, +1), # front (+1, -1, +1),
(-1, -1, +1), (+1, -1, -1), (+1, -1, +1) (+1, -1, -1),
(+1, +1, +1),
(+1, +1, -1),
(-1, +1, +1), # back
(-1, +1, +1),
(+1, +1, -1),
(-1, +1, -1),
(-1, -1, -1),
(-1, +1, -1),
(+1, -1, -1), # bot
(+1, -1, -1),
(-1, +1, -1),
(+1, +1, -1),
(-1, -1, -1),
(-1, -1, +1),
(-1, +1, -1), # left
(-1, +1, -1),
(-1, -1, +1),
(-1, +1, +1),
(-1, -1, -1),
(+1, -1, -1),
(-1, -1, +1), # front
(-1, -1, +1),
(+1, -1, -1),
(+1, -1, +1),
) )
DISC = ( DISC = (
(0.0, 0.0, 0.0), (1.0, 0.0, 0), (0.8660254037844387, 0.49999999999999994, 0), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0.8660254037844387, 0.49999999999999994, 0), (0.5000000000000001, 0.8660254037844386, 0), (1.0, 0.0, 0),
(0.0, 0.0, 0.0), (0.5000000000000001, 0.8660254037844386, 0), (6.123233995736766e-17, 1.0, 0), (0.8660254037844387, 0.49999999999999994, 0),
(0.0, 0.0, 0.0), (6.123233995736766e-17, 1.0, 0), (-0.4999999999999998, 0.8660254037844387, 0), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (-0.4999999999999998, 0.8660254037844387, 0), (-0.8660254037844385, 0.5000000000000003, 0), (0.8660254037844387, 0.49999999999999994, 0),
(0.0, 0.0, 0.0), (-0.8660254037844385, 0.5000000000000003, 0), (-1.0, 1.2246467991473532e-16, 0), (0.5000000000000001, 0.8660254037844386, 0),
(0.0, 0.0, 0.0), (-1.0, 1.2246467991473532e-16, 0), (-0.8660254037844388, -0.4999999999999997, 0), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (-0.8660254037844388, -0.4999999999999997, 0), (-0.5000000000000004, -0.8660254037844384, 0), (0.5000000000000001, 0.8660254037844386, 0),
(0.0, 0.0, 0.0), (-0.5000000000000004, -0.8660254037844384, 0), (-1.8369701987210297e-16, -1.0, 0), (6.123233995736766e-17, 1.0, 0),
(0.0, 0.0, 0.0), (-1.8369701987210297e-16, -1.0, 0), (0.49999999999999933, -0.866025403784439, 0), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0.49999999999999933, -0.866025403784439, 0), (0.8660254037844384, -0.5000000000000004, 0), (6.123233995736766e-17, 1.0, 0),
(0.0, 0.0, 0.0), (0.8660254037844384, -0.5000000000000004, 0), (1.0, 0.0, 0), (-0.4999999999999998, 0.8660254037844387, 0),
(0.0, 0.0, 0.0),
(-0.4999999999999998, 0.8660254037844387, 0),
(-0.8660254037844385, 0.5000000000000003, 0),
(0.0, 0.0, 0.0),
(-0.8660254037844385, 0.5000000000000003, 0),
(-1.0, 1.2246467991473532e-16, 0),
(0.0, 0.0, 0.0),
(-1.0, 1.2246467991473532e-16, 0),
(-0.8660254037844388, -0.4999999999999997, 0),
(0.0, 0.0, 0.0),
(-0.8660254037844388, -0.4999999999999997, 0),
(-0.5000000000000004, -0.8660254037844384, 0),
(0.0, 0.0, 0.0),
(-0.5000000000000004, -0.8660254037844384, 0),
(-1.8369701987210297e-16, -1.0, 0),
(0.0, 0.0, 0.0),
(-1.8369701987210297e-16, -1.0, 0),
(0.49999999999999933, -0.866025403784439, 0),
(0.0, 0.0, 0.0),
(0.49999999999999933, -0.866025403784439, 0),
(0.8660254037844384, -0.5000000000000004, 0),
(0.0, 0.0, 0.0),
(0.8660254037844384, -0.5000000000000004, 0),
(1.0, 0.0, 0),
) )
X3DISC = ( X3DISC = (
(0.0, 0.0, 0.0), (1.0, 0.0, 0), (0.8660254037844387, 0.49999999999999994, 0), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0.8660254037844387, 0.49999999999999994, 0), (0.5000000000000001, 0.8660254037844386, 0), (1.0, 0.0, 0),
(0.0, 0.0, 0.0), (0.5000000000000001, 0.8660254037844386, 0), (6.123233995736766e-17, 1.0, 0), (0.8660254037844387, 0.49999999999999994, 0),
(0.0, 0.0, 0.0), (6.123233995736766e-17, 1.0, 0), (-0.4999999999999998, 0.8660254037844387, 0), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (-0.4999999999999998, 0.8660254037844387, 0), (-0.8660254037844385, 0.5000000000000003, 0), (0.8660254037844387, 0.49999999999999994, 0),
(0.0, 0.0, 0.0), (-0.8660254037844385, 0.5000000000000003, 0), (-1.0, 1.2246467991473532e-16, 0), (0.5000000000000001, 0.8660254037844386, 0),
(0.0, 0.0, 0.0), (-1.0, 1.2246467991473532e-16, 0), (-0.8660254037844388, -0.4999999999999997, 0), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (-0.8660254037844388, -0.4999999999999997, 0), (-0.5000000000000004, -0.8660254037844384, 0), (0.5000000000000001, 0.8660254037844386, 0),
(0.0, 0.0, 0.0), (-0.5000000000000004, -0.8660254037844384, 0), (-1.8369701987210297e-16, -1.0, 0), (6.123233995736766e-17, 1.0, 0),
(0.0, 0.0, 0.0), (-1.8369701987210297e-16, -1.0, 0), (0.49999999999999933, -0.866025403784439, 0), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0.49999999999999933, -0.866025403784439, 0), (0.8660254037844384, -0.5000000000000004, 0), (6.123233995736766e-17, 1.0, 0),
(0.0, 0.0, 0.0), (0.8660254037844384, -0.5000000000000004, 0), (1.0, 0.0, 0), (-0.4999999999999998, 0.8660254037844387, 0),
(0.0, 0.0, 0.0), (0, 1.0, 0.0), (0, 0.8660254037844387, 0.49999999999999994), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0, 0.8660254037844387, 0.49999999999999994), (0, 0.5000000000000001, 0.8660254037844386), (-0.4999999999999998, 0.8660254037844387, 0),
(0.0, 0.0, 0.0), (0, 0.5000000000000001, 0.8660254037844386), (0, 6.123233995736766e-17, 1.0), (-0.8660254037844385, 0.5000000000000003, 0),
(0.0, 0.0, 0.0), (0, 6.123233995736766e-17, 1.0), (0, -0.4999999999999998, 0.8660254037844387), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0, -0.4999999999999998, 0.8660254037844387), (0, -0.8660254037844385, 0.5000000000000003), (-0.8660254037844385, 0.5000000000000003, 0),
(0.0, 0.0, 0.0), (0, -0.8660254037844385, 0.5000000000000003), (0, -1.0, 1.2246467991473532e-16), (-1.0, 1.2246467991473532e-16, 0),
(0.0, 0.0, 0.0), (0, -1.0, 1.2246467991473532e-16), (0, -0.8660254037844388, -0.4999999999999997), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0, -0.8660254037844388, -0.4999999999999997), (0, -0.5000000000000004, -0.8660254037844384), (-1.0, 1.2246467991473532e-16, 0),
(0.0, 0.0, 0.0), (0, -0.5000000000000004, -0.8660254037844384), (0, -1.8369701987210297e-16, -1.0), (-0.8660254037844388, -0.4999999999999997, 0),
(0.0, 0.0, 0.0), (0, -1.8369701987210297e-16, -1.0), (0, 0.49999999999999933, -0.866025403784439), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0, 0.49999999999999933, -0.866025403784439), (0, 0.8660254037844384, -0.5000000000000004), (-0.8660254037844388, -0.4999999999999997, 0),
(0.0, 0.0, 0.0), (0, 0.8660254037844384, -0.5000000000000004), (0, 1.0, 0.0), (-0.5000000000000004, -0.8660254037844384, 0),
(0.0, 0.0, 0.0), (0.0, 0, 1.0), (0.49999999999999994, 0, 0.8660254037844387), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0.49999999999999994, 0, 0.8660254037844387), (0.8660254037844386, 0, 0.5000000000000001), (-0.5000000000000004, -0.8660254037844384, 0),
(0.0, 0.0, 0.0), (0.8660254037844386, 0, 0.5000000000000001), (1.0, 0, 6.123233995736766e-17), (-1.8369701987210297e-16, -1.0, 0),
(0.0, 0.0, 0.0), (1.0, 0, 6.123233995736766e-17), (0.8660254037844387, 0, -0.4999999999999998), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (0.8660254037844387, 0, -0.4999999999999998), (0.5000000000000003, 0, -0.8660254037844385), (-1.8369701987210297e-16, -1.0, 0),
(0.0, 0.0, 0.0), (0.5000000000000003, 0, -0.8660254037844385), (1.2246467991473532e-16, 0, -1.0), (0.49999999999999933, -0.866025403784439, 0),
(0.0, 0.0, 0.0), (1.2246467991473532e-16, 0, -1.0), (-0.4999999999999997, 0, -0.8660254037844388), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (-0.4999999999999997, 0, -0.8660254037844388), (-0.8660254037844384, 0, -0.5000000000000004), (0.49999999999999933, -0.866025403784439, 0),
(0.0, 0.0, 0.0), (-0.8660254037844384, 0, -0.5000000000000004), (-1.0, 0, -1.8369701987210297e-16), (0.8660254037844384, -0.5000000000000004, 0),
(0.0, 0.0, 0.0), (-1.0, 0, -1.8369701987210297e-16), (-0.866025403784439, 0, 0.49999999999999933), (0.0, 0.0, 0.0),
(0.0, 0.0, 0.0), (-0.866025403784439, 0, 0.49999999999999933), (-0.5000000000000004, 0, 0.8660254037844384), (0.8660254037844384, -0.5000000000000004, 0),
(0.0, 0.0, 0.0), (-0.5000000000000004, 0, 0.8660254037844384), (0.0, 0, 1.0), (1.0, 0.0, 0),
(0.0, 0.0, 0.0),
(0, 1.0, 0.0),
(0, 0.8660254037844387, 0.49999999999999994),
(0.0, 0.0, 0.0),
(0, 0.8660254037844387, 0.49999999999999994),
(0, 0.5000000000000001, 0.8660254037844386),
(0.0, 0.0, 0.0),
(0, 0.5000000000000001, 0.8660254037844386),
(0, 6.123233995736766e-17, 1.0),
(0.0, 0.0, 0.0),
(0, 6.123233995736766e-17, 1.0),
(0, -0.4999999999999998, 0.8660254037844387),
(0.0, 0.0, 0.0),
(0, -0.4999999999999998, 0.8660254037844387),
(0, -0.8660254037844385, 0.5000000000000003),
(0.0, 0.0, 0.0),
(0, -0.8660254037844385, 0.5000000000000003),
(0, -1.0, 1.2246467991473532e-16),
(0.0, 0.0, 0.0),
(0, -1.0, 1.2246467991473532e-16),
(0, -0.8660254037844388, -0.4999999999999997),
(0.0, 0.0, 0.0),
(0, -0.8660254037844388, -0.4999999999999997),
(0, -0.5000000000000004, -0.8660254037844384),
(0.0, 0.0, 0.0),
(0, -0.5000000000000004, -0.8660254037844384),
(0, -1.8369701987210297e-16, -1.0),
(0.0, 0.0, 0.0),
(0, -1.8369701987210297e-16, -1.0),
(0, 0.49999999999999933, -0.866025403784439),
(0.0, 0.0, 0.0),
(0, 0.49999999999999933, -0.866025403784439),
(0, 0.8660254037844384, -0.5000000000000004),
(0.0, 0.0, 0.0),
(0, 0.8660254037844384, -0.5000000000000004),
(0, 1.0, 0.0),
(0.0, 0.0, 0.0),
(0.0, 0, 1.0),
(0.49999999999999994, 0, 0.8660254037844387),
(0.0, 0.0, 0.0),
(0.49999999999999994, 0, 0.8660254037844387),
(0.8660254037844386, 0, 0.5000000000000001),
(0.0, 0.0, 0.0),
(0.8660254037844386, 0, 0.5000000000000001),
(1.0, 0, 6.123233995736766e-17),
(0.0, 0.0, 0.0),
(1.0, 0, 6.123233995736766e-17),
(0.8660254037844387, 0, -0.4999999999999998),
(0.0, 0.0, 0.0),
(0.8660254037844387, 0, -0.4999999999999998),
(0.5000000000000003, 0, -0.8660254037844385),
(0.0, 0.0, 0.0),
(0.5000000000000003, 0, -0.8660254037844385),
(1.2246467991473532e-16, 0, -1.0),
(0.0, 0.0, 0.0),
(1.2246467991473532e-16, 0, -1.0),
(-0.4999999999999997, 0, -0.8660254037844388),
(0.0, 0.0, 0.0),
(-0.4999999999999997, 0, -0.8660254037844388),
(-0.8660254037844384, 0, -0.5000000000000004),
(0.0, 0.0, 0.0),
(-0.8660254037844384, 0, -0.5000000000000004),
(-1.0, 0, -1.8369701987210297e-16),
(0.0, 0.0, 0.0),
(-1.0, 0, -1.8369701987210297e-16),
(-0.866025403784439, 0, 0.49999999999999933),
(0.0, 0.0, 0.0),
(-0.866025403784439, 0, 0.49999999999999933),
(-0.5000000000000004, 0, 0.8660254037844384),
(0.0, 0.0, 0.0),
(-0.5000000000000004, 0, 0.8660254037844384),
(0.0, 0, 1.0),
) )
class CustomGizmo(): class CustomGizmo:
# FIXME: highliting/selection doesnt work # FIXME: highliting/selection doesnt work
def draw_very_custom_shape(self, ctx, custom_shape, select_id=None): def draw_very_custom_shape(self, ctx, custom_shape, select_id=None):
# similar to draw_custom_shape # similar to draw_custom_shape
@@ -127,7 +245,7 @@ class CustomGizmo():
color = (*self.color_highlight, self.alpha_highlight) color = (*self.color_highlight, self.alpha_highlight)
else: else:
color = (*self.color, self.alpha) color = (*self.color, self.alpha)
shader.uniform_float('color', color) shader.uniform_float("color", color)
shape.glenable() shape.glenable()
shape.uniform_region(ctx) shape.uniform_region(ctx)
@@ -139,31 +257,32 @@ class CustomGizmo():
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
class OffsetHandle(): class OffsetHandle:
"""Handling mouse to offset gizmo from base along Z axis""" """Handling mouse to offset gizmo from base along Z axis"""
# FIXME: works a bit weird for rotated objects # FIXME: works a bit weird for rotated objects
def invoke(self, ctx, event): def invoke(self, ctx, event):
self.init_value = self.target_get_value('offset') / self.scale_value self.init_value = self.target_get_value("offset") / self.scale_value
coordz = self.project_mouse(ctx, event) coordz = self.project_mouse(ctx, event)
if coordz is None: if coordz is None:
return {'CANCELLED'} return {"CANCELLED"}
self.init_coordz = coordz self.init_coordz = coordz
return {'RUNNING_MODAL'} return {"RUNNING_MODAL"}
def modal(self, ctx, event, tweak): def modal(self, ctx, event, tweak):
coordz = self.project_mouse(ctx, event) coordz = self.project_mouse(ctx, event)
if coordz is None: if coordz is None:
return {'CANCELLED'} return {"CANCELLED"}
delta = coordz - self.init_coordz delta = coordz - self.init_coordz
if 'PRECISE' in tweak: if "PRECISE" in tweak:
delta /= 10.0 delta /= 10.0
value = max(0, self.init_value + delta) value = max(0, self.init_value + delta)
value *= self.scale_value value *= self.scale_value
# ctx.area.header_text_set(f"coords: {self.init_coordz} - {coordz}, delta: {delta}, value: {value}") # ctx.area.header_text_set(f"coords: {self.init_coordz} - {coordz}, delta: {delta}, value: {value}")
ctx.area.header_text_set(f"Depth: {value}") ctx.area.header_text_set(f"Depth: {value}")
self.target_set_value('offset', value) self.target_set_value("offset", value)
return {'RUNNING_MODAL'} return {"RUNNING_MODAL"}
def project_mouse(self, ctx, event): def project_mouse(self, ctx, event):
"""Projecting mouse coords to local axis Z""" """Projecting mouse coords to local axis Z"""
@@ -189,30 +308,29 @@ class OffsetHandle():
def exit(self, ctx, cancel): def exit(self, ctx, cancel):
if cancel: if cancel:
self.target_set_value('offset', self.init_value) self.target_set_value("offset", self.init_value)
else: else:
self.group.update(ctx) self.group.update(ctx)
class UglyDotGizmo(OffsetHandle, types.Gizmo): class UglyDotGizmo(OffsetHandle, types.Gizmo):
"""three orthogonal circles""" """three orthogonal circles"""
bl_idname = "BIM_GT_uglydot_3d" bl_idname = "BIM_GT_uglydot_3d"
bl_target_properties = ( bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
{'id': 'offset', 'type': 'FLOAT', 'array_length': 1},
)
__slots__ = ( __slots__ = (
'scale_value', "scale_value",
'custom_shape', "custom_shape",
'init_value', "init_value",
'init_coordz', "init_coordz",
) )
def setup(self): def setup(self):
self.custom_shape = self.new_custom_shape(type='TRIS', verts=X3DISC) self.custom_shape = self.new_custom_shape(type="TRIS", verts=X3DISC)
def refresh(self): def refresh(self):
offset = self.target_get_value('offset') / self.scale_value offset = self.target_get_value("offset") / self.scale_value
self.matrix_offset.col[3][2] = offset # z-shift self.matrix_offset.col[3][2] = offset # z-shift
def draw(self, ctx): def draw(self, ctx):
@@ -226,15 +344,14 @@ class UglyDotGizmo(OffsetHandle, types.Gizmo):
class DotGizmo(CustomGizmo, OffsetHandle, types.Gizmo): class DotGizmo(CustomGizmo, OffsetHandle, types.Gizmo):
"""Single dot viewport-aligned""" """Single dot viewport-aligned"""
# FIXME: make it selectable # FIXME: make it selectable
bl_idname = "BIM_GT_dot_2d" bl_idname = "BIM_GT_dot_2d"
bl_target_properties = ( bl_target_properties = ({"id": "offset", "type": "FLOAT", "array_length": 1},)
{'id': 'offset', 'type': 'FLOAT', 'array_length': 1},
)
__slots__ = ( __slots__ = (
'scale_value', "scale_value",
'custom_shape', "custom_shape",
) )
def setup(self): def setup(self):
@@ -243,7 +360,7 @@ class DotGizmo(CustomGizmo, OffsetHandle, types.Gizmo):
self.use_draw_scale = False self.use_draw_scale = False
def refresh(self): def refresh(self):
offset = self.target_get_value('offset') / self.scale_value offset = self.target_get_value("offset") / self.scale_value
self.matrix_offset.col[3][2] = offset # z-shifted self.matrix_offset.col[3][2] = offset # z-shifted
def draw(self, ctx): def draw(self, ctx):
@@ -265,15 +382,11 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
Noninteractive gizmo to indicate extrusion depth and planes. Noninteractive gizmo to indicate extrusion depth and planes.
Draws main segment and orthogonal cross at endpoints. Draws main segment and orthogonal cross at endpoints.
""" """
bl_idname = "BIM_GT_extrusion_guides"
bl_target_properties = (
{'id': 'depth', 'type': 'FLOAT', 'array_length': 1},
)
__slots__ = ( bl_idname = "BIM_GT_extrusion_guides"
'scale_value', bl_target_properties = ({"id": "depth", "type": "FLOAT", "array_length": 1},)
'custom_shape'
) __slots__ = ("scale_value", "custom_shape")
def setup(self): def setup(self):
shader = ExtrusionGuidesShader() shader = ExtrusionGuidesShader()
@@ -281,7 +394,7 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
self.use_draw_scale = False self.use_draw_scale = False
def refresh(self): def refresh(self):
depth = self.target_get_value('depth') / self.scale_value depth = self.target_get_value("depth") / self.scale_value
self.matrix_offset.col[2][2] = depth # z-scaled self.matrix_offset.col[2][2] = depth # z-scaled
def draw(self, ctx): def draw(self, ctx):
@@ -291,25 +404,22 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
class DimensionLabelGizmo(types.Gizmo): class DimensionLabelGizmo(types.Gizmo):
"""Text label for a dimension""" """Text label for a dimension"""
# does not work properly, fonts are totally screwed up # does not work properly, fonts are totally screwed up
bl_idname = "BIM_GT_dimension_label" bl_idname = "BIM_GT_dimension_label"
bl_target_properties = ( bl_target_properties = ({"id": "value", "type": "FLOAT", "array_length": 1},)
{'id': 'value', 'type': 'FLOAT', 'array_length': 1},
)
__slots__ = ( __slots__ = "text_label"
'text_label'
)
def setup(self): def setup(self):
pass pass
def refresh(self, ctx): def refresh(self, ctx):
value = self.target_get_value('value') value = self.target_get_value("value")
self.matrix_offset.col[3][2] = value * .5 self.matrix_offset.col[3][2] = value * 0.5
unit_system = ctx.scene.unit_settings.system unit_system = ctx.scene.unit_settings.system
self.text_label = bpy.utils.units.to_string(unit_system, 'LENGTH', value, 3, split_unit=False) self.text_label = bpy.utils.units.to_string(unit_system, "LENGTH", value, 3, split_unit=False)
def draw(self, ctx): def draw(self, ctx):
self.refresh(ctx) self.refresh(ctx)
@@ -337,41 +447,44 @@ class DimensionLabelGizmo(types.Gizmo):
class ExtrusionWidget(types.GizmoGroup): class ExtrusionWidget(types.GizmoGroup):
bl_idname = "bim.extrusion_widget" bl_idname = "bim.extrusion_widget"
bl_label = "Extrusion Gizmos" bl_label = "Extrusion Gizmos"
bl_space_type = 'VIEW_3D' bl_space_type = "VIEW_3D"
bl_region_type = 'WINDOW' bl_region_type = "WINDOW"
bl_options = {'3D', 'PERSISTENT', 'SHOW_MODAL_ALL'} bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
# FIXME: use proper scale from ifc value to blender units # FIXME: use proper scale from ifc value to blender units
@classmethod @classmethod
def poll(cls, ctx): def poll(cls, ctx):
obj = ctx.object obj = ctx.object
return (obj and obj.type == 'MESH' return (
and obj.data.BIMMeshProperties.ifc_parameters.get('IfcExtrudedAreaSolid/Depth') is not None) obj
and obj.type == "MESH"
and obj.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
)
def setup(self, ctx): def setup(self, ctx):
target = ctx.object target = ctx.object
prop = target.data.BIMMeshProperties.ifc_parameters.get('IfcExtrudedAreaSolid/Depth') prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
basis = target.matrix_world.normalized() basis = target.matrix_world.normalized()
theme = ctx.preferences.themes[0].user_interface theme = ctx.preferences.themes[0].user_interface
gz = self.handle = self.gizmos.new('BIM_GT_uglydot_3d') gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d")
gz.matrix_basis = basis gz.matrix_basis = basis
gz.scale_basis = 0.1 gz.scale_basis = 0.1
gz.color = gz.color_highlight = tuple(theme.gizmo_primary) gz.color = gz.color_highlight = tuple(theme.gizmo_primary)
gz.alpha = 0.5 gz.alpha = 0.5
gz.alpha_highlight = 1.0 gz.alpha_highlight = 1.0
gz.use_draw_modal = True gz.use_draw_modal = True
gz.target_set_prop('offset', prop, 'value') gz.target_set_prop("offset", prop, "value")
gz.scale_value = 1000 gz.scale_value = 1000
gz = self.guides = self.gizmos.new('BIM_GT_extrusion_guides') gz = self.guides = self.gizmos.new("BIM_GT_extrusion_guides")
gz.matrix_basis = basis gz.matrix_basis = basis
gz.color = gz.color_highlight = tuple(theme.gizmo_secondary) gz.color = gz.color_highlight = tuple(theme.gizmo_secondary)
gz.alpha = gz.alpha_highlight = 0.5 gz.alpha = gz.alpha_highlight = 0.5
gz.use_draw_modal = True gz.use_draw_modal = True
gz.target_set_prop('depth', prop, 'value') gz.target_set_prop("depth", prop, "value")
gz.scale_value = 1000 gz.scale_value = 1000
# gz = self.label = self.gizmos.new('GIZMO_GT_dimension_label') # gz = self.label = self.gizmos.new('GIZMO_GT_dimension_label')
@@ -395,6 +508,6 @@ class ExtrusionWidget(types.GizmoGroup):
# need to retrieve and rebind them again # need to retrieve and rebind them again
bpy.ops.bim.get_representation_ifc_parameters() bpy.ops.bim.get_representation_ifc_parameters()
target = ctx.object target = ctx.object
prop = target.data.BIMMeshProperties.ifc_parameters.get('IfcExtrudedAreaSolid/Depth') prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
self.handle.target_set_prop('offset', prop, 'value') self.handle.target_set_prop("offset", prop, "value")
self.guides.target_set_prop('depth', prop, 'value') self.guides.target_set_prop("depth", prop, "value")
@@ -0,0 +1,32 @@
import bpy
import blenderbim.bim.module.drawing.decoration as decoration
from bpy.app.handlers import persistent
@persistent
def toggleDecorationsOnLoad(*args):
toggle = bpy.context.scene.DocProperties.should_draw_decorations
if toggle:
decoration.DecorationsHandler.install(bpy.context)
else:
decoration.DecorationsHandler.uninstall()
@persistent
def depsgraph_update_pre_handler(scene):
set_active_camera_resolution(scene)
def set_active_camera_resolution(scene):
if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings:
return
if (
scene.render.resolution_x != scene.camera.data.BIMCameraProperties.raster_x
or scene.render.resolution_y != scene.camera.data.BIMCameraProperties.raster_y
):
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y
current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index]
if scene.camera != current_drawing.camera:
scene.DocProperties.current_drawing_index = scene.DocProperties.drawings.find(scene.camera.name.split("/")[1])
bpy.ops.bim.activate_view(drawing_index=scene.DocProperties.current_drawing_index)
@@ -0,0 +1,360 @@
import bpy
import math
import mathutils.geometry
from mathutils import Vector
# Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2
class BoundingEdge:
def __init__(self, v0, v1):
self.vertex = (v0, v1)
self.vector = v1 - v0
class BoundingFace:
def __init__(self, v0, v1, v2):
self.vertex = (v0, v1, v2)
self.normal = mathutils.geometry.normal(v0, v1, v2)
class BoundingBox:
def __init__(self, ob, vertex=None):
self.vertex = vertex or [ob.matrix_world @ Vector(v) for v in ob.bound_box]
if self.vertex != None:
self.edge = [
BoundingEdge(self.vertex[0], self.vertex[1]),
BoundingEdge(self.vertex[1], self.vertex[2]),
BoundingEdge(self.vertex[2], self.vertex[3]),
BoundingEdge(self.vertex[3], self.vertex[0]),
BoundingEdge(self.vertex[4], self.vertex[5]),
BoundingEdge(self.vertex[5], self.vertex[6]),
BoundingEdge(self.vertex[6], self.vertex[7]),
BoundingEdge(self.vertex[7], self.vertex[4]),
BoundingEdge(self.vertex[0], self.vertex[4]),
BoundingEdge(self.vertex[1], self.vertex[5]),
BoundingEdge(self.vertex[2], self.vertex[6]),
BoundingEdge(self.vertex[3], self.vertex[7]),
]
self.face = [
BoundingFace(self.vertex[0], self.vertex[1], self.vertex[3]),
BoundingFace(self.vertex[0], self.vertex[4], self.vertex[1]),
BoundingFace(self.vertex[0], self.vertex[3], self.vertex[4]),
BoundingFace(self.vertex[6], self.vertex[5], self.vertex[7]),
BoundingFace(self.vertex[6], self.vertex[7], self.vertex[2]),
BoundingFace(self.vertex[6], self.vertex[2], self.vertex[5]),
]
def whichSide(self, vtxs, normal, faceVtx):
retVal = 0
positive = 0
negative = 0
for v in vtxs:
t = normal.dot(v - faceVtx)
if t > 0:
positive = positive + 1
elif t < 0:
negative = negative + 1
if positive != 0 and negative != 0:
return 0
if positive != 0:
retVal = 1
else:
retVal = -1
return retVal
# Taken from: http://www.geometrictools.com/Documentation/MethodOfSeparatingAxes.pdf
def intersect(self, bb):
retVal = False
if self.vertex != None and bb.vertex != None:
# check all the faces of this object for a seperation axis
for i, f in enumerate(self.face):
d = f.normal
if self.whichSide(bb.vertex, d, f.vertex[0]) > 0:
return False # all the vertexes are on the +ve side of the face
# now do it again for the other objects faces
for i, f in enumerate(bb.face):
d = f.normal
if self.whichSide(self.vertex, d, f.vertex[0]) > 0:
return False # all the vertexes are on the +ve side of the face
# do edge checks
for e1 in self.edge:
for e2 in bb.edge:
d = e1.vector.cross(e2.vector)
side0 = self.whichSide(self.vertex, d, e1.vertex[0])
if side0 == 0:
continue
side1 = self.whichSide(bb.vertex, d, e1.vertex[0])
if side1 == 0:
continue
if (side0 * side1) < 0:
return False
retVal = True
return retVal
# This function stolen from https://github.com/kevancress/MeasureIt_ARCH/blob/dcf607ce0896aa2284463c6b4ae9cd023fc54cbe/measureit_arch_baseclass.py
# MeasureIt-ARCH is GPL-v3
# In the future I will need to rewrite this to allow the user to have custom
# settings for each annotation object, not read from Blender.
def format_distance(value, isArea=False, hide_units=True):
s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented)
# Get Scene Unit Settings
scaleFactor = bpy.context.scene.unit_settings.scale_length
unit_system = bpy.context.scene.unit_settings.system
unit_length = bpy.context.scene.unit_settings.length_unit
toInches = 39.3700787401574887
inPerFoot = 11.999
if isArea:
toInches = 1550
inPerFoot = 143.999
value *= scaleFactor
# Imperial Formating
if unit_system == "IMPERIAL":
precision = bpy.context.scene.BIMProperties.imperial_precision
if precision == "NONE":
precision = 256
elif precision == "1":
precision = 1
elif "/" in precision:
precision = int(precision.split("/")[1])
base = int(precision)
decInches = value * toInches
# Seperate ft and inches
# Unless Inches are the specified Length Unit
if unit_length != "INCHES":
feet = math.floor(decInches / inPerFoot)
decInches -= feet * inPerFoot
else:
feet = 0
# Seperate Fractional Inches
inches = math.floor(decInches)
if inches != 0:
frac = round(base * (decInches - inches))
else:
frac = round(base * (decInches))
# Set proper numerator and denominator
if frac != base:
numcycles = int(math.log2(base))
for i in range(numcycles):
if frac % 2 == 0:
frac = int(frac / 2)
base = int(base / 2)
else:
break
else:
frac = 0
inches += 1
# Check values and compose string
if inches == 12:
feet += 1
inches = 0
if not isArea:
tx_dist = ""
if feet:
tx_dist += str(feet) + "'"
if feet and inches:
tx_dist += " - "
if inches:
tx_dist += str(inches)
if inches and frac:
tx_dist += " "
if frac:
tx_dist += str(frac) + "/" + str(base)
if inches or frac:
tx_dist += '"'
else:
tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
# METRIC FORMATING
elif unit_system == "METRIC":
precision = bpy.context.scene.BIMProperties.metric_precision
if precision != 0:
value = precision * round(float(value) / precision)
# Meters
if unit_length == "METERS":
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
# Centimeters
elif unit_length == "CENTIMETERS":
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
# Millimeters
elif unit_length == "MILLIMETERS":
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
d_mm = value * (1000)
tx_dist = fmt % d_mm
# Otherwise Use Adaptive Units
else:
if round(value, 2) >= 1.0:
fmt = "%1.3f"
if hide_units is False:
fmt += " m"
tx_dist = fmt % value
else:
if round(value, 2) >= 0.01:
fmt = "%1.1f"
if hide_units is False:
fmt += " cm"
d_cm = value * (100)
tx_dist = fmt % d_cm
else:
fmt = "%1.0f"
if hide_units is False:
fmt += " mm"
d_mm = value * (1000)
tx_dist = fmt % d_mm
if isArea:
tx_dist += s_code
else:
tx_dist = fmt % value
return tx_dist
def get_active_drawing(scene):
"""Get active drawing collection and camera"""
props = scene.DocProperties
if props.active_drawing_index is None or len(props.drawings) == 0:
return None, None
try:
drawing = props.drawings[props.active_drawing_index]
return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera
except (KeyError, IndexError):
raise RuntimeError("missing drawing collection")
def get_project_collection(scene):
"""Get main project collection"""
colls = [c for c in scene.collection.children if c.name.startswith("IfcProject")]
if len(colls) != 1:
raise RuntimeError("project collection missing or not unique")
return colls[0]
def parse_diagram_scale(camera):
"""Returns numeric value of scale"""
if camera.BIMCameraProperties.diagram_scale == "CUSTOM":
_, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|")
else:
_, fraction = camera.BIMCameraProperties.diagram_scale.split("|")
numerator, denominator = fraction.split("/")
return float(numerator) / float(denominator)
def ortho_view_frame(camera, margin=0.015):
"""Calculates 2d bounding box of camera view area.
Similar to `bpy.types.Camera.view_frame`
:arg camera: camera of drawing
:type camera: bpy.types.Camera + BIMCameraProperties
:arg margin: margins, in scene units
:type margin: float
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x
size = camera.ortho_scale
hwidth = size * 0.5
hheight = size * 0.5 * aspect
scale = parse_diagram_scale(camera)
xmarg = margin * scale
ymarg = margin * scale * aspect
return (-hwidth + xmarg, hwidth - xmarg, -hheight + ymarg, hheight - ymarg, -camera.clip_start, -camera.clip_end)
def almost_zero(v):
return abs(v) < 1e-5
def clip_segment(bounds, segm):
"""Clipping line segment to bounds
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
# LiangBarsky algorithm
xmin, xmax, ymin, ymax, _, _ = bounds
p1, p2 = segm
def clip_side(p, q):
if almost_zero(p): # ~= 0, parallel to the side
if q < 0:
return None # outside
else:
return 0, 1 # inside
t = q / p # the intersection point
if p < 0: # entering
return t, 1
else: # leaving
return 0, t
dlt = p2 - p1
tt = (
clip_side(-dlt.x, p1.x - xmin), # left
clip_side(+dlt.x, xmax - p1.x), # right
clip_side(-dlt.y, p1.y - ymin), # bottom
clip_side(+dlt.y, ymax - p1.y), # top
)
if None in tt:
return None
t1 = max(0, max(t[0] for t in tt))
t2 = min(1, min(t[1] for t in tt))
if t1 >= t2:
return None
p1c = p1 + dlt * t1
p2c = p1 + dlt * t2
return p1c, p2c
def elevate_segment(bounds, segm):
"""Elevate line xy-perpendicular segment vertically
:arg bounds: (xmin, xmax, ymin, ymax)
:arg segm: 2 vertices of the segment
:return: 2 new vertices of segment or None if segment outside the bounding box
"""
_, _, ymin, ymax, zmin, _ = bounds
p1, p2 = segm
dlt = p2 - p1
if not (almost_zero(dlt.x) and almost_zero(dlt.y)):
return None
x = p1.x
return [Vector((x, ymin, zmin)), Vector((x, ymax, zmin))]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,282 @@
import os
import bpy
import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.decoration as decoration
from pathlib import Path
from blenderbim.bim.prop import Attribute, StrProperty
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
diagram_scales_enum = []
titleblocks_enum = []
sheets_enum = []
vector_styles_enum = []
def purge():
global diagram_scales_enum
global titleblocks_enum
global sheets_enum
global vector_styles_enum
diagram_scales_enum = []
titleblocks_enum = []
sheets_enum = []
vector_styles_enum = []
def getDiagramScales(self, context):
global diagram_scales_enum
if (
len(diagram_scales_enum) < 1
or (bpy.context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
or (bpy.context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31)
):
if bpy.context.scene.unit_settings.system == "IMPERIAL":
diagram_scales_enum = [
("CUSTOM", "Custom", ""),
("1'=1'-0\"|1/1", "1'=1'-0\"", ""),
('6"=1\'-0"|1/6', '6"=1\'-0"', ""),
('1-1/2"=1\'-0"|1/8', '1-1/2"=1\'-0"', ""),
('1"=1\'-0"|1/12', '1"=1\'-0"', ""),
('3/4"=1\'-0"|1/16', '3/4"=1\'-0"', ""),
('1/2"=1\'-0"|1/24', '1/2"=1\'-0"', ""),
('3/8"=1\'-0"|1/32', '3/8"=1\'-0"', ""),
('1/4"=1\'-0"|1/48', '1/4"=1\'-0"', ""),
('3/16"=1\'-0"|1/64', '3/16"=1\'-0"', ""),
('1/8"=1\'-0"|1/96', '1/8"=1\'-0"', ""),
('3/32"=1\'-0"|1/128', '3/32"=1\'-0"', ""),
('1/16"=1\'-0"|1/192', '1/16"=1\'-0"', ""),
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
("1\"=10'|1/120", "1\"=10'", ""),
("1\"=20'|1/240", "1\"=20'", ""),
("1\"=30'|1/360", "1\"=30'", ""),
("1\"=40'|1/480", "1\"=40'", ""),
("1\"=50'|1/600", "1\"=50'", ""),
("1\"=60'|1/720", "1\"=60'", ""),
("1\"=70'|1/840", "1\"=70'", ""),
("1\"=80'|1/960", "1\"=80'", ""),
("1\"=90'|1/1080", "1\"=90'", ""),
("1\"=100'|1/1200", "1\"=100'", ""),
("1\"=150'|1/1800", "1\"=150'", ""),
("1\"=200'|1/2400", "1\"=200'", ""),
("1\"=300'|1/3600", "1\"=300'", ""),
("1\"=400'|1/4800", "1\"=400'", ""),
("1\"=500'|1/6000", "1\"=500'", ""),
]
else:
diagram_scales_enum = [
("CUSTOM", "Custom", ""),
("1:5000|1/5000", "1:5000", ""),
("1:2000|1/2000", "1:2000", ""),
("1:1000|1/1000", "1:1000", ""),
("1:500|1/500", "1:500", ""),
("1:200|1/200", "1:200", ""),
("1:100|1/100", "1:100", ""),
("1:50|1/50", "1:50", ""),
("1:20|1/20", "1:20", ""),
("1:10|1/10", "1:10", ""),
("1:5|1/5", "1:5", ""),
("1:2|1/2", "1:2", ""),
("1:1|1/1", "1:1", ""),
]
return diagram_scales_enum
def updateDrawingName(self, context):
if not self.camera:
return
if self.camera.name == self.name:
return
self.camera.name = "IfcAnnotation/{}".format(self.name)
unique_name = "/".join(self.camera.name.split("/")[1:])
self.camera.users_collection[0].name = "IfcGroup/{}".format(unique_name)
if self.name != unique_name:
self.name = unique_name
def refreshActiveDrawingIndex(self, context):
bpy.ops.bim.activate_view(drawing_index=context.scene.DocProperties.active_drawing_index)
def getTitleblocks(self, context):
global titleblocks_enum
if len(titleblocks_enum) < 1:
titleblocks_enum.clear()
for filename in Path(os.path.join(context.scene.BIMProperties.data_dir, "templates", "titleblocks")).glob(
"*.svg"
):
f = str(filename.stem)
titleblocks_enum.append((f, f, ""))
return titleblocks_enum
def refreshTitleblocks(self, context):
global titleblocks_enum
titleblocks_enum.clear()
getTitleblocks(self, context)
def toggleDecorations(self, context):
toggle = self.should_draw_decorations
if toggle:
decoration.DecorationsHandler.install(context)
else:
decoration.DecorationsHandler.uninstall()
def getVectorStyles(self, context):
global vector_styles_enum
if len(vector_styles_enum) < 1:
sheets_enum.clear()
for filename in Path(os.path.join(context.scene.BIMProperties.data_dir, "styles")).glob("*.css"):
f = str(filename.stem)
vector_styles_enum.append((f, f, ""))
return vector_styles_enum
def refreshFontSize(self, context):
annotation.Annotator.resize_text(context.active_object)
class Variable(PropertyGroup):
name: StringProperty(name="Name")
prop_key: StringProperty(name="Property Key")
class Drawing(PropertyGroup):
name: StringProperty(name="Name", update=updateDrawingName)
camera: PointerProperty(name="Camera", type=bpy.types.Object)
class Schedule(PropertyGroup):
name: StringProperty(name="Name")
file: StringProperty(name="File")
class Sheet(PropertyGroup):
def set_name(self, new):
old = self.get("name")
path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets")
if old and os.path.isfile(os.path.join(path, old + ".svg")):
os.rename(os.path.join(path, old + ".svg"), os.path.join(path, new + ".svg"))
self["name"] = new
def get_name(self):
return self.get("name")
name: StringProperty(name="Name", get=get_name, set=set_name)
drawings: CollectionProperty(name="Drawings", type=Drawing)
active_drawing_index: IntProperty(name="Active Drawing Index")
class DrawingStyle(PropertyGroup):
name: StringProperty(name="Name")
raster_style: StringProperty(name="Raster Style")
render_type: EnumProperty(
items=[
("NONE", "None", ""),
("DEFAULT", "Default", ""),
("VIEWPORT", "Viewport", ""),
],
name="Render Type",
default="VIEWPORT",
)
vector_style: EnumProperty(items=getVectorStyles, name="Vector Style")
include_query: StringProperty(name="Include Query")
exclude_query: StringProperty(name="Exclude Query")
attributes: CollectionProperty(name="Attributes", type=StrProperty)
class DocProperties(PropertyGroup):
has_underlay: BoolProperty(name="Underlay", default=False)
has_linework: BoolProperty(name="Linework", default=True)
has_annotation: BoolProperty(name="Annotation", default=True)
should_use_underlay_cache: BoolProperty(name="Use Underlay Cache", default=False)
should_use_linework_cache: BoolProperty(name="Use Linework Cache", default=False)
should_use_annotation_cache: BoolProperty(name="Use Annotation Cache", default=False)
should_extract: BoolProperty(name="Should Extract", default=True)
drawings: CollectionProperty(name="Drawings", type=Drawing)
active_drawing_index: IntProperty(name="Active Drawing Index", update=refreshActiveDrawingIndex)
current_drawing_index: IntProperty(name="Current Drawing Index")
schedules: CollectionProperty(name="Schedules", type=Schedule)
active_schedule_index: IntProperty(name="Active Schedule Index")
titleblock: EnumProperty(items=getTitleblocks, name="Titleblock", update=refreshTitleblocks)
sheets: CollectionProperty(name="Sheets", type=Sheet)
active_sheet_index: IntProperty(name="Active Sheet Index")
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
decorations_colour: FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4
)
class BIMCameraProperties(PropertyGroup):
view_name: StringProperty(name="View Name")
target_view: EnumProperty(
items=[
("PLAN_VIEW", "PLAN_VIEW", ""),
("ELEVATION_VIEW", "ELEVATION_VIEW", ""),
("SECTION_VIEW", "SECTION_VIEW", ""),
("REFLECTED_PLAN_VIEW", "REFLECTED_PLAN_VIEW", ""),
("MODEL_VIEW", "MODEL_VIEW", ""),
],
name="Target View",
default="PLAN_VIEW",
)
diagram_scale: EnumProperty(items=getDiagramScales, name="Drawing Scale")
custom_diagram_scale: StringProperty(name="Custom Scale")
raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000)
is_nts: BoolProperty(name="Is NTS")
cut_objects: EnumProperty(
items=[
(
".IfcWall|.IfcSlab|.IfcCurtainWall|.IfcStair|.IfcStairFlight|.IfcColumn|.IfcBeam|.IfcMember|.IfcCovering|.IfcSpace",
"Overall Plan / Section",
"",
),
(".IfcElement", "Detail Drawing", ""),
("CUSTOM", "Custom", ""),
],
name="Cut Objects",
)
cut_objects_custom: StringProperty(name="Custom Cut")
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
class BIMTextProperties(PropertyGroup):
font_size: EnumProperty(
items=[
("1.8", "1.8 - Small", ""),
("2.5", "2.5 - Regular", ""),
("3.5", "3.5 - Large", ""),
("5.0", "5.0 - Header", ""),
("7.0", "7.0 - Title", ""),
],
update=refreshFontSize,
name="Font Size",
)
symbol: EnumProperty(
items=[
("None", "None", ""),
("rectangle-tag", "Rectangle Tag", ""),
("door-tag", "Door Tag", ""),
],
update=refreshFontSize,
name="Symbol",
)
related_element: PointerProperty(name="Related Element", type=bpy.types.Object)
variables: CollectionProperty(name="Variables", type=Variable)
@@ -1,10 +1,10 @@
from mathutils import Matrix
import bgl import bgl
from mathutils import Matrix
from gpu.types import GPUShader from gpu.types import GPUShader
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
class BaseShader(): class BaseShader:
"""Wrapepr for GPUShader """Wrapepr for GPUShader
To use for viewport decorations with geometry generated on GPU side. To use for viewport decorations with geometry generated on GPU side.
@@ -85,14 +85,15 @@ class BaseShader():
def __init__(self): def __init__(self):
# NB: libcode arg doesn't work # NB: libcode arg doesn't work
self.prog = GPUShader(vertexcode=self.VERT_GLSL, self.prog = GPUShader(
vertexcode=self.VERT_GLSL,
fragcode=self.FRAG_GLSL, fragcode=self.FRAG_GLSL,
geocode=self.LIB_GLSL + self.GEOM_GLSL, geocode=self.LIB_GLSL + self.GEOM_GLSL,
defines=self.DEF_GLSL) defines=self.DEF_GLSL,
)
def batch(self, indices=None, **data): def batch(self, indices=None, **data):
"""Returns automatic GPUBatch filled with provided parameters """Returns automatic GPUBatch filled with provided parameters"""
"""
batch = batch_for_shader(self.prog, self.TYPE, data, indices=indices) batch = batch_for_shader(self.prog, self.TYPE, data, indices=indices)
batch.program_set(self.prog) batch.program_set(self.prog)
return batch return batch
@@ -113,23 +114,26 @@ class BaseShader():
region = ctx.region region = ctx.region
region3d = ctx.region_data region3d = ctx.region_data
try: try:
self.prog.uniform_float('viewMatrix', region3d.perspective_matrix) self.prog.uniform_float("viewMatrix", region3d.perspective_matrix)
except ValueError: # unused uniform except ValueError: # unused uniform
pass pass
try: try:
self.prog.uniform_float('winSize', (region.width / 2, region.height / 2)) self.prog.uniform_float("winSize", (region.width / 2, region.height / 2))
except ValueError: # unused uniform except ValueError: # unused uniform
pass pass
class BaseLinesShader(BaseShader): class BaseLinesShader(BaseShader):
"""Draws line segments with gaps around vertices at endpoints """Draws line segments with gaps around vertices at endpoints"""
"""
TYPE = 'LINES'
DEF_GLSL = BaseShader.DEF_GLSL + """ TYPE = "LINES"
DEF_GLSL = (
BaseShader.DEF_GLSL
+ """
#define GAP_SIZE {gap_size} #define GAP_SIZE {gap_size}
""" """
)
GEOM_GLSL = """ GEOM_GLSL = """
layout(lines) in; layout(lines) in;
@@ -171,6 +175,7 @@ class GizmoShader(BaseShader):
Scaling to match viewport is partially controlled by user preferences and gizmo code. Scaling to match viewport is partially controlled by user preferences and gizmo code.
""" """
# TODO: add some magic to respect gizmo settings/params # TODO: add some magic to respect gizmo settings/params
VERT_GLSL = """ VERT_GLSL = """
@@ -187,12 +192,15 @@ class GizmoShader(BaseShader):
class DotsGizmoShader(GizmoShader): class DotsGizmoShader(GizmoShader):
"""Draws circles of radius 1 around points""" """Draws circles of radius 1 around points"""
TYPE = 'POINTS' TYPE = "POINTS"
DEF_GLSL = BaseShader.DEF_GLSL + """ DEF_GLSL = (
BaseShader.DEF_GLSL
+ """
#define CIRCLE_SEGMENTS 12 #define CIRCLE_SEGMENTS 12
#define CIRCLE_RADIUS 8 #define CIRCLE_RADIUS 8
""" """
)
GEOM_GLSL = """ GEOM_GLSL = """
layout(points) in; layout(points) in;
@@ -235,11 +243,14 @@ class DotsGizmoShader(GizmoShader):
class ExtrusionGuidesShader(GizmoShader): class ExtrusionGuidesShader(GizmoShader):
"""Draws lines and add cross in XY plane at endpoints""" """Draws lines and add cross in XY plane at endpoints"""
TYPE = 'LINES' TYPE = "LINES"
DEF_GLSL = BaseShader.DEF_GLSL + """ DEF_GLSL = (
BaseShader.DEF_GLSL
+ """
#define CROSS_SIZE .5 #define CROSS_SIZE .5
""" """
)
GEOM_GLSL = """ GEOM_GLSL = """
uniform mat4 ModelViewProjectionMatrix; uniform mat4 ModelViewProjectionMatrix;
@@ -13,7 +13,7 @@ class SheetBuilder:
self.scale = "NTS" self.scale = "NTS"
def create(self, name, titleblock_name): def create(self, name, titleblock_name):
sheet_path = "{}sheets/{}.svg".format(self.data_dir, name) sheet_path = os.path.join(self.data_dir, f"{name}.svg")
root = ET.Element("svg") root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg" root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink" root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
@@ -121,9 +121,9 @@ class SheetBuilder:
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height"))) title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet_name): def build(self, sheet_name):
os.makedirs("{}build/{}/".format(self.data_dir, sheet_name), exist_ok=True) os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
sheet_path = "{}sheets/{}.svg".format(self.data_dir, sheet_name) sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
ET.register_namespace("", "http://www.w3.org/2000/svg") ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink") ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
@@ -140,7 +140,7 @@ class SheetBuilder:
self.build_drawings(root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'), sheet_name) self.build_drawings(root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'), sheet_name)
self.build_schedules(root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')) self.build_schedules(root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'))
with open("{}build/{}/{}.svg".format(self.data_dir, sheet_name, sheet_name), "wb") as output: with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
tree.write(output) tree.write(output)
def build_drawings(self, drawings, sheet_name): def build_drawings(self, drawings, sheet_name):
@@ -155,8 +155,9 @@ class SheetBuilder:
view.append(self.parse_embedded_svg(foreground, {})) view.append(self.parse_embedded_svg(foreground, {}))
# Add background # Add background
background_path = "{}sheets/{}".format(self.data_dir, self.get_href(background)) background_path = os.path.join(self.data_dir, "sheets", self.get_href(background))
copy(background_path, "{}build/{}/".format(self.data_dir, sheet_name))
copy(background_path, os.path.join(self.data_dir, "build", sheet_name))
# Add view title # Add view title
foreground_path = self.get_href(foreground) foreground_path = self.get_href(foreground)
@@ -202,7 +203,7 @@ class SheetBuilder:
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y")) self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
) )
svg_path = self.get_href(image) svg_path = self.get_href(image)
with open("{}sheets/{}".format(self.data_dir, svg_path), "r") as template: with open(os.path.join(self.data_dir, "sheets", svg_path), "r") as template:
embedded = ET.fromstring(pystache.render(template.read(), data)) embedded = ET.fromstring(pystache.render(template.read(), data))
# viewBox should not be nested # viewBox should not be nested
embedded.attrib["viewBox"] = "" embedded.attrib["viewBox"] = ""
@@ -6,8 +6,8 @@ import pystache
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import svgwrite import svgwrite
import ifcopenshell import ifcopenshell
from . import annotation import blenderbim.bim.module.drawing.helper as helper
from . import helper import blenderbim.bim.module.drawing.annotation as annotation
from mathutils import Vector from mathutils import Vector
from mathutils import geometry from mathutils import geometry
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
@@ -39,14 +39,17 @@ class External(svgwrite.container.Group):
class SvgWriter: class SvgWriter:
def __init__(self, ifc_cutter): def __init__(self):
self.ifc_cutter = ifc_cutter self.output = "out.svg"
self.data_dir = None
self.vector_style = None
self.human_scale = "NTS" self.human_scale = "NTS"
self.annotations = {}
self.background_image = None
self.scale = 1 / 100 # 1:100 self.scale = 1 / 100 # 1:100
def write(self): def write(self, layer):
self.calculate_scale() self.calculate_scale()
self.output = os.path.join(self.ifc_cutter.data_dir, "diagrams", self.ifc_cutter.diagram_name + ".svg")
self.svg = svgwrite.Drawing( self.svg = svgwrite.Drawing(
self.output, self.output,
debug=False, debug=False,
@@ -56,41 +59,43 @@ class SvgWriter:
data_scale=self.human_scale, data_scale=self.human_scale,
) )
if layer == "underlay":
self.draw_background_image()
elif layer == "annotation":
self.add_stylesheet() self.add_stylesheet()
self.add_markers() self.add_markers()
self.add_symbols() self.add_symbols()
self.add_patterns() self.add_patterns()
self.draw_background_image() # self.draw_background_elements()
self.draw_background_elements() # self.draw_cut_polygons()
self.draw_cut_polygons()
self.draw_annotations() self.draw_annotations()
self.svg.save(pretty=True) self.svg.save(pretty=True)
def calculate_scale(self): def calculate_scale(self):
self.scale *= 1000 # IFC is in meters, SVG is in mm self.scale *= 1000 # IFC is in meters, SVG is in mm
self.raw_width = self.ifc_cutter.section_box["x"] self.raw_width = self.camera_width
self.raw_height = self.ifc_cutter.section_box["y"] self.raw_height = self.camera_height
self.width = self.raw_width * self.scale self.width = self.raw_width * self.scale
self.height = self.raw_height * self.scale self.height = self.raw_height * self.scale
def add_stylesheet(self): def add_stylesheet(self):
with open("{}styles/{}.css".format(self.ifc_cutter.data_dir, self.ifc_cutter.vector_style), "r") as stylesheet: with open(os.path.join(self.data_dir, "styles", f"{self.vector_style}.css"), "r") as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read())) self.svg.defs.add(self.svg.style(stylesheet.read()))
def add_markers(self): def add_markers(self):
tree = ET.parse("{}templates/markers.svg".format(self.ifc_cutter.data_dir)) tree = ET.parse(os.path.join(self.data_dir, "templates", "markers.svg"))
root = tree.getroot() root = tree.getroot()
for child in root.getchildren(): for child in root.getchildren():
self.svg.defs.add(External(child)) self.svg.defs.add(External(child))
def add_symbols(self): def add_symbols(self):
tree = ET.parse("{}templates/symbols.svg".format(self.ifc_cutter.data_dir)) tree = ET.parse(os.path.join(self.data_dir, "templates", "symbols.svg"))
root = tree.getroot() root = tree.getroot()
for child in root.getchildren(): for child in root.getchildren():
self.svg.defs.add(External(child)) self.svg.defs.add(External(child))
def add_patterns(self): def add_patterns(self):
tree = ET.parse("{}templates/patterns.svg".format(self.ifc_cutter.data_dir)) tree = ET.parse(os.path.join(self.data_dir, "templates", "patterns.svg"))
root = tree.getroot() root = tree.getroot()
for child in root.getchildren(): for child in root.getchildren():
self.svg.defs.add(External(child)) self.svg.defs.add(External(child))
@@ -98,12 +103,13 @@ class SvgWriter:
def draw_background_image(self): def draw_background_image(self):
self.svg.add( self.svg.add(
self.svg.image( self.svg.image(
os.path.join("..", "diagrams", os.path.basename(self.ifc_cutter.background_image)), os.path.join("..", "diagrams", os.path.basename(self.background_image)),
**{"width": self.width, "height": self.height} **{"width": self.width, "height": self.height}
) )
) )
def draw_background_elements(self): def draw_background_elements(self):
return # TODO purge?
for element in self.ifc_cutter.background_elements: for element in self.ifc_cutter.background_elements:
if element["type"] == "polygon": if element["type"] == "polygon":
self.draw_polygon(element, "background") self.draw_polygon(element, "background")
@@ -116,16 +122,16 @@ class SvgWriter:
x_offset = self.raw_width / 2 x_offset = self.raw_width / 2
y_offset = self.raw_height / 2 y_offset = self.raw_height / 2
for obj in self.ifc_cutter.equal_objs: for obj in self.annotations.get("equal_objs", []):
self.draw_dimension_annotations(obj, text_override="EQ") self.draw_dimension_annotations(obj, text_override="EQ")
for obj in self.ifc_cutter.dimension_objs: for obj in self.annotations.get("dimension_objs", []):
self.draw_dimension_annotations(obj) self.draw_dimension_annotations(obj)
self.draw_measureit_arch_dimension_annotations() self.draw_measureit_arch_dimension_annotations()
if self.ifc_cutter.break_obj: if self.annotations.get("break_obj"):
self.draw_break_annotations(self.ifc_cutter.break_obj) self.draw_break_annotations(self.annotations["break_obj"])
for grid_obj in self.ifc_cutter.grid_objs: for grid_obj in self.annotations.get("grid_objs", []):
matrix_world = grid_obj.matrix_world matrix_world = grid_obj.matrix_world
for edge in grid_obj.data.edges: for edge in grid_obj.data.edges:
classes = ["annotation", "grid"] classes = ["annotation", "grid"]
@@ -174,21 +180,21 @@ class SvgWriter:
self.draw_ifc_annotation() self.draw_ifc_annotation()
for obj in self.ifc_cutter.misc_objs: for obj in self.annotations.get("misc_objs", []):
self.draw_misc_annotation(obj, ["IfcAnnotation"]) self.draw_misc_annotation(obj, ["IfcAnnotation"])
for obj_data in self.ifc_cutter.hidden_objs: for obj_data in self.annotations.get("hidden_objs", []):
self.draw_line_annotation(obj_data, ["hidden"]) self.draw_line_annotation(obj_data, ["hidden"])
for obj_data in self.ifc_cutter.solid_objs: for obj_data in self.annotations.get("solid_objs", []):
self.draw_line_annotation(obj_data, ["solid"]) self.draw_line_annotation(obj_data, ["solid"])
if self.ifc_cutter.leader_obj: if self.annotations.get("leader_obj"):
self.draw_line_annotation(self.ifc_cutter.leader_obj, ["leader"]) self.draw_line_annotation(self.annotations["leader_obj"], ["leader"])
if self.ifc_cutter.plan_level_obj: if self.annotations.get("plan_level_obj"):
matrix_world = self.ifc_cutter.plan_level_obj.matrix_world matrix_world = self.annotations["plan_level_obj"].matrix_world
for spline in self.ifc_cutter.plan_level_obj.data.splines: for spline in self.annotations["plan_level_obj"].data.splines:
classes = ["annotation", "plan-level"] classes = ["annotation", "plan-level"]
points = self.get_spline_points(spline) points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points] projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -208,7 +214,7 @@ class SvgWriter:
) )
) )
# TODO: allow metric to be configurable # TODO: allow metric to be configurable
rl = ((matrix_world @ points[0].co).xyz + self.ifc_cutter.plan_level_obj.location).z rl = ((matrix_world @ points[0].co).xyz + self.annotations["plan_level_obj"].location).z
if bpy.context.scene.unit_settings.system == "IMPERIAL": if bpy.context.scene.unit_settings.system == "IMPERIAL":
rl = helper.format_distance(rl) rl = helper.format_distance(rl)
else: else:
@@ -231,9 +237,9 @@ class SvgWriter:
) )
) )
if self.ifc_cutter.section_level_obj: if self.annotations.get("section_level_obj"):
matrix_world = self.ifc_cutter.section_level_obj.matrix_world matrix_world = self.annotations["section_level_obj"].matrix_world
for spline in self.ifc_cutter.section_level_obj.data.splines: for spline in self.annotations["section_level_obj"].data.splines:
classes = ["annotation", "section-level"] classes = ["annotation", "section-level"]
points = self.get_spline_points(spline) points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points] projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -273,9 +279,9 @@ class SvgWriter:
) )
) )
if self.ifc_cutter.stair_obj: if self.annotations.get("stair_obj"):
matrix_world = self.ifc_cutter.stair_obj.matrix_world matrix_world = self.annotations["stair_obj"].matrix_world
for spline in self.ifc_cutter.stair_obj.data.splines: for spline in self.annotations["stair_obj"].data.splines:
classes = ["annotation", "stair"] classes = ["annotation", "stair"]
points = self.get_spline_points(spline) points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points] projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
@@ -308,7 +314,7 @@ class SvgWriter:
def draw_ifc_annotation(self): def draw_ifc_annotation(self):
x_offset = self.raw_width / 2 x_offset = self.raw_width / 2
y_offset = self.raw_height / 2 y_offset = self.raw_height / 2
for annotation in self.ifc_cutter.annotation_objs: for annotation in self.annotations.get("annotation_objs", []):
for edge in annotation["edges"]: for edge in annotation["edges"]:
v0_global = annotation["vertices"][edge[0]] v0_global = annotation["vertices"][edge[0]]
v1_global = annotation["vertices"][edge[1]] v1_global = annotation["vertices"][edge[1]]
@@ -357,7 +363,7 @@ class SvgWriter:
classes.append("material-{}".format(re.sub("[^0-9a-zA-Z]+", "", slot.material.name))) classes.append("material-{}".format(re.sub("[^0-9a-zA-Z]+", "", slot.material.name)))
global_id = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId global_id = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId
classes.append("globalid-{}".format(global_id)) classes.append("globalid-{}".format(global_id))
for attribute in self.ifc_cutter.attributes: for attribute in self.annotations.get("attributes", []):
result = self.get_obj_value(obj, attribute) result = self.get_obj_value(obj, attribute)
if result: if result:
classes.append( classes.append(
@@ -435,7 +441,7 @@ class SvgWriter:
x_offset = self.raw_width / 2 x_offset = self.raw_width / 2
y_offset = self.raw_height / 2 y_offset = self.raw_height / 2
for text_obj in self.ifc_cutter.text_objs: for text_obj in self.annotations.get("text_objs", []):
text_position = self.project_point_onto_camera(text_obj.location) text_position = self.project_point_onto_camera(text_obj.location)
text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y))) text_position = Vector(((x_offset + text_position.x), (y_offset - text_position.y)))
@@ -473,8 +479,8 @@ class SvgWriter:
alignment_baseline = "baseline" alignment_baseline = "baseline"
text_body = text_obj.data.body text_body = text_obj.data.body
if text_obj.name in self.ifc_cutter.template_variables: if text_obj.name in self.annotations.get("template_variables", {}):
text_body = pystache.render(text_body, self.ifc_cutter.template_variables[text_obj.name]) text_body = pystache.render(text_body, self.annotations["template_variables"][text_obj.name])
for line_number, text_line in enumerate(text_body.split("\n")): for line_number, text_line in enumerate(text_body.split("\n")):
self.svg.add( self.svg.add(
@@ -587,17 +593,18 @@ class SvgWriter:
) )
def project_point_onto_camera(self, point): def project_point_onto_camera(self, point):
return self.ifc_cutter.camera_obj.matrix_world.inverted() @ geometry.intersect_line_plane( return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
point.xyz, point.xyz,
point.xyz - Vector(self.ifc_cutter.section_box["projection"]), point.xyz - Vector(self.camera_projection),
self.ifc_cutter.camera_obj.location, self.camera.location,
Vector(self.ifc_cutter.section_box["projection"]), Vector(self.camera_projection),
) )
def get_spline_points(self, spline): def get_spline_points(self, spline):
return spline.bezier_points if spline.bezier_points else spline.points return spline.bezier_points if spline.bezier_points else spline.points
def draw_cut_polygons(self): def draw_cut_polygons(self):
return # deprecate?
for polygon in self.ifc_cutter.cut_polygons: for polygon in self.ifc_cutter.cut_polygons:
self.draw_polygon(polygon, "cut") self.draw_polygon(polygon, "cut")
@@ -27,12 +27,17 @@ class BIM_PT_camera(Panel):
dprops = bpy.context.scene.DocProperties dprops = bpy.context.scene.DocProperties
props = context.active_object.data.BIMCameraProperties props = context.active_object.data.BIMCameraProperties
layout.label(text="Generation Options:") col = layout.column(align=True)
row = col.row(align=True)
row.prop(dprops, "has_underlay", icon="OUTLINER_OB_IMAGE")
row.prop(dprops, "should_use_underlay_cache", text="", icon="FILE_REFRESH")
row = col.row(align=True)
row.prop(dprops, "has_linework", icon="IMAGE_DATA")
row.prop(dprops, "should_use_linework_cache", text="", icon="FILE_REFRESH")
row = col.row(align=True)
row.prop(dprops, "has_annotation", icon="MOD_EDGESPLIT")
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = layout.row()
row.prop(dprops, "should_recut")
row = layout.row()
row.prop(dprops, "should_recut_selected")
row = layout.row() row = layout.row()
row.prop(dprops, "should_extract") row.prop(dprops, "should_extract")
@@ -64,7 +69,31 @@ class BIM_PT_camera(Panel):
row = layout.row() row = layout.row()
row.prop(props, "custom_diagram_scale") row.prop(props, "custom_diagram_scale")
layout.label(text="Drawing Styles:") row = layout.row(align=True)
row.operator("bim.create_drawing", text="Create Drawing", icon="OUTPUT")
op = row.operator("bim.open_view", icon="URL", text="")
op.view = context.active_object.name.split("/")[1]
class BIM_PT_drawing_underlay(Panel):
bl_label = "Drawing Underlay"
bl_idname = "BIM_PT_drawing_underlay"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "data"
bl_parent_id = "BIM_PT_camera"
@classmethod
def poll(cls, context):
engine = context.engine
return context.camera and hasattr(context.active_object.data, "BIMCameraProperties")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
dprops = bpy.context.scene.DocProperties
props = context.active_object.data.BIMCameraProperties
row = layout.row(align=True) row = layout.row(align=True)
row.operator("bim.add_drawing_style") row.operator("bim.add_drawing_style")
@@ -101,8 +130,214 @@ class BIM_PT_camera(Panel):
row.operator("bim.save_drawing_style") row.operator("bim.save_drawing_style")
row.operator("bim.activate_drawing_style") row.operator("bim.activate_drawing_style")
class BIM_PT_drawings(Panel):
bl_label = "SVG Drawings"
bl_idname = "BIM_PT_drawings"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "output"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
props = bpy.context.scene.DocProperties
row = layout.row(align=True) row = layout.row(align=True)
row.operator("bim.cut_section", text="Create Drawing") row.operator("bim.add_drawing")
row.operator("bim.create_drawing", text="Create Drawing 2.0") row.operator("bim.refresh_drawing_list", icon="FILE_REFRESH", text="")
if props.drawings:
if props.active_drawing_index < len(props.drawings):
op = row.operator("bim.open_view", icon="URL", text="") op = row.operator("bim.open_view", icon="URL", text="")
op.view = context.active_object.name.split("/")[1] op.view = props.drawings[props.active_drawing_index].name
row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index
layout.template_list("BIM_UL_generic", "", props, "drawings", props, "active_drawing_index")
row = layout.row()
row.operator("bim.add_ifc_file")
for index, ifc_file in enumerate(props.ifc_files):
row = layout.row(align=True)
row.prop(ifc_file, "name", text="IFC #{}".format(index + 1))
row.operator("bim.select_doc_ifc_file", icon="FILE_FOLDER", text="").index = index
row.operator("bim.remove_ifc_file", icon="X", text="").index = index
class BIM_PT_schedules(Panel):
bl_label = "ODS Schedules"
bl_idname = "BIM_PT_schedules"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "output"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
props = bpy.context.scene.DocProperties
row = layout.row(align=True)
row.operator("bim.add_schedule")
if props.schedules:
row.operator("bim.build_schedule", icon="LINENUMBERS_ON", text="")
row.operator("bim.remove_schedule", icon="X", text="").index = props.active_schedule_index
layout.template_list("BIM_UL_generic", "", props, "schedules", props, "active_schedule_index")
row = layout.row()
row.prop(props.schedules[props.active_schedule_index], "file")
row.operator("bim.select_schedule_file", icon="FILE_FOLDER", text="")
class BIM_PT_sheets(Panel):
bl_label = "SVG Sheets"
bl_idname = "BIM_PT_sheets"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "output"
def draw(self, context):
layout = self.layout
props = bpy.context.scene.DocProperties
row = layout.row(align=True)
row.prop(props, "titleblock", text="")
row.operator("bim.add_sheet")
if props.sheets:
row.operator("bim.open_sheet", icon="URL", text="")
row.operator("bim.remove_sheet", icon="X", text="").index = props.active_sheet_index
layout.template_list("BIM_UL_generic", "", props, "sheets", props, "active_sheet_index")
row = layout.row(align=True)
row.operator("bim.add_drawing_to_sheet")
row.operator("bim.add_schedule_to_sheet")
row = layout.row()
row.operator("bim.create_sheets")
class BIM_PT_text(Panel):
bl_label = "Text Paper Space"
bl_idname = "BIM_PT_text"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "data"
@classmethod
def poll(cls, context):
return type(context.curve) is bpy.types.TextCurve
def draw(self, context):
layout = self.layout
layout.use_property_split = True
props = context.active_object.data.BIMTextProperties
row = layout.row()
row.operator("bim.propagate_text_data")
row = layout.row()
row.prop(props, "font_size")
row = layout.row()
row.prop(props, "symbol")
row = layout.row()
row.prop(props, "related_element")
row = layout.row()
row.operator("bim.add_variable")
for index, variable in enumerate(props.variables):
row = layout.row(align=True)
row.prop(variable, "name")
row.operator("bim.remove_variable", icon="X", text="").index = index
row = layout.row()
row.prop(variable, "prop_key")
class BIM_PT_annotation_utilities(Panel):
bl_idname = "BIM_PT_annotation_utilities"
bl_label = "Annotation"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.operator("bim.clean_wireframes")
row = layout.row(align=True)
row.operator("bim.link_ifc")
row = layout.row(align=True)
row.operator("bim.add_grid")
row = layout.row(align=True)
row.operator("bim.add_sections_annotations")
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Dim", icon="ARROW_LEFTRIGHT")
op.obj_name = "Dimension"
op.data_type = "curve"
op = row.operator("bim.add_annotation", text="Dim (Eq)", icon="ARROW_LEFTRIGHT")
op.obj_name = "Equal"
op.data_type = "curve"
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Text", icon="SMALL_CAPS")
op.data_type = "text"
op = row.operator("bim.add_annotation", text="Leader", icon="TRACKING_BACKWARDS")
op.obj_name = "Leader"
op.data_type = "curve"
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Stair Arrow", icon="SCREEN_BACK")
op.obj_name = "Stair"
op.data_type = "curve"
op = row.operator("bim.add_annotation", text="Hidden", icon="CON_TRACKTO")
op.obj_name = "Hidden"
op.data_type = "mesh"
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Level (Plan)", icon="SORTBYEXT")
op.obj_name = "Plan Level"
op.data_type = "curve"
op = row.operator("bim.add_annotation", text="Level (Section)", icon="TRIA_DOWN")
op.obj_name = "Section Level"
op.data_type = "curve"
row = layout.row(align=True)
op = row.operator("bim.add_annotation", text="Breakline", icon="FCURVE")
op.obj_name = "Break"
op.data_type = "mesh"
op = row.operator("bim.add_annotation", text="Misc", icon="MESH_MONKEY")
op.obj_name = "Misc"
op.data_type = "mesh"
props = bpy.context.scene.DocProperties
row = layout.row(align=True)
row.operator("bim.add_drawing")
row.operator("bim.refresh_drawing_list", icon="FILE_REFRESH", text="")
if props.drawings:
if props.active_drawing_index < len(props.drawings):
op = row.operator("bim.open_view", icon="URL", text="")
op.view = props.drawings[props.active_drawing_index].name
row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index
layout.template_list("BIM_UL_drawinglist", "", props, "drawings", props, "active_drawing_index")
layout.prop(props, "should_draw_decorations")
layout.prop(props, "decorations_colour")
class BIM_UL_drawinglist(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.prop(item, "name", text="", emboss=False)
op = row.operator("bim.open_view_camera", icon="OUTLINER_OB_CAMERA", text="")
op.view_name = item.name
else:
layout.label(text="", translate=False)
@@ -4,14 +4,13 @@ from . import ui, prop, operator
classes = ( classes = (
operator.EditObjectPlacement, operator.EditObjectPlacement,
operator.AddRepresentation, operator.AddRepresentation,
operator.MapRepresentations,
operator.MapRepresentation,
operator.SwitchRepresentation, operator.SwitchRepresentation,
operator.RemoveRepresentation, operator.RemoveRepresentation,
operator.UpdateMeshRepresentation, operator.UpdateRepresentation,
operator.UpdateParametricRepresentation, operator.UpdateParametricRepresentation,
operator.GetRepresentationIfcParameters, operator.GetRepresentationIfcParameters,
prop.BIMGeometryProperties, prop.BIMGeometryProperties,
ui.BIM_PT_derived_placements,
ui.BIM_PT_representations, ui.BIM_PT_representations,
ui.BIM_PT_mesh, ui.BIM_PT_mesh,
ui.BIM_PT_workarounds, ui.BIM_PT_workarounds,
@@ -1,5 +1,6 @@
import bpy import bpy
import bmesh import bmesh
import mathutils
import ifcopenshell import ifcopenshell
import ifcopenshell.util.unit import ifcopenshell.util.unit
from math import pi from math import pi
@@ -197,17 +198,24 @@ class Helper:
return {"profile": outer_loop, "inner_curves": inner_loops, "extrusion": extrusion} return {"profile": outer_loop, "inner_curves": inner_loops, "extrusion": extrusion}
# An extrusion edge is an edge that shares a single vertex with a profile # An extrusion edge is an edge that shares a single vertex with a profile
# face and is not parallel to the face. # face and is not on the plane of the face.
def detect_extrusion_edge(self, bm, profile_face): def detect_extrusion_edge(self, bm, profile_face):
bm.edges.ensure_lookup_table() bm.edges.ensure_lookup_table()
extrusion = None extrusion = None
face_verts_set = set(profile_face.verts) face_verts_set = set(profile_face.verts)
for edge in bm.edges: for edge in bm.edges:
edge_vector = edge.verts[1].co - edge.verts[0].co
unshared_verts = set(edge.verts) - face_verts_set unshared_verts = set(edge.verts) - face_verts_set
angle_to_normal = edge_vector.angle(profile_face.normal) if len(unshared_verts) == 1:
if len(unshared_verts) == 1 and (angle_to_normal < 0.001 or angle_to_normal - pi < 0.001): unshared_vert = unshared_verts.pop()
if unshared_verts.pop() == edge.verts[1]: if (
abs(
mathutils.geometry.distance_point_to_plane(
unshared_vert.co, profile_face.verts[0].co, profile_face.normal
)
)
> 0.001
):
if unshared_vert == edge.verts[1]:
return [edge.verts[0].index, edge.verts[1].index] return [edge.verts[0].index, edge.verts[1].index]
return [edge.verts[1].index, edge.verts[0].index] return [edge.verts[1].index, edge.verts[0].index]
@@ -3,6 +3,7 @@ import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.representation
import logging import logging
import ifcopenshell.api import ifcopenshell.api
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
@@ -13,14 +14,6 @@ from ifcopenshell.api.void.data import Data as VoidData
from mathutils import Vector from mathutils import Vector
def get_context_id(context_type, context_identifier, target_view):
for context in ContextData.contexts.values():
if context["ContextType"] == context_type:
for i, subcontext in context["HasSubContexts"].items():
if subcontext["ContextIdentifier"] == context_identifier and subcontext["TargetView"] == target_view:
return i
class EditObjectPlacement(bpy.types.Operator): class EditObjectPlacement(bpy.types.Operator):
bl_idname = "bim.edit_object_placement" bl_idname = "bim.edit_object_placement"
bl_label = "Edit Object Placement" bl_label = "Edit Object Placement"
@@ -64,6 +57,8 @@ class AddRepresentation(bpy.types.Operator):
bl_label = "Add Representation" bl_label = "Add Representation"
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
context_id: bpy.props.IntProperty() context_id: bpy.props.IntProperty()
ifc_representation_class: bpy.props.StringProperty()
profile_set_usage: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
@@ -71,8 +66,11 @@ class AddRepresentation(bpy.types.Operator):
bpy.ops.bim.edit_object_placement(obj=obj.name) bpy.ops.bim.edit_object_placement(obj=obj.name)
if obj.data: if not obj.data:
return {"FINISHED"}
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
context_id = self.context_id or int(bpy.context.scene.BIMProperties.contexts) context_id = self.context_id or int(bpy.context.scene.BIMProperties.contexts)
context_of_items = self.file.by_id(context_id) context_of_items = self.file.by_id(context_id)
@@ -95,6 +93,8 @@ class AddRepresentation(bpy.types.Operator):
"total_items": max(1, len(obj.material_slots)), "total_items": max(1, len(obj.material_slots)),
"should_force_faceted_brep": context.scene.BIMGeometryProperties.should_force_faceted_brep, "should_force_faceted_brep": context.scene.BIMGeometryProperties.should_force_faceted_brep,
"should_force_triangulation": context.scene.BIMGeometryProperties.should_force_triangulation, "should_force_triangulation": context.scene.BIMGeometryProperties.should_force_triangulation,
"ifc_representation_class": self.ifc_representation_class,
"profile_set_usage": self.file.by_id(self.profile_set_usage) if self.profile_set_usage else None
} }
result = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data) result = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
@@ -103,26 +103,10 @@ class AddRepresentation(bpy.types.Operator):
print("Failed to write shape representation") print("Failed to write shape representation")
return {"FINISHED"} return {"FINISHED"}
box_context_id = get_context_id("Model", "Box", "MODEL_VIEW")
old_box = ifcopenshell.util.element.get_representation(product, "Model", "Box", "MODEL_VIEW")
if (
box_context_id
and context_of_items.ContextType == "Model"
and context_of_items.ContextIdentifier
and context_of_items.ContextIdentifier == "Body"
):
if old_box:
bpy.ops.bim.remove_representation(representation_id=old_box.id(), obj=obj.name)
representation_data["context"] = self.file.by_id(box_context_id)
new_box = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, **{"product": product, "representation": new_box}
)
[ [
bpy.ops.bim.add_style(material=s.material.name) bpy.ops.bim.add_style(material=s.material.name)
for s in obj.material_slots for s in obj.material_slots
if not s.material.BIMMaterialProperties.ifc_style_id if s.material and not s.material.BIMMaterialProperties.ifc_style_id
] ]
ifcopenshell.api.run( ifcopenshell.api.run(
@@ -148,38 +132,58 @@ class AddRepresentation(bpy.types.Operator):
mesh.BIMMeshProperties.ifc_definition_id = int(result.id()) mesh.BIMMeshProperties.ifc_definition_id = int(result.id())
obj.data = mesh obj.data = mesh
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
if product.is_a("IfcTypeProduct"):
if self.file.schema == "IFC2X3":
types = product.ObjectTypeOf
else:
types = product.Types
if types:
for element in types[0].RelatedObjects:
Data.load(IfcStore.get_file(), element.id())
return {"FINISHED"} return {"FINISHED"}
class SwitchRepresentation(bpy.types.Operator): class SwitchRepresentation(bpy.types.Operator):
bl_idname = "bim.switch_representation" bl_idname = "bim.switch_representation"
bl_label = "Switch Representation" bl_label = "Switch Representation"
obj: bpy.props.StringProperty()
ifc_definition_id: bpy.props.IntProperty() ifc_definition_id: bpy.props.IntProperty()
should_reload: bpy.props.BoolProperty()
disable_opening_subtractions: bpy.props.BoolProperty() disable_opening_subtractions: bpy.props.BoolProperty()
def execute(self, context): def execute(self, context):
self.obj = bpy.context.active_object self.element_obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
self.oprops = self.obj.BIMObjectProperties self.oprops = self.element_obj.BIMObjectProperties
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
self.context_of_items = self.file.by_id(self.ifc_definition_id).ContextOfItems self.context_of_items = self.file.by_id(self.ifc_definition_id).ContextOfItems
self.mesh_name = "{}/{}".format(self.context_of_items.id(), self.ifc_definition_id) self.mesh_name = self.get_mesh_name()
mesh = bpy.data.meshes.get(self.mesh_name) mesh = bpy.data.meshes.get(self.mesh_name)
if mesh: if mesh:
self.obj.data.user_remap(mesh) self.element_obj.data.user_remap(mesh)
if not mesh or self.should_reload:
self.pull_mesh_from_ifc() self.pull_mesh_from_ifc()
return {"FINISHED"} return {"FINISHED"}
def get_mesh_name(self):
representation = self.resolve_mapped_representation(self.file.by_id(self.ifc_definition_id))
return "{}/{}".format(self.context_of_items.id(), representation.id())
def resolve_mapped_representation(self, representation):
if representation.RepresentationType == "MappedRepresentation":
return self.resolve_mapped_representation(representation.Items[0].MappingSource.MappedRepresentation)
return representation
def pull_mesh_from_ifc(self): def pull_mesh_from_ifc(self):
self.file = IfcStore.get_file()
logger = logging.getLogger("ImportIFC") logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, logger) ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, logger)
element = self.file.by_id(self.oprops.ifc_definition_id) element = self.file.by_id(self.oprops.ifc_definition_id)
settings = ifcopenshell.geom.settings() settings = ifcopenshell.geom.settings()
if self.context_of_items.ContextIdentifier == "Body": if self.context_of_items.ContextIdentifier == "Body":
if self.disable_opening_subtractions: if element.is_a("IfcTypeProduct") or self.disable_opening_subtractions:
shape = ifcopenshell.geom.create_shape(settings, self.file.by_id(self.ifc_definition_id)) shape = ifcopenshell.geom.create_shape(settings, self.file.by_id(self.ifc_definition_id))
else: else:
shape = ifcopenshell.geom.create_shape(settings, self.file.by_id(self.oprops.ifc_definition_id)) shape = ifcopenshell.geom.create_shape(settings, self.file.by_id(self.oprops.ifc_definition_id))
@@ -192,23 +196,24 @@ class SwitchRepresentation(bpy.types.Operator):
mesh = ifc_importer.create_mesh(element, shape) mesh = ifc_importer.create_mesh(element, shape)
mesh.name = self.mesh_name mesh.name = self.mesh_name
mesh.BIMMeshProperties.ifc_definition_id = self.ifc_definition_id mesh.BIMMeshProperties.ifc_definition_id = self.ifc_definition_id
self.obj.data.user_remap(mesh) self.element_obj.data.user_remap(mesh)
material_creator = import_ifc.MaterialCreator(ifc_import_settings, ifc_importer) material_creator = import_ifc.MaterialCreator(ifc_import_settings, ifc_importer)
material_creator.create(element, self.obj, mesh) material_creator.create(element, self.element_obj, mesh)
if self.disable_opening_subtractions and self.context_of_items.ContextIdentifier == "Body": if self.disable_opening_subtractions and self.context_of_items.ContextIdentifier == "Body":
if self.oprops.ifc_definition_id not in VoidData.products: if self.oprops.ifc_definition_id not in VoidData.products:
VoidData.load(IfcStore.get_file(), self.oprops.ifc_definition_id) VoidData.load(IfcStore.get_file(), self.oprops.ifc_definition_id)
for opening_id in VoidData.products[self.oprops.ifc_definition_id]: for opening_id in VoidData.products[self.oprops.ifc_definition_id]:
if opening_id in IfcStore.id_map: opening = IfcStore.get_element(opening_id)
opening = IfcStore.id_map[opening_id] if not opening:
modifier = self.obj.modifiers.new("IfcOpeningElement", "BOOLEAN") continue
modifier = self.element_obj.modifiers.new("IfcOpeningElement", "BOOLEAN")
modifier.operation = "DIFFERENCE" modifier.operation = "DIFFERENCE"
modifier.object = opening modifier.object = opening
else: else:
for modifier in self.obj.modifiers: for modifier in self.element_obj.modifiers:
if modifier.type == "BOOLEAN" and "IfcOpeningElement" in modifier.name: if modifier.type == "BOOLEAN" and "IfcOpeningElement" in modifier.name:
self.obj.modifiers.remove(modifier) self.element_obj.modifiers.remove(modifier)
class RemoveRepresentation(bpy.types.Operator): class RemoveRepresentation(bpy.types.Operator):
@@ -247,64 +252,9 @@ class RemoveRepresentation(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class MapRepresentations(bpy.types.Operator): class UpdateRepresentation(bpy.types.Operator):
bl_idname = "bim.map_representations" bl_idname = "bim.update_representation"
bl_label = "Map Representations" bl_label = "Update Representation"
product_id: bpy.props.IntProperty()
type_product_id: bpy.props.IntProperty()
def execute(self, context):
related_object = IfcStore.id_map[self.product_id]
if self.product_id not in Data.products:
Data.load(IfcStore.get_file(), self.product_id)
for representation_id in Data.products[self.product_id]:
bpy.ops.bim.remove_representation(obj=related_object.name, representation_id=representation_id)
if self.type_product_id not in Data.products:
Data.load(IfcStore.get_file(), self.type_product_id)
for representation_id in Data.products[self.type_product_id]:
bpy.ops.bim.map_representation(
obj=related_object.name,
representation_id=representation_id,
obj_data=IfcStore.id_map[self.type_product_id].data.name,
)
return {"FINISHED"}
class MapRepresentation(bpy.types.Operator):
bl_idname = "bim.map_representation"
bl_label = "Map Representation"
obj: bpy.props.StringProperty()
representation_id: bpy.props.IntProperty()
obj_data: bpy.props.StringProperty()
def execute(self, context):
objs = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects
obj_data = bpy.data.meshes.get(self.obj_data) if self.obj_data else None
self.file = IfcStore.get_file()
for obj in objs:
bpy.ops.bim.edit_object_placement(obj=obj.name)
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if obj_data:
obj.data = obj_data
result = ifcopenshell.api.run(
"geometry.map_representation", self.file, **{"representation": self.file.by_id(self.representation_id)}
)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, **{"product": product, "representation": result}
)
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
return {"FINISHED"}
class UpdateMeshRepresentation(bpy.types.Operator):
bl_idname = "bim.update_mesh_representation"
bl_label = "Update Mesh Representation"
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
ifc_representation_class: bpy.props.StringProperty() ifc_representation_class: bpy.props.StringProperty()
@@ -317,14 +267,14 @@ class UpdateMeshRepresentation(bpy.types.Operator):
for obj in objs: for obj in objs:
self.update_obj_mesh_representation(context, obj) self.update_obj_mesh_representation(context, obj)
IfcStore.edited_objs.discard(obj.name) IfcStore.edited_objs.discard(obj)
return {"FINISHED"} return {"FINISHED"}
def update_obj_mesh_representation(self, context, obj): def update_obj_mesh_representation(self, context, obj):
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if product.is_a("IfcGridAxis"): if product.is_a("IfcGridAxis"):
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"AxisCurve": obj, "grid_axis": product}) ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": product})
return return
bpy.ops.bim.edit_object_placement(obj=obj.name) bpy.ops.bim.edit_object_placement(obj=obj.name)
@@ -356,37 +306,6 @@ class UpdateMeshRepresentation(bpy.types.Operator):
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data) new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
# if product.is_a("IfcWall"):
# # Generate axis representation
# axis_context_id = get_context_id("Model", "Axis", "MODEL_VIEW")
# old_axis = ifcopenshell.util.element.get_representation(product, "Model", "Axis", "MODEL_VIEW")
# if (
# axis_context_id
# and old_axis
# and context_of_items.ContextType == "Model"
# and context_of_items.ContextIdentifier
# and context_of_items.ContextIdentifier == "Body"
# ):
# has_axis_generator = False
# if has_axis_generator:
# # TODO, just pseudocode for now
# representation_data["geometry"] = axis_generator_function_call
# pass
box_context_id = get_context_id("Model", "Box", "MODEL_VIEW")
old_box = ifcopenshell.util.element.get_representation(product, "Model", "Box", "MODEL_VIEW")
if (
box_context_id
and old_box
and context_of_items.ContextType == "Model"
and context_of_items.ContextIdentifier
and context_of_items.ContextIdentifier == "Body"
):
representation_data["context"] = self.file.by_id(box_context_id)
new_box = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
for inverse in self.file.get_inverse(old_box):
ifcopenshell.util.element.replace_attribute(inverse, old_box, new_box)
ifcopenshell.api.run( ifcopenshell.api.run(
"geometry.assign_styles", "geometry.assign_styles",
self.file, self.file,
@@ -422,7 +341,7 @@ class UpdateParametricRepresentation(bpy.types.Operator):
props = obj.data.BIMMeshProperties props = obj.data.BIMMeshProperties
parameter = props.ifc_parameters[self.index] parameter = props.ifc_parameters[self.index]
element = IfcStore.get_file().by_id(parameter.step_id)[parameter.index] = parameter.value element = IfcStore.get_file().by_id(parameter.step_id)[parameter.index] = parameter.value
bpy.ops.bim.switch_representation(ifc_definition_id=props.ifc_definition_id) bpy.ops.bim.switch_representation(ifc_definition_id=props.ifc_definition_id, should_reload=True)
return {"FINISHED"} return {"FINISHED"}
@@ -14,6 +14,8 @@ class BIM_PT_representations(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return IfcStore.get_file() return IfcStore.get_file()
def draw(self, context): def draw(self, context):
@@ -39,6 +41,7 @@ class BIM_PT_representations(Panel):
row.label(text=representation["ContextOfItems"]["TargetView"]) row.label(text=representation["ContextOfItems"]["TargetView"])
row.label(text=representation["RepresentationType"]) row.label(text=representation["RepresentationType"])
op = row.operator("bim.switch_representation", icon="OUTLINER_DATA_MESH", text="") op = row.operator("bim.switch_representation", icon="OUTLINER_DATA_MESH", text="")
op.should_reload = True
op.ifc_definition_id = ifc_definition_id op.ifc_definition_id = ifc_definition_id
op.disable_opening_subtractions = False op.disable_opening_subtractions = False
row.operator("bim.remove_representation", icon="X", text="").representation_id = ifc_definition_id row.operator("bim.remove_representation", icon="X", text="").representation_id = ifc_definition_id
@@ -68,29 +71,31 @@ class BIM_PT_mesh(Panel):
row = layout.row(align=True) row = layout.row(align=True)
op = row.operator("bim.switch_representation", text="Bake Voids", icon="SELECT_SUBTRACT") op = row.operator("bim.switch_representation", text="Bake Voids", icon="SELECT_SUBTRACT")
op.should_reload = True
op.ifc_definition_id = props.ifc_definition_id op.ifc_definition_id = props.ifc_definition_id
op.disable_opening_subtractions = False op.disable_opening_subtractions = False
op = row.operator("bim.switch_representation", text="Dynamic Voids", icon="SELECT_INTERSECT") op = row.operator("bim.switch_representation", text="Dynamic Voids", icon="SELECT_INTERSECT")
op.should_reload = True
op.ifc_definition_id = props.ifc_definition_id op.ifc_definition_id = props.ifc_definition_id
op.disable_opening_subtractions = True op.disable_opening_subtractions = True
row = layout.row() row = layout.row()
row.operator("bim.update_mesh_representation") row.operator("bim.update_representation")
row = layout.row() row = layout.row()
op = row.operator("bim.update_mesh_representation", text="Update Mesh As Rectangle Extrusion") op = row.operator("bim.update_representation", text="Update Mesh As Rectangle Extrusion")
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcRectangleProfileDef" op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcRectangleProfileDef"
row = layout.row() row = layout.row()
op = row.operator("bim.update_mesh_representation", text="Update Mesh As Circle Extrusion") op = row.operator("bim.update_representation", text="Update Mesh As Circle Extrusion")
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcCircleProfileDef" op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcCircleProfileDef"
row = layout.row() row = layout.row()
op = row.operator("bim.update_mesh_representation", text="Update Mesh As Arbitrary Extrusion") op = row.operator("bim.update_representation", text="Update Mesh As Arbitrary Extrusion")
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef" op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef"
row = layout.row() row = layout.row()
op = row.operator("bim.update_mesh_representation", text="Update Mesh As Arbitrary Extrusion With Voids") op = row.operator("bim.update_representation", text="Update Mesh As Arbitrary Extrusion With Voids")
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
row = layout.row() row = layout.row()
@@ -108,6 +113,36 @@ def BIM_PT_transform(self, context):
row.operator("bim.edit_object_placement") row.operator("bim.edit_object_placement")
class BIM_PT_derived_placements(Panel):
bl_label = "IFC Derived Placements"
bl_idname = "BIM_PT_derived_placements"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_parent_id = "OBJECT_PT_transform"
def draw(self, context):
z = context.active_object.matrix_world.translation.z
z_values = [co[2] for co in context.active_object.bound_box]
row = self.layout.row(align=True)
row.label(text="Min Global Z")
row.label(text="{0:.3f}".format(min(z_values) + z))
row = self.layout.row(align=True)
row.label(text="Max Global Z")
row.label(text="{0:.3f}".format(max(z_values) + z))
collection = bpy.data.objects.get(context.active_object.users_collection[0].name)
if collection:
collection_z = collection.matrix_world.translation.z
row = self.layout.row(align=True)
row.label(text="Min Local Z")
row.label(text="{0:.3f}".format(min(z_values) + z - collection_z))
row = self.layout.row(align=True)
row.label(text="Max Local Z")
row.label(text="{0:.3f}".format(max(z_values) + z - collection_z))
class BIM_PT_workarounds(Panel): class BIM_PT_workarounds(Panel):
bl_label = "IFC Vendor Workarounds" bl_label = "IFC Vendor Workarounds"
bl_idname = "BIM_PT_workarounds" bl_idname = "BIM_PT_workarounds"
@@ -117,6 +117,7 @@ class BIM_PT_gis(Panel):
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text="XAxisOrdinate") row.label(text="XAxisOrdinate")
row.label(text=props.blender_x_axis_ordinate) row.label(text=props.blender_x_axis_ordinate)
row = self.layout.row(align=True)
row.label(text="Derived Grid North") row.label(text="Derived Grid North")
row.label( row.label(
text=str( text=str(
@@ -187,6 +188,7 @@ class BIM_PT_gis(Panel):
class BIM_PT_gis_utilities(Panel): class BIM_PT_gis_utilities(Panel):
bl_idname = "BIM_PT_gis_utilities" bl_idname = "BIM_PT_gis_utilities"
bl_label = "Georeferencing Utilities" bl_label = "Georeferencing Utilities"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "BlenderBIM" bl_category = "BlenderBIM"
@@ -11,6 +11,7 @@ classes = (
operator.UnassignGroup, operator.UnassignGroup,
operator.EnableEditingGroup, operator.EnableEditingGroup,
operator.DisableEditingGroup, operator.DisableEditingGroup,
operator.SelectGroupProducts,
prop.Group, prop.Group,
prop.BIMGroupProperties, prop.BIMGroupProperties,
ui.BIM_PT_groups, ui.BIM_PT_groups,
@@ -152,3 +152,20 @@ class UnassignGroup(bpy.types.Operator):
) )
Data.load(IfcStore.get_file()) Data.load(IfcStore.get_file())
return {"FINISHED"} return {"FINISHED"}
class SelectGroupProducts(bpy.types.Operator):
bl_idname = "bim.select_group_products"
bl_label = "Select Group Products"
group: bpy.props.IntProperty()
def execute(self, context):
self.file = IfcStore.get_file()
for obj in bpy.context.visible_objects:
obj.select_set(False)
if not obj.BIMObjectProperties.ifc_definition_id:
continue
product_groups = Data.products.get(obj.BIMObjectProperties.ifc_definition_id, [])
if self.group in product_groups:
obj.select_set(True)
return {"FINISHED"}
@@ -24,7 +24,7 @@ class BIM_PT_groups(Panel):
row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER") row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER")
if self.props.is_editing: if self.props.is_editing:
row.operator("bim.add_group", text="", icon="ADD") row.operator("bim.add_group", text="", icon="ADD")
row.operator("bim.disable_group_editing_ui", text="", icon="CHECKMARK") row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
else: else:
row.operator("bim.load_groups", text="", icon="GREASEPENCIL") row.operator("bim.load_groups", text="", icon="GREASEPENCIL")
@@ -69,11 +69,17 @@ class BIM_UL_groups(UIList):
op.group = item.ifc_definition_id op.group = item.ifc_definition_id
if context.scene.BIMGroupProperties.active_group_id == item.ifc_definition_id: if context.scene.BIMGroupProperties.active_group_id == item.ifc_definition_id:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
op.group = item.ifc_definition_id
row.operator("bim.edit_group", text="", icon="CHECKMARK") row.operator("bim.edit_group", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_group", text="", icon="X") row.operator("bim.disable_editing_group", text="", icon="CANCEL")
elif context.scene.BIMGroupProperties.active_group_id: elif context.scene.BIMGroupProperties.active_group_id:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
op.group = item.ifc_definition_id
row.operator("bim.remove_group", text="", icon="X").group = item.ifc_definition_id row.operator("bim.remove_group", text="", icon="X").group = item.ifc_definition_id
else: else:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
op.group = item.ifc_definition_id
op = row.operator("bim.enable_editing_group", text="", icon="GREASEPENCIL") op = row.operator("bim.enable_editing_group", text="", icon="GREASEPENCIL")
op.group = item.ifc_definition_id op.group = item.ifc_definition_id
row.operator("bim.remove_group", text="", icon="X").group = item.ifc_definition_id row.operator("bim.remove_group", text="", icon="X").group = item.ifc_definition_id
@@ -2,6 +2,7 @@ import bpy
import json import json
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.attribute import ifcopenshell.util.attribute
import blenderbim.bim.helper
from blenderbim.bim.module.material.prop import purge as material_prop_purge from blenderbim.bim.module.material.prop import purge as material_prop_purge
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.material.data import Data from ifcopenshell.api.material.data import Data
@@ -26,7 +27,7 @@ class AssignParameterizedProfile(bpy.types.Operator):
ifcopenshell.api.run( ifcopenshell.api.run(
"material.assign_profile", "material.assign_profile",
self.file, self.file,
**{"material_profile": self.file.by_id(self.material_profile), "profile": profile} **{"material_profile": self.file.by_id(self.material_profile), "profile": profile},
) )
Data.load_profiles() Data.load_profiles()
ProfileData.load(self.file) ProfileData.load(self.file)
@@ -42,7 +43,7 @@ class AddMaterial(bpy.types.Operator):
def execute(self, context): def execute(self, context):
obj = bpy.data.materials.get(self.obj) if self.obj else bpy.context.active_object.active_material obj = bpy.data.materials.get(self.obj) if self.obj else bpy.context.active_object.active_material
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
result = ifcopenshell.api.run("material.add_material", self.file, **{"Name": obj.name}) result = ifcopenshell.api.run("material.add_material", self.file, **{"name": obj.name})
obj.BIMObjectProperties.ifc_definition_id = result.id() obj.BIMObjectProperties.ifc_definition_id = result.id()
Data.load(IfcStore.get_file()) Data.load(IfcStore.get_file())
material_prop_purge() material_prop_purge()
@@ -175,9 +176,7 @@ class RemoveProfile(bpy.types.Operator):
def execute(self, context): def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run("material.remove_profile", self.file, **{"profile": self.file.by_id(self.profile)})
"material.remove_profile", self.file, **{"profile": self.file.by_id(self.profile)}
)
Data.load_profiles() Data.load_profiles()
return {"FINISHED"} return {"FINISHED"}
@@ -313,20 +312,28 @@ class EnableEditingAssignedMaterial(bpy.types.Operator):
elif product_data["type"] == "IfcMaterialLayerSet": elif product_data["type"] == "IfcMaterialLayerSet":
material_set_data = Data.layer_sets[product_data["id"]] material_set_data = Data.layer_sets[product_data["id"]]
elif product_data["type"] == "IfcMaterialLayerSetUsage": elif product_data["type"] == "IfcMaterialLayerSetUsage":
layer_set_usage = Data.layer_set_usages[product_data["id"]] material_set_usage = Data.layer_set_usages[product_data["id"]]
material_set_data = Data.layer_sets[layer_set_usage["ForLayerSet"]] material_set_data = Data.layer_sets[material_set_usage["ForLayerSet"]]
material_set_class = "IfcMaterialLayerSet" material_set_class = "IfcMaterialLayerSet"
elif product_data["type"] == "IfcMaterialProfileSet": elif product_data["type"] == "IfcMaterialProfileSet":
material_set_data = Data.profile_sets[product_data["id"]] material_set_data = Data.profile_sets[product_data["id"]]
elif product_data["type"] == "IfcMaterialProfileSetUsage": elif product_data["type"] == "IfcMaterialProfileSetUsage":
profile_set_usage = Data.profile_set_usages[product_data["id"]] material_set_usage = Data.profile_set_usages[product_data["id"]]
material_set_data = Data.profile_sets[profile_set_usage["ForProfileSet"]] material_set_data = Data.profile_sets[material_set_usage["ForProfileSet"]]
material_set_class = "IfcMaterialProfileSet" material_set_class = "IfcMaterialProfileSet"
elif product_data["type"] == "IfcMaterialList": elif product_data["type"] == "IfcMaterialList":
material_set_data = Data.lists[product_data["id"]] material_set_data = Data.lists[product_data["id"]]
else: else:
material_set_data = {} material_set_data = {}
while len(props.material_set_usage_attributes) > 0:
props.material_set_usage_attributes.remove(0)
if "Usage" in product_data["type"]:
blenderbim.bim.helper.import_attributes(
product_data["type"], props.material_set_usage_attributes, material_set_usage, self.import_attributes
)
while len(props.material_set_attributes) > 0: while len(props.material_set_attributes) > 0:
props.material_set_attributes.remove(0) props.material_set_attributes.remove(0)
@@ -340,6 +347,36 @@ class EnableEditingAssignedMaterial(bpy.types.Operator):
new.string_value = "" if new.is_null else material_set_data[attribute.name()] new.string_value = "" if new.is_null else material_set_data[attribute.name()]
return {"FINISHED"} return {"FINISHED"}
def import_attributes(self, name, prop, data):
if name == "CardinalPoint":
# TODO: complain to buildingSMART
cardinal_point_map = {
1: "bottom left",
2: "bottom centre",
3: "bottom right",
4: "mid-depth left",
5: "mid-depth centre",
6: "mid-depth right",
7: "top left",
8: "top centre",
9: "top right",
10: "geometric centroid",
11: "bottom in line with the geometric centroid",
12: "left in line with the geometric centroid",
13: "right in line with the geometric centroid",
14: "top in line with the geometric centroid",
15: "shear centre",
16: "bottom in line with the shear centre",
17: "left in line with the shear centre",
18: "right in line with the shear centre",
19: "top in line with the shear centre",
}
prop.data_type = "enum"
prop.enum_items = json.dumps(cardinal_point_map)
if data[name]:
prop.enum_value = str(data[name])
return True
class DisableEditingAssignedMaterial(bpy.types.Operator): class DisableEditingAssignedMaterial(bpy.types.Operator):
bl_idname = "bim.disable_editing_assigned_material" bl_idname = "bim.disable_editing_assigned_material"
@@ -358,6 +395,7 @@ class EditAssignedMaterial(bpy.types.Operator):
bl_label = "Edit Assigned Material" bl_label = "Edit Assigned Material"
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
material_set: bpy.props.IntProperty() material_set: bpy.props.IntProperty()
material_set_usage: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
@@ -379,12 +417,24 @@ class EditAssignedMaterial(bpy.types.Operator):
ifcopenshell.api.run( ifcopenshell.api.run(
"material.edit_assigned_material", "material.edit_assigned_material",
self.file, self.file,
**{ **{"element": material_set, "attributes": attributes},
"element": material_set,
"attributes": attributes,
},
) )
Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id)
if self.material_set_usage:
material_set_usage = self.file.by_id(self.material_set_usage)
attributes = blenderbim.bim.helper.export_attributes(props.material_set_usage_attributes)
attributes["CardinalPoint"] = int(attributes["CardinalPoint"]) if attributes["CardinalPoint"] else None
ifcopenshell.api.run(
"material.edit_assigned_material",
self.file,
**{"element": material_set_usage, "attributes": attributes},
)
if material_set_usage.is_a("IfcMaterialLayerSetUsage"):
Data.load_layer_usages()
elif material_set_usage.is_a("IfcMaterialProfileSetUsage"):
Data.load_profile_usages()
if material_set.is_a("IfcMaterialConstituentSet"): if material_set.is_a("IfcMaterialConstituentSet"):
Data.load_constituents() Data.load_constituents()
elif material_set.is_a("IfcMaterialLayerSet"): elif material_set.is_a("IfcMaterialLayerSet"):
@@ -1,5 +1,4 @@
import bpy import bpy
import blenderbim.bim.schema # refactor
from ifcopenshell.api.material.data import Data from ifcopenshell.api.material.data import Data
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty, Attribute from blenderbim.bim.prop import StrProperty, Attribute
@@ -84,6 +83,7 @@ class BIMObjectMaterialProperties(PropertyGroup):
material_type: EnumProperty(items=getMaterialTypes, name="Material Type") material_type: EnumProperty(items=getMaterialTypes, name="Material Type")
material: EnumProperty(items=getMaterials, name="Material") material: EnumProperty(items=getMaterials, name="Material")
is_editing: BoolProperty(name="Is Editing", default=False) is_editing: BoolProperty(name="Is Editing", default=False)
material_set_usage_attributes: CollectionProperty(name="Material Set Usage Attributes", type=Attribute)
material_set_attributes: CollectionProperty(name="Material Set Attributes", type=Attribute) material_set_attributes: CollectionProperty(name="Material Set Attributes", type=Attribute)
active_material_set_item_id: IntProperty(name="Active Material Set ID") active_material_set_item_id: IntProperty(name="Active Material Set ID")
material_set_item_attributes: CollectionProperty(name="Material Set Item Attributes", type=Attribute) material_set_item_attributes: CollectionProperty(name="Material Set Item Attributes", type=Attribute)
@@ -1,3 +1,4 @@
import blenderbim.bim.helper
from bpy.types import Panel from bpy.types import Panel
from ifcopenshell.api.material.data import Data from ifcopenshell.api.material.data import Data
from ifcopenshell.api.profile.data import Data as ProfileData from ifcopenshell.api.profile.data import Data as ProfileData
@@ -35,6 +36,8 @@ class BIM_PT_object_material(Panel):
props = context.active_object.BIMObjectProperties props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id: if not props.ifc_definition_id:
return False return False
if not IfcStore.get_element(props.ifc_definition_id):
return False
if not hasattr(IfcStore.get_file().by_id(props.ifc_definition_id), "HasAssociations"): if not hasattr(IfcStore.get_file().by_id(props.ifc_definition_id), "HasAssociations"):
return False return False
return True return True
@@ -109,7 +112,9 @@ class BIM_PT_object_material(Panel):
if self.props.is_editing: if self.props.is_editing:
op = row.operator("bim.edit_assigned_material", icon="CHECKMARK", text="") op = row.operator("bim.edit_assigned_material", icon="CHECKMARK", text="")
op.material_set = self.material_set_id op.material_set = self.material_set_id
row.operator("bim.disable_editing_assigned_material", icon="X", text="") if "Usage" in self.product_data["type"]:
op.material_set_usage = self.product_data["id"]
row.operator("bim.disable_editing_assigned_material", icon="CANCEL", text="")
else: else:
row.operator("bim.enable_editing_assigned_material", icon="GREASEPENCIL", text="") row.operator("bim.enable_editing_assigned_material", icon="GREASEPENCIL", text="")
row.operator("bim.unassign_material", icon="X", text="") row.operator("bim.unassign_material", icon="X", text="")
@@ -140,6 +145,8 @@ class BIM_PT_object_material(Panel):
self.draw_read_only_set_ui() self.draw_read_only_set_ui()
def draw_editable_set_ui(self): def draw_editable_set_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.material_set_usage_attributes, self.layout)
for attribute in self.props.material_set_attributes: for attribute in self.props.material_set_attributes:
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name) row.prop(attribute, "string_value", text=attribute.name)
@@ -168,7 +175,7 @@ class BIM_PT_object_material(Panel):
row.prop(self.props, "material_set_item_material", icon="MATERIAL") row.prop(self.props, "material_set_item_material", icon="MATERIAL")
op = row.operator("bim.edit_material_set_item", icon="CHECKMARK", text="") op = row.operator("bim.edit_material_set_item", icon="CHECKMARK", text="")
op.material_set_item = set_item_id op.material_set_item = set_item_id
row.operator("bim.disable_editing_material_set_item", icon="X", text="") row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="")
for attribute in self.props.material_set_item_attributes: for attribute in self.props.material_set_item_attributes:
row = box.row(align=True) row = box.row(align=True)
@@ -197,7 +204,7 @@ class BIM_PT_object_material(Panel):
else: else:
# TODO: support non parametric profiles by showing a list of named profiles to select from, or an # TODO: support non parametric profiles by showing a list of named profiles to select from, or an
# eyedropper to pick profile geometry from the scene # eyedropper to pick profile geometry from the scene
row.operator("bim.disable_editing_material_set_item", icon="X", text="") row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="")
def draw_editable_profile_ui(self, layout, item): def draw_editable_profile_ui(self, layout, item):
for attribute in self.props.material_set_item_profile_attributes: for attribute in self.props.material_set_item_profile_attributes:
@@ -224,7 +231,11 @@ class BIM_PT_object_material(Panel):
else: else:
item = self.set_data[set_item_id] item = self.set_data[set_item_id]
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text=item.get("Name", "Unnamed") or "Unnamed", icon="ALIGN_CENTER") item_name = item.get("Name", "Unnamed") or "Unnamed"
thickness = item.get("LayerThickness")
if thickness:
item_name += f" ({thickness})"
row.label(text=item_name, icon="ALIGN_CENTER")
row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL") row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL")
if not is_first: if not is_first:
@@ -261,6 +272,39 @@ class BIM_PT_object_material(Panel):
row.label(text="Description") row.label(text="Description")
row.label(text=str(self.material_set_data["Description"])) row.label(text=str(self.material_set_data["Description"]))
if self.product_data["type"] == "IfcMaterialProfileSetUsage":
# TODO: complain to buildingSMART
cardinal_point_map = {
1: "bottom left",
2: "bottom centre",
3: "bottom right",
4: "mid-depth left",
5: "mid-depth centre",
6: "mid-depth right",
7: "top left",
8: "top centre",
9: "top right",
10: "geometric centroid",
11: "bottom in line with the geometric centroid",
12: "left in line with the geometric centroid",
13: "right in line with the geometric centroid",
14: "top in line with the geometric centroid",
15: "shear centre",
16: "bottom in line with the shear centre",
17: "left in line with the shear centre",
18: "right in line with the shear centre",
19: "top in line with the shear centre",
}
if self.material_set_usage["CardinalPoint"]:
row = self.layout.row(align=True)
row.label(text="CardinalPoint")
row.label(text=cardinal_point_map[self.material_set_usage["CardinalPoint"]])
if self.material_set_usage["ReferenceExtent"]:
row = self.layout.row(align=True)
row.label(text="ReferenceExtent")
row.label(text=str(self.material_set_usage["ReferenceExtent"]))
total_thickness = 0
for item_id in self.set_items: for item_id in self.set_items:
if self.product_data["type"] == "IfcMaterialList": if self.product_data["type"] == "IfcMaterialList":
row = self.layout.row(align=True) row = self.layout.row(align=True)
@@ -269,5 +313,14 @@ class BIM_PT_object_material(Panel):
else: else:
item = self.set_data[item_id] item = self.set_data[item_id]
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text=item.get("Name", "Unnamed") or "Unnamed", icon="ALIGN_CENTER") item_name = item.get("Name", "Unnamed") or "Unnamed"
thickness = item.get("LayerThickness")
if thickness:
item_name += f" ({thickness})"
total_thickness += thickness
row.label(text=item_name, icon="ALIGN_CENTER")
row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL") row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL")
if total_thickness:
row = self.layout.row(align=True)
row.label(text=f"Total Thickness: {total_thickness}")
@@ -1,13 +1,21 @@
import bpy import bpy
from . import grid, wall, stair, door, window, slab, opening, pie from . import handler, prop, ui, grid, product, wall, slab, stair, door, window, opening, pie, workspace
classes = ( classes = (
product.AddTypeInstance,
wall.AddWall,
wall.JoinWall,
wall.AlignWall,
wall.FlipWall,
wall.SplitWall,
prop.BIMModelProperties,
ui.BIM_PT_authoring,
ui.BIM_PT_authoring_architectural,
ui.BIM_PT_misc_utilities,
grid.BIM_OT_add_object, grid.BIM_OT_add_object,
wall.BIM_OT_add_object,
stair.BIM_OT_add_object, stair.BIM_OT_add_object,
door.BIM_OT_add_object, door.BIM_OT_add_object,
window.BIM_OT_add_object, window.BIM_OT_add_object,
slab.BIM_OT_add_object,
opening.BIM_OT_add_object, opening.BIM_OT_add_object,
pie.OpenPieClass, pie.OpenPieClass,
pie.PieUpdateContainer, pie.PieUpdateContainer,
@@ -27,13 +35,14 @@ addon_keymaps = []
def register(): def register():
bpy.utils.register_tool(workspace.WallTool, after={"builtin.scale_cage"}, separator=True, group=True)
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(wall.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(window.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(window.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(slab.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.append(opening.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(opening.add_object_button)
bpy.app.handlers.load_post.append(handler.load_post)
wm = bpy.context.window_manager wm = bpy.context.window_manager
if wm.keyconfigs.addon: if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")
@@ -43,12 +52,13 @@ def register():
def unregister(): def unregister():
bpy.utils.unregister_tool(workspace.WallTool)
del bpy.types.Scene.BIMModelProperties
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(wall.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(stair.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(slab.add_object_button)
bpy.types.VIEW3D_MT_mesh_add.remove(opening.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(opening.add_object_button)
wm = bpy.context.window_manager wm = bpy.context.window_manager
kc = wm.keyconfigs.addon kc = wm.keyconfigs.addon
@@ -0,0 +1,168 @@
import bpy
import bmesh
import math
import ifcopenshell
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
import mathutils.geometry
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
from math import pi, degrees
from mathutils import Vector, Matrix
from ifcopenshell.api.pset.data import Data as PsetData
from ifcopenshell.api.material.data import Data as MaterialData
from blenderbim.bim.module.geometry.helper import Helper
def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in bpy.context.selected_objects + [bpy.context.active_object]:
if (
obj.mode != "EDIT"
or not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbColumn":
return
IfcStore.edited_objs.add(obj)
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.update_edit_mesh(obj.data)
bm.free()
def ensure_solid(usecase_path, ifc_file, settings):
product = ifc_file.by_id(settings["blender_object"].BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbColumn":
return
material = ifcopenshell.util.element.get_material(product)
if material and material.is_a("IfcMaterialProfileSetUsage"):
settings["profile_set_usage"] = material
else:
return
settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage"
class DumbColumnGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
def generate(self):
self.file = IfcStore.get_file()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
material = ifcopenshell.util.element.get_material(self.relating_type)
if material and material.is_a("IfcMaterialProfileSet"):
self.profile_set = material
else:
return
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.length = 3
self.rotation = 0
self.location = Vector((0, 0, 0))
return self.derive_from_cursor()
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
return self.create_column()
def create_column(self):
# A cube
verts = [
Vector((-1, -1, -1)),
Vector((-1, -1, 1)),
Vector((-1, 1, -1)),
Vector((-1, 1, 1)),
Vector((1, -1, -1)),
Vector((1, -1, 1)),
Vector((1, 1, -1)),
Vector((1, 1, 1)),
]
edges = []
faces = [
[0, 2, 3, 1],
[2, 3, 7, 6],
[4, 5, 7, 6],
[0, 1, 5, 4],
[1, 3, 7, 5],
[0, 2, 6, 4],
]
mesh = bpy.data.meshes.new(name="Dumb Column")
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new("Column", mesh)
obj.name = "Column"
obj.location = self.location
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
obj.location[2] = self.collection_obj.location[2]
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(
obj=obj.name, ifc_class="IfcColumn", predefined_type="COLUMN", should_add_representation=False
)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name)
profile_set_usage = ifcopenshell.util.element.get_material(element)
bpy.ops.bim.add_representation(
obj=obj.name,
context_id=ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW").id(),
ifc_representation_class="IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage",
profile_set_usage=profile_set_usage.id(),
)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
bpy.ops.bim.switch_representation(obj=obj.name, ifc_definition_id=representation.id(), should_reload=True)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbColumn"})
MaterialData.load(self.file)
obj.select_set(True)
return obj
class DumbColumnRegenerator:
def regenerate_from_profile(self, usecase_path, ifc_file, settings):
self.file = IfcStore.get_file()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
profile = settings["profile"].Profile
if not profile:
return
for profile_set in [
mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile")
]:
for inverse in ifc_file.get_inverse(profile_set):
if not inverse.is_a("IfcMaterialProfileSetUsage"):
continue
if ifc_file.schema == "IFC2X3":
for rel in ifc_file.get_inverse(inverse):
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
self.change_profile(element)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
self.change_profile(element)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
if not new_material or not new_material.is_a("IfcMaterialProfileSet"):
return
self.change_profile(settings["related_object"])
def change_profile(self, element):
obj = IfcStore.get_element(element.id())
if not obj:
return
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if representation:
bpy.ops.bim.switch_representation(obj=obj.name, ifc_definition_id=representation.id(), should_reload=True)
@@ -28,6 +28,7 @@ def add_object(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
if self.file: if self.file:
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcGrid") bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcGrid")
collection.name = obj.name
grid = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) grid = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if has_site_collection: if has_site_collection:
site_obj = bpy.data.objects.get(grandchild.name) site_obj = bpy.data.objects.get(grandchild.name)
@@ -56,9 +57,10 @@ def add_object(self, context):
result = ifcopenshell.api.run( result = ifcopenshell.api.run(
"grid.create_grid_axis", "grid.create_grid_axis",
self.file, self.file,
**{"AxisTag": tag, "AxisCurve": obj, "UVWAxes": "UAxes", "Grid": grid}, **{"axis_tag": tag, "uvw_axes": "UAxes", "grid": grid},
) )
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"AxisCurve": obj, "grid_axis": result}) IfcStore.link_element(result, obj)
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": result})
obj.BIMObjectProperties.ifc_definition_id = result.id() obj.BIMObjectProperties.ifc_definition_id = result.id()
axes_collection = bpy.data.collections.new("VAxes") axes_collection = bpy.data.collections.new("VAxes")
@@ -77,13 +79,14 @@ def add_object(self, context):
axes_collection.objects.link(obj) axes_collection.objects.link(obj)
if IfcStore.get_file(): if self.file:
result = ifcopenshell.api.run( result = ifcopenshell.api.run(
"grid.create_grid_axis", "grid.create_grid_axis",
self.file, self.file,
**{"AxisTag": tag, "AxisCurve": obj, "UVWAxes": "VAxes", "Grid": grid}, **{"axis_tag": tag, "uvw_axes": "VAxes", "grid": grid},
) )
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"AxisCurve": obj, "grid_axis": result}) IfcStore.link_element(result, obj)
ifcopenshell.api.run("grid.create_axis_curve", self.file, **{"axis_curve": obj, "grid_axis": result})
obj.BIMObjectProperties.ifc_definition_id = result.id() obj.BIMObjectProperties.ifc_definition_id = result.id()
@@ -0,0 +1,62 @@
import bpy
import ifcopenshell
import ifcopenshell.api
from blenderbim.bim.module.model import product, wall, slab, column
from blenderbim.bim.ifc import IfcStore
from bpy.app.handlers import persistent
@persistent
def load_post(*args):
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.Product.GenerateBox", product.generate_box
)
IfcStore.add_element_listener(wall.element_listener)
ifcopenshell.api.add_pre_listener(
"geometry.add_representation", "BlenderBIM.DumbWall.EnsureSolid", wall.ensure_solid
)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbWall.GenerateAxis", wall.generate_axis
)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbWall.CalculateQuantities", wall.calculate_quantities
)
ifcopenshell.api.add_pre_listener(
"material.edit_layer", "BlenderBIM.DumbWall.RegenerateFromLayer", wall.DumbWallPlaner().regenerate_from_layer
)
ifcopenshell.api.add_pre_listener(
"type.assign_type", "BlenderBIM.DumbWall.RegenerateFromType", wall.DumbWallPlaner().regenerate_from_type
)
IfcStore.add_element_listener(slab.element_listener)
ifcopenshell.api.add_pre_listener(
"geometry.add_representation", "BlenderBIM.DumbSlab.EnsureSolid", slab.ensure_solid
)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbSlab.GenerateFootprint", slab.generate_footprint
)
ifcopenshell.api.add_post_listener(
"geometry.add_representation", "BlenderBIM.DumbSlab.CalculateQuantities", slab.calculate_quantities
)
ifcopenshell.api.add_pre_listener(
"material.edit_layer", "BlenderBIM.DumbSlab.RegenerateFromLayer", slab.DumbSlabPlaner().regenerate_from_layer
)
ifcopenshell.api.add_pre_listener(
"type.assign_type", "BlenderBIM.DumbSlab.RegenerateFromType", slab.DumbSlabPlaner().regenerate_from_type
)
IfcStore.add_element_listener(column.element_listener)
ifcopenshell.api.add_pre_listener(
"geometry.add_representation", "BlenderBIM.DumbColumn.EnsureSolid", column.ensure_solid
)
ifcopenshell.api.add_post_listener(
"material.edit_profile",
"BlenderBIM.DumbColumn.RegenerateFromProfile",
column.DumbColumnRegenerator().regenerate_from_profile,
)
ifcopenshell.api.add_post_listener(
"type.assign_type",
"BlenderBIM.DumbColumn.RegenerateFromType",
column.DumbColumnRegenerator().regenerate_from_type,
)
@@ -116,7 +116,7 @@ class VIEW3D_MT_PIE_bim(bpy.types.Menu):
def draw(self, context): def draw(self, context):
pie = self.layout.menu_pie() pie = self.layout.menu_pie()
pie.operator("bim.edit_object_placement") pie.operator("bim.edit_object_placement")
pie.operator("bim.update_mesh_representation") pie.operator("bim.update_representation")
pie.operator("bim.pie_add_opening") pie.operator("bim.pie_add_opening")
pie.operator("bim.pie_update_container") pie.operator("bim.pie_update_container")
pie.operator("bim.open_pie_class", text="Assign IFC Class") pie.operator("bim.open_pie_class", text="Assign IFC Class")
@@ -0,0 +1,96 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.representation
from . import wall, slab, column
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.pset.data import Data as PsetData
from mathutils import Vector
class AddTypeInstance(bpy.types.Operator):
bl_idname = "bim.add_type_instance"
bl_label = "Add Type Instance"
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty()
relating_type: bpy.props.IntProperty()
def execute(self, context):
tprops = context.scene.BIMTypeProperties
ifc_class = self.ifc_class or tprops.ifc_class
relating_type = self.relating_type or tprops.relating_type
if not ifc_class or not relating_type:
return {"FINISHED"}
self.file = IfcStore.get_file()
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema)[0]
if ifc_class == "IfcWallType":
obj = wall.DumbWallGenerator(self.file.by_id(int(relating_type))).generate()
if obj:
return {"FINISHED"}
elif ifc_class == "IfcSlabType":
obj = slab.DumbSlabGenerator(self.file.by_id(int(relating_type))).generate()
if obj:
return {"FINISHED"}
elif ifc_class == "IfcColumnType":
obj = column.DumbColumnGenerator(self.file.by_id(int(relating_type))).generate()
if obj:
return {"FINISHED"}
# A cube
verts = [
Vector((-1, -1, -1)),
Vector((-1, -1, 1)),
Vector((-1, 1, -1)),
Vector((-1, 1, 1)),
Vector((1, -1, -1)),
Vector((1, -1, 1)),
Vector((1, 1, -1)),
Vector((1, 1, 1)),
]
edges = []
faces = [
[0, 2, 3, 1],
[2, 3, 7, 6],
[4, 5, 7, 6],
[0, 1, 5, 4],
[1, 3, 7, 5],
[0, 2, 6, 4],
]
mesh = bpy.data.meshes.new(name="Instance")
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new("Instance", mesh)
obj.location = context.scene.cursor.location
collection = bpy.context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
collection_obj = bpy.data.objects.get(collection.name)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
bpy.ops.bim.assign_type(relating_type=int(tprops.relating_type), related_object=obj.name)
if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id:
obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box])
return {"FINISHED"}
def generate_box(usecase_path, ifc_file, settings):
box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW")
if not box_context:
return
obj = settings["blender_object"]
if 0 in list(obj.dimensions):
return
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
old_box = ifcopenshell.util.representation.get_representation(product, "Model", "Box", "MODEL_VIEW")
if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body":
if old_box:
bpy.ops.bim.remove_representation(representation_id=old_box.id(), obj=obj.name)
new_settings = settings.copy()
new_settings["context"] = box_context
new_box = ifcopenshell.api.run(
"geometry.add_representation", ifc_file, should_run_listeners=False, **new_settings
)
ifcopenshell.api.run(
"geometry.assign_representation",
ifc_file,
should_run_listeners=False,
**{"product": product, "representation": new_box}
)
@@ -0,0 +1,32 @@
import bpy
import ifcopenshell.util.type
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.ifc import IfcStore
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
relating_types_enum = []
def purge():
global relating_types_enum
relating_types_enum = []
def getRelatingTypes(self, context):
global relating_types_enum
if len(relating_types_enum) < 1:
elements = IfcStore.get_file().by_type("IfcWallType")
relating_types_enum.extend((str(e.id()), e.Name, "") for e in elements)
return relating_types_enum
class BIMModelProperties(PropertyGroup):
relating_type: EnumProperty(items=getRelatingTypes, name="Relating Type")
@@ -1,11 +1,231 @@
import bpy import bpy
from bpy.types import Operator import bmesh
from bpy.props import FloatProperty import math
from mathutils import Vector import ifcopenshell
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
import mathutils.geometry
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from math import pi, degrees
from mathutils import Vector, Matrix
from ifcopenshell.api.pset.data import Data as PsetData
from ifcopenshell.api.material.data import Data as MaterialData
from blenderbim.bim.module.geometry.helper import Helper
def add_object(self, context): def element_listener(element, obj):
blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
def mode_callback(obj, data):
for obj in bpy.context.selected_objects + [bpy.context.active_object]:
if (
obj.mode != "EDIT"
or not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab":
return
IfcStore.edited_objs.add(obj)
modifier = [m for m in obj.modifiers if m.type == "SOLIDIFY"]
if modifier:
return
depth = obj.dimensions.z
bm = bmesh.from_edit_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bm.faces.ensure_lookup_table()
non_bottom_faces = []
for face in bm.faces:
if face.normal.z > -0.9:
non_bottom_faces.append(face)
else:
face.normal_flip()
bmesh.ops.delete(bm, geom=non_bottom_faces, context="FACES")
bmesh.update_edit_mesh(obj.data)
bm.free()
modifier = obj.modifiers.new("Slab Depth", "SOLIDIFY")
modifier.use_even_offset = True
modifier.offset = 1
modifier.thickness = depth
def ensure_solid(usecase_path, ifc_file, settings):
product = ifc_file.by_id(settings["blender_object"].BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab":
return
settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
def generate_footprint(usecase_path, ifc_file, settings):
footprint_context = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "FootPrint", "SKETCH_VIEW")
if not footprint_context:
return
obj = settings["blender_object"]
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab":
return
old_footprint = ifcopenshell.util.representation.get_representation(product, "Plan", "FootPrint", "SKETCH_VIEW")
if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body":
if old_footprint:
bpy.ops.bim.remove_representation(representation_id=old_footprint.id(), obj=obj.name)
helper = Helper(ifc_file)
indices = helper.auto_detect_arbitrary_profile_with_voids_extruded_area_solid(settings["geometry"])
bm = bmesh.new()
bm.from_mesh(settings["geometry"])
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
profile_edges = []
def append_profile_edges(profile_edges, indices):
indices.append(indices[0]) # Close the loop
edge_vert_pairs = list(zip(indices, indices[1:]))
for p in edge_vert_pairs:
profile_edges.append(
[e for e in bm.verts[p[0]].link_edges if e.other_vert(bm.verts[p[0]]).index == p[1]][0]
)
append_profile_edges(profile_edges, indices["profile"])
for inner_indices in indices["inner_curves"]:
append_profile_edges(profile_edges, inner_indices)
irrelevant_edges = [e for e in bm.edges if e not in profile_edges]
bmesh.ops.delete(bm, geom=irrelevant_edges, context="EDGES")
mesh = bpy.data.meshes.new("Temporary Footprint")
bm.to_mesh(mesh)
bm.free()
new_settings = settings.copy()
new_settings["context"] = footprint_context
new_settings["geometry"] = mesh
new_footprint = ifcopenshell.api.run(
"geometry.add_representation", ifc_file, should_run_listeners=False, **new_settings
)
ifcopenshell.api.run(
"geometry.assign_representation",
ifc_file,
should_run_listeners=False,
**{"product": product, "representation": new_footprint}
)
bpy.data.meshes.remove(mesh)
def calculate_quantities(usecase_path, ifc_file, settings):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
obj = settings["blender_object"]
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab":
return
qto = ifcopenshell.api.run(
"pset.add_qto", ifc_file, should_run_listeners=False, product=product, name="Qto_SlabBaseQuantities"
)
length = obj.dimensions[0] / unit_scale
width = obj.dimensions[1] / unit_scale
depth = obj.dimensions[2] / unit_scale
perimeter = 0
helper = Helper(ifc_file)
indices = helper.auto_detect_arbitrary_profile_with_voids_extruded_area_solid(settings["geometry"])
bm = bmesh.new()
bm.from_mesh(settings["geometry"])
bm.verts.ensure_lookup_table()
def calculate_profile_length(indices):
indices.append(indices[0]) # Close the loop
edge_vert_pairs = list(zip(indices, indices[1:]))
return sum([(bm.verts[p[1]].co - bm.verts[p[0]].co).length for p in edge_vert_pairs])
perimeter += calculate_profile_length(indices["profile"])
for inner_indices in indices["inner_curves"]:
perimeter += calculate_profile_length(inner_indices)
bm.free()
if product.HasOpenings:
# TODO: calculate gross / net
gross_area = 0
net_area = 0
gross_volume = 0
net_volume = 0
else:
bm = bmesh.new()
bm.from_object(obj, bpy.context.evaluated_depsgraph_get())
bm.faces.ensure_lookup_table()
gross_area = sum([f.calc_area() for f in bm.faces if f.normal.z > 0.9])
net_area = gross_area
gross_volume = bm.calc_volume()
net_volume = gross_volume
bm.free()
properties={
"Depth": round(depth, 2),
"Perimeter": round(perimeter, 2),
"GrossArea": round(gross_area, 2),
"NetArea": round(net_area, 2),
"GrossVolume": round(gross_volume, 2),
"NetVolume": round(net_volume, 2),
}
if round(obj.dimensions[0] * obj.dimensions[1] * obj.dimensions[2], 2) == round(gross_volume, 2):
properties.update({
"Length": round(length, 2),
"Width": round(width, 2),
})
else:
properties.update({
"Length": None,
"Width": None,
})
ifcopenshell.api.run( "pset.edit_qto", ifc_file, should_run_listeners=False, qto=qto, properties=properties)
PsetData.load(ifc_file, obj.BIMObjectProperties.ifc_definition_id)
class DumbSlabGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
def generate(self):
self.file = IfcStore.get_file()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
thicknesses = []
for rel in self.relating_type.HasAssociations:
if rel.is_a("IfcRelAssociatesMaterial"):
material = rel.RelatingMaterial
if material.is_a("IfcMaterialLayerSet"):
thicknesses = [l.LayerThickness for l in material.MaterialLayers]
break
if not thicknesses:
return
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.depth = sum(thicknesses) * unit_scale
self.width = 3
self.length = 3
self.rotation = 0
self.location = Vector((0, 0, 0))
return self.derive_from_cursor()
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
return self.create_slab()
def create_slab(self):
verts = [ verts = [
Vector((0, 0, 0)), Vector((0, 0, 0)),
Vector((0, self.width, 0)), Vector((0, self.width, 0)),
@@ -13,7 +233,7 @@ def add_object(self, context):
Vector((self.length, 0, 0)), Vector((self.length, 0, 0)),
] ]
edges = [] edges = []
faces = [[0, 1, 2, 3]] faces = [[0, 3, 2, 1]]
mesh = bpy.data.meshes.new(name="Dumb Slab") mesh = bpy.data.meshes.new(name="Dumb Slab")
mesh.from_pydata(verts, edges, faces) mesh.from_pydata(verts, edges, faces)
@@ -23,24 +243,77 @@ def add_object(self, context):
modifier.offset = 1 modifier.offset = 1
modifier.thickness = self.depth modifier.thickness = self.depth
obj.name = "Slab" obj.name = "Slab"
context.view_layer.active_layer_collection.collection.objects.link(obj) obj.location = self.location
if IfcStore.get_file(): if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSlab", predefined_type="FLOOR") obj.location[2] = self.collection_obj.location[2] - self.depth
obj.location = context.scene.cursor.location else:
obj.location[2] -= self.depth
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(
obj=obj.name,
ifc_class="IfcSlab",
predefined_type="FLOOR",
ifc_representation_class="IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids",
)
bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbSlab"})
MaterialData.load(self.file)
obj.select_set(True)
return obj
class BIM_OT_add_object(Operator): class DumbSlabPlaner:
bl_idname = "mesh.add_slab" def regenerate_from_layer(self, usecase_path, ifc_file, settings):
bl_label = "Dumb Slab" self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
layer = settings["layer"]
thickness = settings["attributes"].get("LayerThickness")
if thickness is None:
return
for layer_set in layer.ToMaterialLayerSet:
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
if not total_thickness:
continue
for inverse in ifc_file.get_inverse(layer_set):
if not inverse.is_a("IfcMaterialLayerSetUsage"):
continue
if ifc_file.schema == "IFC2X3":
for rel in ifc_file.get_inverse(inverse):
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
self.change_thickness(element, thickness)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
self.change_thickness(element, thickness)
length: FloatProperty(name="Length", default=2) def regenerate_from_type(self, usecase_path, ifc_file, settings):
width: FloatProperty(name="Width", default=2) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
depth: FloatProperty(name="Depth", default=0.2) new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
return
new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers])
self.change_thickness(settings["related_object"], new_thickness)
def execute(self, context): def change_thickness(self, element, thickness):
add_object(self, context) parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric")
return {"FINISHED"} if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab":
return
obj = IfcStore.get_element(element.id())
if not obj:
return
def add_object_button(self, context): delta_thickness = (thickness * self.unit_scale) - obj.dimensions.z
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN") if round(delta_thickness, 2) == 0:
return
modifier = [m for m in obj.modifiers if m.type == "SOLIDIFY"]
if modifier:
modifier = modifier[0]
else:
pass
modifier.thickness += delta_thickness
obj.location[2] -= delta_thickness
@@ -0,0 +1,63 @@
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
class BIM_PT_authoring(Panel):
bl_idname = "BIM_PT_authoring"
bl_label = "Authoring"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
def draw(self, context):
tprops = context.scene.BIMTypeProperties
col = self.layout.column(align=True)
col.prop(tprops, "ifc_class", text="", icon="FILE_VOLUME")
col.prop(tprops, "relating_type", text="", icon="FILE_3D")
col.operator("bim.add_type_instance", icon="ADD")
class BIM_PT_authoring_architectural(Panel):
bl_label = "Architectural"
bl_idname = "BIM_PT_authoring_architectural"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
bl_parent_id = "BIM_PT_authoring"
def draw(self, context):
row = self.layout.row(align=True)
row.operator("bim.join_wall", icon="MOD_SKIN", text="T").join_type = "T"
row.operator("bim.join_wall", icon="MOD_SKIN", text="L").join_type = "L"
row.operator("bim.join_wall", icon="MOD_SKIN", text="V").join_type = "V"
row.operator("bim.join_wall", icon="X", text="").join_type = ""
row = self.layout.row(align=True)
row.operator("bim.align_wall", icon="ANCHOR_TOP", text="Ext.").align_type = "EXTERIOR"
row.operator("bim.align_wall", icon="ANCHOR_CENTER", text="C/L").align_type = "CENTERLINE"
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
row = self.layout.row(align=True)
row.operator("bim.flip_wall", icon="ORIENTATION_NORMAL", text="Flip")
row.operator("bim.split_wall", icon="MOD_PHYSICS", text="Split")
class BIM_PT_misc_utilities(Panel):
bl_idname = "BIM_PT_misc_utilities"
bl_label = "Miscellaneous"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
def draw(self, context):
layout = self.layout
props = context.scene.BIMProperties
row = layout.row()
row.prop(props, "override_colour", text="")
row = layout.row(align=True)
row.operator("bim.set_override_colour")
row = layout.row(align=True)
row.operator("bim.set_viewport_shadow_from_sun")
row = layout.row(align=True)
row.operator("bim.snap_spaces_together")
@@ -1,64 +1,911 @@
import bpy import bpy
from bpy.types import Operator import math
from bpy.props import FloatProperty, BoolProperty import bmesh
from mathutils import Vector import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.representation
import mathutils.geometry
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.pset.data import Data as PsetData
from ifcopenshell.api.material.data import Data as MaterialData
from math import pi, degrees
from mathutils import Vector, Matrix
def add_object(self, context): def element_listener(element, obj):
if self.use_plane: blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback)
verts = [
Vector((0, 0, 0)),
Vector((0, 0, self.height)),
Vector((self.length, 0, self.height)),
Vector((self.length, 0, 0)),
]
edges = []
faces = [[0, 1, 2, 3]]
else:
verts = [
Vector((0, 0, 0)),
Vector((self.length, 0, 0)),
]
edges = [[0, 1]]
faces = []
mesh = bpy.data.meshes.new(name="Dumb Wall")
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new("Wall", mesh)
context.view_layer.active_layer_collection.collection.objects.link(obj)
if not self.use_plane:
modifier = obj.modifiers.new("Wall Height", "SCREW")
modifier.angle = 0
modifier.screw_offset = self.height
modifier.use_smooth_shade = False
modifier.use_normal_calculate = True
modifier.use_normal_flip = True
modifier.steps = 1
modifier.render_steps = 1
modifier = obj.modifiers.new("Wall Width", "SOLIDIFY")
modifier.use_even_offset = True
modifier.thickness = self.width
obj.name = "Wall"
if IfcStore.get_file():
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcWall")
obj.location = context.scene.cursor.location
return obj
class BIM_OT_add_object(Operator): def mode_callback(obj, data):
bl_idname = "mesh.add_wall" for obj in bpy.context.selected_objects + [bpy.context.active_object]:
bl_label = "Dumb Wall" if (
obj.mode != "EDIT"
or not obj.data
or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve))
or not obj.BIMObjectProperties.ifc_definition_id
or not bpy.context.scene.BIMProjectProperties.is_authoring
):
return
product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
return
IfcStore.edited_objs.add(obj)
height: FloatProperty(name="Height", default=3)
length: FloatProperty(name="Length", default=1) class AddWall(bpy.types.Operator):
width: FloatProperty(name="Width", default=0.2) bl_idname = "bim.add_wall"
use_plane: BoolProperty(name="Use Plane", default=False) bl_label = "Add Wall"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
def execute(self, context): def execute(self, context):
add_object(self, context) props = context.scene.BIMModelProperties
bpy.ops.bim.add_type_instance(ifc_class="IfcWallType", relating_type=int(props.relating_type))
return {"FINISHED"} return {"FINISHED"}
def add_object_button(self, context): class JoinWall(bpy.types.Operator):
self.layout.operator(BIM_OT_add_object.bl_idname, icon="PLUGIN") bl_idname = "bim.join_wall"
bl_label = "Join Wall"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
def execute(self, context):
selected_objs = context.selected_objects
if len(selected_objs) == 0:
return {"FINISHED"}
if not self.join_type:
for obj in selected_objs:
DumbWallJoiner(obj, obj).unjoin()
return {"FINISHED"}
if len(selected_objs) < 2 or not context.active_object:
return {"FINISHED"}
for obj in selected_objs:
if obj == context.active_object:
continue
joiner = DumbWallJoiner(obj, context.active_object)
if self.join_type == "T":
joiner.join_T()
elif self.join_type == "L":
joiner.join_L()
elif self.join_type == "V":
joiner.join_V()
IfcStore.edited_objs.add(obj)
if self.join_type != "T":
IfcStore.edited_objs.add(context.active_object)
return {"FINISHED"}
class AlignWall(bpy.types.Operator):
bl_idname = "bim.align_wall"
bl_label = "Align Wall"
bl_options = {"REGISTER", "UNDO"}
align_type: bpy.props.StringProperty()
def execute(self, context):
selected_objs = context.selected_objects
if len(selected_objs) < 2 or not context.active_object:
return {"FINISHED"}
for obj in selected_objs:
if obj == context.active_object:
continue
aligner = DumbWallAligner(obj, context.active_object)
if self.align_type == "CENTERLINE":
aligner.align_centerline()
elif self.align_type == "EXTERIOR":
aligner.align_first_layer()
elif self.align_type == "INTERIOR":
aligner.align_last_layer()
IfcStore.edited_objs.add(obj)
return {"FINISHED"}
class FlipWall(bpy.types.Operator):
bl_idname = "bim.flip_wall"
bl_label = "Flip Wall"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
selected_objs = context.selected_objects
if len(selected_objs) == 0:
return {"FINISHED"}
for obj in selected_objs:
DumbWallFlipper(obj).flip()
IfcStore.edited_objs.add(obj)
return {"FINISHED"}
class SplitWall(bpy.types.Operator):
bl_idname = "bim.split_wall"
bl_label = "Split Wall"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
selected_objs = context.selected_objects
if len(selected_objs) == 0:
return {"FINISHED"}
for obj in selected_objs:
DumbWallSplitter(obj, bpy.context.scene.cursor.location).split()
IfcStore.edited_objs.add(obj)
return {"FINISHED"}
def recalculate_dumb_wall_origin(wall, new_origin=None):
if new_origin is None:
new_origin = wall.matrix_world @ Vector(wall.bound_box[0])
if (wall.matrix_world.translation - new_origin).length < 0.001:
return
wall.data.transform(
Matrix.Translation(
(wall.matrix_world.inverted().to_quaternion() @ (wall.matrix_world.translation - new_origin))
)
)
wall.matrix_world.translation = new_origin
class DumbWallSplitter:
def __init__(self, wall, point):
self.wall = wall
self.point = point
def split(self):
recalculate_dumb_wall_origin(self.wall)
self.point = self.determine_split_point()
if not self.point:
return
new_wall = self.duplicate_wall()
self.snap_end_face_to_point(self.wall, "max")
self.snap_end_face_to_point(new_wall, "min")
def determine_split_point(self):
start = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
end = self.wall.matrix_world @ Vector(self.wall.bound_box[4])
point, distance = mathutils.geometry.intersect_point_line(self.point, start, end)
if round(distance, 2) <= 0 or round(distance, 2) >= 1:
return # The split point is not on the wall
return point
def duplicate_wall(self):
new = self.wall.copy()
self.wall.users_collection[0].objects.link(new)
bpy.ops.bim.copy_class(obj=new.name)
return new
def snap_end_face_to_point(self, wall, which_end):
bm = bmesh.new()
bm.from_mesh(wall.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
min_face, max_face = self.get_wall_end_faces(wall, bm)
face = min_face if which_end == "min" else max_face
local_point = wall.matrix_world.inverted() @ self.point
for vert in face.verts:
vert.co.x = local_point.x
bm.to_mesh(wall.data)
wall.data.update()
bm.free()
IfcStore.edited_objs.add(wall)
# An end face is a quad that is on one end of the wall or the other. It must
# have at least one vertex on either extreme X-axis, and a non-insignificant
# X component of its face normal
def get_wall_end_faces(self, wall, bm):
min_face = None
max_face = None
min_x = min([v[0] for v in wall.bound_box])
max_x = max([v[0] for v in wall.bound_box])
bm.faces.ensure_lookup_table()
for f in bm.faces:
for v in f.verts:
if v.co.x == min_x and abs(f.normal.x) > 0.1:
min_face = f
elif v.co.x == max_x and abs(f.normal.x) > 0.1:
max_face = f
if min_face and max_face:
break
return min_face, max_face
class DumbWallFlipper:
# A flip switches the origin from the min XY corner to the max XY corner, and rotates the origin by 180.
def __init__(self, wall):
self.wall = wall
def flip(self):
if (
self.wall.matrix_world.translation - self.wall.matrix_world @ Vector(self.wall.bound_box[0])
).length < 0.001:
recalculate_dumb_wall_origin(self.wall, self.wall.matrix_world @ Vector(self.wall.bound_box[7]))
self.rotate_wall_180()
else:
recalculate_dumb_wall_origin(self.wall)
def rotate_wall_180(self):
flip_matrix = Matrix.Rotation(pi, 4, "Z")
self.wall.data.transform(flip_matrix)
self.wall.rotation_euler.rotate(flip_matrix)
class DumbWallAligner:
# An alignment shifts the origin of all walls to the closest point on the
# local X axis of the reference wall. In addition, the Z rotation is copied.
# Z translations are ignored for alignment.
def __init__(self, wall, reference_wall):
self.wall = wall
self.reference_wall = reference_wall
def align_centerline(self):
recalculate_dumb_wall_origin(self.wall)
recalculate_dumb_wall_origin(self.reference_wall)
self.align_rotation()
width = (Vector(self.wall.bound_box[3]) - Vector(self.wall.bound_box[0])).y
reference_width = (Vector(self.reference_wall.bound_box[3]) - Vector(self.reference_wall.bound_box[0])).y
if self.is_rotation_flipped():
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, -(reference_width / 2) - (width / 2), 0))
else:
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, (reference_width / 2) - (width / 2), 0))
self.align(
self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[0]),
self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[4]),
offset,
)
def align_last_layer(self):
recalculate_dumb_wall_origin(self.wall)
recalculate_dumb_wall_origin(self.reference_wall)
self.align_rotation()
if self.is_rotation_flipped():
DumbWallFlipper(self.wall).flip()
bpy.context.view_layer.update()
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[3])
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[7])
wall_width = (Vector(self.wall.bound_box[3]) - Vector(self.wall.bound_box[0])).y
offset = self.wall.matrix_world.to_quaternion() @ Vector((0, -wall_width, 0))
self.align(start, end, offset)
def align_first_layer(self):
recalculate_dumb_wall_origin(self.wall)
recalculate_dumb_wall_origin(self.reference_wall)
self.align_rotation()
if self.is_rotation_flipped():
DumbWallFlipper(self.wall).flip()
bpy.context.view_layer.update()
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[0])
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[4])
self.align(start, end)
def align(self, start, end, offset=None):
if offset is None:
offset = Vector((0, 0, 0))
point, distance = mathutils.geometry.intersect_point_line(self.wall.matrix_world.translation, start, end)
new_origin = point + offset
self.wall.matrix_world.translation[0] = new_origin[0]
self.wall.matrix_world.translation[1] = new_origin[1]
def align_rotation(self):
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
wall = (self.wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
angle = reference.angle_signed(wall)
if round(degrees(angle) % 360) in (0, 180):
return
elif angle > (pi / 2):
self.wall.rotation_euler[2] -= pi - angle
else:
self.wall.rotation_euler[2] += angle
bpy.context.view_layer.update()
def is_rotation_flipped(self):
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
wall = (self.wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
angle = reference.angle_signed(wall)
return round(degrees(angle) % 360) == 180
class DumbWallJoiner:
# A dumb wall is a prismatic wall along its local X axis.
# Given two dumb walls, there are three types of wall joints.
# 1. T-junction joints
# 2. L-junction "butt" joints
# 3. V-junction "mitre" joints
# The algorithms that handle all joints rely on three fundamental functions.
# 1. Identify faces at either end of the wall, called "end faces".
# 2. Given an "end face", identify a side "target face" of the other wall
# to project towards.
# 3. Project the vertices of an "end face" to the "target face".
def __init__(self, wall1, wall2):
self.wall1 = wall1
self.wall2 = wall2
self.should_project_to_frontface = True
self.should_attempt_v_junction_projection = False
self.initialise_convenience_variables()
def initialise_convenience_variables(self):
self.wall1_matrix = self.wall1.matrix_world
self.wall2_matrix = self.wall2.matrix_world
self.pos_x = self.wall1_matrix.to_quaternion() @ Vector((1, 0, 0))
self.neg_x = self.wall1_matrix.to_quaternion() @ Vector((-1, 0, 0))
# Unjoining a wall geometrically means to flatten the ends of the wall to
# remove any mitred angle from it.
def unjoin(self):
wall1_min_faces, wall1_max_faces = self.get_wall_end_faces(self.wall1)
min_x = min([v[0] for v in self.wall1.bound_box])
max_x = max([v[0] for v in self.wall1.bound_box])
for face in wall1_min_faces:
for v in face.vertices:
self.wall1.data.vertices[v].co[0] = min_x
for face in wall1_max_faces:
for v in face.vertices:
self.wall1.data.vertices[v].co[0] = max_x
self.recalculate_origins()
# A T-junction is an ordered operation where a single end of wall1 is joined
# to wall2 if possible (i.e. walls aren't parallel). Wall2 is not modified.
# First, wall1 end faces are identified. We attempt to project an end face
# at both ends to a front face of wall2. We then choose the end face that
# has the shortest projection distance, and project it.
def join_T(self):
self._join_T()
self.recalculate_origins()
def _join_T(self):
wall1_min_faces, wall1_max_faces = self.get_wall_end_faces(self.wall1)
wall2_end_faces1, wall2_end_faces2 = self.get_wall_end_faces(self.wall2)
self.wall2_end_faces = wall2_end_faces1 + wall2_end_faces2
ef1_distance, ef1_target_frontface, ef1_target_backface = self.get_projection_target(wall1_min_faces, 1)
ef2_distance, ef2_target_frontface, ef2_target_backface = self.get_projection_target(wall1_max_faces, 2)
# Large distances probably means rounding issues which lead to very long projections
if ef1_distance and ef1_distance > 50:
ef1_distance = None
if ef2_distance and ef2_distance > 50:
ef2_distance = None
# Project only the end faces that are closer to their target
if ef1_distance and ef2_distance is None:
self.project_end_faces(wall1_min_faces, ef1_target_frontface, ef1_target_backface)
return (wall1_min_faces, ef1_target_frontface, ef1_target_backface)
elif ef2_distance and ef1_distance is None:
self.project_end_faces(wall1_max_faces, ef2_target_frontface, ef2_target_backface)
return (wall1_max_faces, ef2_target_frontface, ef2_target_backface)
elif ef1_distance is None and ef2_distance is None:
return (None, None, None) # Life is short. BIM is hard.
elif ef1_distance < ef2_distance:
self.project_end_faces(wall1_min_faces, ef1_target_frontface, ef1_target_backface)
return (wall1_min_faces, ef1_target_frontface, ef1_target_backface)
else:
self.project_end_faces(wall1_max_faces, ef2_target_frontface, ef2_target_backface)
return (wall1_max_faces, ef2_target_frontface, ef2_target_backface)
# An L-junction is ordered operation where a single end of wall1 is joined
# to the backface of a side of wall2, and then a single end of wall2 is
# joined back to wall1 as a regular T-junction.
def join_L(self):
self.should_project_to_frontface = False
self._join_T()
self.swap_walls()
self.should_project_to_frontface = True
self._join_T()
self.recalculate_origins()
# A V-junction is an unordered operation where wall1 is joined to wall2,
# then vice versa. First, we do a T-junction from wall1 to wall2, then vice
# versa. This creates a junction where the inner vertices of the mitre joint
# touches, but the outer vertices do not. So, we just loop through the end
# point vertices of each wall, find outer vertices (i.e. vertices that don't
# touch the other wall), then continue projecting those to the back face of
# the other wall.
def join_V(self):
wall2_end_faces, wall2_target_frontface, wall2_target_backface = self._join_T()
self.swap_walls()
wall1_end_faces, wall1_target_frontface, wall1_target_backface = self._join_T()
for face in wall1_end_faces or []:
for v in face.vertices:
global_co = self.wall1_matrix @ self.wall1.data.vertices[v].co
if self.wall2.closest_point_on_mesh(self.wall2_matrix.inverted() @ global_co, distance=0.001)[0]:
continue # Vertex is already coincident with other wall, do not mitre
target_face_center = self.wall2_matrix @ wall1_target_backface.center
target_face_normal = (self.wall2_matrix.to_quaternion() @ wall1_target_backface.normal).normalized()
self.project_vertex(v, target_face_center, target_face_normal, self.wall1, self.wall1_matrix)
self.swap_walls()
for face in wall2_end_faces or []:
for v in face.vertices:
global_co = self.wall1_matrix @ self.wall1.data.vertices[v].co
if self.wall2.closest_point_on_mesh(self.wall2_matrix.inverted() @ global_co, distance=0.001)[0]:
continue # Vertex is already coincident with other wall, do not mitre
target_face_center = self.wall2_matrix @ wall2_target_backface.center
target_face_normal = (self.wall2_matrix.to_quaternion() @ wall2_target_backface.normal).normalized()
self.project_vertex(v, target_face_center, target_face_normal, self.wall1, self.wall1_matrix)
self.recalculate_origins()
def recalculate_origins(self):
bpy.context.view_layer.update()
recalculate_dumb_wall_origin(self.wall1)
recalculate_dumb_wall_origin(self.wall2)
def swap_walls(self):
self.wall1, self.wall2 = self.wall2, self.wall1
self.initialise_convenience_variables()
def project_end_faces(self, end_faces, target_frontface, target_backface):
target_face = target_frontface if self.should_project_to_frontface else target_backface
target_face_center = self.wall2_matrix @ target_face.center
target_face_normal = (self.wall2_matrix.to_quaternion() @ target_face.normal).normalized()
for end_face in end_faces:
for v in end_face.vertices:
self.project_vertex(v, target_face_center, target_face_normal, self.wall1, self.wall1_matrix)
def project_vertex(self, v, target_face_center, target_face_normal, wall, wall_matrix):
original_point = wall_matrix @ wall.data.vertices[v].co
point = mathutils.geometry.intersect_line_plane(
original_point,
(original_point) + self.pos_x,
target_face_center,
target_face_normal,
)
if not point or (point - original_point).length > 50:
return
local_point = wall_matrix.inverted() @ point
wall.data.vertices[v].co = local_point
# A projection target face is a side face on the target wall that has a
# significant local Y component to its normal (i.e. is not pointing up or
# down or something). In addition, its plane must intersect with the
# projection vector of an end face. Finally, the projection vector and the
# normal of the target face must not be acute.
def get_projection_target(self, end_faces, which_end):
if not end_faces:
return (None, None, None)
# Get a single end face as a sample.
f1 = end_faces[0]
f1_center = self.wall1_matrix @ f1.center
if which_end == 1:
outwards = self.neg_x
inwards = self.pos_x
elif which_end == 2:
outwards = self.pos_x
inwards = self.neg_x
distance = None
target_frontface = None
target_backface = None
for f2 in self.wall2.data.polygons:
if abs(f2.normal.y) < 0.75:
continue # Probably not a side wall
if f2 in self.wall2_end_faces:
continue
# Can we project the end face to the target face?
f2_center = self.wall2_matrix @ f2.center
f1_center_offset_x = f1_center + outwards
f2_normal = (self.wall2_matrix.to_quaternion() @ f2.normal).normalized()
point = mathutils.geometry.intersect_line_plane(
f1_center,
f1_center_offset_x,
f2_center,
f2_normal,
)
if not point:
continue # We can't project to the face at all
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(
point, f1_center, f1_center_offset_x
)
raycast_direction = outwards if signed_distance > 0 else inwards
if raycast_direction == outwards and f2_normal.angle(raycast_direction) < math.pi / 2:
target_backface = f2 # f2 is on the wrong side of the wall
elif raycast_direction == inwards and f2_normal.angle(raycast_direction) > math.pi / 2:
target_backface = f2 # f2 is on the wrong side of the wall
else:
target_frontface = f2
distance = (point - f1_center).length
if distance is not None and target_frontface is not None and target_backface is not None:
return (distance, target_frontface, target_backface)
return (None, None, None)
# An end face is a set of faces that represents either one end of the wall or
# the other. There is typically only 1 quad or 2 tris for each end.
# An end face is defined as having at least one vertex on either extreme
# X-axis, and a non-insignificant X component of its face normal
def get_wall_end_faces(self, wall):
min_faces = []
max_faces = []
min_x = min([v[0] for v in wall.bound_box])
max_x = max([v[0] for v in wall.bound_box])
for f in wall.data.polygons:
end_face_index = self.get_wall_face_end(wall, f, min_x, max_x)
if end_face_index == 1:
min_faces.append(f)
elif end_face_index == 2:
max_faces.append(f)
return (min_faces, max_faces)
# 1 is the leftmost (minimum local X axis) end, and 2 is the rightmost end
def get_wall_face_end(self, wall, face, min_x, max_x):
for v in face.vertices:
if wall.data.vertices[v].co.x == min_x and abs(face.normal.x) > 0.1:
return 1
if wall.data.vertices[v].co.x == max_x and abs(face.normal.x) > 0.1:
return 2
class DumbWallGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
def generate(self):
self.file = IfcStore.get_file()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
thicknesses = []
for rel in self.relating_type.HasAssociations:
if rel.is_a("IfcRelAssociatesMaterial"):
material = rel.RelatingMaterial
if material.is_a("IfcMaterialLayerSet"):
thicknesses = [l.LayerThickness for l in material.MaterialLayers]
break
if not thicknesses:
return
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.width = sum(thicknesses) * unit_scale
self.height = 3
self.length = 1
self.rotation = 0
self.location = Vector((0, 0, 0))
if self.has_sketch():
return self.derive_from_sketch()
return self.derive_from_cursor()
def has_sketch(self):
return (
bpy.context.scene.grease_pencil
and len(bpy.context.scene.grease_pencil.layers) == 1
and bpy.context.scene.grease_pencil.layers[0].active_frame.strokes
)
def derive_from_sketch(self):
objs = []
strokes = []
layer = bpy.context.scene.grease_pencil.layers[0]
for stroke in layer.active_frame.strokes:
if len(stroke.points) == 1:
continue
coords = (stroke.points[0].co, stroke.points[-1].co)
direction = coords[1] - coords[0]
length = direction.length
if length < 0.1:
continue
data = {"coords": coords}
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
self.rotation = math.atan2(direction[1], direction[0])
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_wall()
strokes.append(data)
objs.append(data["obj"])
if len(objs) < 2:
return objs
l_joins = set()
for stroke in strokes:
if not stroke["obj"]:
continue
for stroke2 in strokes:
if stroke2 == stroke or not stroke2["obj"]:
continue
if self.has_nearby_ends(stroke, stroke2):
wall_join = "-JOIN-".join(sorted([stroke["obj"].name, stroke2["obj"].name]))
if wall_join not in l_joins:
l_joins.add(wall_join)
DumbWallJoiner(stroke["obj"], stroke2["obj"]).join_L()
elif self.has_end_near_stroke(stroke, stroke2):
DumbWallJoiner(stroke["obj"], stroke2["obj"]).join_T()
bpy.context.scene.grease_pencil.layers.remove(layer)
return objs
def has_end_near_stroke(self, stroke, stroke2):
point, distance = mathutils.geometry.intersect_point_line(stroke["coords"][0], *stroke2["coords"])
if distance > 0 and distance < 1 and self.is_near(point, stroke["coords"][0]):
return True
point, distance = mathutils.geometry.intersect_point_line(stroke["coords"][1], *stroke2["coords"])
if distance > 0 and distance < 1 and self.is_near(point, stroke["coords"][1]):
return True
def has_nearby_ends(self, stroke, stroke2):
return (
self.is_near(stroke["coords"][0], stroke2["coords"][0])
or self.is_near(stroke["coords"][0], stroke2["coords"][1])
or self.is_near(stroke["coords"][1], stroke2["coords"][0])
or self.is_near(stroke["coords"][1], stroke2["coords"][1])
)
def is_near(self, point1, point2):
return (point1 - point2).length < 0.1
def derive_from_cursor(self):
self.location = bpy.context.scene.cursor.location
if self.collection:
for sibling_obj in self.collection.objects:
if not isinstance(sibling_obj.data, bpy.types.Mesh):
continue
if "IfcWall" not in sibling_obj.name:
continue
local_location = sibling_obj.matrix_world.inverted() @ self.location
raycast = sibling_obj.closest_point_on_mesh(local_location, distance=0.01)
if not raycast[0]:
continue
for face in sibling_obj.data.polygons:
if (
abs(face.normal.y) >= 0.75
and abs(mathutils.geometry.distance_point_to_plane(local_location, face.center, face.normal))
< 0.01
):
# Rotate the wall in the direction of the face normal
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
self.rotation = math.atan2(normal[1], normal[0])
break
return self.create_wall()
def create_wall(self):
verts = [
Vector((0, self.width, 0)),
Vector((0, 0, 0)),
Vector((0, self.width, self.height)),
Vector((0, 0, self.height)),
Vector((self.length, self.width, 0)),
Vector((self.length, 0, 0)),
Vector((self.length, self.width, self.height)),
Vector((self.length, 0, self.height)),
]
faces = [
[1, 3, 2, 0],
[4, 6, 7, 5],
[1, 0, 4, 5],
[3, 7, 6, 2],
[0, 2, 6, 4],
[1, 5, 7, 3],
]
mesh = bpy.data.meshes.new(name="Wall")
mesh.from_pydata(verts, [], faces)
obj = bpy.data.objects.new("Wall", mesh)
obj.location = self.location
obj.rotation_euler[2] = self.rotation
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
obj.location[2] = self.collection_obj.location[2]
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(
obj=obj.name,
ifc_class="IfcWall",
ifc_representation_class="IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef",
)
bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name)
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbWall"})
MaterialData.load(self.file)
obj.select_set(True)
return obj
def ensure_solid(usecase_path, ifc_file, settings):
product = ifc_file.by_id(settings["blender_object"].BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
return
settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef"
def generate_axis(usecase_path, ifc_file, settings):
axis_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Axis", "GRAPH_VIEW")
if not axis_context:
return
obj = settings["blender_object"]
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
return
old_axis = ifcopenshell.util.representation.get_representation(product, "Model", "Axis", "GRAPH_VIEW")
if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body":
if old_axis:
bpy.ops.bim.remove_representation(representation_id=old_axis.id(), obj=obj.name)
new_settings = settings.copy()
new_settings["context"] = axis_context
mesh = bpy.data.meshes.new("Temporary Axis")
start = Vector(obj.bound_box[0])
end = Vector(obj.bound_box[4])
mesh.from_pydata([start, end], [(0, 1)], [])
new_settings["geometry"] = mesh
new_axis = ifcopenshell.api.run(
"geometry.add_representation", ifc_file, should_run_listeners=False, **new_settings
)
ifcopenshell.api.run(
"geometry.assign_representation",
ifc_file,
should_run_listeners=False,
**{"product": product, "representation": new_axis}
)
bpy.data.meshes.remove(mesh)
def calculate_quantities(usecase_path, ifc_file, settings):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
obj = settings["blender_object"]
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
return
qto = ifcopenshell.api.run(
"pset.add_qto", ifc_file, should_run_listeners=False, product=product, name="Qto_WallBaseQuantities"
)
length = obj.dimensions[0] / unit_scale
width = obj.dimensions[1] / unit_scale
height = obj.dimensions[2] / unit_scale
if product.HasOpenings:
# TODO: calculate gross / net
gross_footprint_area = 0
net_footprint_area = 0
gross_side_area = 0
net_side_area = 0
gross_volume = 0
net_volume = 0
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.faces.ensure_lookup_table()
gross_footprint_area = sum([f.calc_area() for f in bm.faces if f.normal.z < -0.9])
net_footprint_area = gross_footprint_area
gross_side_area = sum([f.calc_area() for f in bm.faces if f.normal.y > 0.9])
net_side_area = gross_side_area
gross_volume = bm.calc_volume()
net_volume = gross_volume
bm.free()
ifcopenshell.api.run(
"pset.edit_qto",
ifc_file,
should_run_listeners=False,
qto=qto,
properties={
"Length": round(length, 2),
"Width": round(width, 2),
"Height": round(height, 2),
"GrossFootprintArea": round(gross_footprint_area, 2),
"NetFootprintArea": round(net_footprint_area, 2),
"GrossSideArea": round(gross_side_area, 2),
"NetSideArea": round(net_side_area, 2),
"GrossVolume": round(gross_volume, 2),
"NetVolume": round(net_volume, 2),
},
)
PsetData.load(ifc_file, obj.BIMObjectProperties.ifc_definition_id)
class DumbWallPlaner:
def regenerate_from_layer(self, usecase_path, ifc_file, settings):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
layer = settings["layer"]
thickness = settings["attributes"].get("LayerThickness")
if thickness is None:
return
for layer_set in layer.ToMaterialLayerSet:
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
if not total_thickness:
continue
for inverse in ifc_file.get_inverse(layer_set):
if not inverse.is_a("IfcMaterialLayerSetUsage"):
continue
if ifc_file.schema == "IFC2X3":
for rel in ifc_file.get_inverse(inverse):
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
self.change_thickness(element, thickness)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
self.change_thickness(element, thickness)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
new_material = ifcopenshell.util.element.get_material(settings["relating_type"])
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
return
new_thickness = sum([l.LayerThickness for l in new_material.MaterialLayers])
self.change_thickness(settings["related_object"], new_thickness)
def change_thickness(self, element, thickness):
parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric")
if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall":
return
obj = IfcStore.get_element(element.id())
if not obj:
return
delta_thickness = (thickness * self.unit_scale) - obj.dimensions.y
if round(delta_thickness, 2) == 0:
return
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
min_face, max_face = self.get_wall_end_faces(obj, bm)
self.thicken_face(min_face, delta_thickness)
self.thicken_face(max_face, delta_thickness)
bm.to_mesh(obj.data)
obj.data.update()
bm.free()
IfcStore.edited_objs.add(obj)
def thicken_face(self, face, delta_thickness):
slide_magnitude = abs(delta_thickness) / 2
for vert in face.verts:
slide_vector = None
for edge in vert.link_edges:
other_vert = edge.verts[1] if edge.verts[0] == vert else edge.verts[0]
if delta_thickness > 0:
potential_slide_vector = vert.co - other_vert.co
else:
potential_slide_vector = other_vert.co - vert.co
if abs(potential_slide_vector.x) > 0.9 or abs(potential_slide_vector.z) > 0.9:
continue
slide_vector = potential_slide_vector
break
if not slide_vector:
continue
slide_vector *= slide_magnitude / abs(slide_vector.y)
vert.co += slide_vector
# An end face is a quad that is on one end of the wall or the other. It must
# have at least one vertex on either extreme X-axis, and a non-insignificant
# X component of its face normal
def get_wall_end_faces(self, wall, bm):
min_face = None
max_face = None
min_x = min([v[0] for v in wall.bound_box])
max_x = max([v[0] for v in wall.bound_box])
bm.faces.ensure_lookup_table()
for f in bm.faces:
for v in f.verts:
if v.co.x == min_x and abs(f.normal.x) > 0.1:
min_face = f
elif v.co.x == max_x and abs(f.normal.x) > 0.1:
max_face = f
if min_face and max_face:
break
return min_face, max_face
@@ -0,0 +1,58 @@
import bpy
from bpy.types import WorkSpaceTool
class WallTool(WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.wall_tool"
bl_label = "Wall Tool"
bl_description = "Gives you wall related superpowers"
bl_icon = "ops.generic.select_circle"
bl_widget = None
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
bl_keymap = (
# ("bim.wall_tool_op", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
("bim.add_wall", {"type": "A", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.join_wall", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("join_type", "T")]}),
("bim.join_wall", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("join_type", "L")]}),
("bim.join_wall", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("join_type", "V")]}),
("bim.flip_wall", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.split_wall", {"type": "S", "value": "PRESS", "shift": True}, {"properties": []}),
(
"bim.align_wall",
{"type": "X", "value": "PRESS", "shift": True},
{"properties": [("align_type", "EXTERIOR")]},
),
(
"bim.align_wall",
{"type": "C", "value": "PRESS", "shift": True},
{"properties": [("align_type", "CENTERLINE")]},
),
(
"bim.align_wall",
{"type": "V", "value": "PRESS", "shift": True},
{"properties": [("align_type", "INTERIOR")]},
),
)
def draw_settings(context, layout, tool):
props = context.scene.BIMModelProperties
row = layout.row(align=True)
row.prop(props, "relating_type", text="")
row.label(text="", icon="BLANK1")
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Add", icon="EVENT_A")
row.label(text="Extend", icon="EVENT_E")
row.label(text="Butt", icon="EVENT_T")
row.label(text="Mitre", icon="EVENT_Y")
row.label(text="Flip", icon="EVENT_F")
row.label(text="Split", icon="EVENT_S")
row.label(text="", icon="EVENT_X")
row.label(text="", icon="EVENT_C")
row.label(text="", icon="EVENT_V")
row.label(text="Align")
@@ -0,0 +1,15 @@
import bpy
from . import ui, operator
classes = (
operator.ActivateParametricEngine,
ui.BIM_PT_parametric,
)
def register():
pass
def unregister():
pass
@@ -0,0 +1,10 @@
import bpy
from blenderbim.bim.ifc import IfcStore
class ActivateParametricEngine(bpy.types.Operator):
bl_idname = "bim.activate_parametric_engine"
bl_label = "Activate Parametric Engine"
def execute(self, context):
return {"FINISHED"}
@@ -0,0 +1,22 @@
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.type.data import Data
class BIM_PT_parametric(Panel):
bl_label = "IFC Parametric Engines"
bl_idname = "BIM_PT_parametric"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def draw(self, context):
#props = context.active_object.BIMTypeProperties
row = self.layout.row(align=True)
#row.prop(props, "relating_type_class", text="")
row.operator("bim.activate_parametric_engine", icon="PLUGIN")
@@ -1,3 +1,4 @@
import os
import bpy import bpy
import json import json
@@ -46,7 +47,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
"output": context.scene.BIMPatchProperties.ifc_patch_output, "output": context.scene.BIMPatchProperties.ifc_patch_output,
"recipe": context.scene.BIMPatchProperties.ifc_patch_recipes, "recipe": context.scene.BIMPatchProperties.ifc_patch_recipes,
"arguments": json.loads(context.scene.BIMPatchProperties.ifc_patch_args or "[]"), "arguments": json.loads(context.scene.BIMPatchProperties.ifc_patch_args or "[]"),
"log": context.scene.BIMProperties.data_dir + "process.log", "log": os.path.join(context.scene.BIMProperties.data_dir, "process.log"),
} }
) )
return {"FINISHED"} return {"FINISHED"}
@@ -13,6 +13,9 @@ classes = (
operator.UnassignLibraryDeclaration, operator.UnassignLibraryDeclaration,
operator.SaveLibraryFile, operator.SaveLibraryFile,
operator.AppendLibraryElement, operator.AppendLibraryElement,
operator.EnableEditingHeader,
operator.DisableEditingHeader,
operator.EditHeader,
prop.LibraryElement, prop.LibraryElement,
prop.BIMProjectProperties, prop.BIMProjectProperties,
ui.BIM_PT_project, ui.BIM_PT_project,
@@ -3,6 +3,7 @@ import logging
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import bpy import bpy
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim import import_ifc from blenderbim.bim import import_ifc
@@ -143,7 +144,7 @@ class ChangeLibraryElement(bpy.types.Operator):
[ifc_classes.add(e.is_a()) for e in elements] [ifc_classes.add(e.is_a()) for e in elements]
while len(self.props.library_elements) > 0: while len(self.props.library_elements) > 0:
self.props.library_elements.remove(0) self.props.library_elements.remove(0)
if len(ifc_classes) == 1: if len(ifc_classes) == 1 and list(ifc_classes)[0] == self.element_name:
for element in elements: for element in elements:
new = self.props.library_elements.add() new = self.props.library_elements.add()
new.name = element.Name or "Unnamed" new.name = element.Name or "Unnamed"
@@ -232,9 +233,11 @@ class AppendLibraryElement(bpy.types.Operator):
element = ifcopenshell.api.run( element = ifcopenshell.api.run(
"project.append_asset", "project.append_asset",
IfcStore.get_file(), IfcStore.get_file(),
library=IfcStore.library_file,
element=IfcStore.library_file.by_id(self.definition), element=IfcStore.library_file.by_id(self.definition),
) )
self.import_type_from_ifc(element) self.import_type_from_ifc(element)
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"} return {"FINISHED"}
def import_type_from_ifc(self, element): def import_type_from_ifc(self, element):
@@ -255,3 +258,60 @@ class AppendLibraryElement(bpy.types.Operator):
ifc_importer.type_collection = type_collection ifc_importer.type_collection = type_collection
ifc_importer.create_type_product(element) ifc_importer.create_type_product(element)
ifc_importer.place_objects_in_spatial_tree() ifc_importer.place_objects_in_spatial_tree()
class EnableEditingHeader(bpy.types.Operator):
bl_idname = "bim.enable_editing_header"
bl_label = "Enable Editing Header"
def execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMProjectProperties
props.is_editing = True
mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description)
if "[" in mvd:
props.mvd = mvd.split("[")[1][0:-1]
else:
props.mvd = ""
author = self.file.wrapped_data.header.file_name.author
if author:
props.author_name = author[0]
if len(author) > 1:
props.author_email = author[1]
organisation = self.file.wrapped_data.header.file_name.organization
if organisation:
props.organisation_name = organisation[0]
if len(organisation) > 1:
props.organisation_email = organisation[1]
props.authorisation = self.file.wrapped_data.header.file_name.authorization
return {"FINISHED"}
class EditHeader(bpy.types.Operator):
bl_idname = "bim.edit_header"
bl_label = "Edit Header"
def execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMProjectProperties
props.is_editing = True
self.file.wrapped_data.header.file_description.description = (f'ViewDefinition[{props.mvd}]',)
self.file.wrapped_data.header.file_name.author = (props.author_name, props.author_email)
self.file.wrapped_data.header.file_name.organization = (props.organisation_name, props.organisation_email)
self.file.wrapped_data.header.file_name.authorization = props.authorisation
bpy.ops.bim.disable_editing_header()
return {"FINISHED"}
class DisableEditingHeader(bpy.types.Operator):
bl_idname = "bim.disable_editing_header"
bl_label = "Disable Editing Header"
def execute(self, context):
context.scene.BIMProjectProperties.is_editing = False
return {"FINISHED"}
@@ -21,6 +21,13 @@ class LibraryElement(PropertyGroup):
class BIMProjectProperties(PropertyGroup): class BIMProjectProperties(PropertyGroup):
is_authoring: BoolProperty(name="Enable Authoring Mode", default=True) is_authoring: BoolProperty(name="Enable Authoring Mode", default=True)
is_editing: BoolProperty(name="Is Editing", default=False)
mvd: StringProperty(name="MVD")
author_name: StringProperty(name="Author")
author_email: StringProperty(name="Author Email")
organisation_name: StringProperty(name="Organisation")
organisation_email: StringProperty(name="Organisation Email")
authorisation: StringProperty(name="Authoriser")
active_library_element: StringProperty(name="Enable Authoring Mode", default="") active_library_element: StringProperty(name="Enable Authoring Mode", default="")
library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty) library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty)
library_elements: CollectionProperty(name="Library Elements", type=LibraryElement) library_elements: CollectionProperty(name="Library Elements", type=LibraryElement)
@@ -28,10 +28,40 @@ class BIM_PT_project(Panel):
row.label(text=os.path.basename(props.ifc_file) or "No File Found") row.label(text=os.path.basename(props.ifc_file) or "No File Found")
if IfcStore.get_file(): if IfcStore.get_file():
row.prop(pprops, "is_authoring", icon="GREASEPENCIL", text="") row.prop(pprops, "is_authoring", icon="MODIFIER", text="")
if pprops.is_editing:
row.operator("bim.edit_header", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_header", icon="CANCEL", text="")
else:
row.operator("bim.enable_editing_header", icon="GREASEPENCIL", text="")
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text="IFC Schema", icon="FILE_CACHE") row.label(text="IFC Schema", icon="FILE_CACHE")
row.label(text=IfcStore.get_file().schema) row.label(text=IfcStore.get_file().schema)
if pprops.is_editing:
row = self.layout.row(align=True)
row.prop(pprops, "mvd")
row = self.layout.row(align=True)
row.prop(pprops, "author_name")
row = self.layout.row(align=True)
row.prop(pprops, "author_email")
row = self.layout.row(align=True)
row.prop(pprops, "organisation_name")
row = self.layout.row(align=True)
row.prop(pprops, "organisation_email")
row = self.layout.row(align=True)
row.prop(pprops, "authorisation")
else:
row = self.layout.row(align=True)
row.label(text="IFC MVD", icon="FILE_HIDDEN")
mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description)
if "[" in mvd:
mvd = mvd.split("[")[1][0:-1]
row.label(text=mvd)
else: else:
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text="File Not Loaded", icon="ERROR") row.label(text="File Not Loaded", icon="ERROR")
@@ -2,6 +2,9 @@ import bpy
import json import json
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.pset
import ifcopenshell.util.attribute
import blenderbim.bim.schema
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.pset.data import Data from ifcopenshell.api.pset.data import Data
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
@@ -31,33 +34,99 @@ class EnablePsetEditing(bpy.types.Operator):
obj = bpy.data.objects.get(self.obj) obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material": elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj) obj = bpy.data.materials.get(self.obj)
props = obj.PsetProperties self.props = obj.PsetProperties
while len(props.properties) > 0: while len(self.props.properties) > 0:
props.properties.remove(0) self.props.properties.remove(0)
data = Data.psets if self.pset_id in Data.psets else Data.qtos data = Data.psets if self.pset_id in Data.psets else Data.qtos
props.active_pset_name = data[self.pset_id]["Name"] pset_data = data[self.pset_id]
for prop in data[self.pset_id]["Properties"]: self.props.active_pset_name = pset_data["Name"]
new = props.properties.add()
new.name = prop["Name"]
new.is_null = prop["is_null"]
if prop["type"] == "string":
new.string_value = prop["value"] or ""
elif prop["type"] == "integer":
new.int_value = prop["value"] or 0
elif prop["type"] == "float":
new.float_value = prop["value"] or 0.0
elif prop["type"] == "boolean":
new.bool_value = prop["value"] or False
elif prop["type"] == "enum":
new.enum_items = json.dumps(prop["enum_items"])
if prop["value"]:
new.enum_value = prop["value"]
props.active_pset_id = self.pset_id pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(pset_data["Name"])
if pset_template:
self.load_from_pset_template(pset_template, pset_data)
else:
self.load_from_pset_data(pset_data)
self.props.active_pset_id = self.pset_id
return {"FINISHED"} return {"FINISHED"}
def load_from_pset_template(self, pset_template, pset_data):
data = {Data.properties[p]["Name"]: Data.properties[p]["NominalValue"] for p in pset_data["Properties"]}
for prop_template in pset_template.HasPropertyTemplates:
if not prop_template.is_a("IfcSimplePropertyTemplate"):
continue # Other types not yet supported
if prop_template.TemplateType == "P_SINGLEVALUE":
try:
data_type = ifcopenshell.util.attribute.get_primitive_type(
IfcStore.get_schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
)
except:
# TODO: Occurs if the data type is something that exists in IFC4 and not in IFC2X3. To fully fix
# this we need to generate the IFC2X3 pset template definitions.
continue
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
data_type = "enum"
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
elif prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]:
data_type = "float"
elif prop_template.TemplateType == "Q_COUNT":
data_type = "integer"
else:
continue # Other types not yet supported
new = self.props.properties.add()
new.name = prop_template.Name
new.is_null = data.get(prop_template.Name, None) is None
new.data_type = data_type
if data_type == "string":
new.string_value = "" if new.is_null else data[prop_template.Name]
elif data_type == "integer":
new.int_value = 0 if new.is_null else data[prop_template.Name]
elif data_type == "float":
new.float_value = 0.0 if new.is_null else data[prop_template.Name]
elif data_type == "boolean":
new.bool_value = False if new.is_null else data[prop_template.Name]
elif data_type == "enum":
new.enum_items = json.dumps(enum_items)
if data.get(prop_template.Name):
new.enum_value = data[prop_template.Name]
def load_from_pset_data(self, pset_data):
for prop_id in pset_data["Properties"]:
prop = Data.properties[prop_id]
value = prop["NominalValue"]
if isinstance(value, str):
data_type = "string"
elif isinstance(value, float):
data_type = "float"
elif isinstance(value, bool):
data_type = "boolean"
elif isinstance(value, int):
data_type = "integer"
else:
data_type = "string"
value = str(value)
new = self.props.properties.add()
new.name = prop["Name"]
new.is_null = prop["NominalValue"] is None
new.data_type = data_type
if data_type == "string":
new.string_value = "" if new.is_null else value
elif data_type == "integer":
new.int_value = 0 if new.is_null else value
elif data_type == "float":
new.float_value = 0.0 if new.is_null else value
elif data_type == "boolean":
new.bool_value = False if new.is_null else value
class DisablePsetEditing(bpy.types.Operator): class DisablePsetEditing(bpy.types.Operator):
bl_idname = "bim.disable_pset_editing" bl_idname = "bim.disable_pset_editing"
@@ -98,22 +167,19 @@ class EditPset(bpy.types.Operator):
properties = json.loads(self.properties) properties = json.loads(self.properties)
else: else:
data = Data.psets if pset_id in Data.psets else Data.qtos data = Data.psets if pset_id in Data.psets else Data.qtos
for prop in data[pset_id]["Properties"]: for prop in props.properties:
blender_prop = props.properties.get(prop["Name"]) if prop.is_null:
if not blender_prop: properties[prop.name] = None
continue elif prop.data_type == "string":
if blender_prop.is_null: properties[prop.name] = prop.string_value
properties[prop["Name"]] = None elif prop.data_type == "boolean":
elif prop["type"] == "string": properties[prop.name] = prop.bool_value
properties[prop["Name"]] = blender_prop.string_value elif prop.data_type == "integer":
elif prop["type"] == "boolean": properties[prop.name] = prop.int_value
properties[prop["Name"]] = blender_prop.bool_value elif prop.data_type == "float":
elif prop["type"] == "integer": properties[prop.name] = prop.float_value
properties[prop["Name"]] = blender_prop.int_value elif prop.data_type == "enum":
elif prop["type"] == "float": properties[prop.name] = prop.enum_value
properties[prop["Name"]] = blender_prop.float_value
elif prop["type"] == "enum":
properties[prop["Name"]] = blender_prop.enum_value
if pset_id in Data.psets: if pset_id in Data.psets:
ifcopenshell.api.run( ifcopenshell.api.run(
@@ -121,18 +187,21 @@ class EditPset(bpy.types.Operator):
self.file, self.file,
**{ **{
"pset": self.file.by_id(pset_id), "pset": self.file.by_id(pset_id),
"Name": props.active_pset_name, "name": props.active_pset_name,
"Properties": properties, "properties": properties,
}, },
) )
else: else:
for key, value in properties.items():
if isinstance(value, float):
properties[key] = round(value, 4)
ifcopenshell.api.run( ifcopenshell.api.run(
"pset.edit_qto", "pset.edit_qto",
self.file, self.file,
**{ **{
"qto": self.file.by_id(pset_id), "qto": self.file.by_id(pset_id),
"Name": props.active_pset_name, "name": props.active_pset_name,
"Properties": properties, "properties": properties,
}, },
) )
Data.load(IfcStore.get_file(), oprops.ifc_definition_id) Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
@@ -194,7 +263,7 @@ class AddPset(bpy.types.Operator):
self.file, self.file,
**{ **{
"product": self.file.by_id(oprops.ifc_definition_id), "product": self.file.by_id(oprops.ifc_definition_id),
"Name": pset_name, "name": pset_name,
}, },
) )
Data.load(IfcStore.get_file(), oprops.ifc_definition_id) Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
@@ -215,7 +284,7 @@ class AddQto(bpy.types.Operator):
self.file, self.file,
**{ **{
"product": self.file.by_id(oprops.ifc_definition_id), "product": self.file.by_id(oprops.ifc_definition_id),
"Name": props.qto_name, "name": props.qto_name,
}, },
) )
Data.load(IfcStore.get_file(), oprops.ifc_definition_id) Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
+33 -27
View File
@@ -6,6 +6,8 @@ from blenderbim.bim.ifc import IfcStore
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
box = layout.box() box = layout.box()
row = box.row(align=True) row = box.row(align=True)
if "is_expanded" not in pset:
pset["is_expanded"] = True
icon = "TRIA_DOWN" if pset["is_expanded"] else "TRIA_RIGHT" icon = "TRIA_DOWN" if pset["is_expanded"] else "TRIA_RIGHT"
row.operator("bim.toggle_pset_expansion", icon=icon, text="", emboss=False).pset_id = pset_id row.operator("bim.toggle_pset_expansion", icon=icon, text="", emboss=False).pset_id = pset_id
if not props.active_pset_id: if not props.active_pset_id:
@@ -29,25 +31,26 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
op = row.operator("bim.edit_pset", icon="CHECKMARK", text="") op = row.operator("bim.edit_pset", icon="CHECKMARK", text="")
op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name
op.obj_type = obj_type op.obj_type = obj_type
op = row.operator("bim.disable_pset_editing", icon="X", text="") op = row.operator("bim.disable_pset_editing", icon="CANCEL", text="")
op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name
op.obj_type = obj_type op.obj_type = obj_type
if pset["is_expanded"]: if pset["is_expanded"]:
if props.active_pset_id == pset_id: if props.active_pset_id == pset_id:
for prop in pset["Properties"]: for prop in props.properties:
draw_psetqto_editable_ui(box, props, prop) draw_psetqto_editable_ui(box, props, prop)
else: else:
has_props_displayed = False has_props_displayed = False
for prop in pset["Properties"]: for prop_id in pset["Properties"]:
prop = Data.properties[prop_id]
if context.preferences.addons["blenderbim"].preferences.should_hide_empty_props and ( if context.preferences.addons["blenderbim"].preferences.should_hide_empty_props and (
prop["value"] is None or prop["value"] == "" prop["NominalValue"] is None or prop["NominalValue"] == ""
): ):
continue continue
has_props_displayed = True has_props_displayed = True
row = box.row(align=True) row = box.row(align=True)
row.scale_y = 0.8 row.scale_y = 0.8
row.label(text=prop["Name"]) row.label(text=prop["Name"])
row.label(text=str(prop["value"])) row.label(text=str(prop["NominalValue"]))
if not has_props_displayed: if not has_props_displayed:
row = box.row() row = box.row()
row.scale_y = 0.8 row.scale_y = 0.8
@@ -56,33 +59,32 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
def draw_psetqto_editable_ui(box, props, prop): def draw_psetqto_editable_ui(box, props, prop):
row = box.row(align=True) row = box.row(align=True)
blender_prop = props.properties.get(prop["Name"]) if prop.data_type == "string":
if prop["type"] == "string": row.prop(prop, "string_value", text=prop.name)
row.prop(blender_prop, "string_value", text=prop["Name"]) elif prop.data_type == "integer":
elif prop["type"] == "integer": row.prop(prop, "int_value", text=prop.name)
row.prop(blender_prop, "int_value", text=prop["Name"]) elif prop.data_type == "float":
elif prop["type"] == "float": row.prop(prop, "float_value", text=prop.name)
row.prop(blender_prop, "float_value", text=prop["Name"]) elif prop.data_type == "boolean":
elif prop["type"] == "boolean": row.prop(prop, "bool_value", text=prop.name)
row.prop(blender_prop, "bool_value", text=prop["Name"]) elif prop.data_type == "enum":
elif prop["type"] == "enum": row.prop(prop, "enum_value", text=prop.name)
row.prop(blender_prop, "enum_value", text=prop["Name"]) row.prop(prop, "is_null", icon="RADIOBUT_OFF" if prop.is_null else "RADIOBUT_ON", text="")
row.prop(blender_prop, "is_null", icon="RADIOBUT_OFF" if blender_prop.is_null else "RADIOBUT_ON", text="")
if ( if (
"length" in prop["Name"].lower() "length" in prop.name.lower()
or "width" in prop["Name"].lower() or "width" in prop.name.lower()
or "height" in prop["Name"].lower() or "height" in prop.name.lower()
or "depth" in prop["Name"].lower() or "depth" in prop.name.lower()
or "perimeter" in prop["Name"].lower() or "perimeter" in prop.name.lower()
): ):
op = row.operator("bim.guess_quantity", icon="IPO_EASE_IN_OUT", text="") op = row.operator("bim.guess_quantity", icon="IPO_EASE_IN_OUT", text="")
op.prop = prop["Name"] op.prop = prop.name
elif "area" in prop["Name"].lower(): elif "area" in prop.name.lower():
op = row.operator("bim.guess_quantity", icon="MESH_CIRCLE", text="") op = row.operator("bim.guess_quantity", icon="MESH_CIRCLE", text="")
op.prop = prop["Name"] op.prop = prop.name
elif "volume" in prop["Name"].lower(): elif "volume" in prop.name.lower():
op = row.operator("bim.guess_quantity", icon="SPHERE", text="") op = row.operator("bim.guess_quantity", icon="SPHERE", text="")
op.prop = prop["Name"] op.prop = prop.name
class BIM_PT_object_psets(Panel): class BIM_PT_object_psets(Panel):
@@ -99,6 +101,8 @@ class BIM_PT_object_psets(Panel):
props = context.active_object.BIMObjectProperties props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id: if not props.ifc_definition_id:
return False return False
if not IfcStore.get_element(props.ifc_definition_id):
return False
if props.ifc_definition_id not in Data.products: if props.ifc_definition_id not in Data.products:
Data.load(IfcStore.get_file(), props.ifc_definition_id) Data.load(IfcStore.get_file(), props.ifc_definition_id)
if not Data.products[props.ifc_definition_id]: if not Data.products[props.ifc_definition_id]:
@@ -142,6 +146,8 @@ class BIM_PT_object_qtos(Panel):
props = context.active_object.BIMObjectProperties props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id: if not props.ifc_definition_id:
return False return False
if not IfcStore.get_element(props.ifc_definition_id):
return False
if props.ifc_definition_id not in Data.products: if props.ifc_definition_id not in Data.products:
Data.load(IfcStore.get_file(), props.ifc_definition_id) Data.load(IfcStore.get_file(), props.ifc_definition_id)
if not Data.products[props.ifc_definition_id]: if not Data.products[props.ifc_definition_id]:
@@ -5,6 +5,8 @@ classes = (
operator.CalculateEdgeLengths, operator.CalculateEdgeLengths,
operator.CalculateFaceAreas, operator.CalculateFaceAreas,
operator.CalculateObjectVolumes, operator.CalculateObjectVolumes,
operator.ExecuteQtoMethod,
operator.QuantifyObjects,
prop.BIMQtoProperties, prop.BIMQtoProperties,
ui.BIM_PT_qto_utilities, ui.BIM_PT_qto_utilities,
) )
@@ -0,0 +1,61 @@
import bpy
import bmesh
def calculate_height(obj):
return obj.dimensions[2]
def calculate_volume(obj):
bm = bmesh.new()
bm.from_mesh(obj.data)
result = bm.calc_volume()
bm.free()
return result
def calculate_formwork_area(objs):
"""
Formwork is defined as the surface area required to cover all exposed
surfaces of one or more objects, excluding top surfaces (i.e. that have a
face normal with a significant Z component).
"""
copied_objs = []
result = 0
for obj in objs:
new_obj = obj.copy()
new_obj.data = obj.data.copy()
new_obj.animation_data_clear()
bpy.context.collection.objects.link(new_obj)
copied_objs.append(new_obj)
context_override = {}
context_override["object"] = context_override["active_object"] = copied_objs[0]
context_override["selected_objects"] = context_override["selected_editable_objects"] = copied_objs
bpy.ops.object.join(context_override)
copied_objs[0].name = "Formwork"
copied_objs[0].BIMObjectProperties.ifc_definition_id = 0
modifier = copied_objs[0].modifiers.new("Formwork", "REMESH")
modifier.mode = "SHARP"
# This hardcoded value may be optimised through a better understanding of the octree division.
# These values are based off some trial and error heuristics I've learned through experience.
max_dim = max(copied_objs[0].dimensions)
if max_dim > 45:
modifier.octree_depth = 9
elif max_dim > 35:
modifier.octree_depth = 8
elif max_dim > 12:
modifier.octree_depth = 7
elif max_dim > 5:
modifier.octree_depth = 6
else:
modifier.octree_depth = 5
mesh = copied_objs[0].evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh()
for polygon in mesh.polygons:
if polygon.normal.z > 0.5:
continue
result += polygon.area
return result
@@ -1,5 +1,10 @@
import bpy import bpy
import bmesh import bmesh
import ifcopenshell
import ifcopenshell.api
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.qto import helper
from ifcopenshell.api.pset.data import Data as PsetData
class CalculateEdgeLengths(bpy.types.Operator): class CalculateEdgeLengths(bpy.types.Operator):
@@ -49,3 +54,58 @@ class CalculateObjectVolumes(bpy.types.Operator):
bm.free() bm.free()
bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
return {"FINISHED"} return {"FINISHED"}
class ExecuteQtoMethod(bpy.types.Operator):
bl_idname = "bim.execute_qto_method"
bl_label = "Execute Qto Method"
def execute(self, context):
props = bpy.context.scene.BIMQtoProperties
result = 0
if props.qto_methods == "HEIGHT":
for obj in bpy.context.selected_objects:
result += helper.calculate_height(obj)
elif props.qto_methods == "VOLUME":
for obj in bpy.context.selected_objects:
result += helper.calculate_volume(obj)
elif props.qto_methods == "FORMWORK":
result = helper.calculate_formwork_area(bpy.context.selected_objects)
props.qto_result = str(round(result, 3))
return {"FINISHED"}
class QuantifyObjects(bpy.types.Operator):
bl_idname = "bim.quantify_objects"
bl_label = "Quantify Objects"
def execute(self, context):
props = bpy.context.scene.BIMQtoProperties
self.file = IfcStore.get_file()
for obj in bpy.context.selected_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
result = 0
if props.qto_methods == "HEIGHT":
result = helper.calculate_height(obj)
elif props.qto_methods == "VOLUME":
result = helper.calculate_volume(obj)
elif props.qto_methods == "FORMWORK":
result = helper.calculate_formwork_area([obj])
if not result:
continue
result = round(result, 3)
qto = ifcopenshell.api.run(
"pset.add_qto",
self.file,
product=self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
name=props.qto_name,
)
ifcopenshell.api.run(
"pset.edit_qto",
self.file,
qto=qto,
properties={props.prop_name: result}
)
PsetData.load(self.file, obj.BIMObjectProperties.ifc_definition_id)
return {"FINISHED"}
@@ -14,3 +14,13 @@ from bpy.props import (
class BIMQtoProperties(PropertyGroup): class BIMQtoProperties(PropertyGroup):
qto_result: StringProperty(default="", name="Qto Result") qto_result: StringProperty(default="", name="Qto Result")
qto_methods: EnumProperty(
items=[
("HEIGHT", "Height", "Calculate the Z height of an object"),
("VOLUME", "Volume", "Calculate the volume of an object"),
("FORMWORK", "Formwork", "Calculate the exposed formwork for all bottoms and sides of one or more objects"),
],
name="Qto Methods",
)
qto_name: StringProperty(name="Qto Name")
prop_name: StringProperty(name="Prop Name")
@@ -4,6 +4,7 @@ from bpy.types import Panel
class BIM_PT_qto_utilities(Panel): class BIM_PT_qto_utilities(Panel):
bl_idname = "BIM_PT_qto_utilities" bl_idname = "BIM_PT_qto_utilities"
bl_label = "Quantity Take-off" bl_label = "Quantity Take-off"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "BlenderBIM" bl_category = "BlenderBIM"
@@ -21,3 +22,12 @@ class BIM_PT_qto_utilities(Panel):
row.operator("bim.calculate_face_areas") row.operator("bim.calculate_face_areas")
row = layout.row(align=True) row = layout.row(align=True)
row.operator("bim.calculate_object_volumes") row.operator("bim.calculate_object_volumes")
row = layout.row(align=True)
row.prop(props, "qto_methods", text="")
row.operator("bim.execute_qto_method", icon="PROPERTIES", text="")
row = layout.row(align=True)
row.prop(props, "qto_name", text="")
row.prop(props, "prop_name", text="")
row.operator("bim.quantify_objects", icon="COPYDOWN", text="")

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