Fix #7268. Automatic annotation references (sections, elevations, etc) are now intelligently regenerated

This commit is contained in:
Dion Moult
2025-10-24 12:47:50 +11:00
parent 5dbfe539ac
commit 8824e6e2c2
2 changed files with 371 additions and 224 deletions
+20 -36
View File
@@ -471,56 +471,40 @@ def sync_references(
if not drawing_tool.has_annotation(drawing): if not drawing_tool.has_annotation(drawing):
return return
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing)) if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))):
if not context:
return return
group = drawing_tool.get_drawing_group(drawing) group = drawing_tool.get_drawing_group(drawing)
potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing) potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing)
for element in potential_reference_elements:
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
drawing_tool.sync_object_placement(obj)
for element in drawing_tool.get_group_elements(group): for element in drawing_tool.get_group_elements(group):
if ( if not drawing_tool.is_auto_annotation(element):
drawing_tool.is_auto_annotation(element) continue
and drawing_tool.get_assigned_product(element) not in potential_reference_elements if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
): drawing_tool.sync_object_placement(obj)
reference_element = drawing_tool.get_assigned_product(element)
reference_obj = ifc.get_object(reference_element)
if reference_element not in potential_reference_elements:
# It was auto created, so it makes sense to auto delete
if obj := ifc.get_object(element):
drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element)
elif not drawing_tool.regenerate_reference_annotation(drawing, element, reference_element, context):
if obj := ifc.get_object(element): if obj := ifc.get_object(element):
drawing_tool.delete_object(obj) drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element) ifc.run("root.remove_product", product=element)
for reference_element in potential_reference_elements: for reference_element in potential_reference_elements:
reference_obj = ifc.get_object(reference_element) if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element) if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
should_delete_existing_annotation = False
should_create_annotation = False
# remove annotation only if the reference object was changed
# otherwise we rely on the existing annotation
if annotation:
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
should_delete_existing_annotation = True
if should_delete_existing_annotation or not annotation:
should_create_annotation = True
if should_delete_existing_annotation:
annotation_obj = ifc.get_object(annotation)
if annotation_obj:
drawing_tool.delete_object(annotation_obj)
ifc.run("root.remove_product", product=annotation)
if should_create_annotation:
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
if annotation:
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation) ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
ifc.run("group.assign_group", group=group, products=[annotation]) ifc.run("group.assign_group", group=group, products=[annotation])
collector.assign(ifc.get_object(annotation)) collector.assign(ifc.get_object(annotation))
if reference_obj and ifc.is_moved(reference_obj):
drawing_tool.sync_object_placement(reference_obj)
if reference_obj and ifc.is_edited(reference_obj):
drawing_tool.sync_object_representation(reference_obj)
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None: def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
drawing.select_assigned_product(context) drawing.select_assigned_product(context)
+351 -188
View File
@@ -885,7 +885,13 @@ class Drawing(bonsai.core.tool.Drawing):
def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for rel in element.HasAssignments: for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"): if rel.is_a("IfcRelAssignsToProduct"):
return rel.RelatingProduct product = rel.RelatingProduct
if product.is_a("IfcGrid") and rel.Name:
for attribute in ("UAxes", "VAxes", "WAxes"):
for axis in getattr(product, attribute) or []:
if axis.AxisTag == rel.Name:
return axis
return product
@classmethod @classmethod
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None: def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
@@ -1514,7 +1520,7 @@ class Drawing(bonsai.core.tool.Drawing):
# IfcRelAssignsToProduct.RelatingProduct = IfcGrid # IfcRelAssignsToProduct.RelatingProduct = IfcGrid
# IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag # IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag
grid = None grid = None
for attribute in ("PartOfW", "PartOfV", "PartOfU"): for attribute in ("PartOfU", "PartOfV", "PartOfW"):
if getattr(reference_element, attribute, None): if getattr(reference_element, attribute, None):
grid = getattr(reference_element, attribute)[0] grid = getattr(reference_element, attribute)[0]
break break
@@ -1536,6 +1542,26 @@ class Drawing(bonsai.core.tool.Drawing):
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element: if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element:
return element return element
@classmethod
def regenerate_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
annotation: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
if reference_element.is_a("IfcGridAxis"):
return cls.regenerate_grid_axis_reference_annotation(drawing, annotation, reference_element, context)
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
if target_view == "ELEVATION_VIEW":
return cls.regenerate_elevation_reference_annotation(drawing, annotation, reference_element, context)
elif target_view == "SECTION_VIEW":
return cls.regenerate_section_reference_annotation(drawing, annotation, reference_element, context)
elif reference_element.is_a("IfcBuildingStorey"):
return cls.regenerate_storey_annotation(drawing, annotation, reference_element, context)
return annotation
@classmethod @classmethod
def generate_reference_annotation( def generate_reference_annotation(
cls, cls,
@@ -1545,238 +1571,279 @@ class Drawing(bonsai.core.tool.Drawing):
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
if reference_element.is_a("IfcGridAxis"): if reference_element.is_a("IfcGridAxis"):
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context) return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING": elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
obj = tool.Ifc.get_object(drawing)
if obj is None:
# Annotations in that drawing are lazy loaded as needed
obj = cls.import_drawing(drawing)
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView") target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
if target_view == "ELEVATION_VIEW": if target_view == "ELEVATION_VIEW":
ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_elevation_reference_annotation(drawing, reference_element, context) return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
elif target_view == "SECTION_VIEW": elif target_view == "SECTION_VIEW":
ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_section_reference_annotation(drawing, reference_element, context) return cls.generate_section_reference_annotation(drawing, reference_element, context)
elif reference_element.is_a("IfcBuildingStorey"): elif reference_element.is_a("IfcBuildingStorey"):
return cls.generate_storey_annotation(drawing, reference_element, context) return cls.generate_storey_annotation(drawing, reference_element, context)
@classmethod
def generate_storey_points(
cls, drawing: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance
) -> list | None:
import bonsai.bim.module.drawing.helper as helper
camera = tool.Ifc.get_object(drawing)
if camera.data.type != "ORTHO":
return
if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix()):
return
xmin, xmax, ymin, ymax = helper.ortho_view_frame(camera.data)[:4]
rl = ifcopenshell.util.placement.get_local_placement(storey.ObjectPlacement)[2][3]
y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl))).y
if y < ymin or y > ymax:
return
return (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
@classmethod @classmethod
def generate_storey_annotation( def generate_storey_annotation(
cls, cls,
drawing: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
if not (points := cls.generate_storey_points(drawing, storey)):
return
camera = tool.Ifc.get_object(drawing)
mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new(storey.Name or "Unnamed", mesh)
obj.matrix_world = cls.get_default_annotation_matrix(camera)
element = cls.run_root_assign_class(
obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION_LEVEL", should_add_representation=False
)
tool.Geometry.run_edit_object_placement(obj)
element.Name = storey.Name or "Unnamed"
builder = ShapeBuilder(tool.Ifc.get())
representation = builder.get_representation(context, [builder.polyline(points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
return element
@classmethod
def regenerate_storey_annotation(
cls,
drawing: ifcopenshell.entity_instance,
annotation: ifcopenshell.entity_instance,
storey: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
if not (points := cls.generate_storey_points(drawing, storey)):
return
camera = tool.Ifc.get_object(drawing)
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, annotation)
m = ifcopenshell.util.shape.get_shape_matrix(shape)
mw = cls.get_default_annotation_matrix(camera)
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
new_points = None
if not np.allclose(m, np.array(mw), atol=1e-4):
new_points = points
elif len(existing_verts) != 2:
new_points = points
else:
existing_verts = sorted(existing_verts, key=lambda v: v.x)
xmin, xmax = [v.x for v in existing_verts]
y = points[0].y
if not tool.Cad.is_x(y, existing_verts[0].y) or not tool.Cad.is_x(y, existing_verts[1].y):
new_points = (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
if new_points:
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
ifcopenshell.api.geometry.unassign_representation(
tool.Ifc.get(), product=annotation, representation=representation
)
builder = ShapeBuilder(tool.Ifc.get())
representation = builder.get_representation(context, [builder.polyline(new_points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
if obj := tool.Ifc.get_object(annotation):
obj.matrix_world = mw
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
)
else:
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
annotation.Name = storey.Name or "Unnamed"
return annotation
@classmethod
def generate_section_reference_points(
cls, drawing: ifcopenshell.entity_instance, section: ifcopenshell.entity_instance
) -> list | None:
import bonsai.bim.module.drawing.helper as helper import bonsai.bim.module.drawing.helper as helper
camera = tool.Ifc.get_object(drawing) camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object) if camera.data.type != "ORTHO":
assert isinstance((camera_data := camera.data), bpy.types.Camera) return
props = tool.Drawing.get_camera_props(camera_data) settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, section)
m = ifcopenshell.util.shape.get_shape_matrix(shape)
if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix(m)):
return
if not cls.does_shape_intersect_camera(shape, camera):
return
bounds = helper.ortho_view_frame(camera_data) if camera_data.type == "ORTHO" else None # Get cutting plane as a line
reference_obj = tool.Ifc.get_object(reference_element) verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
assert isinstance(reference_obj, bpy.types.Object) cutting_plane_verts = sorted(verts, key=lambda v: v[2])[-4:]
v1, *_, v2 = sorted(cutting_plane_verts, key=lambda v: v[0]) # Cut is in +X direction
im = camera.matrix_world.inverted()
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in [v1, v2]]
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix: bounds = helper.ortho_view_frame(camera.data)
mat = reference_obj.matrix_world.copy() if not (points := helper.clip_segment(bounds, [v1, v2])):
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation return
xyz[2] = 0 for v in points:
xyz = camera.matrix_world @ xyz v.z = 0
mat.translation = xyz return points
annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
mat.translation += annotation_offset
return mat
def project_point_onto_camera(point: Vector, camera: bpy.types.Object) -> Vector:
projection = camera.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
return camera.matrix_world.inverted() @ mathutils.geometry.intersect_line_plane(
point.xyz, point.xyz - projection, camera.location, projection
)
obj_matrix = to_camera_coords(camera, reference_obj)
if props.raster_x > props.raster_y:
width = camera_data.ortho_scale
height = width / props.raster_x * props.raster_y
else:
height = camera_data.ortho_scale
width = height / props.raster_y * props.raster_x
projection = project_point_onto_camera(reference_obj.location, camera)
co1 = camera.matrix_world @ mathutils.Vector((width / 2, projection[1], -1))
co2 = camera.matrix_world @ mathutils.Vector((-(width / 2), projection[1], -1))
co1 = obj_matrix.inverted() @ co1
co2 = obj_matrix.inverted() @ co2
data = bpy.data.curves.new("Annotation", type="CURVE")
data.dimensions = "3D"
data.resolution_u = 2
polyline = data.splines.new("POLY")
polyline.points.add(1)
polyline.points[-2].co = list(co1) + [1]
polyline.points[-1].co = list(co2) + [1]
obj = bpy.data.objects.new(reference_obj.name, data)
obj.matrix_world = obj_matrix
element = cls.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type="SECTION_LEVEL",
should_add_representation=True,
context=context,
ifc_representation_class=None,
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
bpy.data.curves.remove(data)
return element
@classmethod @classmethod
def generate_section_reference_annotation( def generate_section_reference_annotation(
cls, cls,
drawing: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance, section: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
import bonsai.bim.module.drawing.helper as helper if not (points := cls.generate_section_reference_points(drawing, section)):
return
reference_obj = tool.Ifc.get_object(reference_element)
reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing) camera = tool.Ifc.get_object(drawing)
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new(section.Name or "Unnamed", mesh)
obj.matrix_world = cls.get_default_annotation_matrix(camera)
element = cls.run_root_assign_class(
obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION", should_add_representation=False
)
element.Name = section.Name or "Unnamed"
builder = ShapeBuilder(tool.Ifc.get())
representation = builder.get_representation(context, [builder.polyline(points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
return element
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix: @classmethod
mat = reference_obj.matrix_world.copy() def regenerate_section_reference_annotation(
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation cls,
xyz[2] = 0 drawing: ifcopenshell.entity_instance,
xyz = camera.matrix_world @ xyz annotation: ifcopenshell.entity_instance,
mat.translation = xyz section: ifcopenshell.entity_instance,
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05)) context: ifcopenshell.entity_instance,
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset ) -> ifcopenshell.entity_instance:
mat.translation += annotation_offset if not (points := cls.generate_section_reference_points(drawing, section)):
return mat return
def clip_to_camera_boundary( camera = tool.Ifc.get_object(drawing)
mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float] settings = ifcopenshell.geom.settings()
) -> Union[bpy.types.Mesh, None]: settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
mesh.verts.ensure_lookup_table() shape = ifcopenshell.geom.create_shape(settings, annotation)
points = [v.co for v in mesh.verts[0:2]] m = ifcopenshell.util.shape.get_shape_matrix(shape)
points = helper.clip_segment(bounds, points) mw = cls.get_default_annotation_matrix(camera)
if points is None: existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
return None
mesh.verts[0].co = points[0]
mesh.verts[1].co = points[1]
return mesh
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj): new_points = None
reference_mesh = cls.get_camera_block(reference_obj) if not np.allclose(m, np.array(mw), atol=1e-4):
obj_matrix = to_camera_coords(camera, reference_obj) new_points = points
elif len(existing_verts) != 2:
new_points = points
else:
if not tool.Cad.are_edges_collinear(existing_verts, points):
# Attempt to update the section line by projecting existing verts onto the new line
v1 = tool.Cad.point_on_edge(existing_verts[0], points)
v2 = tool.Cad.point_on_edge(existing_verts[1], points)
existing_length = (existing_verts[0] - existing_verts[1]).length
new_length = (v2 - v1).length
if abs((existing_length - new_length) / existing_length) <= 0.10:
# If the projected line is within 10% of the previous length ...
new_points = (v1, v2)
else:
new_points = points
# The reference mesh vertices represent a view cube. To convert if new_points:
# this into a section line we: if representation := ifcopenshell.util.representation.get_representation(annotation, context):
# 1. Select the 4 +Z vertices local to the reference element. This ifcopenshell.api.geometry.unassign_representation(
# is the cutting plane. tool.Ifc.get(), product=annotation, representation=representation
verts_local_to_reference = [reference_obj.matrix_world.inverted() @ v for v in reference_mesh["verts"]] )
cutting_plane_verts = sorted(verts_local_to_reference, key=lambda x: x.z)[-4:] builder = ShapeBuilder(tool.Ifc.get())
global_cutting_plane_verts = [reference_obj.matrix_world @ v for v in cutting_plane_verts] representation = builder.get_representation(context, [builder.polyline(new_points)])
# 2. Project the cutting plane onto our viewing camera. ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
verts_local_to_camera = [camera.matrix_world.inverted() @ v for v in global_cutting_plane_verts] if obj := tool.Ifc.get_object(annotation):
# 3. Collapse verts with the same XY coords, and set Z to be just obj.matrix_world = mw
# below the clip_start so it's visible bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
collapsed_verts = [] bonsai.core.geometry.switch_representation(
for vert in verts_local_to_camera: tool.Ifc, tool.Geometry, obj=obj, representation=representation
if not [True for v in collapsed_verts if (vert.xy - v.xy).length < 1e-2]: )
collapsed_verts.append(mathutils.Vector((vert.x, vert.y, -camera.data.clip_start - 0.05))) else:
# 4. The first two vertices is the section line ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
section_line = collapsed_verts[0:2] annotation.Name = section.Name or "Unnamed"
# 5. Sort the vertices in the +X direction so that the vertices are return annotation
# ordered to "point" in the direction of the section cut.
section_line = sorted(
section_line, key=lambda co: (reference_obj.matrix_world.inverted() @ camera.matrix_world @ co).x
)
global_section_line = [camera.matrix_world @ v for v in section_line]
local_section_line = [obj_matrix.inverted() @ v for v in global_section_line]
mesh = bpy.data.meshes.new(name="Annotation")
mesh.from_pydata(local_section_line, [(0, 1)], [])
bm = bmesh.new()
bm.from_mesh(mesh)
bm = clip_to_camera_boundary(bm, bounds)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(reference_obj.name, mesh)
obj.matrix_world = obj_matrix
element = cls.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type="SECTION",
should_add_representation=True,
context=context,
ifc_representation_class=None,
)
if representation := ifcopenshell.util.representation.get_representation(element, context):
cls.reload_representation(obj=obj, representation=representation)
return element
@classmethod @classmethod
def generate_elevation_reference_annotation( def generate_elevation_reference_annotation(
cls, cls,
drawing: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance, elevation: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
reference_obj = tool.Ifc.get_object(reference_element)
reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing) camera = tool.Ifc.get_object(drawing)
if camera.data.type != "ORTHO":
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix: return
mat = reference_obj.matrix_world.copy() settings = ifcopenshell.geom.settings()
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation shape = ifcopenshell.geom.create_shape(settings, elevation)
xyz[2] = 0 m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
xyz = camera.matrix_world @ xyz if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
mat.translation = xyz obj = bpy.data.objects.new(elevation.Name or "Unnamed", None)
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
mat.translation += annotation_offset
return mat
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
obj = bpy.data.objects.new(reference_obj.name, None)
obj.empty_display_size = 0.1 obj.empty_display_size = 0.1
obj.matrix_world = to_camera_coords(camera, reference_obj) obj.matrix_world = cls.get_default_annotation_matrix(camera, matrix_world=m)
element = cls.run_root_assign_class( element = cls.run_root_assign_class(
obj=obj, obj=obj, ifc_class="IfcAnnotation", predefined_type="ELEVATION", should_add_representation=False
ifc_class="IfcAnnotation",
predefined_type="ELEVATION",
should_add_representation=False,
context=context,
ifc_representation_class=None,
) )
if representation := ifcopenshell.util.representation.get_representation(element, context): element.Name = elevation.Name or "Unnamed"
cls.reload_representation(obj=obj, representation=representation)
return element return element
@classmethod @classmethod
def generate_grid_axis_reference_annotation( def regenerate_elevation_reference_annotation(
cls, cls,
drawing: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance,
axis: ifcopenshell.entity_instance, annotation: ifcopenshell.entity_instance,
elevation: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]: ) -> ifcopenshell.entity_instance:
camera = tool.Ifc.get_object(drawing)
if camera.data.type != "ORTHO":
return
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, elevation)
m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
existing_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(annotation.ObjectPlacement))
# The user is allowed to shift the elevation, but not rotate it
if not np.allclose(np.array(m.to_3x3()), np.array(existing_matrix.to_3x3()), atol=1e-4):
mw = cls.get_default_annotation_matrix(camera, matrix_world=m)
if obj := tool.Ifc.get_object(annotation):
obj.matrix_world = mw
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
else:
ifcopenshell.api.geometry.edit_object_placement(
tool.Ifc.get(), product=annotation, matrix=np.array(mw)
)
annotation.Name = elevation.Name or "Unnamed"
return annotation
@classmethod
def generate_grid_axis_reference_points(
cls, drawing: ifcopenshell.entity_instance, axis: ifcopenshell.entity_instance
) -> list | None:
import bonsai.bim.module.drawing.helper as helper import bonsai.bim.module.drawing.helper as helper
camera = tool.Ifc.get_object(drawing) camera = tool.Ifc.get_object(drawing)
@@ -1803,13 +1870,24 @@ class Drawing(bonsai.core.tool.Drawing):
return return
else: else:
return return
for v in points:
v.z = 0
return points
@classmethod
def generate_grid_axis_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
axis: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
return
camera = tool.Ifc.get_object(drawing)
mesh = bpy.data.meshes.new("Mesh") mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new(axis.AxisTag or "-", mesh) obj = bpy.data.objects.new(axis.AxisTag or "-", mesh)
obj.matrix_world = camera.matrix_world obj.matrix_world = cls.get_default_annotation_matrix(camera)
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
obj.matrix_world.translation += annotation_offset
element = cls.run_root_assign_class( element = cls.run_root_assign_class(
obj=obj, ifc_class="IfcAnnotation", predefined_type="GRID", should_add_representation=False obj=obj, ifc_class="IfcAnnotation", predefined_type="GRID", should_add_representation=False
) )
@@ -1820,6 +1898,71 @@ class Drawing(bonsai.core.tool.Drawing):
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation) bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
return element return element
@classmethod
def regenerate_grid_axis_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
annotation: ifcopenshell.entity_instance,
axis: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
return
camera = tool.Ifc.get_object(drawing)
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, annotation)
m = ifcopenshell.util.shape.get_shape_matrix(shape)
mw = cls.get_default_annotation_matrix(camera)
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
new_points = None
if not np.allclose(m, np.array(mw), atol=1e-4):
new_points = points
elif len(existing_verts) != 2:
new_points = points
else:
if not tool.Cad.are_edges_collinear(existing_verts, points):
# Attempt to update the section line by projecting existing verts onto the new line
v1 = tool.Cad.point_on_edge(existing_verts[0], points)
v2 = tool.Cad.point_on_edge(existing_verts[1], points)
existing_length = (existing_verts[0] - existing_verts[1]).length
new_length = (v2 - v1).length
if abs((existing_length - new_length) / existing_length) <= 0.10:
# If the projected line is within 10% of the previous length ...
new_points = (v1, v2)
else:
new_points = points
if new_points:
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
ifcopenshell.api.geometry.unassign_representation(
tool.Ifc.get(), product=annotation, representation=representation
)
builder = ShapeBuilder(tool.Ifc.get())
representation = builder.get_representation(context, [builder.polyline(new_points)])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
if obj := tool.Ifc.get_object(annotation):
obj.matrix_world = mw
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Geometry, obj=obj, representation=representation
)
else:
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
annotation.Name = axis.AxisTag or "-"
return annotation
@classmethod
def get_default_annotation_matrix(cls, camera, matrix_world=None):
if matrix_world is None:
matrix_world = camera.matrix_world.copy()
annotation_offset = Vector((0, 0, -camera.data.clip_start - 0.05))
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
matrix_world.translation += annotation_offset
return matrix_world
@classmethod @classmethod
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool: def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))] axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
@@ -1830,6 +1973,16 @@ class Drawing(bonsai.core.tool.Drawing):
return True return True
return False return False
@classmethod
def is_matrix_perpendicular(cls, a: Matrix, b: Matrix) -> bool:
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
a_quaternion = a.to_quaternion()
b_quaternion = b.to_quaternion()
for axis in axes:
if abs((a_quaternion @ axis).angle(b_quaternion @ axis) - (math.pi / 2)) < 1e-5:
return True
return False
@classmethod @classmethod
def get_camera_block(cls, obj: bpy.types.Object) -> dict: def get_camera_block(cls, obj: bpy.types.Object) -> dict:
assert isinstance(camera := obj.data, bpy.types.Camera) assert isinstance(camera := obj.data, bpy.types.Camera)
@@ -1873,6 +2026,16 @@ class Drawing(bonsai.core.tool.Drawing):
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"]) b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"])
return bool(a_tree.overlap(b_tree)) return bool(a_tree.overlap(b_tree))
@classmethod
def does_shape_intersect_camera(cls, shape, camera) -> bool:
a_block = cls.get_camera_block(camera)
a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
m = ifcopenshell.util.shape.get_shape_matrix(shape)
verts = [(m @ np.append(v, 1.0))[:3] for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(verts, faces)
return bool(a_tree.overlap(b_tree))
@classmethod @classmethod
def replace_text_literal_variables( def replace_text_literal_variables(
cls, cls,