Merge branch 'v0.8.0' into ifcmax/initial-refresh

This commit is contained in:
Josef Wienerroither
2026-01-23 07:24:49 +01:00
42 changed files with 1443 additions and 922 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ dependencies = [
"black==25.12", "black==25.12",
"ruff==0.14.13", "ruff==0.14.13",
"poethepoet", "poethepoet",
"gersemi==0.24", "gersemi==0.25.1",
] ]
[tool.black] [tool.black]
+20 -27
View File
@@ -511,14 +511,15 @@ def draw_filter(
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE" row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE"
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE" row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
row = layout.row(align=True) row = layout.row(align=True)
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations: preferences = tool.Blender.get_addon_preferences()
if not preferences.chain_filter_with_set_operations:
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
else: else:
if not filter_groups or not any(fg.filters for fg in filter_groups): row.prop(sprops, "facet", text="")
op = row.operator("bim.add_filter", text="Add Filter", icon="ADD") op = row.operator("bim.add_filter", text="Add Filter", icon="ADD")
op.type = "entity" op.type = sprops.facet
op.index = 0 op.index = 0
op.module = module op.module = module
op = row.operator("bim.edit_filter_query", text="", icon="FILTER") op = row.operator("bim.edit_filter_query", text="", icon="FILTER")
if "module" in op.bl_rna.properties: if "module" in op.bl_rna.properties:
op.module = module op.module = module
@@ -526,26 +527,23 @@ def draw_filter(
for i, filter_group in enumerate(filter_groups): for i, filter_group in enumerate(filter_groups):
box = layout.box() box = layout.box()
row = box.row(align=True) preferences = tool.Blender.get_addon_preferences()
row.prop(sprops, "facet", text="") if not preferences.chain_filter_with_set_operations:
op = row.operator("bim.add_filter", text="Add Filter", icon="ADD") row = box.row(align=True)
op.type = sprops.facet row.prop(sprops, "facet", text="")
op.index = i op = row.operator("bim.add_filter", text="Add Filter", icon="ADD")
op.module = module op.type = sprops.facet
op = row.operator("bim.remove_filter_group", text="", icon="X") op.index = i
op.index = i op.module = module
op.module = module op = row.operator("bim.remove_filter_group", text="", icon="X")
op.index = i
op.module = module
for j, ifc_filter in enumerate(filter_group.filters): for j, ifc_filter in enumerate(filter_group.filters):
if ifc_filter.type == "entity": if ifc_filter.type == "entity":
row = box.row(align=True) row = box.row(align=True)
preferences = tool.Blender.get_addon_preferences() preferences = tool.Blender.get_addon_preferences()
if preferences.chain_filter_with_set_operations: show_mode_toggle = preferences.chain_filter_with_set_operations and j > 0
show_mode_toggle = j > 0
else:
show_mode_toggle = (
preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0
) # PR 7315 mode
if show_mode_toggle: if show_mode_toggle:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator( op = row.operator(
@@ -761,12 +759,7 @@ def draw_filter(
elif ifc_filter.type == "instance": elif ifc_filter.type == "instance":
row = box.row(align=True) row = box.row(align=True)
preferences = tool.Blender.get_addon_preferences() preferences = tool.Blender.get_addon_preferences()
if preferences.chain_filter_with_set_operations: show_mode_toggle = preferences.chain_filter_with_set_operations and j > 0
show_mode_toggle = j > 0
else:
show_mode_toggle = (
preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0
) # PR 7315 mode
if show_mode_toggle: if show_mode_toggle:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator( op = row.operator(
+1 -10
View File
@@ -290,7 +290,6 @@ class IfcImporter:
self.profile_code("Load linked models") self.profile_code("Load linked models")
self.add_project_to_scene() self.add_project_to_scene()
self.profile_code("Add project to scene") self.profile_code("Add project to scene")
self.hide_ifc_spaces()
if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000: if self.ifc_import_settings.should_clean_mesh and len(self.file.by_type("IfcElement")) < 1000:
self.clean_mesh() self.clean_mesh()
self.profile_code("Mesh cleaning") self.profile_code("Mesh cleaning")
@@ -1287,14 +1286,6 @@ class IfcImporter:
properties={"Aggregate_Index": aggregate_index, "Name": name}, properties={"Aggregate_Index": aggregate_index, "Name": name},
) )
def hide_ifc_spaces(self):
"""Hide IfcSpace objects after they've been added to the scene."""
for ifc_definition_id, obj in self.added_data.items():
if isinstance(obj, bpy.types.Object):
element = self.file.by_id(ifc_definition_id)
if element.is_a("IfcSpace"):
obj.hide_set(True)
class IfcImportSettings: class IfcImportSettings:
""" """
@@ -1312,7 +1303,7 @@ class IfcImportSettings:
self.should_load_geometry = True self.should_load_geometry = True
self.should_clean_mesh = False self.should_clean_mesh = False
self.should_cache = True self.should_cache = True
self.deflection_tolerance = 0.001 self.deflection_tolerance = 0.05 # Default is 0.001, but I find this to be more practical
self.angular_tolerance = 0.5 self.angular_tolerance = 0.5
self.void_limit = 30 self.void_limit = 30
self.style_limit = 300 self.style_limit = 300
@@ -74,7 +74,7 @@ class ItemDecorator:
special_verts = [] special_verts = []
special_edges = [] special_edges = []
if len(obj.data.loop_triangles) > 0: if (total_triangles := len(obj.data.loop_triangles)) > 0:
verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices] verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]
tris = [tuple(t.vertices) for t in obj.data.loop_triangles] tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
@@ -140,7 +140,6 @@ classes = (
prop.BIMRailingProperties, prop.BIMRailingProperties,
prop.BIMRoofProperties, prop.BIMRoofProperties,
prop.BIMPolylineProperties, prop.BIMPolylineProperties,
prop.BIMProductPreviewProperties,
prop.BIMExternalParametricGeometryProperties, prop.BIMExternalParametricGeometryProperties,
ui.BIM_PT_array, ui.BIM_PT_array,
ui.BIM_PT_stair, ui.BIM_PT_stair,
@@ -263,7 +262,6 @@ def register():
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties) bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties) bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
bpy.types.Scene.BIMProductPreviewProperties = bpy.props.PointerProperty(type=prop.BIMProductPreviewProperties)
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties) bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties) bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties) bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
@@ -288,7 +286,6 @@ def unregister():
del bpy.types.Scene.BIMModelProperties del bpy.types.Scene.BIMModelProperties
del bpy.types.Scene.BIMPolylineProperties del bpy.types.Scene.BIMPolylineProperties
del bpy.types.Scene.BIMProductPreviewProperties
del bpy.types.Object.BIMArrayProperties del bpy.types.Object.BIMArrayProperties
del bpy.types.Object.BIMStairProperties del bpy.types.Object.BIMStairProperties
del bpy.types.Object.BIMSverchokProperties del bpy.types.Object.BIMSverchokProperties
@@ -52,6 +52,11 @@ class AuthoringData:
cls.is_loaded = True cls.is_loaded = True
cls.props = tool.Model.get_model_props() cls.props = tool.Model.get_model_props()
cls.data["default_container"] = cls.default_container() cls.data["default_container"] = cls.default_container()
if tool.Ifc.get().schema == "IFC2X3":
if ifc_element_type == "IfcDoorType":
ifc_element_type = "IfcDoorStyle"
elif ifc_element_type == "IfcWindowType":
ifc_element_type = "IfcWindowStyle"
cls.data["ifc_element_type"] = ifc_element_type cls.data["ifc_element_type"] = ifc_element_type
cls.data["ifc_classes"] = cls.ifc_classes() cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["ifc_class_current"] = cls.ifc_class_current() cls.data["ifc_class_current"] = cls.ifc_class_current()
+681 -59
View File
@@ -26,16 +26,16 @@ import ifcopenshell
import bonsai.tool as tool import bonsai.tool as tool
import math import math
import mathutils import mathutils
from math import sin, cos, radians from math import sin, cos, tan, radians
from bpy.types import SpaceView3D from bpy.types import SpaceView3D
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from mathutils import Vector, Matrix from mathutils import Vector, Matrix, Quaternion
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_circle_2d from gpu_extras.presets import draw_circle_2d
from typing import Union from typing import Union
from bonsai.bim.module.drawing.helper import format_distance from bonsai.bim.module.drawing.helper import format_distance
from itertools import chain from itertools import chain
from typing import Union, Any from typing import Union, Any, Literal
from bpy_extras.view3d_utils import location_3d_to_region_2d from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -521,6 +521,9 @@ class PolylineDecorator:
color = self.addon_prefs.decorations_colour color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color) blf.color(self.font_id, *color)
screen_coords = {}
for i in range(len(self.polyline_points)): for i in range(len(self.polyline_points)):
if i < 1 and self.measure_type == "POLY_AREA": if i < 1 and self.measure_type == "POLY_AREA":
continue continue
@@ -528,40 +531,32 @@ class PolylineDecorator:
continue continue
dim_text_pos = (Vector(self.polyline_points[i].position) + Vector(self.polyline_points[i - 1].position)) / 2 dim_text_pos = (Vector(self.polyline_points[i].position) + Vector(self.polyline_points[i - 1].position)) / 2
dim_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, dim_text_pos) dim_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, dim_text_pos)
if dim_text_coords:
formatted_value = self.polyline_points[i].dim formatted_value = self.polyline_points[i].dim
text = "d: " + formatted_value
blf.position(self.font_id, dim_text_coords[0], dim_text_coords[1], 0) screen_coords[f"distance_{i}"] = (Vector(dim_text_coords), text)
text = "d: " + formatted_value
text_length = blf.dimensions(self.font_id, text)
self.draw_text_background(context, dim_text_coords, text_length)
blf.draw(self.font_id, text)
if i == 1: if i == 1:
continue continue
angle_text_pos = Vector(self.polyline_points[i - 1].position) angle_text_pos = Vector(self.polyline_points[i - 1].position)
angle_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, angle_text_pos) angle_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, angle_text_pos)
blf.position(self.font_id, angle_text_coords[0], angle_text_coords[1], 0) if angle_text_coords:
text = "a: " + self.polyline_points[i].angle text = "a: " + self.polyline_points[i].angle
text_length = blf.dimensions(self.font_id, text) screen_coords[f"angle_{i}"] = (Vector(angle_text_coords), text)
self.draw_text_background(context, angle_text_coords, text_length)
blf.draw(self.font_id, text)
if self.measure_type == "SINGLE": if self.measure_type == "SINGLE":
axis_line, axis_line_center = self.calculate_measurement_x_y_and_z(context) axis_line, axis_line_center = self.calculate_measurement_x_y_and_z(context)
for i, dim_text_pos in enumerate(axis_line_center): for i, dim_text_pos in enumerate(axis_line_center):
dim_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, dim_text_pos) dim_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, dim_text_pos)
pos = blf.position(self.font_id, dim_text_coords[0], dim_text_coords[1], 0) if dim_text_coords:
value = round((axis_line[i][1] - axis_line[i][0]).length, 4) value = round((axis_line[i][1] - axis_line[i][0]).length, 4)
direction = axis_line[i][1] - axis_line[i][0] direction = axis_line[i][1] - axis_line[i][0]
if (i == 0 and direction.x < 0) or (i == 1 and direction.y < 0) or (i == 2 and direction.z < 0): if (i == 0 and direction.x < 0) or (i == 1 and direction.y < 0) or (i == 2 and direction.z < 0):
value = -value value = -value
prefix = "xyz"[i] prefix = "xyz"[i]
formatted_value = tool.Polyline.format_input_ui_units(value) formatted_value = tool.Polyline.format_input_ui_units(value)
text = f"{prefix}: {formatted_value}" text = f"{prefix}: {formatted_value}"
text_length = blf.dimensions(self.font_id, text) screen_coords[f"xyz_{i}"] = (Vector(dim_text_coords), text)
self.draw_text_background(context, dim_text_coords, text_length)
blf.draw(self.font_id, text)
# Area and Length text # Area and Length text
polyline_verts = [Vector((p.x, p.y, p.z)) for p in self.polyline_points] polyline_verts = [Vector((p.x, p.y, p.z)) for p in self.polyline_points]
@@ -569,35 +564,107 @@ class PolylineDecorator:
# Area # Area
if self.measure_type == "POLY_AREA" and self.polyline_data.area: if self.measure_type == "POLY_AREA" and self.polyline_data.area:
if len(polyline_verts) < 3: if len(polyline_verts) < 3:
blf.disable(self.font_id, blf.SHADOW)
return return
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points center = sum(polyline_verts, Vector()) / len(polyline_verts)
if polyline_verts[0] == polyline_verts[-1]: if polyline_verts[0] == polyline_verts[-1]:
center = sum(polyline_verts[:-1], Vector()) / len( center = sum(polyline_verts[:-1], Vector()) / len(polyline_verts[:-1])
polyline_verts[:-1]
) # Doesn't use the last point if is a closed polyline
area_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, center) area_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, center)
value = self.polyline_data.area if area_text_coords:
text = f"area: {value}" value = self.polyline_data.area
text_length = blf.dimensions(self.font_id, text) text = f"area: {value}"
area_text_coords[0] -= text_length[0] / 2 # Center text horizontally text_length = blf.dimensions(self.font_id, text)
blf.position(self.font_id, area_text_coords[0], area_text_coords[1], 0) area_text_coords = list(area_text_coords)
self.draw_text_background(context, area_text_coords, text_length) area_text_coords[0] -= text_length[0] / 2
blf.draw(self.font_id, text) screen_coords["area"] = (Vector(area_text_coords), text)
# Length # Length
if self.measure_type in {"POLYLINE", "POLY_AREA"}: if self.measure_type in {"POLYLINE", "POLY_AREA"}:
if len(polyline_verts) < 3: if len(polyline_verts) < 3:
blf.disable(self.font_id, blf.SHADOW)
return return
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1]) total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
blf.position(self.font_id, total_length_text_coords[0], total_length_text_coords[1], 0) if total_length_text_coords:
value = self.polyline_data.total_length value = self.polyline_data.total_length
text = f"length: {value}" text = f"length: {value}"
screen_coords["length"] = (Vector(total_length_text_coords), text)
self.adjust_overlapping_labels(screen_coords)
for label_key, (screen_co, text) in screen_coords.items():
blf.position(self.font_id, screen_co.x, screen_co.y, 0)
blf.color(self.font_id, 1, 1, 1, 1)
text_length = blf.dimensions(self.font_id, text) text_length = blf.dimensions(self.font_id, text)
self.draw_text_background(context, total_length_text_coords, text_length) self.draw_text_background(context, screen_co, text_length)
blf.draw(self.font_id, text) blf.draw(self.font_id, text)
blf.disable(self.font_id, blf.SHADOW) blf.disable(self.font_id, blf.SHADOW)
def adjust_overlapping_labels(self, screen_coords):
font_id = self.font_id
text_dimensions = {}
for label_key, (screen_co, text) in screen_coords.items():
text_dimensions[label_key] = blf.dimensions(font_id, text)
if text_dimensions:
first_height = next(iter(text_dimensions.values()))[1]
min_spacing = max(2, first_height * 0.3)
else:
min_spacing = 2
label_keys = list(screen_coords.keys())
for pass_num in range(3): # 3 passes to try to optimize complex overlaps
for i in range(len(label_keys)):
for j in range(i + 1, len(label_keys)):
key1, key2 = label_keys[i], label_keys[j]
co1, _ = screen_coords[key1]
co2, _ = screen_coords[key2]
dim1 = text_dimensions[key1]
dim2 = text_dimensions[key2]
bounds1 = {
"left": co1.x - min_spacing,
"right": co1.x + dim1[0] + min_spacing,
"top": co1.y + dim1[1] + min_spacing,
"bottom": co1.y - min_spacing,
}
bounds2 = {
"left": co2.x - min_spacing,
"right": co2.x + dim2[0] + min_spacing,
"top": co2.y + dim2[1] + min_spacing,
"bottom": co2.y - min_spacing,
}
if (
bounds1["left"] < bounds2["right"]
and bounds1["right"] > bounds2["left"]
and bounds1["bottom"] < bounds2["top"]
and bounds1["top"] > bounds2["bottom"]
):
x_overlap = min(bounds1["right"], bounds2["right"]) - max(bounds1["left"], bounds2["left"])
y_overlap = min(bounds1["top"], bounds2["top"]) - max(bounds1["bottom"], bounds2["bottom"])
separation_multiplier = 1.25
# Move labels in the direction requiring less movement
if x_overlap < y_overlap:
separation_distance = (x_overlap / 2 + min_spacing) * separation_multiplier
if co1.x < co2.x:
co1.x -= separation_distance
co2.x += separation_distance
else:
co1.x += separation_distance
co2.x -= separation_distance
else:
separation_distance = (y_overlap / 2 + min_spacing) * separation_multiplier
if co1.y < co2.y:
co1.y -= separation_distance
co2.y += separation_distance
else:
co1.y += separation_distance
co2.y -= separation_distance
def draw_measurements_poly(self, context): def draw_measurements_poly(self, context):
self.shader_config(context) self.shader_config(context)
polyline_verts: list[Vector] = [] polyline_verts: list[Vector] = []
@@ -879,13 +946,43 @@ class PolylineDecorator:
class ProductDecorator: class ProductDecorator:
is_installed = False is_installed = False
handlers = [] handlers = []
preview_mode: Literal["PROFILE_VERTICAL", "PROFILE_HORIZONTAL", "LAYER2", "LAYER3", "GENERIC"]
relating_type = None relating_type = None
obj_data: dict[str, list] = {}
obj_matrix_i = None
@classmethod @classmethod
def install(cls, context): def install(cls, context):
from bonsai.bim.module.geometry.decorator import ItemDecorator
if cls.is_installed: if cls.is_installed:
cls.uninstall() cls.uninstall()
props = tool.Model.get_model_props()
handler = cls() handler = cls()
if (
(props.relating_type_id)
and (relating_type := tool.Ifc.get().by_id(int(props.relating_type_id)))
and (relating_type_obj := tool.Ifc.get_object(relating_type))
):
handler.relating_type = relating_type
if tool.Model.get_usage_type(relating_type) == "PROFILE":
if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}:
handler.preview_mode = "PROFILE_VERTICAL"
else:
handler.preview_mode = "PROFILE_HORIZONTAL"
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
handler.preview_mode = "LAYER2"
elif tool.Model.get_usage_type(relating_type) == "LAYER3":
handler.preview_mode = "LAYER3"
else:
handler.preview_mode = "GENERIC"
if relating_type_obj.data:
handler.obj_data = ItemDecorator.get_obj_data(relating_type_obj)
handler.obj_data["raw_verts"] = [Vector(v) for v in handler.obj_data["verts"]]
handler.obj_matrix_i = relating_type_obj.matrix_world.inverted()
cls.handlers.append( cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_product_preview, (context,), "WINDOW", "POST_VIEW") SpaceView3D.draw_handler_add(handler.draw_product_preview, (context,), "WINDOW", "POST_VIEW")
) )
@@ -893,10 +990,6 @@ class ProductDecorator:
@classmethod @classmethod
def uninstall(cls): def uninstall(cls):
props = tool.Model.get_product_preview_props() # updated by model/polyline.py
props.verts.clear()
props.edges.clear()
props.tris.clear()
for handler in cls.handlers: for handler in cls.handlers:
try: try:
SpaceView3D.draw_handler_remove(handler, "WINDOW") SpaceView3D.draw_handler_remove(handler, "WINDOW")
@@ -912,14 +1005,6 @@ class ProductDecorator:
shader.uniform_float("color", color) shader.uniform_float("color", color)
batch.draw(shader) batch.draw(shader)
def get_product_preview_data(self, context) -> dict[str, Any]:
props = tool.Model.get_product_preview_props()
data: dict[str, Any] = {}
data["verts"] = [(*v.value_3d,) for v in props.verts]
data["edges"] = [(int(e.value_2d[0]), int(e.value_2d[1])) for e in props.edges]
data["tris"] = [(int(t.value_3d[0]), int(t.value_3d[1]), int(t.value_3d[2])) for t in props.tris]
return data
def draw_product_preview(self, context): def draw_product_preview(self, context):
def transparent_color(color, alpha=0.1): def transparent_color(color, alpha=0.1):
color = [i for i in color] color = [i for i in color]
@@ -945,12 +1030,549 @@ class ProductDecorator:
else: else:
return return
product_preview_data = self.get_product_preview_data(context) if self.preview_mode == "LAYER2":
if product_preview_data: data = self.get_wall_preview_data()
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"]) elif self.preview_mode == "LAYER3":
self.draw_batch( data = self.get_slab_preview_data()
"TRIS", product_preview_data["verts"], transparent_color(decorator_color), product_preview_data["tris"] elif self.preview_mode == "PROFILE_VERTICAL":
data = self.get_vertical_profile_preview_data()
elif self.preview_mode == "PROFILE_HORIZONTAL":
data = self.get_horizontal_profile_preview_data()
elif self.preview_mode == "GENERIC":
data = self.get_generic_preview_data()
if data:
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color), data["tris"])
def get_wall_preview_data(self):
relating_type = self.relating_type
# Get properties from object type
model_props = tool.Model.get_model_props()
direction_sense = model_props.direction_sense
direction = 1
if direction_sense == "NEGATIVE":
direction = -1
layers = tool.Model.get_material_layer_parameters(relating_type)
if not layers["thickness"]:
return
thickness = layers["thickness"]
thickness *= direction
offset_type = model_props.offset_type_vertical
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
offset = model_props.offset * unit_scale
height = float(model_props.extrusion_depth)
rl = float(model_props.rl1)
x_angle = float(model_props.x_angle)
if x_angle > radians(90) or x_angle < radians(-90):
height *= -1
angle_distance = height * tan(x_angle)
thickness *= 1 / cos(x_angle)
data = {}
data["verts"] = []
# Verts
polyline_vertices = []
polyline_props = tool.Model.get_polyline_props()
polyline_data = polyline_props.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
data = []
return
for point in polyline_points:
polyline_vertices.append(Vector((point.x, point.y, point.z)))
is_closed = False
if (
polyline_vertices[0].x == polyline_vertices[-1].x
and polyline_vertices[0].y == polyline_vertices[-1].y
and polyline_vertices[0].z == polyline_vertices[-1].z
):
is_closed = True
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
bm_base = tool.Model.create_bmesh_from_vertices(polyline_vertices, is_closed)
base_vertices = tool.Cad.offset_edges(bm_base, offset)
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
top_vertices = tool.Cad.offset_edges(bm_base, angle_distance + offset)
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distance + thickness + offset)
if is_closed:
base_vertices.append(base_vertices[0])
offset_base_verts.append(offset_base_verts[0])
top_vertices.append(top_vertices[0])
offset_top_verts.append(offset_top_verts[0])
if offset_base_verts is not None:
for v in base_vertices:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in offset_base_verts[::-1]:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in top_vertices:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
for v in offset_top_verts[::-1]:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
bm_base.free()
# Edges and Tris
points = []
side_edges_1 = []
side_edges_2 = []
base_edges = []
for i in range(len(data["verts"])):
points.append(Vector(data["verts"][i]))
n = len(points) // 2
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
side_edges_1.extend(bottom_side_1)
side_edges_2.extend(bottom_side_2)
base_edges.extend(bottom_loop)
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
side_edges_1.extend(upper_side_1)
side_edges_2.extend(upper_side_2)
base_edges.extend(upper_loop)
loops = [side_edges_1, side_edges_2, base_edges]
data["edges"] = []
data["tris"] = []
for i, group in enumerate(loops):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
bm.verts.index_update()
bm.edges.index_update()
if i == 2:
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
bm.verts.index_update()
bm.edges.index_update()
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
data["edges"].extend(edges)
data["tris"].extend(tris)
data["edges"] = list(set(tuple(e) for e in data["edges"]))
data["tris"] = list(set(tuple(t) for t in data["tris"]))
return data
def get_slab_preview_data(self):
relating_type = self.relating_type
model_props = tool.Model.get_model_props()
x_angle = 0 if tool.Cad.is_x(model_props.x_angle, 0, tolerance=0.001) else model_props.x_angle
direction_sense = model_props.direction_sense
direction = 1
if direction_sense == "NEGATIVE":
direction = -1
layers = tool.Model.get_material_layer_parameters(relating_type)
if not layers["thickness"]:
return
thickness = layers["thickness"] * abs(1 / cos(x_angle))
thickness *= direction
offset_type = model_props.offset_type_horizontal
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale
data = {}
data["verts"] = []
# Verts
polyline_vertices = []
polyline_props = tool.Model.get_polyline_props()
polyline_data = polyline_props.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 3:
data = []
return
for point in polyline_points:
polyline_vertices.append(Vector((point.x, point.y, point.z)))
if x_angle:
# Get vertices relative to the first polyline point as origin
local_vertices = [v - Vector(polyline_vertices[0]) for v in polyline_vertices]
# Make the transformation relative to the x_angle
transformed_vertices = [Vector((v.x, v.y * (1 / cos(x_angle)), v.z)) for v in local_vertices]
# Convert back to world origin
polyline_vertices = [v + Vector(polyline_vertices[0]) for v in transformed_vertices]
if offset != 0:
polyline_vertices = [v + Vector((0, 0, offset)) for v in polyline_vertices]
is_closed = True
if (
polyline_vertices[0].x == polyline_vertices[-1].x
and polyline_vertices[0].y == polyline_vertices[-1].y
and polyline_vertices[0].z == polyline_vertices[-1].z
):
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
bm = tool.Model.create_bmesh_from_vertices(polyline_vertices, is_closed)
bm.verts.ensure_lookup_table()
if x_angle:
rot_mat = Matrix.Rotation(x_angle, 3, "X")
if abs(x_angle) > (pi / 2):
rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0))
bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat)
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, thickness))
bm.verts.index_update()
bm.edges.index_update()
verts = [tuple(v.co) for v in bm.verts]
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
return data
def get_vertical_profile_preview_data(self) -> dict[str, Any]:
relating_type = self.relating_type
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile = material.MaterialProfiles[0].Profile
except:
return {}
model_props = tool.Model.get_model_props()
extrusion_depth = model_props.extrusion_depth
cardinal_point = model_props.cardinal_point
rot_mat = Quaternion()
if relating_type.is_a("IfcBeamType"):
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
rot_mat = y_rot @ z_rot
# Get profile data
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts
if not verts:
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create extrusion bmesh
bm = bmesh.new()
grouped_verts.append(grouped_verts[0]) # Close profile
new_verts = [bm.verts.new(v) for v in grouped_verts]
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
# Calculate rotation, mouse position, angle and cardinal point
polyline_props = tool.Model.get_polyline_props()
snap_prop = polyline_props.snap_mouse_point[0]
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
data = {}
verts = [tuple(v.co) for v in bm.verts]
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
verts = [tuple(Vector(v) + mouse_point) for v in verts]
min_z = min(v.co.z for v in bm.verts)
max_z = max(v.co.z for v in bm.verts)
# Add axis verts
verts.append(tuple(mouse_point))
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
# Add only profile edges
edges = []
for edge in bm.edges:
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
):
edges.append(edge)
# Add axis edge
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
edges.append((len(verts) - 1, len(verts) - 2))
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
bm.free()
return data
def get_horizontal_profile_preview_data(self) -> dict[str, Any]:
relating_type = self.relating_type
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile_curve = material.MaterialProfiles[0].Profile
except:
return {}
model_props = tool.Model.get_model_props()
cardinal_point = model_props.cardinal_point
polyline_verts = []
polyline_props = tool.Model.get_polyline_props()
polyline_data = polyline_props.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
return {}
for point in polyline_points:
polyline_verts.append(Vector((point.x, point.y, point.z)))
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
# Get profile shape
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
verts = shape.verts
if not verts:
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
data: dict[str, Any] = {}
data["verts"] = []
data["edges"] = []
data["tris"] = []
grouped_verts = [(v) for v in grouped_verts]
all_bm = bmesh.new()
for i in range(len(polyline_verts) - 1):
mesh = bpy.data.meshes.new("TempMesh")
# Create the initial mesh from the profile verts
bm = tool.Model.create_bmesh_from_vertices(grouped_verts, is_closed=True)
bm.verts.ensure_lookup_table()
# Creates the clipping plane formed by two segments.
# The first one is for the profile start, based on the current and previous segment of the polyline.
# The second is for the profile end, based on the current and the next segment.
if i == 0:
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
clip_start = d
else:
d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized()
d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized()
clip_start = (d1 - d2).normalized()
if i == len(polyline_verts) - 2:
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
clip_end = d
else:
d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized()
clip_end = (d1 - d2).normalized()
# Rotates the profile face to the right direction
direction = polyline_verts[i + 1] - polyline_verts[i]
position = polyline_verts[i]
rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4()
bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix)
bmesh.ops.translate(bm, verts=bm.verts, vec=position)
bmesh.ops.translate(bm, verts=bm.verts, vec=-direction)
# Extrude and move the new face
last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)]
bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3)
# Apply the cutting planes
cut = bmesh.ops.bisect_plane(
bm,
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
plane_co=polyline_verts[i],
plane_no=clip_start,
clear_inner=True,
) )
bm.verts.index_update()
bm.edges.index_update()
cut = bmesh.ops.bisect_plane(
bm,
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
plane_co=polyline_verts[i + 1],
plane_no=clip_end,
clear_outer=True,
)
bm.to_mesh(mesh)
bm.free()
mesh.update()
all_bm.from_mesh(mesh)
bpy.data.meshes.remove(bpy.data.meshes["TempMesh"])
# It's necessary to add the mesh to an object to get the expected result.
mesh = bpy.data.meshes.new("TempMesh2")
all_bm.to_mesh(mesh)
all_bm.free()
obj = bpy.data.objects.new("TempObj", mesh)
bm = bmesh.new()
bm.from_mesh(obj.data)
bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"])
verts = [tuple(v.co) for v in bm.verts]
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
bm.free()
return data
def get_generic_preview_data(self):
if not (data := self.obj_data):
return
relating_type = self.relating_type
model_props = tool.Model.get_model_props()
if relating_type.is_a("IfcDoorType"):
rl = float(model_props.rl1)
elif relating_type.is_a("IfcWindowType"):
rl = float(model_props.rl2)
else:
rl = 0
polyline_props = tool.Model.get_polyline_props()
snap_prop = polyline_props.snap_mouse_point[0]
default_container_elevation = tool.Root.get_default_container_elevation()
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
snap_element = tool.Ifc.get_entity(snap_obj)
rot_mat = Matrix()
if relating_type.is_a() in ["IfcDoorType", "IfcWindowType"] and snap_element and snap_element.is_a("IfcWall"):
layers = tool.Model.get_material_layer_parameters(snap_element)
axes = tool.Model.get_wall_axis(snap_obj, layers=layers)
axis_base = axes["base"]
axis_side = axes["side"]
point_on_base_axis = tool.Cad.point_on_edge(mouse_point, axis_base)
point_on_side_axis = tool.Cad.point_on_edge(mouse_point, axis_side)
if (point_on_base_axis - mouse_point).length_squared <= (point_on_side_axis - mouse_point).length_squared:
# mouse is snapped to the base axis, the preview looks exactly like the placed door / window
rot_mat = snap_obj.matrix_world
else:
# mouse is snapped to the side axis, the preview is inverted, rotate it now and correct x position later
rot_mat = (
(snap_obj.matrix_world.to_quaternion() @ Quaternion(Vector((0, 0, 1)), radians(180)))
.to_matrix()
.to_4x4()
)
mouse_point.z = snap_obj.matrix_world.translation.z
if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)):
container_obj = tool.Ifc.get_object(container)
mouse_point.z = container_obj.location.z
obj_type = tool.Ifc.get_object(relating_type)
subcontexts = tool.Drawing.get_active_drawing_subcontexts()
if not subcontexts:
subcontexts = [("Model", "Body", "MODEL_VIEW")]
active_context = tool.Geometry.get_active_representation_context(obj_type)
active_context_params = tool.Geometry.get_subcontext_parameters(active_context)
for subcontext in subcontexts:
if subcontext == active_context_params:
break
representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext)
if representation:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj_type,
representation,
)
context.view_layer.update()
break
translate_mouse = Matrix.Translation(mouse_point)
translate_rl = Matrix.Translation((0.0, 0.0, rl))
combined_m = translate_mouse @ rot_mat @ translate_rl @ self.obj_matrix_i
data["verts"] = [tuple(combined_m @ v) for v in data["raw_verts"]]
return data
class WallAxisDecorator: class WallAxisDecorator:
+1 -583
View File
@@ -35,563 +35,12 @@ import bonsai.core.root
import bonsai.core.geometry import bonsai.core.geometry
import bonsai.core.model as core import bonsai.core.model as core
import bonsai.tool as tool import bonsai.tool as tool
from math import pi, sin, cos, degrees, tan, radians from mathutils import Vector
from mathutils import Vector, Matrix, Quaternion
from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.decorator import PolylineDecorator from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.geometry.decorator import ItemDecorator
from typing import Optional, Union, Literal, Any from typing import Optional, Union, Literal, Any
from lark import Lark, Transformer from lark import Lark, Transformer
def create_bmesh_from_vertices(vertices, is_closed=False):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in vertices]
if is_closed:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
new_edges.append(
bm.edges.new((new_verts[-1], new_verts[0]))
) # Add an edge between the last an first point to make it closed.
else:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
return bm
def get_wall_preview_data(context, relating_type):
# Get properties from object type
model_props = tool.Model.get_model_props()
direction_sense = model_props.direction_sense
direction = 1
if direction_sense == "NEGATIVE":
direction = -1
layers = tool.Model.get_material_layer_parameters(relating_type)
if not layers["thickness"]:
return
thickness = layers["thickness"]
thickness *= direction
offset_type = model_props.offset_type_vertical
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
offset = model_props.offset * unit_scale
height = float(model_props.extrusion_depth)
rl = float(model_props.rl1)
x_angle = float(model_props.x_angle)
if x_angle > radians(90) or x_angle < radians(-90):
height *= -1
angle_distance = height * tan(x_angle)
thickness *= 1 / cos(x_angle)
data = {}
data["verts"] = []
# Verts
polyline_vertices = []
polyline_props = tool.Model.get_polyline_props()
polyline_data = polyline_props.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
data = []
return
for point in polyline_points:
polyline_vertices.append(Vector((point.x, point.y, point.z)))
is_closed = False
if (
polyline_vertices[0].x == polyline_vertices[-1].x
and polyline_vertices[0].y == polyline_vertices[-1].y
and polyline_vertices[0].z == polyline_vertices[-1].z
):
is_closed = True
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
bm_base = create_bmesh_from_vertices(polyline_vertices, is_closed)
base_vertices = tool.Cad.offset_edges(bm_base, offset)
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
top_vertices = tool.Cad.offset_edges(bm_base, angle_distance + offset)
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distance + thickness + offset)
if is_closed:
base_vertices.append(base_vertices[0])
offset_base_verts.append(offset_base_verts[0])
top_vertices.append(top_vertices[0])
offset_top_verts.append(offset_top_verts[0])
if offset_base_verts is not None:
for v in base_vertices:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in offset_base_verts[::-1]:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
for v in top_vertices:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
for v in offset_top_verts[::-1]:
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
bm_base.free()
# Edges and Tris
points = []
side_edges_1 = []
side_edges_2 = []
base_edges = []
for i in range(len(data["verts"])):
points.append(Vector(data["verts"][i]))
n = len(points) // 2
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
side_edges_1.extend(bottom_side_1)
side_edges_2.extend(bottom_side_2)
base_edges.extend(bottom_loop)
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
side_edges_1.extend(upper_side_1)
side_edges_2.extend(upper_side_2)
base_edges.extend(upper_loop)
loops = [side_edges_1, side_edges_2, base_edges]
data["edges"] = []
data["tris"] = []
for i, group in enumerate(loops):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
bm.verts.index_update()
bm.edges.index_update()
if i == 2:
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
bm.verts.index_update()
bm.edges.index_update()
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
data["edges"].extend(edges)
data["tris"].extend(tris)
data["edges"] = list(set(tuple(e) for e in data["edges"]))
data["tris"] = list(set(tuple(t) for t in data["tris"]))
return data
def get_slab_preview_data(context, relating_type):
model_props = tool.Model.get_model_props()
x_angle = 0 if tool.Cad.is_x(model_props.x_angle, 0, tolerance=0.001) else model_props.x_angle
direction_sense = model_props.direction_sense
direction = 1
if direction_sense == "NEGATIVE":
direction = -1
layers = tool.Model.get_material_layer_parameters(relating_type)
if not layers["thickness"]:
return
thickness = layers["thickness"] * abs(1 / cos(x_angle))
thickness *= direction
offset_type = model_props.offset_type_horizontal
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale
data = {}
data["verts"] = []
# Verts
polyline_vertices = []
polyline_props = tool.Model.get_polyline_props()
polyline_data = polyline_props.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 3:
data = []
return
for point in polyline_points:
polyline_vertices.append(Vector((point.x, point.y, point.z)))
if x_angle:
# Get vertices relative to the first polyline point as origin
local_vertices = [v - Vector(polyline_vertices[0]) for v in polyline_vertices]
# Make the transformation relative to the x_angle
transformed_vertices = [Vector((v.x, v.y * (1 / cos(x_angle)), v.z)) for v in local_vertices]
# Convert back to world origin
polyline_vertices = [v + Vector(polyline_vertices[0]) for v in transformed_vertices]
if offset != 0:
polyline_vertices = [v + Vector((0, 0, offset)) for v in polyline_vertices]
is_closed = True
if (
polyline_vertices[0].x == polyline_vertices[-1].x
and polyline_vertices[0].y == polyline_vertices[-1].y
and polyline_vertices[0].z == polyline_vertices[-1].z
):
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
bm = create_bmesh_from_vertices(polyline_vertices, is_closed)
bm.verts.ensure_lookup_table()
if x_angle:
rot_mat = Matrix.Rotation(x_angle, 3, "X")
if abs(x_angle) > (pi / 2):
rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0))
bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat)
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, thickness))
bm.verts.index_update()
bm.edges.index_update()
verts = [tuple(v.co) for v in bm.verts]
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
return data
def get_vertical_profile_preview_data(
context: bpy.types.Context, relating_type: ifcopenshell.entity_instance
) -> dict[str, Any]:
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile = material.MaterialProfiles[0].Profile
except:
return {}
model_props = tool.Model.get_model_props()
extrusion_depth = model_props.extrusion_depth
cardinal_point = model_props.cardinal_point
rot_mat = Quaternion()
if relating_type.is_a("IfcBeamType"):
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
rot_mat = y_rot @ z_rot
# Get profile data
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts
if not verts:
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create extrusion bmesh
bm = bmesh.new()
grouped_verts.append(grouped_verts[0]) # Close profile
new_verts = [bm.verts.new(v) for v in grouped_verts]
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
# Calculate rotation, mouse position, angle and cardinal point
polyline_props = tool.Model.get_polyline_props()
snap_prop = polyline_props.snap_mouse_point[0]
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
data = {}
verts = [tuple(v.co) for v in bm.verts]
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
verts = [tuple(Vector(v) + mouse_point) for v in verts]
min_z = min(v.co.z for v in bm.verts)
max_z = max(v.co.z for v in bm.verts)
# Add axis verts
verts.append(tuple(mouse_point))
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
# Add only profile edges
edges = []
for edge in bm.edges:
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
):
edges.append(edge)
# Add axis edge
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
edges.append((len(verts) - 1, len(verts) - 2))
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
bm.free()
return data
def get_horizontal_profile_preview_data(
context: bpy.types.Context, relating_type: ifcopenshell.entity_instance
) -> dict[str, Any]:
material = ifcopenshell.util.element.get_material(relating_type)
try:
profile_curve = material.MaterialProfiles[0].Profile
except:
return {}
model_props = tool.Model.get_model_props()
cardinal_point = model_props.cardinal_point
polyline_verts = []
polyline_props = tool.Model.get_polyline_props()
polyline_data = polyline_props.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
if len(polyline_points) < 2:
return {}
for point in polyline_points:
polyline_verts.append(Vector((point.x, point.y, point.z)))
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
# Get profile shape
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
verts = shape.verts
if not verts:
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.")
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
# Create offsets based on cardinal point
min_x = min(v[0] for v in grouped_verts)
max_x = max(v[0] for v in grouped_verts)
min_y = min(v[1] for v in grouped_verts)
max_y = max(v[1] for v in grouped_verts)
x_offset = (max_x - min_x) / 2
y_offset = (max_y - min_y) / 2
match cardinal_point:
case "1":
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "2":
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
case "3":
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
case "4":
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
case "5":
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
case "6":
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
case "7":
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
case "8":
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
data: dict[str, Any] = {}
data["verts"] = []
data["edges"] = []
data["tris"] = []
grouped_verts = [(v) for v in grouped_verts]
all_bm = bmesh.new()
for i in range(len(polyline_verts) - 1):
mesh = bpy.data.meshes.new("TempMesh")
# Create the initial mesh from the profile verts
bm = create_bmesh_from_vertices(grouped_verts, is_closed=True)
bm.verts.ensure_lookup_table()
# Creates the clipping plane formed by two segments.
# The first one is for the profile start, based on the current and previous segment of the polyline.
# The second is for the profile end, based on the current and the next segment.
if i == 0:
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
clip_start = d
else:
d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized()
d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized()
clip_start = (d1 - d2).normalized()
if i == len(polyline_verts) - 2:
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
clip_end = d
else:
d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized()
clip_end = (d1 - d2).normalized()
# Rotates the profile face to the right direction
direction = polyline_verts[i + 1] - polyline_verts[i]
position = polyline_verts[i]
rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4()
bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix)
bmesh.ops.translate(bm, verts=bm.verts, vec=position)
bmesh.ops.translate(bm, verts=bm.verts, vec=-direction)
# Extrude and move the new face
last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)]
bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3)
# Apply the cutting planes
cut = bmesh.ops.bisect_plane(
bm,
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
plane_co=polyline_verts[i],
plane_no=clip_start,
clear_inner=True,
)
bm.verts.index_update()
bm.edges.index_update()
cut = bmesh.ops.bisect_plane(
bm,
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
plane_co=polyline_verts[i + 1],
plane_no=clip_end,
clear_outer=True,
)
bm.to_mesh(mesh)
bm.free()
mesh.update()
all_bm.from_mesh(mesh)
bpy.data.meshes.remove(bpy.data.meshes["TempMesh"])
# It's necessary to add the mesh to an object to get the expected result.
mesh = bpy.data.meshes.new("TempMesh2")
all_bm.to_mesh(mesh)
all_bm.free()
obj = bpy.data.objects.new("TempObj", mesh)
bm = bmesh.new()
bm.from_mesh(obj.data)
bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"])
verts = [tuple(v.co) for v in bm.verts]
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
bm.free()
return data
def get_generic_product_preview_data(context, relating_type):
model_props = tool.Model.get_model_props()
if relating_type.is_a("IfcDoorType"):
rl = float(model_props.rl1)
elif relating_type.is_a("IfcWindowType"):
rl = float(model_props.rl2)
else:
rl = 0
polyline_props = tool.Model.get_polyline_props()
snap_prop = polyline_props.snap_mouse_point[0]
default_container_elevation = tool.Root.get_default_container_elevation()
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
snap_element = tool.Ifc.get_entity(snap_obj)
rot_mat = Quaternion()
if relating_type.is_a() in ["IfcDoorType", "IfcWindowType"] and snap_element and snap_element.is_a("IfcWall"):
layers = tool.Model.get_material_layer_parameters(snap_element)
axes = tool.Model.get_wall_axis(snap_obj, layers=layers)
axis_base = axes["base"]
axis_side = axes["side"]
point_on_base_axis = tool.Cad.point_on_edge(mouse_point, axis_base)
point_on_side_axis = tool.Cad.point_on_edge(mouse_point, axis_side)
if (point_on_base_axis - mouse_point).length_squared <= (point_on_side_axis - mouse_point).length_squared:
# mouse is snapped to the base axis, the preview looks exactly like the placed door / window
rot_mat = snap_obj.matrix_world.to_quaternion()
else:
# mouse is snapped to the side axis, the preview is inverted, rotate it now and correct x position later
rot_mat = snap_obj.matrix_world.to_quaternion() @ Quaternion(Vector((0, 0, 1)), radians(180))
mouse_point.z = snap_obj.matrix_world.translation.z
if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)):
container_obj = tool.Ifc.get_object(container)
mouse_point.z = container_obj.location.z
obj_type = tool.Ifc.get_object(relating_type)
subcontexts = tool.Drawing.get_active_drawing_subcontexts()
if not subcontexts:
subcontexts = [("Model", "Body", "MODEL_VIEW")]
active_context = tool.Geometry.get_active_representation_context(obj_type)
active_context_params = tool.Geometry.get_subcontext_parameters(active_context)
for subcontext in subcontexts:
if subcontext == active_context_params:
break
representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext)
if representation:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj_type,
representation,
)
context.view_layer.update()
break
if obj_type.data:
data = ItemDecorator.get_obj_data(obj_type)
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point) for v in data["verts"]]
return data
class PolylineOperator: class PolylineOperator:
# TODO Fill doc strings # TODO Fill doc strings
""" """ """ """
@@ -987,37 +436,6 @@ class PolylineOperator:
tool.Blender.update_viewport() tool.Blender.update_viewport()
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
if tool.Model.get_usage_type(relating_type) == "PROFILE":
if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}:
data = get_vertical_profile_preview_data(context, relating_type)
else:
data = get_horizontal_profile_preview_data(context, relating_type)
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
data = get_wall_preview_data(context, relating_type)
elif tool.Model.get_usage_type(relating_type) == "LAYER3":
data = get_slab_preview_data(context, relating_type)
else:
data = get_generic_product_preview_data(context, relating_type)
# Update properties so it can be used by the decorator
props = tool.Model.get_product_preview_props()
props.verts.clear()
props.edges.clear()
props.tris.clear()
if not data:
return
for vert in data["verts"]:
v = props.verts.add()
v.value_3d = vert
for edge in data["edges"]:
e = props.edges.add()
e.value_2d = edge
for tri in data["tris"]:
t = props.tris.add()
t.value_3d = tri
def set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None: def set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
props = tool.Model.get_model_props() props = tool.Model.get_model_props()
direction_sense = props.direction_sense direction_sense = props.direction_sense
@@ -249,8 +249,6 @@ class DrawOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
if event.value == "RELEASE" and event.type == "LEFTMOUSE": if event.value == "RELEASE" and event.type == "LEFTMOUSE":
self.create_occurrence(context, event) self.create_occurrence(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event) cancel = self.handle_cancelation(context, event)
if cancel is not None: if cancel is not None:
ProductDecorator.uninstall() ProductDecorator.uninstall()
@@ -1189,11 +1189,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
return {"FINISHED"} return {"FINISHED"}
self.handle_keyboard_input(context, event) self.handle_keyboard_input(context, event)
self.handle_inserting_polyline(context, event) self.handle_inserting_polyline(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event) cancel = self.handle_cancelation(context, event)
if cancel is not None: if cancel is not None:
ProductDecorator.uninstall() ProductDecorator.uninstall()
@@ -1697,17 +1697,6 @@ class ProductPreviewItem(PropertyGroup):
value_2d: tuple[float, float] value_2d: tuple[float, float]
class BIMProductPreviewProperties(PropertyGroup):
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
if TYPE_CHECKING:
verts: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
edges: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
tris: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None: def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
if self.is_editing: if self.is_editing:
return return
@@ -1135,11 +1135,8 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
return {"FINISHED"} return {"FINISHED"}
self.handle_keyboard_input(context, event) self.handle_keyboard_input(context, event)
self.handle_inserting_polyline(context, event) self.handle_inserting_polyline(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event) cancel = self.handle_cancelation(context, event)
if cancel is not None: if cancel is not None:
ProductDecorator.uninstall() ProductDecorator.uninstall()
+9 -1
View File
@@ -691,9 +691,17 @@ class BIM_PT_external_parametric_geometry(bpy.types.Panel):
# should find a way to update only on graph changes. # should find a way to update only on graph changes.
res = tool.Model.update_mesh_from_sverchok(obj, props.sverchok_nodes) res = tool.Model.update_mesh_from_sverchok(obj, props.sverchok_nodes)
if res is not None: if res is not None:
print(res)
layout.label(text=f"Error Updating from Graph, See System Console", icon="ERROR") layout.label(text=f"Error Updating from Graph, See System Console", icon="ERROR")
layout.label(text="Parameters:")
box = layout.box()
group_node = tool.Model.get_ifcsverchok_group_node(props.sverchok_nodes)
node_tree = group_node.node_tree
for socket, interface_socket in zip(group_node.inputs, node_tree.sockets("INPUT")):
socket.draw_group_property(box, socket.name, interface_socket)
def draw_door_properties(layout: bpy.types.UILayout, props: module_prop.BIMDoorProperties) -> None: def draw_door_properties(layout: bpy.types.UILayout, props: module_prop.BIMDoorProperties) -> None:
"""Draw door properties UI (shared between properties panel and preferences).""" """Draw door properties UI (shared between properties panel and preferences)."""
@@ -787,11 +787,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
return {"FINISHED"} return {"FINISHED"}
self.handle_keyboard_input(context, event) self.handle_keyboard_input(context, event)
self.handle_inserting_polyline(context, event) self.handle_inserting_polyline(context, event)
self.get_product_preview_data(context, self.relating_type)
cancel = self.handle_cancelation(context, event) cancel = self.handle_cancelation(context, event)
if cancel is not None: if cancel is not None:
ProductDecorator.uninstall() ProductDecorator.uninstall()
+1 -1
View File
@@ -319,7 +319,7 @@ class BIMProjectProperties(PropertyGroup):
), ),
default=False, default=False,
) )
deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.001) deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.05)
angular_tolerance: FloatProperty(name="Angular Tolerance", default=0.5) angular_tolerance: FloatProperty(name="Angular Tolerance", default=0.5)
void_limit: IntProperty( void_limit: IntProperty(
name="Void Limit", name="Void Limit",
+12 -18
View File
@@ -445,20 +445,17 @@ class FilterValueSuggestions(Operator):
if pset.HasProperties: if pset.HasProperties:
for prop in pset.HasProperties: for prop in pset.HasProperties:
if hasattr(prop, "Name") and prop.Name == property_name: if hasattr(prop, "Name") and prop.Name == property_name:
if hasattr(prop, "NominalValue") and prop.NominalValue: if prop.is_a("IfcPropertyEnumeratedValue"):
try: if hasattr(prop, "EnumerationReference") and prop.EnumerationReference:
value = prop.NominalValue.wrappedValue enum_reference = prop.EnumerationReference
if value is not None and value != "": if hasattr(enum_reference, "EnumerationValues"):
if not hasattr(value, "is_a") and not isinstance( for enum_value in enum_reference.EnumerationValues:
value, (tuple, list) property_values.add(str(enum_value.wrappedValue))
): if hasattr(prop, "EnumerationValues") and prop.EnumerationValues:
str_value = str(value) for enum_value in prop.EnumerationValues:
if not str_value.startswith("#") and not str_value.startswith( property_values.add(str(enum_value.wrappedValue))
"(" elif hasattr(prop, "NominalValue") and prop.NominalValue:
): property_values.add(str(prop.NominalValue.wrappedValue))
property_values.add(str_value)
except:
continue
except: except:
continue continue
return property_values return property_values
@@ -765,10 +762,7 @@ class Search(Operator):
preferences = tool.Blender.get_addon_preferences() preferences = tool.Blender.get_addon_preferences()
# Migrate old ! prefix filters to new filter_mode system when preferences are enabled # Migrate old ! prefix filters to new filter_mode system when preferences are enabled
if ( if preferences.chain_filter_with_set_operations:
preferences.chain_filter_with_set_operations
or preferences.default_filter_with_set_operations_for_globalid_and_class
):
for filter_group in props.filter_groups: for filter_group in props.filter_groups:
for ifc_filter in filter_group.filters: for ifc_filter in filter_group.filters:
if ifc_filter.type not in ["entity", "instance"]: if ifc_filter.type not in ["entity", "instance"]:
@@ -21,11 +21,13 @@ from . import ui, prop, operator, decorator
classes = ( classes = (
operator.AddPort, operator.AddPort,
operator.AddRelatedPortConnection,
operator.AddSystem, operator.AddSystem,
operator.AddZone, operator.AddZone,
operator.AssignSystem, operator.AssignSystem,
operator.AssignUnassignFlowControl, operator.AssignUnassignFlowControl,
operator.ConnectPort, operator.ConnectPort,
operator.CycleFlowDirection,
operator.DisableEditingSystem, operator.DisableEditingSystem,
operator.DisableEditingZone, operator.DisableEditingZone,
operator.DisableSystemEditingUI, operator.DisableSystemEditingUI,
+100 -4
View File
@@ -198,7 +198,16 @@ class ShowPorts(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
# Ifc.Operator - as operator will sync object's position with IFC. # Ifc.Operator - as operator will sync object's position with IFC.
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=tool.Ifc.get_entity(context.active_object)) element = tool.Ifc.get_entity(context.active_object)
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=element)
for port in tool.System.get_ports(element):
connected_port = tool.System.get_connected_port(port)
if connected_port:
connected_port_obj = tool.Ifc.get_object(connected_port)
if not connected_port_obj:
parent_element = tool.System.get_port_relating_element(connected_port)
core.show_ports(tool.Ifc, tool.System, tool.Spatial, element=parent_element)
class HidePorts(bpy.types.Operator, tool.Ifc.Operator): class HidePorts(bpy.types.Operator, tool.Ifc.Operator):
@@ -217,7 +226,7 @@ class HidePorts(bpy.types.Operator, tool.Ifc.Operator):
class AddPort(bpy.types.Operator, tool.Ifc.Operator): class AddPort(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_port" bl_idname = "bim.add_port"
bl_description = "Add port at current cursor position" bl_description = "Add USERDEFINED port at current cursor position"
bl_label = "Add Port" bl_label = "Add Port"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -246,7 +255,41 @@ class ConnectPort(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
obj1 = context.active_object obj1 = context.active_object
obj2 = context.selected_objects[0] if context.selected_objects[1] == obj1 else context.selected_objects[1] obj2 = context.selected_objects[0] if context.selected_objects[1] == obj1 else context.selected_objects[1]
core.connect_port(tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2)) direction = tool.Ifc.get_entity(obj1).FlowDirection or "NOTDEFINED"
core.connect_port(
tool.Ifc, port1=tool.Ifc.get_entity(obj1), port2=tool.Ifc.get_entity(obj2), direction=direction
)
class AddRelatedPortConnection(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_related_port_connection"
bl_label = "Connect Port"
bl_options = {"REGISTER", "UNDO"}
relating_port_id: bpy.props.IntProperty()
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
props = tool.System.get_system_props()
self.layout.prop(props, "related_port", text="Select Port")
def _execute(self, context):
props = tool.System.get_system_props()
if not props.related_port or props.related_port == "NONE":
return {"CANCELLED"}
port_obj = bpy.data.objects.get(props.related_port)
related_port = tool.Ifc.get_entity(port_obj)
relating_port = tool.Ifc.get().by_id(self.relating_port_id)
direction = relating_port.FlowDirection or "NOTDEFINED"
core.connect_port(tool.Ifc, port1=relating_port, port2=related_port, direction=direction)
PortData.is_loaded = False
return {"FINISHED"}
class DisconnectPort(bpy.types.Operator, tool.Ifc.Operator): class DisconnectPort(bpy.types.Operator, tool.Ifc.Operator):
@@ -256,6 +299,9 @@ class DisconnectPort(bpy.types.Operator, tool.Ifc.Operator):
element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"}) element_id: bpy.props.IntProperty(default=0, options={"SKIP_SAVE"})
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
def _execute(self, context): def _execute(self, context):
if self.element_id != 0: if self.element_id != 0:
element = tool.Ifc.get().by_id(self.element_id) element = tool.Ifc.get().by_id(self.element_id)
@@ -310,7 +356,8 @@ class MEPConnectElements(bpy.types.Operator, tool.Ifc.Operator):
ports_distance[(port1, port2)] = distance ports_distance[(port1, port2)] = distance
closest_ports = min(ports_distance, key=lambda x: ports_distance[x]) closest_ports = min(ports_distance, key=lambda x: ports_distance[x])
core.connect_port(tool.Ifc, *closest_ports) direction = closest_ports[0].FlowDirection or "NOTDEFINED"
core.connect_port(tool.Ifc, *closest_ports, direction=direction)
bpy.ops.bim.regenerate_distribution_element() bpy.ops.bim.regenerate_distribution_element()
return {"FINISHED"} return {"FINISHED"}
@@ -379,6 +426,55 @@ class SetFlowDirection(bpy.types.Operator, tool.Ifc.Operator):
return {"CANCELLED"} return {"CANCELLED"}
class CycleFlowDirection(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.cycle_flow_direction"
bl_label = "Cycle Flow Direction"
bl_options = {"REGISTER", "UNDO"}
port_id: bpy.props.IntProperty()
@classmethod
def description(cls, context, operator):
port = tool.Ifc.get().by_id(operator.port_id)
if port and port.is_a("IfcDistributionPort"):
current_direction = port.FlowDirection or "NOTDEFINED"
return f"Current flow direction: {current_direction}. Click to cycle: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED"
return "Cycle through flow directions: SOURCE → SINK → SOURCEANDSINK → NOTDEFINED → SOURCE..."
def _execute(self, context):
port = tool.Ifc.get().by_id(self.port_id)
if not port or not port.is_a("IfcDistributionPort"):
return {"CANCELLED"}
current_direction = port.FlowDirection or "NOTDEFINED"
flow_cycle_map = {
"SOURCE": "SINK",
"SINK": "SOURCEANDSINK",
"SOURCEANDSINK": "NOTDEFINED",
"NOTDEFINED": "SOURCE",
}
next_direction = flow_cycle_map.get(current_direction, "SOURCE")
tool.Ifc.run("attribute.edit_attributes", product=port, attributes={"FlowDirection": next_direction})
connected_port = tool.System.get_connected_port(port)
if connected_port:
connected_direction_map = {
"SOURCE": "SINK",
"SINK": "SOURCE",
"SOURCEANDSINK": "SOURCEANDSINK",
"NOTDEFINED": "NOTDEFINED",
}
connected_direction = connected_direction_map.get(next_direction, "NOTDEFINED")
tool.Ifc.run(
"attribute.edit_attributes", product=connected_port, attributes={"FlowDirection": connected_direction}
)
PortData.is_loaded = False
return {"FINISHED"}
class LoadZones(bpy.types.Operator): class LoadZones(bpy.types.Operator):
bl_idname = "bim.load_zones" bl_idname = "bim.load_zones"
bl_label = "Load Zones" bl_label = "Load Zones"
@@ -92,6 +92,28 @@ def toggle_decorations(self: "BIMSystemProperties", context: bpy.types.Context)
decorator.SystemDecorator.uninstall() decorator.SystemDecorator.uninstall()
def get_available_ports_for_connection(
self: "BIMSystemProperties", context: bpy.types.Context
) -> list[tuple[str, str, str]]:
items = []
active_object_ports = set(tool.System.get_ports(tool.Ifc.get_entity(context.active_object)))
ifc_file = tool.Ifc.get()
for ifc_port in ifc_file.by_type("IfcDistributionPort"):
port = tool.Ifc.get_object(ifc_port)
if not port:
continue
if tool.System.get_connected_port(ifc_port) is not None or ifc_port in active_object_ports:
continue
port_object = tool.Ifc.get_object(tool.System.get_port_relating_element(ifc_port))
suggestion = f"{port_object.name} > {port.name}"
items.append((port.name, suggestion, ""))
return items if items else [("NONE", "Ports are hidden or not available", "")]
class BIMSystemProperties(PropertyGroup): class BIMSystemProperties(PropertyGroup):
system_attributes: CollectionProperty(name="System Attributes", type=Attribute) system_attributes: CollectionProperty(name="System Attributes", type=Attribute)
is_editing: BoolProperty(name="Is Editing", default=False) is_editing: BoolProperty(name="Is Editing", default=False)
@@ -107,6 +129,11 @@ class BIMSystemProperties(PropertyGroup):
should_draw_decorations: BoolProperty( should_draw_decorations: BoolProperty(
name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations name="Should Draw Decorations", description="Toggle system decorations", update=toggle_decorations
) )
related_port: EnumProperty(
name="Connect To Port",
description="Select a port to connect to",
items=get_available_ports_for_connection,
)
if TYPE_CHECKING: if TYPE_CHECKING:
system_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] system_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
@@ -119,6 +146,7 @@ class BIMSystemProperties(PropertyGroup):
edited_system_id: int edited_system_id: int
system_class: str system_class: str
should_draw_decorations: bool should_draw_decorations: bool
related_port: str
@property @property
def active_system_ui_item(self) -> Union[System, None]: def active_system_ui_item(self) -> Union[System, None]:
+119 -68
View File
@@ -30,10 +30,10 @@ if TYPE_CHECKING:
FLOW_DIRECTION_TO_ICON = { FLOW_DIRECTION_TO_ICON = {
"SOURCE": "FORWARD", "SOURCE": "FULLSCREEN_ENTER",
"SINK": "BACK", "SINK": "FULLSCREEN_EXIT",
"SOURCEANDSINK": "ARROW_LEFTRIGHT", "SOURCEANDSINK": "CHECKBOX_DEHLT",
"NOTDEFINED": "CHECKBOX_DEHLT", "NOTDEFINED": "QUESTION",
} }
@@ -163,44 +163,87 @@ class BIM_PT_ports(Panel):
return return
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text="Change Flow Direction:") row.label(text=f"Ports located in: {context.active_object.name} and connected Port/Objects:")
current_flow_direction = PortData.data["selected_objects_flow_direction"]
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
row.operator(
"bim.set_flow_direction",
icon=FLOW_DIRECTION_TO_ICON[flow_direction],
depress=flow_direction == current_flow_direction,
text="",
).direction = flow_direction
row.enabled = len(context.selected_objects) == 2
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text="Ports located on object and connected objects:") cols = [row.column(align=True) for i in range(9)]
row = self.layout.row(align=True) cols[3].scale_x = 1.0
cols = [row.column(align=True) for i in range(6)] cols[6].scale_x = 1.0
cols[8].scale_x = 1.33
for port_data in PortData.data["located_ports_data"]: for port_data in PortData.data["located_ports_data"]:
flow_direction_icon = FLOW_DIRECTION_TO_ICON[port_data["FlowDirection"] or "NOTDEFINED"] flow_direction_icon = FLOW_DIRECTION_TO_ICON[port_data["FlowDirection"] or "NOTDEFINED"]
if port_data["port_obj_name"]:
cols[0].label(text="", icon=flow_direction_icon) if port_data["connected_obj_name"]:
cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port_data["id"] cols[0].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port_data["id"]
cols[2].label(text=port_data["port_obj_name"]) op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True)
op.port_id = port_data["id"]
else: else:
cols[0].label(text="", icon=flow_direction_icon) op = cols[0].operator("bim.add_related_port_connection", text="", icon="PLUGIN")
cols[1].label(text="", icon="HIDE_ON") op.relating_port_id = port_data["id"]
cols[2].label(text="Port is hidden") op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True)
op.port_id = port_data["id"]
if port_data["port_obj_name"]:
cols[2].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port_data["id"]
cols[3].label(text=port_data["port_obj_name"])
else:
cols[2].label(text="", icon="HIDE_ON")
cols[3].label(text="Port is hidden")
if port_data["connected_obj_name"]: if port_data["connected_obj_name"]:
connected_obj = bpy.data.objects[port_data["connected_obj_name"]] connected_obj = bpy.data.objects[port_data["connected_obj_name"]]
cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port_data["id"]
port = tool.Ifc.get().by_id(port_data["id"])
connected_port = tool.System.get_connected_port(port)
if connected_port:
connected_port_obj = tool.Ifc.get_object(connected_port)
if connected_port_obj:
connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"]
op = cols[4].operator(
"bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True
)
op.port_id = connected_port.id()
cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = (
connected_port.id()
)
cols[6].label(text=connected_port_obj.name)
else:
blank4 = cols[4].column(align=True)
blank4.scale_x = 0.1
blank4.label(text="", icon="BLANK1")
blank5 = cols[5].column(align=True)
blank5.scale_x = 0.1
blank5.label(text="", icon="BLANK1")
cols[6].label(text="Port is hidden")
else:
blank4 = cols[4].column(align=True)
blank4.scale_x = 0.1
blank4.label(text="", icon="BLANK1")
blank5 = cols[5].column(align=True)
blank5.scale_x = 0.1
blank5.label(text="", icon="BLANK1")
cols[6].label(text="")
ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) ifc_id = tool.Blender.get_ifc_definition_id(connected_obj)
cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id
cols[5].label(text=port_data["connected_obj_name"]) cols[8].label(text=port_data["connected_obj_name"])
else: else:
cols[3].label(text="", icon="UNLINKED") blank4 = cols[4].column(align=True)
cols[4].label(text="", icon="BLANK1") blank4.scale_x = 0.1
cols[5].label(text="Port is disconnected") blank4.label(text="", icon="BLANK1")
blank5 = cols[5].column(align=True)
blank5.scale_x = 0.1
blank5.label(text="", icon="BLANK1")
cols[6].label(text="Port is disconnected")
blank7 = cols[7].column(align=True)
blank7.scale_x = 0.1
blank7.label(text="", icon="BLANK1")
blank8 = cols[8].column(align=True)
blank8.scale_x = 0.1
blank8.label(text="", icon="BLANK1")
class BIM_PT_port(Panel): class BIM_PT_port(Panel):
@@ -223,14 +266,8 @@ class BIM_PT_port(Panel):
return True return True
def draw(self, context): def draw(self, context):
self.props = tool.System.get_system_props()
layout = self.layout layout = self.layout
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="IfcDistributionPort")
row.operator("bim.connect_port", icon="PLUGIN", text="")
row.operator("bim.disconnect_port", icon="UNLINKED", text="")
row.operator("bim.remove_port", icon="X", text="")
if not PortData.is_loaded: if not PortData.is_loaded:
PortData.load() PortData.load()
@@ -238,42 +275,56 @@ class BIM_PT_port(Panel):
if not PortData.data["is_port"]: if not PortData.data["is_port"]:
return return
element = tool.Ifc.get_entity(context.active_object)
current_flow_direction = str(element.FlowDirection)
row = layout.row(align=True)
row.label(text="Flow Direction:")
row.label(text=current_flow_direction)
# port located on
row = layout.row(align=True)
relating_object_name = PortData.data["port_relating_object_name"] relating_object_name = PortData.data["port_relating_object_name"]
relating_object = bpy.data.objects[relating_object_name] row.label(text=f"IfcDistributionPort located in: {relating_object_name}")
row.label(text="Port located on:") row.operator("bim.remove_port", icon="X", text="")
row.label(text=relating_object_name)
ifc_id = tool.Blender.get_ifc_definition_id(relating_object) element = tool.Ifc.get_entity(context.active_object)
row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id
# object connected to the port
row = layout.row(align=True) row = layout.row(align=True)
connected_object_name = PortData.data["port_connected_object_name"] cols = [row.column(align=True) for i in range(9)]
if connected_object_name: cols[3].scale_x = 1.0
connected_object = bpy.data.objects[connected_object_name] cols[6].scale_x = 1.0
row.label(text="Port connected to:") cols[8].scale_x = 1.33
row.label(text=connected_object_name)
ifc_id = tool.Blender.get_ifc_definition_id(connected_object) flow_direction_icon = FLOW_DIRECTION_TO_ICON[element.FlowDirection or "NOTDEFINED"]
row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id connected_port = tool.System.get_connected_port(element)
if connected_port:
cols[0].operator("bim.disconnect_port", text="", icon="UNLINKED")
op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True)
op.port_id = element.id()
else: else:
row.label(text="Port is not connected to any element") cols[0].operator("bim.connect_port", icon="PLUGIN", text="")
op = cols[1].operator("bim.cycle_flow_direction", text="", icon=flow_direction_icon, emboss=True)
op.port_id = element.id()
row = layout.row(align=True) cols[2].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = element.id()
row.label(text="Change Flow Direction:") cols[3].label(text=context.active_object.name)
for flow_direction in FLOW_DIRECTION_TO_ICON.keys():
row.operator( if connected_port:
"bim.set_flow_direction", connected_port_flow_dir = FLOW_DIRECTION_TO_ICON[connected_port.FlowDirection or "NOTDEFINED"]
icon=FLOW_DIRECTION_TO_ICON[flow_direction], op = cols[4].operator("bim.cycle_flow_direction", text="", icon=connected_port_flow_dir, emboss=True)
depress=flow_direction == current_flow_direction, op.port_id = connected_port.id()
text="", cols[5].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = connected_port.id()
).direction = flow_direction connected_port_obj = tool.Ifc.get_object(connected_port)
cols[6].label(text=connected_port_obj.name if connected_port_obj else "Hidden Port")
connected_object_name = PortData.data["port_connected_object_name"]
if connected_object_name:
connected_obj = bpy.data.objects[connected_object_name]
ifc_id = tool.Blender.get_ifc_definition_id(connected_obj)
cols[7].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id
cols[8].label(text=connected_object_name)
else:
cols[7].label(text="", icon="BLANK1")
cols[8].label(text="")
else:
cols[4].label(text="", icon="BLANK1")
cols[5].label(text="", icon="BLANK1")
cols[6].label(text="Port is disconnected")
cols[7].label(text="", icon="BLANK1")
cols[8].label(text="")
class BIM_PT_flow_controls(Panel): class BIM_PT_flow_controls(Panel):
+2 -13
View File
@@ -724,15 +724,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
) )
chain_filter_with_set_operations: BoolProperty( chain_filter_with_set_operations: BoolProperty(
name="NEW filter mode: Enable chained filters with set operations", name="NEW Filter mode: Enable chained filters with set operations",
description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values", description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values",
default=False, default=False,
) )
default_filter_with_set_operations_for_globalid_and_class: BoolProperty(
name="DEFAULT filter mode: Enable set operations for GlobalId/Class",
description="Enable ADD/SUBTRACT/FILTER toggle buttons on entity (Class) and instance (GlobalId) filters for the DEFAULT filter mode",
default=False,
)
save_metadata_blend_file: BoolProperty( save_metadata_blend_file: BoolProperty(
name="Save non ifc data to metadata blend File", name="Save non ifc data to metadata blend File",
@@ -783,7 +778,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
container_hide_show_isolate: bool container_hide_show_isolate: bool
mass_time_units_in_wizard: bool mass_time_units_in_wizard: bool
chain_filter_with_set_operations: bool chain_filter_with_set_operations: bool
default_filter_with_set_operations_for_globalid_and_class: bool
save_metadata_blend_file: bool save_metadata_blend_file: bool
def draw(self, context: bpy.types.Context) -> None: def draw(self, context: bpy.types.Context) -> None:
@@ -979,14 +973,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "container_hide_show_isolate") layout.prop(self, "container_hide_show_isolate")
layout.prop(self, "mass_time_units_in_wizard") layout.prop(self, "mass_time_units_in_wizard")
layout.label(text="Filtering modes:") row = layout.row(align=True)
box = layout.box()
row = box.row(align=True)
row.prop(self, "chain_filter_with_set_operations") row.prop(self, "chain_filter_with_set_operations")
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270" row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
row = box.row(align=True)
row.prop(self, "default_filter_with_set_operations_for_globalid_and_class")
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/comment/27030"
layout.prop(self, "save_metadata_blend_file") layout.prop(self, "save_metadata_blend_file")
if self.save_metadata_blend_file: if self.save_metadata_blend_file:
row = layout.row() row = layout.row()
+9 -5
View File
@@ -123,9 +123,8 @@ def hide_ports(ifc: type[tool.Ifc], system: type[tool.System], element: ifcopens
def add_port(ifc: type[tool.Ifc], system: type[tool.System], element: ifcopenshell.entity_instance) -> None: def add_port(ifc: type[tool.Ifc], system: type[tool.System], element: ifcopenshell.entity_instance) -> None:
system.load_ports(element, system.get_ports(element)) system.load_ports(element, system.get_ports(element))
obj = system.create_empty_at_cursor_with_element_orientation(element) port = system.create_port_at_cursor(element)
port = system.run_root_assign_class(obj=obj, ifc_class="IfcDistributionPort", should_add_representation=False) system.load_ports(element, [port])
ifc.run("system.assign_port", element=element, port=port)
def remove_port(ifc: type[tool.Ifc], system: type[tool.System], port: ifcopenshell.entity_instance) -> None: def remove_port(ifc: type[tool.Ifc], system: type[tool.System], port: ifcopenshell.entity_instance) -> None:
@@ -133,8 +132,13 @@ def remove_port(ifc: type[tool.Ifc], system: type[tool.System], port: ifcopenshe
ifc.run("root.remove_product", product=port) ifc.run("root.remove_product", product=port)
def connect_port(ifc: type[tool.Ifc], port1: ifcopenshell.entity_instance, port2: ifcopenshell.entity_instance) -> None: def connect_port(
ifc.run("system.connect_port", port1=port1, port2=port2) ifc: type[tool.Ifc],
port1: ifcopenshell.entity_instance,
port2: ifcopenshell.entity_instance,
direction: str = "NOTDEFINED",
) -> None:
ifc.run("system.connect_port", port1=port1, port2=port2, direction=direction)
def disconnect_port(ifc: type[tool.Ifc], port: ifcopenshell.entity_instance) -> None: def disconnect_port(ifc: type[tool.Ifc], port: ifcopenshell.entity_instance) -> None:
+1
View File
@@ -1082,6 +1082,7 @@ class Surveyor:
@interface @interface
class System: class System:
def create_empty_at_cursor_with_element_orientation(cls, element): pass def create_empty_at_cursor_with_element_orientation(cls, element): pass
def create_port_at_cursor(cls, system): pass
def delete_element_objects(cls, elements): pass def delete_element_objects(cls, elements): pass
def disable_editing_system(cls): pass def disable_editing_system(cls): pass
def disable_system_editing_ui(cls): pass def disable_system_editing_ui(cls): pass
+4 -2
View File
@@ -50,12 +50,12 @@ class Collector(bonsai.core.tool.Collector):
tool.Geometry.lock_object(obj) tool.Geometry.lock_object(obj)
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0] element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
if not tool.Spatial.get_grid_props().is_visible: if not tool.Spatial.get_grid_props().is_visible:
obj.hide_set(True) obj.hide_viewport = True
elif element.is_a("IfcGrid"): elif element.is_a("IfcGrid"):
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj) tool.Geometry.lock_object(obj)
if not tool.Spatial.get_grid_props().is_visible: if not tool.Spatial.get_grid_props().is_visible:
obj.hide_set(True) obj.hide_viewport = True
if element.is_a("IfcProject"): if element.is_a("IfcProject"):
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
@@ -71,6 +71,8 @@ class Collector(bonsai.core.tool.Collector):
tool.Geometry.lock_object(obj) tool.Geometry.lock_object(obj)
collection = cls._create_project_child_collection("IfcSpace") collection = cls._create_project_child_collection("IfcSpace")
cls.link_collection_object_safe(collection, obj) cls.link_collection_object_safe(collection, obj)
if not tool.Spatial.get_spatial_props().is_visible:
obj.hide_viewport = True
elif element.is_a("IfcStructuralItem"): elif element.is_a("IfcStructuralItem"):
collection = cls._create_project_child_collection("IfcStructuralItem") collection = cls._create_project_child_collection("IfcStructuralItem")
cls.link_collection_object_safe(collection, obj) cls.link_collection_object_safe(collection, obj)
+27 -7
View File
@@ -66,8 +66,8 @@ if TYPE_CHECKING:
BIMRailingProperties, BIMRailingProperties,
BIMExternalParametricGeometryProperties, BIMExternalParametricGeometryProperties,
BIMPolylineProperties, BIMPolylineProperties,
BIMProductPreviewProperties,
) )
from sverchok.core.node_group import SvGroupTreeNode
class Model(bonsai.core.tool.Model): class Model(bonsai.core.tool.Model):
@@ -108,11 +108,6 @@ class Model(bonsai.core.tool.Model):
assert (scene := bpy.context.scene) assert (scene := bpy.context.scene)
return scene.BIMPolylineProperties # pyright: ignore[reportAttributeAccessIssue] return scene.BIMPolylineProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod
def get_product_preview_props(cls) -> BIMProductPreviewProperties:
assert (scene := bpy.context.scene)
return scene.BIMProductPreviewProperties # pyright: ignore[reportAttributeAccessIssue]
@classmethod @classmethod
def convert_si_to_unit(cls, value: T) -> T: def convert_si_to_unit(cls, value: T) -> T:
if isinstance(value, (tuple, list)): if isinstance(value, (tuple, list)):
@@ -2571,13 +2566,21 @@ class Model(bonsai.core.tool.Model):
return [s for s in group_node.inputs if s.type != "GEOMETRY"] return [s for s in group_node.inputs if s.type != "GEOMETRY"]
@classmethod
def get_ifcsverchok_group_node(cls, node_tree: sverchok.node_tree.SverchCustomTree) -> SvGroupTreeNode:
from sverchok.core.node_group import SvGroupTreeNode
return next(n for n in node_tree.nodes if isinstance(n, SvGroupTreeNode) and n.label == "BBIM_EPG")
@classmethod @classmethod
def get_ifcsverchok_shape_output( def get_ifcsverchok_shape_output(
cls, node_tree: sverchok.node_tree.SverchCustomTree cls, node_tree: sverchok.node_tree.SverchCustomTree
) -> ifcsverchok.nodes.ifc.shape_builder.shape_output.SvSbShapeOutput: ) -> ifcsverchok.nodes.ifc.shape_builder.shape_output.SvSbShapeOutput:
from ifcsverchok.nodes.ifc.shape_builder.shape_output import SvSbShapeOutput from ifcsverchok.nodes.ifc.shape_builder.shape_output import SvSbShapeOutput
return next(n for n in node_tree.nodes if isinstance(n, SvSbShapeOutput)) group_node = cls.get_ifcsverchok_group_node(node_tree)
subtree = group_node.node_tree
return next(n for n in subtree.nodes if isinstance(n, SvSbShapeOutput))
@classmethod @classmethod
def update_mesh_from_sverchok( def update_mesh_from_sverchok(
@@ -2750,3 +2753,20 @@ class Model(bonsai.core.tool.Model):
list(nodes_to_update) list(nodes_to_update)
finally: finally:
SvIfcStore.use_bonsai_file = False SvIfcStore.use_bonsai_file = False
@classmethod
def create_bmesh_from_vertices(cls, vertices, is_closed=False):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in vertices]
if is_closed:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
new_edges.append(
bm.edges.new((new_verts[-1], new_verts[0]))
) # Add an edge between the last an first point to make it closed.
else:
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.verts.index_update()
bm.edges.index_update()
return bm
+5 -20
View File
@@ -132,10 +132,7 @@ class Search(bonsai.core.tool.Search):
else: else:
value = ifc_filter.value value = ifc_filter.value
if ( if preferences.chain_filter_with_set_operations:
preferences.chain_filter_with_set_operations
or preferences.default_filter_with_set_operations_for_globalid_and_class
):
value = value.lstrip("!") value = value.lstrip("!")
if ifc_filter.filter_mode == "SUBTRACT": if ifc_filter.filter_mode == "SUBTRACT":
value = f"!{value}" value = f"!{value}"
@@ -144,10 +141,7 @@ class Search(bonsai.core.tool.Search):
elif ifc_filter.type == "entity": elif ifc_filter.type == "entity":
value = ifc_filter.value value = ifc_filter.value
if ( if preferences.chain_filter_with_set_operations:
preferences.chain_filter_with_set_operations
or preferences.default_filter_with_set_operations_for_globalid_and_class
):
value = value.lstrip("!") value = value.lstrip("!")
if ifc_filter.filter_mode == "SUBTRACT": if ifc_filter.filter_mode == "SUBTRACT":
value = f"!{value}" value = f"!{value}"
@@ -215,19 +209,10 @@ class Search(bonsai.core.tool.Search):
if filter_index == 0: if filter_index == 0:
mode = "ADD" mode = "ADD"
else: else:
if ifc_filter.type in ["entity", "instance"]: if preferences.chain_filter_with_set_operations:
if ( mode = ifc_filter.filter_mode
preferences.chain_filter_with_set_operations
or preferences.default_filter_with_set_operations_for_globalid_and_class
):
mode = ifc_filter.filter_mode
else:
mode = "FILTER" if group_results else "ADD"
else: else:
if preferences.chain_filter_with_set_operations: mode = "FILTER" if group_results else "ADD"
mode = ifc_filter.filter_mode
else:
mode = "FILTER" if group_results else "ADD"
if mode == "ADD": if mode == "ADD":
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
+4 -4
View File
@@ -1213,18 +1213,18 @@ class Spatial(bonsai.core.tool.Spatial):
for element in elements: for element in elements:
if obj := tool.Ifc.get_object(element): if obj := tool.Ifc.get_object(element):
if obj.hide_viewport is True and is_visible: if obj.hide_viewport is True and is_visible:
obj.hide_set(False) obj.hide_viewport = False
elif obj.hide_viewport is False and not is_visible: elif obj.hide_viewport is False and not is_visible:
obj.hide_set(True) obj.hide_viewport = True
@classmethod @classmethod
def set_grid_visibility(cls, is_visible: bool) -> None: def set_grid_visibility(cls, is_visible: bool) -> None:
for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"): for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"):
if obj := tool.Ifc.get_object(element): if obj := tool.Ifc.get_object(element):
if obj.hide_viewport is True and is_visible: if obj.hide_viewport is True and is_visible:
obj.hide_set(False) obj.hide_viewport = False
elif obj.hide_viewport is False and not is_visible: elif obj.hide_viewport is False and not is_visible:
obj.hide_set(True) obj.hide_viewport = True
@classmethod @classmethod
def toggle_spaces_visibility_wired_and_textured(cls, spaces: list[ifcopenshell.entity_instance]) -> None: def toggle_spaces_visibility_wired_and_textured(cls, spaces: list[ifcopenshell.entity_instance]) -> None:
+37 -5
View File
@@ -100,6 +100,7 @@ class System(bonsai.core.tool.System):
@classmethod @classmethod
def create_empty_at_cursor_with_element_orientation(cls, element: ifcopenshell.entity_instance) -> bpy.types.Object: def create_empty_at_cursor_with_element_orientation(cls, element: ifcopenshell.entity_instance) -> bpy.types.Object:
# Is this necessary anymore?
element_obj = tool.Ifc.get_object(element) element_obj = tool.Ifc.get_object(element)
obj = bpy.data.objects.new("Port", None) obj = bpy.data.objects.new("Port", None)
obj.matrix_world = element_obj.matrix_world obj.matrix_world = element_obj.matrix_world
@@ -107,6 +108,26 @@ class System(bonsai.core.tool.System):
bpy.context.scene.collection.objects.link(obj) bpy.context.scene.collection.objects.link(obj)
return obj return obj
@classmethod
def create_port_at_cursor(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
ifc_file = tool.Ifc.get()
element_obj = tool.Ifc.get_object(element)
port = ifcopenshell.api.system.add_port(ifc_file, element=element)
port.FlowDirection = "NOTDEFINED"
port.PredefinedType = "USERDEFINED"
systems = ifcopenshell.util.system.get_element_systems(element)
system = systems[0] if systems else None
port.SystemType = getattr(system, "PredefinedType", None) or "USERDEFINED"
matrix = element_obj.matrix_world.copy()
matrix.translation = bpy.context.scene.cursor.matrix.translation
ifcopenshell.api.geometry.edit_object_placement(ifc_file, product=port, matrix=matrix, is_si=True)
return port
@classmethod @classmethod
def delete_element_objects(cls, elements: list[ifcopenshell.entity_instance]) -> None: def delete_element_objects(cls, elements: list[ifcopenshell.entity_instance]) -> None:
for element in elements: for element in elements:
@@ -192,11 +213,22 @@ class System(bonsai.core.tool.System):
ifc_importer.process_context_filter() ifc_importer.process_context_filter()
ifc_importer.create_generic_elements(set(ports_to_create)) ifc_importer.create_generic_elements(set(ports_to_create))
container = ifcopenshell.util.element.get_container(element) if element.is_a("IfcTypeProduct"):
if container: target_collection = None
collection = tool.Blender.get_object_bim_props(tool.Ifc.get_object(container)).collection for collection in obj.users_collection:
ifc_importer.collections[container.GlobalId] = collection target_collection = collection
ifc_importer.place_objects_in_collections() break
if target_collection:
for port_obj in ifc_importer.added_data.values():
if isinstance(port_obj, bpy.types.Object):
tool.Collector.link_collection_object_safe(target_collection, port_obj)
else:
container = ifcopenshell.util.element.get_container(element)
if container:
collection = tool.Blender.get_object_bim_props(tool.Ifc.get_object(container)).collection
ifc_importer.collections[container.GlobalId] = collection
ifc_importer.place_objects_in_collections()
for port_obj in ifc_importer.added_data.values(): for port_obj in ifc_importer.added_data.values():
assert isinstance(port_obj, bpy.types.Object) assert isinstance(port_obj, bpy.types.Object)
+1 -1
View File
@@ -337,7 +337,7 @@ def an_untestable_scenario():
def an_empty_blender_session(): def an_empty_blender_session():
IfcStore.purge() IfcStore.purge()
if not PYTEST_BLENDER_NO_BACKGROUND: if not PYTEST_BLENDER_NO_BACKGROUND:
bpy.ops.wm.read_homefile(app_template="") bpy.ops.wm.read_homefile(app_template="", use_factory_startup=True)
if len(bpy.data.objects) > 0: if len(bpy.data.objects) > 0:
bpy.data.batch_remove(bpy.data.objects) bpy.data.batch_remove(bpy.data.objects)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
+4 -5
View File
@@ -153,11 +153,10 @@ class TestAddPort:
def test_run(self, ifc, system): def test_run(self, ifc, system):
system.get_ports("element").should_be_called().will_return(["port"]) system.get_ports("element").should_be_called().will_return(["port"])
system.load_ports("element", ["port"]).should_be_called() system.load_ports("element", ["port"]).should_be_called()
system.create_empty_at_cursor_with_element_orientation("element").should_be_called().will_return("obj") #system.create_empty_at_cursor_with_element_orientation("element").should_be_called().will_return("obj")
system.run_root_assign_class( #system.run_root_assign_class(obj="obj", ifc_class="IfcDistributionPort", should_add_representation=False).should_be_called().will_return("port")
obj="obj", ifc_class="IfcDistributionPort", should_add_representation=False system.create_port_at_cursor("element").should_be_called().will_return("port")
).should_be_called().will_return("port") system.load_ports("element", ["port"]).should_be_called()
ifc.run("system.assign_port", element="element", port="port").should_be_called()
subject.add_port(ifc, system, element="element") subject.add_port(ifc, system, element="element")
+13
View File
@@ -103,6 +103,19 @@ class TestCreateEmptyAtCursorWithElementOrientation(NewFile):
obj = subject.create_empty_at_cursor_with_element_orientation(element) obj = subject.create_empty_at_cursor_with_element_orientation(element)
assert obj.matrix_world == bpy.context.scene.cursor.matrix assert obj.matrix_world == bpy.context.scene.cursor.matrix
class TestCreatePortAtCursor(NewFile):
def test_run(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc().set(ifc)
system = ifcopenshell.api.system.add_system(ifc)
element = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcDuctSegment")
ifcopenshell.api.system.assign_system(ifc, products=[element], system=system)
obj = tool.Ifc.link(element, bpy.data.objects.new("Object", None))
port = subject.create_port_at_cursor(element)
assert port.is_a("IfcDistributionPort")
assert ifcopenshell.util.system.get_ports(element) == [port]
class TestDeleteElementObjects(NewFile): class TestDeleteElementObjects(NewFile):
def test_run(self): def test_run(self):
+122
View File
@@ -184,6 +184,78 @@ class ClassPropertyContractV1(TypedDict):
qudtCodes: NotRequired[list[str]] qudtCodes: NotRequired[list[str]]
class ClassRelationItemContractV1(TypedDict):
relationType: str
classUri: str
className: NotRequired[str]
fraction: NotRequired[float]
dictionaryUri: NotRequired[str]
class ClassRelationsContractV1(TypedDict):
totalCount: NotRequired[int]
offset: NotRequired[int]
count: NotRequired[int]
classUri: NotRequired[str]
areReversedRelations: NotRequired[bool]
classRelations: NotRequired[list[ClassRelationItemContractV1]]
class ClassPropertiesContractV1(TypedDict):
classUri: NotRequired[str]
totalCount: NotRequired[int]
offset: NotRequired[int]
count: NotRequired[int]
classProperties: list[ClassPropertyContractV1]
class ClassPropertyItemContractV1(TypedDict):
name: str
propertySet: str
uri: str
description: NotRequired[str]
definition: NotRequired[str]
dataType: NotRequired[str]
dimension: NotRequired[str]
dimensionLength: NotRequired[int]
dimensionMass: NotRequired[int]
dimensionTime: NotRequired[int]
dimensionElectricCurrent: NotRequired[int]
dimensionThermodynamicTemperature: NotRequired[int]
dimensionAmountOfSubstance: NotRequired[int]
dimensionLuminousIntensity: NotRequired[int]
dynamicParameterPropertyCodes: NotRequired[list[str]]
example: NotRequired[str]
isDynamic: NotRequired[bool]
isRequired: NotRequired[bool]
isWritable: NotRequired[bool]
maxExclusive: NotRequired[float]
maxInclusive: NotRequired[float]
minExclusive: NotRequired[float]
minInclusive: NotRequired[float]
pattern: NotRequired[str]
physicalQuantity: NotRequired[str]
allowedValues: NotRequired[list[ClassPropertyValueItemContractV1]]
predefinedValue: NotRequired[str]
propertyCode: NotRequired[str]
propertyDictionaryName: NotRequired[str]
propertyDictionaryUri: NotRequired[str]
propertyUri: NotRequired[str]
propertyStatus: NotRequired[str]
propertyValueKind: NotRequired[str]
symbol: NotRequired[str]
units: NotRequired[list[str]]
qudtCodes: NotRequired[list[str]]
class ClassPropertyValueItemContractV1(TypedDict):
uri: NotRequired[str]
code: NotRequired[str]
value: str
description: NotRequired[str]
sortNumber: NotRequired[int]
class PropertyContractV5(TypedDict): class PropertyContractV5(TypedDict):
dictionaryUri: NotRequired[str] dictionaryUri: NotRequired[str]
activationDateUtc: str activationDateUtc: str
@@ -710,6 +782,56 @@ class Client:
params = {k: v for k, v in params.items() if v is not None} params = {k: v for k, v in params.items() if v is not None}
return self.get(endpoint, params) return self.get(endpoint, params)
def get_class_relations(
self,
class_uri: str,
get_reverse_relations: bool = False,
search_text: str = "",
offset: int = 0,
limit: int = 1000,
language_code: str = "",
version=1,
) -> ClassRelationsContractV1:
"""
Get class relations or reverse relations (paginated)
"""
endpoint = f"Class/Relations/v{version}"
params = {
"ClassUri": class_uri,
"GetReverseRelations": get_reverse_relations,
"SearchText": search_text,
"Offset": offset,
"Limit": limit,
"languageCode": language_code,
}
return self.get(endpoint, params)
def get_class_properties(
self,
class_uri: str,
property_set: str = "",
property_code: str = "",
search_text: str = "",
offset: int = 0,
limit: int = 1000,
language_code: str = "",
version=1,
) -> ClassPropertiesContractV1:
"""
Get class properties (paginated)
"""
endpoint = f"Class/Properties/v{version}"
params = {
"ClassUri": class_uri,
"PropertySet": property_set,
"PropertyCode": property_code,
"SearchText": search_text,
"Offset": offset,
"Limit": limit,
"languageCode": language_code,
}
return self.get(endpoint, params)
def get_property(self, uri, language_code="", version: int = 5) -> PropertyContractV5: def get_property(self, uri, language_code="", version: int = 5) -> PropertyContractV5:
""" """
Get Property details. Get Property details.
+16
View File
@@ -37,6 +37,22 @@ def test_get_class():
] ]
def test_get_class_relations():
uri_light_fixture = next(l for l in get_ifc_classes()["classes"] if "IfcLightFixture" == l["code"])["uri"]
ifc4x3_light_fixture_relations = client.get_class_properties(uri_light_fixture, True)
assert "Electrical unit for light-line system" and "Tubelight system" in [
r["className"] for r in ifc4x3_light_fixture_relations["classRelations"]
]
def test_get_class_properties():
uri_light_fixture = next(l for l in get_ifc_classes()["classes"] if "IfcLightFixture" == l["code"])["uri"]
ifc4x3_light_fixture_properties = client.get_class_properties(uri_light_fixture)
assert "Maintenance Factor" and "Light Fixture Mounting Type" in [
l["name"] for l in ifc4x3_light_fixture_properties["classProperties"]
]
def test_search_class(): def test_search_class():
ss_heat_pump_sys = client.search_class("Ss_60_40_36", [nbs_uri]) ss_heat_pump_sys = client.search_class("Ss_60_40_36", [nbs_uri])
li = [l + "source heat pump systems" for l in ["Air ", "Ground ", "Water "]] li = [l + "source heat pump systems" for l in ["Air ", "Ground ", "Water "]]
@@ -340,8 +340,12 @@ In IFC, meshes may be stored as **Faceted BReps**, **Tessellations**, or
# These vertices and faces represent a 2m square 1m high pyramid in SI units. # These vertices and faces represent a 2m square 1m high pyramid in SI units.
# Note how they are nested lists. Each nested list represents a "mesh". There may be multiple meshes. # Note how they are nested lists. Each nested list represents a "mesh". There may be multiple meshes.
vertices = [[(0.,0.,0.), (0.,2.,0.), (2.,2.,0.), (2.,0.,0.), (1.,1.,1.)]] vertices = [
faces = [[(0,1,2,3), (0,4,1), (1,4,2), (2,4,3), (3,4,0)]] [(0.,0.,0.), (0.,2.,0.), (2.,2.,0.), (2.,0.,0.), (1.,1.,1.)] # A single mesh
]
faces = [
[(0,1,2,3), (0,4,1), (1,4,2), (2,4,3), (3,4,0)] # A single mesh
]
representation = ifcopenshell.api.geometry.add_mesh_representation(model, context=body, vertices=vertices, faces=faces) representation = ifcopenshell.api.geometry.add_mesh_representation(model, context=body, vertices=vertices, faces=faces)
.. image:: images/mesh-representation.png .. image:: images/mesh-representation.png
@@ -381,6 +381,7 @@ class Usecase:
def append_type_product(self): def append_type_product(self):
self.whitelisted_inverse_attributes = { self.whitelisted_inverse_attributes = {
"IfcObjectDefinition": ["HasAssociations"], "IfcObjectDefinition": ["HasAssociations"],
"IfcDistributionElementType": ["IsNestedBy"],
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"], self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
"IfcRepresentationItem": ["StyledByItem", "LayerAssignment"], "IfcRepresentationItem": ["StyledByItem", "LayerAssignment"],
"IfcRepresentation": ["LayerAssignments"], "IfcRepresentation": ["LayerAssignments"],
@@ -397,6 +398,7 @@ class Usecase:
"IfcObjectDefinition": ["HasAssociations"], "IfcObjectDefinition": ["HasAssociations"],
"IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"], "IfcObject": ["IsDefinedBy.IfcRelDefinesByProperties"],
"IfcElement": ["HasOpenings"], "IfcElement": ["HasOpenings"],
"IfcDistributionElement": ["IsNestedBy"],
self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"], self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"],
"IfcRepresentationItem": [ "IfcRepresentationItem": [
"StyledByItem", "StyledByItem",
@@ -569,6 +571,8 @@ class Usecase:
return False return False
elif element.is_a("IfcRoot") and self.by_guid(element.GlobalId) is not None: elif element.is_a("IfcRoot") and self.by_guid(element.GlobalId) is not None:
return False return False
elif element.is_a("IfcDistributionPort"):
return False
elif element.is_a(self.target_class): elif element.is_a(self.target_class):
return True return True
elif self.target_class == "IfcProduct" and element.is_a("IfcTypeProduct"): elif self.target_class == "IfcProduct" and element.is_a("IfcTypeProduct"):
@@ -65,8 +65,6 @@ def disconnect_port(file: ifcopenshell.file, port: ifcopenshell.entity_instance)
rels += port.ConnectedFrom or () rels += port.ConnectedFrom or ()
for rel in rels: for rel in rels:
rel.RelatingPort.FlowDirection = None
rel.RelatedPort.FlowDirection = None
history = rel.OwnerHistory history = rel.OwnerHistory
file.remove(rel) file.remove(rel)
if history: if history:
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Literal
import bpy import bpy
import ifcopenshell import ifcopenshell
from ifcopenshell.util.shape_builder import ShapeBuilder from ifcopenshell.util.shape_builder import ShapeBuilder
from sverchok.core.sockets import SvVerticesSocket
from sverchok.data_structure import updateNode from sverchok.data_structure import updateNode
from sverchok.node_tree import SverchCustomTreeNode from sverchok.node_tree import SverchCustomTreeNode
@@ -62,6 +63,7 @@ class SvIfcSbExtrude(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.Sv
self.inputs, self.inputs,
"Position", "Position",
data_type="list[list[tuple[float, float, float]]]", data_type="list[list[tuple[float, float, float]]]",
socket_type=SvVerticesSocket,
) )
helper.create_socket( helper.create_socket(
self.outputs, self.outputs,
+4 -12
View File
@@ -300,23 +300,15 @@ class Specification:
if not is_applicable: if not is_applicable:
continue continue
self.applicable_entities.append(element) self.applicable_entities.append(element)
for facet in self.requirements: if self.maxOccurs != 0: # Requirements are skipped for prohibited applicability
result = facet(element) for facet in self.requirements:
is_pass = bool(result) result = facet(element)
if self.maxOccurs != 0: # This is a required or optional specification if bool(result):
if is_pass:
self.passed_entities.add(element) self.passed_entities.add(element)
facet.passed_entities.add(element) facet.passed_entities.add(element)
else: else:
self.failed_entities.add(element) self.failed_entities.add(element)
facet.failures.append(FacetFailure(element=element, reason=str(result))) facet.failures.append(FacetFailure(element=element, reason=str(result)))
else: # This is a prohibited specification
if is_pass:
self.failed_entities.add(element)
facet.failures.append(FacetFailure(element=element, reason=str(result)))
else:
self.passed_entities.add(element)
facet.passed_entities.add(element)
self.status = True self.status = True
for facet in self.requirements: for facet in self.requirements:
+32 -2
View File
@@ -81,10 +81,12 @@ class ResultsSpecification(TypedDict):
description: str description: str
instructions: str instructions: str
status: bool status: bool
is_skipped: bool
is_ifc_version: bool is_ifc_version: bool
total_applicable: int total_applicable: int
total_applicable_pass: int total_applicable_pass: int
total_applicable_fail: int total_applicable_fail: int
applicable_entities: list[ResultsEntity]
percent_applicable_pass: ResultsPercent percent_applicable_pass: ResultsPercent
total_checks: int total_checks: int
total_checks_pass: int total_checks_pass: int
@@ -367,16 +369,24 @@ class Json(Reporter):
cardinality = "optional" cardinality = "optional"
elif specification.minOccurs == 0 and specification.maxOccurs == 0: elif specification.minOccurs == 0 and specification.maxOccurs == 0:
cardinality = "prohibited" cardinality = "prohibited"
elif specification.minOccurs >= 1:
# Any minimum occurrence >= 1 means the specification is required
cardinality = "required"
else:
# minOccurs == 0 with any other maxOccurs value means optional
cardinality = "optional"
return ResultsSpecification( return ResultsSpecification(
name=specification.name, name=specification.name,
description=specification.description, description=specification.description,
instructions=specification.instructions, instructions=specification.instructions,
status=specification.status, status=specification.status,
is_skipped=cardinality == "optional" and total_checks == 0,
is_ifc_version=specification.is_ifc_version, is_ifc_version=specification.is_ifc_version,
total_applicable=total_applicable, total_applicable=total_applicable,
total_applicable_pass=total_applicable_pass, total_applicable_pass=total_applicable_pass,
total_applicable_fail=total_applicable - total_applicable_pass, total_applicable_fail=total_applicable - total_applicable_pass,
applicable_entities=self.report_applicable_entities(specification),
percent_applicable_pass=percent_applicable_pass, percent_applicable_pass=percent_applicable_pass,
total_checks=total_checks, total_checks=total_checks,
total_checks_pass=total_checks_pass, total_checks_pass=total_checks_pass,
@@ -387,6 +397,24 @@ class Json(Reporter):
requirements=requirements, requirements=requirements,
) )
def report_applicable_entities(self, specification: Specification) -> list[ResultsEntity]:
return [
ResultsEntity(
{
"element": e,
"element_type": ifcopenshell.util.element.get_type(e),
"class": e.is_a(),
"predefined_type": ifcopenshell.util.element.get_predefined_type(e),
"name": getattr(e, "Name", None),
"description": getattr(e, "Description", None),
"id": e.id(),
"global_id": getattr(e, "GlobalId", None),
"tag": getattr(e, "Tag", None),
}
)
for e in specification.applicable_entities
]
def report_passed_entities(self, requirement: Facet) -> list[ResultsEntity]: def report_passed_entities(self, requirement: Facet) -> list[ResultsEntity]:
return [ return [
ResultsEntity( ResultsEntity(
@@ -448,11 +476,13 @@ class Html(Json):
def report(self) -> None: def report(self) -> None:
super().report() super().report()
for spec in self.results["specifications"]: for spec in self.results["specifications"]:
if spec["cardinality"] == "optional" and spec["total_checks"] == 0:
spec["is_skipped"] = True
spec["is_prohibited"] = spec["cardinality"] == "prohibited" spec["is_prohibited"] = spec["cardinality"] == "prohibited"
spec["cardinality"] = spec["cardinality"].capitalize() spec["cardinality"] = spec["cardinality"].capitalize()
spec["has_requirements"] = bool(spec["requirements"]) spec["has_requirements"] = bool(spec["requirements"])
total_applicable_entities = len(spec["applicable_entities"])
spec["applicable_entities"] = self.limit_entities(spec["applicable_entities"])
spec["has_omitted_applicable"] = total_applicable_entities > self.entity_limit
spec["total_omitted_applicable"] = total_applicable_entities - self.entity_limit
for requirement in spec["requirements"]: for requirement in spec["requirements"]:
total_passed_entities = len(requirement["passed_entities"]) total_passed_entities = len(requirement["passed_entities"])
total_failed_entities = len(requirement["failed_entities"]) total_failed_entities = len(requirement["failed_entities"])
+39 -1
View File
@@ -152,7 +152,6 @@
<p> <p>
<strong>Requirements</strong> <strong>Requirements</strong>
</p> </p>
{{/has_requirements}}
<ol> <ol>
{{#requirements}} {{#requirements}}
<li class="{{^total_checks}}skipped{{/total_checks}}{{#total_checks}}{{#status}}pass{{/status}}{{^status}}fail{{/status}}{{/total_checks}}"> <li class="{{^total_checks}}skipped{{/total_checks}}{{#total_checks}}{{#status}}pass{{/status}}{{^status}}fail{{/status}}{{/total_checks}}">
@@ -252,6 +251,45 @@
</li> </li>
{{/requirements}} {{/requirements}}
</ol> </ol>
{{/has_requirements}}
{{#is_prohibited}}
{{#total_applicable}}
<table class="fail">
<thead>
<tr>
<th>Class</th>
<th>PredefinedType</th>
<th>Name</th>
<th>Description</th>
<th>GlobalId</th>
<th>Tag</th>
</tr>
</thead>
<tbody>
{{#applicable_entities}}
<tr>
<td>{{class}}</td>
<td>{{predefined_type}}</td>
<td>{{name}}</td>
<td>{{description}}</td>
<td>{{global_id}}</td>
<td>{{tag}}</td>
</tr>
{{#extra_of_type}}
<tr>
<td colspan="7">... {{extra_of_type}} more of the same element type ({{type_name}} with Tag {{type_tag}} and GlobalId {{type_global_id}}) not shown ...</td>
</tr>
{{/extra_of_type}}
{{/applicable_entities}}
{{#has_omitted_applicable}}
<tr>
<td colspan="7"> ... {{total_omitted_applicable}} more failing elements not shown out of {{total_applicable}} total ...</td>
</tr>
{{/has_omitted_applicable}}
</tbody>
</table>
{{/total_applicable}}
{{/is_prohibited}}
</div> </div>
</section> </section>
{{/specifications}} {{/specifications}}
+8 -25
View File
@@ -1955,9 +1955,9 @@
} }
}, },
"node_modules/minizlib": { "node_modules/minizlib": {
"version": "3.0.2", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1967,22 +1967,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mode-watcher": { "node_modules/mode-watcher": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz",
@@ -2867,17 +2851,16 @@
} }
}, },
"node_modules/tar": { "node_modules/tar": {
"version": "7.4.3", "version": "7.5.6",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
"dev": true, "dev": true,
"license": "ISC", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/fs-minipass": "^4.0.0", "@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0", "chownr": "^3.0.0",
"minipass": "^7.1.2", "minipass": "^7.1.2",
"minizlib": "^3.0.1", "minizlib": "^3.1.0",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0" "yallist": "^5.0.0"
}, },
"engines": { "engines": {
@@ -50,13 +50,7 @@
function getSpecificationStatus(specIndex, auditData) { function getSpecificationStatus(specIndex, auditData) {
const spec = auditData.specifications[specIndex]; const spec = auditData.specifications[specIndex];
if (!spec) return null; if (!spec) return null;
return spec.is_skipped ? 'skipped' : spec.status;
// If no applicable elements and no checks, and it passed, it's actually skipped
if (spec.total_applicable === 0 && spec.total_checks === 0 && spec.status === true) {
return 'skipped';
}
return spec.status;
} }
function getSpecificationStats(specIndex, auditData) { function getSpecificationStats(specIndex, auditData) {
@@ -79,11 +73,13 @@
const status = getSpecificationStatus(specIndex, auditData); const status = getSpecificationStatus(specIndex, auditData);
if (status === 'skipped') { if (status === 'skipped') {
return "Skipped because no applicable entities were found and the cardinality is OPTIONAL or PROHIBITED"; return "Skipped because no applicable entities were found and the cardinality is OPTIONAL";
} }
if (status === false) { // Failed if (status === false) { // Failed
if (spec.total_applicable === 0) { if (spec.cardinality === 'prohibited') {
return `Failed because ${spec.total_applicable} prohibited entities were found`;
} else if (spec.total_applicable === 0) {
return "Failed because no applicable entities were found but the cardinality is REQUIRED"; return "Failed because no applicable entities were found but the cardinality is REQUIRED";
} else { } else {
const failedChecks = spec.total_checks - spec.total_checks_pass; const failedChecks = spec.total_checks - spec.total_checks_pass;
@@ -240,19 +236,30 @@
{#if "@description" in spec} {#if "@description" in spec}
<p class="spec-description">{spec["@description"]}</p> <p class="spec-description">{spec["@description"]}</p>
{/if} {/if}
<div class="spec-stats">
{#if spec.applicability["@minOccurs"] === 1 && spec.applicability["@maxOccurs"] === 'unbounded'}
<span class="stat-item">Required</span>
{/if}
{#if spec.applicability["@minOccurs"] === 0 && spec.applicability["@maxOccurs"] === 'unbounded'}
<span class="stat-item">Optional</span>
{/if}
{#if spec.applicability["@minOccurs"] === 0 && spec.applicability["@maxOccurs"] === 0}
<span class="stat-item">Prohibited</span>
{/if}
{#if auditReport}
{@const stats = getSpecificationStats(index, auditReport.data)}
{@const status = getSpecificationStatus(index, auditReport.data)}
{#if stats && spec.applicability["@maxOccurs"] !== 0 && status !== 'skipped'}
<span class="stat-item">Checks: {stats.checksPassed}/{stats.checksTotal}</span>
<span class="stat-item">Requirements: {stats.requirementsPassed}/{stats.requirements}</span>
{/if}
{/if}
</div>
{#if auditReport} {#if auditReport}
{@const reason = getSpecificationReason(index, auditReport.data)} {@const reason = getSpecificationReason(index, auditReport.data)}
{#if reason} {#if reason}
<p class="spec-reason">{reason}</p> <p class="spec-reason">{reason}</p>
{/if} {/if}
{@const stats = getSpecificationStats(index, auditReport.data)}
{@const status = getSpecificationStatus(index, auditReport.data)}
{#if stats && status !== 'skipped'}
<div class="spec-stats">
<span class="stat-item">Checks: {stats.checksPassed}/{stats.checksTotal}</span>
<span class="stat-item">Requirements: {stats.requirementsPassed}/{stats.requirements}</span>
</div>
{/if}
{/if} {/if}
</div> </div>
<div class="spec-actions"> <div class="spec-actions">
@@ -292,9 +299,105 @@
{/if} {/if}
{/each} {/each}
</div> </div>
{#if auditReport}
{@const status = getSpecificationStatus(index, auditReport.data)}
{#if ! status && spec.applicability["@maxOccurs"] == 0}
{@const specReport = auditReport.data.specifications[index]}
<div class="entity-tables">
{#if specReport.applicable_entities && specReport.applicable_entities.length > 0}
<div class="entity-table-section fail">
<h4>Failed Elements ({specReport.applicable_entities.length})</h4>
<div class="entity-table-container">
<Tooltip.Provider>
<table class="entity-table">
<thead>
<tr>
<th>Class</th>
<th>PredefinedType</th>
<th>Name</th>
<th>Description</th>
<th>Warning</th>
<th>GlobalId</th>
<th>Tag</th>
</tr>
</thead>
<tbody>
{#each specReport.applicable_entities.slice(0, 10) as entity}
<tr>
<td>{entity.class}</td>
<td>{entity.predefined_type || '-'}</td>
<td>
<Tooltip.Root>
<Tooltip.Trigger>
<div class="truncated-text">{entity.name || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.name || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
<td>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<div class="truncated-text">{entity.description || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.description || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
<td>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<div class="truncated-text">{entity.reason || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.reason || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
<td>
<Tooltip.Root>
<Tooltip.Trigger>
<div class="truncated-text">{entity.global_id || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.global_id || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
<td>
<Tooltip.Root>
<Tooltip.Trigger>
<div class="truncated-text">{entity.tag || '-'}</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{entity.tag || '-'}</p>
</Tooltip.Content>
</Tooltip.Root>
</td>
</tr>
{/each}
{#if specReport.applicable_entities.length > 10}
<tr class="more-row">
<td colspan="7">... {specReport.applicable_entities.length - 10} more failing elements not shown ...</td>
</tr>
{/if}
</tbody>
</table>
</Tooltip.Provider>
</div>
</div>
{/if}
</div>
{/if}
{/if}
</div> </div>
<!-- Requirements Section --> <!-- Requirements Section -->
{#if Array.isArray(spec.requirements) && spec.requirements.length > 0}
<div class="facet-section"> <div class="facet-section">
<h3>Requirements</h3> <h3>Requirements</h3>
@@ -497,6 +600,7 @@
{/each} {/each}
</div> </div>
</div> </div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
@@ -1128,4 +1232,4 @@
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
} }
</style> </style>