fix conflicts merging with v0.7.0

This commit is contained in:
Bruno Perdigão
2023-11-14 11:58:35 -03:00
349 changed files with 137139 additions and 5697 deletions
+1 -1
View File
@@ -169,7 +169,7 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9cc1f5f-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-6c9e130-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
+18 -8
View File
@@ -125,26 +125,33 @@ classes = [
ui.BIM_UL_generic,
ui.BIM_UL_topics,
ui.BIM_ADDON_preferences,
# Project overview
ui.BIM_PT_project_info,
ui.BIM_PT_project_setup,
ui.BIM_PT_geometry,
ui.BIM_PT_tab_grouping_and_filtering,
# Tabs panel
ui.BIM_PT_tabs,
# Project overview
ui.BIM_PT_tab_project_info,
ui.BIM_PT_tab_project_setup,
ui.BIM_PT_tab_geometry,
ui.BIM_PT_tab_stakeholders,
ui.BIM_PT_tab_grouping_and_filtering,
# Object information
ui.BIM_PT_tab_object_metadata,
ui.BIM_PT_tab_misc,
# Geometry and materials
ui.BIM_PT_tab_placement,
ui.BIM_PT_tab_representations,
ui.BIM_PT_tab_geometric_relationships,
ui.BIM_PT_tab_parametric_geometry,
ui.BIM_PT_tab_profiles,
ui.BIM_PT_tab_materials,
ui.BIM_PT_tab_styles,
# Drawings and documents
ui.BIM_PT_tab_sheets,
ui.BIM_PT_tab_drawings,
ui.BIM_PT_tab_schedules,
ui.BIM_PT_tab_references,
# Services and systems
ui.BIM_PT_tab_services,
ui.BIM_PT_tab_services_object,
ui.BIM_PT_tab_zones,
# Structural analysis
ui.BIM_PT_tab_structural,
# Construction scheduling
@@ -212,8 +219,8 @@ def register():
wm = bpy.context.window_manager
if wm.keyconfigs.addon:
km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY')
kmi = km.keymap_items.new('bim.switch_tab', 'TAB', 'PRESS', ctrl=True)
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
kmi = km.keymap_items.new("bim.switch_tab", "TAB", "PRESS", ctrl=True)
addon_keymaps.append((km, kmi))
global icons
@@ -231,12 +238,14 @@ def register():
global last_commit_hash
try:
import git
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
except:
pass
def unregister():
global icons
@@ -280,6 +289,7 @@ def unregister():
"SCENE_PT_rigid_body_world",
"SCENE_PT_audio",
"SCENE_PT_keying_sets",
"SCENE_PT_simulation",
"SCENE_PT_custom_props",
]:
try:
+12
View File
@@ -319,6 +319,17 @@ if getattr(bpy.types, "SCENE_PT_custom_props"):
return tool.Blender.is_tab(context, "BLENDER")
# available on Blender 4.0+
if getattr(bpy.types, "SCENE_PT_simulation", None):
class Override_SCENE_PT_simulation(bpy.types.SCENE_PT_simulation):
bl_idname = "SCENE_PT_simulation_override"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "BLENDER")
@persistent
def load_post(scene):
global global_subscription_owner
@@ -361,6 +372,7 @@ def load_post(scene):
"SCENE_PT_rigid_body_world",
"SCENE_PT_audio",
"SCENE_PT_keying_sets",
"SCENE_PT_simulation",
"SCENE_PT_custom_props",
]:
if getattr(bpy.types, panel, None):
+1 -6
View File
@@ -260,12 +260,7 @@ def convert_property_group_from_si(property_group, skip_props=()):
for prop_name in property_group.bl_rna.properties.keys():
if prop_name in skip_props:
continue
prop_bl_rna = property_group.bl_rna.properties[prop_name]
if prop_bl_rna.array_length > 0:
prop_value = prop_bl_rna.default_array
else:
prop_value = prop_bl_rna.default
prop_value = tool.Blender.get_blender_prop_default_value(property_group, prop_name)
if type(prop_value) is float:
prop_value = prop_value * conversion_k
elif type(prop_value) is bpy.types.bpy_prop_array:
+255 -251
View File
@@ -96,7 +96,9 @@ class MaterialCreator:
has_parsed = True
elif hasattr(element, "RepresentationMaps"):
for representation_map in element.RepresentationMaps:
if self.parse_representation(representation_map.MappedRepresentation):
if not representation_map.MappedRepresentation:
has_parsed = True # Accommodate invalid IFC data from Revit
elif self.parse_representation(representation_map.MappedRepresentation):
has_parsed = True
return has_parsed
@@ -214,20 +216,8 @@ class IfcImporter:
self.ifc_import_settings = ifc_import_settings
self.diff = None
self.file = None
self.settings = ifcopenshell.geom.settings()
self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
self.settings.set(self.settings.STRICT_TOLERANCE, True)
self.settings_body_2d = ifcopenshell.geom.settings()
self.settings_body_2d.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
self.settings_body_2d.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
self.settings_body_2d.set(self.settings_body_2d.STRICT_TOLERANCE, True)
self.settings_body_2d.set(self.settings_body_2d.INCLUDE_CURVES, True)
self.settings_native = ifcopenshell.geom.settings()
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
self.settings_plan_2d = ifcopenshell.geom.settings()
self.settings_plan_2d.set(self.settings_plan_2d.INCLUDE_CURVES, True)
self.settings_plan_2d.set(self.settings_plan_2d.STRICT_TOLERANCE, True)
self.context_settings = []
self.contexts = []
self.project = None
self.has_existing_project = False
self.collections = {}
@@ -311,7 +301,8 @@ class IfcImporter:
self.profile_code("Merging by colour")
self.set_default_context()
self.profile_code("Setting default context")
self.setup_viewport_camera()
if self.ifc_import_settings.should_setup_viewport_camera:
self.setup_viewport_camera()
self.setup_arrays()
self.update_progress(100)
bpy.context.window_manager.progress_end()
@@ -335,34 +326,83 @@ class IfcImporter:
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
def process_context_filter(self):
# Facetation is to accommodate broken Revit files
# See https://forums.buildingsmart.org/t/suggestions-on-how-to-improve-clarity-of-representation-context-usage-in-documentation/3663/6?u=moult
self.body_contexts = [
c.id()
for c in self.file.by_type("IfcGeometricRepresentationSubContext")
if c.ContextIdentifier in ["Body", "Facetation"]
]
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
if not self.body_contexts:
self.body_contexts.extend(
[
c.id()
for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
if c.ContextType == "Model"
]
)
if self.body_contexts:
self.settings.set_context_ids(self.body_contexts)
self.settings_body_2d.set_context_ids(self.body_contexts)
# Annotation ContextType is to accommodate broken Revit files
# See https://github.com/Autodesk/revit-ifc/issues/187
self.plan_contexts = [
c.id()
for c in self.file.by_type("IfcGeometricRepresentationContext")
if c.ContextType in ["Plan", "Annotation"] or c.ContextIdentifier == "Annotation"
type_priority = ["Model", "Plan", "Annotation"]
identifier_priority = [
"Body",
"Body-FallBack",
"Facetation",
"FootPrint",
"Profile",
"Surface",
"Reference",
"Axis",
"Clearance",
"Box",
"Lighting",
"Annotation",
"CoG",
]
if self.plan_contexts:
self.settings_plan_2d.set_context_ids(self.plan_contexts)
target_view_priority = [
"MODEL_VIEW",
"PLAN_VIEW",
"REFLECTED_PLAN_VIEW",
"ELEVATION_VIEW",
"SECTION_VIEW",
"GRAPH_VIEW",
"SKETCH_VIEW",
"USERDEFINED",
"NOTDEFINED",
]
def sort_context(context):
priority = []
if context.ContextType in type_priority:
priority.append(len(type_priority) - type_priority.index(context.ContextType))
else:
priority.append(0)
return tuple(priority)
def sort_subcontext(context):
priority = []
if context.ContextType in type_priority:
priority.append(len(type_priority) - type_priority.index(context.ContextType))
else:
priority.append(0)
if context.ContextIdentifier in identifier_priority:
priority.append(len(identifier_priority) - identifier_priority.index(context.ContextIdentifier))
else:
priority.append(0)
if context.TargetView in target_view_priority:
priority.append(len(target_view_priority) - target_view_priority.index(context.TargetView))
else:
priority.append(0)
priority.append(context.TargetScale or 0) # Big then small
return tuple(priority)
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
self.contexts = sorted(
self.file.by_type("IfcGeometricRepresentationSubContext"), key=sort_subcontext, reverse=True
) + sorted(
self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False),
key=sort_context,
reverse=True,
)
for context in self.contexts:
settings = ifcopenshell.geom.settings()
settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
settings.set(settings.STRICT_TOLERANCE, True)
settings.set(settings.INCLUDE_CURVES, True)
settings.set_context_ids([context.id()])
self.context_settings.append(settings)
def process_element_filter(self):
offset = self.ifc_import_settings.element_offset
@@ -433,13 +473,51 @@ class IfcImporter:
or getattr(element, "HasOpenings", None)
):
return
representations = self.get_transformed_body_representations(element.Representation.Representations)
representation = None
representation_priority = None
context = None
for rep in element.Representation.Representations:
if rep.ContextOfItems in self.contexts:
rep_priority = self.contexts.index(rep.ContextOfItems)
if representation is None or rep_priority < representation_priority:
representation = rep
representation_priority = rep_priority
context = rep.ContextOfItems
if not representation:
return
matrix = np.eye(4)
representation_id = None
rep = representation
while True:
if len(rep.Items) == 1 and rep.Items[0].is_a("IfcMappedItem"):
rep_matrix = ifcopenshell.util.placement.get_mappeditem_transformation(rep.Items[0])
if not np.allclose(rep_matrix, np.eye(4)):
matrix = rep_matrix @ matrix
if representation_id is None:
representation_id = rep.id()
rep = rep.Items[0].MappingSource.MappedRepresentation
else:
if representation_id is None:
representation_id = rep.id()
break
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
matrix[0][3] *= self.unit_scale
matrix[1][3] *= self.unit_scale
matrix[2][3] *= self.unit_scale
# Single swept disk solids (e.g. rebar) are better natively represented as beveled curves
if self.is_native_swept_disk_solid(element, representations):
if self.is_native_swept_disk_solid(element, resolved_representation):
self.native_data[element.GlobalId] = {
"representations": representations,
"representation": self.get_body_representation(element.Representation.Representations),
"matrix": matrix,
"context": context,
"geometry_id": representation_id,
"representation": resolved_representation,
"type": "IfcSweptDiskSolid",
}
return True
@@ -448,51 +526,53 @@ class IfcImporter:
return False # Performance improvements only occur on edge cases currently
# FacetedBreps (without voids) are meshes. See #841.
if self.is_native_faceted_brep(representations):
if self.is_native_faceted_brep(resolved_representation):
self.native_data[element.GlobalId] = {
"representations": representations,
"representation": self.get_body_representation(element.Representation.Representations),
"matrix": matrix,
"context": context,
"geometry_id": representation_id,
"representation": resolved_representation,
"type": "IfcFacetedBrep",
}
return True
if self.is_native_face_based_surface_model(representations):
if self.is_native_face_based_surface_model(resolved_representation):
self.native_data[element.GlobalId] = {
"representations": representations,
"representation": self.get_body_representation(element.Representation.Representations),
"matrix": matrix,
"context": context,
"geometry_id": representation_id,
"representation": resolved_representation,
"type": "IfcFaceBasedSurfaceModel",
}
return True
def is_native_swept_disk_solid(self, element, representations):
for representation in representations:
items = representation["raw"].Items or [] # Be forgiving of invalid IFCs because Revit :(
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
if tool.Blender.Modifier.is_railing(element):
return False
return True
elif len(items) and ( # See #2508 why we accommodate for invalid IFCs here
items[0].is_a("IfcSweptDiskSolid")
and len({i.is_a() for i in items}) == 1
and len({i.Radius for i in items}) == 1
):
if tool.Blender.Modifier.is_railing(element):
return False
return True
def is_native_swept_disk_solid(self, element, representation):
items = [i["item"] for i in ifcopenshell.util.representation.resolve_items(representation)]
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
if tool.Blender.Modifier.is_railing(element):
return False
return True
elif len(items) and ( # See #2508 why we accommodate for invalid IFCs here
items[0].is_a("IfcSweptDiskSolid")
and len({i.is_a() for i in items}) == 1
and len({i.Radius for i in items}) == 1
):
if tool.Blender.Modifier.is_railing(element):
return False
return True
return False
def is_native_faceted_brep(self, representations):
for representation in representations:
for i in representation["raw"].Items:
if i.is_a() != "IfcFacetedBrep":
return False
def is_native_faceted_brep(self, representation):
# TODO handle mapped items
for i in representation.Items:
if i.is_a() != "IfcFacetedBrep":
return False
return True
def is_native_face_based_surface_model(self, representations):
for representation in representations:
for i in representation["raw"].Items:
if i.is_a() != "IfcFaceBasedSurfaceModel":
return False
def is_native_face_based_surface_model(self, representation):
for i in representation.Items:
if i.is_a() != "IfcFaceBasedSurfaceModel":
return False
return True
def get_products_from_shape_representation(self, element):
@@ -590,13 +670,9 @@ class IfcImporter:
return
if not self.does_element_likely_have_geometry_far_away(element):
continue
try:
shape = ifcopenshell.geom.create_shape(self.settings, element)
except:
try:
shape = ifcopenshell.geom.create_shape(self.settings_body_2d, element)
except:
continue
shape = self.create_generic_shape(element)
if not shape:
continue
m = shape.transformation.matrix.data
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
@@ -668,7 +744,7 @@ class IfcImporter:
self.ifc_import_settings.logger.error("An invalid grid was found %s", grid)
continue
if grid.Representation:
shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, grid)
shape = self.create_generic_shape(grid)
grid_obj = self.create_product(grid, shape)
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
grid_obj.lock_location = (True, True, True)
@@ -687,7 +763,7 @@ class IfcImporter:
def create_grid_axes(self, axes, grid_collection, grid_obj):
for axis in axes:
shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, axis.AxisCurve)
shape = self.create_generic_shape(axis.AxisCurve)
mesh = self.create_mesh(axis, shape)
obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh)
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
@@ -707,27 +783,21 @@ class IfcImporter:
self.ifc_import_settings.logger.info("Creating object %s", element)
mesh = None
if self.ifc_import_settings.should_load_geometry:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Plan", "Annotation")
if not representation:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Annotation")
if representation:
for context in self.contexts:
representation = ifcopenshell.util.representation.get_representation(element, context)
if not representation:
continue
mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id())
mesh = self.meshes.get(mesh_name)
if mesh is None:
shape = None
try:
shape = ifcopenshell.geom.create_shape(self.settings, representation)
except:
try:
shape = ifcopenshell.geom.create_shape(self.settings_plan_2d, representation)
except:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
shape = self.create_generic_shape(representation)
if shape:
mesh = self.create_mesh(element, shape)
tool.Loader.link_mesh(shape, mesh)
self.meshes[mesh_name] = mesh
else:
self.ifc_import_settings.logger.error("Failed to generate shape for %s", element)
break
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
self.link_element(element, obj)
self.material_creator.create(element, obj, mesh)
@@ -751,20 +821,16 @@ class IfcImporter:
checkpoint = time.time()
self.incrementally_merge_objects()
native_data = self.native_data[element.GlobalId]
representation = native_data["representation"]
if not representation:
continue
context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
mesh_name = f"{context_id}/{representation.id()}"
mesh_name = f"{native_data['context'].id()}/{native_data['geometry_id']}"
mesh = self.meshes.get(mesh_name)
if mesh is None:
if native_data["type"] == "IfcSweptDiskSolid":
mesh = self.create_native_swept_disk_solid(element, mesh_name)
mesh = self.create_native_swept_disk_solid(element, mesh_name, native_data)
elif native_data["type"] == "IfcFacetedBrep":
mesh = self.create_native_faceted_brep(element, mesh_name)
mesh = self.create_native_faceted_brep(element, mesh_name, native_data)
elif native_data["type"] == "IfcFaceBasedSurfaceModel":
mesh = self.create_native_faceted_brep(element, mesh_name)
tool.Ifc.link(representation, mesh)
mesh = self.create_native_faceted_brep(element, mesh_name, native_data)
tool.Ifc.link(tool.Ifc.get().by_id(native_data["geometry_id"]), mesh)
mesh.name = mesh_name
self.meshes[mesh_name] = mesh
self.create_product(element, mesh=mesh)
@@ -776,21 +842,25 @@ class IfcImporter:
def create_elements(self):
self.create_generic_elements(self.elements)
def create_generic_shape(self, element):
for settings in self.context_settings:
try:
result = ifcopenshell.geom.create_shape(settings, element)
if result:
return result
except:
pass
def create_generic_elements(self, elements):
if isinstance(self.file, ifcopenshell.sqlite):
return self.create_generic_sqlite_elements(elements)
# Based on my experience in viewing BIM models, representations are prioritised as follows:
# 1. 3D Body, 2. 2D Body, 3. 2D Plans / annotations, 4. Point clouds, 5. No representation
# If an element has a representation that doesn't follow 1, 2, 3, or 4, it will not show by default.
# The user can load them later if they want to view them.
if self.ifc_import_settings.should_load_geometry:
products = self.create_products(elements)
elements -= products
products = self.create_products(elements, settings=self.settings_body_2d)
elements -= products
products = self.create_products(elements, settings=self.settings_plan_2d)
elements -= products
for settings in self.context_settings:
if not elements:
break
products = self.create_products(elements, settings=settings)
elements -= products
products = self.create_pointclouds(elements)
elements -= products
@@ -848,9 +918,6 @@ class IfcImporter:
self.create_product(element, mesh=mesh)
def create_products(self, products, settings=None):
if settings is None:
settings = self.settings
results = set()
if not products:
return results
@@ -887,16 +954,8 @@ class IfcImporter:
shape = iterator.get()
if shape:
product = self.file.by_id(shape.id)
if self.body_contexts:
self.create_product(product, shape)
results.add(product)
else:
if shape.context not in ["Body", "Facetation"] and IfcStore.get_element(shape.id):
# We only load a single context, and we prioritise the Body context. See #1290.
pass
else:
self.create_product(product, shape)
results.add(product)
self.create_product(product, shape)
results.add(product)
if not iterator.next():
break
print("Done creating geometry")
@@ -924,10 +983,10 @@ class IfcImporter:
self.structural_collection.children.link(self.structural_connection_collection)
self.project["blender"].children.link(self.structural_collection)
self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_plan_2d)
self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_plan_2d)
self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_plan_2d)
self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_plan_2d)
self.create_generic_elements(set(self.file.by_type("IfcStructuralCurveMember")))
self.create_generic_elements(set(self.file.by_type("IfcStructuralCurveConnection")))
self.create_generic_elements(set(self.file.by_type("IfcStructuralSurfaceMember")))
self.create_generic_elements(set(self.file.by_type("IfcStructuralSurfaceConnection")))
self.create_structural_point_connections()
def create_structural_point_connections(self):
@@ -1076,7 +1135,7 @@ class IfcImporter:
elif style.is_a("IfcPresentationStyleAssignment"):
styles.extend(style.Styles)
def create_native_faceted_brep(self, element, mesh_name):
def create_native_faceted_brep(self, element, mesh_name, native_data):
# TODO: georeferencing?
# co [x y z x y z x y z ...]
# vertex_index [i i i i i ...]
@@ -1093,10 +1152,11 @@ class IfcImporter:
"material_ids": [],
}
for representation in element.Representation.Representations:
if representation.ContextOfItems.id() not in self.body_contexts:
continue
self.convert_representation(representation)
for item in native_data["representation"].Items:
if item.is_a() == "IfcFacetedBrep":
self.convert_representation_item_faceted_brep(item)
elif item.is_a() == "IfcFaceBasedSurfaceModel":
self.convert_representation_item_face_based_surface_model(item)
mesh = bpy.data.meshes.new("Native")
@@ -1113,19 +1173,33 @@ class IfcImporter:
)
verts = [None] * len(self.mesh_data["co"])
for i in range(0, len(self.mesh_data["co"]), 3):
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
self.mesh_data["co"][i] * self.unit_scale,
self.mesh_data["co"][i + 1] * self.unit_scale,
self.mesh_data["co"][i + 2] * self.unit_scale,
offset_point[0] * self.unit_scale,
offset_point[1] * self.unit_scale,
offset_point[2] * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
verts[i], verts[i + 1], verts[i + 2], _ = native_data["matrix"] @ mathutils.Vector(
(
*ifcopenshell.util.geolocation.enh2xyz(
self.mesh_data["co"][i] * self.unit_scale,
self.mesh_data["co"][i + 1] * self.unit_scale,
self.mesh_data["co"][i + 2] * self.unit_scale,
offset_point[0] * self.unit_scale,
offset_point[1] * self.unit_scale,
offset_point[2] * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
),
1,
)
)
mesh["has_cartesian_point_offset"] = True
else:
verts = [c * self.unit_scale for c in self.mesh_data["co"]]
verts = [None] * len(self.mesh_data["co"])
for i in range(0, len(self.mesh_data["co"]), 3):
verts[i], verts[i + 1], verts[i + 2], _ = native_data["matrix"] @ mathutils.Vector(
(
self.mesh_data["co"][i] * self.unit_scale,
self.mesh_data["co"][i + 1] * self.unit_scale,
self.mesh_data["co"][i + 2] * self.unit_scale,
1,
)
)
mesh["has_cartesian_point_offset"] = False
mesh.vertices.add(self.mesh_data["total_verts"])
@@ -1135,25 +1209,13 @@ class IfcImporter:
mesh.polygons.add(self.mesh_data["total_polygons"])
mesh.polygons.foreach_set("loop_start", self.mesh_data["loop_start"])
mesh.polygons.foreach_set("loop_total", self.mesh_data["loop_total"])
mesh.polygons.foreach_set("use_smooth", [0] * self.mesh_data["total_polygons"])
mesh.update()
mesh["ios_materials"] = self.mesh_data["materials"]
mesh["ios_material_ids"] = self.mesh_data["material_ids"]
return mesh
def convert_representation(self, representation):
for item in representation.Items:
self.convert_representation_item(item)
def convert_representation_item(self, item):
if item.is_a("IfcMappedItem"):
# mapping_target = matrix
self.convert_representation(item.MappingSource.MappedRepresentation)
elif item.is_a() == "IfcFacetedBrep":
self.convert_representation_item_faceted_brep(item)
elif item.is_a() == "IfcFaceBasedSurfaceModel":
self.convert_representation_item_face_based_surface_model(item)
def convert_representation_item_face_based_surface_model(self, item):
mesh = item.get_info_2(recursive=True)
for face_set in mesh["FbsmFaces"]:
@@ -1248,63 +1310,39 @@ class IfcImporter:
self.mesh_data["loop_start"].extend(loop_start)
# list(di1.keys())
def create_native_swept_disk_solid(self, element, mesh_name):
def create_native_swept_disk_solid(self, element, mesh_name, native_data):
# TODO: georeferencing?
curve = bpy.data.curves.new(mesh_name, type="CURVE")
curve.dimensions = "3D"
curve.resolution_u = 2
polyline = curve.splines.new("POLY")
for representation in self.native_data[element.GlobalId]["representations"]:
for item in representation["raw"].Items:
# TODO: support inner radius, start param, and end param
geometry = ifcopenshell.geom.create_shape(self.settings_native, item.Directrix)
e = geometry.edges
v = geometry.verts
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
v2 = None
for edge in edges:
v1 = vertices[edge[0]]
if v1 != v2:
polyline = curve.splines.new("POLY")
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v1)
v2 = vertices[edge[1]]
polyline.points.add(1)
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v2)
for item_data in ifcopenshell.util.representation.resolve_items(native_data["representation"]):
item = item_data["item"]
matrix = item_data["matrix"]
matrix[0][3] *= self.unit_scale
matrix[1][3] *= self.unit_scale
matrix[2][3] *= self.unit_scale
# TODO: support inner radius, start param, and end param
geometry = self.create_generic_shape(item.Directrix)
e = geometry.edges
v = geometry.verts
vertices = [list(matrix @ [v[i], v[i + 1], v[i + 2], 1]) for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
v2 = None
for edge in edges:
v1 = vertices[edge[0]]
if v1 != v2:
polyline = curve.splines.new("POLY")
polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v1)
v2 = vertices[edge[1]]
polyline.points.add(1)
polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v2)
curve.bevel_depth = self.unit_scale * item.Radius
curve.use_fill_caps = True
return curve
def create_native_annotation(self, element, mesh_name):
# TODO: georeferencing?
curve = bpy.data.curves.new(mesh_name, type="CURVE")
curve.dimensions = "3D"
curve.resolution_u = 2
polyline = curve.splines.new("POLY")
for representation in self.native_data[element.GlobalId]["representations"]:
for item in representation["raw"].Items:
# TODO: support inner radius, start param, and end param
geometry = ifcopenshell.geom.create_shape(self.settings_native, item.Directrix)
e = geometry.edges
v = geometry.verts
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
v2 = None
for edge in edges:
v1 = vertices[edge[0]]
if v1 != v2:
polyline = curve.splines.new("POLY")
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v1)
v2 = vertices[edge[1]]
polyline.points.add(1)
polyline.points[-1].co = representation["matrix"] @ mathutils.Vector(v2)
curve.bevel_depth = self.unit_scale * item.Radius
return curve
def merge_by_class(self):
merge_set = {}
id_set = {}
@@ -1369,7 +1407,8 @@ class IfcImporter:
context_override = {}
context_override["object"] = context_override["active_object"] = target
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
bpy.ops.object.join(context_override)
with bpy.context.temp_override(**context_override):
bpy.ops.object.join()
target.data.name += "-merge"
for ifc_definition_id in id_set[group_name][1:]:
del self.added_data[ifc_definition_id]
@@ -1426,11 +1465,10 @@ class IfcImporter:
types_collection.hide_viewport = False
bpy.context.view_layer.objects.active = last_obj
context_override = {}
bpy.ops.object.editmode_toggle(context_override)
bpy.ops.mesh.tris_convert_to_quads(context_override)
bpy.ops.mesh.normals_make_consistent(context_override)
bpy.ops.object.editmode_toggle(context_override)
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.tris_convert_to_quads()
bpy.ops.mesh.normals_make_consistent()
bpy.ops.object.editmode_toggle()
types_collection.hide_viewport = True
bpy.context.view_layer.objects.active = last_obj
@@ -1706,41 +1744,6 @@ class IfcImporter:
result[2][3] *= self.unit_scale
return result
def get_body_representation(self, representations):
for representation in representations:
if (
representation.RepresentationIdentifier == "Body"
and representation.RepresentationType == "MappedRepresentation"
):
if len(representation.Items) > 1:
return representation
return self.get_body_representation([representation.Items[0].MappingSource.MappedRepresentation])
elif representation.RepresentationIdentifier == "Body":
return representation
def get_transformed_body_representations(self, representations, matrix=None):
if matrix is None:
matrix = mathutils.Matrix()
results = []
for representation in representations:
if (
representation.RepresentationIdentifier == "Body"
and representation.RepresentationType == "MappedRepresentation"
):
for item in representation.Items:
# TODO: Confirm if this transformation is right
transform = self.get_axis2placement(item.MappingSource.MappingOrigin)
if item.MappingTarget:
transform = transform @ self.get_cartesiantransformationoperator(item.MappingTarget)
results.extend(
self.get_transformed_body_representations(
[item.MappingSource.MappedRepresentation], transform @ matrix
)
)
elif representation.RepresentationIdentifier == "Body":
results.append({"raw": representation, "matrix": self.scale_matrix(matrix)})
return results
def scale_matrix(self, matrix):
matrix[0][3] *= self.unit_scale
matrix[1][3] *= self.unit_scale
@@ -1923,7 +1926,7 @@ class IfcImporter:
def link_element(self, element, obj):
self.added_data[element.id()] = obj
IfcStore.link_element(element, obj)
tool.Ifc.link(element, obj)
def set_matrix_world(self, obj, matrix_world):
obj.matrix_world = matrix_world
@@ -1967,6 +1970,7 @@ class IfcImportSettings:
self.element_limit = 30000
self.has_filter = None
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.elements = set()
self.collection_mode = "DECOMPOSITION"
@@ -62,14 +62,14 @@ class BoundaryDecorator:
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = []
selected_edges = []
@@ -421,7 +421,7 @@ class EnableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context))
if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"}
@@ -31,7 +31,7 @@ class BIM_PT_SceneBoundaries(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
bl_parent_id = "BIM_PT_tab_geometry"
@classmethod
def poll(cls, context):
@@ -28,7 +28,7 @@ class BIM_PT_bsdd(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
props = context.scene.BIMBSDDProperties
@@ -36,7 +36,7 @@ class BIM_PT_classifications(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
@classmethod
def poll(cls, context):
@@ -29,7 +29,7 @@ class BIM_PT_constraints(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
@classmethod
def poll(cls, context):
@@ -29,7 +29,7 @@ class BIM_PT_context(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
bl_parent_id = "BIM_PT_tab_geometry"
@classmethod
def poll(cls, context):
@@ -136,13 +136,11 @@ class CostSchedulesData:
data["UnitBasisUnitSymbol"] = "U"
if cost_value.Category == "*":
is_sum = True
cost_quantity = data["TotalCostQuantity"] or 1
if has_unit_basis:
data["TotalCost"] = data["TotalAppliedValue"] / data["UnitBasisValueComponent"]
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity / data["UnitBasisValueComponent"]
else:
if data["TotalCostQuantity"] is not None:
data["TotalCost"] = data["TotalAppliedValue"] * data["TotalCostQuantity"]
else:
data["TotalCost"] = data["TotalAppliedValue"]
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity
if is_sum:
data["TotalAppliedValue"] = None
@@ -160,8 +158,10 @@ class CostSchedulesData:
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
if unit:
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
else:
data["UnitSymbol"] = "U"
if quantity.is_a("IfcPhysicalSimpleQuantity"):
measure_class = quantity.wrapped_data.declaration().as_entity().attribute_by_index(3).type_of_attribute().declared_type().name()
if "Count" in measure_class:
data["UnitSymbol"] = "U"
# same_unit_nested_cost_item = set()
# data["DerivedTotalCostQuantity"] = None
@@ -30,11 +30,10 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
import blenderbim.tool as tool
import blenderbim.core.geometry
import ifcopenshell
#import blenderbim.tool as tool
#import blenderbim.core.geometry
#import ifcopenshell
class BIMCoveringProperties(PropertyGroup):
pass
# depth: bpy.props.FloatProperty(name="Depth", default=0.1, subtype="DISTANCE", description="Flooring depth")
ceiling_height: bpy.props.FloatProperty(name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height")
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell
import blenderbim.tool as tool
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.model.data import AuthoringData
@@ -42,13 +43,15 @@ class CoveringTool(WorkSpaceTool):
bl_description = "Create and edit coverings"
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.covering")
bl_widget = None
ifc_element_type = "IfcCoveringType"
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
("bim.covering_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.covering_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
)
@classmethod
def draw_settings(cls, context, layout, ws_tool):
CoveringToolUI.draw(context, layout, ifc_element_type="IfcCoveringType")
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey(layout, text, hotkey, description):
args = ["covering", layout, text, hotkey, description]
@@ -61,6 +64,7 @@ class CoveringToolUI:
cls.props = context.scene.BIMModelProperties
cls.covering_props = context.scene.BIMCoveringProperties
row = cls.layout.row(align=True)
if not tool.Ifc.get():
row.label(text="No IFC Project", icon="ERROR")
@@ -71,7 +75,6 @@ class CoveringToolUI:
elif AuthoringData.data["ifc_element_type"] != ifc_element_type:
AuthoringData.load(ifc_element_type)
if context.region.type == "TOOL_HEADER":
cls.draw_header_interface()
elif context.region.type in ("UI", "WINDOW"):
@@ -87,15 +90,94 @@ class CoveringToolUI:
@classmethod
def draw_default_interface(cls):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl3", text="RL")
row = cls.layout.row(align=True)
row.prop(data=cls.covering_props, property="ceiling_height", text="Ceiling Height")
if AuthoringData.data["ifc_classes"]:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
collection = bpy.context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
relating_type_id = int(cls.props.relating_type_id)
type_material_usage = ifcopenshell.util.element.get_material(tool.Ifc.get().by_id(relating_type_id)).is_a()
# PLEASE KEEP COMMENTS AS A REMINDER
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
if (type_material_usage == "IfcMaterialLayerSet" and
not bpy.context.selected_objects):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
if tool.Ifc.get_entity(collection_obj):
if AuthoringData.data["predefined_type"] == "FLOORING":
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
elif AuthoringData.data["predefined_type"] == "CEILING":
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
elif (AuthoringData.data["predefined_type"] == "FLOORING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
elif (AuthoringData.data["predefined_type"] == "CEILING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
elif (element and
bpy.context.selected_objects and
element.is_a("IfcCovering") and
# AuthoringData.data["predefined_type"] == "FLOORING" and
AuthoringData.data["active_material_usage"] == "LAYER3"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# row = cls.layout.row(align=True)
# row.prop(data=cls.props, property="ceiling_height", text="ceiling height")
# if element and bpy.context.selected_objects and element.is_a("IfcWall"):
# op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
# else:
# op = row.operator("bim.add_instance_ceiling_from_cursor")
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
else:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
@@ -159,8 +241,32 @@ class Hotkey(bpy.types.Operator, Operator):
def hotkey_S_A(self):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
collection = bpy.context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if AuthoringData.data["predefined_type"] == "FLOORING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
elif tool.Ifc.get_entity(collection_obj):
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
elif AuthoringData.data["predefined_type"] == "CEILING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
elif tool.Ifc.get_entity(collection_obj):
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
else:
bpy.ops.bim.add_constr_type_instance()
def hotkey_S_G(self):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if (element and
bpy.context.selected_objects and
element.is_a("IfcCovering") and
AuthoringData.data["active_material_usage"] == "LAYER3"):
bpy.ops.bim.regen_selected_covering_object()
@@ -207,6 +207,7 @@ class ExportIfcCsv(bpy.types.Operator):
empty=props.empty_value,
bool_true=props.true_value,
bool_false=props.false_value,
concat=props.concat_value,
sort=sort,
groups=groups,
summaries=summaries,
@@ -72,6 +72,7 @@ class CsvProperties(PropertyGroup):
empty_value: StringProperty(default="-", name="Empty String Value")
true_value: StringProperty(default="YES", name="True Value")
false_value: StringProperty(default="NO", name="False Value")
concat_value: StringProperty(default=", ", name="Concat Value")
csv_delimiter: EnumProperty(
items=[
(";", ";", ""),
@@ -79,6 +79,8 @@ class BIM_PT_ifccsv(Panel):
row.prop(props, "true_value")
row = layout.row()
row.prop(props, "false_value")
row = layout.row()
row.prop(props, "concat_value")
layout.use_property_split = False
blenderbim.bim.helper.draw_filter(self.layout, props, SearchData, "csv")
@@ -218,7 +218,7 @@ class CreateAllShapes(bpy.types.Operator):
len(shape.geometry.edges),
len(shape.geometry.faces),
)
print(f"Failures: {len(failures)}")
self.report({"INFO"}, f"Failed shapes: {len(failures)}, check the system console for details.")
for failure in failures:
print(failure)
return {"FINISHED"}
@@ -29,7 +29,7 @@ class BIM_PT_documents(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
@classmethod
def poll(cls, context):
@@ -92,10 +92,10 @@ classes = (
prop.BIMTextProperties,
prop.BIMAssignedProductProperties,
prop.BIMAnnotationProperties,
ui.BIM_PT_camera,
ui.BIM_PT_drawing_underlay,
ui.BIM_PT_sheets,
ui.BIM_PT_drawings,
ui.BIM_PT_camera,
ui.BIM_PT_drawing_underlay,
ui.BIM_PT_schedules,
ui.BIM_PT_references,
ui.BIM_PT_product_assignments,
@@ -123,6 +123,8 @@ class DrawingsData:
[(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcBuildingStorey")]
)
return results
elif bpy.context.scene.DocProperties.target_view in ["MODEL_VIEW"]:
return [(h.upper(), h, "") for h in ["Orthographic", "Perspective"]]
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
@classmethod
@@ -161,9 +161,9 @@ class BaseDecorator:
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
)
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.base_shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.base_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
def get_camera_width_mm(self):
# Horrific prototype code to ensure bgl draws at drawing scales
@@ -390,8 +390,6 @@ class BaseDecorator:
# 0 is the default font, but we're fancier than that
font_id = self.font_id
dpi = context.preferences.system.dpi
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
ang = -Vector((1, 0)).angle_signed(text_dir)
@@ -411,7 +409,7 @@ class BaseDecorator:
font_size_px = int(0.004118616 * mm_to_px) * font_size_mm / 2.5
pos = pos - line_no * font_size_px * rotation_matrix[1]
blf.size(font_id, font_size_px, dpi)
blf.size(font_id, font_size_px)
if box_alignment or center or vcenter:
w, h = blf.dimensions(font_id, text)
@@ -1563,15 +1561,15 @@ class CutDecorator:
gpu.state.point_size_set(1)
gpu.state.blend_set("ALPHA")
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
black = (0, 0, 0, 1)
@@ -1931,7 +1929,7 @@ class DecorationsHandler:
if cls.installed:
cls.uninstall()
handler = cls()
# NOTE: we USE POST_PIXEL here so that we can use both 3D_POLYLINE_UNIFORM_COLOR
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
@@ -18,6 +18,7 @@
import bpy
import blenderbim.bim.module.drawing.decoration as decoration
import blenderbim.tool as tool
from bpy.app.handlers import persistent
@@ -37,10 +38,28 @@ def depsgraph_update_pre_handler(scene):
def set_active_camera_resolution(scene):
if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings:
return
props = scene.camera.data.BIMCameraProperties
ortho_scale = max((props.width, props.height))
aspect_ratio = props.width / props.height
if (
scene.render.resolution_x != scene.camera.data.BIMCameraProperties.raster_x
or scene.render.resolution_y != scene.camera.data.BIMCameraProperties.raster_y
(scene.camera.data.ortho_scale != ortho_scale)
or (scene.render.resolution_x / scene.render.resolution_y != aspect_ratio)
):
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y
scene.camera.data.ortho_scale = ortho_scale
diagram_scale = tool.Drawing.get_diagram_scale(scene.camera)
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
if props.width > props.height:
aspect_ratio = props.height / props.width
raster_x = ortho_scale * scale_ratio * props.dpi / 0.0254
raster_y = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
else:
aspect_ratio = props.width / props.height
raster_x = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
raster_y = ortho_scale * scale_ratio * props.dpi / 0.0254
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index]
@@ -259,7 +259,9 @@ class CreateDrawing(bpy.types.Operator):
with profile("Combine SVG layers"):
svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg)
tool.Drawing.open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path)
tool.Drawing.open_with_user_command(
context.preferences.addons["blenderbim"].preferences.svg_command, svg_path
)
if self.print_all:
bpy.ops.bim.activate_drawing(drawing=original_drawing_id, camera_view_point=False)
@@ -534,7 +536,7 @@ class CreateDrawing(bpy.types.Operator):
root = etree.fromstring(results)
group = root.find("{http://www.w3.org/2000/svg}g")
if not group:
if group is None:
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
@@ -918,7 +920,7 @@ class CreateDrawing(bpy.types.Operator):
join_criteria = join_criteria.split(",")
else:
# Drawing convention states that same objects classes with the same material are merged when cut.
join_criteria = ["class", "material.Name", 'r"Pset.*Common"."Status"']
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status"]
group = root.find("{http://www.w3.org/2000/svg}g")
joined_paths = {}
@@ -312,7 +312,6 @@ class DocProperties(PropertyGroup):
should_use_underlay_cache: BoolProperty(name="Use Underlay Cache", default=False)
should_use_linework_cache: BoolProperty(name="Use Linework Cache", default=False)
should_use_annotation_cache: BoolProperty(name="Use Annotation Cache", default=False)
should_extract: BoolProperty(name="Should Extract", default=True)
is_editing_drawings: BoolProperty(name="Is Editing Drawings", default=False)
is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
is_editing_references: BoolProperty(name="Is Editing References", default=False)
@@ -375,6 +374,9 @@ class BIMCameraProperties(PropertyGroup):
custom_scale_denominator: bpy.props.StringProperty(default="100", update=update_diagram_scale)
raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000)
dpi: IntProperty(name="DPI", default=75)
width: FloatProperty(name="Width", default=50, subtype="DISTANCE")
height: FloatProperty(name="Height", default=50, subtype="DISTANCE")
is_nts: BoolProperty(name="Is NTS", update=update_is_nts)
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
@@ -288,9 +288,9 @@ class BaseShader:
"""
def __init__(self):
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.base_shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.base_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
def get_shader(self):
"""Returns shader for this type"""
@@ -160,7 +160,8 @@ class SheetBuilder:
x = float(image.attrib["x"])
y = float(image.attrib["y"])
if image.attrib["data-type"] == "view-title":
image.attrib["x"] = str(x - readjust.x)
image.attrib["x"] = str(x + readjust.x)
# negate y offset because view-title comes AFTER foreground
image.attrib["y"] = str(y - readjust.y)
else:
image.attrib["x"] = str(x + readjust.x)
@@ -160,7 +160,7 @@ class SvgWriter:
def find_xml_symbol_by_id(self, id):
tree = ET.parse(self.resource_paths["Symbols"])
xml_symbol = tree.find(f'.//*[@id="{id}"]')
return External(xml_symbol) if xml_symbol else None
return External(xml_symbol) if xml_symbol is not None else None
def add_patterns(self):
path = self.resource_paths["Patterns"]
@@ -31,28 +31,28 @@ from blenderbim.bim.module.drawing.prop import ANNOTATION_TYPES_DATA
class BIM_PT_camera(Panel):
bl_label = "Drawing Generation"
bl_label = "Active Drawing"
bl_idname = "BIM_PT_camera"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "data"
@classmethod
def poll(cls, context):
return context.camera and hasattr(context.active_object.data, "BIMCameraProperties")
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_drawings"
def draw(self, context):
layout = self.layout
if "/" not in context.active_object.name:
layout.label(text="This is not a BIM camera.")
if not (context.scene.camera and hasattr(context.scene.camera.data, "BIMCameraProperties")):
row = self.layout.row()
row.label(text="No Active Drawing", icon="ERROR")
return
layout.use_property_split = True
dprops = context.scene.DocProperties
props = context.active_object.data.BIMCameraProperties
if "/" not in context.scene.camera.name:
self.layout.label(text="This is not a BIM camera.")
return
col = layout.column(align=True)
self.layout.use_property_split = True
dprops = context.scene.DocProperties
props = context.scene.camera.data.BIMCameraProperties
col = self.layout.column(align=True)
row = col.row(align=True)
row.prop(props, "has_underlay", icon="OUTLINER_OB_IMAGE")
row.prop(dprops, "should_use_underlay_cache", text="", icon="FILE_REFRESH")
@@ -63,32 +63,36 @@ class BIM_PT_camera(Panel):
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = layout.row()
row = self.layout.row()
row.prop(props, "calculate_shapely_surfaces")
row = layout.row()
row = self.layout.row()
row.prop(props, "calculate_svgfill_surfaces")
row = layout.row()
row.prop(dprops, "should_extract")
row = self.layout.row()
row.prop(props, "width")
row = self.layout.row()
row.prop(props, "height")
row = layout.row()
row.prop(props, "raster_x")
row = layout.row()
row.prop(props, "raster_y")
row = self.layout.row()
row.prop(context.scene.camera.data, "clip_end", text="Depth")
row = layout.row(align=True)
row.prop(props, "diagram_scale")
row = self.layout.row(align=True)
row.prop(props, "diagram_scale", text="Scale")
row.prop(props, "is_nts", text="", icon="MOD_EDGESPLIT")
if props.diagram_scale == "CUSTOM":
row = layout.row(align=True)
row = self.layout.row(align=True)
row.prop(props, "custom_scale_numerator", text="Custom Scale")
row.prop(props, "custom_scale_denominator", text="")
row = layout.row(align=True)
if props.has_underlay:
row = self.layout.row()
row.prop(props, "dpi")
row = self.layout.row(align=True)
row.operator("bim.create_drawing", text="Create Drawing", icon="OUTPUT")
op = row.operator("bim.open_drawing", icon="URL", text="")
op.view = context.active_object.name.split("/")[1]
op.view = context.scene.camera.name.split("/")[1]
class BIM_PT_drawing_underlay(Panel):
@@ -97,12 +101,12 @@ class BIM_PT_drawing_underlay(Panel):
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "data"
bl_context = "scene"
bl_parent_id = "BIM_PT_camera"
@classmethod
def poll(cls, context):
return context.camera and hasattr(context.active_object.data, "BIMCameraProperties")
return context.scene.camera and hasattr(context.active_object.data, "BIMCameraProperties")
def draw(self, context):
layout = self.layout
@@ -164,10 +168,8 @@ class BIM_PT_drawings(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
bl_parent_id = "BIM_PT_tab_drawings"
bl_options = {"HIDE_HEADER"}
def draw(self, context):
if not DrawingsData.is_loaded:
@@ -245,10 +247,8 @@ class BIM_PT_schedules(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
bl_parent_id = "BIM_PT_tab_schedules"
bl_options = {"HIDE_HEADER"}
def draw(self, context):
if not DocumentsData.is_loaded:
@@ -297,10 +297,8 @@ class BIM_PT_references(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
bl_parent_id = "BIM_PT_tab_references"
bl_options = {"HIDE_HEADER"}
def draw(self, context):
if not DocumentsData.is_loaded:
@@ -341,10 +339,8 @@ class BIM_PT_sheets(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return tool.Blender.is_tab(context, "DRAWINGS") and tool.Ifc.get()
bl_parent_id = "BIM_PT_tab_sheets"
bl_options = {"HIDE_HEADER"}
def draw(self, context):
if not SheetsData.is_loaded:
@@ -20,8 +20,10 @@ import bpy
from . import ui, prop, operator
classes = (
operator.SelectFMIfcFile,
operator.ExecuteIfcFM,
operator.ExecuteIfcFMFederate,
operator.SelectFMIfcFile,
operator.SelectFMSpreadsheetFiles,
prop.BIMFMProperties,
ui.BIM_PT_fm,
)
@@ -19,6 +19,7 @@
import os
import bpy
import json
import ifcfm
import logging
import tempfile
import ifcopenshell
@@ -32,9 +33,16 @@ class SelectFMIfcFile(bpy.types.Operator):
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
def execute(self, context):
context.scene.BIMFMProperties.ifc_file = self.filepath
props = context.scene.BIMFMProperties
props.ifc_files.clear()
dirname = os.path.dirname(self.filepath)
for f in self.files:
new = props.ifc_files.add()
new.name = os.path.join(dirname, f.name)
props.ifc_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -62,20 +70,87 @@ class ExecuteIfcFM(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
import ifcfm
props = context.scene.BIMFMProperties
ifc_file = tool.Ifc.get()
filepaths = []
if not (ifc_file and props.should_load_from_memory):
ifc_file = ifcopenshell.open(props.ifc_file)
if len(props.ifc_files):
ifc_files = [ifcopenshell.open(f.name) for f in props.ifc_files]
filepaths = [f.name for f in props.ifc_files]
else:
ifc_files = [ifcopenshell.open(props.ifc_file)]
else:
ifc_files = [ifc_file]
for i, ifc_file in enumerate(ifc_files):
if filepaths:
dirname = os.path.dirname(self.filepath)
prefix, _ = os.path.splitext(os.path.basename(filepaths[i]))
basename = os.path.basename(self.filepath)
filepath = os.path.join(dirname, f"{prefix}-{basename}")
else:
filepath = self.filepath
parser = ifcfm.Parser(preset=props.engine)
parser.parse(ifc_file)
writer = ifcfm.Writer(parser)
writer.write()
if props.format == "csv":
writer.write_csv("tmp/")
elif props.format == "ods":
writer.write_ods(filepath)
elif props.format == "xlsx":
writer.write_xlsx(filepath)
return {"FINISHED"}
class SelectFMSpreadsheetFiles(bpy.types.Operator):
bl_idname = "bim.select_fm_spreadsheet_files"
bl_label = "Select FM Spreadsheet Files"
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
def execute(self, context):
props = context.scene.BIMFMProperties
props.spreadsheet_files.clear()
dirname = os.path.dirname(self.filepath)
for f in self.files:
new = props.spreadsheet_files.add()
new.name = os.path.join(dirname, f.name)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class ExecuteIfcFMFederate(bpy.types.Operator):
bl_idname = "bim.execute_ifcfm_federate"
bl_label = "Execute IfcFM"
file_format: bpy.props.StringProperty()
filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
props = context.scene.BIMFMProperties
return props.spreadsheet_files
def invoke(self, context, event):
props = context.scene.BIMFMProperties
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
props = context.scene.BIMFMProperties
parser = ifcfm.Parser(preset=props.engine)
parser.parse(ifc_file)
parser.federate([f.name for f in props.spreadsheet_files])
writer = ifcfm.Writer(parser)
writer.write()
if props.format == "csv":
writer.write_csv('tmp/')
elif props.format == "ods":
if props.format == "ods":
writer.write_ods(self.filepath)
elif props.format == "xlsx":
writer.write_xlsx(self.filepath)
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -32,6 +33,8 @@ from bpy.props import (
class BIMFMProperties(PropertyGroup):
ifc_file: StringProperty(default="", name="IFC File")
ifc_files: CollectionProperty(name="IFC Files", type=StrProperty)
spreadsheet_files: CollectionProperty(name="Spreadsheets", type=StrProperty)
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
engine: EnumProperty(
items=[
+11 -1
View File
@@ -42,7 +42,10 @@ class BIM_PT_fm(Panel):
if not IfcStore.get_file() or not props.should_load_from_memory:
row = layout.row(align=True)
row.prop(props, "ifc_file")
if len(props.ifc_files) > 1:
row.label(text=f"{len(props.ifc_files)} Files Selected")
else:
row.prop(props, "ifc_file")
row.operator("bim.select_fm_ifc_file", icon="FILE_FOLDER", text="")
row = layout.row()
@@ -52,3 +55,10 @@ class BIM_PT_fm(Panel):
row = layout.row()
op = row.operator("bim.execute_ifcfm", text="Convert To Spreadsheet")
row = layout.row(align=True)
row.label(text=f"{len(props.spreadsheet_files)} Spreadsheets Selected")
row.operator("bim.select_fm_spreadsheet_files", icon="FILE_FOLDER", text="")
row = layout.row()
op = row.operator("bim.execute_ifcfm_federate", text="Merge Spreadsheets")
@@ -22,7 +22,10 @@ from . import ui, prop, operator
classes = (
operator.AddRepresentation,
operator.CopyRepresentation,
operator.DisableEditingRepresentationItems,
operator.EditObjectPlacement,
operator.EnableEditingRepresentationItems,
operator.FlipObject,
operator.GetRepresentationIfcParameters,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
@@ -46,15 +49,20 @@ classes = (
operator.SwitchRepresentation,
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
prop.RepresentationItem,
prop.BIMObjectGeometryProperties,
prop.BIMGeometryProperties,
ui.BIM_PT_derived_placements,
ui.BIM_PT_placement,
ui.BIM_PT_representations,
ui.BIM_PT_representation_items,
ui.BIM_PT_connections,
ui.BIM_PT_mesh,
ui.BIM_PT_derived_coordinates,
ui.BIM_PT_workarounds,
ui.BIM_MT_object_set_origin,
ui.BIM_MT_separate,
ui.BIM_MT_hotkey_separate,
ui.BIM_UL_representation_items,
)
@@ -71,7 +79,6 @@ def register():
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
bpy.types.Scene.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMGeometryProperties)
bpy.types.OBJECT_PT_transform.append(ui.BIM_PT_transform)
bpy.types.VIEW3D_MT_object.append(ui.object_menu)
bpy.types.OUTLINER_MT_object.append(ui.outliner_menu)
bpy.types.VIEW3D_MT_object_context_menu.append(ui.object_menu)
@@ -100,6 +107,9 @@ def register():
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Curve", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
@@ -116,7 +126,6 @@ def register():
def unregister():
bpy.types.VIEW3D_MT_object.remove(ui.object_menu)
bpy.types.OBJECT_PT_transform.remove(ui.BIM_PT_transform)
bpy.types.OUTLINER_MT_object.remove(ui.outliner_menu)
bpy.types.VIEW3D_MT_object_context_menu.remove(ui.outliner_menu)
bpy.types.VIEW3D_MT_edit_mesh.remove(ui.edit_mesh_menu)
@@ -23,8 +23,10 @@ from mathutils import Vector
def refresh():
DerivedPlacementsData.is_loaded = False
PlacementData.is_loaded = False
DerivedCoordinatesData.is_loaded = False
RepresentationsData.is_loaded = False
RepresentationItemsData.is_loaded = False
ConnectionsData.is_loaded = False
@@ -91,6 +93,50 @@ class RepresentationsData:
return results
class RepresentationItemsData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {
"total_items": cls.total_items(),
"active_surface_style": cls.active_surface_style(),
"active_layer": cls.active_layer(),
}
cls.is_loaded = True
@classmethod
def total_items(cls):
active_representation_id = None
result = 0
if bpy.context.active_object.data and hasattr(bpy.context.active_object.data, "BIMMeshProperties"):
active_representation_id = bpy.context.active_object.data.BIMMeshProperties.ifc_definition_id
element = tool.Ifc.get().by_id(active_representation_id)
if not element.is_a("IfcShapeRepresentation"):
return 0
queue = list(element.Items)
while queue:
item = queue.pop()
if item.is_a("IfcMappedItem"):
queue.extend(item.MappingSource.MappedRepresentation.Items)
else:
result += 1
return result
@classmethod
def active_surface_style(cls):
props = bpy.context.active_object.BIMGeometryProperties
if props.active_item_index < len(props.items):
return props.items[props.active_item_index].surface_style
@classmethod
def active_layer(cls):
props = bpy.context.active_object.BIMGeometryProperties
if props.active_item_index < len(props.items):
return props.items[props.active_item_index].layer
class ConnectionsData:
data = {}
is_loaded = False
@@ -184,7 +230,7 @@ class ConnectionsData:
return results
class DerivedPlacementsData:
class DerivedCoordinatesData:
data = {}
is_loaded = False
@@ -263,6 +309,25 @@ class DerivedPlacementsData:
for i, storey in enumerate(storeys):
if storey[0] != element:
continue
if i >= len(storeys):
if i >= len(storeys) - 1:
return "N/A"
return "{0:.3f}".format(round(storeys[i + 1][1] - storey[1], 3))
class PlacementData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {
"has_placement": cls.has_placement(),
}
cls.is_loaded = True
@classmethod
def has_placement(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element and hasattr(element, "ObjectPlacement"):
return True
return False
@@ -34,6 +34,7 @@ import blenderbim.core.drawing
import blenderbim.tool as tool
import blenderbim.bim.handler
from mathutils import Vector, Matrix
from time import time
from blenderbim.bim import import_ifc
from blenderbim.bim.ifc import IfcStore
@@ -66,9 +67,12 @@ class OverrideMeshSeparate(bpy.types.Operator, Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
if not element:
return
# You cannot separate meshes if the representation is mapped.
relating_type = tool.Root.get_element_type(tool.Ifc.get_entity(obj))
relating_type = tool.Root.get_element_type(element)
if relating_type and tool.Root.does_type_have_representations(relating_type):
# We toggle edit mode to ensure that once representations are
# unmapped, our Blender mesh only has a single user.
@@ -210,18 +214,22 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
should_switch_all_meshes: bpy.props.BoolProperty()
def _execute(self, context):
target = tool.Ifc.get().by_id(self.ifc_definition_id).ContextOfItems
target_representation = tool.Ifc.get().by_id(self.ifc_definition_id)
target = target_representation.ContextOfItems
is_subcontext = target.is_a("IfcGeometricRepresentationSubContext")
for obj in set(context.selected_objects + [context.active_object]):
element = tool.Ifc.get_entity(obj)
if not element:
continue
if is_subcontext:
representation = ifcopenshell.util.representation.get_representation(
element, target.ContextType, target.ContextIdentifier, target.TargetView
)
if obj == context.active_object:
representation = target_representation
else:
representation = ifcopenshell.util.representation.get_representation(element, target.ContextType)
if is_subcontext:
representation = ifcopenshell.util.representation.get_representation(
element, target.ContextType, target.ContextIdentifier, target.TargetView
)
else:
representation = ifcopenshell.util.representation.get_representation(element, target.ContextType)
if not representation:
continue
core.switch_representation(
@@ -465,7 +473,7 @@ class CopyRepresentation(bpy.types.Operator, Operator):
if not element:
continue
bm.to_mesh(obj.data)
old_rep = self.get_representation_by_context(element, geometric_context)
old_rep = tool.Geometry.get_representation_by_context(element, geometric_context)
if old_rep:
ifcopenshell.api.run(
"geometry.unassign_representation", tool.Ifc.get(), product=element, representation=old_rep
@@ -482,16 +490,6 @@ class CopyRepresentation(bpy.types.Operator, Operator):
profile_set_usage=None,
)
def get_representation_by_context(self, element, context):
if element.is_a("IfcProduct") and element.Representation:
for r in element.Representation.Representations:
if r.ContextOfItems == context:
return r
elif element.is_a("IfcTypeProduct") and element.RepresentationMaps:
for r in element.RepresentationMaps:
if r.MappedRepresentation.ContextOfItems == context:
return r.MappedRepresentation
class OverrideDelete(bpy.types.Operator):
bl_idname = "bim.override_object_delete"
@@ -800,7 +798,8 @@ class OverrideDuplicateMove(bpy.types.Operator):
new = blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
# clean up the orphaned mesh with ifc id of the original object to avoid confusion
if new and temp_data:
# IfcGridAxis keeps the same mesh data (it's pointing to ifc id 0, so it's not a problem)
if new and temp_data and not new.is_a("IfcGridAxis"):
tool.Blender.remove_data_block(temp_data)
if new:
@@ -1003,6 +1002,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
def _execute(self, context):
self.group_name = 'Linked Aggregate'
refresh_start_time = time()
self.new_active_obj = None
old_to_new = {}
def delete_objects(element):
parts = ifcopenshell.util.element.get_parts(element)
@@ -1084,6 +1086,11 @@ class RefreshLinkedAggregate(bpy.types.Operator):
blenderbim.bim.handler.refresh_ui_data()
operator_time = time() - refresh_start_time
if operator_time > 10:
self.report({"INFO"}, "Refresh Aggregate was finished in {:.2f} seconds".format(operator_time))
return {"FINISHED"}
class OverrideJoin(bpy.types.Operator, Operator):
bl_idname = "bim.override_object_join"
@@ -1429,3 +1436,70 @@ class OverrideModeSetObject(bpy.types.Operator):
if self.edited_objs:
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
class FlipObject(bpy.types.Operator):
bl_idname = "bim.flip_object"
bl_label = "Flip Object"
bl_description = "Flip object's local axes, keep the position"
bl_options = {"REGISTER", "UNDO"}
flip_local_axes: bpy.props.EnumProperty(
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
)
def execute(self, context):
for obj in context.selected_objects:
tool.Geometry.flip_object(obj, self.flip_local_axes)
return {"FINISHED"}
class EnableEditingRepresentationItems(bpy.types.Operator, Operator):
bl_idname = "bim.enable_editing_representation_items"
bl_label = "Enable Editing Representation Items"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
props.is_editing = True
props.items.clear()
if bpy.context.active_object.data and hasattr(bpy.context.active_object.data, "BIMMeshProperties"):
active_representation_id = bpy.context.active_object.data.BIMMeshProperties.ifc_definition_id
element = tool.Ifc.get().by_id(active_representation_id)
if not element.is_a("IfcShapeRepresentation"):
return
queue = list(element.Items)
while queue:
item = queue.pop()
if item.is_a("IfcMappedItem"):
queue.extend(item.MappingSource.MappedRepresentation.Items)
else:
new = props.items.add()
new.name = item.is_a()
new.ifc_definition_id = item.id()
styles = []
for inverse in tool.Ifc.get().get_inverse(item):
if inverse.is_a("IfcStyledItem"):
styles = inverse.Styles
if styles and styles[0].is_a("IfcPresentationStyleAssignment"):
styles = styles[0].Styles
for style in styles:
if style.is_a("IfcSurfaceStyle"):
new.surface_style = style.Name or "Unnamed"
elif inverse.is_a("IfcPresentationLayerAssignment"):
new.layer = inverse.Name or "Unnamed"
class DisableEditingRepresentationItems(bpy.types.Operator, Operator):
bl_idname = "bim.disable_editing_representation_items"
bl_label = "Disable Editing Representation Items"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
obj.BIMGeometryProperties.is_editing = False
@@ -38,8 +38,18 @@ def get_contexts(self, context):
return RepresentationsData.data["contexts"]
class RepresentationItem(PropertyGroup):
name: StringProperty(name="Name")
surface_style: StringProperty(name="Surface Style")
layer: StringProperty(name="Layer")
ifc_definition_id: IntProperty(name="IFC Definition ID")
class BIMObjectGeometryProperties(PropertyGroup):
contexts: EnumProperty(items=get_contexts, name="Contexts")
is_editing: BoolProperty(name="Is Editing", default=False)
items: CollectionProperty(name="Representation Items", type=RepresentationItem)
active_item_index: IntProperty(name="Active Representation Item Index")
class BIMGeometryProperties(PropertyGroup):
@@ -17,11 +17,18 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.bim
import blenderbim.tool as tool
from bpy.types import Panel, Menu
from bpy.types import Panel, Menu, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import prop_with_search
from blenderbim.bim.module.geometry.data import RepresentationsData, ConnectionsData, DerivedPlacementsData
from blenderbim.bim.module.geometry.data import (
RepresentationsData,
RepresentationItemsData,
ConnectionsData,
PlacementData,
DerivedCoordinatesData,
)
def object_menu(self, context):
@@ -47,6 +54,22 @@ class BIM_MT_separate(Menu):
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE"
class BIM_MT_hotkey_separate(Menu):
bl_idname = "BIM_MT_hotkey_separate"
bl_label = "Separate"
def draw(self, context):
self.layout.label(text="IFC Separate", icon_value=blenderbim.bim.icons["IFC"].icon_id)
self.layout.operator("bim.override_mesh_separate", text="Selection").type = "SELECTED"
self.layout.operator("bim.override_mesh_separate", text="By Material").type = "MATERIAL"
self.layout.operator("bim.override_mesh_separate", text="By Loose Parts").type = "LOOSE"
self.layout.separator()
self.layout.label(text="Blender Separate", icon="BLENDER")
self.layout.operator("mesh.separate", text="Selection").type = "SELECTED"
self.layout.operator("mesh.separate", text="By Material").type = "MATERIAL"
self.layout.operator("mesh.separate", text="By Loose Parts").type = "LOOSE"
class BIM_MT_object_set_origin(Menu):
bl_idname = "BIM_MT_object_set_origin"
bl_label = "IFC Set Origin"
@@ -124,6 +147,53 @@ class BIM_PT_representations(Panel):
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
class BIM_PT_representation_items(Panel):
bl_label = "Representation Items"
bl_idname = "BIM_PT_representation_items"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_order = 1
bl_parent_id = "BIM_PT_tab_representations"
@classmethod
def poll(cls, context):
if not context.active_object:
return False
if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return IfcStore.get_file()
def draw(self, context):
if not RepresentationItemsData.is_loaded:
RepresentationItemsData.load()
props = context.active_object.BIMGeometryProperties
row = self.layout.row(align=True)
row.label(text=f"{RepresentationItemsData.data['total_items']} Items Found")
if props.is_editing:
row.operator("bim.disable_editing_representation_items", icon="CANCEL", text="")
else:
row.operator("bim.enable_editing_representation_items", icon="IMPORT", text="")
return
self.layout.template_list("BIM_UL_representation_items", "", props, "items", props, "active_item_index")
row = self.layout.row()
if RepresentationItemsData.data["active_surface_style"]:
row.label(text=RepresentationItemsData.data["active_surface_style"], icon="MATERIAL")
else:
row.label(text="No Surface Style", icon="MESH_UVSPHERE")
row = self.layout.row()
if RepresentationItemsData.data["active_layer"]:
row.label(text=RepresentationItemsData.data["active_layer"], icon="STICKY_UVS_LOC")
else:
row.label(text="No Presentation Layer", icon="STICKY_UVS_LOC")
class BIM_PT_connections(Panel):
bl_label = "Connections"
bl_idname = "BIM_PT_connections"
@@ -227,6 +297,9 @@ class BIM_PT_mesh(Panel):
layout = self.layout
row = layout.row()
row.operator("bim.update_representation", text="Manually Save Representation")
row = layout.row()
row.operator("bim.copy_representation", text="Copy Mesh From Active To Selected")
@@ -250,50 +323,99 @@ class BIM_PT_mesh(Panel):
op = row.operator("bim.update_representation", text="Convert To Arbitrary Extrusion With Voids")
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
def BIM_PT_transform(self, context):
if context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id:
row = self.layout.row(align=True)
row.label(text="Blender Offset")
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
row = self.layout.row()
row.operator("bim.edit_object_placement")
if context.active_object and context.active_object.data:
mprops = context.active_object.data.BIMMeshProperties
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
row = layout.row(align=True)
row.prop(ifc_parameter, "name", text="")
row.prop(ifc_parameter, "value", text="")
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
class BIM_PT_derived_placements(Panel):
bl_label = "Derived Placements"
bl_idname = "BIM_PT_derived_placements"
bl_options = {"DEFAULT_CLOSED"}
class BIM_PT_placement(Panel):
bl_label = "Placement"
bl_idname = "BIM_PT_placement"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_context = "scene"
bl_order = 1
bl_parent_id = "OBJECT_PT_transform"
bl_parent_id = "BIM_PT_tab_placement"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id
def draw(self, context):
if not DerivedPlacementsData.is_loaded:
DerivedPlacementsData.load()
if not PlacementData.is_loaded:
PlacementData.load()
if PlacementData.data["has_placement"]:
row = self.layout.row()
row.prop(context.active_object, "location", text="Location")
row = self.layout.row()
row.prop(context.active_object, "rotation_euler", text="Rotation")
else:
row = self.layout.row()
row.label(text="No Object Placement Found")
if context.active_object.BIMObjectProperties.blender_offset_type != "NONE":
row = self.layout.row(align=True)
row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS")
row.label(text=context.active_object.BIMObjectProperties.blender_offset_type)
class BIM_PT_derived_coordinates(Panel):
bl_label = "Derived Coordinates"
bl_idname = "BIM_PT_derived_coordinates"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_order = 3
bl_parent_id = "BIM_PT_tab_placement"
bl_options = {"DEFAULT_CLOSED"}
@classmethod
def poll(cls, context):
return context.active_object is not None
def draw(self, context):
if not DerivedCoordinatesData.is_loaded:
DerivedCoordinatesData.load()
row = self.layout.row()
row.operator("bim.edit_object_placement", icon="EXPORT")
row = self.layout.row()
row.label(text="XYZ Dimensions")
row = self.layout.row(align=True)
row.enabled = False
row.prop(context.active_object, "dimensions", text="X", index=0, slider=True)
row.prop(context.active_object, "dimensions", text="Y", index=1, slider=True)
row.prop(context.active_object, "dimensions", text="Z", index=2, slider=True)
row = self.layout.row(align=True)
row.label(text="Min Global Z")
row.label(text=DerivedPlacementsData.data["min_global_z"])
row.label(text=DerivedCoordinatesData.data["min_global_z"])
row = self.layout.row(align=True)
row.label(text="Max Global Z")
row.label(text=DerivedPlacementsData.data["max_global_z"])
row.label(text=DerivedCoordinatesData.data["max_global_z"])
if DerivedPlacementsData.data["has_collection"]:
if DerivedCoordinatesData.data["has_collection"]:
row = self.layout.row(align=True)
row.label(text="Min Decomposed Z")
row.label(text=DerivedPlacementsData.data["min_decomposed_z"])
row.label(text=DerivedCoordinatesData.data["min_decomposed_z"])
row = self.layout.row(align=True)
row.label(text="Max Decomposed Z")
row.label(text=DerivedPlacementsData.data["max_decomposed_z"])
row.label(text=DerivedCoordinatesData.data["max_decomposed_z"])
if DerivedPlacementsData.data["is_storey"]:
if DerivedCoordinatesData.data["is_storey"]:
row = self.layout.row(align=True)
row.label(text="Storey Height")
row.label(text=DerivedPlacementsData.data["storey_height"])
row.label(text=DerivedCoordinatesData.data["storey_height"])
class BIM_PT_workarounds(Panel):
@@ -322,3 +444,13 @@ class BIM_PT_workarounds(Panel):
row.prop(props, "should_force_triangulation")
row = self.layout.row()
row.prop(props, "should_use_presentation_style_assignment")
class BIM_UL_representation_items(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE"
row = layout.row(align=True)
row.label(text=item.name, icon=icon)
if item.layer:
row.label(text="", icon="STICKY_UVS_LOC")
@@ -56,7 +56,12 @@ class GeoreferenceData:
@classmethod
def map_conversion(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return {}
project = tool.Ifc.get().by_type("IfcProject")[0]
map_conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")
if not map_conversion:
return {}
del map_conversion["id"]
return map_conversion
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.HasCoordinateOperation:
@@ -72,8 +77,8 @@ class GeoreferenceData:
def map_derived_angle(cls):
if (
cls.data["map_conversion"]
and cls.data["map_conversion"]["XAxisAbscissa"] is not None
and cls.data["map_conversion"]["XAxisOrdinate"] is not None
and cls.data["map_conversion"].get("XAxisAbscissa", None) is not None
and cls.data["map_conversion"].get("XAxisOrdinate", None) is not None
):
return str(
round(
@@ -88,7 +93,12 @@ class GeoreferenceData:
@classmethod
def projected_crs(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return {}
project = tool.Ifc.get().by_type("IfcProject")[0]
projected_crs = ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS")
if not projected_crs:
return {}
del projected_crs["id"]
return projected_crs
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.HasCoordinateOperation:
@@ -29,7 +29,7 @@ class BIM_PT_gis(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
bl_parent_id = "BIM_PT_tab_geometry"
def draw(self, context):
self.layout.use_property_split = True
@@ -78,10 +78,14 @@ class BIM_PT_gis(Panel):
def draw_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
if tool.Ifc.get_schema() == "IFC2X3":
row = self.layout.row()
row.label(text="IFC2X3 Fallback In Use", icon="ERROR")
if not GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Not Georeferenced")
if tool.Ifc.get_schema != "IFC2X3":
if tool.Ifc.get_schema() != "IFC2X3":
row.operator("bim.add_georeferencing", icon="ADD", text="")
if props.has_blender_offset:
@@ -110,8 +114,9 @@ class BIM_PT_gis(Panel):
if GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
row.operator("bim.remove_georeferencing", icon="X", text="")
if tool.Ifc.get_schema() != "IFC2X3":
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
row.operator("bim.remove_georeferencing", icon="X", text="")
for key, value in GeoreferenceData.data["projected_crs"].items():
if not value:
@@ -184,4 +189,4 @@ class BIM_PT_gis_utilities(Panel):
row = self.layout.row(align=True)
row.prop(props, "y_axis_abscissa_output", text="North Abscissa")
row = self.layout.row(align=True)
row.prop(props, "y_axis_ordinate_output", text="North Ordinate")
row.prop(props, "y_axis_ordinate_output", text="North Ordinate")
@@ -29,7 +29,6 @@ classes = (
operator.LoadGroups,
operator.RemoveGroup,
operator.SelectGroupProducts,
operator.ToggleAssigningGroup,
operator.ToggleGroup,
operator.UnassignGroup,
operator.UpdateGroup,
@@ -173,16 +173,6 @@ class DisableEditingGroup(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class ToggleAssigningGroup(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.toggle_assigning_group"
bl_label = "Toggle Assigning Group"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
context.scene.BIMGroupProperties.is_adding = not context.scene.BIMGroupProperties.is_adding
return {"FINISHED"}
class AssignGroup(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_group"
bl_label = "Assign Group"
@@ -49,7 +49,6 @@ class Group(PropertyGroup):
class BIMGroupProperties(PropertyGroup):
group_attributes: CollectionProperty(name="Group Attributes", type=Attribute)
is_editing: BoolProperty(name="Is Editing", default=False)
is_adding: BoolProperty(name="Is Adding", default=False)
groups: CollectionProperty(name="Groups", type=Group)
active_group_index: IntProperty(name="Active Group Index")
active_group_id: IntProperty(name="Active Group Id")
@@ -89,9 +89,9 @@ class BIM_PT_object_groups(Panel):
ObjectGroupsData.load()
self.props = context.scene.BIMGroupProperties
row = self.layout.row(align=True)
if self.props.is_adding:
if self.props.is_editing:
row.label(text="Adding Groups", icon="OUTLINER")
row.operator("bim.toggle_assigning_group", text="", icon="CANCEL")
row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
self.layout.template_list(
"BIM_UL_object_groups",
"",
@@ -102,7 +102,7 @@ class BIM_PT_object_groups(Panel):
)
else:
row.label(text=f"{ObjectGroupsData.data['total_groups']} Groups in IFC Project", icon="OUTLINER")
row.operator("bim.toggle_assigning_group", text="", icon="ADD")
row.operator("bim.load_groups", text="", icon="GREASEPENCIL")
for group in ObjectGroupsData.data["groups"]:
row = self.layout.row(align=True)
@@ -11,7 +11,7 @@ class IFCGIT_PT_panel(bpy.types.Panel):
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_project_info"
bl_parent_id = "BIM_PT_tab_project_info"
def draw(self, context):
@@ -63,7 +63,7 @@ class EnableEditingLayer(bpy.types.Operator):
def execute(self, context):
props = context.scene.BIMLayerProperties
props.layer_attributes.clear()
blenderbim.bim.helper.import_attributes(tool.Ifc.get().by_id(self.layer), props.layer_attributes)
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.layer), props.layer_attributes)
props.active_layer_id = self.layer
return {"FINISHED"}
@@ -29,7 +29,7 @@ class BIM_PT_libraries(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
@classmethod
def poll(cls, context):
@@ -40,10 +40,12 @@ classes = (
operator.EditMaterial,
operator.EditMaterialSetItem,
operator.EditMaterialSetItemProfile,
operator.EditMaterialStyle,
operator.EnableEditingAssignedMaterial,
operator.EnableEditingMaterial,
operator.EnableEditingMaterialSetItem,
operator.EnableEditingMaterialSetItemProfile,
operator.EnableEditingMaterialStyle,
operator.ExpandMaterialCategory,
operator.LoadMaterials,
operator.RemoveConstituent,
@@ -55,6 +57,7 @@ classes = (
operator.ReorderMaterialSetItem,
operator.SelectByMaterial,
operator.UnassignMaterial,
operator.UnassignMaterialStyle,
operator.UnlinkMaterial,
prop.Material,
prop.BIMMaterialProperties,
@@ -38,6 +38,9 @@ class MaterialsData:
"total_materials": cls.total_materials(),
"material_types": cls.material_types(),
"profiles": cls.profiles(),
"styles": cls.styles(),
"contexts": cls.contexts(),
"active_styles": cls.active_styles(),
}
cls.is_loaded = True
@@ -79,6 +82,58 @@ class MaterialsData:
if p.ProfileName
]
@classmethod
def contexts(cls):
results = []
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
results.append((str(element.id()), element.ContextType or "Unnamed", ""))
for element in tool.Ifc.get().by_type("IfcGeometricRepresentationSubContext", include_subtypes=False):
results.append(
(
str(element.id()),
"{}/{}/{}".format(
element.ContextType or "Unnamed",
element.ContextIdentifier or "Unnamed",
element.TargetView or "Unnamed",
),
"",
)
)
return results
@classmethod
def styles(cls):
return [(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcSurfaceStyle") if s.Name]
@classmethod
def active_styles(cls):
props = bpy.context.scene.BIMMaterialProperties
results = []
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index].ifc_definition_id
if not material:
return results
material = tool.Ifc.get().by_id(material)
for definition in material.HasRepresentation:
for representation in definition.Representations:
if not representation.is_a("IfcStyledRepresentation"):
continue
context = representation.ContextOfItems
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
continue
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
results.append({
"context_type": context.ContextType,
"context_identifier": getattr(context, "ContextIdentifier", ""),
"target_view": getattr(context, "TargetView", ""),
"name": style.Name or "Unnamed",
"id": style.id(),
"context_id": context.id(),
})
return results
class ObjectMaterialData:
data = {}
@@ -173,6 +228,8 @@ class ObjectMaterialData:
items = cls.material.MaterialProfiles
elif cls.material.is_a("IfcMaterialConstituentSet"):
items = cls.material.MaterialConstituents
elif cls.material.is_a("IfcMaterialList"):
items = cls.material.Materials
icon = "LAYER_ACTIVE"
if "Layer" in cls.material.is_a():
@@ -191,7 +248,9 @@ class ObjectMaterialData:
data["name"] = "No Profile"
if item.is_a("IfcMaterialLayer"):
data["name"] += f" ({item.LayerThickness})"
if not item.is_a("IfcMaterialList"):
if item.is_a("IfcMaterial"):
data["material"] = item.Name or "Unnamed"
else:
data["material"] = item.Material.Name or "Unnamed"
results.append(data)
return results
@@ -118,10 +118,18 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Add Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
name: bpy.props.StringProperty(default="Default")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
row = self.layout
row.prop(self, "name", text="Name")
def _execute(self, context):
obj = bpy.data.materials.get(self.obj) if self.obj else None
core.add_material(tool.Ifc, tool.Material, tool.Style, obj=obj)
core.add_material(tool.Ifc, tool.Material, tool.Style, obj=obj, name=self.name)
material_prop_purge()
@@ -174,7 +182,7 @@ class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
objects = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
core.assign_material(tool.Ifc, tool.Material, material_type= self.material_type , objects=objects )
core.assign_material(tool.Ifc, tool.Material, material_type=self.material_type, objects=objects)
class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
@@ -185,7 +193,7 @@ class UnassignMaterial(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
objects = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
core.unassign_material(tool.Ifc, tool.Material, objects=objects )
core.unassign_material(tool.Ifc, tool.Material, objects=objects)
class AddConstituent(bpy.types.Operator, tool.Ifc.Operator):
@@ -350,10 +358,8 @@ class AddListItem(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.run(
"material.add_list_item",
self.file,
**{
"material_list": self.file.by_id(self.list_item_set),
"material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
},
material_list=self.file.by_id(self.list_item_set),
material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material)),
)
@@ -709,3 +715,50 @@ class ContractMaterialCategory(bpy.types.Operator):
category.is_expanded = False
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_material_style"
bl_label = "Enable Editing Material Style"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
def _execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
props.active_material_id = self.material
props.editing_material_type = "STYLE"
class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_material_style"
bl_label = "Edit Material Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
material = tool.Ifc.get().by_id(props.active_material_id)
style = tool.Ifc.get().by_id(int(props.styles))
context = tool.Ifc.get().by_id(int(props.contexts))
ifcopenshell.api.run(
"style.assign_material_style", tool.Ifc.get(), material=material, style=style, context=context
)
tool.Material.disable_editing_material()
core.load_materials(tool.Material, props.material_type)
class UnassignMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_material_style"
bl_label = "Unassign Material Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty()
context: bpy.props.IntProperty()
def _execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
material = tool.Ifc.get().by_id(props.materials[props.active_material_index].ifc_definition_id)
style = tool.Ifc.get().by_id(self.style)
context = tool.Ifc.get().by_id(self.context)
ifcopenshell.api.run(
"style.unassign_material_style", tool.Ifc.get(), material=material, style=style, context=context
)
core.load_materials(tool.Material, props.material_type)
@@ -103,11 +103,24 @@ def get_profiles(self, context):
return MaterialsData.data["profiles"]
def get_styles(self, context):
if not MaterialsData.is_loaded:
MaterialsData.load()
return MaterialsData.data["styles"]
def get_contexts(self, context):
if not MaterialsData.is_loaded:
MaterialsData.load()
return MaterialsData.data["contexts"]
class Material(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
is_category: BoolProperty(name="Is Category", default=False)
is_expanded: BoolProperty(name="Is Expanded", default=True)
has_style: BoolProperty(name="Has Style", default=True)
total_elements: IntProperty(name="Total Elements")
@@ -119,7 +132,9 @@ class BIMMaterialProperties(PropertyGroup):
profiles: EnumProperty(items=get_profiles, name="Profiles")
active_material_id: IntProperty(name="Active Material ID")
material_attributes: CollectionProperty(name="Material Attributes", type=Attribute)
editing_material_type = StringProperty(name="Editing Material Type")
editing_material_type: StringProperty(name="Editing Material Type")
styles: EnumProperty(items=get_styles, name="Styles")
contexts: EnumProperty(items=get_contexts, name="Contexts")
class BIMObjectMaterialProperties(PropertyGroup):
@@ -28,11 +28,11 @@ from blenderbim.bim.module.material.data import MaterialsData, ObjectMaterialDat
class BIM_PT_materials(Panel):
bl_label = "Materials"
bl_idname = "BIM_PT_materials"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
bl_parent_id = "BIM_PT_tab_materials"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
@@ -45,11 +45,10 @@ class BIM_PT_materials(Panel):
self.props = context.scene.BIMMaterialProperties
row = self.layout.row(align=True)
row.label(text="{} Materials Found".format(MaterialsData.data["total_materials"]), icon="MATERIAL")
row.label(text="{} Materials".format(MaterialsData.data["total_materials"]), icon="NODE_MATERIAL")
if self.props.is_editing:
row.operator("bim.disable_editing_materials", text="", icon="CANCEL")
else:
row = self.layout.row(align=True)
prop_with_search(row, self.props, "material_type", text="")
row.operator("bim.load_materials", text="", icon="IMPORT")
return
@@ -58,20 +57,19 @@ class BIM_PT_materials(Panel):
row.alignment = "RIGHT"
if self.props.material_type == "IfcMaterial":
if not self.props.active_material_id:
row.operator("bim.add_material", text="", icon="ADD")
row.operator("bim.add_material", text="", icon="ADD")
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
if material.ifc_definition_id:
if self.props.active_material_id:
row.operator("bim.edit_material", text="", icon="CHECKMARK").material = material.ifc_definition_id
row.operator("bim.disable_editing_material", text="", icon="CANCEL").material = material.ifc_definition_id
self.draw_editable_material_attributes_ui()
else:
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL").material = material.ifc_definition_id
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL")
op.material = material.ifc_definition_id
op = row.operator("bim.enable_editing_material_style", text="", icon="SHADING_RENDERED")
op.material = material.ifc_definition_id
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
self.draw_editing_ui()
else:
row.operator("bim.add_material_set", text="", icon="ADD").set_type = self.props.material_type
if self.props.materials and self.props.active_material_index < len(self.props.materials):
@@ -83,9 +81,34 @@ class BIM_PT_materials(Panel):
self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index")
for style in MaterialsData.data["active_styles"]:
row = self.layout.row(align=True)
row.label(text="", icon="SHADING_RENDERED")
row.label(text=style["context_type"])
row.label(text=style["context_identifier"])
row.label(text=style["target_view"])
row.label(text=style["name"])
op = row.operator("bim.unassign_material_style", text="", icon="X")
op.style = style["id"]
op.context = style["context_id"]
def draw_editing_ui(self):
if not self.props.active_material_id:
return
ifc_definition_id = self.props.active_material_id
if self.props.editing_material_type == "ATTRIBUTES":
blenderbim.bim.helper.draw_attributes(self.props.material_attributes, self.layout)
row = self.layout.row(align=True)
row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
elif self.props.editing_material_type == "STYLE":
row = self.layout.row(align=True)
row.prop(self.props, "contexts", text="")
row.prop(self.props, "styles", text="")
row = self.layout.row(align=True)
row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK")
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
def draw_editable_material_attributes_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.material_attributes, self.layout)
class BIM_PT_material(Panel):
bl_label = "Material"
@@ -220,9 +243,7 @@ class BIM_PT_object_material(Panel):
elif self.props.active_material_set_item_id == set_item["id"]:
self.draw_editable_set_item_ui(set_item)
else:
self.draw_read_only_set_item_ui(
set_item, index, is_first=index == 0, is_last=index == total_items - 1
)
self.draw_read_only_set_item_ui(set_item, index, is_first=index == 0, is_last=index == total_items - 1)
def draw_editable_set_item_profile_ui(self, set_item):
box = self.layout.box()
@@ -268,7 +289,10 @@ class BIM_PT_object_material(Panel):
op.old_index = index
op.new_index = index + 1
setattr(op, "material_set", ObjectMaterialData.data["set"]["id"])
if not self.props.active_material_set_item_id and ObjectMaterialData.data["material_class"] != "IfcMaterialList":
if (
not self.props.active_material_set_item_id
and ObjectMaterialData.data["material_class"] != "IfcMaterialList"
):
if "Profile" in ObjectMaterialData.data["material_class"]:
op = row.operator("bim.enable_editing_material_set_item_profile", icon="ITALIC", text="")
op.material_set_item = set_item["id"]
@@ -284,9 +308,11 @@ class BIM_PT_object_material(Panel):
setattr(op, f"{ObjectMaterialData.data['set_item_name']}_index", index)
def draw_read_only_set_ui(self):
row = self.layout.row(align=True)
row.label(text="Name")
row.label(text=ObjectMaterialData.data["set"]["name"])
if ObjectMaterialData.data["material_class"] != "IfcMaterialList":
row = self.layout.row(align=True)
row.label(text="Name")
row.label(text=ObjectMaterialData.data["set"]["name"])
if ObjectMaterialData.data["set"]["description"]:
row = self.layout.row(align=True)
row.label(text="Description")
@@ -335,3 +361,6 @@ class BIM_UL_materials(UIList):
row2 = row.row()
row2.alignment = "RIGHT"
row2.label(text=str(item.total_elements))
if item.has_style:
row2.label(text="", icon="SHADING_RENDERED")
@@ -108,6 +108,10 @@ classes = (
space.GenerateSpace,
space.GenerateSpacesFromWalls,
covering.AddInstanceFlooringCoveringsFromWalls,
covering.AddInstanceCeilingCoveringsFromWalls,
covering.AddInstanceFlooringCoveringFromCursor,
covering.AddInstanceCeilingCoveringFromCursor,
covering.RegenSelectedCoveringObject,
space.ToggleSpaceVisibility,
mep.FitFlowSegments,
mep.RegenerateDistributionElement,
@@ -59,7 +59,7 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Parent": element.GlobalId, "Data": json.dumps(data)},
properties={"Parent": element.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
)
return {"FINISHED"}
@@ -132,7 +132,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
tool.Model.regenerate_array(parent, data)
pset = tool.Ifc.get().by_id(pset["id"])
data = json.dumps(data)
data = tool.Ifc.get().createIfcText(json.dumps(data))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True)
@@ -169,7 +169,6 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
data = json.loads(pset["Data"])
data[self.item]["count"] = 1
if (self.keep_objs) & (self.item < (len(data) - 1)):
self.report(
@@ -188,6 +187,8 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.Modifier.Array.bake_children_transform(element, self.item)
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, False)
if not self.keep_objs:
data[self.item]["count"] = 1
tool.Model.regenerate_array(parent, data, self.keep_objs)
pset = tool.Ifc.get().by_id(pset["id"])
@@ -195,7 +196,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
else:
del data[self.item]
data = json.dumps(data)
data = tool.Ifc.get().createIfcText(json.dumps(data))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
@@ -247,21 +248,30 @@ class SelectAllArrayObjects(bpy.types.Operator):
return True
def execute(self, context):
object = context.active_object
element = tool.Ifc.get_entity(object)
array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
if not array_pset:
self.report({"ERROR"}, f"Object is not part of an array.")
return {"CANCELLED"}
objects = context.selected_objects
for object in objects:
element = tool.Ifc.get_entity(object)
if not element:
self.report({"ERROR"}, f"Non IFC objects, were deselected.")
object.select_set(False)
try:
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
except RuntimeError:
self.report({"ERROR"}, f"Couldn't find array parent by guid '{array_pset['Parent']}'")
return {"CANCELLED"}
if element:
array_pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
if not array_pset:
self.report({"ERROR"}, f"Objects not part of an array, were deselected.")
object.select_set(False)
array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element)
tool.Blender.set_objects_selection(context, active_object=array_objects[0], selected_objects=array_objects)
if array_pset:
try:
parent_element = tool.Ifc.get().by_guid(array_pset["Parent"])
except RuntimeError:
self.report({"ERROR"}, f"Objects that don't have an array parent, were deselected.")
object.select_set(False)
array_objects = tool.Blender.Modifier.Array.get_all_objects(parent_element)
tool.Blender.set_objects_selection(
context, active_object=array_objects[0], selected_objects=array_objects, clear_previous_selection=False
)
return {"FINISHED"}
@@ -22,10 +22,93 @@ import ifcopenshell
import blenderbim.tool as tool
import blenderbim.core.covering as core
class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_flooring_covering_from_cursor"
bl_label = "Add Flooring From Cursor"
bl_options = {"REGISTER"}
bl_description = "Add a typed instance flooring covering from cursor position. Move the cursor position into the desired position, select the right space collection and run the operator"
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
def _execute(self, context):
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.add_instance_flooring_covering_from_cursor(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry)
class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_ceiling_covering_from_cursor"
bl_label = "Add Ceiling From Cursor"
bl_options = {"REGISTER"}
bl_description = "Add a typed instance ceiling covering from cursor position. Move the cursor position into the desired position, select the right space collection and run the operator"
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
def _execute(self, context):
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.add_instance_ceiling_covering_from_cursor(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry, tool.Covering)
class RegenSelectedCoveringObject(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.regen_selected_covering_object"
bl_label = "Regen"
bl_options = {"REGISTER"}
bl_description = "Regen selected covering object"
@classmethod
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
return element and element.is_a("IfcCovering")
def _execute(self, context):
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if not element.is_a("IfcCovering"):
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.regen_selected_covering_object(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry)
class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_flooring_coverings_from_walls"
bl_label = "Add Typed Covering From Walls"
bl_label = "Add Flooring From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add instance flooring coverings from selected walls. The active object must be a wall and layered vertically"
@@ -62,3 +145,42 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
core.add_instance_flooring_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry)
class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_ceiling_coverings_from_walls"
bl_label = "Add Ceilings From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add instance ceiling coverings from selected walls. The active object must be a wall and layered vertically"
@classmethod
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
# walls (i.e. prismatic) in the active object storey.
# In order to run, the active object must be a wall and
# there must be selected walls
active_obj = bpy.context.active_object
if not active_obj:
self.report({"ERROR"}, "No active object. Please select a wall")
return
element = tool.Ifc.get_entity(active_obj)
if element and not element.is_a("IfcWall"):
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
container = ifcopenshell.util.element.get_container(element)
if not container:
self.report({"ERROR"}, "The wall is not contained.")
if not bpy.context.selected_objects:
self.report({"ERROR"}, "No selected objects found. Please select walls.")
return
core.add_instance_ceiling_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry, tool.Covering)
@@ -53,6 +53,7 @@ class AuthoringData:
cls.data["ifc_element_type"] = cls.ifc_element_type
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes()
cls.data["predefined_type"] = cls.predefined_type()
cls.data["type_class"] = cls.type_class()
# only after .type_class()
@@ -212,6 +213,7 @@ class AuthoringData:
if bpy.context.active_object:
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
if representation and representation.is_a("IfcShapeRepresentation"):
representation = tool.Geometry.resolve_mapped_representation(representation)
return representation.RepresentationType
@classmethod
@@ -247,6 +249,17 @@ class AuthoringData:
return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in results]
return []
@classmethod
def predefined_type(cls):
relating_type_id = cls.props.relating_type_id
if not relating_type_id:
return
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
if not hasattr(relating_type, "PredefinedType"):
return
predefined_type = relating_type.PredefinedType
return predefined_type
@classmethod
def selected_material_usages(cls):
selected_usages = {}
@@ -303,6 +316,7 @@ class StairData:
if not cls.data["pset_data"]:
return
cls.data["general_params"] = cls.general_params()
cls.data["calculated_params"] = cls.calculated_params()
@classmethod
def pset_data(cls):
@@ -319,6 +333,10 @@ class StairData:
general_params[prop_readable_name] = prop_value
return general_params
@classmethod
def calculated_params(cls):
return tool.Model.get_active_stair_calculated_params(cls.data["pset_data"]["data_dict"])
class SverchokData:
data = {}
@@ -345,7 +363,7 @@ class SverchokData:
def get_prop_from_data(props, data, prop_name):
prop_value = data[prop_name]
prop_value = data.get(prop_name, tool.Blender.get_blender_prop_default_value(props, prop_name))
prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value
prop_readable_name = props.bl_rna.properties[prop_name].name
return prop_readable_name, prop_value
@@ -191,15 +191,15 @@ class ProfileDecorator:
unselected_edges.append(edge_indices)
### Actually drawing
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
# Draw faces
@@ -68,13 +68,7 @@ def update_door_modifier_representation(context, obj):
},
}
def get_active_representation_context(obj):
active_representation = tool.Geometry.get_active_representation(obj)
if active_representation:
return active_representation.ContextOfItems
return ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
previously_active_context = get_active_representation_context(obj)
previously_active_context = tool.Geometry.get_active_representation_context(obj)
# ELEVATION_VIEW representation
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
@@ -124,7 +118,7 @@ def update_door_modifier_representation(context, obj):
# adding switch representation at the end instead of changing order of representations
# to prevent #2744
if get_active_representation_context(obj) != previously_active_context:
if tool.Geometry.get_active_representation_context(obj) != previously_active_context:
previously_active_representation = ifcopenshell.util.representation.get_representation(
element,
previously_active_context.ContextType,
@@ -555,7 +549,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(door_data, default=list)},
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))},
)
update_door_modifier_representation(context, obj)
return {"FINISHED"}
@@ -614,7 +608,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
update_door_modifier_representation(context, obj)
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
door_data = json.dumps(door_data, default=list)
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": door_data})
return {"FINISHED"}
@@ -18,12 +18,10 @@
import bpy
import ifcopenshell.api
import blenderbim.core.spatial
import blenderbim.tool as tool
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty
from mathutils import Vector
from blenderbim.bim.ifc import IfcStore
def add_object(self, context):
@@ -66,7 +64,7 @@ def add_object(self, context):
tool.Ifc.get(),
**{"axis_tag": tag, "uvw_axes": "UAxes", "grid": grid},
)
IfcStore.link_element(result, obj)
tool.Ifc.link(result, obj)
ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), **{"axis_curve": obj, "grid_axis": result})
obj.BIMObjectProperties.ifc_definition_id = result.id()
@@ -91,7 +89,7 @@ def add_object(self, context):
tool.Ifc.get(),
**{"axis_tag": tag, "uvw_axes": "VAxes", "grid": grid},
)
IfcStore.link_element(result, obj)
tool.Ifc.link(result, obj)
ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), **{"axis_curve": obj, "grid_axis": result})
obj.BIMObjectProperties.ifc_definition_id = result.id()
@@ -311,6 +311,7 @@ class MEPGenerator:
profile_joiner.set_depth(connected_obj, connected_element_length)
def get_segment_data(self, segment):
"""returns points data is in world space"""
ports = tool.System.get_ports(segment)
segment_object = tool.Ifc.get_object(segment)
start_point = segment_object.location
@@ -451,11 +452,12 @@ class MEPGenerator:
# 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 to exact cases
for test_segment_data in fitting_data[:]:
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)
segments_data.remove(base_segment_data)
break
# all segments were sorted
return len(segments_data) == 0
@@ -767,7 +769,8 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
profile_offset *= V(1, -1)
# world space profile offset
profile_offset_ws = start_object_rotation @ (profile_offset * si_conversion).to_3d()
profile_offset_si = (profile_offset * si_conversion).to_3d()
profile_offset_ws = start_object_rotation @ profile_offset_si
def get_segments_length():
start_dir = (start_point - first_segment_start).normalized()
@@ -855,9 +858,9 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator):
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(transition_data, default=list)},
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(transition_data, default=list))},
)
tool.System.add_ports(obj, offset_end_port=profile_offset_ws)
tool.System.add_ports(obj, offset_end_port=profile_offset_si)
# NOTE: at this point we loose current blender objects selection
# create transition element
@@ -1007,6 +1010,8 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
segments_intersection_ws, (end_segment_data["start_point"], end_segment_data["end_point"])
)
# start_/end_segment_sign indicate
# whether segments' z axes are directed towards the bend
start_port = points_ports_map[start_point]
end_port = points_ports_map[end_point]
start_point_on_origin = start_point == start_segment_data["start_point"]
@@ -1021,7 +1026,15 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
# TODO: profile offset may need to be flipped (check transition code)
to_start_object_space = start_object_rotation.inverted()
profile_offset = (to_start_object_space @ end_point) - (to_start_object_space @ start_point)
ref_point = end_point.copy()
end_segment_dir = (second_segment_end - end_point).normalized()
# we prioritize direction between end_point and start_point for bend_vector
# if those point match we use general end segment direction
if tool.Cad.is_x((end_point - start_point).length, 0):
ref_point = end_point + end_segment_dir
bend_vector = (to_start_object_space @ ref_point) - (to_start_object_space @ start_point)
z_axis_end_object_local = to_start_object_space @ tool.Cad.get_basis_vector(end_object, 2)
def check_for_double_bends():
# The theory is To avoid double bends, the profile offset should occur along only two axes:
@@ -1035,8 +1048,6 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
# NOTE: some double bends are only possible for square profiles:
# https://i.imgur.com/ZhdGbEp.png
z_axis_end_object = end_object.matrix_world.col[2].normalized().to_3d()
z_axis_end_object_local = to_start_object_space @ z_axis_end_object
lateral_axes = [i for i in range(2) if not tool.Cad.is_x(z_axis_end_object_local[i], 0)]
if len(lateral_axes) != 1:
@@ -1046,7 +1057,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
)
non_lateral_axis = 0 if lateral_axes[0] == 1 else 1
non_lateral_axis_offset = profile_offset[non_lateral_axis]
non_lateral_axis_offset = bend_vector[non_lateral_axis]
if not tool.Cad.is_x(non_lateral_axis_offset, 0):
return (
None,
@@ -1072,27 +1083,27 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
angle, rotation_axis = get_bend_rotation()
lateral_sign = tool.Cad.sign(profile_offset[lateral_axis])
lateral_sign = tool.Cad.sign(bend_vector[lateral_axis])
radial_offset = V(0, 0, 0)
ref_point_radius = self.radius + profile_dim[lateral_axis]
radial_offset[lateral_axis] = ref_point_radius * (1 - cos(angle)) * lateral_sign
radial_offset.z = ref_point_radius * sin(angle)
def get_segments_extend():
segments_intersection = segments_intersection_ws - start_point
segments_intersection = to_start_object_space @ segments_intersection
radial_offset.z = ref_point_radius * sin(angle) * start_segment_sign
end_port_offset = radial_offset + V(0, 0, self.start_length * start_segment_sign)
end_port_offset += z_axis_end_object_local * (self.end_length * -end_segment_sign)
def get_segments_extend_points():
# since tangent segments are equal
# if drawn for the circle from the same point
required_offset = ref_point_radius * tan(angle / 2)
current_start_offset = segments_intersection.length
current_end_offset = (segments_intersection - profile_offset).length
start_segment_extend_point = segments_intersection_ws - start_segment_sign * (
self.start_length + required_offset
) * get_z_basis(start_object)
end_segment_extend_point = segments_intersection_ws - end_segment_sign * (
self.end_length + required_offset
) * get_z_basis(end_object)
start_extend = current_start_offset - (required_offset + self.start_length)
end_extend = current_end_offset - (required_offset + self.end_length)
return start_extend, end_extend
return start_segment_extend_point, end_segment_extend_point
def check_new_segment_length(start_point, end_point, extend_point):
"""Check if segment is placed too near to the bend point.
@@ -1112,8 +1123,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
return None
# adjust segments to fit the radius and angle
start_segment_extend, end_segment_extend = get_segments_extend()
start_segment_extend_point = start_point + start_segment_sign * start_segment_extend * get_z_basis(start_object)
start_segment_extend_point, end_segment_extend_point = get_segments_extend_points()
projection = check_new_segment_length(first_segment_start, start_point, start_segment_extend_point)
if projection is not None:
self.report(
@@ -1122,7 +1132,6 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
)
return {"ERROR"}
end_segment_extend_point = end_point + end_segment_sign * end_segment_extend * get_z_basis(end_object)
projection = check_new_segment_length(second_segment_end, end_point, end_segment_extend_point)
if projection is not None:
self.report(
@@ -1143,7 +1152,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
self.end_length / si_conversion,
angle,
self.radius / si_conversion,
profile_offset / si_conversion,
bend_vector / si_conversion,
flip_z_axis=start_segment_sign == -1,
)
@@ -1198,9 +1207,9 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(bend_data, default=list)},
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(bend_data, default=list))},
)
tool.System.add_ports(obj, offset_end_port=start_object_rotation @ (radial_offset * V(1, 1, 0)))
tool.System.add_ports(obj, end_port_pos=end_port_offset)
# NOTE: at this point we loose current blender objects selection
# create transition element
@@ -18,6 +18,7 @@
import bpy
import gpu
import json
import bmesh
import shapely
import logging
@@ -160,7 +161,10 @@ class FilledOpeningGenerator:
for voided_obj in voided_objs:
if voided_obj.data:
representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
voided_element = tool.Ifc.get_entity(voided_obj)
context = tool.Geometry.get_active_representation_context(voided_obj)
representation = tool.Geometry.get_representation_by_context(voided_element, context)
blenderbim.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -393,18 +397,7 @@ class FlipFill(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
if not element or not element.FillsVoids:
continue
flip_matrix = Matrix.Rotation(pi, 4, "Z")
bottom_left = obj.matrix_world @ Vector(obj.bound_box[0])
top_right = obj.matrix_world @ Vector(obj.bound_box[6])
center = obj.matrix_world.translation.copy()
center_offset = center - bottom_left
flipped_center = top_right - center_offset
obj.matrix_world = obj.matrix_world @ flip_matrix
obj.matrix_world.translation.xy = flipped_center.xy
bpy.context.view_layer.update()
tool.Geometry.flip_object(obj, "XY")
return {"FINISHED"}
@@ -531,10 +524,11 @@ class AddBoolean(Operator, tool.Ifc.Operator):
elif obj2.data:
mesh_data = {"type": "Mesh", "blender_obj": obj1, "blender_void": obj2}
ifcopenshell.api.run(
booleans = ifcopenshell.api.run(
"geometry.add_boolean", tool.Ifc.get(), representation=representation, operator="DIFFERENCE", **mesh_data
)
tool.Model.mark_manual_booleans(element1, booleans)
tool.Model.clear_scene_openings()
blenderbim.core.geometry.switch_representation(
@@ -655,6 +649,7 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
def _execute(self, context):
upstream_obj = None
bbim_boolean_updates = {}
for obj in context.selected_objects:
if (
not obj.data
@@ -663,13 +658,21 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
):
continue
try:
boolean = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id)
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_boolean_id)
except:
continue
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=boolean)
boolean = None
for inverse in tool.Ifc.get().get_inverse(item):
if inverse.is_a("IfcBooleanResult"):
boolean = inverse
break
ifcopenshell.api.run("geometry.remove_boolean", tool.Ifc.get(), item=item)
if obj.data.BIMMeshProperties.obj:
upstream_obj = obj.data.BIMMeshProperties.obj
element = tool.Ifc.get_entity(upstream_obj)
bbim_boolean_updates.setdefault(element, []).append(boolean)
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if body:
blenderbim.core.geometry.switch_representation(
@@ -682,7 +685,10 @@ class RemoveBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
should_sync_changes_first=False,
)
bpy.data.objects.remove(obj)
for element, booleans in bbim_boolean_updates.items():
tool.Model.unmark_manual_booleans(element, booleans)
tool.Blender.set_active_object(upstream_obj)
return {"FINISHED"}
@@ -726,6 +732,9 @@ class HideOpenings(Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcOpeningElement"):
element = element.VoidsElements[0].RelatingBuildingElement
obj = tool.Ifc.get_object(element)
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
for opening in openings:
opening_obj = tool.Ifc.get_object(opening)
@@ -753,10 +762,14 @@ class EditOpenings(Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcOpeningElement"):
element = element.VoidsElements[0].RelatingBuildingElement
obj = tool.Ifc.get_object(element)
openings = [r.RelatedOpeningElement for r in element.HasOpenings]
for opening in openings:
similar_openings = [o for o in all_openings if o.ObjectPlacement == opening.ObjectPlacement]
opening_obj = tool.Ifc.get_object(opening)
building_objs.add(obj)
if opening_obj:
if tool.Ifc.is_edited(opening_obj):
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
@@ -764,9 +777,12 @@ class EditOpenings(Operator, tool.Ifc.Operator):
blenderbim.core.geometry.edit_object_placement(
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
)
for similar_opening in similar_openings:
similar_opening.ObjectPlacement = opening.ObjectPlacement
building_objs.add(obj)
for similar_opening in similar_openings:
similar_opening.ObjectPlacement = opening.ObjectPlacement
element = similar_opening.VoidsElements[0].RelatingBuildingElement
obj = tool.Ifc.get_object(element)
building_objs.add(obj)
building_objs.update(self.get_all_building_objects_of_similar_openings(opening))
tool.Ifc.unlink(element=opening, obj=opening_obj)
bpy.data.objects.remove(opening_obj)
@@ -775,7 +791,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
return {"FINISHED"}
def get_all_building_objects_of_similar_openings(self, opening):
if not opening.HasFillings:
if not opening.is_a("IfcOpeningElement") or not opening.HasFillings:
return []
results = set()
for rel in opening.HasFillings:
@@ -790,6 +806,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
results.add(obj)
return results
class CloneOpening(Operator, tool.Ifc.Operator):
bl_idname = "bim.clone_opening"
bl_label = "Clone Opening"
@@ -813,12 +830,15 @@ class CloneOpening(Operator, tool.Ifc.Operator):
new_opening = ifcopenshell.api.run("root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement")
for representation in opening_representations:
ifcopenshell.api.run("geometry.assign_representation", tool.Ifc.get(), product = new_opening, representation = representation)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=new_opening, representation=representation
)
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening = new_opening, element = wall)
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=new_opening, element=wall)
new_opening.ObjectPlacement = opening_placement
return {"FINISHED"}
# TODO: merge with ProfileDecorator?
class DecorationsHandler:
installed = None
@@ -863,14 +883,14 @@ class DecorationsHandler:
if not obj:
continue
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
verts = []
selected_edges = []
@@ -80,9 +80,13 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMModelProperties
ifc_file = tool.Ifc.get()
props.type_class = self.ifc_element_type
if self.ifc_element_type == "IfcWallType":
props.type_predefined_type = "SOLIDWALL"
if ifc_file.schema == "IFC2X3":
props.type_predefined_type = "STANDARD"
else:
props.type_predefined_type = "SOLIDWALL"
props.type_template = "LAYERSET_AXIS2"
elif self.ifc_element_type == "IfcSlabType":
props.type_predefined_type = "FLOOR"
@@ -245,6 +249,8 @@ class AddConstrTypeInstance(bpy.types.Operator):
mat.translation *= unit_scale
mat = obj.matrix_world @ mat
new_port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
new_port.PredefinedType = port.PredefinedType
new_port.SystemType = port.SystemType
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)
@@ -555,5 +561,6 @@ def ensure_material_unassigned(usecase_path, ifc_file, settings):
total_removed = 0
for i in to_remove:
obj.active_material_index = i - total_removed
bpy.ops.object.material_slot_remove({"object": obj})
with bpy.context.temp_override(object=obj):
bpy.ops.object.material_slot_remove()
total_removed += 1
@@ -449,6 +449,16 @@ class DumbProfileJoiner:
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis
)
def get_placement_axes(body_representation):
if not body_representation:
return None, None
extrusion = tool.Model.get_extrusion(body_representation)
if not extrusion:
return None, None
position = extrusion.Position
return (position.Axis.DirectionRatios, position.RefDirection.DirectionRatios)
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
new_body = ifcopenshell.api.run(
"geometry.add_profile_representation",
tool.Ifc.get(),
@@ -457,9 +467,9 @@ class DumbProfileJoiner:
depth=depth,
cardinal_point=usage.CardinalPoint if usage else None,
clippings=self.clippings,
placement_zx_axes=get_placement_axes(old_body),
)
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if old_body:
for inverse in tool.Ifc.get().get_inverse(old_body):
ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body)
@@ -573,15 +583,11 @@ class DumbProfileJoiner:
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.translation, plane.col[2].to_3d()
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
self.body[1] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.translation, plane.col[2].to_3d()
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -624,15 +630,11 @@ class DumbProfileJoiner:
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.translation, plane.col[2].to_3d()
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
self.body[0] = intersect
else:
plane = self.get_profile_plane(profile2, furthest_plane, z_inwards=False)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.translation, plane.col[2].to_3d()
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -676,9 +678,7 @@ class DumbProfileJoiner:
if connection1 == "ATEND":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.translation, plane.col[2].to_3d()
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
self.body[1] = intersect
else:
plane = self.get_profile_plane(
@@ -686,9 +686,7 @@ class DumbProfileJoiner:
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.translation, plane.col[2].to_3d()
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(
@@ -701,9 +699,7 @@ class DumbProfileJoiner:
elif connection1 == "ATSTART":
if tool.Cad.is_x(abs(xy_angle), (0, 90, 180), tolerance=0.001) and is_orthogonal:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.translation, plane.col[2].to_3d()
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
self.body[0] = intersect
else:
plane = self.get_profile_plane(
@@ -711,9 +707,7 @@ class DumbProfileJoiner:
furthest_plane if is_relating else closest_plane,
z_inwards=False if is_relating else True,
)
intersect = mathutils.geometry.intersect_line_plane(
*axis1, plane.translation, plane.col[2].to_3d()
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.translation, plane.col[2].to_3d())
max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
self.clippings.append(
@@ -967,7 +961,7 @@ class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_axis(context))
if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"}
@@ -79,6 +79,7 @@ def update_type_class(self, context):
def update_relating_type_id(self, context):
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
AuthoringData.data["predefined_type"] = AuthoringData.predefined_type()
def update_type_page(self, context):
@@ -139,6 +140,7 @@ class BIMModelProperties(PropertyGroup):
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation")
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=-pi / 180 * 89, max=pi / 180 * 89)
type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
# fmt: off
type_template: bpy.props.EnumProperty(
items=(
("MESH", "Custom Mesh", "Use as a representation currently active object mesh or default cube if no object selected"),
@@ -158,6 +160,7 @@ class BIMModelProperties(PropertyGroup):
name="Type Template",
default="MESH",
)
# fmt: on
type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class)
type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
@@ -190,6 +193,10 @@ class BIMArrayProperties(PropertyGroup):
class BIMStairProperties(PropertyGroup):
def validate_nosing_value(self, context):
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
self["nosing_length"] = 0
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
stair_types = (
("CONCRETE", "Concrete", ""),
@@ -200,13 +207,37 @@ class BIMStairProperties(PropertyGroup):
is_editing: bpy.props.BoolProperty(default=False)
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
height: bpy.props.FloatProperty(name="Height", default=1.0, soft_min=0.01, subtype="DISTANCE")
number_of_treads: bpy.props.IntProperty(name="Number of treads", default=6, soft_min=1)
number_of_treads: bpy.props.IntProperty(name="Number of Treads", default=6, soft_min=1)
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, soft_min=0.01, subtype="DISTANCE")
tread_run: bpy.props.FloatProperty(name="Tread Run", default=0.3, soft_min=0.01, subtype="DISTANCE")
base_slab_depth: bpy.props.FloatProperty(name="Base slab depth", default=0.25, soft_min=0, subtype="DISTANCE")
top_slab_depth: bpy.props.FloatProperty(name="Top slab depth", default=0.25, soft_min=0, subtype="DISTANCE")
has_top_nib: bpy.props.BoolProperty(name="Has top nib", default=True)
stair_type: bpy.props.EnumProperty(name="Stair type", items=stair_types, default="CONCRETE")
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE")
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
stair_type: bpy.props.EnumProperty(
name="Stair Type", items=stair_types, default="CONCRETE", update=validate_nosing_value
)
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
name="Custom First / Last Treads Widths",
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
default=(0, 0),
min=0,
unit="LENGTH",
size=2,
)
# TODO: need to clamp at zero for non WOOD/STEEL
nosing_length: bpy.props.FloatProperty(
name="Nosing Length",
description=(
"Overhang of the tread, not counted as a part of the tread run.\n"
"Can be negative for WOOD/STEEL stair (then it becomes a tread gap)"
),
default=0,
unit="LENGTH",
update=validate_nosing_value,
)
nosing_depth: bpy.props.FloatProperty(
name="Nosing Depth", description="Depth of the tread's nosing", min=0, default=0, unit="LENGTH"
)
def get_props_kwargs(self, convert_to_project_units=False, stair_type=None):
if not stair_type:
@@ -217,10 +248,12 @@ class BIMStairProperties(PropertyGroup):
"height": self.height,
"number_of_treads": self.number_of_treads,
"tread_run": self.tread_run,
"nosing_length": self.nosing_length,
}
if stair_type == "CONCRETE":
concrete_props = {
"nosing_depth": self.nosing_depth,
"base_slab_depth": self.base_slab_depth,
"top_slab_depth": self.top_slab_depth,
"has_top_nib": self.has_top_nib,
@@ -235,7 +268,13 @@ class BIMStairProperties(PropertyGroup):
stair_kwargs.update(wood_steel_props)
elif stair_type == "GENERIC":
pass
generic_props = {
"nosing_depth": self.nosing_depth,
}
stair_kwargs.update(generic_props)
# defined here to appear last in UI
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
if not convert_to_project_units:
return stair_kwargs
@@ -114,7 +114,7 @@ def update_bbim_railing_pset(element, railing_data):
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Railing")
railing_data = json.dumps(railing_data, default=list)
railing_data = tool.Ifc.get().createIfcText(json.dumps(railing_data, default=list))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": railing_data})
@@ -455,7 +455,7 @@ class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator):
if bpy.context.active_object.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
tool.Blender.set_viewport_tool("bim.cad_tool")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: cancel_editing_railing_path(context))
return {"FINISHED"}
@@ -412,7 +412,7 @@ def update_bbim_roof_pset(element, roof_data):
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
if not pset:
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Roof")
roof_data = json.dumps(roof_data, default=list)
roof_data = tool.Ifc.get().createIfcText(json.dumps(roof_data, default=list))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": roof_data})
@@ -649,7 +649,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
if bpy.context.active_object.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
tool.Blender.set_viewport_tool("bim.cad_tool")
def mark_preview_edges(bm, bew_verts, new_edges, new_faces):
preview_layer = bm.edges.layers.int["BBIM_preview"]
@@ -288,7 +288,7 @@ class DumbSlabPlaner:
"geometry.add_slab_representation",
tool.Ifc.get(),
context=body_context,
depth=thickness,
depth=thickness * self.unit_scale,
x_angle=x_angle,
)
for inverse in tool.Ifc.get().get_inverse(representation):
@@ -313,7 +313,7 @@ class DumbSlabPlaner:
"geometry.add_slab_representation",
tool.Ifc.get(),
context=body_context,
depth=thickness,
depth=thickness * self.unit_scale,
x_angle=x_angle,
)
ifcopenshell.api.run(
@@ -625,7 +625,7 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_profile(context))
if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"}
@@ -18,16 +18,11 @@
import bpy
import bmesh
import shapely
import ifcopenshell
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.core.spatial as core
import blenderbim.core.type
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon, MultiPolygon
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
@@ -50,13 +45,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
props = context.scene.BIMModelProperties
relating_type_id = props.relating_type_id
relating_type = None
if relating_type_id:
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
if not relating_type.is_a("IfcSpaceType"):
relating_type = None
# props = context.scene.BIMModelProperties
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
@@ -68,151 +57,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
active_obj = bpy.context.active_object
element = None
if bpy.context.selected_objects and active_obj:
element = tool.Ifc.get_entity(active_obj)
mat = active_obj.matrix_world
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
x = global_bbox_center.x
y = global_bbox_center.y
z = (mat @ Vector(active_obj.bound_box[0])).z
h = active_obj.dimensions.z
else:
x, y = context.scene.cursor.location.xy
z = collection_obj.matrix_world.translation.z
mat = Matrix()
mat.translation = (x, y, z)
h = 3
calculation_rl = context.scene.BIMModelProperties.rl3
self.cut_point = collection_obj.matrix_world.translation.copy() + Vector((0, 0, calculation_rl))
self.cut_normal = Vector((0, 0, 1))
boundary_lines = []
gross_settings = ifcopenshell.geom.settings()
gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True)
for obj in bpy.context.visible_objects:
visible_element = tool.Ifc.get_entity(obj)
if (
not visible_element
or obj.type != "MESH"
or not self.is_bounding_class(visible_element)
or not tool.Drawing.is_intersecting_plane(obj, self.cut_point, self.cut_normal)
):
continue
old_mesh = None
if visible_element.HasOpenings:
new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, visible_element))
old_mesh = obj.data
obj.data = new_mesh
local_cut_point = obj.matrix_world.inverted() @ self.cut_point
local_cut_normal = obj.matrix_world.inverted().to_quaternion() @ self.cut_normal
verts, edges = tool.Drawing.bisect_mesh_with_plane(obj, local_cut_point, local_cut_normal)
if old_mesh:
obj.data = old_mesh
bpy.data.meshes.remove(new_mesh)
for edge in edges or []:
boundary_lines.append(
shapely.LineString(
[Vector((round(x, 3) for x in verts[edge[0]])), Vector((round(x, 3) for x in verts[edge[1]]))]
)
)
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
space_polygon = None
for polygon in closed_polygons.geoms:
if shapely.contains_xy(polygon, x, y):
space_polygon = shapely.force_3d(polygon)
if not space_polygon:
return
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
mat_invert = mat.inverted()
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], z])) for v in space_polygon.exterior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
for interior in space_polygon.interiors:
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], z])) for v in interior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.triangle_fill(bm, edges=bm.edges)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
mesh = bpy.data.meshes.new(name="Space")
bm.to_mesh(mesh)
bm.free()
if element and element.is_a("IfcSpace"):
mesh.name = active_obj.data.name
mesh.BIMMeshProperties.ifc_definition_id = active_obj.data.BIMMeshProperties.ifc_definition_id
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
tool.Ifc.edit(active_obj)
else:
if relating_type:
name = tool.Model.generate_occurrence_name(relating_type, "IfcSpace")
else:
name = "Space"
obj = bpy.data.objects.new("Space", mesh)
obj.matrix_world = mat
collection.objects.link(obj)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
element = tool.Ifc.get_entity(obj)
if relating_type:
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
def is_bounding_class(self, element):
for ifc_class in ["IfcWall", "IfcColumn", "IfcMember", "IfcVirtualElement", "IfcPlate"]:
if element.is_a(ifc_class):
return True
return False
def create_mesh(self, shape):
geometry = shape.geometry
mesh = bpy.data.meshes.new("tmp")
verts = geometry.verts
if geometry.faces:
num_vertices = len(verts) // 3
total_faces = len(geometry.faces)
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(geometry.faces)
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry.faces)
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
mesh.update()
return mesh
core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type)
class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
@@ -233,6 +78,26 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
# walls (i.e. prismatic) in the active object storey.
# In order to run, the active object must be a wall and
# there must be selected walls
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
container = tool.Spatial.get_container(element)
if not active_obj:
self.report({"ERROR"}, "No active object. Please select a wall")
return
element = tool.Ifc.get_entity(active_obj)
if element and not element.is_a("IfcWall"):
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
if not container:
self.report({"ERROR"}, "The wall is not contained.")
if not bpy.context.selected_objects:
self.report({"ERROR"}, "No selected objects found. Please select walls.")
return
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
@@ -17,22 +17,17 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import bmesh
import ifcopenshell
import blenderbim
import blenderbim.tool as tool
from mathutils import Vector
from bmesh.types import BMVert
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
import bmesh
from bmesh.types import BMVert
import ifcopenshell
from ifcopenshell.util.shape_builder import V, ShapeBuilder
import blenderbim
import blenderbim.tool as tool
from mathutils import Vector
from pprint import pprint
import json
def add_dumb_stair_object(self, context):
verts = [
@@ -82,138 +77,10 @@ class BIM_OT_add_object(Operator, AddObjectHelper):
return {"FINISHED"}
def generate_stair_2d_profile(
number_of_treads,
height,
width,
tread_run,
stair_type,
# WOOD/STEEL CONCRETE STAIR ARGUMENTS
tread_depth=None,
# CONCRETE STAIR ARGUMENTS
has_top_nib=None,
top_slab_depth=None,
base_slab_depth=None,
):
vertices = []
edges = []
faces = []
number_of_risers = number_of_treads + 1
tread_rise = height / number_of_risers
length = tread_run * number_of_risers
if stair_type == "WOOD/STEEL":
builder = ShapeBuilder(None)
tread_shape = builder.get_rectangle_coords(
size=V(tread_run, 0, tread_depth),
position=V(0, 0, -(tread_depth-tread_rise))
)
tread_offset = V(tread_run, 0, tread_rise)
for i in range(number_of_risers):
cur_trade_shape = [v + tread_offset * i for v in tread_shape]
vertices.extend(cur_trade_shape)
cur_vertex = i * 4
edges.extend([
(cur_vertex, cur_vertex+1),
(cur_vertex+1, cur_vertex+2),
(cur_vertex+2, cur_vertex+3),
(cur_vertex+3, cur_vertex),
])
faces.append(list(range(cur_vertex, cur_vertex + 1)))
return (vertices, edges, faces)
elif stair_type == "GENERIC":
vertices.append(Vector([0, 0, 0]))
tread_verts = [
Vector([0, 0, tread_rise]),
Vector([tread_run, 0, tread_rise])
]
tread_offset = Vector([tread_run, 0, tread_rise])
for i in range(number_of_risers):
current_tread_verts = [v + tread_offset * i for v in tread_verts]
last_vert_i = len(vertices) - 1
edges.extend([
(last_vert_i, last_vert_i + 1),
(last_vert_i + 1, last_vert_i + 2)
])
vertices.extend(current_tread_verts)
last_vert_i = len(vertices)
vertices.append(vertices[-1] * V(1,0,0))
edges.extend([
(last_vert_i - 1, last_vert_i),
(last_vert_i, 0)
])
return (vertices, edges, faces)
elif stair_type == "CONCRETE":
for i in range(number_of_risers):
vertices.extend([
Vector((tread_run*i, 0, tread_rise*i)),
Vector((tread_run*i, 0, tread_rise*(i+1)))
])
cur_vertex = i * 2
if i != 0:
edges.append((cur_vertex - 1, cur_vertex))
edges.append((cur_vertex, cur_vertex + 1))
vertices.append(Vector((tread_run * number_of_risers, 0, tread_rise * number_of_risers)))
edges.append((number_of_risers * 2, number_of_risers * 2 - 1))
td_vector = Vector((vertices[2][2], 0, -vertices[2][0])).normalized() * tread_depth
k = tread_rise / tread_run
s0 = vertices[0] + td_vector
b = s0.z - k * s0.x # comes from y = kx + b
# you could use td_vector as depth_vector
# but then stair won't be perpendicular to the X+
# b is kind of vertical tread_depth (along Z+)
depth_vector = Vector((0, 0, b))
# top nib
if has_top_nib:
vertices.append( vertices[number_of_risers * 2] + Vector((0, 0, -top_slab_depth)) )
vertices.append( vertices[number_of_risers * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth)) )
last_vertex_i = len(vertices) - 1
edges.append( (number_of_risers * 2, last_vertex_i - 1) )
edges.append( (last_vertex_i - 1, last_vertex_i) )
else:
vertices.append(vertices[number_of_risers * 2] + depth_vector)
last_vertex_i = len(vertices) - 1
edges.append((number_of_risers * 2, last_vertex_i))
top_nib_end = len(vertices) - 1
# bottom nib
if abs(b) <= base_slab_depth:
vertices.append(vertices[0] + depth_vector)
edges.append((0, len(vertices) - 1))
bottom_nib_end = len(vertices) - 1
else:
vertices.append(vertices[0] + Vector(((-base_slab_depth - b) / k, 0, -base_slab_depth)))
vertices.append(vertices[0] + Vector((0, 0, -base_slab_depth)))
last_vertex_i = len(vertices) - 1
edges.append( (0, last_vertex_i) )
edges.append( (last_vertex_i - 1, last_vertex_i) )
bottom_nib_end = len(vertices) - 2
edges.append( (bottom_nib_end, top_nib_end) )
faces = [list(range(len(vertices)))]
return (vertices, edges, faces)
def update_stair_modifier(context):
def regenerate_stair_mesh(context):
obj = context.active_object
props_kwargs = obj.BIMStairProperties.get_props_kwargs()
vertices, edges, faces = generate_stair_2d_profile(**props_kwargs)
vertices, edges, faces = tool.Model.generate_stair_2d_profile(**props_kwargs)
obj = context.active_object
bm = bmesh.new()
@@ -221,7 +88,7 @@ def update_stair_modifier(context):
bm.edges.index_update()
new_verts = [bm.verts.new(v) for v in vertices]
new_edges = [bm.edges.new( (new_verts[e[0]], new_verts[e[1]]) ) for e in edges]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in edges]
bm.verts.index_update()
bm.edges.index_update()
@@ -242,6 +109,26 @@ def update_stair_modifier(context):
obj.data.update()
def update_stair_representation(obj):
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
representation = ifcopenshell.api.run(
"geometry.add_representation",
tool.Ifc.get(),
context=body,
blender_object=obj,
geometry=obj.data,
coordinate_offset=tool.Geometry.get_cartesian_point_coordinate_offset(obj),
total_items=tool.Geometry.get_total_representation_items(obj),
should_force_faceted_brep=tool.Geometry.should_force_faceted_brep(),
should_force_triangulation=tool.Geometry.should_force_triangulation(),
should_generate_uvs=tool.Geometry.should_generate_uvs(obj),
ifc_representation_class=None,
profile_set_usage=None,
)
tool.Model.replace_object_ifc_representation(body, obj, representation)
tool.Ifc.finish_edit(obj)
def update_ifc_stair_props(obj):
"""should be called after new geometry settled
since it's going to update ifc representation
@@ -259,6 +146,7 @@ def update_ifc_stair_props(obj):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
riser_height = props.height / number_of_risers / si_conversion
tread_length = props.tread_depth / si_conversion
nosing_length = props.nosing_length / si_conversion
if element.is_a("IfcStairFlight"):
if tool.Ifc.get_schema() == "IFC2X3":
@@ -284,6 +172,7 @@ def update_ifc_stair_props(obj):
"NumberOfTreads": props.number_of_treads,
"RiserHeight": riser_height,
"TreadLength": tread_length,
"NosingLength": nosing_length,
},
)
tool.Ifc.edit(obj)
@@ -327,7 +216,7 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -335,7 +224,7 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
tool.Root,
obj=obj,
ifc_class="IfcStairFlight",
should_add_representation=True,
should_add_representation=False,
context=body_context,
)
if tool.Ifc.get_schema() != "IFC2X3":
@@ -370,10 +259,11 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
"pset.edit_pset",
ifc_file,
pset=pset,
properties={"Data": json.dumps(stair_data)},
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(stair_data))},
)
update_stair_modifier(context)
regenerate_stair_mesh(context)
update_ifc_stair_props(obj)
update_stair_representation(obj)
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
@@ -388,7 +278,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
props = obj.BIMStairProperties
# restore previous settings since editing was canceled
props.set_props_kwargs_from_ifc_data(data)
update_stair_modifier(context)
regenerate_stair_mesh(context)
props.is_editing = False
@@ -407,10 +297,11 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
data = props.get_props_kwargs(convert_to_project_units=True)
props.is_editing = False
update_stair_modifier(context)
regenerate_stair_mesh(context)
update_stair_representation(obj)
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
data = json.dumps(data)
data = tool.Ifc.get().createIfcText(json.dumps(data))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
# update IfcStairFlight properties
@@ -99,7 +99,7 @@ class DeleteSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def draw(self, context):
self.layout.label(text="WARNING. The graph will be removed permamently.")
self.layout.label(text="WARNING. The graph will be removed permanently.")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -169,7 +169,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator):
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(sverchok_data)},
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(sverchok_data))},
)
update_sverchok_modifier(context)
@@ -30,12 +30,13 @@ from blenderbim.bim.module.model.data import (
RoofData,
)
from blenderbim.bim.module.model.prop import get_ifc_class
from blenderbim.bim.module.model.stair import update_stair_modifier
from blenderbim.bim.module.model.stair import regenerate_stair_mesh
from blenderbim.bim.module.model.window import update_window_modifier_bmesh
from blenderbim.bim.module.model.door import update_door_modifier_bmesh
from blenderbim.bim.module.model.railing import update_railing_modifier_bmesh
from blenderbim.bim.module.model.roof import update_roof_modifier_bmesh
from blenderbim.bim.helper import prop_with_search
from collections.abc import Iterable
class LaunchTypeManager(bpy.types.Operator):
@@ -160,7 +161,7 @@ class BIM_PT_Grids(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids")
@@ -259,35 +260,42 @@ class BIM_PT_stair(bpy.types.Panel):
row = self.layout.row(align=True)
row.label(text="Stair parameters", icon="IPO_CONSTANT")
stair_data = StairData.data["pset_data"]["data_dict"]
if props.is_editing:
calculated_params = tool.Model.get_active_stair_calculated_params()
row = self.layout.row(align=True)
row.operator("bim.finish_editing_stair", icon="CHECKMARK", text="Finish Editing")
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs():
self.layout.prop(props, prop_name)
update_stair_modifier(context)
prop_value = getattr(props, prop_name)
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="")
else:
self.layout.prop(props, prop_name)
regenerate_stair_mesh(context)
else:
calculated_params = StairData.data["calculated_params"]
row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="")
row.operator("bim.remove_stair", icon="X", text="")
row = self.layout.row(align=True)
for prop_name, prop_value in StairData.data["general_params"].items():
row = self.layout.row(align=True)
row.label(text=prop_name)
row.label(text=str(prop_value))
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
row.label(text=f"{prop_name}:")
row = self.layout.row(align=True)
for prop_value_item in prop_value:
row.label(text=str(prop_value_item))
else:
row.label(text=prop_name)
row.label(text=str(prop_value))
# calculated properties
number_of_rises = props.number_of_treads + 1
row = self.layout.row(align=True)
row.label(text="Number of risers")
row.label(text=str(number_of_rises))
row = self.layout.row(align=True)
row.label(text="Tread rise")
row.label(text=str(round(props.height / number_of_rises, 5)))
row = self.layout.row(align=True)
row.label(text="Length")
row.label(text=str(round(props.tread_run * number_of_rises, 5)))
for prop_name, prop_value in calculated_params.items():
row = self.layout.row(align=True)
row.label(text=prop_name)
row.label(text=str(prop_value))
else:
row = self.layout.row()
row.label(text="No Stair Found")
@@ -1262,8 +1262,13 @@ class DumbWallJoiner:
results["is_sloped"] = True
results["height"] = (item.Depth * self.unit_scale) / (1 / cos(results["x_angle"]))
break
elif item.is_a("IfcBooleanClippingResult"):
elif item.is_a("IfcBooleanClippingResult"): # should be before IfcBooleanResult check
item = item.FirstOperand
elif item.is_a("IfcBooleanResult"):
if item.FirstOperand.is_a("IfcExtrudedAreaSolid") or item.FirstOperand.is_a("IfcBooleanResult"):
item = item.FirstOperand
else:
item = item.SecondOperand
else:
break
return results
@@ -119,13 +119,7 @@ def update_window_modifier_representation(context, obj):
}
representation_data["panel_properties"].append(panel_data)
def get_active_representation_context(obj):
active_representation = tool.Geometry.get_active_representation(obj)
if active_representation:
return active_representation.ContextOfItems
return ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
previously_active_context = get_active_representation_context(obj)
previously_active_context = tool.Geometry.get_active_representation_context(obj)
# ELEVATION_VIEW representation
profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW")
@@ -154,7 +148,7 @@ def update_window_modifier_representation(context, obj):
# adding switch representation at the end instead of changing order of representations
# to prevent #2744
if get_active_representation_context(obj) != previously_active_context:
if tool.Geometry.get_active_representation_context(obj) != previously_active_context:
previously_active_representation = ifcopenshell.util.representation.get_representation(
element,
previously_active_context.ContextType,
@@ -502,7 +496,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Data": json.dumps(window_data, default=list)},
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))},
)
update_window_modifier_representation(context, obj)
return {"FINISHED"}
@@ -559,7 +553,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
update_window_modifier_representation(context, obj)
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
window_data = json.dumps(window_data, default=list)
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": window_data})
return {"FINISHED"}
@@ -26,6 +26,7 @@ from blenderbim.bim.helper import prop_with_search, close_operator_panel
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.drawing.data import DecoratorData
from blenderbim.bim.module.system.data import PortData
from blenderbim.bim.module.model.prop import get_ifc_class
@@ -218,6 +219,9 @@ class BimToolUI:
row.label(text="No IFC Project", icon="ERROR")
return
if not PortData.is_loaded:
PortData.load()
if not AuthoringData.is_loaded:
AuthoringData.load(ifc_element_type)
elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None:
@@ -348,16 +352,15 @@ class BimToolUI:
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
add_layout_hotkey_operator(
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
)
if context.region.type != "TOOL_HEADER":
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
cls.layout.operator("bim.mep_add_obstruction")
cls.layout.operator("bim.mep_connect_elements")
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
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__)
@@ -401,6 +404,12 @@ class BimToolUI:
row.label(text="", icon=f"EVENT_TAB")
row.operator("bim.enable_editing_roof_path", text="Edit Roof Path")
if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0:
add_layout_hotkey_operator(
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
)
cls.layout.operator("bim.mep_connect_elements")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
@@ -418,11 +427,14 @@ class BimToolUI:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", text="")
if len(context.selected_objects) == 2:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_L")
row.operator("bim.clone_opening", text="Clone Opening")
cls.layout.row(align=True).label(text="Align")
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
@@ -615,6 +627,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
usage = tool.Model.get_usage_type(element)
if not usage:
representation = tool.Geometry.get_active_representation(obj)
representation = tool.Geometry.resolve_mapped_representation(representation)
if representation and representation.RepresentationType == "SweptSolid":
usage = "SWEPTSOLID"
else:
@@ -668,25 +681,29 @@ 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 ("IfcBeam", "IfcColumn"):
bpy.ops.bim.flip_object(flip_local_axes="XZ")
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
def hotkey_S_G(self):
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
if not bpy.context.selected_objects:
if self.props.ifc_class == "IfcSpaceType":
bpy.ops.bim.generate_space()
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.recalculate_wall()
elif tool.System.get_ports(element):
bpy.ops.bim.regenerate_distribution_element()
elif self.active_material_usage == "PROFILE":
if self.active_class in (
if self.active_class not in (
"IfcCableCarrierSegment",
"IfcCableSegment",
"IfcDuctSegment",
"IfcPipeSegment",
):
bpy.ops.bim.regenerate_distribution_element()
else:
bpy.ops.bim.recalculate_profile()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.recalculate_fill()
@@ -83,7 +83,7 @@ class BIM_PT_people(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_stakeholders"
@classmethod
def poll(cls, context):
@@ -142,7 +142,7 @@ class BIM_PT_organisations(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_stakeholders"
@classmethod
def poll(cls, context):
@@ -189,7 +189,7 @@ class BIM_PT_owner(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_stakeholders"
@classmethod
def poll(cls, context):
@@ -240,7 +240,7 @@ class BIM_PT_actor(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_stakeholders"
@classmethod
def poll(cls, context):
@@ -143,7 +143,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_arbitrary_profile(context))
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
tool.Blender.set_viewport_tool("bim.cad_tool")
def disable_editing_arbitrary_profile(context):
@@ -27,11 +27,11 @@ from blenderbim.bim.module.profile.prop import generate_thumbnail_for_active_pro
class BIM_PT_profiles(Panel):
bl_label = "Profiles"
bl_idname = "BIM_PT_profiles"
bl_options = {"DEFAULT_CLOSED"}
bl_options = {"HIDE_HEADER"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
bl_parent_id = "BIM_PT_tab_profiles"
@classmethod
def poll(cls, context):
@@ -58,11 +58,11 @@ class BIM_PT_profiles(Panel):
box.template_icon(icon_value=preview_image.icon_id, scale=5)
row = self.layout.row(align=True)
row.label(text=f"{ProfileData.data['total_profiles']} Named Profiles Found", icon="SNAP_GRID")
row.label(text=f"{ProfileData.data['total_profiles']} Named Profiles", icon="ITALIC")
if self.props.is_editing:
row.operator("bim.disable_profile_editing_ui", text="", icon="CANCEL")
else:
row.operator("bim.load_profiles", text="", icon="GREASEPENCIL")
row.operator("bim.load_profiles", text="", icon="IMPORT")
if not self.props.is_editing:
return
@@ -472,6 +472,7 @@ class AppendLibraryElement(bpy.types.Operator):
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = self.file
ifc_importer.type_collection = type_collection
ifc_importer.process_context_filter()
ifc_importer.material_creator.load_existing_materials()
self.import_materials(element, ifc_importer)
self.import_styles(element, ifc_importer)
@@ -808,9 +809,9 @@ class ToggleFilterCategories(bpy.types.Operator):
class LinkIfc(bpy.types.Operator):
bl_idname = "bim.link_ifc"
bl_label = "Link IFC"
bl_label = "Link Blend/IFC File"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Link a Blender file"
bl_description = "This will link the Blender file that is synced with the IFC file"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement)
directory: bpy.props.StringProperty(subtype="DIR_PATH")
@@ -1114,8 +1115,8 @@ class ExportIFC(bpy.types.Operator):
@classmethod
def description(cls, context, properties):
if properties.should_save_as:
return "Export the IFC project to a selected file"
return "Export the IFC project to this file"
return "Save the IFC file under a new name, or relocate file"
return "Save the IFC file. Will save both .IFC/.BLEND files if synced together"
class ImportIFC(bpy.types.Operator):
@@ -84,7 +84,7 @@ class BIM_PT_project(Panel):
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"HIDE_HEADER"}
bl_parent_id = "BIM_PT_project_info"
bl_parent_id = "BIM_PT_tab_project_info"
def draw(self, context):
if not ProjectData.is_loaded:
@@ -244,7 +244,7 @@ class BIM_PT_project_library(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
self.layout.use_property_decorate = False
@@ -287,7 +287,7 @@ class BIM_PT_links(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
self.props = context.scene.BIMProjectProperties
@@ -206,6 +206,7 @@ class EnablePsetEditing(bpy.types.Operator):
for prop in props:
if prop.is_a("IfcPropertyEnumeratedValue"):
simple_prop = self.props.properties.add()
simple_prop.name = prop.Name
simple_prop.value_type = "IfcPropertyEnumeratedValue"
metadata = simple_prop.metadata
metadata.name = prop.Name
@@ -227,6 +228,7 @@ class EnablePsetEditing(bpy.types.Operator):
elif prop.is_a("IfcPhysicalSimpleQuantity"):
value = prop[3]
new_prop = self.props.properties.add()
new_prop.name = prop.Name
metadata = new_prop.metadata
metadata.set_value(value)
metadata.name = prop.Name
@@ -29,7 +29,7 @@ class BIM_PT_pset_template(Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
def draw(self, context):
if not PsetTemplatesData.is_loaded:
@@ -79,7 +79,8 @@ def calculate_formwork_area(objs, context):
modifier = copied_obj.modifiers.new(type="BOOLEAN", name="Boolean")
modifier.operation = "UNION"
modifier.object = obj
bpy.ops.object.modifier_apply({"object": copied_obj}, modifier="Boolean")
with context.temp_override(object=copied_obj):
bpy.ops.object.modifier_apply(modifier="Boolean")
copied_obj.name = "Formwork"
copied_obj.BIMObjectProperties.ifc_definition_id = 0
@@ -66,6 +66,7 @@ class EnableReassignClass(bpy.types.Operator):
context.scene.BIMRootProperties.ifc_product = ifc_product
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
context.scene.BIMRootProperties.ifc_class = element.is_a()
context.scene.BIMRootProperties.relating_class_object = None
if hasattr(element, "PredefinedType") and element.PredefinedType:
context.scene.BIMRootProperties.ifc_predefined_type = element.PredefinedType
return {"FINISHED"}
@@ -91,7 +92,10 @@ class ReassignClass(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
if self.obj:
objects = [bpy.data.objects.get(self.obj)]
else:
objects = set(context.selected_objects + [context.active_object])
self.file = IfcStore.get_file()
predefined_type = context.scene.BIMRootProperties.ifc_predefined_type
if predefined_type == "USERDEFINED":
@@ -100,14 +104,12 @@ class ReassignClass(bpy.types.Operator):
product = ifcopenshell.api.run(
"root.reassign_class",
self.file,
**{
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
"ifc_class": context.scene.BIMRootProperties.ifc_class,
"predefined_type": predefined_type,
},
product=self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
ifc_class=context.scene.BIMRootProperties.ifc_class,
predefined_type=predefined_type,
)
obj.name = "{}/{}".format(product.is_a(), getattr(product, "Name", "None"))
IfcStore.link_element(product, obj)
tool.Ifc.link(product, obj)
obj.BIMObjectProperties.is_reassigning_class = False
return {"FINISHED"}
@@ -174,15 +176,21 @@ class UnlinkObject(bpy.types.Operator):
if element:
if self.should_delete:
obj_copy = obj.copy()
# copy object data, so it won't be removed by `delete_ifc_object`
if obj.data:
obj_copy.data = obj.data.copy()
if obj.type == "MESH":
obj_copy.data.BIMMeshProperties.ifc_definition_id = 0
for collection in obj.users_collection:
collection.objects.link(obj_copy)
tool.Geometry.delete_ifc_object(obj)
obj = obj_copy
if obj in IfcStore.edited_objs:
IfcStore.edited_objs.remove(obj)
IfcStore.unlink_element(obj=obj)
if obj.data:
obj.data = obj.data.copy()
tool.Ifc.unlink(obj=obj)
for material_slot in obj.material_slots:
if material_slot.material:
material_slot.material = material_slot.material.copy()
@@ -19,6 +19,7 @@
import bpy
import ifcopenshell
import ifcopenshell.util.schema
import blenderbim.tool as tool
from blenderbim.bim.module.root.data import IfcClassData
from bpy.types import PropertyGroup
from bpy.props import (
@@ -80,12 +81,46 @@ def get_contexts(self, context):
return IfcClassData.data["contexts"]
def update_relating_class_from_object(self, context):
if self.relating_class_object is None:
return
element = tool.Ifc.get_entity(self.relating_class_object)
if not element:
return
self.ifc_class = element.is_a()
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
if predefined_type:
if element.PredefinedType == "USERDEFINED":
self.ifc_predefined_type = "USERDEFINED"
self.ifc_userdefined_type = predefined_type
else:
self.ifc_predefined_type = predefined_type
bpy.ops.bim.reassign_class()
def is_object_class_applicable(self, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return False
active_element = tool.Ifc.get_entity(bpy.context.active_object)
if not active_element:
return False
return element.is_a("IfcTypeObject") == active_element.is_a("IfcTypeObject")
class BIMRootProperties(PropertyGroup):
contexts: EnumProperty(items=get_contexts, name="Contexts")
ifc_product: EnumProperty(items=get_ifc_products, name="Products", update=refresh_classes)
ifc_class: EnumProperty(items=get_ifc_classes, name="Class", update=refresh_predefined_types)
ifc_predefined_type: EnumProperty(items=get_ifc_predefined_types, name="Predefined Type", default=None)
ifc_userdefined_type: StringProperty(name="Userdefined Type")
relating_class_object: PointerProperty(
type=bpy.types.Object,
name="Copy Class",
update=update_relating_class_from_object,
poll=is_object_class_applicable,
description="Copy the selected object's class and predefined type to the active object",
)
getter_enum_suggestions = {
"ifc_class": get_ifc_classes_suggestions,
@@ -58,6 +58,7 @@ class BIM_PT_class(Panel):
root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context),
is_reassigning_class=True,
)
self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN")
else:
row = self.layout.row(align=True)
row.label(text=IfcClassData.data["name"])
@@ -40,6 +40,7 @@ classes = (
operator.SaveSearch,
operator.SaveSelectorQuery,
operator.Search,
operator.SelectByProperty,
operator.SelectFilterElements,
operator.SelectGlobalId,
operator.SelectIfcClass,
@@ -333,6 +333,36 @@ class ColourByProperty(Operator):
data["area"].spaces[0].shading.color_type = "OBJECT"
class SelectByProperty(Operator):
bl_idname = "bim.select_by_property"
bl_label = "Select by Property"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = context.scene.BIMSearchProperties
return props.active_colourscheme_index < len(props.colourscheme)
def execute(self, context):
props = context.scene.BIMSearchProperties
query = props.colourscheme_query
if not query:
self.report({"ERROR"}, "No Query Provided")
return {"CANCELLED"}
active_value = props.colourscheme[props.active_colourscheme_index].name
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
value = str(ifcopenshell.util.selector.get_element_value(element, query))
if value == active_value:
obj.select_set(True)
return {"FINISHED"}
class SaveColourscheme(Operator, tool.Ifc.Operator):
bl_idname = "bim.save_colourscheme"
bl_label = "Save Colourscheme"
@@ -356,7 +386,9 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
description["colourscheme_query"] = query
group.Description = json.dumps(description)
else:
description = json.dumps({"type": "BBIM_Search", "colourscheme": coulour_scheme,"colourscheme_query": query})
description = json.dumps(
{"type": "BBIM_Search", "colourscheme": coulour_scheme, "colourscheme_query": query}
)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
def invoke(self, context, event):
@@ -822,10 +854,13 @@ class SelectSimilar(Operator, tool.Ifc.Operator):
props = context.scene.BIMSearchProperties
obj = context.active_object
element = tool.Ifc.get_entity(obj)
value = ifcopenshell.util.selector.get_element_value(element, props.element_key)
key = props.element_key
if props.element_key == "PredefinedType":
key = "predefined_type"
value = ifcopenshell.util.selector.get_element_value(element, key)
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if ifcopenshell.util.selector.get_element_value(element, props.element_key) == value:
if ifcopenshell.util.selector.get_element_value(element, key) == value:
obj.select_set(True)
@@ -17,9 +17,9 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.tool as tool
from ifcopenshell import util
from ifcopenshell.util.selector import Selector
import blenderbim.tool as tool
from blenderbim.bim.prop import ObjProperty, StrProperty
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.search.data import SearchData, ColourByPropertyData, SelectSimilarData
@@ -87,9 +87,12 @@ class BIM_PT_colour_by_property(Panel):
row = self.layout.row(align=True)
row.operator("bim.colour_by_property", icon="BRUSH_DATA")
row.operator("bim.reset_object_colours")
row.operator("bim.select_by_property", icon="RESTRICT_SELECT_OFF", text="")
if len(props.colourscheme):
self.layout.template_list("BIM_UL_colourscheme", "", props, "colourscheme", props, "active_colourscheme_index")
self.layout.template_list(
"BIM_UL_colourscheme", "", props, "colourscheme", props, "active_colourscheme_index"
)
class BIM_PT_select_similar(Panel):
@@ -57,6 +57,25 @@ def updateContainerName(self, context):
props.container_name = self.name
def update_relating_container_from_object(self, context):
if self.relating_container_object is None or context.active_object is None:
return
element = tool.Ifc.get_entity(self.relating_container_object)
if not element:
return
container = ifcopenshell.util.element.get_container(element)
if container:
bpy.ops.bim.assign_container(structure=container.id())
else:
bpy.ops.bim.disable_editing_container()
bpy.ops.bim.remove_container()
def is_object_applicable(self, obj):
element = tool.Ifc.get_entity(obj)
return bool(element)
class SpatialElement(PropertyGroup):
name: StringProperty(name="Name")
long_name: StringProperty(name="Long Name")
@@ -73,6 +92,13 @@ class BIMSpatialProperties(PropertyGroup):
class BIMObjectSpatialProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_container_object: PointerProperty(
type=bpy.types.Object,
name="Copy Container",
update=update_relating_container_from_object,
poll=is_object_applicable,
description="Copy the target object's container to the active object",
)
class BIMContainer(PropertyGroup):
@@ -62,6 +62,7 @@ class BIM_PT_spatial(Panel):
row.operator("bim.disable_editing_container", icon="CANCEL", text="")
self.layout.template_list("BIM_UL_containers", "", props, "containers", props, "active_container_index")
self.layout.prop(osprops, "relating_container_object")
else:
row = self.layout.row(align=True)
if SpatialData.data["label"]:
@@ -103,7 +104,7 @@ class BIM_PT_SpatialManager(Panel):
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_project_setup"
bl_parent_id = "BIM_PT_tab_project_setup"
@classmethod
def poll(cls, context):
@@ -20,24 +20,29 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AddStyle,
operator.EnableEditingStyle,
operator.DisableEditingStyle,
operator.EditStyle,
operator.UpdateCurrentStyle,
operator.EnableEditingExternalStyle,
operator.DisableEditingExternalStyle,
operator.EditExternalStyle,
operator.DisableEditingStyles,
operator.BrowseExternalStyle,
operator.ActivateExternalStyle,
operator.AddPresentationStyle,
operator.AddStyle,
operator.BrowseExternalStyle,
operator.ClearTextureMapPath,
operator.DisableAddingPresentationStyle,
operator.DisableEditingExternalStyle,
operator.DisableEditingStyle,
operator.DisableEditingStyles,
operator.EditExternalStyle,
operator.EditStyle,
operator.EditSurfaceStyle,
operator.EnableAddingPresentationStyle,
operator.EnableEditingExternalStyle,
operator.EnableEditingStyle,
operator.EnableEditingSurfaceStyle,
operator.LoadStyles,
operator.RemoveStyle,
operator.SelectByStyle,
operator.UnlinkStyle,
operator.UpdateCurrentStyle,
operator.UpdateStyleColours,
operator.UpdateStyleTextures,
operator.ClearTextureMapPath,
prop.Style,
prop.BIMStylesProperties,
prop.BIMStyleProperties,
@@ -18,8 +18,8 @@
import bpy
import ifcopenshell
from ifcopenshell.util.doc import get_entity_doc
import blenderbim.tool as tool
from ifcopenshell.util.doc import get_entity_doc
def refresh():
@@ -33,9 +33,18 @@ class StylesData:
@classmethod
def load(cls):
cls.data = {"style_types": cls.style_types(), "total_styles": cls.total_styles()}
cls.data = {
"style_types": cls.style_types(),
"total_styles": cls.total_styles(),
"reflectance_methods": cls.reflectance_methods(),
}
cls.is_loaded = True
@classmethod
def reflectance_methods(cls):
declaration = tool.Ifc.schema().declaration_by_name("IfcReflectanceMethodEnum")
return [(i, i, "") for i in declaration.enumeration_items()]
@classmethod
def style_types(cls):
declaration = tool.Ifc.schema().declaration_by_name("IfcPresentationStyle")
@@ -16,15 +16,16 @@
# 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 blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.style as core
import ifcopenshell.util.representation
from pathlib import Path
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.style.data import StylesData, StyleAttributesData
from pathlib import Path
import os
class UpdateStyleColours(bpy.types.Operator, tool.Ifc.Operator):
@@ -99,9 +100,16 @@ class EnableEditingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_style"
bl_label = "Enable Editing Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty(default=0)
def _execute(self, context):
core.enable_editing_style(tool.Style, obj=context.active_object.active_material)
props = bpy.context.scene.BIMStylesProperties
style = tool.Ifc.get().by_id(self.style)
props.is_editing_style = style.id()
props.is_editing_class = "IfcSurfaceStyle"
attributes = props.attributes
attributes.clear()
blenderbim.bim.helper.import_attributes2(style, attributes)
class DisableEditingStyle(bpy.types.Operator, tool.Ifc.Operator):
@@ -110,7 +118,8 @@ class DisableEditingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Disable Editing Style"
def _execute(self, context):
core.disable_editing_style(tool.Style, obj=context.active_object.active_material)
props = bpy.context.scene.BIMStylesProperties
props.is_editing_style = 0
class EditStyle(bpy.types.Operator, tool.Ifc.Operator):
@@ -119,7 +128,12 @@ class EditStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.edit_style(tool.Ifc, tool.Style, obj=context.active_object.active_material)
props = bpy.context.scene.BIMStylesProperties
style = tool.Ifc.get().by_id(props.is_editing_style)
attributes = blenderbim.bim.helper.export_attributes(props.attributes)
ifcopenshell.api.run("style.edit_presentation_style", tool.Ifc.get(), style=style, attributes=attributes)
props.is_editing_style = 0
core.load_styles(tool.Style, style_type=props.style_type)
class UpdateCurrentStyle(bpy.types.Operator):
@@ -415,3 +429,240 @@ class ClearTextureMapPath(bpy.types.Operator):
props = context.material.BIMStyleProperties
setattr(props, self.texture_map_prop, "")
return {"FINISHED"}
class EnableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_adding_presentation_style"
bl_label = "Enable Add Presentation Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMStylesProperties
props.is_adding = True
class DisableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_adding_presentation_style"
bl_label = "Disable Add Presentation Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMStylesProperties
props.is_adding = False
class AddPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_presentation_style"
bl_label = "Add Presentation Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMStylesProperties
if props.style_type == "IfcSurfaceStyle":
style = ifcopenshell.api.run("style.add_style", tool.Ifc.get(), name=props.style_name)
if props.surface_style_class in ("IfcSurfaceStyleShading", "IfcSurfaceStyleRendering"):
surface_style = ifcopenshell.api.run(
"style.add_surface_style",
tool.Ifc.get(),
style=style,
ifc_class=props.surface_style_class,
attributes={
"SurfaceColour": {
"Name": None,
"Red": props.surface_colour[0],
"Green": props.surface_colour[1],
"Blue": props.surface_colour[2],
}
},
)
if props.surface_style_class == "IfcSurfaceStyleRendering":
surface_style.ReflectanceMethod = "NOTDEFINED"
material = bpy.data.materials.new(style.Name)
tool.Ifc.link(style, material)
material.use_fake_user = True
if surface_style.is_a("IfcSurfaceStyleShading"):
tool.Loader.create_surface_style_shading(material, surface_style)
elif surface_style.is_a("IfcSurfaceStyleRendering"):
tool.Loader.create_surface_style_rendering(material, surface_style)
props.is_adding = False
core.load_styles(tool.Style, style_type=props.style_type)
class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_surface_style"
bl_label = "Enable Editing Surface Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty(default=0)
ifc_class: bpy.props.StringProperty(default="")
def _execute(self, context):
props = bpy.context.scene.BIMStylesProperties
style = tool.Ifc.get().by_id(self.style)
shading = None
surface_style = None
for style2 in style.Styles:
if style2.is_a() == self.ifc_class:
surface_style = style2
if style2.is_a() == "IfcSurfaceStyleShading":
shading = style2
color_to_tuple = lambda x: (x.Red, x.Green, x.Blue)
if surface_style:
if self.ifc_class == "IfcSurfaceStyleShading":
props.surface_colour = color_to_tuple(surface_style.SurfaceColour)
props.transparency = surface_style.Transparency or 0.0
elif self.ifc_class == "IfcSurfaceStyleRendering":
props.surface_colour = color_to_tuple(surface_style.SurfaceColour)
props.transparency = surface_style.Transparency or 0.0
if surface_style.DiffuseColour:
props.is_diffuse_colour_null = False
if surface_style.DiffuseColour.is_a("IfcColourRgb"):
props.diffuse_colour_class = "IfcColourRgb"
props.diffuse_colour = color_to_tuple(surface_style.DiffuseColour)
else:
props.diffuse_colour_class = "IfcNormalisedRatioMeasure"
props.diffuse_colour_ratio = surface_style.DiffuseColour.wrappedValue
else:
props.is_diffuse_colour_null = False
if surface_style.SpecularColour:
props.is_specular_colour_null = False
if surface_style.SpecularColour.is_a("IfcColourRgb"):
props.specular_colour_class = "IfcColourRgb"
props.specular_colour = color_to_tuple(surface_style.SpecularColour)
else:
props.specular_colour_class = "IfcNormalisedRatioMeasure"
props.specular_colour_ratio = surface_style.SpecularColour.wrappedValue
else:
props.is_specular_colour_null = False
if surface_style.SpecularHighlight:
props.is_specular_highlight_null = False
if surface_style.SpecularHighlight.is_a("IfcSpecularRoughness"):
props.specular_highlight = surface_style.SpecularHighlight.wrappedValue
else:
props.is_specular_highlight_null = False # Exponent is meaningless
else:
props.is_specular_highlight_null = True
props.reflectance_method = surface_style.ReflectanceMethod
elif shading:
props.surface_colour = color_to_tuple(shading.SurfaceColour)
props.transparency = shading.Transparency or 0.0
props.is_editing_style = self.style
props.is_editing_class = self.ifc_class
class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_surface_style"
bl_label = "Edit Surface Style"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.props = bpy.context.scene.BIMStylesProperties
self.style = tool.Ifc.get().by_id(self.props.is_editing_style)
self.surface_style = None
self.shading_style = None
self.rendering_style = None
for style2 in self.style.Styles:
if style2.is_a() == self.props.is_editing_class:
self.surface_style = style2
if style2.is_a() == "IfcSurfaceStyleShading":
self.shading_style = style2
if style2.is_a() == "IfcSurfaceStyleRendering":
self.rendering_style = style2
if self.surface_style:
self.edit_existing_style()
else:
self.add_new_style()
self.props.is_editing_style = 0
core.load_styles(tool.Style, style_type=self.props.style_type)
def edit_existing_style(self):
material = tool.Ifc.get_object(self.style)
if self.surface_style.is_a() == "IfcSurfaceStyleShading":
ifcopenshell.api.run(
"style.edit_surface_style",
tool.Ifc.get(),
style=self.surface_style,
attributes={
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
"Transparency": self.props.transparency or None,
},
)
tool.Loader.create_surface_style_shading(material, self.surface_style)
elif self.surface_style.is_a() == "IfcSurfaceStyleRendering":
ifcopenshell.api.run(
"style.edit_surface_style",
tool.Ifc.get(),
style=self.surface_style,
attributes=self.get_rendering_attributes(),
)
tool.Loader.create_surface_style_rendering(material, self.surface_style)
def add_new_style(self):
material = tool.Ifc.get_object(self.style)
if self.props.is_editing_class == "IfcSurfaceStyleShading":
surface_style = ifcopenshell.api.run(
"style.add_surface_style",
tool.Ifc.get(),
style=self.style,
ifc_class="IfcSurfaceStyleShading",
attributes=self.get_shading_attributes(),
)
tool.Loader.create_surface_style_shading(material, surface_style)
elif self.props.is_editing_class == "IfcSurfaceStyleRendering":
surface_style = ifcopenshell.api.run(
"style.add_surface_style",
tool.Ifc.get(),
style=self.style,
ifc_class="IfcSurfaceStyleRendering",
attributes=self.get_rendering_attributes(),
)
tool.Loader.create_surface_style_rendering(material, surface_style)
def get_shading_attributes(self):
return {
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
"Transparency": self.props.transparency or None,
}
def get_rendering_attributes(self):
if self.props.is_diffuse_colour_null:
diffuse_colour = None
elif self.props.diffuse_colour_class == "IfcColourRgb":
diffuse_colour = self.color_to_dict(self.props.diffuse_colour)
elif self.props.diffuse_colour_class == "IfcNormalisedRatioMeasure":
diffuse_colour = self.props.diffuse_colour_ratio
if self.props.is_specular_colour_null:
specular_colour = None
elif self.props.specular_colour_class == "IfcColourRgb":
specular_colour = self.color_to_dict(self.props.specular_colour)
elif self.props.specular_colour_class == "IfcNormalisedRatioMeasure":
specular_colour = self.props.specular_colour_ratio
if self.props.is_specular_highlight_null:
specular_highlight = None
else:
specular_highlight = {"SpecularRoughness": self.props.specular_highlight}
return {
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
"Transparency": self.props.transparency or None,
"ReflectanceMethod": self.props.reflectance_method,
"DiffuseColour": diffuse_colour,
"SpecularColour": specular_colour,
"SpecularHighlight": specular_highlight,
}
def color_to_dict(self, x):
return {"Red": x[0], "Green": x[1], "Blue": x[2]}
@@ -40,10 +40,25 @@ def get_style_types(self, context):
return StylesData.data["style_types"]
def get_reflectance_methods(self, context):
if not StylesData.is_loaded:
StylesData.load()
return StylesData.data["reflectance_methods"]
class Style(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
total_elements: IntProperty(name="Total Elements")
style_classes: CollectionProperty(name="Style Classes", type=StrProperty)
has_surface_colour: BoolProperty(name="Has Surface Colour", default=False)
surface_colour: bpy.props.FloatVectorProperty(
name="Surface Colour", subtype="COLOR", default=(1, 1, 1), min=0.0, max=1.0, size=3
)
has_diffuse_colour: BoolProperty(name="Has Diffuse Colour", default=False)
diffuse_colour: bpy.props.FloatVectorProperty(
name="Diffuse Colour", subtype="COLOR", default=(1, 1, 1), min=0.0, max=1.0, size=3
)
STYLE_TYPES = [
@@ -60,8 +75,53 @@ def update_shading_styles(self, context):
class BIMStylesProperties(PropertyGroup):
is_adding: BoolProperty(name="Is Adding")
is_editing: BoolProperty(name="Is Editing")
style_type: EnumProperty(items=get_style_types, name="Style Type")
is_editing_style: IntProperty(name="Is Editing Style")
is_editing_class: StringProperty(name="Is Editing Class")
attributes: CollectionProperty(name="Attributes", type=Attribute)
style_type: EnumProperty(items=get_style_types, default=2, name="Style Type")
style_name: StringProperty(name="Style Name")
surface_style_class: EnumProperty(
items=[
(x, x, "")
for x in (
"IfcSurfaceStyleShading",
"IfcSurfaceStyleRendering",
"IfcSurfaceStyleWithTextures",
"IfcSurfaceStyleLighting",
"IfcSurfaceStyleRefraction",
"IfcExternallyDefinedSurfaceStyle",
)
],
name="Surface Style Class",
default="IfcSurfaceStyleShading",
)
surface_colour: bpy.props.FloatVectorProperty(
name="Surface Colour", subtype="COLOR", default=(1, 1, 1), min=0.0, max=1.0, size=3
)
transparency: bpy.props.FloatProperty(name="Transparency", default=0.0, min=0.0, max=1.0)
is_diffuse_colour_null: BoolProperty(name="Is Null")
diffuse_colour_class: EnumProperty(
items=[(x, x, "") for x in ("IfcColourRgb", "IfcNormalisedRatioMeasure")],
name="Diffuse Colour Class",
)
diffuse_colour: bpy.props.FloatVectorProperty(
name="Diffuse Colour", subtype="COLOR", default=(1, 1, 1), min=0.0, max=1.0, size=3
)
diffuse_colour_ratio: bpy.props.FloatProperty(name="Diffuse Ratio", default=0.0, min=0.0, max=1.0)
is_specular_colour_null: BoolProperty(name="Is Null")
specular_colour_class: EnumProperty(
items=[(x, x, "") for x in ("IfcColourRgb", "IfcNormalisedRatioMeasure")],
name="Specular Colour Class",
)
specular_colour: bpy.props.FloatVectorProperty(
name="Specular Colour", subtype="COLOR", default=(1, 1, 1), min=0.0, max=1.0, size=3
)
specular_colour_ratio: bpy.props.FloatProperty(name="Specular Ratio", default=0.0, min=0.0, max=1.0)
is_specular_highlight_null: BoolProperty(name="Is Null")
specular_highlight: bpy.props.FloatProperty(name="Specular Highlight", default=0.0, min=0.0, max=1.0)
reflectance_method: EnumProperty(name="Reflectance Method", items=get_reflectance_methods)
styles: CollectionProperty(name="Styles", type=Style)
active_style_index: IntProperty(name="Active Style Index")
active_style_type: EnumProperty(
+147 -13
View File
@@ -28,11 +28,11 @@ from blenderbim.tool.style import TEXTURE_MAPS_BY_METHODS, STYLE_TEXTURE_PROPS_M
class BIM_PT_styles(Panel):
bl_label = "Styles"
bl_idname = "BIM_PT_styles"
bl_options = {"DEFAULT_CLOSED"}
bl_options = {"HIDE_HEADER"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
bl_parent_id = "BIM_PT_tab_styles"
@classmethod
def poll(cls, context):
@@ -44,28 +44,146 @@ class BIM_PT_styles(Panel):
self.props = context.scene.BIMStylesProperties
row = self.layout.row(align=True)
row.label(text="{} Styles Found".format(StylesData.data["total_styles"]), icon="MATERIAL")
row = self.layout.row(align=True)
if self.props.is_editing:
row = self.layout.row(align=True)
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
if not self.props.is_adding:
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
else:
row = self.layout.row(align=True)
row.label(text="{} Styles".format(StylesData.data["total_styles"]), icon="SHADING_RENDERED")
blenderbim.bim.helper.prop_with_search(row, self.props, "style_type", text="")
row.operator("bim.load_styles", text="", icon="IMPORT").style_type = self.props.style_type
return
row = self.layout.row(align=True)
row.alignment = "RIGHT"
if self.props.is_adding:
box = self.layout.box()
row = box.row()
row.prop(self.props, "style_name", text="Name")
if self.props.style_type == "IfcSurfaceStyle":
row = box.row()
row.prop(self.props, "surface_style_class", text="Class")
if self.props.surface_style_class in ("IfcSurfaceStyleShading", "IfcSurfaceStyleRendering"):
row = box.row()
row.prop(self.props, "surface_colour", text="Colour")
row = box.row(align=True)
row.operator("bim.add_presentation_style", text="Save New Style", icon="CHECKMARK")
row.operator("bim.disable_adding_presentation_style", text="", icon="CANCEL")
# row.operator("bim.add_presentation_style", text="", icon="ADD")
if self.props.styles and self.props.active_style_index < len(self.props.styles):
row = self.layout.row(align=True)
style = self.props.styles[self.props.active_style_index]
op = row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF")
op = row.operator("bim.enable_editing_style", text="Edit Style", icon="GREASEPENCIL")
op.style = style.ifc_definition_id
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
if self.props.style_type == "IfcSurfaceStyle":
col = self.layout.column(align=True)
row = col.row(align=True)
op = row.operator("bim.enable_editing_surface_style", text="Shade", icon="SHADING_SOLID")
op.ifc_class = "IfcSurfaceStyleShading"
op.style = style.ifc_definition_id
op = row.operator("bim.enable_editing_surface_style", text="Render", icon="SHADING_RENDERED")
op.ifc_class = "IfcSurfaceStyleRendering"
op.style = style.ifc_definition_id
op = row.operator("bim.enable_editing_surface_style", text="Texture", icon="SHADING_TEXTURE")
op.ifc_class = "IfcSurfaceStyleWithTextures"
op.style = style.ifc_definition_id
row = col.row(align=True)
op = row.operator("bim.enable_editing_surface_style", text="Lighting", icon="LIGHT_SUN")
op.ifc_class = "IfcSurfaceStyleLighting"
op.style = style.ifc_definition_id
op = row.operator("bim.enable_editing_surface_style", text="Refract", icon="LIGHT_POINT")
op.ifc_class = "IfcSurfaceStyleRefraction"
op.style = style.ifc_definition_id
op = row.operator("bim.enable_editing_surface_style", text="External", icon="APPEND_BLEND")
op.ifc_class = "IfcExternallyDefinedSurfaceStyle"
op.style = style.ifc_definition_id
self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index")
if self.props.is_editing_style:
if self.props.is_editing_class == "IfcSurfaceStyle":
blenderbim.bim.helper.draw_attributes(self.props.attributes, self.layout)
row = self.layout.row(align=True)
row.operator("bim.edit_style", text="Save Attributes", icon="CHECKMARK")
row.operator("bim.disable_editing_style", text="", icon="CANCEL")
elif self.props.is_editing_class == "IfcSurfaceStyleShading":
self.draw_surface_style_shading()
elif self.props.is_editing_class == "IfcSurfaceStyleRendering":
self.draw_surface_style_rendering()
def draw_surface_style_shading(self):
row = self.layout.row()
row.prop(self.props, "surface_colour")
row = self.layout.row()
row.prop(self.props, "transparency")
row = self.layout.row(align=True)
row.operator("bim.edit_surface_style", text="Save Shading Style", icon="CHECKMARK")
row.operator("bim.disable_editing_style", text="", icon="CANCEL")
def draw_surface_style_rendering(self):
row = self.layout.row()
row.prop(self.props, "surface_colour")
row = self.layout.row()
row.prop(self.props, "transparency")
row = self.layout.row()
row.prop(self.props, "reflectance_method")
row = self.layout.row(align=True)
row.label(text="Diffuse")
row.prop(self.props, "diffuse_colour_class", text="")
if self.props.diffuse_colour_class == "IfcColourRgb":
row.prop(self.props, "diffuse_colour", text="")
else:
row.prop(self.props, "diffuse_colour_ratio", text="")
row.prop(
self.props,
"is_diffuse_colour_null",
text="",
icon="RADIOBUT_OFF" if self.props.is_diffuse_colour_null else "RADIOBUT_ON",
)
row = self.layout.row(align=True)
if self.props.reflectance_method in ("PHYSICAL", "NOTDEFINED"):
row.label(text="Metallic")
else:
row.label(text="Specular")
row.prop(self.props, "specular_colour_class", text="")
if self.props.specular_colour_class == "IfcColourRgb":
row.prop(self.props, "specular_colour", text="")
else:
row.prop(self.props, "specular_colour_ratio", text="")
row.prop(
self.props,
"is_specular_colour_null",
text="",
icon="RADIOBUT_OFF" if self.props.is_specular_colour_null else "RADIOBUT_ON",
)
row = self.layout.row(align=True)
if self.props.reflectance_method in ("PHYSICAL", "NOTDEFINED"):
row.label(text="Roughness")
elif self.props.reflectance_method in ("PHONG"):
row.label(text="Shininess")
else:
row.label(text="Highlight")
row.prop(self.props, "specular_highlight", text="")
row.prop(
self.props,
"is_specular_highlight_null",
text="",
icon="RADIOBUT_OFF" if self.props.is_specular_highlight_null else "RADIOBUT_ON",
)
row = self.layout.row(align=True)
row.operator("bim.edit_surface_style", text="Save Rendering Style", icon="CHECKMARK")
row.operator("bim.disable_editing_style", text="", icon="CANCEL")
def draw_style_ui(self, context):
mat = context.material
@@ -145,9 +263,6 @@ class BIM_PT_style_attributes(Panel):
row.operator("bim.disable_editing_style", icon="CANCEL", text="")
blenderbim.bim.helper.draw_attributes(props.attributes, self.layout)
else:
row = self.layout.row()
row.operator("bim.enable_editing_style", icon="GREASEPENCIL", text="Edit")
row = self.layout.row(align=True)
row.label(text="STEP ID")
row.label(text=str(mprops.ifc_style_id))
@@ -218,9 +333,28 @@ class BIM_UL_styles(UIList):
if item:
row = layout.row(align=True)
row.label(text=item.name)
if item.has_surface_colour:
row = row.row(align=True)
row.enabled = False
row.prop(item, "surface_colour", text="")
if item.has_diffuse_colour:
row.prop(item, "diffuse_colour", text="")
row2 = row.row()
row2.alignment = "RIGHT"
row2.label(text=str(item.total_elements))
for style in item.style_classes:
if style.name == "IfcSurfaceStyleShading":
row2.label(text="", icon="SHADING_SOLID")
elif style.name == "IfcSurfaceStyleRendering":
row2.label(text="", icon="SHADING_RENDERED")
elif style.name == "IfcSurfaceStyleWithTextures":
row2.label(text="", icon="SHADING_TEXTURE")
elif style.name == "IfcSurfaceStyleLighting":
row2.label(text="", icon="LIGHT_SUN")
elif style.name == "IfcSurfaceStyleRefraction":
row2.label(text="", icon="LIGHT_POINT")
elif style.name == "IfcExternallyDefinedSurfaceStyle":
row2.label(text="", icon="APPEND_BLEND")
class BIM_PT_STYLE_GRAPH(Panel):
@@ -273,7 +407,7 @@ class BIM_PT_STYLE_GRAPH(Panel):
row.prop(props, path_name)
op = row.operator("bim.clear_texture_map_path", text="", icon="X")
op.texture_map_prop = path_name
layout.separator()
texture_maps = TEXTURE_MAPS_BY_METHODS.get(props.reflectance_method, [])
if not texture_maps:
@@ -22,38 +22,52 @@ from . import ui, prop, operator, decorator
classes = (
operator.AddPort,
operator.AddSystem,
operator.AddZone,
operator.AssignSystem,
operator.AssignUnassignFlowControl,
operator.ConnectPort,
operator.DisableEditingSystem,
operator.DisableEditingZone,
operator.DisableSystemEditingUI,
operator.DisconnectPort,
operator.EditSystem,
operator.EditZone,
operator.EnableEditingSystem,
operator.EnableEditingZone,
operator.HidePorts,
operator.LoadSystems,
operator.LoadZones,
operator.MEPConnectElements,
operator.RemovePort,
operator.RemoveSystem,
operator.RemoveZone,
operator.SelectSystemProducts,
operator.MEPConnectElements,
operator.SetFlowDirection,
operator.ShowPorts,
operator.UnassignSystem,
operator.UnloadZones,
prop.System,
prop.Zone,
prop.BIMSystemProperties,
prop.BIMZoneProperties,
ui.BIM_PT_systems,
ui.BIM_PT_object_systems,
ui.BIM_PT_zones,
ui.BIM_PT_active_object_zones,
ui.BIM_PT_ports,
ui.BIM_PT_port,
ui.BIM_PT_flow_controls,
ui.BIM_UL_systems,
ui.BIM_UL_object_systems,
ui.BIM_UL_zones,
)
def register():
bpy.types.Scene.BIMSystemProperties = bpy.props.PointerProperty(type=prop.BIMSystemProperties)
bpy.types.Scene.BIMZoneProperties = bpy.props.PointerProperty(type=prop.BIMZoneProperties)
bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load)
def unregister():
del bpy.types.Scene.BIMSystemProperties
del bpy.types.Scene.BIMZoneProperties
bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load)
@@ -25,6 +25,8 @@ import blenderbim.tool as tool
def refresh():
SystemData.is_loaded = False
ZonesData.is_loaded = False
ActiveObjectZonesData.is_loaded = False
ObjectSystemData.is_loaded = False
PortData.is_loaded = False
SystemDecorationData.is_loaded = False
@@ -47,6 +49,7 @@ class SystemData:
declaration = tool.Ifc.schema().declaration_by_name("IfcSystem")
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
version = tool.Ifc.get_schema()
# We're only interested in systems for services. Not sure why IFC groups these together.
return [
(c, c, get_entity_doc(version, c).get("description", ""))
@@ -56,7 +59,7 @@ class SystemData:
@classmethod
def total_systems(cls):
return len(tool.Ifc.get().by_type("IfcSystem"))
return len(tool.System.get_systems())
class ObjectSystemData:
@@ -67,9 +70,9 @@ class ObjectSystemData:
def load(cls):
cls.data = {
"systems": cls.systems(),
"total_systems": cls.total_systems(),
# AFTER SYSTEMS
"connected_elements": cls.connected_elements(),
"flow_controls_data": cls.flow_controls_data(),
}
cls.is_loaded = True
@@ -83,16 +86,32 @@ class ObjectSystemData:
results.append({"id": system.id(), "name": system.Name or "Unnamed", "ifc_class": system.is_a()})
return results
@classmethod
def total_systems(cls):
return len(tool.Ifc.get().by_type("IfcSystem"))
@classmethod
def connected_elements(cls):
if not cls.element:
return set()
return tool.System.get_connected_elements(cls.element)
@classmethod
def flow_controls_data(cls):
flow_controls_data = {}
if not cls.element or not (
cls.element.is_a("IfcDistributionControlElement") or cls.element.is_a("IfcDistributionFlowElement")
):
return flow_controls_data
if cls.element.is_a("IfcDistributionControlElement"):
flow_controls_data["type"] = "IfcDistributionControlElement"
flow_element = tool.System.get_flow_control_flow_element(cls.element)
flow_element_obj = tool.Ifc.get_object(flow_element).name if flow_element else None
flow_controls_data["flow_element"] = flow_element, flow_element_obj
else:
flow_controls_data["type"] = "IfcDistributionFlowElement"
controls = [(c, tool.Ifc.get_object(c).name) for c in tool.System.get_flow_element_controls(cls.element)]
flow_controls_data["controls"] = controls
return flow_controls_data
class PortData:
data = {}
@@ -107,8 +126,8 @@ class PortData:
"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,
"port_connected_object_name": cls.port_connected_object_name() if is_port else None,
"port_relating_object_name": cls.port_relating_object_name() if is_port else None,
}
# AFTER located_ports_data
cls.data["selected_objects_flow_direction"] = cls.selected_objects_flow_direction() if not is_port else None
@@ -123,16 +142,16 @@ class PortData:
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))
def port_relating_object_name(cls):
return tool.Ifc.get_object(tool.System.get_port_relating_element(cls.element)).name
@classmethod
def port_connected_object(cls):
def port_connected_object_name(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)
return tool.Ifc.get_object(connected_element).name
@classmethod
def located_ports_data(cls):
@@ -140,19 +159,23 @@ class PortData:
data = []
for port in ports:
port_obj = tool.Ifc.get_object(port)
# port may be not present as a scene object
port_obj_name = getattr(tool.Ifc.get_object(port), "name", None)
connected_port = tool.System.get_connected_port(port)
if connected_port:
connected_obj = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port))
connected_obj_name = tool.Ifc.get_object(tool.System.get_port_relating_element(connected_port)).name
else:
connected_obj = None
connected_obj_name = None
data.append((port, port_obj, connected_obj))
data.append((port, port_obj_name, connected_obj_name))
return data
@classmethod
def selected_objects_flow_direction(cls):
for port, port_obj, connected_obj in cls.data["located_ports_data"]:
for port, _, connected_obj_name in cls.data["located_ports_data"]:
if connected_obj_name is None:
continue
connected_obj = bpy.data.objects[connected_obj_name]
if connected_obj in bpy.context.selected_objects:
return port.FlowDirection
@@ -164,7 +187,9 @@ class SystemDecorationData:
@classmethod
def load(cls):
cls.data = {}
cls.data = {
"decorated_elements": cls.decorated_elements(),
}
cls.is_loaded = True
cls.elements_ports_positions = {}
@@ -190,3 +215,55 @@ class SystemDecorationData:
ports_data.append(port_data)
cls.elements_ports_positions[element] = ports_data
return cls.elements_ports_positions[element]
@classmethod
def decorated_elements(cls):
if not ObjectSystemData.is_loaded:
ObjectSystemData.load()
# Priority:
# 1. currently selected systems
# 2. active system
# 3. if previous steps didn't worked - decorate connected elements
decorated_elements = set()
if ObjectSystemData.data["systems"]:
for system in ObjectSystemData.data["systems"]:
system = tool.Ifc.get().by_id(system["id"])
decorated_elements.update(ifcopenshell.util.system.get_system_elements(system))
elif active_system := tool.System.get_active_system():
decorated_elements = set(ifcopenshell.util.system.get_system_elements(active_system))
if not decorated_elements:
decorated_elements.update(ObjectSystemData.data["connected_elements"])
return decorated_elements
class ZonesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"total_zones": cls.total_zones()}
cls.is_loaded = True
@classmethod
def total_zones(cls):
return len(tool.Ifc.get().by_type("IfcZone"))
class ActiveObjectZonesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"zones": cls.zones()}
cls.is_loaded = True
@classmethod
def zones(cls):
systems = ifcopenshell.util.system.get_element_systems(tool.Ifc.get_entity(bpy.context.active_object))
return [s.Name or "Unnamed" for s in systems if s.is_a("IfcZone")]
@@ -122,15 +122,15 @@ class SystemDecorator:
selected_vertices = decoration_data["selected_vertices"]
### Actually drawing
# 3D_POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)

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