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
This commit is contained in:
Andrej730
2025-04-24 15:16:48 +05:00
parent d0dc31588d
commit 82f25f50cc
5 changed files with 98 additions and 53 deletions
+10 -19
View File
@@ -37,28 +37,19 @@ def depsgraph_update_pre_handler(scene):
def set_active_camera_resolution(scene: bpy.types.Scene) -> None: 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() props = tool.Drawing.get_document_props()
camera_obj = scene.camera camera_obj = scene.camera
if not camera_obj or "/" not in camera_obj.name or not props.drawings: if not camera_obj or "/" not in camera_obj.name or not props.drawings:
return return
assert isinstance((camera := camera_obj.data), bpy.types.Camera) assert isinstance((camera := camera_obj.data), bpy.types.Camera)
props = tool.Drawing.get_camera_props(camera) props = tool.Drawing.get_camera_props(camera)
ortho_scale = max((props.width, props.height)) ortho_scale, aspect_ratio = props.get_scale_and_aspect_ratio()
aspect_ratio = props.width / props.height scene_render = scene.render
if (camera.ortho_scale != ortho_scale) or (scene.render.resolution_x / scene.render.resolution_y != aspect_ratio): if (camera.ortho_scale != ortho_scale) or not tool.Cad.is_x(
camera.ortho_scale = ortho_scale scene_render.resolution_x / scene_render.resolution_y, aspect_ratio
):
diagram_scale = tool.Drawing.get_diagram_scale(camera_obj) raster_x, raster_y = props.update_camera_resolution()
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"]) scene_render.resolution_x = raster_x
scene_render.resolution_y = raster_y
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)
@@ -277,7 +277,7 @@ class CreateDrawing(bpy.types.Operator):
self.drawing_name = self.drawing.Name self.drawing_name = self.drawing.Name
self.metadata = tool.Drawing.get_drawing_metadata(self.camera_element) self.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
self.get_scale(context) 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="") bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
# Reassign props as data is recreated during the update. # Reassign props as data is recreated during the update.
self.cprops = tool.Drawing.get_camera_props(self.camera) self.cprops = tool.Drawing.get_camera_props(self.camera)
@@ -2164,7 +2164,7 @@ class ActivateDrawingBase:
# Save drawing bounds to the .ifc file # Save drawing bounds to the .ifc file
camera = context.scene.camera camera = context.scene.camera
camera_props = tool.Drawing.get_camera_props(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="") bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
# See 6452 and 6478. # See 6452 and 6478.
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT") # bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
+58 -10
View File
@@ -28,6 +28,7 @@ import bonsai.tool as tool
import bonsai.core.drawing as core import bonsai.core.drawing as core
import bonsai.bim.module.drawing.annotation as annotation import bonsai.bim.module.drawing.annotation as annotation
import bonsai.bim.module.drawing.decoration as decoration import bonsai.bim.module.drawing.decoration as decoration
from mathutils import Matrix
from bonsai.bim.prop import BIMFilterGroup from bonsai.bim.prop import BIMFilterGroup
from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data 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: if not self.update_props:
return return
assert context.scene 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 return
element = tool.Ifc.get_entity(context.scene.camera) if not (element := tool.Ifc.get_entity(camera)):
if not element:
return return
try: try:
element = ( element = (
@@ -93,6 +93,7 @@ def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context
else: else:
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing") 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) 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: def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
@@ -492,6 +493,10 @@ class DocProperties(PropertyGroup):
classes_to_wireframe: str classes_to_wireframe: str
def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
self.update_camera_resolution()
class BIMCameraProperties(PropertyGroup): class BIMCameraProperties(PropertyGroup):
linework_mode: EnumProperty( linework_mode: EnumProperty(
items=[ items=[
@@ -553,8 +558,8 @@ class BIMCameraProperties(PropertyGroup):
raster_x: IntProperty(name="Raster X", default=1000) raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000) raster_y: IntProperty(name="Raster Y", default=1000)
dpi: IntProperty(name="DPI", default=75, update=update_dpi) dpi: IntProperty(name="DPI", default=75, update=update_dpi)
width: FloatProperty(name="Width", default=50, subtype="DISTANCE") width: FloatProperty(name="Width", default=50, subtype="DISTANCE", update=update_width_height)
height: FloatProperty(name="Height", default=50, subtype="DISTANCE") height: FloatProperty(name="Height", default=50, subtype="DISTANCE", update=update_width_height)
is_nts: BoolProperty(name="Is NTS", update=update_is_nts) is_nts: BoolProperty(name="Is NTS", update=update_is_nts)
active_drawing_style_index: IntProperty(name="Active Drawing Style Index") active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
filter_mode: StringProperty(name="Filter Mode", default="NONE") 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" # 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 # 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: def update_representation(self, matrix_world: Matrix) -> bool:
assert isinstance(obj.data, bpy.types.Camera) """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( 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_x": self.raster_x,
"raster_y": self.raster_y, "raster_y": self.raster_y,
"ortho_scale": obj.data.ortho_scale, "ortho_scale": camera.ortho_scale,
"clip_end": obj.data.clip_end, "clip_end": camera.clip_end,
} }
) )
if self.representation != representation: if self.representation != representation:
@@ -611,6 +624,41 @@ class BIMCameraProperties(PropertyGroup):
return True return True
return False 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 DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
BOX_ALIGNMENT_POSITIONS = [ BOX_ALIGNMENT_POSITIONS = [
+15 -11
View File
@@ -745,6 +745,16 @@ class Drawing(bonsai.core.tool.Drawing):
for obj in ifc_importer.added_data.values(): for obj in ifc_importer.added_data.values():
tool.Collector.assign(obj) 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 # NOTE: EPsetDrawing pset is completely synced with BIMCameraProperties
# but BIMCameraProperties are only synced with EPsetDrawing at drawing import # but BIMCameraProperties are only synced with EPsetDrawing at drawing import
# therefore camera props can differ from pset if the user changed them from pset. # 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) cls.import_camera_props(drawing, camera)
tool.Ifc.link(drawing, obj) tool.Ifc.link(drawing, obj)
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape)) obj.matrix_world = cls.get_camera_shape_matrix(drawing, shape)
obj.matrix_world = mat
if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW":
obj.matrix_world[1][1] *= -1
tool.Geometry.record_object_position(obj) tool.Geometry.record_object_position(obj)
tool.Collector.assign(obj) tool.Collector.assign(obj)
@@ -787,10 +793,8 @@ class Drawing(bonsai.core.tool.Drawing):
obj.data = camera obj.data = camera
else: else:
obj = bpy.data.objects.new(tool.Loader.get_name(drawing), camera) obj = bpy.data.objects.new(tool.Loader.get_name(drawing), camera)
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
obj.matrix_world = mat obj.matrix_world = cls.get_camera_shape_matrix(drawing, shape)
if cls.get_drawing_target_view(drawing) == "REFLECTED_PLAN_VIEW":
obj.matrix_world[1][1] *= -1
return obj return obj
@classmethod @classmethod
@@ -2225,8 +2229,8 @@ class Drawing(bonsai.core.tool.Drawing):
return float(numerator) / float(denominator) return float(numerator) / float(denominator)
@classmethod @classmethod
def get_diagram_scale(cls, obj: bpy.types.Object) -> dict[str, str]: def get_diagram_scale(cls, camera: Union[bpy.types.Object, bpy.types.Camera]) -> dict[str, str]:
props = cls.get_camera_props(obj) props = cls.get_camera_props(camera)
scale = props.diagram_scale scale = props.diagram_scale
if scale != "CUSTOM": if scale != "CUSTOM":
human_scale, scale = scale.split("|") human_scale, scale = scale.split("|")
+13 -11
View File
@@ -815,15 +815,13 @@ class Loader(bonsai.core.tool.Loader):
cls, cls,
element: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance,
representation: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType], shape: ifcopenshell.geom.ShapeElementType,
) -> bpy.types.Camera: ) -> bpy.types.Camera:
from bonsai.bim.module.drawing.prop import get_diagram_scales """Create camera data.
if isinstance(shape, ifcopenshell.geom.ShapeElementType):
geometry = shape.geometry
else:
geometry = shape
Camera props are automatically updated based on ``element`` and ``shape``.
"""
geometry = shape.geometry
v = geometry.verts v = geometry.verts
x = [v[i] for i in range(0, len(v), 3)] x = [v[i] for i in range(0, len(v), 3)]
y = [v[i + 1] 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_start = 0.002 # Technically 0, but Blender doesn't allow this, so 2mm it is!
camera.clip_end = depth camera.clip_end = depth
props.width = width props["width"] = width
props.height = height props["height"] = height
elif camera_type == "PERSP": elif camera_type == "PERSP":
abs_min_z = abs(min(z)) abs_min_z = abs(min(z))
abs_max_z = abs(max(z)) abs_max_z = abs(max(z))
@@ -854,8 +852,8 @@ class Loader(bonsai.core.tool.Loader):
camera.clip_end = abs_min_z camera.clip_end = abs_min_z
max_res = 1000 max_res = 1000
props.width = width props["width"] = width
props.height = height props["height"] = height
if width > height: if width > height:
fov = 2 * atan(width / (2 * abs_min_z)) fov = 2 * atan(width / (2 * abs_min_z))
@@ -865,6 +863,10 @@ class Loader(bonsai.core.tool.Loader):
camera.angle = fov camera.angle = fov
tool.Drawing.import_camera_props(element, camera) 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 return camera
@classmethod @classmethod