From fdf741b2099ae5dacfcfe3645eb9c31e40ebfc60 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Tue, 16 Dec 2025 09:26:28 +0100 Subject: [PATCH] Add description to Image Scaling Tool operator for clarity + black --- src/bonsai/bonsai/bim/import_ifc.py | 15 ++--- .../bonsai/bim/module/drawing/operator.py | 55 +++++++++---------- .../bonsai/bim/module/project/__init__.py | 2 +- .../bonsai/bim/module/project/operator.py | 51 ++++++++--------- .../bonsai/bim/module/project/workspace.py | 15 +++-- 5 files changed, 71 insertions(+), 67 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 027e220992..05bcbed01a 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -895,38 +895,39 @@ class IfcImporter: material = obj.data.materials[0] if material.use_nodes and material.node_tree: for node in material.node_tree.nodes: - if node.type == 'TEX_IMAGE' and node.image: + if node.type == "TEX_IMAGE" and node.image: image = node.image break if image: import bmesh + bm = bmesh.new() bm.from_mesh(obj.data) if not bm.loops.layers.uv: uv_layer = bm.loops.layers.uv.new() else: uv_layer = bm.loops.layers.uv.active - + if bm.verts: min_x = min(v.co.x for v in bm.verts) max_x = max(v.co.x for v in bm.verts) min_y = min(v.co.y for v in bm.verts) max_y = max(v.co.y for v in bm.verts) - + width = max_x - min_x height = max_y - min_y - + for face in bm.faces: for loop in face.loops: vert = loop.vert u = (vert.co.x - min_x) / width if width > 0 else 0.5 v = (vert.co.y - min_y) / height if height > 0 else 0.5 - + u = max(0.0, min(1.0, u)) v = max(0.0, min(1.0, v)) - + loop[uv_layer].uv = (u, v) - + bm.to_mesh(obj.data) bm.free() obj.data.update() diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 7397c93f63..31a3b90932 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3688,7 +3688,7 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): description="Existing object name to add a style with reference image to. If not provided will create a new object.", options={"SKIP_SAVE"}, ) - + x_length: bpy.props.FloatProperty( name="X Length", description="Width of the reference image in project units", @@ -3698,20 +3698,20 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): precision=3, ) y_length: bpy.props.FloatProperty( - name="Y Length", + name="Y Length", description="Height of the reference image in project units", default=1.0, min=0.001, soft_min=0.01, precision=3, ) - + show_dimensions_dialog: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) def draw(self, context): layout = self.layout - - if getattr(self, 'show_dimensions_dialog', False): + + if getattr(self, "show_dimensions_dialog", False): if tool.Ifc.get(): length_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT") if length_unit: @@ -3733,47 +3733,47 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): layout.label(text="to use relative paths.") layout.prop(self, "override_existing_image") layout.prop(self, "use_existing_object_by_name") - + def invoke(self, context, event): - if not getattr(self, 'show_dimensions_dialog', False): + if not getattr(self, "show_dimensions_dialog", False): context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} else: return context.window_manager.invoke_props_dialog(self) def execute(self, context): - if not getattr(self, 'show_dimensions_dialog', False): + if not getattr(self, "show_dimensions_dialog", False): abs_path = Path(self.filepath).absolute().resolve() if self.override_existing_image: params = {"check_existing": True, "force_reload": True} else: params = {"check_existing": False} - + try: image = load_image(abs_path.name, str(abs_path.parent), **params) - + image_width_px = image.size[0] image_height_px = image.size[1] aspect_ratio = image_width_px / image_height_px - + if aspect_ratio >= 1.0: self.x_length = 1.0 self.y_length = 1.0 / aspect_ratio else: self.x_length = aspect_ratio self.y_length = 1.0 - + bpy.data.images.remove(image) - + except Exception as e: - self.report({'ERROR'}, f"Failed to load image: {str(e)}") - return {'CANCELLED'} - + self.report({"ERROR"}, f"Failed to load image: {str(e)}") + return {"CANCELLED"} + self.show_dimensions_dialog = True return context.window_manager.invoke_props_dialog(self) - + return self._execute(context) - + def _execute(self, context): space = tool.Blender.get_view3d_space() if space.shading.color_type != "TEXTURE": @@ -3795,7 +3795,7 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def bm_add_image_plane(mesh): bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True) - + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) plane_scale = Vector((self.x_length * unit_scale / 2.0, self.y_length * unit_scale / 2.0, 1.0)) matrix = Matrix.LocRotScale(None, None, plane_scale) @@ -3810,16 +3810,16 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): max_x = max(v.co.x for v in bm.verts) min_y = min(v.co.y for v in bm.verts) max_y = max(v.co.y for v in bm.verts) - + width = max_x - min_x height = max_y - min_y - + for face in bm.faces: for loop in face.loops: vert = loop.vert u = (vert.co.x - min_x) / width if width > 0 else 0.5 v = (vert.co.y - min_y) / height if height > 0 else 0.5 - + u = max(0.0, min(1.0, u)) v = max(0.0, min(1.0, v)) loop[uv_layer].uv = (u, v) @@ -3844,7 +3844,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): ) tool.Blender.remove_data_block(temp_mesh) - element = tool.Ifc.get_entity(obj) if element and isinstance(obj.data, bpy.types.Mesh): representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") @@ -3853,9 +3852,9 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): num_faces = len(obj.data.polygons) obj.data["ios_item_ids"] = [item_id] * num_faces tool.Blender.Attribute.fill_attribute(obj.data, "ios_item_ids", "FACE", "INT", [item_id] * num_faces) - + for item in representation.Items: - if item.is_a('IfcPolygonalFaceSet') and item.Coordinates: + if item.is_a("IfcPolygonalFaceSet") and item.Coordinates: new_coords = [] for vertex in obj.data.vertices: co = obj.matrix_world @ vertex.co @@ -3901,8 +3900,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): ) tool.Style.reload_material_from_ifc(material) tool.Geometry.record_object_materials(obj) - - return {'FINISHED'} + + return {"FINISHED"} class ConvertSVGToDXF(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index bae6a119d6..6db688185c 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -131,4 +131,4 @@ def unregister(): if kc: for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) - addon_keymaps.clear() \ No newline at end of file + addon_keymaps.clear() diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 30eedd038e..55246171b0 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2843,6 +2843,7 @@ class ClearMeasurement(bpy.types.Operator): tool.Blender.update_viewport() return {"FINISHED"} + class ImageScalingTool(bpy.types.Operator, PolylineOperator): bl_idname = "bim.image_scaling_tool" bl_label = "Image Scaling Tool" @@ -2862,7 +2863,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): self.current_distance_value = "" self.is_typing_distance = False self.calculated_distance = 0.0 - + if tool.Ifc.get(): self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) else: @@ -2872,7 +2873,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): if not self.target_object or not context.active_object or context.active_object != self.target_object: self.report({"ERROR"}, "Image annotation was deselected. Tool cancelled.") return self.cancel_tool(context) - + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() @@ -2891,9 +2892,9 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): if event.type == "LEFTMOUSE" and event.value == "PRESS": if len(self.selected_points) < 2: snapped_point = self.snapping_points[0] - point_3d = snapped_point['point'].copy() + point_3d = snapped_point["point"].copy() self.selected_points.append(point_3d) - + if len(self.selected_points) == 2: self.calculate_distance() self.current_distance_value = f"{self.calculated_distance:.3f}" @@ -2903,7 +2904,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): elif len(self.selected_points) == 2: if event.type in {"RET", "NUMPAD_ENTER"} and event.value == "PRESS": return self.apply_scaling(context) - + if event.unicode and event.unicode.isprintable() and event.value == "PRESS": if event.unicode.isdigit() or event.unicode == ".": if not self.is_typing_distance: @@ -2911,14 +2912,16 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): self.is_typing_distance = True else: self.current_distance_value += event.unicode - + distance_value = float(self.current_distance_value) self.input_ui.set_value("DISTANCE", distance_value) - + elif event.type in {"BACK_SPACE", "DEL"} and event.value == "PRESS": if len(self.current_distance_value) > 0: self.current_distance_value = self.current_distance_value[:-1] - distance_value = float(self.current_distance_value) if self.current_distance_value else self.calculated_distance + distance_value = ( + float(self.current_distance_value) if self.current_distance_value else self.calculated_distance + ) self.input_ui.set_value("DISTANCE", distance_value) self.handle_keyboard_input(context, event) @@ -2937,7 +2940,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): def cancel_tool(self, context): context.workspace.status_text_set(text=None) - if hasattr(self, 'tool_state'): + if hasattr(self, "tool_state"): self.tool_state.plane_method = None PolylineDecorator.uninstall() tool.Blender.update_viewport() @@ -2955,7 +2958,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): instruction_text = f"Measured: {self.calculated_distance:.3f} - Type New Distance or Press Enter" else: instruction_text = "Image Scaling Tool" - + context.workspace.status_text_set(text=instruction_text) def calculate_distance(self): @@ -2984,51 +2987,51 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): if self.target_object: import bmesh - + mesh = self.target_object.data - + bm = bmesh.new() bm.from_mesh(mesh) - + bmesh.ops.scale(bm, vec=(scale_factor, scale_factor, 1.0), verts=bm.verts) - + if bm.loops.layers.uv: uv_layer = bm.loops.layers.uv.active - + min_x = min(v.co.x for v in bm.verts) max_x = max(v.co.x for v in bm.verts) min_y = min(v.co.y for v in bm.verts) max_y = max(v.co.y for v in bm.verts) - + width = max_x - min_x height = max_y - min_y - + for face in bm.faces: for loop in face.loops: vert = loop.vert u = (vert.co.x - min_x) / width if width > 0 else 0.5 v = (vert.co.y - min_y) / height if height > 0 else 0.5 - + u = max(0.0, min(1.0, u)) v = max(0.0, min(1.0, v)) loop[uv_layer].uv = (u, v) - + bm.to_mesh(mesh) bm.free() mesh.update() - + element = tool.Ifc.get_entity(self.target_object) if element and element.Representation: for representation in element.Representation.Representations: for item in representation.Items: - if item.is_a('IfcPolygonalFaceSet') and item.Coordinates: + if item.is_a("IfcPolygonalFaceSet") and item.Coordinates: new_coords = [] for vertex in mesh.vertices: co = self.target_object.matrix_world @ vertex.co new_coords.append([co.x, co.y, co.z]) - + item.Coordinates.CoordList = new_coords - + self.report({"INFO"}, f"Applied scale factor: {scale_factor:.4f}") context.workspace.status_text_set(text=None) @@ -3037,5 +3040,3 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator): tool.Blender.update_viewport() return {"FINISHED"} - - diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index a5ec38a5ee..59b86e431d 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -78,6 +78,7 @@ class ExploreTool(bpy.types.WorkSpaceTool): row = layout.row(align=True) op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE") op.hotkey = "S_S" + op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them" class ExploreHotkey(bpy.types.Operator): @@ -123,12 +124,14 @@ class ExploreHotkey(bpy.types.Operator): active_obj = bpy.context.active_object selected_objects = tool.Blender.get_selected_objects() element = tool.Ifc.get_entity(active_obj) if active_obj else None - - if (not active_obj or - not element or - not element.is_a("IfcAnnotation") or - len(selected_objects) != 1 or - not tool.Drawing.is_annotation_object_type(element, "IMAGE")): + + if ( + not active_obj + or not element + or not element.is_a("IfcAnnotation") + or len(selected_objects) != 1 + or not tool.Drawing.is_annotation_object_type(element, "IMAGE") + ): self.report({"ERROR"}, "Please select one image annotation first.") return