diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 799732494d..69c1fd6ca7 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -2408,6 +2408,7 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): def execute(self, context): space = self.get_view_3d(context) # Do not remove. It is used later in eval scene = context.scene + assert scene style = {} eval_namespace = {"context": context, "scene": scene, "space": space} @@ -2445,7 +2446,9 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): if self.index: index = int(self.index) else: - index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index + assert (camera := scene.camera) + props = tool.Drawing.get_camera_props(camera) + index = props.active_drawing_style_index props = tool.Drawing.get_document_props() props.drawing_styles[index].raster_style = json.dumps(style) @@ -2520,8 +2523,10 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): def execute(self, context): scene = context.scene + assert scene and (camera := scene.camera) + camera_props = tool.Drawing.get_camera_props(camera) ifc_file = tool.Ifc.get() - active_drawing_style_index = scene.camera.data.BIMCameraProperties.active_drawing_style_index + active_drawing_style_index = camera_props.active_drawing_style_index props = tool.Drawing.get_document_props() if active_drawing_style_index >= len(props.drawing_styles): @@ -2841,7 +2846,8 @@ class AddDrawingStyleAttribute(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.camera.data.BIMCameraProperties + assert context.scene and (camera := context.scene.camera) + props = tool.Drawing.get_camera_props(camera) dprops = tool.Drawing.get_document_props() dprops.drawing_styles[props.active_drawing_style_index].attributes.add() return {"FINISHED"} @@ -2855,7 +2861,8 @@ class RemoveDrawingStyleAttribute(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - props = context.scene.camera.data.BIMCameraProperties + assert context.scene and (camera := context.scene.camera) + props = tool.Drawing.get_camera_props(camera) dprops = tool.Drawing.get_document_props() dprops.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index) return {"FINISHED"} @@ -3402,11 +3409,15 @@ class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator): filter_mode: bpy.props.StringProperty() def _execute(self, context): - obj = bpy.context.scene.camera + assert context.scene + obj = context.scene.camera if not obj: return - obj.data.BIMCameraProperties.filter_mode = self.filter_mode + assert (camera := context.scene.camera) + props = tool.Drawing.get_camera_props(camera) + props.filter_mode = self.filter_mode element = tool.Ifc.get_entity(obj) + assert element if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()): filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}") try: @@ -3422,10 +3433,12 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): filter_mode: bpy.props.StringProperty() def _execute(self, context): - obj = bpy.context.scene.camera + assert context.scene + obj = context.scene.camera assert obj - props = obj.data.BIMCameraProperties + props = tool.Drawing.get_camera_props(obj) element = tool.Ifc.get_entity(obj) + assert element pset = tool.Pset.get_element_pset(element, "EPset_Drawing") if self.filter_mode == "INCLUDE": query = tool.Search.export_filter_query(props.include_filter_groups) or None @@ -3433,7 +3446,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): elif self.filter_mode == "EXCLUDE": query = tool.Search.export_filter_query(props.exclude_filter_groups) or None ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Exclude": query}) - obj.data.BIMCameraProperties.filter_mode = "NONE" + props.filter_mode = "NONE" bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False) diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index db52f9e330..56a234cd60 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -22,6 +22,7 @@ import json import enum import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.pset import ifcopenshell.util.element import bonsai.tool as tool import bonsai.core.drawing as core @@ -65,9 +66,10 @@ def get_location_hint(self, context): return DrawingsData.data["location_hint"] -def update_diagram_scale(self, context): +def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context) -> None: if not self.update_props: return + assert context.scene if not context.scene.camera or context.scene.camera.data != self.id_data: return element = tool.Ifc.get_entity(context.scene.camera) @@ -89,13 +91,14 @@ def update_diagram_scale(self, context): if pset: pset = tool.Ifc.get().by_id(pset["id"]) else: - pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing") - ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale) + 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) def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None: if not self.update_props: return + assert context.scene if not context.scene.camera or context.scene.camera.data != self.id_data: return element = tool.Ifc.get_entity(context.scene.camera) @@ -118,8 +121,9 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts}) -def get_diagram_scales(self, context): +def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: global diagram_scales_enum + assert context.scene if ( len(diagram_scales_enum) < 1 or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13) @@ -200,44 +204,45 @@ def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None: bpy.ops.bim.save_drawing_styles_data(rename_style=True, rename_style_from=old_value, rename_style_to=new_value) -def update_document_name(self, context): +def update_document_name(self: "Document", context: bpy.types.Context) -> None: document = tool.Ifc.get().by_id(self.ifc_definition_id) core.update_document_name(tool.Ifc, tool.Drawing, document=document, name=self.name) -def update_has_underlay(self, context): +def update_has_underlay(self: "BIMCameraProperties", context: bpy.types.Context) -> None: update_layer(self, context, "HasUnderlay", self.has_underlay) + assert context.scene # making sure that camera is active if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data): bpy.ops.bim.reload_drawing_styles() bpy.ops.bim.activate_drawing_style() -def update_has_linework(self, context): +def update_has_linework(self: "BIMCameraProperties", context: bpy.types.Context) -> None: update_layer(self, context, "HasLinework", self.has_linework) -def update_has_annotation(self, context): +def update_has_annotation(self: "BIMCameraProperties", context: bpy.types.Context) -> None: update_layer(self, context, "HasAnnotation", self.has_annotation) -def update_dpi(self, context): +def update_dpi(self: "BIMCameraProperties", context: bpy.types.Context) -> None: update_layer(self, context, "DPI", self.dpi) -def update_linework_mode(self, context): +def update_linework_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None: update_layer(self, context, "LineworkMode", self.linework_mode) -def update_fill_mode(self, context): +def update_fill_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None: update_layer(self, context, "FillMode", self.fill_mode) -def update_cut_mode(self, context): +def update_cut_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None: update_layer(self, context, "CutMode", self.cut_mode) -def update_layer(self, context, name, value): +def update_layer(self: "BIMCameraProperties", context: bpy.types.Context, name: str, value: Any) -> None: if not self.update_props: return if not context.scene.camera or context.scene.camera.data != self.id_data: @@ -520,7 +525,11 @@ class BIMCameraProperties(PropertyGroup): filter_mode: StringProperty(name="Filter Mode", default="NONE") include_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Include Filter") exclude_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Exclude Filter") - update_props: BoolProperty(name="Enable Props Auto Update", default=True) + update_props: BoolProperty( + name="Enable Props Auto Update", + description="Update related EPset_Drawing pset on any change in camera properties.", + default=True, + ) if TYPE_CHECKING: linework_mode: Literal["OPENCASCADE", "FREESTYLE"] diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 7b79499736..f26541d10d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.bim.helper import bonsai.tool as tool @@ -28,6 +29,10 @@ from bonsai.bim.module.drawing.data import ( ElementFiltersData, DecoratorData, ) +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.drawing.prop import DocProperties, Drawing class BIM_PT_camera(Panel): @@ -39,18 +44,21 @@ class BIM_PT_camera(Panel): bl_parent_id = "BIM_PT_tab_drawings" def draw(self, context): - if not (context.scene.camera and hasattr(context.scene.camera.data, "BIMCameraProperties")): + assert context.scene and self.layout + camera = context.scene.camera + if not camera: row = self.layout.row() row.label(text="No Active Drawing", icon="ERROR") return - if "/" not in context.scene.camera.name: + if not tool.Ifc.get_entity(camera): self.layout.label(text="This is not a BIM camera.") return + assert isinstance(camera_data := camera.data, bpy.types.Camera) + props = tool.Drawing.get_camera_props(camera) self.layout.use_property_split = True dprops = tool.Drawing.get_document_props() - props = context.scene.camera.data.BIMCameraProperties col = self.layout.column(align=True) row = col.row(align=True) @@ -77,7 +85,7 @@ class BIM_PT_camera(Panel): row.prop(props, "height") row = self.layout.row() - row.prop(context.scene.camera.data, "clip_end", text="Depth") + row.prop(camera_data, "clip_end", text="Depth") row = self.layout.row(align=True) row.prop(props, "diagram_scale", text="Scale") @@ -110,7 +118,8 @@ class BIM_PT_element_filters(Panel): if not ElementFiltersData.is_loaded: ElementFiltersData.load() - props = context.scene.camera.data.BIMCameraProperties + assert context.scene and (camera := context.scene.camera) + props = tool.Drawing.get_camera_props(camera) if props.filter_mode == "INCLUDE": bonsai.bim.helper.draw_filter( @@ -159,10 +168,9 @@ class BIM_PT_drawing_underlay(Panel): def draw(self, context): layout = self.layout layout.use_property_split = True - camera = context.scene.camera - assert camera + assert context.scene and (camera := context.scene.camera) dprops = tool.Drawing.get_document_props() - props = camera.data.BIMCameraProperties + props = tool.Drawing.get_camera_props(camera) drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles) if not DrawingsData.is_loaded: @@ -607,7 +615,7 @@ class BIM_PT_text(Panel): class BIM_UL_drawinglist(bpy.types.UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item(self, context, layout, data: DocProperties, item: Drawing, icon, active_data, active_propname): if not item: layout.label(text="", translate=False) return diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index c2fd0e95a4..b0d14c5f5d 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -298,20 +298,21 @@ class Drawing(bonsai.core.tool.Drawing): def create_camera( cls, name: str, matrix: Matrix, location_hint: Literal["PERSPECTIVE", "ORTHOGRAPHIC"] ) -> bpy.types.Object: - camera = bpy.data.objects.new(name, bpy.data.cameras.new(name)) + camera = bpy.data.objects.new(name, (camera_data := bpy.data.cameras.new(name))) + props = cls.get_camera_props(camera_data) camera.location = (0, 0, 1.5) # The view shall be 1.5m above the origin - camera.data.show_limits = True + camera_data.show_limits = True if location_hint == "PERSPECTIVE": - camera.data.type = "PERSP" + camera_data.type = "PERSP" else: - camera.data.type = "ORTHO" - camera.data.ortho_scale = 50 # The default of 6m is too small - camera.data.clip_start = 0.002 # 2mm is close to zero but allows any GPU-drawn lines to be visible. - camera.data.clip_end = 10 # A slightly more reasonable default + camera_data.type = "ORTHO" + camera_data.ortho_scale = 50 # The default of 6m is too small + camera_data.clip_start = 0.002 # 2mm is close to zero but allows any GPU-drawn lines to be visible. + camera_data.clip_end = 10 # A slightly more reasonable default if bpy.context.scene.unit_settings.system == "IMPERIAL": - camera.data.BIMCameraProperties.diagram_scale = '1/8"=1\'-0"|1/96' + props.diagram_scale = '1/8"=1\'-0"|1/96' else: - camera.data.BIMCameraProperties.diagram_scale = "1:100|1/100" + props.diagram_scale = "1:100|1/100" camera.matrix_world = matrix return camera @@ -797,50 +798,50 @@ class Drawing(bonsai.core.tool.Drawing): from bonsai.bim.module.drawing.prop import get_diagram_scales # Temporarily clear the definition id to prevent prop update callbacks to IFC. - camera_props = tool.Drawing.get_camera_props(camera) + camera_props = cls.get_camera_props(camera) update_props = camera_props.update_props camera_props.update_props = False - camera.BIMCameraProperties.has_underlay = False - camera.BIMCameraProperties.has_linework = True - camera.BIMCameraProperties.has_annotation = True - camera.BIMCameraProperties.target_view = "PLAN_VIEW" - camera.BIMCameraProperties.is_nts = False + camera_props.has_underlay = False + camera_props.has_linework = True + camera_props.has_annotation = True + camera_props.target_view = "PLAN_VIEW" + camera_props.is_nts = False pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") if pset: if "TargetView" in pset: - camera.BIMCameraProperties.target_view = pset["TargetView"] + camera_props.target_view = pset["TargetView"] if "Scale" in pset: valid_scales = [ i[0] for i in get_diagram_scales(None, bpy.context) if pset["Scale"] == i[0].split("|")[-1] ] if valid_scales: - camera.BIMCameraProperties.diagram_scale = valid_scales[0] + camera_props.diagram_scale = valid_scales[0] else: - camera.BIMCameraProperties.diagram_scale = "CUSTOM" + camera_props.diagram_scale = "CUSTOM" if ":" in pset["HumanScale"]: numerator, denominator = pset["HumanScale"].split(":") else: numerator, denominator = pset["HumanScale"].split("=") - camera.BIMCameraProperties.custom_scale_numerator = numerator - camera.BIMCameraProperties.custom_scale_denominator = denominator + camera_props.custom_scale_numerator = numerator + camera_props.custom_scale_denominator = denominator if "HasUnderlay" in pset: - camera.BIMCameraProperties.has_underlay = bool(pset["HasUnderlay"]) + camera_props.has_underlay = bool(pset["HasUnderlay"]) if "HasLinework" in pset: - camera.BIMCameraProperties.has_linework = bool(pset["HasLinework"]) + camera_props.has_linework = bool(pset["HasLinework"]) if "HasAnnotation" in pset: - camera.BIMCameraProperties.has_annotation = bool(pset["HasAnnotation"]) + camera_props.has_annotation = bool(pset["HasAnnotation"]) if "IsNTS" in pset: - camera.BIMCameraProperties.is_nts = bool(pset["IsNTS"]) + camera_props.is_nts = bool(pset["IsNTS"]) if "DPI" in pset: - camera.BIMCameraProperties.dpi = int(pset["DPI"]) + camera_props.dpi = int(pset["DPI"]) if "LineworkMode" in pset: - camera.BIMCameraProperties.linework_mode = str(pset["LineworkMode"]) + camera_props.linework_mode = str(pset["LineworkMode"]) if "FillMode" in pset: - camera.BIMCameraProperties.fill_mode = str(pset["FillMode"]) + camera_props.fill_mode = str(pset["FillMode"]) if "CutMode" in pset: - camera.BIMCameraProperties.cut_mode = str(pset["CutMode"]) + camera_props.cut_mode = str(pset["CutMode"]) camera_props.update_props = update_props @@ -853,7 +854,7 @@ class Drawing(bonsai.core.tool.Drawing): cls.drawing_selected_states.update({d.ifc_definition_id: d.is_selected for d in props.drawings if d.is_drawing}) props.drawings.clear() drawings = [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"] - grouped_drawings = { + grouped_drawings: dict[str, list[ifcopenshell.entity_instance]] = { "MODEL_VIEW": [], "PLAN_VIEW": [], "SECTION_VIEW": [], @@ -1327,8 +1328,13 @@ class Drawing(bonsai.core.tool.Drawing): import bonsai.bim.module.drawing.helper as helper camera = tool.Ifc.get_object(drawing) - bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None + assert isinstance(camera, bpy.types.Object) + assert isinstance((camera_data := camera.data), bpy.types.Camera) + props = tool.Drawing.get_camera_props(camera_data) + + bounds = helper.ortho_view_frame(camera_data) if camera_data.type == "ORTHO" else None reference_obj = tool.Ifc.get_object(reference_element) + assert isinstance(reference_obj, bpy.types.Object) def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix: mat = reference_obj.matrix_world.copy() @@ -1336,12 +1342,12 @@ class Drawing(bonsai.core.tool.Drawing): xyz[2] = 0 xyz = camera.matrix_world @ xyz mat.translation = xyz - annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05)) + annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05)) annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset mat.translation += annotation_offset return mat - def project_point_onto_camera(point, camera): + def project_point_onto_camera(point: Vector, camera: bpy.types.Object) -> Vector: projection = camera.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1)) return camera.matrix_world.inverted() @ mathutils.geometry.intersect_line_plane( point.xyz, point.xyz - projection, camera.location, projection @@ -1349,12 +1355,12 @@ class Drawing(bonsai.core.tool.Drawing): obj_matrix = to_camera_coords(camera, reference_obj) - if camera.data.BIMCameraProperties.raster_x > camera.data.BIMCameraProperties.raster_y: - width = camera.data.ortho_scale - height = width / camera.data.BIMCameraProperties.raster_x * camera.data.BIMCameraProperties.raster_y + if props.raster_x > props.raster_y: + width = camera_data.ortho_scale + height = width / props.raster_x * props.raster_y else: - height = camera.data.ortho_scale - width = height / camera.data.BIMCameraProperties.raster_y * camera.data.BIMCameraProperties.raster_x + height = camera_data.ortho_scale + width = height / props.raster_y * props.raster_x projection = project_point_onto_camera(reference_obj.location, camera) co1 = camera.matrix_world @ mathutils.Vector((width / 2, projection[1], -1)) @@ -1618,16 +1624,18 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_camera_block(cls, obj: bpy.types.Object) -> dict: - raster_x = obj.data.BIMCameraProperties.raster_x - raster_y = obj.data.BIMCameraProperties.raster_y + assert isinstance(camera := obj.data, bpy.types.Camera) + props = tool.Drawing.get_camera_props(camera) + raster_x = props.raster_x + raster_y = props.raster_y if raster_x > raster_y: - width = obj.data.ortho_scale + width = camera.ortho_scale height = width / raster_x * raster_y else: - height = obj.data.ortho_scale + height = camera.ortho_scale width = height / raster_y * raster_x - depth = obj.data.clip_end + depth = camera.clip_end verts = ( obj.matrix_world @ mathutils.Vector((-width / 2, -height / 2, -depth)), @@ -2054,7 +2062,7 @@ class Drawing(bonsai.core.tool.Drawing): def get_elements_in_camera_view( cls, camera: bpy.types.Object, objs: list[bpy.types.Object] ) -> set[ifcopenshell.entity_instance]: - props = camera.data.BIMCameraProperties + props = tool.Drawing.get_camera_props(camera) x = props.width y = props.height diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 6c4c338b3d..17e2de20ef 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -726,8 +726,10 @@ class TestDrawingMaintainingSheetPosition(NewFile): assert drawing_data["foreground"] == (30.0, 30.0, 500.0, 500.0) assert drawing_data["view-title"] == (30.0, 535.0, 50.22, 10.0) - bpy.context.scene.camera.data.BIMCameraProperties.width = 25 - bpy.context.scene.camera.data.BIMCameraProperties.height = 25 + assert (scene := bpy.context.scene) and (camera := scene.camera) + props = tool.Drawing.get_camera_props(camera) + props.width = 25 + props.height = 25 tool.Blender.force_depsgraph_update() bpy.ops.bim.create_drawing() @@ -866,7 +868,9 @@ class TestDrawingStyles(NewFile): def test_drawing_styles_loaded_on_underlay_enabled(self): self.setup_project_with_drawing() - bpy.context.scene.camera.data.BIMCameraProperties.has_underlay = True + assert (scene := bpy.context.scene) and (camera := scene.camera) + props = tool.Drawing.get_camera_props(camera) + props.has_underlay = True assert len(self.drawing_styles) == 3 def test_drawing_styles_reload(self):