diff --git a/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc b/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc index 70d6049ad7..114b766ff8 100644 --- a/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc +++ b/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc @@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_D FILE_SCHEMA(('IFC4')); ENDSEC; DATA; -#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2)); +#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2)); #2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); @@ -33,5 +33,7 @@ DATA; #26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#29=IFCSIMPLEPROPERTYTEMPLATE('0lP6Y8q9v2QhDnR4sT7uVx',$,'PerspectiveShiftX','Horizontal perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#30=IFCSIMPLEPROPERTYTEMPLATE('2mR8b1NcW5EoFyG7hJ9kLp',$,'PerspectiveShiftY','Vertical perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); ENDSEC; END-ISO-10303-21; diff --git a/src/bonsai/bonsai/bim/module/drawing/handler.py b/src/bonsai/bonsai/bim/module/drawing/handler.py index 026d274fb3..aac48b9479 100644 --- a/src/bonsai/bonsai/bim/module/drawing/handler.py +++ b/src/bonsai/bonsai/bim/module/drawing/handler.py @@ -50,6 +50,9 @@ def set_active_camera_resolution(scene: bpy.types.Scene) -> None: if camera.type != props.camera_type: camera.type = props.camera_type + if props.update_props and (drawing := tool.Ifc.get_entity(camera_obj)): + tool.Drawing.sync_perspective_camera_shifts(drawing, camera) + 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( diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 1647d44773..f75de7fd34 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -604,6 +604,7 @@ class BIMCameraProperties(PropertyGroup): return tool.Blender.get_active_uilist_element(dprops.drawing_styles, self.active_drawing_style_index) # For now, this JSON dump are all the parameters that determine a camera's "Block representation" + # Perspective camera shift is stored in EPset_Drawing and intentionally excluded here. # By checking this, you will know whether or not the camera IFC representation needs to be refreshed def update_representation(self, matrix_world: Matrix) -> bool: """Update ``representation`` based on current camera properties and the provided world matrix. diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index c1809995a0..f7e9d51821 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -99,6 +99,10 @@ class BIM_PT_camera(Panel): if props.target_view == "MODEL_VIEW": row = self.layout.row() row.prop(props, "camera_type") + if props.camera_type == "PERSP": + row = self.layout.row(align=True) + row.prop(camera_data, "shift_x") + row.prop(camera_data, "shift_y") row = self.layout.row() row.prop(props, "linework_mode") diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 40ab9037dd..3eaceda36f 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -78,6 +78,7 @@ if TYPE_CHECKING: class Drawing(bonsai.core.tool.Drawing): ANNOTATION_DATA_TYPE = Literal["empty", "curve", "mesh"] + PERSPECTIVE_CAMERA_SHIFT_PROPERTIES = ("PerspectiveShiftX", "PerspectiveShiftY") DOCUMENT_TYPE = Literal["SCHEDULE", "REFERENCE"] LocationHintLiteral = Literal["PERSPECTIVE", "ORTHOGRAPHIC", "NORTH", "SOUTH", "EAST", "WEST"] LOCATION_HINT_LITERALS = ("PERSPECTIVE", "ORTHOGRAPHIC", "NORTH", "SOUTH", "EAST", "WEST") @@ -453,6 +454,43 @@ class Drawing(bonsai.core.tool.Drawing): camera.matrix_world = matrix return camera + @classmethod + def get_perspective_camera_shifts(cls, drawing: ifcopenshell.entity_instance) -> dict[str, float]: + pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") or {} + shift_x_prop, shift_y_prop = cls.PERSPECTIVE_CAMERA_SHIFT_PROPERTIES + return { + "shift_x": float(pset.get(shift_x_prop, 0.0) or 0.0), + "shift_y": float(pset.get(shift_y_prop, 0.0) or 0.0), + } + + @classmethod + def sync_perspective_camera_shifts( + cls, drawing: ifcopenshell.entity_instance, camera: bpy.types.Camera + ) -> None: + if camera.type != "PERSP": + return + + shift_x_prop, shift_y_prop = cls.PERSPECTIVE_CAMERA_SHIFT_PROPERTIES + current_shifts = cls.get_perspective_camera_shifts(drawing) + new_shifts = {"shift_x": float(camera.shift_x or 0.0), "shift_y": float(camera.shift_y or 0.0)} + if tool.Cad.is_x(current_shifts["shift_x"], new_shifts["shift_x"]) and tool.Cad.is_x( + current_shifts["shift_y"], new_shifts["shift_y"] + ): + return + + ifc_file = tool.Ifc.get() + pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing") + if not pset: + pset = ifcopenshell.api.pset.add_pset(ifc_file, product=drawing, name="EPset_Drawing") + ifcopenshell.api.pset.edit_pset( + ifc_file, + pset=pset, + properties={ + shift_x_prop: new_shifts["shift_x"], + shift_y_prop: new_shifts["shift_y"], + }, + ) + @classmethod def create_svg_schedule(cls, schedule: ifcopenshell.entity_instance) -> None: import bonsai.bim.module.drawing.scheduler as scheduler @@ -1009,6 +1047,8 @@ class Drawing(bonsai.core.tool.Drawing): camera_props.has_annotation = True camera_props.target_view = "PLAN_VIEW" camera_props.is_nts = False + camera.shift_x = 0.0 + camera.shift_y = 0.0 pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") if pset: @@ -1044,6 +1084,10 @@ class Drawing(bonsai.core.tool.Drawing): camera_props.fill_mode = str(pset["FillMode"]) if "CutMode" in pset: camera_props.cut_mode = str(pset["CutMode"]) + if camera.type == "PERSP": + shifts = cls.get_perspective_camera_shifts(drawing) + camera.shift_x = shifts["shift_x"] + camera.shift_y = shifts["shift_y"] camera_props.update_props = update_props diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index e3de6ba2d9..57ac52424f 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -73,6 +73,83 @@ class TestCreateCamera(NewFile): assert obj.users_collection == tuple() +class TestImportCameraProps(NewFile): + def test_imports_perspective_camera_shifts_from_drawing_pset(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + drawing = ifc.createIfcAnnotation(ObjectType="DRAWING") + pset = ifcopenshell.api.pset.add_pset(ifc, product=drawing, name="EPset_Drawing") + ifcopenshell.api.pset.edit_pset( + ifc, + pset=pset, + properties={"PerspectiveShiftX": 0.125, "PerspectiveShiftY": -0.375}, + ) + camera = bpy.data.cameras.new("Camera") + camera.type = "PERSP" + + subject.import_camera_props(drawing, camera) + + assert camera.shift_x == pytest.approx(0.125) + assert camera.shift_y == pytest.approx(-0.375) + + def test_non_perspective_import_defaults_camera_shifts_to_zero(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + drawing = ifc.createIfcAnnotation(ObjectType="DRAWING") + pset = ifcopenshell.api.pset.add_pset(ifc, product=drawing, name="EPset_Drawing") + ifcopenshell.api.pset.edit_pset( + ifc, + pset=pset, + properties={"PerspectiveShiftX": 0.125, "PerspectiveShiftY": -0.375}, + ) + camera = bpy.data.cameras.new("Camera") + camera.type = "ORTHO" + camera.shift_x = 1.0 + camera.shift_y = -1.0 + + subject.import_camera_props(drawing, camera) + + assert camera.shift_x == 0.0 + assert camera.shift_y == 0.0 + + +class TestSyncPerspectiveCameraShifts(NewFile): + def test_round_trips_perspective_camera_shifts_through_drawing_pset(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + drawing = ifc.createIfcAnnotation(ObjectType="DRAWING") + camera = bpy.data.cameras.new("Camera") + camera.type = "PERSP" + camera.shift_x = 0.25 + camera.shift_y = -0.5 + + subject.sync_perspective_camera_shifts(drawing, camera) + + pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") + assert pset["PerspectiveShiftX"] == pytest.approx(0.25) + assert pset["PerspectiveShiftY"] == pytest.approx(-0.5) + + reloaded_camera = bpy.data.cameras.new("ReloadedCamera") + reloaded_camera.type = "PERSP" + subject.import_camera_props(drawing, reloaded_camera) + + assert reloaded_camera.shift_x == pytest.approx(0.25) + assert reloaded_camera.shift_y == pytest.approx(-0.5) + + def test_ignores_non_perspective_camera_shifts(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + drawing = ifc.createIfcAnnotation(ObjectType="DRAWING") + camera = bpy.data.cameras.new("Camera") + camera.type = "ORTHO" + camera.shift_x = 0.25 + camera.shift_y = -0.5 + + subject.sync_perspective_camera_shifts(drawing, camera) + + assert ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") is None + + class TestCreateSvgSheet(NewFile): def test_run(self): ifc = ifcopenshell.file()