2021-11-16 08:54:16 +11:00
|
|
|
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
|
|
|
|
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
|
|
|
|
#
|
|
|
|
|
# This file is part of BlenderBIM Add-on.
|
|
|
|
|
#
|
|
|
|
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
#
|
|
|
|
|
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
|
#
|
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
import os
|
2022-01-22 21:00:04 +11:00
|
|
|
import re
|
2021-11-16 08:54:16 +11:00
|
|
|
import bpy
|
2022-03-29 10:01:41 +11:00
|
|
|
import math
|
2022-03-18 16:00:49 +11:00
|
|
|
import bmesh
|
2022-01-29 21:11:23 +11:00
|
|
|
import mathutils
|
2022-01-29 13:42:46 +11:00
|
|
|
import webbrowser
|
2022-03-18 16:00:49 +11:00
|
|
|
import numpy as np
|
2021-11-16 08:54:16 +11:00
|
|
|
import blenderbim.core.tool
|
2022-03-18 16:00:49 +11:00
|
|
|
import blenderbim.core.geometry
|
2021-11-16 08:54:16 +11:00
|
|
|
import blenderbim.tool as tool
|
|
|
|
|
import ifcopenshell.util.representation
|
2022-01-29 13:42:46 +11:00
|
|
|
import blenderbim.bim.module.drawing.sheeter as sheeter
|
2022-02-04 19:21:24 +11:00
|
|
|
import blenderbim.bim.module.drawing.annotation as annotation
|
2022-03-18 16:00:49 +11:00
|
|
|
import blenderbim.bim.module.drawing.helper as helper
|
2021-11-16 08:54:16 +11:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Drawing(blenderbim.core.tool.Drawing):
|
2022-02-04 19:21:24 +11:00
|
|
|
@classmethod
|
|
|
|
|
def create_annotation_object(cls, object_type):
|
|
|
|
|
data_type = {
|
|
|
|
|
"DIMENSION": "curve",
|
|
|
|
|
"EQUAL_DIMENSION": "curve",
|
|
|
|
|
"TEXT": "empty",
|
|
|
|
|
"TEXT_LEADER": "curve",
|
|
|
|
|
"STAIR_ARROW": "curve",
|
|
|
|
|
"HIDDEN_LINE": "mesh",
|
|
|
|
|
"PLAN_LEVEL": "curve",
|
|
|
|
|
"SECTION_LEVEL": "curve",
|
|
|
|
|
"BREAKLINE": "mesh",
|
|
|
|
|
"MISC": "mesh",
|
|
|
|
|
}[object_type]
|
|
|
|
|
obj = annotation.Annotator.get_annotation_obj(object_type, data_type)
|
|
|
|
|
if object_type == "BREAKLINE":
|
|
|
|
|
obj = annotation.Annotator.add_plane_to_annotation(obj)
|
|
|
|
|
elif object_type != "TEXT":
|
|
|
|
|
obj = annotation.Annotator.add_line_to_annotation(obj)
|
|
|
|
|
return obj
|
|
|
|
|
|
2022-01-29 21:11:23 +11:00
|
|
|
@classmethod
|
|
|
|
|
def create_camera(cls, name, matrix):
|
|
|
|
|
camera = bpy.data.objects.new(name, bpy.data.cameras.new(name))
|
|
|
|
|
camera.location = (0, 0, 1.5) # The view shall be 1.5m above the origin
|
|
|
|
|
camera.data.type = "ORTHO"
|
|
|
|
|
camera.data.ortho_scale = 50 # The default of 6m is too small
|
|
|
|
|
camera.data.clip_end = 10 # A slightly more reasonable default
|
|
|
|
|
if bpy.context.scene.unit_settings.system == "IMPERIAL":
|
|
|
|
|
camera.data.BIMCameraProperties.diagram_scale = '1/8"=1\'-0"|1/96'
|
|
|
|
|
else:
|
|
|
|
|
camera.data.BIMCameraProperties.diagram_scale = "1:100|1/100"
|
|
|
|
|
camera.matrix_world = matrix
|
|
|
|
|
bpy.context.scene.collection.objects.link(camera)
|
|
|
|
|
return camera
|
|
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
@classmethod
|
|
|
|
|
def create_svg_sheet(cls, document, titleblock):
|
|
|
|
|
sheet_builder = sheeter.SheetBuilder()
|
|
|
|
|
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
|
|
|
|
|
sheet_builder.create(cls.get_sheet_filename(document), titleblock)
|
|
|
|
|
|
2022-02-04 10:54:18 +11:00
|
|
|
@classmethod
|
|
|
|
|
def delete_collection(cls, collection):
|
|
|
|
|
bpy.data.collections.remove(collection, do_unlink=True)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def delete_drawing_elements(cls, elements):
|
|
|
|
|
for element in elements:
|
|
|
|
|
tool.Ifc.delete(element)
|
|
|
|
|
obj = tool.Ifc.get_object(element)
|
|
|
|
|
if obj:
|
|
|
|
|
bpy.data.objects.remove(obj)
|
|
|
|
|
|
2022-03-18 16:00:49 +11:00
|
|
|
@classmethod
|
|
|
|
|
def delete_object(cls, obj):
|
|
|
|
|
bpy.data.objects.remove(obj)
|
|
|
|
|
|
2022-01-29 17:26:09 +11:00
|
|
|
@classmethod
|
|
|
|
|
def disable_editing_drawings(cls):
|
|
|
|
|
bpy.context.scene.DocProperties.is_editing_drawings = False
|
|
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
@classmethod
|
|
|
|
|
def disable_editing_sheets(cls):
|
|
|
|
|
bpy.context.scene.DocProperties.is_editing_sheets = False
|
|
|
|
|
|
2021-11-16 08:54:16 +11:00
|
|
|
@classmethod
|
|
|
|
|
def disable_editing_text(cls, obj):
|
|
|
|
|
obj.BIMTextProperties.is_editing = False
|
|
|
|
|
|
2022-01-14 18:00:08 +11:00
|
|
|
@classmethod
|
|
|
|
|
def disable_editing_text_product(cls, obj):
|
|
|
|
|
obj.BIMTextProperties.is_editing_product = False
|
|
|
|
|
|
2022-02-04 19:21:24 +11:00
|
|
|
@classmethod
|
|
|
|
|
def enable_editing(cls, obj):
|
|
|
|
|
bpy.ops.object.select_all(action="DESELECT")
|
|
|
|
|
bpy.context.view_layer.objects.active = obj
|
|
|
|
|
obj.select_set(True)
|
|
|
|
|
if obj.data:
|
|
|
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
|
|
|
|
2022-01-29 17:26:09 +11:00
|
|
|
@classmethod
|
|
|
|
|
def enable_editing_drawings(cls):
|
|
|
|
|
bpy.context.scene.DocProperties.is_editing_drawings = True
|
|
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
@classmethod
|
|
|
|
|
def enable_editing_sheets(cls):
|
|
|
|
|
bpy.context.scene.DocProperties.is_editing_sheets = True
|
|
|
|
|
|
2021-11-16 08:54:16 +11:00
|
|
|
@classmethod
|
|
|
|
|
def enable_editing_text(cls, obj):
|
|
|
|
|
obj.BIMTextProperties.is_editing = True
|
|
|
|
|
|
2022-01-14 18:00:08 +11:00
|
|
|
@classmethod
|
|
|
|
|
def enable_editing_text_product(cls, obj):
|
|
|
|
|
obj.BIMTextProperties.is_editing_product = True
|
|
|
|
|
|
2022-01-29 17:26:09 +11:00
|
|
|
@classmethod
|
|
|
|
|
def ensure_unique_drawing_name(cls, name):
|
|
|
|
|
names = [e.Name for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]
|
|
|
|
|
while name in names:
|
|
|
|
|
name += "-X"
|
|
|
|
|
return name
|
|
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
@classmethod
|
|
|
|
|
def ensure_unique_identification(cls, identification):
|
|
|
|
|
if tool.Ifc.get_schema() == "IFC2X3":
|
|
|
|
|
ids = [d.DocumentId for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"]
|
|
|
|
|
else:
|
|
|
|
|
ids = [
|
|
|
|
|
d.Identification for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"
|
|
|
|
|
]
|
|
|
|
|
while identification in ids:
|
|
|
|
|
identification += "-X"
|
|
|
|
|
return identification
|
|
|
|
|
|
2021-11-16 08:54:16 +11:00
|
|
|
@classmethod
|
|
|
|
|
def export_text_literal_attributes(cls, obj):
|
|
|
|
|
return blenderbim.bim.helper.export_attributes(obj.BIMTextProperties.attributes)
|
|
|
|
|
|
2022-02-04 19:21:24 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_annotation_context(cls, target_view):
|
|
|
|
|
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Annotation", target_view)
|
|
|
|
|
|
2022-01-30 15:09:54 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_body_context(cls):
|
|
|
|
|
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
|
|
|
|
|
2022-02-04 10:54:18 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_drawing_collection(cls, drawing):
|
|
|
|
|
obj = tool.Ifc.get_object(drawing)
|
|
|
|
|
return obj.users_collection[0]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_drawing_group(cls, drawing):
|
|
|
|
|
for rel in drawing.HasAssignments or []:
|
|
|
|
|
if rel.is_a("IfcRelAssignsToGroup"):
|
|
|
|
|
return rel.RelatingGroup
|
|
|
|
|
|
2022-02-04 19:21:24 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_drawing_target_view(cls, drawing):
|
|
|
|
|
return ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"]["TargetView"]
|
|
|
|
|
|
2022-02-04 10:54:18 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_group_elements(cls, group):
|
|
|
|
|
for rel in group.IsGroupedBy or []:
|
|
|
|
|
return rel.RelatedObjects
|
|
|
|
|
|
2022-02-04 19:21:24 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_ifc_representation_class(cls, object_type):
|
|
|
|
|
if object_type == "TEXT":
|
|
|
|
|
return "IfcTextLiteral"
|
|
|
|
|
elif object_type == "TEXT_LEADER":
|
|
|
|
|
return "IfcGeometricCurveSet/IfcTextLiteral"
|
|
|
|
|
return ""
|
|
|
|
|
|
2022-02-04 15:04:30 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_name(cls, element):
|
|
|
|
|
return element.Name
|
|
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_sheet_filename(cls, document):
|
|
|
|
|
if hasattr(document, "Identification"):
|
|
|
|
|
name = document.Identification or "X"
|
|
|
|
|
else:
|
|
|
|
|
name = document.DocumentId or "X"
|
|
|
|
|
name += " - " + document.Name or "Unnamed"
|
|
|
|
|
return name
|
|
|
|
|
|
2022-01-29 21:11:23 +11:00
|
|
|
@classmethod
|
|
|
|
|
def generate_drawing_matrix(cls, target_view, location_hint):
|
2022-02-04 08:14:36 +11:00
|
|
|
x = 0 if location_hint == 0 else bpy.context.scene.cursor.matrix[0][3]
|
|
|
|
|
y = 0 if location_hint == 0 else bpy.context.scene.cursor.matrix[1][3]
|
|
|
|
|
z = 0 if location_hint == 0 else bpy.context.scene.cursor.matrix[2][3]
|
2022-02-03 17:28:09 +11:00
|
|
|
if target_view == "PLAN_VIEW":
|
|
|
|
|
if location_hint:
|
2022-02-04 08:14:36 +11:00
|
|
|
z = tool.Ifc.get_object(tool.Ifc.get().by_id(location_hint)).matrix_world[2][3]
|
|
|
|
|
return mathutils.Matrix(((1, 0, 0, x), (0, 1, 0, y), (0, 0, 1, z + 1.6), (0, 0, 0, 1)))
|
|
|
|
|
elif target_view == "REFLECTED_PLAN_VIEW":
|
|
|
|
|
if location_hint:
|
|
|
|
|
z = tool.Ifc.get_object(tool.Ifc.get().by_id(location_hint)).matrix_world[2][3]
|
|
|
|
|
return mathutils.Matrix(((-1, 0, 0, x), (0, 1, 0, y), (0, 0, -1, z + 1.6), (0, 0, 0, 1)))
|
|
|
|
|
return mathutils.Matrix(((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1)))
|
|
|
|
|
elif target_view == "ELEVATION_VIEW":
|
|
|
|
|
if location_hint == "NORTH":
|
|
|
|
|
return mathutils.Matrix(((-1, 0, 0, x), (0, 0, 1, y), (0, 1, 0, z), (0, 0, 0, 1)))
|
|
|
|
|
elif location_hint == "SOUTH":
|
|
|
|
|
return mathutils.Matrix(((1, 0, 0, x), (0, 0, -1, y), (0, 1, 0, z), (0, 0, 0, 1)))
|
|
|
|
|
elif location_hint == "EAST":
|
|
|
|
|
return mathutils.Matrix(((0, 0, 1, x), (1, 0, 0, y), (0, 1, 0, z), (0, 0, 0, 1)))
|
|
|
|
|
elif location_hint == "WEST":
|
|
|
|
|
return mathutils.Matrix(((0, 0, -1, x), (-1, 0, 0, y), (0, 1, 0, z), (0, 0, 0, 1)))
|
|
|
|
|
elif target_view == "SECTION_VIEW":
|
|
|
|
|
if location_hint == "NORTH":
|
|
|
|
|
return mathutils.Matrix(((1, 0, 0, x), (0, 0, -1, y), (0, 1, 0, z), (0, 0, 0, 1)))
|
|
|
|
|
elif location_hint == "SOUTH":
|
|
|
|
|
return mathutils.Matrix(((-1, 0, 0, x), (0, 0, 1, y), (0, 1, 0, z), (0, 0, 0, 1)))
|
|
|
|
|
elif location_hint == "EAST":
|
|
|
|
|
return mathutils.Matrix(((0, 0, -1, x), (-1, 0, 0, y), (0, 1, 0, z), (0, 0, 0, 1)))
|
|
|
|
|
elif location_hint == "WEST":
|
|
|
|
|
return mathutils.Matrix(((0, 0, 1, x), (1, 0, 0, y), (0, 1, 0, z), (0, 0, 0, 1)))
|
2022-01-29 21:11:23 +11:00
|
|
|
return mathutils.Matrix()
|
|
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
@classmethod
|
|
|
|
|
def generate_sheet_identification(cls):
|
|
|
|
|
number = len([d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"])
|
|
|
|
|
return "A" + str(number).zfill(2)
|
|
|
|
|
|
2021-11-16 08:54:16 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_text_literal(cls, obj):
|
|
|
|
|
element = tool.Ifc.get_entity(obj)
|
|
|
|
|
if not element:
|
|
|
|
|
return
|
|
|
|
|
rep = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
|
|
|
|
|
if not rep:
|
|
|
|
|
return
|
|
|
|
|
items = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
|
|
|
|
|
if items:
|
|
|
|
|
return items[0]
|
|
|
|
|
|
2022-01-14 18:00:08 +11:00
|
|
|
@classmethod
|
|
|
|
|
def get_text_product(cls, element):
|
|
|
|
|
for rel in element.HasAssignments:
|
|
|
|
|
if rel.is_a("IfcRelAssignsToProduct"):
|
|
|
|
|
return rel.RelatingProduct
|
|
|
|
|
|
2022-01-29 17:26:09 +11:00
|
|
|
@classmethod
|
|
|
|
|
def import_drawings(cls):
|
|
|
|
|
bpy.context.scene.DocProperties.drawings.clear()
|
|
|
|
|
drawings = [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]
|
|
|
|
|
for drawing in drawings:
|
|
|
|
|
new = bpy.context.scene.DocProperties.drawings.add()
|
|
|
|
|
new.ifc_definition_id = drawing.id()
|
|
|
|
|
new.name = drawing.Name or "Unnamed"
|
2022-03-18 15:49:56 +11:00
|
|
|
new.target_view = cls.get_drawing_target_view(drawing)
|
2022-01-29 17:26:09 +11:00
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
@classmethod
|
|
|
|
|
def import_sheets(cls):
|
2022-03-30 18:14:51 +11:00
|
|
|
props = bpy.context.scene.DocProperties
|
|
|
|
|
expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded}
|
|
|
|
|
props.sheets.clear()
|
2022-01-29 13:42:46 +11:00
|
|
|
sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "DOCUMENTATION"]
|
|
|
|
|
for sheet in sheets:
|
2022-03-30 18:14:51 +11:00
|
|
|
new = props.sheets.add()
|
2022-01-29 13:42:46 +11:00
|
|
|
new.ifc_definition_id = sheet.id()
|
|
|
|
|
if tool.Ifc.get_schema() == "IFC2X3":
|
|
|
|
|
new.identification = sheet.DocumentId
|
|
|
|
|
else:
|
|
|
|
|
new.identification = sheet.Identification
|
|
|
|
|
new.name = sheet.Name
|
2022-03-30 18:14:51 +11:00
|
|
|
new.is_sheet = True
|
|
|
|
|
new.is_expanded = sheet.id() in expanded_sheets
|
|
|
|
|
|
|
|
|
|
if not new.is_expanded:
|
|
|
|
|
continue
|
|
|
|
|
|
2022-04-13 12:21:18 +10:00
|
|
|
for reference in cls.get_document_references(sheet):
|
2022-03-30 18:14:51 +11:00
|
|
|
new = props.sheets.add()
|
|
|
|
|
new.ifc_definition_id = reference.id()
|
|
|
|
|
new.is_sheet = False
|
|
|
|
|
|
|
|
|
|
if tool.Ifc.get_schema() == "IFC2X3":
|
|
|
|
|
new.identification = reference.ItemReference or "X"
|
|
|
|
|
else:
|
|
|
|
|
new.identification = reference.Identification or "X"
|
2022-04-13 12:21:18 +10:00
|
|
|
|
|
|
|
|
element = cls.get_reference_element(reference)
|
2022-03-30 18:14:51 +11:00
|
|
|
|
|
|
|
|
new.name = element.Name
|
|
|
|
|
new.reference_type = "DRAWING"
|
2022-01-29 13:42:46 +11:00
|
|
|
|
2021-11-16 08:54:16 +11:00
|
|
|
@classmethod
|
|
|
|
|
def import_text_attributes(cls, obj):
|
|
|
|
|
props = obj.BIMTextProperties
|
|
|
|
|
props.attributes.clear()
|
|
|
|
|
text = cls.get_text_literal(obj)
|
|
|
|
|
blenderbim.bim.helper.import_attributes2(text, props.attributes)
|
|
|
|
|
|
2022-01-14 18:00:08 +11:00
|
|
|
@classmethod
|
|
|
|
|
def import_text_product(cls, obj):
|
|
|
|
|
element = tool.Ifc.get_entity(obj)
|
|
|
|
|
product = cls.get_text_product(element)
|
|
|
|
|
if product:
|
|
|
|
|
obj.BIMTextProperties.relating_product = tool.Ifc.get_object(product)
|
|
|
|
|
else:
|
|
|
|
|
obj.BIMTextProperties.relating_product = None
|
|
|
|
|
|
2022-01-29 13:42:46 +11:00
|
|
|
@classmethod
|
|
|
|
|
def open_with_user_command(cls, user_command, path):
|
|
|
|
|
if user_command:
|
|
|
|
|
commands = eval(user_command)
|
|
|
|
|
for command in commands:
|
|
|
|
|
subprocess.run(command)
|
|
|
|
|
else:
|
|
|
|
|
webbrowser.open("file://" + path)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def open_svg(cls, filename):
|
|
|
|
|
cls.open_with_user_command(
|
|
|
|
|
bpy.context.preferences.addons["blenderbim"].preferences.svg_command,
|
|
|
|
|
os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", filename + ".svg"),
|
|
|
|
|
)
|
|
|
|
|
|
2022-01-29 21:11:23 +11:00
|
|
|
@classmethod
|
2022-02-01 19:16:13 +11:00
|
|
|
def run_root_assign_class(
|
|
|
|
|
cls,
|
|
|
|
|
obj=None,
|
|
|
|
|
ifc_class=None,
|
|
|
|
|
predefined_type=None,
|
|
|
|
|
should_add_representation=True,
|
|
|
|
|
context=None,
|
|
|
|
|
ifc_representation_class=None,
|
|
|
|
|
):
|
2022-01-30 15:09:54 +11:00
|
|
|
return blenderbim.core.root.assign_class(
|
|
|
|
|
tool.Ifc,
|
|
|
|
|
tool.Collector,
|
|
|
|
|
tool.Root,
|
|
|
|
|
obj=obj,
|
|
|
|
|
ifc_class=ifc_class,
|
|
|
|
|
predefined_type=predefined_type,
|
|
|
|
|
should_add_representation=should_add_representation,
|
|
|
|
|
context=context,
|
|
|
|
|
ifc_representation_class=ifc_representation_class,
|
|
|
|
|
)
|
2022-01-29 21:11:23 +11:00
|
|
|
|
2022-02-04 15:04:30 +11:00
|
|
|
@classmethod
|
|
|
|
|
def set_drawing_collection_name(cls, group, collection):
|
|
|
|
|
collection.name = f"IfcGroup/{group.Name}"
|
|
|
|
|
|
2022-02-04 19:21:24 +11:00
|
|
|
@classmethod
|
|
|
|
|
def show_decorations(cls):
|
|
|
|
|
bpy.context.scene.DocProperties.should_draw_decorations = True
|
|
|
|
|
|
2021-11-16 08:54:16 +11:00
|
|
|
@classmethod
|
|
|
|
|
def update_text_value(cls, obj):
|
|
|
|
|
element = cls.get_text_literal(obj)
|
2022-01-22 21:00:04 +11:00
|
|
|
if not element:
|
|
|
|
|
return
|
|
|
|
|
value = element.Literal
|
|
|
|
|
product = cls.get_text_product(tool.Ifc.get_entity(obj))
|
|
|
|
|
if product:
|
|
|
|
|
selector = ifcopenshell.util.selector.Selector()
|
|
|
|
|
variables = {}
|
|
|
|
|
for variable in re.findall("{{.*?}}", value):
|
|
|
|
|
value = value.replace(variable, selector.get_element_value(product, variable[2:-2]) or "")
|
|
|
|
|
obj.BIMTextProperties.value = value
|
2022-03-18 15:49:56 +11:00
|
|
|
|
|
|
|
|
# TODO below this point is highly experimental prototype code with no tests
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def generate_drawing_name(cls, target_view, location_hint):
|
|
|
|
|
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and location_hint:
|
|
|
|
|
location = tool.Ifc.get().by_id(location_hint)
|
|
|
|
|
if target_view == "REFLECTED_PLAN_VIEW":
|
|
|
|
|
target_view = "RCP_VIEW"
|
|
|
|
|
return (location.Name or "UNNAMED").upper() + " " + target_view.split("_")[0]
|
|
|
|
|
elif target_view in ("SECTION_VIEW", "ELEVATION_VIEW") and location_hint:
|
|
|
|
|
return location_hint + " " + target_view.split("_")[0]
|
|
|
|
|
return target_view
|
2022-03-18 16:00:49 +11:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_potential_reference_elements(cls, drawing):
|
|
|
|
|
elements = []
|
|
|
|
|
existing_references = cls.get_group_elements(cls.get_drawing_group(drawing))
|
|
|
|
|
for element in tool.Ifc.get().by_type("IfcAnnotation"):
|
|
|
|
|
if element in existing_references or element == drawing:
|
|
|
|
|
continue
|
|
|
|
|
if element.ObjectType == "DRAWING":
|
|
|
|
|
psets = ifcopenshell.util.element.get_psets(element)
|
|
|
|
|
if psets.get("EPset_Drawing", {}).get("TargetView", None) in ("SECTION_VIEW", "ELEVATION_VIEW"):
|
|
|
|
|
elements.append(element)
|
|
|
|
|
for element in tool.Ifc.get().by_type("IfcGridAxis"):
|
|
|
|
|
elements.append(element)
|
|
|
|
|
return elements
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_drawing_reference_annotation(cls, drawing, reference_element):
|
|
|
|
|
if drawing == reference_element:
|
|
|
|
|
return True
|
|
|
|
|
for element in cls.get_group_elements(cls.get_drawing_group(drawing)):
|
|
|
|
|
if element.is_a("IfcAnnotation"):
|
|
|
|
|
for rel in element.HasAssignments:
|
|
|
|
|
if rel.is_a("IfcRelAssignsToProduct"):
|
|
|
|
|
if rel.RelatingProduct == reference_element:
|
|
|
|
|
return element
|
|
|
|
|
# We cannot associate IfcGridAxis directly, so we establish a convention:
|
|
|
|
|
# IfcRelAssignsToProduct.RelatingProduct = IfcGrid
|
|
|
|
|
# IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag
|
|
|
|
|
elif reference_element.is_a("IfcGridAxis") and rel.Name == reference_element.AxisTag:
|
|
|
|
|
return element
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def generate_reference_annotation(cls, drawing, reference_element, context):
|
|
|
|
|
if reference_element.is_a("IfcGridAxis"):
|
|
|
|
|
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
|
2022-03-29 10:01:41 +11:00
|
|
|
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
|
|
|
|
|
psets = ifcopenshell.util.element.get_psets(reference_element)
|
|
|
|
|
target_view = psets.get("EPset_Drawing", {}).get("TargetView", None)
|
|
|
|
|
if target_view == "ELEVATION_VIEW":
|
|
|
|
|
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
|
2022-03-29 17:04:50 +11:00
|
|
|
elif target_view == "SECTION_VIEW":
|
|
|
|
|
return cls.generate_section_reference_annotation(drawing, reference_element, context)
|
2022-03-29 10:01:41 +11:00
|
|
|
|
|
|
|
|
@classmethod
|
2022-03-29 17:04:50 +11:00
|
|
|
def generate_section_reference_annotation(cls, drawing, reference_element, context):
|
2022-03-29 10:01:41 +11:00
|
|
|
reference_obj = tool.Ifc.get_object(reference_element)
|
|
|
|
|
reference_obj.matrix_world
|
|
|
|
|
camera = tool.Ifc.get_object(drawing)
|
2022-03-29 17:04:50 +11:00
|
|
|
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
|
2022-03-29 10:01:41 +11:00
|
|
|
|
2022-03-29 17:04:50 +11:00
|
|
|
def to_camera_coords(camera, reference_obj):
|
|
|
|
|
mat = reference_obj.matrix_world.copy()
|
|
|
|
|
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
|
|
|
|
xyz[2] = 0
|
|
|
|
|
xyz = camera.matrix_world @ xyz
|
|
|
|
|
mat[0][3] = xyz[0]
|
|
|
|
|
mat[1][3] = xyz[1]
|
|
|
|
|
mat[2][3] = xyz[2]
|
|
|
|
|
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
|
|
|
|
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
|
|
|
|
mat[0][3] += annotation_offset[0]
|
|
|
|
|
mat[1][3] += annotation_offset[1]
|
|
|
|
|
mat[2][3] += annotation_offset[2]
|
|
|
|
|
return mat
|
|
|
|
|
|
|
|
|
|
def clip_to_camera_boundary(mesh, bounds):
|
|
|
|
|
mesh.verts.ensure_lookup_table()
|
|
|
|
|
points = [v.co for v in mesh.verts[0:2]]
|
|
|
|
|
points = helper.clip_segment(bounds, points)
|
|
|
|
|
if points is None:
|
|
|
|
|
return None
|
|
|
|
|
mesh.verts[0].co = points[0]
|
|
|
|
|
mesh.verts[1].co = points[1]
|
|
|
|
|
return mesh
|
|
|
|
|
|
|
|
|
|
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
|
|
|
|
|
reference_mesh = cls.get_camera_block(reference_obj)
|
|
|
|
|
obj_matrix = to_camera_coords(camera, reference_obj)
|
|
|
|
|
|
|
|
|
|
# The reference mesh vertices represent a view cube. To convert this into a section line we:
|
|
|
|
|
# 1. Select the 4 +Z vertices local to the reference element. This is the cutting plane.
|
|
|
|
|
verts_local_to_reference = [reference_obj.matrix_world.inverted() @ v for v in reference_mesh["verts"]]
|
|
|
|
|
cutting_plane_verts = sorted(verts_local_to_reference, key=lambda x: x.z)[-4:]
|
|
|
|
|
global_cutting_plane_verts = [reference_obj.matrix_world @ v for v in cutting_plane_verts]
|
|
|
|
|
# 2. Project the cutting plane onto our viewing camera.
|
|
|
|
|
verts_local_to_camera = [camera.matrix_world.inverted() @ v for v in global_cutting_plane_verts]
|
|
|
|
|
# 3. Collapse verts with the same XY coords, and set Z to be just below the clip_start so it's visible
|
|
|
|
|
collapsed_verts = []
|
|
|
|
|
for vert in verts_local_to_camera:
|
|
|
|
|
if not [True for v in collapsed_verts if (vert.xy - v.xy).length < 1e-2]:
|
|
|
|
|
collapsed_verts.append(mathutils.Vector((vert.x, vert.y, -camera.data.clip_start - 0.05)))
|
|
|
|
|
# 4. The first two vertices is the section line
|
|
|
|
|
section_line = collapsed_verts[0:2]
|
|
|
|
|
global_section_line = [camera.matrix_world @ v for v in section_line]
|
|
|
|
|
local_section_line = [obj_matrix.inverted() @ v for v in global_section_line]
|
|
|
|
|
|
|
|
|
|
mesh = bpy.data.meshes.new(name="Annotation")
|
|
|
|
|
mesh.from_pydata(local_section_line, [(0, 1)], [])
|
|
|
|
|
bm = bmesh.new()
|
|
|
|
|
bm.from_mesh(mesh)
|
|
|
|
|
bm = clip_to_camera_boundary(bm, bounds)
|
|
|
|
|
bm.to_mesh(mesh)
|
|
|
|
|
bm.free()
|
|
|
|
|
|
|
|
|
|
obj = bpy.data.objects.new(reference_obj.name, mesh)
|
|
|
|
|
obj.matrix_world = obj_matrix
|
|
|
|
|
|
|
|
|
|
element = cls.run_root_assign_class(
|
|
|
|
|
obj=obj,
|
|
|
|
|
ifc_class="IfcAnnotation",
|
|
|
|
|
predefined_type="SECTION",
|
|
|
|
|
should_add_representation=True,
|
|
|
|
|
context=context,
|
|
|
|
|
ifc_representation_class=None,
|
2022-03-29 10:01:41 +11:00
|
|
|
)
|
2022-03-29 17:04:50 +11:00
|
|
|
return element
|
2022-03-29 10:01:41 +11:00
|
|
|
|
2022-03-29 17:04:50 +11:00
|
|
|
@classmethod
|
|
|
|
|
def generate_elevation_reference_annotation(cls, drawing, reference_element, context):
|
|
|
|
|
reference_obj = tool.Ifc.get_object(reference_element)
|
|
|
|
|
reference_obj.matrix_world
|
|
|
|
|
camera = tool.Ifc.get_object(drawing)
|
2022-03-29 10:01:41 +11:00
|
|
|
|
|
|
|
|
def to_camera_coords(camera, reference_obj):
|
|
|
|
|
mat = reference_obj.matrix_world.copy()
|
|
|
|
|
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
|
|
|
|
xyz[2] = 0
|
|
|
|
|
xyz = camera.matrix_world @ xyz
|
|
|
|
|
mat[0][3] = xyz[0]
|
|
|
|
|
mat[1][3] = xyz[1]
|
|
|
|
|
mat[2][3] = xyz[2]
|
2022-03-29 17:04:50 +11:00
|
|
|
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
2022-03-29 10:01:41 +11:00
|
|
|
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
|
|
|
|
mat[0][3] += annotation_offset[0]
|
|
|
|
|
mat[1][3] += annotation_offset[1]
|
|
|
|
|
mat[2][3] += annotation_offset[2]
|
|
|
|
|
return mat
|
|
|
|
|
|
2022-03-29 17:04:50 +11:00
|
|
|
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
|
2022-03-29 10:01:41 +11:00
|
|
|
obj = bpy.data.objects.new(reference_obj.name, None)
|
|
|
|
|
obj.empty_display_size = 0.1
|
|
|
|
|
obj.matrix_world = to_camera_coords(camera, reference_obj)
|
|
|
|
|
|
|
|
|
|
element = cls.run_root_assign_class(
|
|
|
|
|
obj=obj,
|
|
|
|
|
ifc_class="IfcAnnotation",
|
|
|
|
|
predefined_type="ELEVATION",
|
|
|
|
|
should_add_representation=False,
|
|
|
|
|
context=context,
|
|
|
|
|
ifc_representation_class=None,
|
|
|
|
|
)
|
|
|
|
|
return element
|
2022-03-18 16:00:49 +11:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def generate_grid_axis_reference_annotation(cls, drawing, reference_element, context):
|
|
|
|
|
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
|
|
|
|
|
|
|
|
|
camera = tool.Ifc.get_object(drawing)
|
|
|
|
|
|
|
|
|
|
is_ortho = camera.data.type == "ORTHO"
|
|
|
|
|
bounds = helper.ortho_view_frame(camera.data) if is_ortho else None
|
|
|
|
|
clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW")
|
|
|
|
|
elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW")
|
|
|
|
|
|
|
|
|
|
def clone(src):
|
|
|
|
|
dst = src.copy()
|
|
|
|
|
dst.data = dst.data.copy()
|
|
|
|
|
dst.name = dst.name.replace("IfcGridAxis/", "")
|
|
|
|
|
dst.BIMObjectProperties.ifc_definition_id = 0
|
|
|
|
|
dst.data.BIMMeshProperties.ifc_definition_id = 0
|
|
|
|
|
return dst
|
|
|
|
|
|
|
|
|
|
def disassemble(obj):
|
|
|
|
|
mesh = bmesh.new()
|
|
|
|
|
mesh.verts.ensure_lookup_table()
|
|
|
|
|
mesh.from_mesh(obj.data)
|
|
|
|
|
return obj, mesh
|
|
|
|
|
|
|
|
|
|
def assemble(obj, mesh):
|
|
|
|
|
mesh.to_mesh(obj.data)
|
|
|
|
|
return obj
|
|
|
|
|
|
|
|
|
|
def to_camera_coords(obj, mesh):
|
|
|
|
|
mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
|
|
|
|
|
obj.matrix_world = camera.matrix_world
|
2022-03-29 17:04:50 +11:00
|
|
|
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
2022-03-18 16:00:49 +11:00
|
|
|
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
|
|
|
|
obj.matrix_world[0][3] += annotation_offset[0]
|
|
|
|
|
obj.matrix_world[1][3] += annotation_offset[1]
|
|
|
|
|
obj.matrix_world[2][3] += annotation_offset[2]
|
|
|
|
|
return obj, mesh
|
|
|
|
|
|
|
|
|
|
def clip_to_camera_boundary(mesh):
|
|
|
|
|
mesh.verts.ensure_lookup_table()
|
|
|
|
|
points = [v.co for v in mesh.verts[0:2]]
|
|
|
|
|
points = helper.clip_segment(bounds, points)
|
|
|
|
|
if points is None:
|
|
|
|
|
return None
|
|
|
|
|
mesh.verts[0].co = points[0]
|
|
|
|
|
mesh.verts[1].co = points[1]
|
|
|
|
|
return mesh
|
|
|
|
|
|
|
|
|
|
def draw_grids_vertically(mesh):
|
|
|
|
|
mesh.verts.ensure_lookup_table()
|
|
|
|
|
points = [v.co for v in mesh.verts[0:2]]
|
|
|
|
|
points = helper.elevate_segment(bounds, points)
|
|
|
|
|
if points is None:
|
|
|
|
|
return None
|
|
|
|
|
points = helper.clip_segment(bounds, points)
|
|
|
|
|
if points is None:
|
|
|
|
|
return None
|
|
|
|
|
mesh.verts[0].co = points[0]
|
|
|
|
|
mesh.verts[1].co = points[1]
|
|
|
|
|
return mesh
|
|
|
|
|
|
|
|
|
|
obj = tool.Ifc.get_object(reference_element)
|
|
|
|
|
if not obj:
|
|
|
|
|
return
|
|
|
|
|
obj, mesh = to_camera_coords(*disassemble(clone(obj)))
|
|
|
|
|
|
|
|
|
|
if clipping:
|
|
|
|
|
mesh = clip_to_camera_boundary(mesh)
|
|
|
|
|
elif elevating:
|
|
|
|
|
mesh = draw_grids_vertically(mesh)
|
|
|
|
|
|
|
|
|
|
if mesh is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
assemble(obj, mesh)
|
|
|
|
|
|
|
|
|
|
element = cls.run_root_assign_class(
|
|
|
|
|
obj=obj,
|
|
|
|
|
ifc_class="IfcAnnotation",
|
|
|
|
|
predefined_type="GRID",
|
|
|
|
|
should_add_representation=True,
|
|
|
|
|
context=context,
|
|
|
|
|
ifc_representation_class=None,
|
|
|
|
|
)
|
|
|
|
|
return element
|
|
|
|
|
|
2022-03-29 17:04:50 +11:00
|
|
|
@classmethod
|
|
|
|
|
def is_perpendicular(cls, a, b):
|
|
|
|
|
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
|
|
|
|
|
a_quaternion = a.matrix_world.to_quaternion()
|
|
|
|
|
b_quaternion = b.matrix_world.to_quaternion()
|
|
|
|
|
for axis in axes:
|
|
|
|
|
if abs((a_quaternion @ axis).angle((b_quaternion @ axis)) - (math.pi / 2)) < 1e-5:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_camera_block(cls, obj):
|
|
|
|
|
raster_x = obj.data.BIMCameraProperties.raster_x
|
|
|
|
|
raster_y = obj.data.BIMCameraProperties.raster_y
|
|
|
|
|
|
|
|
|
|
if raster_x > raster_y:
|
|
|
|
|
width = obj.data.ortho_scale
|
|
|
|
|
height = width / raster_x * raster_y
|
|
|
|
|
else:
|
|
|
|
|
height = obj.data.ortho_scale
|
|
|
|
|
width = height / raster_y * raster_x
|
|
|
|
|
depth = obj.data.clip_end
|
|
|
|
|
|
|
|
|
|
verts = (
|
2022-03-30 18:14:51 +11:00
|
|
|
obj.matrix_world @ mathutils.Vector((-width / 2, -height / 2, -depth)),
|
|
|
|
|
obj.matrix_world @ mathutils.Vector((-width / 2, -height / 2, 0)),
|
|
|
|
|
obj.matrix_world @ mathutils.Vector((-width / 2, height / 2, -depth)),
|
|
|
|
|
obj.matrix_world @ mathutils.Vector((-width / 2, height / 2, 0)),
|
|
|
|
|
obj.matrix_world @ mathutils.Vector((width / 2, -height / 2, -depth)),
|
|
|
|
|
obj.matrix_world @ mathutils.Vector((width / 2, -height / 2, 0)),
|
|
|
|
|
obj.matrix_world @ mathutils.Vector((width / 2, height / 2, -depth)),
|
|
|
|
|
obj.matrix_world @ mathutils.Vector((width / 2, height / 2, 0)),
|
2022-03-29 17:04:50 +11:00
|
|
|
)
|
|
|
|
|
faces = [
|
|
|
|
|
[0, 1, 3, 2],
|
|
|
|
|
[2, 3, 7, 6],
|
|
|
|
|
[6, 7, 5, 4],
|
|
|
|
|
[4, 5, 1, 0],
|
|
|
|
|
[2, 6, 4, 0],
|
|
|
|
|
[7, 3, 1, 5],
|
|
|
|
|
]
|
|
|
|
|
return {"verts": verts, "faces": faces}
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def is_intersecting(cls, a, b):
|
|
|
|
|
a_block = cls.get_camera_block(a)
|
|
|
|
|
a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
|
|
|
|
|
b_block = cls.get_camera_block(b)
|
|
|
|
|
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"])
|
|
|
|
|
return bool(a_tree.overlap(b_tree))
|
|
|
|
|
|
2022-03-18 16:00:49 +11:00
|
|
|
@classmethod
|
|
|
|
|
def sync_object_representation(cls, obj):
|
|
|
|
|
bpy.ops.bim.update_representation(obj=obj.name)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def sync_object_placement(cls, obj):
|
|
|
|
|
blender_matrix = np.array(obj.matrix_world)
|
|
|
|
|
element = tool.Ifc.get_entity(obj)
|
|
|
|
|
if (obj.scale - mathutils.Vector((1.0, 1.0, 1.0))).length > 1e-4:
|
|
|
|
|
bpy.ops.bim.update_representation(obj=obj.name)
|
|
|
|
|
return element
|
|
|
|
|
if element.is_a("IfcGridAxis"):
|
|
|
|
|
return cls.sync_grid_axis_object_placement(obj, element)
|
|
|
|
|
if not hasattr(element, "ObjectPlacement"):
|
|
|
|
|
return
|
|
|
|
|
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
|
|
|
|
return element
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def sync_grid_axis_object_placement(cls, obj, element):
|
|
|
|
|
grid = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
|
|
|
|
grid_obj = tool.Ifc.get_object(grid)
|
|
|
|
|
if grid_obj:
|
|
|
|
|
cls.sync_object_placement(grid_obj)
|
|
|
|
|
if grid_obj.matrix_world != obj.matrix_world:
|
|
|
|
|
bpy.ops.bim.update_representation(obj=obj.name)
|
|
|
|
|
tool.Geometry.record_object_position(obj)
|
2022-04-13 12:21:18 +10:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_document_references(cls, document):
|
|
|
|
|
if tool.Ifc.get_schema() == "IFC2X3":
|
|
|
|
|
return document.DocumentReferences or []
|
|
|
|
|
return document.HasDocumentReferences or []
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_reference_element(cls, reference):
|
|
|
|
|
if tool.Ifc.get_schema() == "IFC2X3":
|
|
|
|
|
return [
|
|
|
|
|
r for r in tool.Ifc.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference
|
|
|
|
|
][0].RelatedObjects[0]
|
|
|
|
|
return reference.DocumentRefForObjects[0].RelatedObjects[0]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_drawing_human_scale(cls, drawing):
|
|
|
|
|
return ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"]["HumanScale"]
|