Compare commits

..

2 Commits

Author SHA1 Message Date
Andrej730 59eaa05a62 tests for system.get_connected_to / get_connected_from 2023-08-01 13:38:18 +05:00
Andrej730 99a3535959 error message on setting flow direction for disconnected port
previously it just did nothing which was a bit confusing
2023-08-01 13:17:35 +05:00
51 changed files with 320 additions and 1855 deletions
+4 -5
View File
@@ -22,14 +22,13 @@ import site
bl_info = {
"name": "BlenderBIM",
"description": "Transforms Blender into a native Building Information Model authoring platform using IFC.",
"description": "Author, import, and export data using the Industry Foundation Classes schema",
"author": "IfcOpenShell Contributors",
"blender": (3, 1, 0),
"blender": (2, 80, 0),
"version": (0, 0, 999999),
"location": "File Menu, Scene Properties Tab. See documentation for more.",
"doc_url": "https://blenderbim.org/docs",
"location": "File > Export, File > Import, Scene / Object / Material / Mesh Properties",
"tracker_url": "https://github.com/IfcOpenShell/IfcOpenShell/issues",
"category": "System",
"category": "Import-Export",
}
if sys.modules.get("bpy", None):
+1 -5
View File
@@ -1823,10 +1823,6 @@ class IfcImporter:
loop_total = [3] * num_loops
num_vertex_indices = len(geometry.faces)
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
@@ -1913,7 +1909,7 @@ class IfcImportSettings:
self.should_merge_materials_by_colour = False
self.should_load_geometry = True
self.should_use_native_meshes = False
self.should_clean_mesh = False
self.should_clean_mesh = True
self.should_cache = True
self.is_coordinating = True
self.deflection_tolerance = 0.001
@@ -21,7 +21,7 @@ from . import ui, prop, operator
classes = (
operator.AddBrick,
operator.AddBrickRelation,
operator.AddBrickFeed,
operator.AssignBrickReference,
operator.CloseBrickProject,
operator.ConvertBrickProject,
@@ -36,7 +36,6 @@ classes = (
operator.SerializeBrick,
operator.AddBrickNamespace,
operator.SetBrickListRoot,
operator.RemoveBrickRelation,
prop.Brick,
prop.BIMBrickProperties,
ui.BIM_PT_brickschema,
@@ -41,7 +41,7 @@ class BrickschemaData:
cls.is_loaded = True
cls.data = {
"is_loaded": cls.get_is_loaded(),
"active_relations": cls.active_relations(),
"attributes": cls.attributes(),
}
@classmethod
@@ -49,7 +49,7 @@ class BrickschemaData:
return BrickStore.graph is not None
@classmethod
def active_relations(cls):
def attributes(cls):
if BrickStore.graph is None:
return []
props = bpy.context.scene.BIMBrickProperties
@@ -80,32 +80,28 @@ class BrickschemaData:
)
)
for row in query:
predicate = row.get("predicate")
predicate_name = predicate.toPython().split("#")[-1]
predicate = row.get("predicate").toPython().split("#")[-1]
object = row.get("object")
object_name = object.toPython().split("#")[-1]
results.append(
{
"predicate": predicate,
"predicate_name": predicate_name,
"object": object,
"object_name": object_name,
"object": object.toPython().split("#")[-1],
"is_uri": isinstance(object, URIRef),
"object_uri": object.toPython(),
"is_globalid": predicate == "globalID",
}
)
# if isinstance(row.get("object"), BNode):
# for s, p, o in BrickStore.graph.triples((object, None, None)):
# results.append(
# {
# "predicate": predicate + ":" + p.toPython().split("#")[-1],
# "object": o.toPython().split("#")[-1],
# "is_uri": isinstance(o, URIRef),
# "object_uri": o.toPython(),
# "is_globalid": p.toPython().split("#")[-1] == "globalID",
# }
# )
if isinstance(row.get("object"), BNode):
for s, p, o in BrickStore.graph.triples((object, None, None)):
results.append(
{
"predicate": predicate + ":" + p.toPython().split("#")[-1],
"object": o.toPython().split("#")[-1],
"is_uri": isinstance(o, URIRef),
"object_uri": o.toPython(),
"is_globalid": p.toPython().split("#")[-1] == "globalID",
}
)
return results
@@ -16,7 +16,6 @@
# 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/>.
import os
import bpy
import ifcopenshell.api
import blenderbim.tool as tool
@@ -41,11 +40,8 @@ class LoadBrickProject(bpy.types.Operator, Operator):
filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"})
def _execute(self, context):
if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower():
root = context.scene.BIMBrickProperties.brick_list_root
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
else:
self.report({'ERROR'}, f'Failed to load {self.filepath}')
root = context.scene.BIMBrickProperties.brick_list_root
core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
@@ -57,10 +53,9 @@ class ViewBrickClass(bpy.types.Operator, Operator):
bl_label = "View Brick Class"
bl_options = {"REGISTER", "UNDO"}
brick_class: bpy.props.StringProperty(name="Brick Class")
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
def _execute(self, context):
core.view_brick_class(tool.Brick, brick_class=self.brick_class, split_screen=self.split_screen)
core.view_brick_class(tool.Brick, brick_class=self.brick_class)
class ViewBrickItem(bpy.types.Operator, Operator):
@@ -68,20 +63,18 @@ class ViewBrickItem(bpy.types.Operator, Operator):
bl_label = "View Brick Item"
bl_options = {"REGISTER", "UNDO"}
item: bpy.props.StringProperty(name="Brick Item")
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
def _execute(self, context):
core.view_brick_item(tool.Brick, item=self.item, split_screen=self.split_screen)
core.view_brick_item(tool.Brick, item=self.item)
class RewindBrickClass(bpy.types.Operator, Operator):
bl_idname = "bim.rewind_brick_class"
bl_label = "Rewind Brick Class"
bl_options = {"REGISTER", "UNDO"}
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
def _execute(self, context):
core.rewind_brick_class(tool.Brick, split_screen=self.split_screen)
core.rewind_brick_class(tool.Brick)
class CloseBrickProject(bpy.types.Operator, Operator):
@@ -133,31 +126,25 @@ class AddBrick(bpy.types.Operator, Operator):
tool.Brick,
element=tool.Ifc.get_entity(context.active_object) if context.selected_objects else None,
namespace=props.namespace,
brick_class=props.brick_entity_class,
brick_class=props.brick_entity_classes,
library=library,
label=props.new_brick_label,
)
class AddBrickRelation(bpy.types.Operator, Operator):
bl_idname = "bim.add_brick_relation"
bl_label = "Add Brick Relation"
class AddBrickFeed(bpy.types.Operator, Operator):
bl_idname = "bim.add_brick_feed"
bl_label = "Add Brick Feed"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = context.scene.BIMBrickProperties
brick = props.bricks[props.active_brick_index]
if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
object = props.new_brick_relation_object
elif props.split_screen_toggled:
object = props.split_screen_bricks[props.split_screen_active_brick_index].uri
else:
object = props.new_brick_relation_namespace + props.new_brick_relation_object
core.add_brick_relation(
source = tool.Ifc.get_entity([o for o in context.selected_objects if o != context.active_object][0])
destination = tool.Ifc.get_entity(context.active_object)
core.add_brick_feed(
tool.Ifc,
tool.Brick,
brick_uri=brick.uri,
predicate=props.new_brick_relation_type,
object=object
source=source,
destination=destination,
)
@@ -210,10 +197,9 @@ class RefreshBrickViewer(bpy.types.Operator, Operator):
bl_idname = "bim.refresh_brick_viewer"
bl_label = "Refresh Brick Viewer"
bl_options = {"REGISTER", "UNDO"}
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
def _execute(self, context):
core.refresh_brick_viewer(tool.Brick, split_screen=self.split_screen)
core.refresh_brick_viewer(tool.Brick)
class RemoveBrick(bpy.types.Operator, Operator):
@@ -274,24 +260,7 @@ class SetBrickListRoot(bpy.types.Operator, Operator):
bl_idname = "bim.set_brick_list_root"
bl_label = "Set Brick View Type"
bl_options = {"REGISTER", "UNDO"}
split_screen: bpy.props.BoolProperty(name="Split Screen", default=False, options={"HIDDEN"})
def _execute(self, context):
if self.split_screen:
root = context.scene.BIMBrickProperties.split_screen_brick_list_root
else:
root = context.scene.BIMBrickProperties.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=self.split_screen)
class RemoveBrickRelation(bpy.types.Operator, Operator):
bl_idname = "bim.remove_brick_relation"
bl_label = "Remove Relation"
bl_options = {"REGISTER", "UNDO"}
predicate: bpy.props.StringProperty(name="Relation")
object: bpy.props.StringProperty(name="Object")
def _execute(self, context):
props = context.scene.BIMBrickProperties
brick = props.bricks[props.active_brick_index]
core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object)
root = context.scene.BIMBrickProperties.brick_list_root
core.set_brick_list_root(tool.Brick, brick_root=root)
@@ -55,16 +55,6 @@ def get_brick_roots(self, context):
return [(root, root, "") for root in BrickStore.root_classes]
def get_brick_relations(self, context):
def is_label(relation):
return relation["predicate_name"] == "label"
if not list(filter(is_label, BrickschemaData.data["active_relations"])):
new_relations = BrickStore.relationships.copy()
new_relations.append(("http://www.w3.org/2000/01/rdf-schema#label", "label", ""))
return new_relations
return BrickStore.relationships
class Brick(PropertyGroup):
name: StringProperty(name="Name")
label: StringProperty(name="Label")
@@ -78,28 +68,11 @@ class BIMBrickProperties(PropertyGroup):
bricks: CollectionProperty(name="Bricks", type=Brick)
active_brick_index: IntProperty(name="Active Brick Index", update=update_active_brick_index)
libraries: EnumProperty(name="Libraries", items=get_libraries)
set_list_root_toggled: BoolProperty(name="Set List Root Toggled", default=False)
brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots)
# namespace manager
namespace: EnumProperty(name="Namespace", items=get_namespaces)
brick_entity_classes: EnumProperty(name="Brick Equipment Class", items=get_brick_entity_classes)
brick_settings_toggled: BoolProperty(name="Brick Settings Toggled", default=False)
new_brick_label: StringProperty(name="New Brick Label")
new_brick_namespace_alias: StringProperty(name="New Brick Namespace Alias")
new_brick_namespace_uri: StringProperty(name="New Brick Namespace URI")
# create brick entity
new_brick_label: StringProperty(name="New Brick Label")
brick_entity_create_type: EnumProperty(name="Brick Entity Types", items=get_brick_roots)
brick_entity_class: EnumProperty(name="Brick Equipment Class", items=get_brick_entity_classes)
# create relations
brick_create_relations_toggled: BoolProperty(name="Brick Create Relations Toggled", default=False)
brick_edit_relations_toggled: BoolProperty(name="Brick Edit Relations Toggled", default=False)
new_brick_relation_type: EnumProperty(name="New Brick Relation Type", items=get_brick_relations)
new_brick_relation_namespace: EnumProperty(name="New Brick Relation Namespace", items=get_namespaces)
new_brick_relation_object: StringProperty(name="New Brick Relation Object")
add_relation_failed: BoolProperty(name="Add Relation Failed", default=False)
# create relations split screen
split_screen_toggled: BoolProperty(name="Split Screen Toggled", default=False)
split_screen_bricks: CollectionProperty(name="Split Screen Bricks", type=Brick)
split_screen_active_brick_index: IntProperty(name="Split Screen Active Brick Index", update=update_active_brick_index)
split_screen_active_brick_class: StringProperty(name="Split Screen Active Brick Class")
split_screen_brick_breadcrumbs: CollectionProperty(name="Split Screen Brick Breadcrumbs", type=StrProperty)
split_screen_brick_list_root: EnumProperty(name="Split Screen Brick List Root", items=get_brick_roots)
brick_list_root: EnumProperty(name="Brick List Root", items=get_brick_roots)
brick_entity_create_type: EnumProperty(name="Brick Entity Types", items=get_brick_roots)
@@ -76,6 +76,10 @@ class BIM_PT_brickschema(Panel):
row.prop(data=self.props, property="new_brick_namespace_uri", text="")
row.operator("bim.add_brick_namespace", text="", icon="ADD")
row = box.row(align=True)
row.operator("bim.set_brick_list_root", text="Set View")
row.prop(data=self.props, property="brick_list_root", text="")
row = self.layout.row(align=True)
row.label(text="Create Entity:")
@@ -84,108 +88,35 @@ class BIM_PT_brickschema(Panel):
row = self.layout.row(align=True)
row.prop(data=self.props, property="new_brick_label", text="")
prop_with_search(row, self.props, "brick_entity_class", text="")
prop_with_search(row, self.props, "brick_entity_classes", text="")
row.operator("bim.add_brick", text="", icon="ADD")
# row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH")
row = self.layout.row(align=True)
col = row.column()
col.alignment = "RIGHT"
row.prop(data=self.props, property="split_screen_toggled", text="", icon="WINDOW")
grid = self.layout.grid_flow(even_columns=True)
grid1 = grid.column(align=True)
row = grid1.row(align=True)
col.alignment = "LEFT"
if len(self.props.brick_breadcrumbs):
op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV")
op.split_screen = False
row.prop(data=self.props, property="set_list_root_toggled", text="", icon="OUTLINER")
row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV")
col = row.column()
col.alignment = "RIGHT"
# row.operator("bim.add_brick_feed", text="", icon="PLUGIN")
row.operator("bim.remove_brick", text="", icon="X")
# row.operator("bim.refresh_brick_viewer", text="", icon="FILE_REFRESH")
row = self.layout.row(align=True)
row.label(text=self.props.active_brick_class)
if self.props.set_list_root_toggled:
row = grid1.row(align=True)
op = row.operator("bim.set_brick_list_root", text="Set View")
op.split_screen = False
row.prop(data=self.props, property="brick_list_root", text="")
self.layout.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
row = grid1.row()
BIM_UL_bricks.split_screen = False
row.template_list("BIM_UL_bricks", "", self.props, "bricks", self.props, "active_brick_index")
if self.props.split_screen_toggled:
grid2 = grid.column(align=True)
row = grid2.row(align=True)
if len(self.props.split_screen_brick_breadcrumbs):
op = row.operator("bim.rewind_brick_class", text="", icon="FRAME_PREV")
op.split_screen = True
row.label(text=self.props.split_screen_active_brick_class)
if self.props.set_list_root_toggled:
row = grid2.row(align=True)
op = row.operator("bim.set_brick_list_root", text="Set View")
op.split_screen = True
row.prop(data=self.props, property="split_screen_brick_list_root", text="")
row = grid2.row()
BIM_UL_bricks.split_screen = True
row.template_list("BIM_UL_bricks", "", self.props, "split_screen_bricks", self.props, "split_screen_active_brick_index")
if BrickschemaData.data["active_relations"]:
for attribute in BrickschemaData.data["attributes"]:
row = self.layout.row(align=True)
col = row.column()
col.alignment = "RIGHT"
row.prop(data=self.props, property="brick_create_relations_toggled", text="", icon="PLUGIN")
row.prop(data=self.props, property="brick_edit_relations_toggled", text="", icon="TOOL_SETTINGS")
row.operator("bim.remove_brick", text="", icon="X")
row = self.layout.row(align=True)
row.label(text="Create Relation:")
if self.props.brick_create_relations_toggled and self.props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label":
row = self.layout.row(align=True)
prop_with_search(row, self.props, "new_brick_relation_type", text="")
row.prop(data=self.props, property="new_brick_relation_object", text="")
row.operator("bim.add_brick_relation", text="", icon="ADD")
elif self.props.brick_create_relations_toggled and self.props.split_screen_toggled:
row = self.layout.row(align=True)
split_screen_selection = self.props.split_screen_bricks[self.props.split_screen_active_brick_index]
if split_screen_selection.total_items:
row.label(text="No selection", icon="INFO")
else:
prop_with_search(row, self.props, "new_brick_relation_type", text="")
row.label(text=split_screen_selection.label if split_screen_selection.label else split_screen_selection.name)
row.operator("bim.add_brick_relation", text="", icon="ADD")
elif self.props.brick_create_relations_toggled:
row = self.layout.row(align=True)
prop_with_search(row, self.props, "new_brick_relation_namespace", text="")
row = self.layout.row(align=True)
prop_with_search(row, self.props, "new_brick_relation_type", text="")
row.prop(data=self.props, property="new_brick_relation_object", text="")
row.operator("bim.add_brick_relation", text="", icon="ADD")
if self.props.brick_create_relations_toggled and self.props.add_relation_failed:
row = self.layout.row(align=True)
row.label(text="Failed to find this entity!", icon="ERROR")
for relation in BrickschemaData.data["active_relations"]:
row = self.layout.row(align=True)
row.label(text=relation["predicate_name"])
row.label(text=relation["object_name"])
if self.props.brick_edit_relations_toggled and relation["predicate_name"] != "type":
op = row.operator("bim.remove_brick_relation", text="", icon="UNLINKED")
op.predicate = relation["predicate"]
op.object = relation["object"]
if relation["is_uri"] and relation["predicate_name"] != "type":
row.label(text=attribute["predicate"])
row.label(text=attribute["object"])
if attribute["is_uri"]:
op = row.operator("bim.view_brick_item", text="", icon="DISCLOSURE_TRI_RIGHT")
op.item = relation["object_uri"]
if relation["is_globalid"]:
op.item = attribute["object_uri"]
if attribute["is_globalid"]:
op = row.operator("bim.select_global_id", icon="RESTRICT_SELECT_OFF", text="")
op.global_id = relation["object_name"]
op.global_id = attribute["object"]
class BIM_PT_ifc_brickschema_references(Panel):
@@ -238,15 +169,12 @@ class BIM_PT_ifc_brickschema_references(Panel):
class BIM_UL_bricks(UIList):
split_screen = False
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
label = item.label if item.label else item.name
if item.total_items:
op = row.operator("bim.view_brick_class", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False)
op.brick_class = item.name
op.split_screen = self.split_screen
label = label + " (" + str(item.total_items) + ")"
row.label(text=label)
row.label(text=item.label if item.label else item.name)
if item.total_items:
row.label(text=str(item.total_items))
@@ -228,7 +228,6 @@ class CreateShapeFromStepId(bpy.types.Operator):
bl_description = "Recreate a mesh object from a STEP ID"
bl_options = {"REGISTER", "UNDO"}
should_include_curves: bpy.props.BoolProperty()
step_id: bpy.props.IntProperty(default=0)
@classmethod
def poll(cls, context):
@@ -241,7 +240,7 @@ class CreateShapeFromStepId(bpy.types.Operator):
logger = logging.getLogger("ImportIFC")
self.ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
self.file = IfcStore.get_file()
element = self.file.by_id(self.step_id or int(context.scene.BIMDebugProperties.step_id))
element = self.file.by_id(int(context.scene.BIMDebugProperties.step_id))
settings = ifcopenshell.geom.settings()
if self.should_include_curves:
settings.set(settings.INCLUDE_CURVES, True)
@@ -1360,7 +1360,6 @@ class OverrideModeSetEdit(bpy.types.Operator):
should_sync_changes_first=False,
apply_openings=False,
)
tool.Geometry.dissolve_triangulated_edges(obj)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
else:
obj.select_set(False)
@@ -31,7 +31,6 @@ from . import (
stair,
window,
opening,
mep,
pie,
workspace,
profile,
@@ -39,7 +38,6 @@ from . import (
door,
railing,
roof,
mep,
)
classes = (
@@ -106,8 +104,6 @@ classes = (
space.GenerateSpace,
space.GenerateSpacesFromWalls,
space.ToggleSpaceVisibility,
mep.FitFlowSegments,
mep.RegenerateDistributionElement,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -176,8 +172,6 @@ classes = (
roof.EnableEditingRoofPath,
roof.RemoveRoof,
roof.SetGableRoofEdgeAngle,
mep.MEPAddObstruction,
mep.MEPAddTransition,
)
addon_keymaps = []
+17 -619
View File
@@ -18,14 +18,10 @@
import bpy
import math
import collections
import bmesh
import re
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.unit
import ifcopenshell.util.system
import ifcopenshell.util.element
import ifcopenshell.util.representation
import mathutils.geometry
@@ -34,168 +30,11 @@ import blenderbim.core.type
import blenderbim.core.root
import blenderbim.core.geometry
import blenderbim.tool as tool
from math import pi, degrees, radians
from copy import copy
from math import pi, degrees
from mathutils import Vector, Matrix
from ifcopenshell.util.shape_builder import ShapeBuilder
from blenderbim.bim.module.model.profile import DumbProfileJoiner
V = lambda *x: Vector([float(i) for i in x])
class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.regenerate_distribution_element"
bl_description = (
"Regenerates the positions and segment lengths of a distribution element and all connected elements."
)
bl_label = "Regenerate Distribution Element"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
current_element = tool.Ifc.get_entity(bpy.context.active_object)
processed_elements = set()
# The goal is to regenerate all recursively connected elements that
# minimise movement as much as possible.
# A queue is a list of branches. A branch is a list of elements in
# sequence, each one connecting to another element. An element in a
# branch may have a child queue. The queue and child queues are
# acyclic.
def extend_branch(element, branch, predecessor=None):
processed_elements.add(element)
branch_element = {"element": element, "children": [], "predecessor": predecessor}
branch.append(branch_element)
connected = {e for e in ifcopenshell.util.system.get_connected_to(element) if e not in processed_elements}
connected.update(
[e for e in ifcopenshell.util.system.get_connected_from(element) if e not in processed_elements]
)
if len(connected) == 1:
extend_branch(list(connected)[0], branch, element)
else:
for connected_element in connected:
branch_element["children"].append(extend_branch(connected_element, [], element))
return branch
queue = extend_branch(current_element, [])[0]["children"]
# import pprint
# pprint.pprint(queue)
def process_branch(branch):
for branch_element in branch:
element = branch_element["element"]
print("processing", element)
predecessor = branch_element["predecessor"]
if False: # If the element does not need to be transformed, return early.
return
# Perform the extend, translate, rotate, etc the element as necessary based on the predecessor.
# For segments, prioritise extensions instead of translations.
# For everything else, only translate. No rotation.
for child_branch in branch_element["children"]:
process_branch(child_branch)
for branch in queue:
process_branch(branch)
class FitFlowSegments(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.fit_flow_segments"
bl_description = "Add a fitting based on currently selected elements and cursor"
bl_label = "Fit Flow Segments"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
# TODO: need to add ui for parameters:
# - obstruction cap thickness
# - start/end thickness and angle for transition
selected_objs = []
selected_profiles = []
selected_class = None
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if element and element.is_a("IfcFlowSegment"):
if selected_class and not element.is_a(selected_class):
return # The user is mixing up ducts and pipes.
profile = tool.Model.get_flow_segment_profile(element)
if profile:
selected_profiles.append(profile)
selected_objs.append(obj)
selected_class = element.is_a()
total_selected_objs = len(selected_objs)
total_profiles = len(set(selected_profiles))
fitting_type = None
if total_selected_objs == 1:
fitting_type = "OBSTRUCTION"
bpy.ops.bim.mep_add_obstruction()
elif total_selected_objs == 2:
# Shorten the axis by the profile size to allow for fuzzy intersections
# e.g. if two ducts touch, we want a bend, not a cross.
axis1 = tool.Model.get_flow_segment_axis(selected_objs[0])
profile_size = max(selected_objs[0].dimensions.x, selected_objs[0].dimensions.y)
offset = (axis1[1] - axis1[0]).normalized() * profile_size
axis1 = (axis1[0] + offset, axis1[1] - offset)
axis2 = tool.Model.get_flow_segment_axis(selected_objs[1])
profile_size = max(selected_objs[1].dimensions.x, selected_objs[1].dimensions.y)
offset = (axis2[1] - axis2[0]).normalized() * profile_size
axis2 = (axis2[0] + offset, axis2[1] - offset)
angle = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True)
is_parallel = tool.Cad.is_x(angle, (0, 180), tolerance=0.001)
if total_profiles == 1:
if is_parallel:
return
intersect1, intersect2 = tool.Cad.intersect_edges(axis1, axis2)
is_on_axis1 = tool.Cad.is_point_on_edge(intersect1, axis1)
is_on_axis2 = tool.Cad.is_point_on_edge(intersect2, axis2)
if not is_on_axis1 and not is_on_axis2:
fitting_type = "BEND"
elif is_on_axis1 and is_on_axis2:
fitting_type = "CROSS"
else:
fitting_type = "TEE"
elif total_profiles == 2:
if is_parallel:
fitting_type = "TRANSITION"
elif total_selected_objs == 3:
if total_profiles > 1:
return
axis1 = tool.Model.get_flow_segment_axis(selected_objs[0])
axis2 = tool.Model.get_flow_segment_axis(selected_objs[1])
axis3 = tool.Model.get_flow_segment_axis(selected_objs[2])
angle12 = tool.Cad.angle_edges(axis1, axis2, signed=False, degrees=True)
angle13 = tool.Cad.angle_edges(axis1, axis3, signed=False, degrees=True)
angle21 = tool.Cad.angle_edges(axis2, axis1, signed=False, degrees=True)
angle23 = tool.Cad.angle_edges(axis2, axis3, signed=False, degrees=True)
is_parallel12 = tool.Cad.is_x(angle12, (0, 180), tolerance=0.001)
is_parallel13 = tool.Cad.is_x(angle13, (0, 180), tolerance=0.001)
is_parallel21 = tool.Cad.is_x(angle21, (0, 180), tolerance=0.001)
is_parallel23 = tool.Cad.is_x(angle23, (0, 180), tolerance=0.001)
if not all(is_parallel12, is_parallel13, is_parallel21, is_parallel23):
fitting_type = "WYE"
if not fitting_type:
return
print(fitting_type)
class MEPGenerator:
class MepGenerator:
def __init__(self, relating_type=None):
self.relating_type = relating_type
@@ -203,465 +42,24 @@ class MEPGenerator:
self.file = tool.Ifc.get()
self.collection = bpy.context.view_layer.active_layer_collection.collection
segment = tool.Ifc.get_entity(obj)
representation = ifcopenshell.util.representation.get_representation(segment, "Model", "Body", "MODEL_VIEW")
element = tool.Ifc.get_entity(obj)
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
extrusion = tool.Model.get_extrusion(representation)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
length = extrusion.Depth * si_conversion
start_port_matrix = obj.matrix_world @ Matrix()
end_port_matrix = obj.matrix_world @ Matrix.Translation((0, 0, length))
end_port_matrix = Matrix.Translation((0, 0, length))
ports = tool.System.get_ports(segment)
if segment.is_a("IfcFlowSegment") and not ports:
tool.System.add_ports(obj)
ports = tool.System.get_ports(element)
if not ports:
start_port_matrix = Matrix()
for mat, flow_direction in zip([start_port_matrix, end_port_matrix], ("SINK", "SOURCE")):
port = tool.Ifc.run("system.add_port", element=element)
port.FlowDirection = flow_direction
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True)
return
# adjust current segment ports and related flow segments
segment_data = self.get_segment_data(segment)
for port_position in ("start_port", "end_port"):
port = segment_data.get(port_position, None)
if not port:
continue
# no need to correct start port position - it's corrected automatically
# as DumbProfileJoiner already moved the general segment position in that case
if port_position == "end_port":
tool.Model.edit_element_placement(port, end_port_matrix)
connected_port = tool.System.get_connected_port(port)
if not connected_port:
continue
# handle only obstructions for now
connected_element = tool.System.get_port_relating_element(connected_port)
def get_predefined_type(element):
element_type = ifcopenshell.util.element.get_type(element)
if element_type:
return element_type.PredefinedType
return element.PredefinedType
connected_obj = tool.Ifc.get_object(connected_element)
connected_element_length = connected_obj.dimensions.z
if (segment.is_a("IfcFlowSegment") and get_predefined_type(connected_element) == "OBSTRUCTION") or (
segment.is_a("IfcFlowFitting") and connected_element.is_a("IfcFlowSegment")
):
if port_position == "start_port":
if segment.is_a("IfcFlowFitting"):
connected_element_length = (
tool.Model.get_flow_segment_axis(connected_obj)[0]
- tool.Model.get_flow_segment_axis(obj)[0]
).length
connected_port_matrix = start_port_matrix @ Matrix.Translation((0, 0, -connected_element_length))
else:
connected_port_matrix = end_port_matrix
connected_obj.matrix_world = connected_port_matrix
if port_position == "start_port" and segment.is_a("IfcFlowFitting"):
profile_joiner = DumbProfileJoiner()
profile_joiner.set_depth(connected_obj, connected_element_length)
def get_segment_data(self, segment):
ports = tool.System.get_ports(segment)
segment_object = tool.Ifc.get_object(segment)
start_point = segment_object.location
extrusion_depth = segment_object.dimensions.z
end_point = segment_object.matrix_world @ V(0, 0, extrusion_depth)
segment_data = {
"start_point": start_point.copy().freeze(),
"end_point": end_point.freeze(),
"ports": ports,
"extrusion_depth": extrusion_depth,
}
for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if tool.Cad.is_x(port_local_position.length, 0.0):
segment_data["start_port"] = port
else:
segment_data["end_port"] = port
return segment_data
def get_mep_element_class_name(self, element, mep_class_type):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
class_name = "".join(split_camel_case(element.is_a())[:-1] + [mep_class_type])
return class_name
def get_compatible_fitting_type(self, segment_or_segments, port_or_ports, predefined_type):
"""
returns a dict of compatible fitting_type and start_port_match flag to correctly place the fitting.
We find compatible fitting only by checking
if they were already used with that segment type before
and fitting's ports should match `port_or_ports` by PredefinedType and SystemType.
If port from `port_or_ports` has PredefinedType/SystemType == None/NOTDEFINED then
those parameters won't be taken into account checking compatibility.
There lies the problem that it won't be
able to identify the fittings that were not yet connected to any segments yet.
"""
if not isinstance(segment_or_segments, collections.abc.Iterable):
segments = [segment_or_segments]
ports = [port_or_ports]
else:
segments = segment_or_segments
ports = port_or_ports
segments_data = []
for segment, port in zip(segments, ports, strict=True):
segment_type = ifcopenshell.util.element.get_type(segment)
# if segment doesn't have type we cannot check compatibility by available occurences
if segment_type is None:
return
segments_data.append((segment_type, port.PredefinedType, port.SystemType))
def are_connected_elements_compatible(segments_data, fitting_data):
# prevent arguments mutation, not using deepcopy because of the errors with ifc elements
segments_data = [copy(i) for i in segments_data]
fitting_data = [copy(i) for i in fitting_data]
not_defined_values = {"NOTDEFINED", None}
if len(segments_data) != len(fitting_data):
return False
def are_segments_compatible(test_segment_data, base_segment_data):
segment_type, predefined_type, system_type = test_segment_data
base_segment_type, base_predefined_type, base_system_type = base_segment_data
if segment_type != base_segment_type:
return False
if predefined_type not in not_defined_values and predefined_type != base_predefined_type:
return False
if system_type not in not_defined_values and system_type != base_system_type:
return False
return True
# NOTE: I have a feeling that there are cases where order
# in which we're checking the segments is important
# but I couldn't pin it down exact cases
for test_segment_data in fitting_data[:]:
for base_segment_data in segments_data:
if not are_segments_compatible(test_segment_data, base_segment_data):
continue
segments_data.remove(test_segment_data)
# all segments were sorted
return len(segments_data) == 0
def pack_return_data(fitting_type, ports, segments_data):
for port in ports:
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
if tool.Cad.is_x(port_local_position.length, 0.0):
start_port = port
break
connected_port = tool.System.get_connected_port(start_port)
connected_element = tool.System.get_port_relating_element(connected_port)
element_type = ifcopenshell.util.element.get_type(connected_element)
return {"fitting_type": fitting_type, "start_port_match": element_type == segments_data[0][0]}
fitting_types = tool.Ifc.get().by_type(self.get_mep_element_class_name(segments[0], "FittingType"))
for fitting_type in fitting_types:
if fitting_type.PredefinedType != predefined_type:
continue
fittings = tool.Ifc.get_all_element_occurences(fitting_type)
if not fittings:
continue
fitting = fittings[0]
ports = ifcopenshell.util.system.get_ports(fitting)
fitting_data = []
fitting_connected_to_none_type = False
for port in ports:
connected_port = tool.System.get_connected_port(port)
connected_element = tool.System.get_port_relating_element(connected_port)
element_type = ifcopenshell.util.element.get_type(connected_element)
if element_type is None:
fitting_connected_to_none_type = True
break
fitting_data.append((element_type, port.PredefinedType, port.SystemType))
if fitting_connected_to_none_type:
continue
if are_connected_elements_compatible(segments_data, fitting_data):
return pack_return_data(fitting_type, ports, segments_data)
def create_obstruction_type(self, segment):
# code is very similar to "bim.add_type"
profile_set = ifcopenshell.util.element.get_material(segment, should_skip_usage=True)
material_profile = profile_set.MaterialProfiles[0]
profile = material_profile.Profile
material = material_profile.Material
ifc_class = self.get_mep_element_class_name(segment, "FittingType")
ifc_file = tool.Ifc.get()
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
obj = bpy.data.objects.new("Obstruction", None)
# TODO: OBSTRUCTION predefined type is available only for IfcDuctFitting and IfcPipeFitting
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=ifc_class,
predefined_type="OBSTRUCTION",
should_add_representation=True,
context=body,
ifc_representation_class=None,
)
rel = ifcopenshell.api.run("material.assign_material", ifc_file, product=element, type="IfcMaterialProfileSet")
profile_set = rel.RelatingMaterial
material_profile = ifcopenshell.api.run(
"material.add_profile", ifc_file, profile_set=profile_set, material=material
)
ifcopenshell.api.run("material.assign_profile", ifc_file, material_profile=material_profile, profile=profile)
return element
def add_obstruction(self, segment, length, at_segment_start=False):
"""
`segment` is a segment ifc element
`length` is obstruction length provided in si units
returns `(None, error_message)` if there was some error in the process
or returns `(obstruction_element, None)` if everything went fine.
"""
related_port_name = "start" if at_segment_start else "end"
segment_data = self.get_segment_data(segment)
related_port = segment_data[f"{related_port_name}_port"]
# communicate error cases
if related_port.ConnectedTo or related_port.ConnectedFrom:
return None, f"Failed to add obstruction - {related_port_name} port is already connected."
if length >= segment_data["extrusion_depth"]:
return None, "Failed to add obstruction - obstruction length is larger than the segment."
segment_obj = tool.Ifc.get_object(segment)
segment_matrix = segment_obj.matrix_world
segment_rotation = segment_matrix.to_quaternion()
fitting_data = self.get_compatible_fitting_type(segment, related_port, "OBSTRUCTION")
obstruction_type = fitting_data["fitting_type"] if fitting_data else None
if not obstruction_type:
obstruction_type = self.create_obstruction_type(segment)
profile_joiner = DumbProfileJoiner()
# create obstruction occurence and setup it's length and port
# NOTE: at this point we loose current blender objects selection
bpy.ops.bim.add_constr_type_instance(relating_type_id=obstruction_type.id())
obstruction_obj = bpy.context.active_object
obstruction_obj.matrix_world = segment_matrix
profile_joiner.set_depth(obstruction_obj, length)
obstruction_port = tool.System.add_ports(
obstruction_obj,
add_start_port=not at_segment_start,
add_end_port=at_segment_start,
)[0]
# change segment length
new_segment_length = segment_data["extrusion_depth"] - length
profile_joiner.set_depth(segment_obj, new_segment_length)
if at_segment_start:
segment_obj.location += segment_rotation @ V(0, 0, length)
else:
obstruction_obj.location += segment_rotation @ V(0, 0, new_segment_length)
tool.Ifc.run("system.connect_port", port1=related_port, port2=obstruction_port, direction="NOTDEFINED")
obstruction = tool.Ifc.get_entity(obstruction_obj)
return obstruction, None
class MEPAddObstruction(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mep_add_obstruction"
bl_label = "Add Obstruction"
bl_description = "Adds obstruction to the MEP segment"
bl_options = {"REGISTER", "UNDO"}
length: bpy.props.FloatProperty(
name="Obstruction Length", description="Obstruction length in SI units", default=0.1, subtype="DISTANCE"
)
segment_id: bpy.props.IntProperty(name="Segment Element ID", default=0)
def _execute(self, context):
if self.segment_id:
element = tool.Ifc.get().by_id(self.segment_id)
else:
element = tool.Ifc.get_entity(context.active_object)
if not element:
return {"CANCELLED"}
if not element.is_a("IfcFlowSegment"):
self.report({"ERROR"}, f"Failed to add obstruction - object is not a MEP segment: {element.is_a()}.")
return {"CANCELLED"}
# derive obstruction position from the cursor
cursor_location = bpy.context.scene.cursor.location
obj = tool.Ifc.get_object(element)
axis = tool.Model.get_flow_segment_axis(obj)
# check if cursor is closer to the segment start
at_segment_start = tool.Cad.edge_percent(cursor_location, axis) < 0.5
obstruction, error_msg = MEPGenerator().add_obstruction(element, self.length, at_segment_start)
if error_msg:
self.report({"ERROR"}, error_msg)
return {"CANCELLED"}
return {"FINISHED"}
class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.mep_add_transition"
bl_label = "Add Transition"
bl_description = (
"Adds transition between two MEP elements. Elements are either provided by ID or selected in Blender"
)
bl_options = {"REGISTER", "UNDO"}
start_length: bpy.props.FloatProperty(
name="Start Length", description="Transition start length in SI units", default=0.1, subtype="DISTANCE"
)
end_length: bpy.props.FloatProperty(
name="End Length", description="Transition end length in SI units", default=0.1, subtype="DISTANCE"
)
start_segment_id: bpy.props.IntProperty(name="Start Segment Element ID", default=0)
end_segment_id: bpy.props.IntProperty(name="End Segment Element ID", default=0)
def _execute(self, context):
start_element, end_element = None, None
ifc_file = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
if self.start_segment_id and self.end_segment_id:
start_element = ifc_file.by_id(self.start_segment_id)
end_element = ifc_file.by_id(self.end_segment_id)
start_object = tool.Ifc.get_object(start_element)
end_object = tool.Ifc.get_object(end_element)
elif len(context.selected_objects) == 2:
start_object = context.active_object
end_object = next(o for o in context.selected_objects if o != context.active_object)
start_element = tool.Ifc.get_entity(start_object)
end_element = tool.Ifc.get_entity(end_object)
if not start_element or not end_element:
self.report({"ERROR"}, f"Two IFC elements should be selected for the transition")
return {"CANCELLED"}
else:
self.report({"ERROR"}, f"Two IFC elements should be provided for the transition")
return {"CANCELLED"}
# TODO: support IfcFlowTerminal
def is_mep(element):
return element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting")
if not is_mep(start_element) or not is_mep(end_element):
self.report(
{"ERROR"},
f"Failed to add transition - some object is not a MEP element: {start_element.is_a()}, {end_element.is_a()}.",
)
return {"CANCELLED"}
start_axis = tool.Model.get_flow_segment_axis(start_object)
end_axis = tool.Model.get_flow_segment_axis(end_object)
# TODO: support cases when segments are partially or completely overlapping each other
if not tool.Cad.are_edges_collinear(start_axis, end_axis):
self.report({"ERROR"}, f"Failed to add transition - non collinear segments are not yet supported.")
return {"CANCELLED"}
start_segment_data = MEPGenerator().get_segment_data(start_element)
end_segment_data = MEPGenerator().get_segment_data(end_element)
end_port = end_segment_data["start_port"]
start_port = start_segment_data["end_port"]
points_ports_map = {
start_segment_data["start_point"]: start_segment_data["start_port"],
start_segment_data["end_point"]: start_segment_data["end_port"],
end_segment_data["start_point"]: end_segment_data["start_port"],
end_segment_data["end_point"]: end_segment_data["end_port"],
}
start_point, end_point = tool.Cad.closest_points(
(start_segment_data["start_point"], start_segment_data["end_point"]),
(end_segment_data["start_point"], end_segment_data["end_point"]),
)
transition_dir = (end_point - start_point).normalized()
start_port = points_ports_map[start_point]
end_port = points_ports_map[end_point]
# add transition representation
builder = ShapeBuilder(ifc_file)
rep, transition_data = builder.mep_transition_shape(
start_element, end_element, self.start_length / si_conversion, self.end_length / si_conversion
)
if not rep:
self.report({"ERROR"}, f"Failed to add transition - this kind of profiles is not yet supported.")
return {"CANCELLED"}
middle_point = (start_point + end_point) / 2
full_transition_length = transition_data["full_transition_length"] * si_conversion
start_segment_extend_point = middle_point - transition_dir * full_transition_length / 2
end_segment_extend_point = middle_point + transition_dir * full_transition_length / 2
DumbProfileJoiner().join_E(start_object, start_segment_extend_point)
DumbProfileJoiner().join_E(end_object, end_segment_extend_point)
fitting_data = MEPGenerator().get_compatible_fitting_type(
[start_element, end_element], [start_port, end_port], "TRANSITION"
)
transition_type = fitting_data["fitting_type"] if fitting_data else None
start_port_match = fitting_data["start_port_match"] if fitting_data else True
if not transition_type:
mesh = bpy.data.meshes.new("Transition")
obj = bpy.data.objects.new("Transition", mesh)
transition_type = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=MEPGenerator().get_mep_element_class_name(start_element, "FittingType"),
predefined_type="TRANSITION",
should_add_representation=False,
)
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
tool.Model.replace_object_ifc_representation(body, obj, rep)
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=transition_type, name="BBIM_Fitting")
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(transition_data, default=list)},
)
# NOTE: at this point we loose current blender objects selection
bpy.ops.bim.add_constr_type_instance(relating_type_id=transition_type.id())
transition_obj = bpy.context.active_object
# adjust transition segment rotation and location
transition_obj.matrix_world = start_object.matrix_world
context.view_layer.update()
transition_obj_dir = tool.Cad.get_edge_direction(tool.Model.get_flow_segment_axis(transition_obj))
direction_match = tool.Cad.are_vectors_equal(transition_obj_dir, transition_dir)
# if there are no mismatches or everything matches up we don't need to flip the transition
if start_port_match != direction_match:
transition_obj.matrix_world = start_object.matrix_world @ Matrix.Rotation(radians(180), 4, "X")
transition_obj.location = start_segment_extend_point if start_port_match else end_segment_extend_point
# add ports and connect them
ports = tool.System.add_ports(transition_obj)
if not start_port_match:
start_port, end_port = end_port, start_port
tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED")
tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED")
return {"FINISHED"}
# TODO: better way to find the port to be moved
end_port = next((p for p in ports if p.FlowDirection == "SOURCE"), None)
if not end_port:
return
tool.Model.edit_element_placement(end_port, obj.matrix_world @ end_port_matrix)
@@ -265,7 +265,7 @@ class FilledOpeningGenerator:
extrusion = shape_builder.extrude(
get_curve_2d_from_3d(profile),
magnitude=thickness / unit_scale,
position=Vector([0.0, -thickness * 0.5 / unit_scale, 0.0]),
position=Vector([0.0, - thickness * 0.5 / unit_scale, 0.0]),
position_x_axis=Vector((1, 0, 0)),
position_z_axis=Vector((0, -1, 0)),
extrusion_vector=Vector((0, 0, -1)),
@@ -273,7 +273,7 @@ class FilledOpeningGenerator:
return shape_builder.get_representation(context, [extrusion])
x, y, z = filling_obj.dimensions
opening_position = Vector([0.0, -thickness * 0.5 / unit_scale, 0.0])
opening_position = Vector([0.0, - thickness * 0.5 / unit_scale, 0.0])
opening_size = Vector([x, z]) / unit_scale
# Windows and doors can have a casing that overlaps the wall
@@ -754,7 +754,27 @@ class EditOpenings(Operator, tool.Ifc.Operator):
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
tool.Model.reload_body_representation(building_objs)
decomposed_building_objs = set()
for obj in building_objs:
decomposed_building_objs.add(obj)
for subelement in ifcopenshell.util.element.get_decomposition(tool.Ifc.get_entity(obj)):
subobj = tool.Ifc.get_object(subelement)
if subobj:
decomposed_building_objs.add(subobj)
for obj in decomposed_building_objs:
if obj.data:
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
return {"FINISHED"}
def get_all_building_objects_of_similar_openings(self, opening):
@@ -100,11 +100,9 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator):
props.type_predefined_type = "BEAM"
props.type_template = "PROFILESET"
elif self.ifc_element_type == "IfcDuctSegmentType":
props.type_predefined_type = "RIGIDSEGMENT"
props.type_template = "FLOW_SEGMENT_RECTANGULAR"
return
elif self.ifc_element_type == "IfcPipeSegmentType":
props.type_predefined_type = "RIGIDSEGMENT"
props.type_template = "FLOW_SEGMENT_CIRCULAR"
return
bpy.ops.bim.add_type()
@@ -113,6 +111,7 @@ class AddConstrTypeInstance(bpy.types.Operator):
bl_label = "Add"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add Type Instance to the model"
ifc_class: bpy.props.StringProperty()
relating_type_id: bpy.props.IntProperty()
from_invoke: bpy.props.BoolProperty(default=False)
@@ -139,10 +138,9 @@ class AddConstrTypeInstance(bpy.types.Operator):
if material and material.is_a("IfcMaterialProfileSet"):
if obj := profile.DumbProfileGenerator(relating_type).generate():
tool.Blender.select_and_activate_single_object(context, obj)
if relating_type.is_a("IfcFlowSegmentType"):
self.set_flow_segment_rl(obj)
mep.MEPGenerator(relating_type).setup_ports(obj)
mep.MepGenerator(relating_type).setup_ports(obj)
return {"FINISHED"}
elif material and material.is_a("IfcMaterialLayerSet"):
if self.generate_layered_element(ifc_class, relating_type):
@@ -232,13 +230,15 @@ class AddConstrTypeInstance(bpy.types.Operator):
bpy.ops.bim.add_filled_opening(voided_obj=building_obj.name, filling_obj=obj.name)
else:
if collection_obj and tool.Ifc.get_entity(collection_obj):
obj.location.z = collection_obj.location.z - tool.Blender.get_object_bounding_box(obj)["min_z"]
obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box])
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for port in ifcopenshell.util.system.get_ports(relating_type):
mat = Matrix(ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement))
mat.translation *= unit_scale
mat = obj.matrix_world @ mat
mat = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement)
mat[0][3] *= unit_scale
mat[1][3] *= unit_scale
mat[2][3] *= unit_scale
mat = obj.matrix_world @ mathutils.Matrix(mat)
new_port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
tool.Ifc.run("system.assign_port", element=element, port=new_port)
tool.Ifc.run("geometry.edit_object_placement", product=new_port, matrix=mat, is_si=True)
@@ -34,6 +34,7 @@ from mathutils import Vector, Matrix, Quaternion
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.wall import DumbWallRecalculator
from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.model.mep import MepGenerator
class DumbProfileGenerator:
@@ -491,11 +492,8 @@ class DumbProfileJoiner:
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
if element.is_a("IfcFlowSegment") or element.is_a("IfcFlowFitting"):
# lazy import to avoid circular import errors
from blenderbim.bim.module.model.mep import MEPGenerator
MEPGenerator().setup_ports(obj)
if element.is_a("IfcFlowSegment"):
MepGenerator().setup_ports(obj)
def join(self, profile1, profile2, connection1, connection2, is_relating=True, description="BUTT"):
element1 = tool.Ifc.get_entity(profile1)
@@ -803,23 +801,14 @@ class RecalculateProfile(bpy.types.Operator, tool.Ifc.Operator):
class DumbProfileRecalculator:
def recalculate(self, profiles):
"`profiles` is a list of blender profile objects"
queue = set()
# also recalculate all connected elements
for profile in profiles:
element = tool.Ifc.get_entity(profile)
queue.add((element, profile))
connected_elements = []
for rel in getattr(element, "ConnectedTo", []):
connected_elements.append(rel.RelatedElement)
queue.add((rel.RelatedElement, tool.Ifc.get_object(rel.RelatedElement)))
for rel in getattr(element, "ConnectedFrom", []):
connected_elements.append(rel.RelatingElement)
for element in connected_elements:
queue.add((element, tool.Ifc.get_object(element)))
queue.add((rel.RelatingElement, tool.Ifc.get_object(rel.RelatingElement)))
joiner = DumbProfileJoiner()
for element, profile in queue:
if profile:
@@ -830,7 +819,7 @@ class ChangeProfileDepth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_profile_depth"
bl_label = "Change Profile Length"
bl_options = {"REGISTER", "UNDO"}
depth: bpy.props.FloatProperty(subtype="DISTANCE")
depth: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
@@ -151,9 +151,9 @@ class BIMModelProperties(PropertyGroup):
("STAIR", "Stair", "Parametric stair"),
("RAILING", "Railing", "Parametric railing"),
("ROOF", "Roof", "Parametric roof"),
("FLOW_SEGMENT_RECTANGULAR", "Rectangular Distribution Segment", "Works similarly to Profile, has distribution ports"),
("FLOW_SEGMENT_CIRCULAR", "Circular Distribution Segment", "Works similarly to Profile, has distribution ports"),
("FLOW_SEGMENT_CIRCULAR_HOLLOW", "Circular Hollow Distribution Segment", "Works similarly to Profile, has distribution ports"),
("DISTRIBUTION_SEGMENT_RECTANGULAR", "Rectangular Distribution Segment", "Works similarly to Profile, has distribution ports"),
("DISTRIBUTION_SEGMENT_CIRCULAR", "Circular Distribution Segment", "Works similarly to Profile, has distribution ports"),
("DISTRIBUTION_SEGMENT_CIRCULAR_HOLLOW", "Circular Hollow Distribution Segment", "Works similarly to Profile, has distribution ports"),
),
name="Type Template",
default="MESH",
@@ -51,9 +51,8 @@ class LaunchTypeManager(bpy.types.Operator):
props = context.scene.BIMModelProperties
props.type_page = 1
if get_ifc_class(None, context):
ifc_class = props.ifc_class or AuthoringData.data["ifc_element_type"]
props.type_class = ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
props.type_class = props.ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class, offset=0, limit=9)
return context.window_manager.invoke_popup(self, width=550)
def draw(self, context):
@@ -125,6 +124,7 @@ class LaunchTypeManager(bpy.types.Operator):
text = f"Add {relating_type['predefined_type']}" if relating_type["predefined_type"] else "Add"
op = row.operator("bim.add_constr_type_instance", icon="ADD", text=text)
op.from_invoke = True
op.ifc_class = relating_type["ifc_class"]
op.relating_type_id = relating_type["id"]
op = row.operator("bim.rename_type", icon="GREASEPENCIL", text="")
@@ -82,7 +82,18 @@ def update_simple_openings(element, opening_width, opening_height):
has_replaced_opening_representation = True
tool.Model.reload_body_representation(voided_objs)
for obj in voided_objs:
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
def update_window_modifier_representation(context, obj):
@@ -318,20 +318,10 @@ class BimToolUI:
op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
"IfcCableSegment",
"IfcDuctSegment",
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
@@ -368,6 +358,14 @@ class BimToolUI:
elif AuthoringData.data["active_class"] in ("IfcSpace",):
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
elif AuthoringData.data["active_class"] in (
"IfcCableCarrierSegmentType",
"IfcCableSegmentType",
"IfcDuctSegmentType",
"IfcPipeSegmentType",
):
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
@@ -413,6 +411,7 @@ class BimToolUI:
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
op.ifc_class = cls.props.ifc_class
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
@@ -464,6 +463,7 @@ class BimToolUI:
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
op.ifc_class = cls.props.ifc_class
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
@@ -633,8 +633,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.flip_wall()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.flip_fill()
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
def hotkey_S_G(self):
if not bpy.context.selected_objects:
@@ -1028,14 +1028,10 @@ class ExportIFC(bpy.types.Operator):
output_file = os.path.relpath(output_file, bpy.path.abspath("//"))
if scene.BIMProperties.ifc_file != output_file and extension not in ["ifczip", "ifcjson"]:
scene.BIMProperties.ifc_file = output_file
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
if save_blend_file:
if bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
blenderbim.bim.handler.purge_module_data()
self.report(
{"INFO"},
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
)
self.report({"INFO"}, f'IFC Project "{os.path.basename(output_file)}" Saved')
if bpy.data.is_saved:
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
@@ -151,7 +151,7 @@ class BIMProjectProperties(PropertyGroup):
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_load_geometry: BoolProperty(name="Load Geometry", default=True)
should_use_native_meshes: BoolProperty(name="Native Meshes", default=False)
should_clean_mesh: BoolProperty(name="Clean Meshes", default=False)
should_clean_mesh: BoolProperty(name="Clean Meshes", default=True)
should_cache: BoolProperty(name="Cache", default=False)
is_coordinating: BoolProperty(name="For Coordination Only", default=False)
deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.001)
@@ -91,15 +91,8 @@ class PortData:
@classmethod
def load(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
cls.element = element
is_port = cls.is_port()
cls.data = {
"total_ports": cls.total_ports(),
"located_ports_data": cls.located_ports_data(),
"is_port": is_port,
"port_connected_object": cls.port_connected_object() if is_port else None,
"port_relating_object": cls.port_relating_object() if is_port else None,
}
cls.is_loaded = True
@@ -107,36 +100,3 @@ class PortData:
def total_ports(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
return len(ifcopenshell.util.system.get_ports(element))
@classmethod
def is_port(cls):
return cls.element and cls.element.is_a("IfcDistributionPort")
@classmethod
def port_relating_object(cls):
return tool.Ifc.get_object(tool.System.get_port_relating_element(cls.element))
@classmethod
def port_connected_object(cls):
connected_port = tool.System.get_connected_port(cls.element)
if not connected_port:
return
connected_element = tool.System.get_port_relating_element(connected_port)
return tool.Ifc.get_object(connected_element)
@classmethod
def located_ports_data(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
ports = ifcopenshell.util.system.get_ports(element)
data = []
for port in ports:
port_obj = tool.Ifc.get_object(port)
connected_port = tool.System.get_connected_port(port)
if connected_port:
connected_element = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port))
else:
connected_element = None
data.append((port, port_obj, connected_element))
return data
@@ -22,7 +22,6 @@ import blenderbim.tool as tool
import blenderbim.core.system as core
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.system.data import PortData
class Operator:
@@ -140,15 +139,6 @@ class ShowPorts(bpy.types.Operator, Operator):
bl_label = "Show Ports"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not PortData.is_loaded:
PortData.load()
if PortData.data["total_ports"] == 0:
cls.poll_message_set("No ports found")
return False
return True
def _execute(self, context):
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=tool.Ifc.get_entity(context.active_object))
@@ -158,17 +148,12 @@ class HidePorts(bpy.types.Operator, Operator):
bl_label = "Hide Ports"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return ShowPorts.poll(context)
def _execute(self, context):
core.hide_ports(tool.Ifc, tool.System, element=tool.Ifc.get_entity(context.active_object))
class AddPort(bpy.types.Operator, Operator):
bl_idname = "bim.add_port"
bl_description = "Add port at current cursor position"
bl_label = "Add Port"
bl_options = {"REGISTER", "UNDO"}
@@ -205,14 +190,8 @@ class DisconnectPort(bpy.types.Operator, Operator):
bl_label = "Disconnect Ports"
bl_options = {"REGISTER", "UNDO"}
element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"})
def _execute(self, context):
if self.element_id != 0:
element = tool.Ifc.get().by_id(self.element_id)
else:
element = tool.Ifc.get_entity(context.active_object)
core.disconnect_port(tool.Ifc, port=element)
core.disconnect_port(tool.Ifc, port=tool.Ifc.get_entity(context.active_object))
class SetFlowDirection(bpy.types.Operator, Operator):
@@ -225,7 +204,7 @@ class SetFlowDirection(bpy.types.Operator, Operator):
port = tool.Ifc.get_entity(context.active_object)
second_port = tool.System.get_connected_port(port)
if not second_port:
self.report({"ERROR"}, "To set flow direction port has to be connected to another one.")
self.report({"ERROR"}, "To set flow direction port hast to be connected to another one.")
return
core.set_flow_direction(
tool.Ifc, tool.System, port=tool.Ifc.get_entity(context.active_object), direction=self.direction
@@ -23,14 +23,6 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.system.data import SystemData, ObjectSystemData, PortData
FLOW_DIRECTION_TO_ICON = {
"SOURCE": "FORWARD",
"SINK": "BACK",
"SOURCEANDSINK": "ARROW_LEFTRIGHT",
"NOTDEFINED": "RESTRICT_INSTANCED_ON",
}
class BIM_PT_systems(Panel):
bl_label = "Systems"
bl_idname = "BIM_PT_systems"
@@ -164,43 +156,11 @@ class BIM_PT_ports(Panel):
self.props = context.scene.BIMSystemProperties
row = self.layout.row(align=True)
total_ports = PortData.data["total_ports"]
row.label(text=f"{total_ports} Ports Found", icon="PLUGIN")
row.label(text=f"{PortData.data['total_ports']} Ports Found", icon="PLUGIN")
row.operator("bim.show_ports", icon="HIDE_OFF", text="")
row.operator("bim.hide_ports", icon="HIDE_ON", text="")
row.operator("bim.add_port", icon="ADD", text="")
if total_ports == 0:
return
row = self.layout.row(align=True)
row.label(text="Ports located on object and connected objects:")
row = self.layout.row(align=True)
cols = [row.column(align=True) for i in range(6)]
for i, port_data in enumerate(PortData.data["located_ports_data"]):
port, port_obj, connected_obj = port_data
flow_direction_icon = FLOW_DIRECTION_TO_ICON[port.FlowDirection or "NOTDEFINED"]
if port_obj:
cols[0].label(text="", icon=flow_direction_icon)
cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port.id()
cols[2].label(text=port_obj.name)
else:
cols[0].label(text="", icon=flow_direction_icon)
cols[1].label(text="", icon="HIDE_ON")
cols[2].label(text="Port is hidden")
if connected_obj:
cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port.id()
cols[4].operator(
"bim.select_entity", text="", icon="RESTRICT_SELECT_OFF"
).ifc_id = connected_obj.BIMObjectProperties.ifc_definition_id
cols[5].label(text=f"{connected_obj.name}")
else:
cols[3].label(text="", icon="UNLINKED")
cols[4].label(text="", icon="BLANK1")
cols[5].label(text="Port is disconnected")
class BIM_PT_port(Panel):
bl_label = "Port"
@@ -224,56 +184,36 @@ class BIM_PT_port(Panel):
def draw(self, context):
self.props = context.scene.BIMSystemProperties
element = tool.Ifc.get_entity(context.active_object)
port_class = element.is_a()
layout = self.layout
row = layout.row(align=True)
row.label(text="IfcDistributionPort")
row.label(text=port_class)
row.operator("bim.connect_port", icon="PLUGIN", text="")
row.operator("bim.disconnect_port", icon="UNLINKED", text="")
row.operator("bim.remove_port", icon="X", text="")
if not PortData.is_loaded:
PortData.load()
if port_class == "IfcDistributionPort":
current_flow_direction = str(element.FlowDirection)
row = layout.row(align=True)
row.label(text="Flow Direction:")
row.label(text=current_flow_direction)
if not PortData.data["is_port"]:
return
# TODO: replace with enum property?
flow_directions = (
("SOURCE", "FORWARD"),
("SINK", "BACK"),
("SOURCEANDSINK", "ARROW_LEFTRIGHT"),
("NOTDEFINED", "RESTRICT_INSTANCED_ON"),
)
element = tool.Ifc.get_entity(context.active_object)
current_flow_direction = str(element.FlowDirection)
row = layout.row(align=True)
row.label(text="Flow Direction:")
row.label(text=current_flow_direction)
# port located on
row = layout.row(align=True)
relating_object = PortData.data["port_relating_object"]
row.label(text="Port located on:")
row.label(text=relating_object.name)
row.operator(
"bim.select_entity", text="", icon="RESTRICT_SELECT_OFF"
).ifc_id = relating_object.BIMObjectProperties.ifc_definition_id
# object connected to the port
row = layout.row(align=True)
connected_object = PortData.data["port_connected_object"]
if connected_object:
row.label(text="Port connected to:")
row.label(text=connected_object.name)
row.operator(
"bim.select_entity", text="", icon="RESTRICT_SELECT_OFF"
).ifc_id = connected_object.BIMObjectProperties.ifc_definition_id
else:
row.label(text="Port is not connected to any element")
# TODO: replace with enum property?
row = layout.row(align=True)
row.label(text="Change Flow Direction:")
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
row = layout.row()
row.operator(
"bim.set_flow_direction", icon=FLOW_DIRECTION_TO_ICON[flow_direction], text=flow_direction
).direction = flow_direction
if flow_direction == current_flow_direction:
row.enabled = False
row = layout.row(align=True)
row.label(text="Change Flow Direction:")
for flow_direction, icon in flow_directions:
row = layout.row()
row.operator("bim.set_flow_direction", icon=icon, text=flow_direction).direction = flow_direction
if flow_direction == current_flow_direction:
row.enabled = False
class BIM_UL_systems(UIList):
@@ -36,7 +36,7 @@ class BIM_PT_tester(Panel):
if tool.Ifc.get():
row = self.layout.row()
row.prop(props, "should_load_from_memory")
row.prop(self.props, "should_load_from_memory")
row = self.layout.row()
row.prop(props, "generate_html_report")
@@ -282,7 +282,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
axis = "AXIS3"
ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset, properties={"LayerSetDirection": axis})
elif template == "PROFILESET" or template.startswith("FLOW_SEGMENT_"):
elif template == "PROFILESET" or template.startswith("DISTRIBUTION_SEGMENT_"):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
obj = bpy.data.objects.new(name, None)
element = blenderbim.core.root.assign_class(
@@ -301,8 +301,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
material = materials[0] # Arbitrarily pick a material
else:
material = self.add_default_material()
named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName]
if template == "PROFILESET":
named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName]
if named_profiles:
profile = named_profiles[0]
else:
@@ -312,36 +312,32 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
)
else:
# NOTE: defaults dims are in meters / mm
# for now default names are hardcoded to mm
if template == "FLOW_SEGMENT_RECTANGULAR":
default_x_dim = 0.4
default_y_dim = 0.2
profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_y_dim*1000}"
if template == "DISTRIBUTION_SEGMENT_RECTANGULAR":
default_x_dim = 0.4 / unit_scale
default_y_dim = 0.2 / unit_scale
profile_name = f"{ifc_class}-{default_x_dim*1000}x{default_x_dim*1000}"
profile = ifc_file.create_entity(
"IfcRectangleProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
XDim=default_x_dim / unit_scale,
YDim=default_y_dim / unit_scale,
XDim=default_x_dim,
YDim=default_y_dim,
)
elif template == "FLOW_SEGMENT_CIRCULAR":
default_diameter = 0.1
elif template == "DISTRIBUTION_SEGMENT_CIRCULAR":
default_diameter = 0.1 / unit_scale
profile_name = f"{ifc_class}-{default_diameter*1000}"
profile = ifc_file.create_entity(
"IfcCircleProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
Radius=(default_diameter / 2) / unit_scale,
"IfcCircleProfileDef", ProfileName=profile_name, ProfileType="AREA", Radius=default_diameter / 2
)
elif template == "FLOW_SEGMENT_CIRCULAR_HOLLOW":
default_diameter = 0.15
default_thickness = 0.005
elif template == "DISTRIBUTION_SEGMENT_CIRCULAR_HOLLOW":
default_diameter = 0.15 / unit_scale
default_thickness = 0.005 / unit_scale
profile_name = f"{ifc_class}-{default_diameter*1000}x{default_thickness*1000}"
profile = ifc_file.create_entity(
"IfcCircleHollowProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
Radius=(default_diameter / 2) / unit_scale,
Radius=default_diameter / 2,
WallThickness=default_thickness,
)
+1 -1
View File
@@ -247,7 +247,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
row.prop(context.scene.BIMProperties, "pset_dir")
row.prop(context.scene.BIMProperties, "psets_dir")
row = self.layout.row(align=True)
row.prop(context.scene.DocProperties, "sheets_dir")
row = self.layout.row(align=True)
+25 -40
View File
@@ -20,37 +20,34 @@
def load_brick_project(brick, filepath=None, brick_root=None):
brick.load_brick_file(filepath)
brick.import_brick_classes(brick_root)
brick.import_brick_classes(brick_root, split_screen=True)
brick.set_active_brick_class(brick_root)
brick.set_active_brick_class(brick_root, split_screen=True)
def view_brick_class(brick, brick_class=None, split_screen=False):
brick.add_brick_breadcrumb(split_screen=split_screen)
brick.clear_brick_browser(split_screen=split_screen)
brick.import_brick_classes(brick_class, split_screen=split_screen)
brick.import_brick_items(brick_class, split_screen=split_screen)
brick.set_active_brick_class(brick_class, split_screen=split_screen)
def view_brick_class(brick, brick_class=None):
brick.add_brick_breadcrumb()
brick.clear_brick_browser()
brick.import_brick_classes(brick_class)
brick.import_brick_items(brick_class)
brick.set_active_brick_class(brick_class)
def view_brick_item(brick, item=None, split_screen=False):
def view_brick_item(brick, item=None):
brick_class = brick.get_item_class(item)
view_brick_class(brick, brick_class=brick_class, split_screen=split_screen)
brick.select_browser_item(item, split_screen=split_screen)
view_brick_class(brick, brick_class=brick_class)
brick.select_browser_item(item)
def rewind_brick_class(brick, split_screen=False):
previous_class = brick.pop_brick_breadcrumb(split_screen=split_screen)
brick.clear_brick_browser(split_screen=split_screen)
brick.import_brick_classes(previous_class, split_screen=split_screen)
brick.import_brick_items(previous_class, split_screen=split_screen)
brick.set_active_brick_class(previous_class, split_screen=split_screen)
def rewind_brick_class(brick):
previous_class = brick.pop_brick_breadcrumb()
brick.clear_brick_browser()
brick.import_brick_classes(previous_class)
brick.import_brick_items(previous_class)
brick.set_active_brick_class(previous_class)
def close_brick_project(brick):
brick.clear_project()
brick.clear_brick_browser()
brick.clear_brick_browser(split_screen=True)
def convert_brick_project(ifc, brick):
@@ -79,13 +76,11 @@ def add_brick(ifc, brick, element=None, namespace=None, brick_class=None, librar
else:
brick_uri = brick.add_brick(namespace, brick_class, label)
brick.run_refresh_brick_viewer()
brick.run_refresh_brick_viewer(split_screen=True)
def add_brick_relation(brick, brick_uri=None, predicate=None, object=None):
brick.add_relation(brick_uri, predicate, object)
def add_brick_feed(ifc, brick, source=None, destination=None):
brick.add_feed(brick.get_brick(source), brick.get_brick(destination))
brick.run_refresh_brick_viewer()
brick.run_refresh_brick_viewer(split_screen=True)
def convert_ifc_to_brick(brick, namespace=None, library=None):
@@ -100,17 +95,12 @@ def convert_ifc_to_brick(brick, namespace=None, library=None):
def new_brick_file(brick, brick_root=None):
brick.new_brick_file()
brick.import_brick_classes(brick_root)
brick.import_brick_classes(brick_root, split_screen=True)
brick.set_active_brick_class(brick_root)
brick.set_active_brick_class(brick_root, split_screen=True)
def refresh_brick_viewer(brick, split_screen=False):
if split_screen:
brick.run_view_brick_class(brick_class=brick.get_active_brick_class(split_screen=split_screen), split_screen=split_screen)
else:
brick.run_view_brick_class(brick_class=brick.get_active_brick_class(), split_screen=split_screen)
brick.pop_brick_breadcrumb(split_screen=split_screen)
def refresh_brick_viewer(brick):
brick.run_view_brick_class(brick_class=brick.get_active_brick_class())
brick.pop_brick_breadcrumb()
def remove_brick(ifc, brick, library=None, brick_uri=None):
@@ -120,7 +110,6 @@ def remove_brick(ifc, brick, library=None, brick_uri=None):
ifc.run("library.remove_reference", reference=reference)
brick.remove_brick(brick_uri)
brick.run_refresh_brick_viewer()
brick.run_refresh_brick_viewer(split_screen=True)
def serialize_brick(brick):
@@ -131,13 +120,9 @@ def add_namespace(brick, alias=None, uri=None):
brick.add_namespace(alias, uri)
def set_brick_list_root(brick, brick_root=None, split_screen=False):
brick.clear_brick_browser(split_screen=split_screen)
brick.import_brick_classes(brick_root, split_screen=split_screen)
brick.set_active_brick_class(brick_root, split_screen=split_screen)
brick.clear_breadcrumbs(split_screen=split_screen)
def set_brick_list_root(brick, brick_root=None):
brick.clear_brick_browser()
brick.import_brick_classes(brick_root)
brick.set_active_brick_class(brick_root)
brick.clear_breadcrumbs()
def remove_brick_relation(brick, brick_uri=None, predicate=None, object=None):
brick.remove_relation(brick_uri, predicate, object)
brick.run_refresh_brick_viewer()
@@ -115,7 +115,6 @@ def switch_representation(
geometry.change_object_data(obj, data, is_global=is_global)
geometry.record_object_materials(obj)
geometry.remove_triangulation(obj)
if should_reload and existing_data:
geometry.delete_data(existing_data)
+4 -6
View File
@@ -136,15 +136,13 @@ class Blender:
return False
@classmethod
def show_info_message(cls, text, message_type="INFO"):
"""useful for showing error messages outside blender operators
def show_error_message(cls, text):
"""useful for showing error messages outside blender operators"""
Possible `message_type`: `INFO` / `ERROR`"""
def message_ui(self, context):
def error(self, context):
self.layout.label(text=text)
bpy.context.window_manager.popup_menu(message_ui, title=message_type.capitalize(), icon=message_type)
bpy.context.window_manager.popup_menu(error, title="Error", icon="ERROR")
@classmethod
def get_blender_prop_default_value(cls, props, prop_name):
+30 -107
View File
@@ -53,14 +53,9 @@ class Brick(blenderbim.core.tool.Brick):
return str(brick)
@classmethod
def add_brick_breadcrumb(cls, split_screen=False):
props = bpy.context.scene.BIMBrickProperties
if split_screen:
new = props.split_screen_brick_breadcrumbs.add()
new.name = props.split_screen_active_brick_class
else:
new = props.brick_breadcrumbs.add()
new.name = props.active_brick_class
def add_brick_breadcrumb(cls):
new = bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.add()
new.name = bpy.context.scene.BIMBrickProperties.active_brick_class
@classmethod
def add_brick_from_element(cls, element, namespace, brick_class):
@@ -105,44 +100,19 @@ class Brick(blenderbim.core.tool.Brick):
)
@classmethod
def add_relation(cls, brick_uri, predicate, object):
if predicate == "http://www.w3.org/2000/01/rdf-schema#label":
with BrickStore.new_changeset() as cs:
cs.add((URIRef(brick_uri), URIRef(predicate), Literal(object)))
bpy.context.scene.BIMBrickProperties.new_brick_relation_type = BrickStore.relationships[0][0]
bpy.context.scene.BIMBrickProperties.add_relation_failed = False
return
query = BrickStore.graph.query(
"ASK { <{object_uri}> a ?o . }".replace(
"{object_uri}", object
)
)
if query:
with BrickStore.new_changeset() as cs:
cs.add((URIRef(brick_uri), URIRef(predicate), URIRef(object)))
bpy.context.scene.BIMBrickProperties.add_relation_failed = False
else:
bpy.context.scene.BIMBrickProperties.add_relation_failed = True
def add_feed(cls, source, destination):
ns_brick = Namespace("https://brickschema.org/schema/Brick#")
BrickStore.graph.add((URIRef(source), ns_brick["feeds"], URIRef(destination)))
@classmethod
def remove_relation(cls, brick_uri, predicate, object):
with BrickStore.new_changeset() as cs:
for triple in BrickStore.graph.triples((brick_uri, predicate, object)):
cs.remove(triple)
@classmethod
def clear_brick_browser(cls, split_screen=False):
props = bpy.context.scene.BIMBrickProperties
if split_screen:
props.split_screen_bricks.clear()
else:
props.bricks.clear()
def clear_brick_browser(cls):
bpy.context.scene.BIMBrickProperties.bricks.clear()
@classmethod
def clear_project(cls):
BrickStore.purge()
bpy.context.scene.BIMBrickProperties.active_brick_class == ""
bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == ""
bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear()
@classmethod
def export_brick_attributes(cls, brick_uri):
@@ -152,9 +122,7 @@ class Brick(blenderbim.core.tool.Brick):
return {"Identification": brick_uri, "Name": brick_uri.split("#")[-1]}
@classmethod
def get_active_brick_class(cls, split_screen=False):
if split_screen:
return bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class
def get_active_brick_class(cls):
return bpy.context.scene.BIMBrickProperties.active_brick_class
@classmethod
@@ -235,7 +203,7 @@ class Brick(blenderbim.core.tool.Brick):
return uri.split("#")[0] + "#"
@classmethod
def import_brick_classes(cls, brick_class, split_screen=False):
def import_brick_classes(cls, brick_class):
query = BrickStore.graph.query(
"""
PREFIX brick: <https://brickschema.org/schema/Brick#>
@@ -254,12 +222,8 @@ class Brick(blenderbim.core.tool.Brick):
"{brick_class}", brick_class
)
)
if split_screen:
bricks = bpy.context.scene.BIMBrickProperties.split_screen_bricks
else:
bricks = bpy.context.scene.BIMBrickProperties.bricks
for row in query:
new = bricks.add()
new = bpy.context.scene.BIMBrickProperties.bricks.add()
label = row.get("label")
if label:
new.label = label.toPython()
@@ -268,7 +232,7 @@ class Brick(blenderbim.core.tool.Brick):
new.total_items = row.get("total_items").toPython()
@classmethod
def import_brick_items(cls, brick_class, split_screen=False):
def import_brick_items(cls, brick_class):
query = BrickStore.graph.query(
"""
PREFIX brick: <https://brickschema.org/schema/Brick#>
@@ -285,12 +249,8 @@ class Brick(blenderbim.core.tool.Brick):
"{brick_class}", brick_class
)
)
if split_screen:
bricks = bpy.context.scene.BIMBrickProperties.split_screen_bricks
else:
bricks = bpy.context.scene.BIMBrickProperties.bricks
for row in query:
new = bricks.add()
new = bpy.context.scene.BIMBrickProperties.bricks.add()
label = row.get("label")
if label:
new.label = label.toPython()
@@ -310,7 +270,6 @@ class Brick(blenderbim.core.tool.Brick):
BrickStore.path = filepath
BrickStore.load_namespaces()
BrickStore.load_entity_classes()
BrickStore.load_relationships()
@classmethod
def new_brick_file(cls):
@@ -323,19 +282,13 @@ class Brick(blenderbim.core.tool.Brick):
BrickStore.graph.bind("digitaltwin", Namespace("https://example.org/digitaltwin#"))
BrickStore.load_namespaces()
BrickStore.load_entity_classes()
BrickStore.load_relationships()
@classmethod
def pop_brick_breadcrumb(cls, split_screen=False):
props = bpy.context.scene.BIMBrickProperties
if split_screen:
breadcrumbs = props.split_screen_brick_breadcrumbs
else:
breadcrumbs = props.brick_breadcrumbs
crumb = breadcrumbs[-1]
def pop_brick_breadcrumb(cls):
crumb = bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[-1]
name = crumb.name
last_index = len(breadcrumbs) - 1
breadcrumbs.remove(last_index)
last_index = len(bpy.context.scene.BIMBrickProperties.brick_breadcrumbs) - 1
bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.remove(last_index)
return name
@classmethod
@@ -352,29 +305,22 @@ class Brick(blenderbim.core.tool.Brick):
)
@classmethod
def run_refresh_brick_viewer(cls, split_screen=False):
return blenderbim.core.brick.refresh_brick_viewer(tool.Brick, split_screen)
def run_refresh_brick_viewer(cls):
return blenderbim.core.brick.refresh_brick_viewer(tool.Brick)
@classmethod
def run_view_brick_class(cls, brick_class=None, split_screen=False):
return blenderbim.core.brick.view_brick_class(tool.Brick, brick_class=brick_class, split_screen=split_screen)
def run_view_brick_class(cls, brick_class=None):
return blenderbim.core.brick.view_brick_class(tool.Brick, brick_class=brick_class)
@classmethod
def select_browser_item(cls, item, split_screen=False):
def select_browser_item(cls, item):
name = item.split("#")[-1]
props = bpy.context.scene.BIMBrickProperties
if split_screen:
props.split_screen_active_brick_index = props.split_screen_bricks.find(name)
else:
props.active_brick_index = props.bricks.find(name)
props.active_brick_index = props.bricks.find(name)
@classmethod
def set_active_brick_class(cls, brick_class, split_screen=False):
props = bpy.context.scene.BIMBrickProperties
if split_screen:
props.split_screen_active_brick_class = brick_class
else:
props.active_brick_class = brick_class
def set_active_brick_class(cls, brick_class):
bpy.context.scene.BIMBrickProperties.active_brick_class = brick_class
@classmethod
def serialize_brick(cls):
@@ -386,11 +332,8 @@ class Brick(blenderbim.core.tool.Brick):
BrickStore.load_namespaces()
@classmethod
def clear_breadcrumbs(cls, split_screen=False):
if split_screen:
bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs.clear()
else:
bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear()
def clear_breadcrumbs(cls):
bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear()
class BrickStore:
schema = None # this is now a os path
@@ -402,14 +345,8 @@ class BrickStore:
current_changesets = 0
history_size = 64
namespaces = []
root_classes = ["Equipment",
"Electrical_Equipment", "Fire_Safety_Equipment", "HVAC_Equipment", "Lighting_Equipment", "Meter",
"Location",
"System",
"Point",
"Alarm", "Command", "Parameter", "Sensor", "Setpoint", "Status"]
root_classes = ["Equipment", "Location", "System", "Point"]
entity_classes = {}
relationships = []
@staticmethod
def purge():
@@ -418,7 +355,6 @@ class BrickStore:
BrickStore.path = None
BrickStore.namespaces = []
BrickStore.entity_classes = {}
BrickStore.relationships = []
@classmethod
def get_project(cls):
@@ -439,6 +375,7 @@ class BrickStore:
@classmethod
def load_entity_classes(cls):
for root_class in BrickStore.root_classes:
query = BrickStore.graph.query(
"""
@@ -455,20 +392,6 @@ class BrickStore:
for uri in sorted([x[0].toPython() for x in query]):
BrickStore.entity_classes[root_class].append((uri, uri.split("#")[-1], ""))
@classmethod
def load_relationships(cls):
query = BrickStore.graph.query(
"""
PREFIX brick: <https://brickschema.org/schema/Brick#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT DISTINCT ?relation WHERE {
?relation rdfs:subPropertyOf brick:Relationship .
}
"""
)
for uri in sorted([x[0].toPython() for x in query]):
BrickStore.relationships.append((uri, uri.split("#")[-1], ""))
@classmethod
def set_history_size(cls, size):
cls.history_size = size
+2 -48
View File
@@ -91,14 +91,10 @@ class Cad:
tolerance = VTX_PRECISION
if isinstance(x, (list, tuple)):
for y in x:
if (y + tolerance) > value > (y - tolerance):
if value > (y - tolerance) and value < (y + tolerance):
return True
return False
return (x + tolerance) > value > (x - tolerance)
@classmethod
def are_vectors_equal(cls, v1: Vector, v2: Vector):
return cls.is_x((v2 - v1).length, 0)
return value > (x - tolerance) and value < (x + tolerance)
@classmethod
def intersect_edges(cls, edge1, edge2):
@@ -231,48 +227,6 @@ class Cad:
res = [cls.is_point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]]
return len([i for i in res if i])
@classmethod
def get_edge_direction(cls, edge):
return (edge[1] - edge[0]).normalized()
@classmethod
def are_edges_collinear(cls, edge1, edge2):
def is_point_on_line(p, edge):
a1, a2 = edge
# comparing slopes between PA1 and A2A1
# using cross multiplication to avoid division by zero
return cls.is_x((p.y - a1.y) * (a2.x - a1.x), (a2.y - a1.y) * (p.x - a1.x))
edge1_dir = edge1[1] - edge1[0]
edge2_dir = edge2[1] - edge2[0]
if cls.is_x(edge1_dir.cross(edge2_dir).length_squared, 0): # check they are parallel
if is_point_on_line(edge1[0], edge2) or is_point_on_line(edge1[1], edge2):
return True
return False
@classmethod
def closest_points(cls, edge1, edge2):
"""
closest end points between `edge1` and `edge2` assuming `edge1` and `edge2` are collinear.
< returns two points, first one belongs to `edge1` and second to `edge2`
"""
direction = (edge1[1] - edge1[0]).normalized()
# Project points onto the line to get scalar values along the direction
points1_values = [(p, p.dot(direction)) for p in edge1]
points2_values = [(p, p.dot(direction)) for p in edge2]
# Sort the projections for both edges
sorted_points1 = sorted(points1_values, key=lambda el: el[1])
sorted_points2 = sorted(points2_values, key=lambda el: el[1])
# The closest points will be the last point of the first edge and the first point of the second edge
return sorted_points1[-1][0], sorted_points2[0][0]
@classmethod
def find_intersecting_edges(cls, bm, pt, idx1, idx2):
"""
@@ -31,7 +31,6 @@ import blenderbim.bim.import_ifc
from math import radians
from mathutils import Vector
from blenderbim.bim.ifc import IfcStore
from math import radians
class Geometry(blenderbim.core.tool.Geometry):
@@ -119,21 +118,6 @@ class Geometry(blenderbim.core.tool.Geometry):
except:
pass
@classmethod
def dissolve_triangulated_edges(cls, obj):
if obj.data and "ios_edges" in obj.data:
bm = bmesh.new()
bm.from_mesh(obj.data)
edges_to_keep = set(map(frozenset, obj.data["ios_edges"]))
edges_to_dissolve = []
for edge in bm.edges:
if frozenset([vert.index for vert in edge.verts]) not in edges_to_keep:
edges_to_dissolve.append(edge)
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
bm.to_mesh(obj.data)
bm.free()
del obj.data["ios_edges"]
@classmethod
def does_representation_id_exist(cls, representation_id):
try:
@@ -546,19 +530,6 @@ class Geometry(blenderbim.core.tool.Geometry):
def rename_object(cls, obj, name):
obj.name = name
@classmethod
def remove_triangulation(cls, obj):
"""
Convert triangles to quads without bpy.ops.
Note that it uses bmesh based on object mesh.
"""
mesh = obj.data
bm = tool.Blender.get_bmesh_for_mesh(mesh)
# angle values come from defaults for `bpy.ops.mesh.tris_convert_to_quads()``
bmesh.ops.join_triangles(bm, faces=bm.faces[:], angle_face_threshold=radians(40), angle_shape_threshold=radians(40))
bm.normal_update()
tool.Blender.apply_bmesh(mesh, bm, obj)
@classmethod
def replace_object_with_empty(cls, obj):
element = tool.Ifc.get_entity(obj)
+1 -1
View File
@@ -110,7 +110,7 @@ class Loader(blenderbim.core.tool.Loader):
"IfcNormalisedRatioMeasure"
):
diffuse_color_value = surface_style["DiffuseColour"].wrappedValue
diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColour"][:3]] + [1]
diffuse_color = [v * diffuse_color_value for v in surface_style["SurfaceColor"][:3]] + [1]
surface_style["DiffuseColour"] = ("IfcNormalisedRatioMeasure", diffuse_color)
else:
surface_style["DiffuseColour"] = None
-43
View File
@@ -495,17 +495,6 @@ class Model(blenderbim.core.tool.Model):
items.append(item.FirstOperand)
return booleans
@classmethod
def get_flow_segment_axis(cls, obj):
z_values = [v[2] for v in obj.bound_box]
return (obj.matrix_world @ Vector((0, 0, min(z_values))), obj.matrix_world @ Vector((0, 0, max(z_values))))
@classmethod
def get_flow_segment_profile(cls, element):
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if material and material.is_a("IfcMaterialProfileSet") and len(material.MaterialProfiles) == 1:
return material.MaterialProfiles[0].Profile
@classmethod
def get_usage_type(cls, element):
material = ifcopenshell.util.element.get_material(element, should_inherit=False)
@@ -767,35 +756,3 @@ class Model(blenderbim.core.tool.Model):
obj.matrix_world = matrix
return
tool.Ifc.run("geometry.edit_object_placement", product=element, matrix=matrix, is_si=True)
@classmethod
def reload_body_representation(cls, obj_or_objects):
"""Update body representation including all decomposed objects"""
if isinstance(obj_or_objects, collections.abc.Iterable):
objects = set(obj_or_objects)
else:
objects = {obj_or_objects}
# decompose objects
decomposed_objs = objects.copy()
for obj in objects:
for subelement in ifcopenshell.util.element.get_decomposition(tool.Ifc.get_entity(obj)):
subobj = tool.Ifc.get_object(subelement)
if subobj:
decomposed_objs.add(subobj)
# update representation
for obj in decomposed_objs:
if not obj.data:
continue
element = tool.Ifc.get_entity(obj)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
+1 -38
View File
@@ -21,35 +21,9 @@ import ifcopenshell.util.system
import blenderbim.core.tool
import blenderbim.tool as tool
from blenderbim.bim import import_ifc
import re
from mathutils import Matrix
class System(blenderbim.core.tool.System):
@classmethod
def add_ports(cls, obj, add_start_port=True, add_end_port=True):
def add_port(mep_element, matrix):
port = tool.Ifc.run("system.add_port", element=mep_element)
port.FlowDirection = "NOTDEFINED"
port.PredefinedType = tool.System.get_port_predefined_type(mep_element)
tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=matrix, is_si=True)
return port
# make sure obj.dimensions and .matrix_world has valid data
bpy.context.view_layer.update()
# need to make sure .ObjectPlacement is also updated when we're going to add ports
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
mep_element = tool.Ifc.get_entity(obj)
length = obj.dimensions.z
ports = []
if add_start_port:
ports.append(add_port(mep_element, obj.matrix_world @ Matrix()))
if add_end_port:
ports.append(add_port(mep_element, obj.matrix_world @ Matrix.Translation((0, 0, length))))
return ports
@classmethod
def create_empty_at_cursor_with_element_orientation(cls, element):
element_obj = tool.Ifc.get_object(element)
@@ -90,18 +64,6 @@ class System(blenderbim.core.tool.System):
def get_ports(cls, element):
return ifcopenshell.util.system.get_ports(element)
@classmethod
def get_port_relating_element(cls, port):
return port.Nests[0].RelatingObject
@classmethod
def get_port_predefined_type(cls, mep_element):
split_camel_case = lambda x: re.findall("[A-Z][^A-Z]*", x)
class_name = "".join(split_camel_case(mep_element.is_a())[1:-1]).upper()
if class_name == "CONVEYOR":
return "NOTDEFINED"
return class_name
@classmethod
def import_system_attributes(cls, system):
props = bpy.context.scene.BIMSystemProperties
@@ -167,6 +129,7 @@ class System(blenderbim.core.tool.System):
ifc_representation_class=ifc_representation_class,
)
@classmethod
def select_system_products(cls, system):
tool.Spatial.select_products(ifcopenshell.util.system.get_system_elements(system))
+1 -1
View File
@@ -754,7 +754,7 @@ def i_display_the_construction_type_browser():
@when("I add the construction type")
def i_add_the_active_construction_type():
props = bpy.context.scene.BIMModelProperties
bpy.ops.bim.add_constr_type_instance(relating_type_id=int(props.relating_type_id))
bpy.ops.bim.add_constr_type_instance(ifc_class=props.ifc_class, relating_type_id=int(props.relating_type_id))
@then(parsers.parse("construction type is {relating_type_name}"))
@@ -268,10 +268,6 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
weld_offset_ += welds.size();
welds.clear();
// When welding vertices, vertex coords will be shared among faces so we need to per-shape set
// to keep track of which edges were already emitted.
std::set<std::pair<int, int>> emitted_edges;
int surface_style_id = -1;
if (iit->hasStyle()) {
Material adapter(iit->StylePtr());
@@ -323,6 +319,7 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
// Keep track of the number of times an edge is used
// Manifold edges (i.e. edges used twice) are deemed invisible
std::map<std::pair<int, int>, int> edgecount;
std::vector<std::pair<int, int> > edges_temp;
std::vector<gp_XYZ> coords;
BRepGProp_Face prop(face);
@@ -392,20 +389,15 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
_material_ids.push_back(surface_style_id);
_item_ids.push_back(iit->ItemId());
addEdge(dict[n1], dict[n2], edgecount);
addEdge(dict[n2], dict[n3], edgecount);
addEdge(dict[n3], dict[n1], edgecount);
addEdge(dict[n1], dict[n2], edgecount, edges_temp);
addEdge(dict[n2], dict[n3], edgecount, edges_temp);
addEdge(dict[n3], dict[n1], edgecount, edges_temp);
}
for (auto& p : edgecount) {
// @todo should be != 2?
if (p.second == 1 && emitted_edges.find(p.first) == emitted_edges.end()) {
for (std::vector<std::pair<int, int> >::const_iterator jt = edges_temp.begin(); jt != edges_temp.end(); ++jt) {
if (edgecount[*jt] == 1) {
// non manifold edge, face boundary
_edges.push_back(p.first.first);
_edges.push_back(p.first.second);
if (settings().get(IteratorSettings::WELD_VERTICES)) {
// only relevant while welding, because otherwise vertices are not shared among distinct faces
emitted_edges.insert(p.first);
}
_edges.push_back(jt->first);
_edges.push_back(jt->second);
}
}
}
@@ -428,8 +420,6 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
for (int i = 1; i <= n; ++i) {
gp_XYZ p = tessellater.Value(i).XYZ();
auto p_local = p;
trsf.Transforms(p);
int current = addVertex(iit->ItemId(), surface_style_id, p);
@@ -457,10 +447,11 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
}
d3 = d1.XYZ() + d2.XYZ();
d4 = d1.XYZ() - d2.XYZ();
p2 = p_local - d3.XYZ() / 10.;
p3 = p_local - d4.XYZ() / 10.;
p2 = p - d3.XYZ() / 10.;
p3 = p - d4.XYZ() / 10.;
trsf.Transforms(p2);
trsf.Transforms(p3);
trsf.Transforms(p);
int left = addVertex(iit->ItemId(), surface_style_id, p2);
int right = addVertex(iit->ItemId(), surface_style_id, p3);
@@ -535,7 +526,9 @@ int IfcGeom::Representation::Triangulation::addVertex(int item_index, int materi
return i;
}
void IfcGeom::Representation::Triangulation::addEdge(int n1, int n2, std::map<std::pair<int, int>, int>& edgecount) {
void IfcGeom::Representation::Triangulation::addEdge(int n1, int n2, std::map<std::pair<int, int>, int>& edgecount, std::vector<std::pair<int, int>>& edges_temp) {
const Edge e = Edge((std::min)(n1, n2), (std::max)(n1, n2));
edgecount[e] ++;
if (edgecount.find(e) == edgecount.end()) edgecount[e] = 1;
else edgecount[e] ++;
edges_temp.push_back(e);
}
@@ -176,7 +176,7 @@ namespace IfcGeom {
private:
/// Welds vertices that belong to different faces
int addVertex(int item_index, int material_index, const gp_XYZ& p);
void addEdge(int n1, int n2, std::map<std::pair<int, int>, int>& edgecount);
void addEdge(int n1, int n2, std::map<std::pair<int, int>, int>& edgecount, std::vector<std::pair<int, int> >& edges_temp);
Triangulation();
Triangulation(const Triangulation&);
@@ -658,7 +658,7 @@ responsibility to make sure the geometry is correct.
# It's now our responsibility to create a compatible representation.
# Notice how our thickness of 0.118 must equal .013 + .092 + .013 from our type
body = ifcopenshell.util.representation.get_context(model, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body")
representation = ifcopenshell.api.run("geometry.add_wall_representation", model,
context=body, length=5, height=3, thickness=0.118)
@@ -685,14 +685,14 @@ responsibility to make sure the geometry is correct.
beam_type = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcBeamType", name="B1")
# First, let's create a material set. This will later be assigned to our beam type element.
material_set = ifcopenshell.api.run("material.add_material_set", model,
material_set = ifcopenshell.api.run("material.add_profile_set", model,
name="B1", set_type="IfcMaterialProfileSet")
# Create a steel material.
steel = ifcopenshell.api.run("material.add_material", model, name="ST01", category="steel")
# Create an I-beam profile curve. Notice how we use standardised steel profile names.
hea100 = model.create_entity(
hea100 = self.file.create_entity(
"IfcIShapeProfileDef", ProfileName="HEA100", ProfileType="AREA",
OverallWidth=100, OverallDepth=96, WebThickness=5, FlangeThickness=8, FilletRadius=12,
)
@@ -714,7 +714,7 @@ responsibility to make sure the geometry is correct.
# It's now our responsibility to create a compatible representation.
# Notice how we reuse our profile instead of creating a new profile.
body = ifcopenshell.util.representation.get_context(model, "Model", "Body", "MODEL_VIEW")
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body")
representation = run("geometry.add_profile_representation", model, context=body, profile=hea100, depth=1)
# Assign our new body geometry back to our beam
@@ -63,11 +63,7 @@ class Usecase:
def apply_clippings(self, first_operand):
while self.settings["clippings"]:
clipping = self.settings["clippings"].pop()
if isinstance(clipping, ifcopenshell.entity_instance):
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
elif clipping["operand_type"] == "IfcHalfSpaceSolid":
if clipping["operand_type"] == "IfcHalfSpaceSolid":
matrix = clipping["matrix"]
second_operand = self.file.createIfcHalfSpaceSolid(
self.file.createIfcPlane(
@@ -85,7 +81,7 @@ class Usecase:
),
False,
)
first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand)
first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand)
return first_operand
def convert_si_to_unit(self, co):
@@ -95,11 +95,7 @@ class Usecase:
def apply_clippings(self, first_operand):
while self.settings["clippings"]:
clipping = self.settings["clippings"].pop()
if isinstance(clipping, ifcopenshell.entity_instance):
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
elif clipping["operand_type"] == "IfcHalfSpaceSolid":
if clipping["operand_type"] == "IfcHalfSpaceSolid":
matrix = clipping["matrix"]
second_operand = self.file.createIfcHalfSpaceSolid(
self.file.createIfcPlane(
@@ -117,7 +113,7 @@ class Usecase:
),
False,
)
first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand)
first_operand = self.file.create_entity(clipping["type"], "DIFFERENCE", first_operand, second_operand)
return first_operand
def convert_si_to_unit(self, co):
@@ -22,7 +22,7 @@ import ifcopenshell.util.unit
class Usecase:
def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None, is_si=True):
def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None):
self.file = file
self.settings = {
"element": element,
@@ -32,25 +32,15 @@ class Usecase:
"elevation": elevation,
"height": height,
"thickness": thickness,
"is_si": is_si
}
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["p1"] = np.array(self.settings["p1"]).astype(float)
self.settings["p2"] = np.array(self.settings["p2"]).astype(float)
self.settings["p1"] = np.array(self.settings["p1"])
self.settings["p2"] = np.array(self.settings["p2"])
length = float(np.linalg.norm(self.settings["p2"] - self.settings["p1"]))
if not self.settings["is_si"]:
length=self.convert_unit_to_si(length)
self.settings["height"]=self.convert_unit_to_si(self.settings["height"])
self.settings["thickness"]=self.convert_unit_to_si(self.settings["thickness"])
self.settings["p1"][0] = self.convert_unit_to_si(self.settings["p1"][0])
self.settings["p1"][1] = self.convert_unit_to_si(self.settings["p1"][1])
self.settings["elevation"] = self.convert_unit_to_si(self.settings["elevation"])
representation = ifcopenshell.api.run(
"geometry.add_wall_representation",
self.file,
@@ -65,7 +55,7 @@ class Usecase:
[
[v[0], -v[1], 0, self.settings["p1"][0]],
[v[1], v[0], 0, self.settings["p1"][1]],
[0, 0, 1, self.settings["elevation"]],
[0, 0, 1, self.convert_si_to_unit(self.settings["elevation"])],
[0, 0, 0, 1],
]
)
@@ -74,5 +64,7 @@ class Usecase:
)
return representation
def convert_unit_to_si(self, co):
return co * self.settings["unit_scale"]
def convert_si_to_unit(self, co):
if isinstance(co, (tuple, list)):
return [self.convert_si_to_unit(o) for o in co]
return co / self.settings["unit_scale"]
@@ -172,7 +172,6 @@ class Usecase:
should_run_listeners=False,
related_object=element,
relating_type=new_type,
should_map_representations=False,
)
ifcopenshell.api.owner.settings.restore()
@@ -65,29 +65,8 @@ class Usecase:
representations = self.settings["product"].Representation.Representations or []
else:
representations = []
# remove object placements
object_placement = self.settings["product"].ObjectPlacement
if object_placement:
if self.file.get_total_inverses(object_placement) == 1:
self.settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work
ifcopenshell.util.element.remove_deep2(self.file, object_placement)
elif self.settings["product"].is_a("IfcTypeProduct"):
representations = [rm.MappedRepresentation for rm in self.settings["product"].RepresentationMaps or []]
# remove psets
psets = self.settings["product"].HasPropertySets or []
for pset in psets:
if self.file.get_total_inverses(pset) != 1:
continue
ifcopenshell.api.run(
"pset.remove_pset",
self.file,
product=self.settings["product"],
pset=pset,
)
for representation in representations:
ifcopenshell.api.run(
"geometry.unassign_representation",
@@ -153,12 +132,6 @@ class Usecase:
):
continue
self.file.remove(inverse)
elif inverse.is_a("IfcRelConnectsPorts"):
if self.settings["product"] not in (inverse.RelatingPort, inverse.RelatedPort):
# if it's not RelatingPort/RelatedPort then it's optional RealizingElement
# so we keep the relationship
continue
self.file.remove(inverse)
elif inverse.is_a("IfcRelAssignsToGroup"):
if len(inverse.RelatedObjects) == 1:
self.file.remove(inverse)
@@ -22,7 +22,7 @@ import ifcopenshell.util.element
class Usecase:
def __init__(self, file, related_object=None, relating_type=None, should_map_representations=True):
def __init__(self, file, related_object=None, relating_type=None):
"""Assigns a type to an occurrence of an object
IFC supports the concept of occurrences and types. An occurrence is an
@@ -87,11 +87,6 @@ class Usecase:
:type related_object: ifcopenshell.entity_instance.entity_instance
:param relating_type: The IfcElementType type.
:type relating_type: ifcopenshell.entity_instance.entity_instance
:param should_map_representations: If a type has a representation map,
IFC requires all occurrences to map those representations. Some IFC
vendors might disobey this, or you might want to handle it
yourself. In this scenario, you may set this to False.
:type should_map_representations: bool
:return: The IfcRelDefinesByType relationship
:rtype: ifcopenshell.entity_instance.entity_instance
@@ -169,7 +164,6 @@ class Usecase:
self.settings = {
"related_object": related_object,
"relating_type": relating_type,
"should_map_representations": should_map_representations,
}
def execute(self):
@@ -213,15 +207,14 @@ class Usecase:
}
)
if self.settings["should_map_representations"]:
if getattr(self.settings["relating_type"], "RepresentationMaps", None):
ifcopenshell.api.run(
"type.map_type_representations",
self.file,
related_object=self.settings["related_object"],
relating_type=self.settings["relating_type"],
)
self.map_material_usages()
if getattr(self.settings["relating_type"], "RepresentationMaps", None):
ifcopenshell.api.run(
"type.map_type_representations",
self.file,
related_object=self.settings["related_object"],
relating_type=self.settings["relating_type"],
)
self.map_material_usages()
return types
def map_material_usages(self):
@@ -54,27 +54,6 @@ def xyz2enh(x, y, z, eastings, northings, orthogonal_height, x_axis_abscissa, x_
return (eastings, northings, height)
def xyz2enh_ifc4x3(
x,
y,
z,
eastings,
northings,
orthogonal_height,
x_axis_abscissa,
x_axis_ordinate,
scale=1.0,
factor_x=1.0,
factor_y=1.0,
factor_z=1.0,
):
theta = math.atan2(x_axis_ordinate, x_axis_abscissa)
eastings = (scale * factor_x * math.cos(theta) * x) - (scale * factor_y * math.sin(theta) * y) + eastings
northings = (scale * factor_x * math.sin(theta) * x) + (scale * factor_y * math.cos(theta) * y) + northings
height = (scale * factor_z * z) + orthogonal_height
return (eastings, northings, height)
def auto_z2e(ifc_file, z):
"""Convert a Z coordinate to an elevation using model georeferencing data
@@ -99,9 +78,6 @@ def auto_z2e(ifc_file, z):
h = conversion.OrthogonalHeight
map_unit = conversion.TargetCRS.MapUnit
if map_unit:
# Warning! This definition has changed in IFC4X3 such that map_unit no
# longer affects unit conversion, only the Scale attribute affects unit
# conversion. TODO: consolidate once IFC4X3 confirmed.
project_unit = ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT")
h = ifcopenshell.util.unit.convert(
h,
@@ -152,46 +128,6 @@ def local2global(matrix, eastings, northings, orthogonal_height, x_axis_abscissa
return intermediate
def local2global_ifc4x3(
matrix,
eastings,
northings,
orthogonal_height,
x_axis_abscissa,
x_axis_ordinate,
scale=1.0,
factor_x=1.0,
factor_y=1.0,
factor_z=1.0,
):
# Matrix is a 4x4 matrix typically describing the object placement of an element.
theta = math.atan2(x_axis_ordinate, x_axis_abscissa)
scale_and_factor_matrix = np.array(
[
[scale * factor_x, 0, 0, 0],
[0, scale * factor_y, 0, 0],
[0, 0, scale * factor_z, 0],
[0, 0, 0, 1],
]
)
rotation_matrix = np.array(
[
[math.cos(theta), -math.sin(theta), 0, 0],
[math.sin(theta), math.cos(theta), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
]
)
result = rotation_matrix @ scale_and_factor_matrix @ matrix
result[:, 0][0:3] /= np.linalg.norm(result[:, 0][0:3])
result[:, 1][0:3] /= np.linalg.norm(result[:, 1][0:3])
result[:, 2][0:3] /= np.linalg.norm(result[:, 2][0:3])
result[0][3] += eastings
result[1][3] += northings
result[2][3] += orthogonal_height
return result
def global2local(matrix, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None):
if scale is None:
scale = 1.0
@@ -257,13 +193,12 @@ def get_true_north(ifc_file):
def angle2xaxis(angle):
angle_rad = math.radians(angle)
x = math.cos(angle_rad)
y = -math.sin(angle_rad)
y = - math.sin(angle_rad)
return x, y
# Used for converting True North angle as seen in CAD (relative to +Y)
def angle2yaxis(angle):
angle_rad = math.radians(angle)
x = -math.sin(angle_rad)
x = - math.sin(angle_rad)
y = math.cos(angle_rad)
return x, y
@@ -19,7 +19,7 @@
import collections
import ifcopenshell
import ifcopenshell.api
from math import cos, sin, pi, tan, radians
from math import cos, sin, pi
from mathutils import Vector, Matrix
from itertools import chain
@@ -539,7 +539,7 @@ class ShapeBuilder:
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcPositiveLengthMeasure.htm#8.11.2.71.3-Formal-representation"
)
if not profile_or_curve.is_a("IfcProfileDef"):
if profile_or_curve.is_a() not in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"):
profile_or_curve = self.profile(profile_or_curve)
if position_y_axis:
@@ -579,8 +579,6 @@ class ShapeBuilder:
representation_type = "AdvancedSweptSolid"
elif "IfcExtrudedAreaSolid" in item_types:
representation_type = "SweptSolid"
elif items[0].is_a("IfcTessellatedItem"):
representation_type = "Tessellation"
elif items[0].is_a("IfcCurve") and items[0].Dim == 3:
representation_type = "Curve3D"
else:
@@ -748,10 +746,8 @@ class ShapeBuilder:
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return (points, segments, ifc_curve)
def create_z_profile_lips_curve(
self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius
):
def create_z_profile_lips_curve(self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius):
x1 = FirstFlangeWidth
x2 = SecondFlangeWidth
y = Depth / 2
@@ -774,21 +770,20 @@ class ShapeBuilder:
(-x1+t, -y+t),
(-t/2, -y+t)
)
# fmt: on
# option for no additional thickness in outer radius:
# points, segments, ifc_curve = create_curve_from_coords(
# coords, fillets = (0, 1, 4, 5, 6, 7, 10, 11), fillet_radius=r, closed=True, ifc_file=ifc_file
# )
points, segments, ifc_curve = self.get_simple_2dcurve_data(
coords,
points, segments, ifc_curve = self.get_simple_2dcurve_data(coords,
fillets = (0, 1, 4, 5, 6, 7, 10, 11),
fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r),
closed=True, create_ifc_curve=True)
# fmt: on
return ifc_curve
def create_transition_arc_ifc(self, width, height, create_ifc_curve=False):
# create an arc in the rectangle with specified width and height
# if it's not possible to make a complete arc
@@ -819,114 +814,4 @@ class ShapeBuilder:
points, segments, transition_arc = self.get_simple_2dcurve_data(
curve_coords, fillets, fillet_radius, closed=False, create_ifc_curve=create_ifc_curve
)
return points, segments, transition_arc
def polygonal_face_set(self, points, faces):
"""
> `points` - list of points
> `faces` - list of faces consisted of point indices (points indices starting from 0)
< IfcPolygonalFaceSet
"""
ifc_points = self.file.createIfcCartesianPointList3D(points)
ifc_faces = []
for face in faces:
face = [i + 1 for i in face]
ifc_faces.append(self.file.createIfcIndexedPolygonalFace(face))
face_set = self.file.createIfcPolygonalFaceSet(Coordinates=ifc_points, Faces=ifc_faces)
return face_set
def mep_transition_shape(self, start_segment, end_segment, start_length, end_length, angle=30.0):
"""
returns tuple of Model/Body/MODEL_VIEW IfcRepresentation and transition shape data
"""
# good default values from angle = 30/60 deg
# 30 degree angle will result in 75 degrees on the transition (= 90 - α/2) - https://i.imgur.com/tcoYDWu.png
# TODO: get rid of reliance on profiles
def get_profile(element):
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
if material and material.is_a("IfcMaterialProfileSet") and len(material.MaterialProfiles) == 1:
return material.MaterialProfiles[0].Profile
start_profile = get_profile(start_segment)
end_profile = get_profile(end_segment)
# TODO: support more profiles
if not start_profile.is_a("IfcRectangleProfileDef") or not end_profile.is_a("IfcRectangleProfileDef"):
# Non rectangular profiles are not yet supported
return None, None
start_half_dim = V(start_profile.XDim / 2, start_profile.YDim / 2, start_length)
end_half_dim = V(end_profile.XDim / 2, end_profile.YDim / 2, end_length)
transition_items = []
end_extrusion_offset = V(0, 0, start_length)
def get_transition_legth(start_half_dim, end_half_dim, angle):
diff = start_half_dim.xy - end_half_dim.xy
diff = Vector([abs(i) for i in diff])
c = diff.x * tan(radians(90 - angle / 2))
a = diff.y
b = (c**2 - a**2) ** 0.5
return b
transition_length = get_transition_legth(start_half_dim, end_half_dim, angle)
faces = []
if transition_length != 0:
end_extrusion_offset.z += transition_length
faces += [(3, 4, 7, 0), (11, 8, 15, 12), (3, 11, 12, 4), (7, 15, 8, 0)]
# NOTE: clockwise order for correct face orientation
faces += [
# start extrusion
(0, 1, 2, 3),
(8, 11, 10, 9),
(0, 8, 9, 1),
(1, 9, 10, 2),
(2, 10, 11, 3),
# end extrusion
(4, 5, 6, 7),
(12, 15, 14, 13),
(4, 12, 13, 5),
(5, 13, 14, 6),
(6, 14, 15, 7),
]
points = [
start_half_dim * V(-1, -1, 1),
start_half_dim * V(-1, -1, 0),
start_half_dim * V(1, -1, 0),
start_half_dim * V(1, -1, 1),
end_half_dim * V(1, -1, 0) + end_extrusion_offset,
end_half_dim * V(1, -1, 1) + end_extrusion_offset,
end_half_dim * V(-1, -1, 1) + end_extrusion_offset,
end_half_dim * V(-1, -1, 0) + end_extrusion_offset,
start_half_dim * V(-1, 1, 1),
start_half_dim * V(-1, 1, 0),
start_half_dim * V(1, 1, 0),
start_half_dim * V(1, 1, 1),
end_half_dim * V(1, 1, 0) + end_extrusion_offset,
end_half_dim * V(1, 1, 1) + end_extrusion_offset,
end_half_dim * V(-1, 1, 1) + end_extrusion_offset,
end_half_dim * V(-1, 1, 0) + end_extrusion_offset,
]
face_set = self.polygonal_face_set(points, faces)
transition_items.append(face_set)
body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW")
representation = self.get_representation(body, transition_items, "Tesselation")
transition_data = {
"start_length": start_length,
"end_length": end_length,
"angle": angle,
"transition_length": transition_length,
"full_transition_length": start_length + transition_length + end_length,
}
return representation, transition_data
return points, segments, transition_arc
@@ -26,52 +26,6 @@ class TestRemoveProduct(test.bootstrap.IFC4):
ifcopenshell.api.run("root.remove_product", self.file, product=element)
assert len(self.file.by_type("IfcWall")) == 0
def test_removing_an_element_local_placement(self):
# just removing the product with the placement
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
placement = ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=element)
ifcopenshell.api.run("root.remove_product", self.file, product=element)
assert len(self.file.by_type("IfcObjectPlacement")) == 0
# removing the product that shares the placement with other product
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
placement = ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=element)
element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element1.ObjectPlacement = placement
ifcopenshell.api.run("root.remove_product", self.file, product=element)
assert len(self.file.by_type("IfcObjectPlacement")) == 1
ifcopenshell.api.run("root.remove_product", self.file, product=element1)
assert len(self.file.by_type("IfcObjectPlacement")) == 0
# removing the product that's placement used as a reference point for another placement
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
placement = ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=element)
element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
placement1 = ifcopenshell.api.run("geometry.edit_object_placement", self.file, product=element1)
placement.PlacementRelTo = placement1
ifcopenshell.api.run("root.remove_product", self.file, product=element)
assert len(self.file.by_type("IfcObjectPlacement")) == 1
ifcopenshell.api.run("root.remove_product", self.file, product=element1)
assert len(self.file.by_type("IfcObjectPlacement")) == 0
def test_removing_element_type_psets(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType")
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Foo": "Bar"})
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType")
element2.HasPropertySets = (pset,)
# make sure it won't remove the pset if it's connected elsewhere
ifcopenshell.api.run("root.remove_product", self.file, product=element2)
assert len(self.file.by_type("IfcPropertySet")) == 1
assert len(self.file.by_type("IfcPropertySingleValue")) == 1
# if it's the product is the only inverse for pset, it should remove the pset
ifcopenshell.api.run("root.remove_product", self.file, product=element)
assert len(self.file.by_type("IfcPropertySet")) == 0
assert len(self.file.by_type("IfcPropertySingleValue")) == 0
def test_removing_all_representations_of_an_element(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
ifcopenshell.api.run("unit.assign_unit", self.file)
@@ -312,29 +266,6 @@ class TestRemoveProduct(test.bootstrap.IFC4):
assert len(self.file.by_type("IfcSlab")) == 1
assert len(self.file.by_type("IfcWall")) == 1
def test_removing_ports_connection_relationship(self):
port1 = ifcopenshell.api.run("system.add_port", self.file)
element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment")
ifcopenshell.api.run("system.assign_port", self.file, element=element1, port=port1)
port2 = ifcopenshell.api.run("system.add_port", self.file)
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment")
ifcopenshell.api.run("system.assign_port", self.file, element=element2, port=port2)
ifcopenshell.api.run("system.connect_port", self.file, port1=port1, port2=port2, direction="SOURCE")
connection = self.file.by_type("IfcRelConnectsPorts")[0]
# making sure removing realizing element won't remove the entire connection since it's optional
element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment")
connection.RealizingElement = element3
ifcopenshell.api.run("root.remove_product", self.file, product=element3)
assert len(self.file.by_type("IfcRelConnectsPorts")) == 1
ifcopenshell.api.run("root.remove_product", self.file, product=element1)
assert len(self.file.by_type("IfcRelConnectsPorts")) == 0
assert len(self.file.by_type("IfcFlowSegment")) == 1
assert len(self.file.by_type("IfcDistributionPort")) == 1
def test_removing_all_property_relationships_of_an_element(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar")
@@ -1,102 +0,0 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import pytest
import numpy as np
import test.bootstrap
import ifcopenshell.util.geolocation as subject
class TestXYZ2ENH(test.bootstrap.IFC4):
def test_converting_from_a_local_xyz_point_to_a_global_easting_northing_height(self):
assert subject.xyz2enh(0, 0, 0, 0, 0, 0, 1, 0) == (0, 0, 0)
assert subject.xyz2enh(0, 0, 0, 1, 2, 3, 1, 0) == (1, 2, 3)
assert subject.xyz2enh(0, 0, 0, 1, 2, 3, 0, 1) == (1, 2, 3)
assert np.allclose(subject.xyz2enh(1, 1, 0, 1, 2, 3, 1, 0), (2, 3, 3))
assert np.allclose(subject.xyz2enh(1, 1, 0, 1, 2, 3, 1, 0, 2), (3, 4, 3))
assert np.allclose(subject.xyz2enh(1, 1, 0, 1, 2, 3, 0, 1), (0, 3, 3))
class TestXYZ2ENHIfc4X3(test.bootstrap.IFC4):
def test_converting_from_a_local_xyz_point_to_a_global_easting_northing_height(self):
assert subject.xyz2enh_ifc4x3(0, 0, 0, 0, 0, 0, 1, 0) == (0, 0, 0)
assert subject.xyz2enh_ifc4x3(0, 0, 0, 1, 2, 3, 1, 0) == (1, 2, 3)
assert subject.xyz2enh_ifc4x3(0, 0, 0, 1, 2, 3, 0, 1) == (1, 2, 3)
assert np.allclose(subject.xyz2enh_ifc4x3(1, 1, 0, 1, 2, 3, 1, 0), (2, 3, 3))
assert np.allclose(subject.xyz2enh_ifc4x3(1, 1, 0, 1, 2, 3, 1, 0, 2), (3, 4, 3))
assert np.allclose(subject.xyz2enh_ifc4x3(1, 1, 1, 1, 2, 3, 1, 0, 2, 2, 3, 4), (5, 8, 11))
assert np.allclose(subject.xyz2enh_ifc4x3(1, 1, 0, 1, 2, 3, 0, 1), (0, 3, 3))
class TestLocal2Global(test.bootstrap.IFC4):
def test_converting_from_a_local_matrix_to_a_global_matrix(self):
m = np.eye(4)
m2 = np.eye(4)
assert np.allclose(subject.local2global(m, 0, 0, 0, 1.0, 0.0), m2)
m2[:, 3][0:3] = [1, 2, 3]
assert np.allclose(subject.local2global(m, 1, 2, 3, 1.0, 0.0), m2)
m2[:, 0][0:3] = [0, 1, 0]
m2[:, 1][0:3] = [-1, 0, 0]
assert np.allclose(subject.local2global(m, 1, 2, 3, 0.0, 1.0), m2)
m[:, 3][0:3] = [1, 1, 0]
m2 = np.eye(4)
m2[:, 3][0:3] = [2, 3, 3]
assert np.allclose(subject.local2global(m, 1, 2, 3, 1.0, 0.0), m2)
m2[:, 3][0:3] = [3, 4, 3]
assert np.allclose(subject.local2global(m, 1, 2, 3, 1.0, 0.0, 2), m2)
m2[:, 0][0:3] = [0, 1, 0]
m2[:, 1][0:3] = [-1, 0, 0]
m2[:, 3][0:3] = [0, 3, 3]
assert np.allclose(subject.local2global(m, 1, 2, 3, 0.0, 1.0), m2)
class TestLocal2GlobalIfc4X3(test.bootstrap.IFC4):
def test_converting_from_a_local_matrix_to_a_global_matrix(self):
m = np.eye(4)
m2 = np.eye(4)
assert np.allclose(subject.local2global_ifc4x3(m, 0, 0, 0, 1.0, 0.0), m2)
m2[:, 3][0:3] = [1, 2, 3]
assert np.allclose(subject.local2global_ifc4x3(m, 1, 2, 3, 1.0, 0.0), m2)
m2[:, 0][0:3] = [0, 1, 0]
m2[:, 1][0:3] = [-1, 0, 0]
assert np.allclose(subject.local2global_ifc4x3(m, 1, 2, 3, 0.0, 1.0), m2)
m[:, 3][0:3] = [1, 1, 0]
m2 = np.eye(4)
m2[:, 3][0:3] = [2, 3, 3]
assert np.allclose(subject.local2global_ifc4x3(m, 1, 2, 3, 1.0, 0.0), m2)
m2[:, 3][0:3] = [3, 4, 3]
assert np.allclose(subject.local2global_ifc4x3(m, 1, 2, 3, 1.0, 0.0, 2), m2)
m2[:, 0][0:3] = [0, 1, 0]
m2[:, 1][0:3] = [-1, 0, 0]
m2[:, 3][0:3] = [0, 3, 3]
assert np.allclose(subject.local2global_ifc4x3(m, 1, 2, 3, 0.0, 1.0), m2)
m[:, 3][0:3] = [1, 1, 1]
m2 = np.eye(4)
m2[:, 3][0:3] = [5, 8, 11]
assert np.allclose(subject.local2global_ifc4x3(m, 1, 2, 3, 1.0, 0.0, 2, 2, 3, 4), m2)
@@ -26,9 +26,8 @@ class TestGetApplicableTypes(test.bootstrap.IFC4):
def test_run(self):
assert subject.get_applicable_types("IfcWall") == ["IfcWallType"]
assert subject.get_applicable_types("IfcWallStandardCase") == ["IfcWallType"]
assert subject.get_applicable_types("IfcFlowSegment") == ["IfcDistributionElementType"]
assert subject.get_applicable_types("IfcFlowSegment") == []
assert subject.get_applicable_types("IfcDuctSegment") == ["IfcDuctSegmentType"]
assert subject.get_applicable_types("IfcTask") == []
class TestGetApplicableTypesIFC2X3(test.bootstrap.IFC2X3):
+2 -16
View File
@@ -21,8 +21,6 @@ import sys
import math
import logging
import datetime
import ifcopenshell
import ifcopenshell.util.element
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -165,7 +163,6 @@ class Json(Reporter):
return self.results
def report_specification(self, specification):
applicability = [a.to_string("applicability") for a in specification.applicability]
requirements = []
for requirement in specification.requirements:
requirements.append(
@@ -185,23 +182,12 @@ class Json(Reporter):
"total": total,
"percentage": percentage,
"required": specification.minOccurs != 0,
"applicability": applicability,
"requirements": requirements,
}
def report_failed_entities(self, requirement):
return [
{
"reason": requirement.failed_reasons[i],
"element": str(e),
"class": e.is_a(),
"predefined_type": ifcopenshell.util.element.get_predefined_type(e),
"name": getattr(e, "Name", None),
"description": getattr(e, "Description", None),
"id": e.id(),
"global_id": getattr(e, "GlobalId", None),
"tag": getattr(e, "Tag", None),
}
{"reason": requirement.failed_reasons[i], "element": str(e)}
for i, e in enumerate(requirement.failed_entities)
]
@@ -356,7 +342,7 @@ class Bcf(Json):
continue
for failure in requirement["failed_entities"]:
element = failure["element"]
title = f"ID:[{element.id()}]/GUID:[{element.GlobalId}]/{element.is_a()}/"
title = f"{element.id()}/{element.is_a()}/"
title += getattr(element, "Name", None) or "Unnamed"
title += " - " + failure.get("reason", "No reason")
description = f'{specification["name"]} - {requirement["description"]}'