diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index cc5972b9b3..22b825209a 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -407,7 +407,7 @@ class IfcImporter: matrix[2][3] *= self.unit_scale # Single swept disk solids (e.g. rebar) are better natively represented as beveled curves - if self.is_native_swept_disk_solid(element, resolved_representation): + if tool.Loader.is_native_swept_disk_solid(element, resolved_representation): self.native_data[element.GlobalId] = { "matrix": matrix, "context": context, @@ -418,24 +418,6 @@ class IfcImporter: return True return False - def is_native_swept_disk_solid( - self, element: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance - ) -> bool: - items = [i["item"] for i in ifcopenshell.util.representation.resolve_items(representation)] - if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"): - if tool.Blender.Modifier.is_railing(element): - return False - return True - elif len(items) and ( # See #2508 why we accommodate for invalid IFCs here - items[0].is_a("IfcSweptDiskSolid") - and len({i.is_a() for i in items}) == 1 - and len({i.Radius for i in items}) == 1 - ): - if tool.Blender.Modifier.is_railing(element): - return False - return True - return False - def calculate_model_offset(self) -> None: props = bpy.context.scene.BIMGeoreferenceProperties if self.ifc_import_settings.false_origin_mode == "MANUAL": @@ -543,15 +525,12 @@ class IfcImporter: curve_thickness = None if mesh is None: if native_data["type"] == "IfcSweptDiskSolid": - mesh, curve_thickness = self.create_native_swept_disk_solid(element, mesh_name, native_data) + mesh, curve_thickness = tool.Loader.create_native_swept_disk_solid(element, mesh_name, native_data) tool.Ifc.link(tool.Ifc.get().by_id(native_data["geometry_id"]), mesh) mesh.name = mesh_name self.meshes[mesh_name] = mesh obj = self.create_product(element, mesh=mesh) - if curve_thickness: - modifier = obj.modifiers.new("Curve Thickness", type="SOLIDIFY") - assert isinstance(modifier, bpy.types.SolidifyModifier) - modifier.thickness = curve_thickness + tool.Loader.setup_native_swept_disk_solid_thickness(obj, curve_thickness) print("Done creating geometry") @@ -851,89 +830,6 @@ class IfcImporter: def load_existing_meshes(self) -> None: self.meshes.update({m.name: m for m in bpy.data.meshes}) - def create_native_swept_disk_solid( - self, element: ifcopenshell.entity_instance, mesh_name: str, native_data: dict[str, Any] - ) -> tuple[bpy.types.Curve, Union[float, None]]: - """Create Blender curve based on element using IfcSweptDiskAreaSolid. - - :return: created curve and it's thickness (suppose to add the thickness to the object - using solidify modifier). Returns `None` instead of thickness if disk has no inner radius - or if it's invalid. - """ - # TODO: georeferencing? - curve = bpy.data.curves.new(mesh_name, type="CURVE") - curve.dimensions = "3D" - curve.resolution_u = 2 - - rep_items = ifcopenshell.util.representation.resolve_items(native_data["representation"]) - - # Find item styles and add them to the curve. - material_style = None - material = ifcopenshell.util.element.get_material(element) - if material: - material_style = tool.Material.get_style(material) - item_styles: list[Union[bpy.types.Material, None]] = [] - for item_data in rep_items: - item = item_data["item"] - item_style = tool.Style.get_representation_item_style(item) or material_style - if item_style is not None: - item_style = tool.Ifc.get_object(item_style) - assert isinstance(item_style, bpy.types.Material) - item_styles.append(item_style) - item_styles_unique = list(set(item_styles)) - for item_style in item_styles_unique: - curve.materials.append(item_style) - use_same_material_index = len(item_styles_unique) < 2 - - def new_polyline(item_style: Union[bpy.types.Material, None]) -> bpy.types.Spline: - if use_same_material_index: - material_index = 0 - else: - material_index = item_styles_unique.index(item_style) - - polyline = curve.splines.new("POLY") - polyline.material_index = material_index - return polyline - - for item_data, item_style in zip(rep_items, item_styles): - item = item_data["item"] - - polyline = new_polyline(item_style) - matrix = item_data["matrix"] - matrix[0][3] *= self.unit_scale - matrix[1][3] *= self.unit_scale - matrix[2][3] *= self.unit_scale - - # TODO: start param, and end param - geometry = tool.Loader.create_generic_shape(item.Directrix) - if not geometry: - continue - e = geometry.edges - v = geometry.verts - vertices = [list(matrix @ [v[i], v[i + 1], v[i + 2], 1]) for i in range(0, len(v), 3)] - edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)] - v2 = None - for edge in edges: - v1 = vertices[edge[0]] - if v1 != v2: - polyline = new_polyline(item_style) - polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v1) - v2 = vertices[edge[1]] - polyline.points.add(1) - polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v2) - - curve.bevel_depth = self.unit_scale * item.Radius - thickness = None - if (inner_radius := item.InnerRadius) and (thickness := max(item.Radius - inner_radius, 0)): - thickness *= self.unit_scale - curve.use_fill_caps = False - # Shade flat. - for spline in curve.splines: - spline.use_smooth = False - else: - curve.use_fill_caps = True - return curve, thickness - def merge_materials_by_colour(self): cleaned_materials = {} for m in bpy.data.materials: diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 37c898b500..47f8a8d47e 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -703,6 +703,30 @@ class Geometry(bonsai.core.tool.Geometry): cls.clear_modifiers(obj) cls.clear_cache(element) + # Import swept disk solids as Blender curves if possible. + elements_without_openings = {e for e in elements if not getattr(e, "HasOpenings", False)} + curve, curve_thickness = None, None + for element_ in elements_without_openings | element_types: + if not tool.Loader.is_native_swept_disk_solid(element, representation): + continue + if curve is None: + mesh_name = tool.Loader.get_mesh_name(context.id(), representation.id()) + native_data = { + "representation": representation, + # TODO: calculate mapped item matrix. + "matrix": np.eye(4), + } + curve, curve_thickness = tool.Loader.create_native_swept_disk_solid(element, mesh_name, native_data) + tool.Ifc.link(representation, curve) + obj = tool.Ifc.get_object(element) + change_data(obj, element, curve) + tool.Loader.setup_native_swept_disk_solid_thickness(obj, curve_thickness) + elements.discard(element_) + element_types.discard(element_) + + if not elements and not element_types: + return + # Fallback to custom methods as IOS doesn't process points, see #5218. if representation.RepresentationType in ("PointCloud", "Point"): mesh = tool.Loader.create_point_cloud_mesh(representation) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index b9a34ed31c..9b8cee1429 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1031,3 +1031,116 @@ class Loader(bonsai.core.tool.Loader): return assert name and uri tool.Bsdd.set_active_bsdd(name, uri) + + @classmethod + def is_native_swept_disk_solid( + cls, element: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance + ) -> bool: + items = [i["item"] for i in ifcopenshell.util.representation.resolve_items(representation)] + if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"): + if tool.Blender.Modifier.is_railing(element): + return False + return True + elif len(items) and ( # See #2508 why we accommodate for invalid IFCs here + items[0].is_a("IfcSweptDiskSolid") + and len({i.is_a() for i in items}) == 1 + and len({i.Radius for i in items}) == 1 + ): + if tool.Blender.Modifier.is_railing(element): + return False + return True + return False + + @classmethod + def create_native_swept_disk_solid( + cls, element: ifcopenshell.entity_instance, mesh_name: str, native_data: dict[str, Any] + ) -> tuple[bpy.types.Curve, Union[float, None]]: + """Create Blender curve based on element using IfcSweptDiskAreaSolid. + + :return: created curve and it's thickness (suppose to add the thickness to the object + using solidify modifier). Returns `None` instead of thickness if disk has no inner radius + or if it's invalid. + """ + # TODO: georeferencing? + ifc_file = tool.Ifc.get() + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) + curve = bpy.data.curves.new(mesh_name, type="CURVE") + curve.dimensions = "3D" + curve.resolution_u = 2 + + rep_items = ifcopenshell.util.representation.resolve_items(native_data["representation"]) + + # Find item styles and add them to the curve. + material_style = None + material = ifcopenshell.util.element.get_material(element) + if material: + material_style = tool.Material.get_style(material) + item_styles: list[Union[bpy.types.Material, None]] = [] + for item_data in rep_items: + item = item_data["item"] + item_style = tool.Style.get_representation_item_style(item) or material_style + if item_style is not None: + item_style = tool.Ifc.get_object(item_style) + assert isinstance(item_style, bpy.types.Material) + item_styles.append(item_style) + item_styles_unique = list(set(item_styles)) + for item_style in item_styles_unique: + curve.materials.append(item_style) + use_same_material_index = len(item_styles_unique) < 2 + + def new_polyline(item_style: Union[bpy.types.Material, None]) -> bpy.types.Spline: + if use_same_material_index: + material_index = 0 + else: + material_index = item_styles_unique.index(item_style) + + polyline = curve.splines.new("POLY") + polyline.material_index = material_index + return polyline + + for item_data, item_style in zip(rep_items, item_styles): + item = item_data["item"] + + polyline = new_polyline(item_style) + matrix = item_data["matrix"] + matrix[0][3] *= unit_scale + matrix[1][3] *= unit_scale + matrix[2][3] *= unit_scale + + # TODO: start param, and end param + geometry = tool.Loader.create_generic_shape(item.Directrix) + if not geometry: + continue + e = geometry.edges + v = geometry.verts + vertices = [list(matrix @ [v[i], v[i + 1], v[i + 2], 1]) for i in range(0, len(v), 3)] + edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)] + v2 = None + for edge in edges: + v1 = vertices[edge[0]] + if v1 != v2: + polyline = new_polyline(item_style) + polyline.points[-1].co = native_data["matrix"] @ Vector(v1) + v2 = vertices[edge[1]] + polyline.points.add(1) + polyline.points[-1].co = native_data["matrix"] @ Vector(v2) + + curve.bevel_depth = unit_scale * item.Radius + thickness = None + if (inner_radius := item.InnerRadius) and (thickness := max(item.Radius - inner_radius, 0)): + thickness *= unit_scale + curve.use_fill_caps = False + # Shade flat. + for spline in curve.splines: + spline.use_smooth = False + else: + curve.use_fill_caps = True + return curve, thickness + + @classmethod + def setup_native_swept_disk_solid_thickness(cls, obj: bpy.types.Object, thickness: Union[float, None]) -> None: + if not thickness: + return + modifier = obj.modifiers.new("Curve Thickness", type="SOLIDIFY") + assert isinstance(modifier, bpy.types.SolidifyModifier) + modifier.thickness = thickness