Skip to content

Commit d4388ec

Browse files
falken10vdlMoult
andauthored
AddReferenceImage: fix regression with IFC2X3 support, refactor to no longer depend on add_representation or update_representation, remove legacy style updating functionality
* Enhance AddReferenceImage operator to use file browser instead of independent popup dialogue * Fix dimensions assertion in TestAddReferenceImage * Remove error in return in _execute (it is not execute) * Add IFC2X3 support to AddReferenceImage * Adde unit="LENGTH" to the x/y properties (every length dimension everywhere in the UI is in project length units. No need to say it explicitly) * Manually create the texture always, not just for IFC2X3 * Add poll method to AddReferenceImage operator to check for loaded IFC project * Refactor AddReferenceImage to add representation manually following pattern in root/operator.py's bim.add_element * Improve File explorer options between new and select from existing project Ifc Reference Images * Refactor get_existing_reference_images to use selector for filtering image annotations * No extra args needed after should_add_representation is False * Doing clean=True deletes everything * Don't manually add geometry and materials, don't call bpy.ops. Only create IFC data, then use preexisting loading functions to create geometry. * Black formatting, also now we can start to remove this operator as it becomes obsolete * Consolidate duplicate UV generation into Loader.load_generated_uv_map Replace 3 identical XY-UV baking blocks (create_object IMAGE, bm_add_image_plane, ImageScalingTool) with a single reusable classmethod in tool.Loader. * Fix IFC4 texture display in Solid viewport Texture mode IFC4 IfcTextureCoordinateGenerator Mode=COORD is used, load_texture_maps falls back to load_generated_uv_map to bake XY-UV data onto the mesh. * Fix IFC2X3 texture display * This looks wrong * Remove legacy override image feature, because we now have a proper styles and texture manager * Remove legacy override existing image element, because we now have a dedicated styles texture manager * Remove unnecessary roundtrip to bmesh and mesh --------- Co-authored-by: Dion Moult <dion@thinkmoult.com>
1 parent 7141f2c commit d4388ec

6 files changed

Lines changed: 152 additions & 193 deletions

File tree

src/bonsai/bonsai/bim/import_ifc.py

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def create(
8585
if element.is_a("IfcTypeProduct"):
8686
self.parse_element_type_material_styles(element)
8787
self.parsed_meshes.add(self.mesh.name)
88-
if not self.ifc_import_settings.load_indexed_maps:
88+
if self.ifc_import_settings.load_indexed_maps:
8989
self.load_texture_maps(shape_has_openings)
9090
self.assign_material_slots_to_faces()
9191
tool.Geometry.record_object_materials(obj)
@@ -117,7 +117,6 @@ def get_ifc_coordinate(self, material: bpy.types.Material) -> Union[ifcopenshell
117117
for texture in texture_style.Textures or []:
118118
if coords := getattr(texture, "IsMappedBy", None):
119119
coords = coords[0]
120-
# IfcTextureCoordinateGenerator handled in the style shader graph
121120
if coords.is_a("IfcIndexedTextureMap"):
122121
return coords
123122
# TODO: support IfcTextureMap
@@ -135,6 +134,10 @@ def load_texture_maps(self, shape_has_openings: bool) -> None:
135134
if shape_has_openings and coords.is_a("IfcIndexedTextureMap"):
136135
continue
137136
tool.Loader.load_indexed_map(coords, self.mesh)
137+
elif tool.Style.get_texture_style(material):
138+
# No explicit coordinate mapping (e.g. IFC2X3 has no IsMappedBy,
139+
# and IFC4 COORD uses generated UVs). Bake XY→UV as fallback.
140+
tool.Loader.load_generated_uv_map(self.mesh)
138141

139142
def assign_material_slots_to_faces(self) -> None:
140143
if not self.mesh["ios_materials"]:
@@ -892,48 +895,6 @@ def create_product(
892895
obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element))
893896
)
894897

895-
if element.is_a("IfcAnnotation") and getattr(element, "ObjectType", None) == "IMAGE":
896-
image = None
897-
if obj.data and obj.data.materials and obj.data.materials[0]:
898-
material = obj.data.materials[0]
899-
if material.use_nodes and material.node_tree:
900-
for node in material.node_tree.nodes:
901-
if node.type == "TEX_IMAGE" and node.image:
902-
image = node.image
903-
break
904-
if image:
905-
import bmesh
906-
907-
bm = bmesh.new()
908-
bm.from_mesh(obj.data)
909-
if not bm.loops.layers.uv:
910-
uv_layer = bm.loops.layers.uv.new()
911-
else:
912-
uv_layer = bm.loops.layers.uv.active
913-
914-
if bm.verts:
915-
min_x = min(v.co.x for v in bm.verts)
916-
max_x = max(v.co.x for v in bm.verts)
917-
min_y = min(v.co.y for v in bm.verts)
918-
max_y = max(v.co.y for v in bm.verts)
919-
920-
width = max_x - min_x
921-
height = max_y - min_y
922-
923-
for face in bm.faces:
924-
for loop in face.loops:
925-
vert = loop.vert
926-
u = (vert.co.x - min_x) / width if width > 0 else 0.5
927-
v = (vert.co.y - min_y) / height if height > 0 else 0.5
928-
929-
u = max(0.0, min(1.0, u))
930-
v = max(0.0, min(1.0, v))
931-
932-
loop[uv_layer].uv = (u, v)
933-
934-
bm.to_mesh(obj.data)
935-
bm.free()
936-
obj.data.update()
937898
return obj
938899

939900
def load_existing_meshes(self) -> None:

src/bonsai/bonsai/bim/module/drawing/operator.py

Lines changed: 107 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import bmesh
4242
import bpy
43+
import logging
4344
import ifcopenshell
4445
import ifcopenshell.api
4546
import ifcopenshell.api.document
@@ -50,6 +51,7 @@
5051
import ifcopenshell.util.element
5152
import ifcopenshell.util.representation
5253
import ifcopenshell.util.selector
54+
import ifcopenshell.util.shape_builder
5355
import ifcopenshell.util.unit
5456
import numpy as np
5557
import shapely
@@ -59,6 +61,7 @@
5961
from lxml import etree
6062
from mathutils import Color, Matrix, Vector
6163

64+
import bonsai.bim.import_ifc
6265
import bonsai.bim.export_ifc
6366
import bonsai.bim.handler
6467
import bonsai.bim.helper
@@ -138,7 +141,7 @@ def _execute(self, context):
138141
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
139142

140143
if props.create_representation_for_type and object_type == "IMAGE":
141-
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
144+
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name)
142145

143146

144147
class EnableAddAnnotationType(bpy.types.Operator):
@@ -1759,7 +1762,7 @@ def _execute(self, context):
17591762
enable_editing=True,
17601763
)
17611764
if props.object_type == "IMAGE":
1762-
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
1765+
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name)
17631766

17641767

17651768
class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -3802,27 +3805,70 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
38023805
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
38033806
filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
38043807
filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
3805-
3806-
override_existing_image: bpy.props.BoolProperty(
3807-
name="Override Existing Image",
3808-
default=True,
3809-
description=(
3810-
"Override image if it was previously loaded to Blender. If disabled, will always create a new image"
3811-
),
3808+
x_length: bpy.props.FloatProperty(
3809+
name="X Length",
3810+
description="Width of the reference image",
3811+
default=1.0,
3812+
min=0.001,
3813+
soft_min=0.01,
3814+
precision=3,
3815+
unit="LENGTH",
38123816
)
3813-
use_existing_object_by_name: bpy.props.StringProperty(
3814-
name="Use Existing Object By Name",
3815-
description="Existing object name to add a style with reference image to. If not provided will create a new object.",
3816-
options={"SKIP_SAVE"},
3817+
y_length: bpy.props.FloatProperty(
3818+
name="Y Length",
3819+
description="Height of the reference image",
3820+
default=1.0,
3821+
min=0.001,
3822+
soft_min=0.01,
3823+
precision=3,
3824+
unit="LENGTH",
38173825
)
3818-
size: bpy.props.FloatProperty(name="Size", description="Size of the reference image", default=1.0, unit="LENGTH")
3826+
3827+
@classmethod
3828+
def poll(cls, context):
3829+
if not tool.Ifc.get():
3830+
cls.poll_message_set("No IFC project is loaded.")
3831+
return False
3832+
return True
3833+
3834+
def invoke(self, context, event):
3835+
self._last_filepath = ""
3836+
return super().invoke(context, event)
3837+
3838+
def check(self, context):
3839+
if not hasattr(self, "_last_filepath"):
3840+
self._last_filepath = ""
3841+
3842+
if self.filepath and self.filepath != self._last_filepath:
3843+
self._last_filepath = self.filepath
3844+
3845+
abs_path = Path(self.filepath).absolute().resolve()
3846+
if abs_path.exists() and abs_path.is_file():
3847+
image = load_image(abs_path.name, str(abs_path.parent), check_existing=False)
3848+
image_width_px = image.size[0]
3849+
image_height_px = image.size[1]
3850+
aspect_ratio = image_width_px / image_height_px
3851+
3852+
if aspect_ratio >= 1.0:
3853+
self.x_length = 1.0
3854+
self.y_length = 1.0 / aspect_ratio
3855+
else:
3856+
self.x_length = aspect_ratio
3857+
self.y_length = 1.0
3858+
3859+
bpy.data.images.remove(image)
3860+
return True
3861+
3862+
return False
38193863

38203864
def draw(self, context):
3865+
layout = self.layout
38213866
if Path(tool.Ifc.get_path()).is_file():
3822-
self.layout.prop(self, "use_relative_path")
3823-
self.layout.prop(self, "override_existing_image")
3824-
self.layout.prop(self, "use_existing_object_by_name")
3825-
self.layout.prop(self, "size")
3867+
layout.prop(self, "use_relative_path")
3868+
else:
3869+
self.use_relative_path = False
3870+
layout.prop(self, "x_length")
3871+
layout.prop(self, "y_length")
38263872

38273873
def _execute(self, context):
38283874
space = tool.Blender.get_view3d_space()
@@ -3837,127 +3883,66 @@ def _execute(self, context):
38373883
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
38383884
ifc_file = tool.Ifc.get()
38393885

3840-
if self.override_existing_image:
3841-
params = {"check_existing": True, "force_reload": True}
3842-
else:
3843-
params = {"check_existing": False}
3886+
params = {"check_existing": False}
38443887
image = load_image(abs_path.name, str(abs_path.parent), **params)
38453888

3846-
aspect_ratio = image.size[0] / image.size[1]
3847-
if aspect_ratio >= 1.0: # Landscape
3848-
x_length = self.size
3849-
y_length = self.size / aspect_ratio
3850-
else:
3851-
x_length = self.size / aspect_ratio
3852-
y_length = self.size
3853-
3854-
def bm_add_image_plane(mesh):
3855-
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True)
3856-
3857-
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
3858-
plane_scale = Vector((x_length / 2.0, y_length / 2.0, 1.0))
3859-
matrix = Matrix.LocRotScale(None, None, plane_scale)
3860-
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False)
3861-
3862-
if not bm.loops.layers.uv:
3863-
uv_layer = bm.loops.layers.uv.new()
3864-
else:
3865-
uv_layer = bm.loops.layers.uv.active
3866-
3867-
min_x = min(v.co.x for v in bm.verts)
3868-
max_x = max(v.co.x for v in bm.verts)
3869-
min_y = min(v.co.y for v in bm.verts)
3870-
max_y = max(v.co.y for v in bm.verts)
3871-
3872-
width = max_x - min_x
3873-
height = max_y - min_y
3874-
3875-
for face in bm.faces:
3876-
for loop in face.loops:
3877-
vert = loop.vert
3878-
u = (vert.co.x - min_x) / width if width > 0 else 0.5
3879-
v = (vert.co.y - min_y) / height if height > 0 else 0.5
3880-
3881-
u = max(0.0, min(1.0, u))
3882-
v = max(0.0, min(1.0, v))
3883-
loop[uv_layer].uv = (u, v)
3889+
mesh = bpy.data.meshes.new(image_filepath.stem)
3890+
obj = bpy.data.objects.new(image_filepath.stem, mesh)
3891+
element = tool.Drawing.run_root_assign_class(
3892+
obj=obj, ifc_class="IfcAnnotation", predefined_type="IMAGE", should_add_representation=False
3893+
)
38843894

3885-
tool.Blender.apply_bmesh(mesh, bm)
3895+
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
3896+
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
3897+
hx = self.x_length * 0.5 / unit_scale
3898+
hy = self.y_length * 0.5 / unit_scale
3899+
verts = [(-hx, -hy, 0.0), ( hx, -hy, 0.0), ( hx, hy, 0.0), (-hx, hy, 0.0)]
3900+
item = builder.mesh(verts, [[0, 1, 2, 3]])
38863901

3887-
if self.use_existing_object_by_name:
3888-
obj = bpy.data.objects[self.use_existing_object_by_name]
3889-
bm_add_image_plane(obj.data)
3890-
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
3891-
else:
3892-
temp_mesh = bpy.data.meshes.new("temp_mesh")
3893-
bm_add_image_plane(temp_mesh)
3894-
obj = bpy.data.objects.new(image_filepath.stem, temp_mesh)
3895-
tool.Drawing.run_root_assign_class(
3896-
obj=obj,
3897-
ifc_class="IfcAnnotation",
3898-
predefined_type="IMAGE",
3899-
should_add_representation=True,
3900-
context=ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW"),
3901-
ifc_representation_class=None,
3902-
)
3903-
tool.Blender.remove_data_block(temp_mesh)
3902+
ifc_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
3903+
representation = builder.get_representation(ifc_context, [item])
3904+
ifcopenshell.api.geometry.assign_representation(ifc_file, element, representation)
39043905

3905-
element = tool.Ifc.get_entity(obj)
3906-
if element and isinstance(obj.data, bpy.types.Mesh):
3907-
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
3908-
if representation and representation.Items:
3909-
item_id = representation.Items[0].id()
3910-
num_faces = len(obj.data.polygons)
3911-
obj.data["ios_item_ids"] = [item_id] * num_faces
3912-
tool.Blender.Attribute.fill_attribute(obj.data, "ios_item_ids", "FACE", "INT", [item_id] * num_faces)
3913-
3914-
for item in representation.Items:
3915-
if item.is_a("IfcPolygonalFaceSet") and item.Coordinates:
3916-
new_coords = []
3917-
for vertex in obj.data.vertices:
3918-
co = obj.matrix_world @ vertex.co
3919-
new_coords.append([co.x, co.y, co.z])
3920-
item.Coordinates.CoordList = new_coords
3921-
3922-
tool.Blender.set_active_object(obj)
3923-
3924-
material = bpy.data.materials.new(name=image_filepath.stem)
3925-
obj.data.materials.append(None) # new slot
3926-
obj.material_slots[0].material = material
3927-
bpy.ops.bim.add_style()
3928-
3929-
style = tool.Ifc.get_entity(material)
3930-
assert style
3931-
tool.Style.assign_style_to_object(style, obj)
3906+
style = ifcopenshell.api.style.add_style(tool.Ifc.get(), name=image_filepath.stem)
3907+
ifcopenshell.api.style.assign_representation_styles(
3908+
ifc_file, shape_representation=representation, styles=[style]
3909+
)
39323910

39333911
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
39343912
# we don't support IfcSurfaceStyleWithTextures without Rendering yet
39353913
shading_attributes = {
3936-
"SurfaceColour": {
3937-
"Red": 1.0,
3938-
"Green": 1.0,
3939-
"Blue": 1.0,
3940-
},
3914+
"SurfaceColour": {"Red": 1.0, "Green": 1.0, "Blue": 1.0},
39413915
"Transparency": 0.0,
39423916
"ReflectanceMethod": "NOTDEFINED",
39433917
}
39443918
ifcopenshell.api.style.add_surface_style(
3945-
tool.Ifc.get(),
3946-
style=style,
3947-
ifc_class="IfcSurfaceStyleRendering",
3948-
attributes=shading_attributes,
3919+
tool.Ifc.get(), style=style, ifc_class="IfcSurfaceStyleRendering", attributes=shading_attributes
39493920
)
3950-
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
3921+
3922+
if tool.Ifc.get_schema() == "IFC2X3":
3923+
texture = ifc_file.create_entity(
3924+
"IfcImageTexture",
3925+
RepeatS=True,
3926+
RepeatT=True,
3927+
TextureType="TEXTURE",
3928+
UrlReference=image_filepath.as_posix(),
3929+
)
3930+
else:
3931+
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
3932+
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD")
3933+
39513934
textures = [texture]
3952-
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map
39533935
ifcopenshell.api.style.add_surface_style(
3954-
ifc_file,
3955-
style=style,
3956-
ifc_class="IfcSurfaceStyleWithTextures",
3957-
attributes={"Textures": textures},
3936+
ifc_file, style=style, ifc_class="IfcSurfaceStyleWithTextures", attributes={"Textures": textures}
39583937
)
3959-
tool.Style.reload_material_from_ifc(material)
3960-
tool.Geometry.record_object_materials(obj)
3938+
3939+
logger = logging.getLogger("ImportIFC")
3940+
ifc_import_settings = bonsai.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
3941+
ifc_importer = bonsai.bim.import_ifc.IfcImporter(ifc_import_settings)
3942+
ifc_importer.file = tool.Ifc.get()
3943+
ifc_importer.create_style(style)
3944+
3945+
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
39613946

39623947

39633948
class ConvertSVGToDXF(bpy.types.Operator):

0 commit comments

Comments
 (0)