Support for rotated and scaled symbols with IfcAnnotation/SYMBOL

See: https://community.osarch.org/discussion/2027/bbim-rotation-of-ifcsymbol
This commit is contained in:
Andrej730
2024-03-14 14:51:46 +05:00
parent fad51eb388
commit 4b634b91ac
@@ -29,6 +29,7 @@ import svgwrite
import ifcopenshell import ifcopenshell
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.representation import ifcopenshell.util.representation
import ifcopenshell.util.selector
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.bim.module.drawing.helper as helper import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.module.drawing.annotation as annotation import blenderbim.bim.module.drawing.annotation as annotation
@@ -40,6 +41,7 @@ from blenderbim.bim.ifc import IfcStore
from math import pi, ceil, atan, degrees, acos from math import pi, ceil, atan, degrees, acos
from mathutils import geometry, Vector from mathutils import geometry, Vector
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from typing import Optional
class External(svgwrite.container.Group): class External(svgwrite.container.Group):
@@ -323,7 +325,7 @@ class SvgWriter:
) )
) )
def draw_misc_annotation(self, obj): def draw_misc_annotation(self, obj: bpy.types.Object) -> None:
# We have to decide whether this should come from Blender or from IFC. # We have to decide whether this should come from Blender or from IFC.
# For the moment, for convenience of experimenting with ideas, it comes # For the moment, for convenience of experimenting with ideas, it comes
# from Blender. In the future, it should probably come from IFC. # from Blender. In the future, it should probably come from IFC.
@@ -357,14 +359,14 @@ class SvgWriter:
d = "M{}".format(d[1:]) d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes))) path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def get_attribute_classes(self, obj): def get_attribute_classes(self, obj: bpy.types.Object) -> list[str]:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
global_id = "GlobalId-{}".format(element.GlobalId) global_id = "GlobalId-{}".format(element.GlobalId)
predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name( predefined_type = "PredefinedType-" + tool.Drawing.canonicalise_class_name(
str(ifcopenshell.util.element.get_predefined_type(element)) str(ifcopenshell.util.element.get_predefined_type(element))
) )
classes = [global_id, element.is_a(), predefined_type] classes = [global_id, element.is_a(), predefined_type]
custom_classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes") custom_classes: str = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
if custom_classes: if custom_classes:
classes.extend(custom_classes.split()) classes.extend(custom_classes.split())
for key in self.metadata: for key in self.metadata:
@@ -788,7 +790,7 @@ class SvgWriter:
attrib["filter"] = "url(#fill-background)" attrib["filter"] = "url(#fill-background)"
return element return element
def draw_text_annotation(self, text_obj, position): def draw_text_annotation(self, text_obj: bpy.types.Object, position: Vector) -> None:
x_offset = self.raw_width / 2 x_offset = self.raw_width / 2
y_offset = self.raw_height / 2 y_offset = self.raw_height / 2
element = tool.Ifc.get_entity(text_obj) element = tool.Ifc.get_entity(text_obj)
@@ -800,17 +802,7 @@ class SvgWriter:
text_position_svg = text_position * self.svg_scale text_position_svg = text_position * self.svg_scale
text_position_svg_str = ", ".join(map(str, text_position_svg)) text_position_svg_str = ", ".join(map(str, text_position_svg))
def get_basis_vector(matrix, i=0): angle = self.get_empty_object_angle(text_obj)
"""returns basis vector for i in world space, unaffected by object scale"""
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(text_obj.matrix_world)
# RCP cameras may be scaled, so reset scales.
camera_matrix = tool.Drawing.get_camera_matrix(self.camera)
text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
angle = math.degrees(-text_dir.angle_signed(Vector((1, 0))))
classes = self.get_attribute_classes(text_obj) classes = self.get_attribute_classes(text_obj)
classes_str = " ".join(classes) classes_str = " ".join(classes)
fill_bg = "fill-bg" in classes fill_bg = "fill-bg" in classes
@@ -818,8 +810,7 @@ class SvgWriter:
symbol = tool.Drawing.get_annotation_symbol(element) symbol = tool.Drawing.get_annotation_symbol(element)
template_text_fields = [] template_text_fields = []
if symbol: if symbol:
# NOTE: for now we assume that scale is uniform symbol_transform = self.get_symbol_transform(text_position_svg_str, angle, text_obj)
symbol_transform = f"translate({text_position_svg_str}) rotate({angle}) scale({text_obj.scale.x})"
symbol_svg = self.find_xml_symbol_by_id(symbol) symbol_svg = self.find_xml_symbol_by_id(symbol)
if symbol_svg: if symbol_svg:
@@ -848,7 +839,7 @@ class SvgWriter:
return None return None
if not symbol_svg or not template_text_fields: if not symbol_svg or not template_text_fields:
self.svg.add(self.svg.use(f"#{symbol}", transform=symbol_transform)) self.draw_symbol(symbol, symbol_transform)
line_number = 0 line_number = 0
for text_literal in text_literals: for text_literal in text_literals:
@@ -866,7 +857,7 @@ class SvgWriter:
self.svg.add(tag) self.svg.add(tag)
line_number += len(tag.elements) line_number += len(tag.elements)
def draw_empty_annotation(self, obj, classes): def draw_empty_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
x_offset = self.raw_width / 2 x_offset = self.raw_width / 2
y_offset = self.raw_height / 2 y_offset = self.raw_height / 2
@@ -884,9 +875,37 @@ class SvgWriter:
point = Vector(((x_offset + point.x), (y_offset - point.y))) point = Vector(((x_offset + point.x), (y_offset - point.y)))
symbol_position_svg = point * self.svg_scale symbol_position_svg = point * self.svg_scale
self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg)) svg_position_str = ", ".join(map(str, symbol_position_svg))
def draw_point_annotation(self, obj, classes): angle = self.get_empty_object_angle(obj)
symbol_transform = self.get_symbol_transform(svg_position_str, angle, obj)
self.draw_symbol(svg_id, symbol_transform)
def get_empty_object_angle(self, obj: bpy.types.Object) -> float:
def get_basis_vector(matrix: bpy.types.Object, i: int = 0) -> Vector:
"""returns basis vector for i in world space, unaffected by object scale"""
return matrix.inverted()[i].to_3d().normalized()
text_dir_world_x_axis = get_basis_vector(obj.matrix_world)
# RCP cameras may be scaled, so reset scales.
camera_matrix = tool.Drawing.get_camera_matrix(self.camera)
text_dir = (camera_matrix.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
return math.degrees(-text_dir.angle_signed(Vector((1, 0))))
def get_symbol_transform(
self, svg_position_str: str, angle: float = 0.0, obj: Optional[bpy.types.Object] = None
) -> str:
"""`obj` will be used to identify symbol scale,
ignore it if object scale doesn't match the symbol scale"""
# NOTE: for now we assume that scale is uniform
scale = 1.0 if not obj else obj.scale.x
return f"translate({svg_position_str}) rotate({angle}) scale({scale})"
def draw_symbol(self, symbol_id: str, symbol_transform: str) -> None:
self.svg.add(self.svg.use(f"#{symbol_id}", transform=symbol_transform))
def draw_point_annotation(self, obj: bpy.types.Object, classes: list[str]) -> None:
x_offset = self.raw_width / 2 x_offset = self.raw_width / 2
y_offset = self.raw_height / 2 y_offset = self.raw_height / 2
@@ -906,7 +925,10 @@ class SvgWriter:
for symbol_position in projected_points: for symbol_position in projected_points:
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y))) symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
symbol_position_svg = symbol_position * self.svg_scale symbol_position_svg = symbol_position * self.svg_scale
self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg)) svg_position_str = ", ".join(map(str, symbol_position_svg))
symbol_transform = self.get_symbol_transform(svg_position_str)
self.draw_symbol(svg_id, symbol_transform)
def draw_break_annotations(self, obj): def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2 x_offset = self.raw_width / 2
@@ -1405,7 +1427,7 @@ class SvgWriter:
return text_tags return text_tags
def project_point_onto_camera(self, point): def project_point_onto_camera(self, point: Vector) -> Vector:
# TODO is this needlessly complex? # TODO is this needlessly complex?
return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane( return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
point.xyz, point.xyz,