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:
"""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
@@ -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")
+58 -10
View File
@@ -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 = [
+15 -11
View File
@@ -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("|")
+13 -11
View File
@@ -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