From 82f25f50ccf4f44cb2a4e6f639ded79e51b8f379 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 24 Apr 2025 15:16:48 +0500 Subject: [PATCH] Fix resaving camera to IFC on every drawing activation (32899eb) `camera_props.update_representation` basically always returned True leading to `bim.update_representation` call on camera on every drawing activation. It was always True because `create_camera` wasn't setting `representation` and some other related camera props. Changes: - create_camera now setting representation - ortho_scale, rather_x and raster_y now explicitly is tied to width, height and drawing scale props, not on depsgraph update. Depsgraph update wasn't allowing to set these props in time for create_camera to record representation correctly - depsgraph update now only in charge of ortho_scale (just to be sure user won't touch it by hand) and scene.render resolution --- .../bonsai/bim/module/drawing/handler.py | 29 +++----- .../bonsai/bim/module/drawing/operator.py | 4 +- src/bonsai/bonsai/bim/module/drawing/prop.py | 68 ++++++++++++++++--- src/bonsai/bonsai/tool/drawing.py | 26 ++++--- src/bonsai/bonsai/tool/loader.py | 24 ++++--- 5 files changed, 98 insertions(+), 53 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/handler.py b/src/bonsai/bonsai/bim/module/drawing/handler.py index 0ae6ef74b4..5c164b8c9e 100644 --- a/src/bonsai/bonsai/bim/module/drawing/handler.py +++ b/src/bonsai/bonsai/bim/module/drawing/handler.py @@ -37,28 +37,19 @@ def depsgraph_update_pre_handler(scene): def set_active_camera_resolution(scene: bpy.types.Scene) -> None: + """Sync scene render resolution with the active drawing + and prevent user from manually changing ``ortho_scale`` on IFC camera.""" props = tool.Drawing.get_document_props() camera_obj = scene.camera if not camera_obj or "/" not in camera_obj.name or not props.drawings: return assert isinstance((camera := camera_obj.data), bpy.types.Camera) props = tool.Drawing.get_camera_props(camera) - ortho_scale = max((props.width, props.height)) - aspect_ratio = props.width / props.height - if (camera.ortho_scale != ortho_scale) or (scene.render.resolution_x / scene.render.resolution_y != aspect_ratio): - camera.ortho_scale = ortho_scale - - diagram_scale = tool.Drawing.get_diagram_scale(camera_obj) - scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"]) - - if props.width > props.height: - aspect_ratio = props.height / props.width - raster_x = ortho_scale * scale_ratio * props.dpi / 0.0254 - raster_y = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254 - else: - aspect_ratio = props.width / props.height - raster_x = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254 - raster_y = ortho_scale * scale_ratio * props.dpi / 0.0254 - - scene.render.resolution_x = props.raster_x = int(raster_x) - scene.render.resolution_y = props.raster_y = int(raster_y) + ortho_scale, aspect_ratio = props.get_scale_and_aspect_ratio() + scene_render = scene.render + if (camera.ortho_scale != ortho_scale) or not tool.Cad.is_x( + scene_render.resolution_x / scene_render.resolution_y, aspect_ratio + ): + raster_x, raster_y = props.update_camera_resolution() + scene_render.resolution_x = raster_x + scene_render.resolution_y = raster_y diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 29dfc995b0..5dfb9b99b4 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -277,7 +277,7 @@ class CreateDrawing(bpy.types.Operator): self.drawing_name = self.drawing.Name self.metadata = tool.Drawing.get_drawing_metadata(self.camera_element) self.get_scale(context) - if self.cprops.update_representation(self.camera): + if self.cprops.update_representation(self.camera.matrix_world): bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="") # Reassign props as data is recreated during the update. self.cprops = tool.Drawing.get_camera_props(self.camera) @@ -2164,7 +2164,7 @@ class ActivateDrawingBase: # Save drawing bounds to the .ifc file camera = context.scene.camera camera_props = tool.Drawing.get_camera_props(camera) - if camera_props.update_representation(camera): + if camera_props.update_representation(camera.matrix_world): bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="") # See 6452 and 6478. # bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT") diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 53cc240bf4..d23f2727b4 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -28,6 +28,7 @@ import bonsai.tool as tool import bonsai.core.drawing as core import bonsai.bim.module.drawing.annotation as annotation import bonsai.bim.module.drawing.decoration as decoration +from mathutils import Matrix from bonsai.bim.prop import BIMFilterGroup from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data @@ -70,10 +71,9 @@ def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context if not self.update_props: return assert context.scene - if not context.scene.camera or context.scene.camera.data != self.id_data: + if not (camera := context.scene.camera) or camera.data != self.id_data: return - element = tool.Ifc.get_entity(context.scene.camera) - if not element: + if not (element := tool.Ifc.get_entity(camera)): return try: element = ( @@ -93,6 +93,7 @@ def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context else: pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing") ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=diagram_scale) + self.update_camera_resolution() def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None: @@ -492,6 +493,10 @@ class DocProperties(PropertyGroup): classes_to_wireframe: str +def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None: + self.update_camera_resolution() + + class BIMCameraProperties(PropertyGroup): linework_mode: EnumProperty( items=[ @@ -553,8 +558,8 @@ class BIMCameraProperties(PropertyGroup): raster_x: IntProperty(name="Raster X", default=1000) raster_y: IntProperty(name="Raster Y", default=1000) dpi: IntProperty(name="DPI", default=75, update=update_dpi) - width: FloatProperty(name="Width", default=50, subtype="DISTANCE") - height: FloatProperty(name="Height", default=50, subtype="DISTANCE") + width: FloatProperty(name="Width", default=50, subtype="DISTANCE", update=update_width_height) + height: FloatProperty(name="Height", default=50, subtype="DISTANCE", update=update_width_height) is_nts: BoolProperty(name="Is NTS", update=update_is_nts) active_drawing_style_index: IntProperty(name="Active Drawing Style Index") filter_mode: StringProperty(name="Filter Mode", default="NONE") @@ -595,15 +600,23 @@ class BIMCameraProperties(PropertyGroup): # For now, this JSON dump are all the parameters that determine a camera's "Block representation" # By checking this, you will know whether or not the camera IFC representation needs to be refreshed - def update_representation(self, obj: bpy.types.Object) -> bool: - assert isinstance(obj.data, bpy.types.Camera) + def update_representation(self, matrix_world: Matrix) -> bool: + """Update ``representation`` based on current camera properties and the provided world matrix. + + :return: ``True`` if ``representation`` was updated and + representation should also be updated in IFC. + """ + # Matrix is used instead of Object so this works before the Object exists, + # allowing all camera initialization to stay encapsulated in `create_camera`. + camera = self.id_data + assert isinstance(camera, bpy.types.Camera) representation = json.dumps( { - "matrix": [list(x) for x in obj.matrix_world], + "matrix": [list(x) for x in matrix_world], "raster_x": self.raster_x, "raster_y": self.raster_y, - "ortho_scale": obj.data.ortho_scale, - "clip_end": obj.data.clip_end, + "ortho_scale": camera.ortho_scale, + "clip_end": camera.clip_end, } ) if self.representation != representation: @@ -611,6 +624,41 @@ class BIMCameraProperties(PropertyGroup): return True return False + def update_camera_resolution(self) -> tuple[int, int]: + """Update ``camera.ortho_scale``, ``raster_x`` and ``raster_y`` + based on current ``width`` and ``height`` and diagram scale props. + + :return: tuple[resolution_x, resolution_y] + """ + assert isinstance(camera := self.id_data, bpy.types.Camera) + ortho_scale, aspect_ratio = self.get_scale_and_aspect_ratio() + aspect_ratio = self.width / self.height + + camera.ortho_scale = ortho_scale + diagram_scale = tool.Drawing.get_diagram_scale(camera) + scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"]) + + if self.width > self.height: + aspect_ratio = self.height / self.width + raster_x = ortho_scale * scale_ratio * self.dpi / 0.0254 + raster_y = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254 + else: + aspect_ratio = self.width / self.height + raster_x = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254 + raster_y = ortho_scale * scale_ratio * self.dpi / 0.0254 + + raster_x, raster_y = int(raster_x), int(raster_y) + self.raster_x, self.raster_y = raster_x, raster_y + return raster_x, raster_y + + def get_scale_and_aspect_ratio(self) -> tuple[float, float]: + """ + :return: A tuple of calculated ortho scale and aspect ratio values. + """ + ortho_scale = max(self.width, self.height) + aspect_ratio = self.width / self.height + return ortho_scale, aspect_ratio + DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2 BOX_ALIGNMENT_POSITIONS = [ diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 66bb1a224a..135ce99314 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -745,6 +745,16 @@ class Drawing(bonsai.core.tool.Drawing): for obj in ifc_importer.added_data.values(): tool.Collector.assign(obj) + @classmethod + def get_camera_shape_matrix( + cls, drawing: ifcopenshell.entity_instance, shape: ifcopenshell.geom.ShapeElementType + ) -> Matrix: + mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape)) + + if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW": + mat[1][1] *= -1 + return mat + # NOTE: EPsetDrawing pset is completely synced with BIMCameraProperties # but BIMCameraProperties are only synced with EPsetDrawing at drawing import # therefore camera props can differ from pset if the user changed them from pset. @@ -763,11 +773,7 @@ class Drawing(bonsai.core.tool.Drawing): cls.import_camera_props(drawing, camera) tool.Ifc.link(drawing, obj) - mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape)) - obj.matrix_world = mat - - if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW": - obj.matrix_world[1][1] *= -1 + obj.matrix_world = cls.get_camera_shape_matrix(drawing, shape) tool.Geometry.record_object_position(obj) tool.Collector.assign(obj) @@ -787,10 +793,8 @@ class Drawing(bonsai.core.tool.Drawing): obj.data = camera else: obj = bpy.data.objects.new(tool.Loader.get_name(drawing), camera) - mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape)) - obj.matrix_world = mat - if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW": - obj.matrix_world[1][1] *= -1 + + obj.matrix_world = cls.get_camera_shape_matrix(drawing, shape) return obj @classmethod @@ -2225,8 +2229,8 @@ class Drawing(bonsai.core.tool.Drawing): return float(numerator) / float(denominator) @classmethod - def get_diagram_scale(cls, obj: bpy.types.Object) -> dict[str, str]: - props = cls.get_camera_props(obj) + def get_diagram_scale(cls, camera: Union[bpy.types.Object, bpy.types.Camera]) -> dict[str, str]: + props = cls.get_camera_props(camera) scale = props.diagram_scale if scale != "CUSTOM": human_scale, scale = scale.split("|") diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index f45242f760..5f50dd0d3a 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -815,15 +815,13 @@ class Loader(bonsai.core.tool.Loader): cls, element: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance, - shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType], + shape: ifcopenshell.geom.ShapeElementType, ) -> bpy.types.Camera: - from bonsai.bim.module.drawing.prop import get_diagram_scales - - if isinstance(shape, ifcopenshell.geom.ShapeElementType): - geometry = shape.geometry - else: - geometry = shape + """Create camera data. + Camera props are automatically updated based on ``element`` and ``shape``. + """ + geometry = shape.geometry v = geometry.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -845,8 +843,8 @@ class Loader(bonsai.core.tool.Loader): camera.clip_start = 0.002 # Technically 0, but Blender doesn't allow this, so 2mm it is! camera.clip_end = depth - props.width = width - props.height = height + props["width"] = width + props["height"] = height elif camera_type == "PERSP": abs_min_z = abs(min(z)) abs_max_z = abs(max(z)) @@ -854,8 +852,8 @@ class Loader(bonsai.core.tool.Loader): camera.clip_end = abs_min_z max_res = 1000 - props.width = width - props.height = height + props["width"] = width + props["height"] = height if width > height: fov = 2 * atan(width / (2 * abs_min_z)) @@ -865,6 +863,10 @@ class Loader(bonsai.core.tool.Loader): camera.angle = fov tool.Drawing.import_camera_props(element, camera) + props = tool.Drawing.get_camera_props(camera) + props.update_camera_resolution() # Only after all props are imported. + mat = tool.Drawing.get_camera_shape_matrix(element, shape) + props.update_representation(mat) return camera @classmethod