This commit is contained in:
Andrej730
2025-04-21 12:43:08 +05:00
parent 558bda1aea
commit 476604202a
11 changed files with 127 additions and 65 deletions
@@ -38,18 +38,17 @@ def depsgraph_update_pre_handler(scene):
def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
props = tool.Drawing.get_document_props()
if not scene.camera or "/" not in scene.camera.name or not props.drawings:
camera_obj = scene.camera
if not camera_obj or "/" not in camera_obj.name or not props.drawings:
return
assert isinstance(scene.camera.data, bpy.types.Camera)
props = scene.camera.data.BIMCameraProperties
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 (scene.camera.data.ortho_scale != ortho_scale) or (
scene.render.resolution_x / scene.render.resolution_y != aspect_ratio
):
scene.camera.data.ortho_scale = ortho_scale
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(scene.camera)
diagram_scale = tool.Drawing.get_diagram_scale(camera_obj)
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
if props.width > props.height:
@@ -61,5 +60,5 @@ def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
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 = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
scene.render.resolution_x = props.raster_x = int(raster_x)
scene.render.resolution_y = props.raster_y = int(raster_y)
@@ -413,12 +413,13 @@ def get_project_collection(scene):
return colls[0]
def parse_diagram_scale(camera):
def parse_diagram_scale(camera: bpy.types.Camera) -> float:
"""Returns numeric value of scale"""
if camera.BIMCameraProperties.diagram_scale == "CUSTOM":
_, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|")
props = tool.Drawing.get_camera_props(camera)
if props.diagram_scale == "CUSTOM":
_, fraction = props.custom_diagram_scale.split("|")
else:
_, fraction = camera.BIMCameraProperties.diagram_scale.split("|")
_, fraction = props.diagram_scale.split("|")
numerator, denominator = fraction.split("/")
return float(numerator) / float(denominator)
@@ -431,12 +432,11 @@ def ortho_view_frame(
Similar to `bpy.types.Camera.view_frame`
:arg camera: camera of drawing
:type camera: bpy.types.Camera + BIMCameraProperties
:arg margin: margins, in scene units
:type margin: float
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x
props = tool.Drawing.get_camera_props(camera)
aspect = props.raster_y / props.raster_x
size = camera.ortho_scale
hwidth = size * 0.5
hheight = size * 0.5 * aspect
@@ -228,7 +228,9 @@ class CreateDrawing(bpy.types.Operator):
if not tool.Drawing.is_drawing_active():
cls.poll_message_set("No active drawing.")
return False
if context.scene.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE" and not hasattr(
assert context.scene
assert (camera_obj := context.scene.camera)
if tool.Drawing.get_camera_props(camera_obj).linework_mode == "FREESTYLE" and not hasattr(
context.scene, "svg_export"
):
cls.poll_message_set(
@@ -248,6 +250,7 @@ class CreateDrawing(bpy.types.Operator):
def execute(self, context):
self.props = tool.Drawing.get_document_props()
assert context.scene and context.scene.camera
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
if self.print_all:
@@ -262,13 +265,14 @@ class CreateDrawing(bpy.types.Operator):
bpy.ops.bim.activate_drawing(drawing=drawing_id, should_view_from_camera=False)
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
assert (camera_element := tool.Ifc.get_entity(self.camera))
self.camera_element = camera_element
self.camera_document = tool.Drawing.get_drawing_document(self.camera_element)
self.file = tool.Ifc.get()
with profile("Drawing generation process"):
with profile("Initialize drawing generation process"):
self.cprops = self.camera.data.BIMCameraProperties
self.cprops = tool.Drawing.get_camera_props(self.camera)
self.drawing = self.file.by_id(drawing_id)
self.drawing_name = self.drawing.Name
self.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
@@ -297,11 +301,11 @@ class CreateDrawing(bpy.types.Operator):
with profile("Generate linework"):
if tool.Drawing.is_camera_orthographic():
if self.camera.data.BIMCameraProperties.linework_mode == "OPENCASCADE":
if self.cprops.linework_mode == "OPENCASCADE":
linework_svg = self.generate_linework(context)
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
elif self.cprops.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
elif self.cprops.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
with profile("Generate annotation"):
@@ -876,19 +880,19 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
if self.cprops.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
elif self.cprops.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
if self.camera.data.BIMCameraProperties.fill_mode == "SHAPELY":
if self.cprops.fill_mode == "SHAPELY":
# shapely variant
group = root.find("{http://www.w3.org/2000/svg}g")
@@ -960,7 +964,7 @@ class CreateDrawing(bpy.types.Operator):
path.set("class", " ".join(list(classes)))
group.insert(0, path)
if self.camera.data.BIMCameraProperties.fill_mode == "SVGFILL":
if self.cprops.fill_mode == "SVGFILL":
results = etree.tostring(root).decode("utf8")
svg_data_1 = results
from xml.dom.minidom import parseString
@@ -1449,10 +1453,9 @@ class CreateDrawing(bpy.types.Operator):
g.set("class", " ".join(list(classes)))
group.append(g)
def drawing_to_model_co(self, x, y):
def drawing_to_model_co(self, x: float, y: float) -> Vector:
camera_xy = np.array((x, -y)) / self.scale / 1000
camera_props = self.camera.data.BIMCameraProperties
camera_xy += np.array((camera_props.width / -2, camera_props.height / 2)) # top left offset
camera_xy += np.array((self.cprops.width / -2, self.cprops.height / 2)) # top left offset
return self.camera.matrix_world @ Vector(camera_xy).to_3d()
def pythonize(self, arr):
@@ -2158,7 +2161,7 @@ class ActivateDrawingBase:
# Save drawing bounds to the .ifc file
camera = context.scene.camera
camera_props = camera.data.BIMCameraProperties
camera_props = tool.Drawing.get_camera_props(camera)
if camera_props.update_representation(camera):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
# See 6452 and 6478.
@@ -2298,7 +2301,8 @@ class ReloadDrawingStyles(bpy.types.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
camera_props = context.scene.camera.data.BIMCameraProperties
assert context.scene and (camera := context.scene.camera)
camera_props = tool.Drawing.get_camera_props(camera)
# added this part as a temporary fallback
# TODO: should remove it a bit later when projects get more accommodated
@@ -2365,12 +2369,14 @@ class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
assert context.scene and (camera_obj := context.scene.camera)
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
new = drawing_styles.add()
# drawing style is saved to ifc on rename
new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles)
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = len(drawing_styles) - 1
camera_props = tool.Drawing.get_camera_props(camera_obj)
camera_props.active_drawing_style_index = len(drawing_styles) - 1
return {"FINISHED"}
@@ -2381,9 +2387,11 @@ class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
assert context.scene and (camera_obj := context.scene.camera)
props = tool.Drawing.get_document_props()
props.drawing_styles.remove(self.index)
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = max(self.index - 1, 0)
camera_props = tool.Drawing.get_camera_props(camera_obj)
camera_props.active_drawing_style_index = max(self.index - 1, 0)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
+26 -1
View File
@@ -522,9 +522,34 @@ class BIMCameraProperties(PropertyGroup):
exclude_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Exclude Filter")
update_props: BoolProperty(name="Enable Props Auto Update", default=True)
if TYPE_CHECKING:
linework_mode: Literal["OPENCASCADE", "FREESTYLE"]
fill_mode: Literal["NONE", "SHAPELY", "SVGFILL"]
cut_mode: Literal["BISECT", "OPENCASCADE"]
has_underlay: bool
has_linework: bool
has_annotation: bool
representation: str
view_name: str
diagram_scale: str
custom_scale_numerator: str
custom_scale_denominator: str
raster_x: int
raster_y: int
dpi: int
width: float
height: float
is_nts: bool
active_drawing_style_index: int
filter_mode: str
include_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
exclude_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
update_props: bool
# 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):
def update_representation(self, obj: bpy.types.Object) -> bool:
assert isinstance(obj.data, bpy.types.Camera)
representation = json.dumps(
{
"matrix": [list(x) for x in obj.matrix_world],
@@ -1192,6 +1192,9 @@ class LinkIfc(bpy.types.Operator, ImportHelper):
filepath: str
files: list[bpy.types.OperatorFileListElement]
directory: str
filter_glob: str
use_relative_path: bool
use_cache: bool
def draw(self, context):
pprops = tool.Project.get_project_props()
+9 -9
View File
@@ -82,7 +82,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def activate_camera(cls, obj: bpy.types.Object) -> None:
area = tool.Blender.get_view3d_area()
area = cls.get_view3d_area()
is_local_view = area.spaces[0].local_view is not None
if is_local_view:
@@ -206,7 +206,7 @@ class Blender(bonsai.core.tool.Blender):
if context is None:
context = bpy.context
if obj_type == "Object":
props = tool.Blender.get_object_bim_props(bpy.data.objects[obj])
props = cls.get_object_bim_props(bpy.data.objects[obj])
return props.ifc_definition_id
elif obj_type == "Material":
props = tool.Material.get_material_props()
@@ -239,7 +239,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def is_ifc_object(cls, obj: bpy.types.Object) -> bool:
props = tool.Blender.get_object_bim_props(obj)
props = cls.get_object_bim_props(obj)
return bool(props.ifc_definition_id)
@classmethod
@@ -338,7 +338,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def set_viewport_tool(cls, tool_name: str) -> None:
with bpy.context.temp_override(**tool.Blender.get_viewport_context()):
with bpy.context.temp_override(**cls.get_viewport_context()):
bpy.ops.wm.tool_set_by_id(name=tool_name)
@classmethod
@@ -398,7 +398,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def update_viewport(cls) -> None:
tool.Blender.get_viewport_context()["area"].tag_redraw()
cls.get_viewport_context()["area"].tag_redraw()
@classmethod
def force_depsgraph_update(cls) -> None:
@@ -711,7 +711,7 @@ class Blender(bonsai.core.tool.Blender):
False if enum is still invalid (as there no enum items)
and update callback was not triggered (may need to trigger it manually).
"""
current_value = tool.Blender.get_enum_safe(props, prop_name)
current_value = cls.get_enum_safe(props, prop_name)
if current_value is not None:
# Value is valid, just trigger the update callback.
setattr(props, prop_name, current_value)
@@ -958,7 +958,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def get_layer_collection(cls, collection: bpy.types.Collection) -> Union[bpy.types.LayerCollection, None]:
project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
project_collection = tool.Blender.get_object_bim_props(project).collection
project_collection = cls.get_object_bim_props(project).collection
for layer_collection in bpy.context.view_layer.layer_collection.children:
if layer_collection.collection == project_collection:
for layer_collection2 in layer_collection.children:
@@ -1564,7 +1564,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def get_user_data_dir(cls) -> Path:
props = tool.Blender.get_bim_props()
props = cls.get_bim_props()
return Path(props.data_dir)
@classmethod
@@ -1649,7 +1649,7 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
if isinstance(obj, bpy.types.Object):
return tool.Blender.get_object_bim_props(obj).ifc_definition_id
return cls.get_object_bim_props(obj).ifc_definition_id
return tool.Style.get_material_style_props(obj).ifc_definition_id
@classmethod
+21 -4
View File
@@ -55,7 +55,13 @@ from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECK
from pathlib import Path
if TYPE_CHECKING:
from bonsai.bim.module.drawing.prop import DocProperties, Sheet, BIMAnnotationProperties, BIMTextProperties
from bonsai.bim.module.drawing.prop import (
DocProperties,
Sheet,
BIMAnnotationProperties,
BIMTextProperties,
BIMCameraProperties,
)
from bonsai.bim.module.drawing.prop import Drawing as DrawingProperties
@@ -106,6 +112,17 @@ class Drawing(bonsai.core.tool.Drawing):
def get_text_props(cls, obj: bpy.types.Object) -> BIMTextProperties:
return obj.BIMTextProperties
@classmethod
def get_camera_props(cls, camera: Union[bpy.types.Object, bpy.types.Camera]) -> BIMCameraProperties:
"""
:param camera: Camera object or camera.
"""
if isinstance(camera, bpy.types.Camera):
data = camera
else:
assert isinstance(data := camera.data, bpy.types.Camera)
return data.BIMCameraProperties
@classmethod
def canonicalise_class_name(cls, name: str) -> str:
return re.sub("[^0-9a-zA-Z]+", "", name)
@@ -780,7 +797,7 @@ 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 = camera.BIMCameraProperties
camera_props = tool.Drawing.get_camera_props(camera)
update_props = camera_props.update_props
camera_props.update_props = False
@@ -2199,8 +2216,8 @@ class Drawing(bonsai.core.tool.Drawing):
return float(numerator) / float(denominator)
@classmethod
def get_diagram_scale(cls, obj: bpy.types.Object) -> dict[str, float]:
props = obj.data.BIMCameraProperties
def get_diagram_scale(cls, obj: bpy.types.Object) -> dict[str, str]:
props = cls.get_camera_props(obj)
scale = props.diagram_scale
if scale != "CUSTOM":
human_scale, scale = scale.split("|")
+5 -4
View File
@@ -839,13 +839,14 @@ class Loader(bonsai.core.tool.Loader):
camera = bpy.data.cameras.new(tool.Loader.get_mesh_name_from_shape(geometry))
camera.type = camera_type
camera.show_limits = True
props = tool.Drawing.get_camera_props(camera)
if camera_type == "ORTHO":
camera.clip_start = 0.002 # Technically 0, but Blender doesn't allow this, so 2mm it is!
camera.clip_end = depth
camera.BIMCameraProperties.width = width
camera.BIMCameraProperties.height = height
props.width = width
props.height = height
elif camera_type == "PERSP":
abs_min_z = abs(min(z))
abs_max_z = abs(max(z))
@@ -853,8 +854,8 @@ class Loader(bonsai.core.tool.Loader):
camera.clip_end = abs_min_z
max_res = 1000
camera.BIMCameraProperties.width = width
camera.BIMCameraProperties.height = height
props.width = width
props.height = height
if width > height:
fov = 2 * atan(width / (2 * abs_min_z))
+4 -2
View File
@@ -51,9 +51,11 @@ class Search(bonsai.core.tool.Search):
elif module == "diff":
return tool.Blender.get_diff_props().filter_groups
elif module == "drawing_include":
return bpy.context.scene.camera.data.BIMCameraProperties.include_filter_groups
assert (scene := bpy.context.scene) and (camera_obj := (scene.camera))
return tool.Drawing.get_camera_props(camera_obj).include_filter_groups
elif module == "drawing_exclude":
return bpy.context.scene.camera.data.BIMCameraProperties.exclude_filter_groups
assert (scene := bpy.context.scene) and (camera_obj := (scene.camera))
return tool.Drawing.get_camera_props(camera_obj).exclude_filter_groups
elif module.startswith("clash"):
_, clash_set_index, ab, clash_source_index = module.split("_")
props = tool.Clash.get_clash_props()
@@ -28,6 +28,7 @@ from typing import Union, Optional, Literal, Any, TYPE_CHECKING
from ifcopenshell.util.shape_builder import ifc_safe_vector_type, VectorType
if TYPE_CHECKING:
import bonsai.tool as tool
from bonsai.bim.module.geometry.helper import Helper
@@ -81,9 +82,11 @@ def add_representation(
"""
# lazy import Helper to avoid circular import
if "Helper" not in globals():
import bonsai.tool as tool
from bonsai.bim.module.geometry.helper import Helper
globals()["Helper"] = Helper
globals()["tool"] = tool
usecase = Usecase()
# TODO: This usecase currently depends on Blender's data model
@@ -111,7 +114,7 @@ class Usecase:
settings: dict[str, Any]
ifc_vertices: list[ifcopenshell.entity_instance]
coordinate_offset: Union[npt.NDArray[np.float64], None]
geometry: Union[bpy.types.Mesh, bpy.types.Curve]
geometry: Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera]
blender_object: bpy.types.Object
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
@@ -319,8 +322,10 @@ class Usecase:
return self.create_mesh_representation()
def create_camera_block_representation(self) -> ifcopenshell.entity_instance:
raster_x = self.settings["geometry"].BIMCameraProperties.raster_x
raster_y = self.settings["geometry"].BIMCameraProperties.raster_y
assert isinstance(self.geometry, bpy.types.Camera)
props = tool.Drawing.get_camera_props(self.geometry)
raster_x = props.raster_x
raster_y = props.raster_y
if self.is_camera_landscape():
width = self.settings["geometry"].ortho_scale
@@ -347,8 +352,10 @@ class Usecase:
)
def create_camera_pyramid_representation(self) -> ifcopenshell.entity_instance:
raster_x = self.settings["geometry"].BIMCameraProperties.raster_x
raster_y = self.settings["geometry"].BIMCameraProperties.raster_y
assert isinstance(self.geometry, bpy.types.Camera)
props = tool.Drawing.get_camera_props(self.geometry)
raster_x = props.raster_x
raster_y = props.raster_y
fov = self.settings["geometry"].angle
clip_end = self.settings["geometry"].clip_end
@@ -389,10 +396,9 @@ class Usecase:
)
def is_camera_landscape(self) -> bool:
return (
self.settings["geometry"].BIMCameraProperties.raster_x
> self.settings["geometry"].BIMCameraProperties.raster_y
)
assert isinstance(self.geometry, bpy.types.Camera)
props = tool.Drawing.get_camera_props(self.geometry)
return props.raster_x > props.raster_y
def create_swept_disk_solid_representation(self) -> ifcopenshell.entity_instance:
return self.file.createIfcShapeRepresentation(
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import datetime
import isodate
from re import findall
@@ -37,7 +38,7 @@ def timedelta2duration(timedelta):
return isodate.Duration(**components)
def ifc2datetime(element):
def ifc2datetime(element: Union[str, int, ifcopenshell.entity_instance]):
if isinstance(element, str) and "P" in element[0:2]: # IfcDuration
duration = parse_duration(element)
if isinstance(duration, datetime.timedelta):
@@ -246,7 +247,7 @@ def parse_duration(value: Union[str, None]) -> Union[datetime.timedelta, None]:
return None
def canonicalise_time(time):
def canonicalise_time(time: Union[datetime.datetime, None]) -> str:
if not time:
return "-"
return time.strftime("%d/%m/%y")