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>
This commit is contained in:
falken10vdl
2026-02-19 01:56:10 +01:00
committed by GitHub
parent 7141f2cf90
commit d4388ec76d
6 changed files with 153 additions and 194 deletions
+5 -44
View File
@@ -85,7 +85,7 @@ class MaterialCreator:
if element.is_a("IfcTypeProduct"): if element.is_a("IfcTypeProduct"):
self.parse_element_type_material_styles(element) self.parse_element_type_material_styles(element)
self.parsed_meshes.add(self.mesh.name) self.parsed_meshes.add(self.mesh.name)
if not self.ifc_import_settings.load_indexed_maps: if self.ifc_import_settings.load_indexed_maps:
self.load_texture_maps(shape_has_openings) self.load_texture_maps(shape_has_openings)
self.assign_material_slots_to_faces() self.assign_material_slots_to_faces()
tool.Geometry.record_object_materials(obj) tool.Geometry.record_object_materials(obj)
@@ -117,7 +117,6 @@ class MaterialCreator:
for texture in texture_style.Textures or []: for texture in texture_style.Textures or []:
if coords := getattr(texture, "IsMappedBy", None): if coords := getattr(texture, "IsMappedBy", None):
coords = coords[0] coords = coords[0]
# IfcTextureCoordinateGenerator handled in the style shader graph
if coords.is_a("IfcIndexedTextureMap"): if coords.is_a("IfcIndexedTextureMap"):
return coords return coords
# TODO: support IfcTextureMap # TODO: support IfcTextureMap
@@ -135,6 +134,10 @@ class MaterialCreator:
if shape_has_openings and coords.is_a("IfcIndexedTextureMap"): if shape_has_openings and coords.is_a("IfcIndexedTextureMap"):
continue continue
tool.Loader.load_indexed_map(coords, self.mesh) tool.Loader.load_indexed_map(coords, self.mesh)
elif tool.Style.get_texture_style(material):
# No explicit coordinate mapping (e.g. IFC2X3 has no IsMappedBy,
# and IFC4 COORD uses generated UVs). Bake XY→UV as fallback.
tool.Loader.load_generated_uv_map(self.mesh)
def assign_material_slots_to_faces(self) -> None: def assign_material_slots_to_faces(self) -> None:
if not self.mesh["ios_materials"]: if not self.mesh["ios_materials"]:
@@ -892,48 +895,6 @@ class IfcImporter:
obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)) obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element))
) )
if element.is_a("IfcAnnotation") and getattr(element, "ObjectType", None) == "IMAGE":
image = None
if obj.data and obj.data.materials and obj.data.materials[0]:
material = obj.data.materials[0]
if material.use_nodes and material.node_tree:
for node in material.node_tree.nodes:
if node.type == "TEX_IMAGE" and node.image:
image = node.image
break
if image:
import bmesh
bm = bmesh.new()
bm.from_mesh(obj.data)
if not bm.loops.layers.uv:
uv_layer = bm.loops.layers.uv.new()
else:
uv_layer = bm.loops.layers.uv.active
if bm.verts:
min_x = min(v.co.x for v in bm.verts)
max_x = max(v.co.x for v in bm.verts)
min_y = min(v.co.y for v in bm.verts)
max_y = max(v.co.y for v in bm.verts)
width = max_x - min_x
height = max_y - min_y
for face in bm.faces:
for loop in face.loops:
vert = loop.vert
u = (vert.co.x - min_x) / width if width > 0 else 0.5
v = (vert.co.y - min_y) / height if height > 0 else 0.5
u = max(0.0, min(1.0, u))
v = max(0.0, min(1.0, v))
loop[uv_layer].uv = (u, v)
bm.to_mesh(obj.data)
bm.free()
obj.data.update()
return obj return obj
def load_existing_meshes(self) -> None: def load_existing_meshes(self) -> None:
+108 -123
View File
@@ -40,6 +40,7 @@ from typing import (
import bmesh import bmesh
import bpy import bpy
import logging
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.api.document import ifcopenshell.api.document
@@ -50,6 +51,7 @@ import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.representation import ifcopenshell.util.representation
import ifcopenshell.util.selector import ifcopenshell.util.selector
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit import ifcopenshell.util.unit
import numpy as np import numpy as np
import shapely import shapely
@@ -59,6 +61,7 @@ from bpy_extras.io_utils import ImportHelper
from lxml import etree from lxml import etree
from mathutils import Color, Matrix, Vector from mathutils import Color, Matrix, Vector
import bonsai.bim.import_ifc
import bonsai.bim.export_ifc import bonsai.bim.export_ifc
import bonsai.bim.handler import bonsai.bim.handler
import bonsai.bim.helper import bonsai.bim.helper
@@ -138,7 +141,7 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}" element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
if props.create_representation_for_type and object_type == "IMAGE": if props.create_representation_for_type and object_type == "IMAGE":
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name) bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name)
class EnableAddAnnotationType(bpy.types.Operator): class EnableAddAnnotationType(bpy.types.Operator):
@@ -1759,7 +1762,7 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
enable_editing=True, enable_editing=True,
) )
if props.object_type == "IMAGE": if props.object_type == "IMAGE":
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name) bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name)
class AddSheet(bpy.types.Operator, tool.Ifc.Operator): class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -3802,27 +3805,70 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
x_length: bpy.props.FloatProperty(
name="X Length",
description="Width of the reference image",
default=1.0,
min=0.001,
soft_min=0.01,
precision=3,
unit="LENGTH",
)
y_length: bpy.props.FloatProperty(
name="Y Length",
description="Height of the reference image",
default=1.0,
min=0.001,
soft_min=0.01,
precision=3,
unit="LENGTH",
)
override_existing_image: bpy.props.BoolProperty( @classmethod
name="Override Existing Image", def poll(cls, context):
default=True, if not tool.Ifc.get():
description=( cls.poll_message_set("No IFC project is loaded.")
"Override image if it was previously loaded to Blender. If disabled, will always create a new image" return False
), return True
)
use_existing_object_by_name: bpy.props.StringProperty( def invoke(self, context, event):
name="Use Existing Object By Name", self._last_filepath = ""
description="Existing object name to add a style with reference image to. If not provided will create a new object.", return super().invoke(context, event)
options={"SKIP_SAVE"},
) def check(self, context):
size: bpy.props.FloatProperty(name="Size", description="Size of the reference image", default=1.0, unit="LENGTH") if not hasattr(self, "_last_filepath"):
self._last_filepath = ""
if self.filepath and self.filepath != self._last_filepath:
self._last_filepath = self.filepath
abs_path = Path(self.filepath).absolute().resolve()
if abs_path.exists() and abs_path.is_file():
image = load_image(abs_path.name, str(abs_path.parent), check_existing=False)
image_width_px = image.size[0]
image_height_px = image.size[1]
aspect_ratio = image_width_px / image_height_px
if aspect_ratio >= 1.0:
self.x_length = 1.0
self.y_length = 1.0 / aspect_ratio
else:
self.x_length = aspect_ratio
self.y_length = 1.0
bpy.data.images.remove(image)
return True
return False
def draw(self, context): def draw(self, context):
layout = self.layout
if Path(tool.Ifc.get_path()).is_file(): if Path(tool.Ifc.get_path()).is_file():
self.layout.prop(self, "use_relative_path") layout.prop(self, "use_relative_path")
self.layout.prop(self, "override_existing_image") else:
self.layout.prop(self, "use_existing_object_by_name") self.use_relative_path = False
self.layout.prop(self, "size") layout.prop(self, "x_length")
layout.prop(self, "y_length")
def _execute(self, context): def _execute(self, context):
space = tool.Blender.get_view3d_space() space = tool.Blender.get_view3d_space()
@@ -3837,127 +3883,66 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)) image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
if self.override_existing_image: params = {"check_existing": False}
params = {"check_existing": True, "force_reload": True}
else:
params = {"check_existing": False}
image = load_image(abs_path.name, str(abs_path.parent), **params) image = load_image(abs_path.name, str(abs_path.parent), **params)
aspect_ratio = image.size[0] / image.size[1] mesh = bpy.data.meshes.new(image_filepath.stem)
if aspect_ratio >= 1.0: # Landscape obj = bpy.data.objects.new(image_filepath.stem, mesh)
x_length = self.size element = tool.Drawing.run_root_assign_class(
y_length = self.size / aspect_ratio obj=obj, ifc_class="IfcAnnotation", predefined_type="IMAGE", should_add_representation=False
else: )
x_length = self.size / aspect_ratio
y_length = self.size
def bm_add_image_plane(mesh): builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
hx = self.x_length * 0.5 / unit_scale
hy = self.y_length * 0.5 / unit_scale
verts = [(-hx, -hy, 0.0), ( hx, -hy, 0.0), ( hx, hy, 0.0), (-hx, hy, 0.0)]
item = builder.mesh(verts, [[0, 1, 2, 3]])
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) ifc_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
plane_scale = Vector((x_length / 2.0, y_length / 2.0, 1.0)) representation = builder.get_representation(ifc_context, [item])
matrix = Matrix.LocRotScale(None, None, plane_scale) ifcopenshell.api.geometry.assign_representation(ifc_file, element, representation)
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False)
if not bm.loops.layers.uv: style = ifcopenshell.api.style.add_style(tool.Ifc.get(), name=image_filepath.stem)
uv_layer = bm.loops.layers.uv.new() ifcopenshell.api.style.assign_representation_styles(
else: ifc_file, shape_representation=representation, styles=[style]
uv_layer = bm.loops.layers.uv.active )
min_x = min(v.co.x for v in bm.verts)
max_x = max(v.co.x for v in bm.verts)
min_y = min(v.co.y for v in bm.verts)
max_y = max(v.co.y for v in bm.verts)
width = max_x - min_x
height = max_y - min_y
for face in bm.faces:
for loop in face.loops:
vert = loop.vert
u = (vert.co.x - min_x) / width if width > 0 else 0.5
v = (vert.co.y - min_y) / height if height > 0 else 0.5
u = max(0.0, min(1.0, u))
v = max(0.0, min(1.0, v))
loop[uv_layer].uv = (u, v)
tool.Blender.apply_bmesh(mesh, bm)
if self.use_existing_object_by_name:
obj = bpy.data.objects[self.use_existing_object_by_name]
bm_add_image_plane(obj.data)
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
else:
temp_mesh = bpy.data.meshes.new("temp_mesh")
bm_add_image_plane(temp_mesh)
obj = bpy.data.objects.new(image_filepath.stem, temp_mesh)
tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type="IMAGE",
should_add_representation=True,
context=ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW"),
ifc_representation_class=None,
)
tool.Blender.remove_data_block(temp_mesh)
element = tool.Ifc.get_entity(obj)
if element and isinstance(obj.data, bpy.types.Mesh):
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if representation and representation.Items:
item_id = representation.Items[0].id()
num_faces = len(obj.data.polygons)
obj.data["ios_item_ids"] = [item_id] * num_faces
tool.Blender.Attribute.fill_attribute(obj.data, "ios_item_ids", "FACE", "INT", [item_id] * num_faces)
for item in representation.Items:
if item.is_a("IfcPolygonalFaceSet") and item.Coordinates:
new_coords = []
for vertex in obj.data.vertices:
co = obj.matrix_world @ vertex.co
new_coords.append([co.x, co.y, co.z])
item.Coordinates.CoordList = new_coords
tool.Blender.set_active_object(obj)
material = bpy.data.materials.new(name=image_filepath.stem)
obj.data.materials.append(None) # new slot
obj.material_slots[0].material = material
bpy.ops.bim.add_style()
style = tool.Ifc.get_entity(material)
assert style
tool.Style.assign_style_to_object(style, obj)
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because # TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
# we don't support IfcSurfaceStyleWithTextures without Rendering yet # we don't support IfcSurfaceStyleWithTextures without Rendering yet
shading_attributes = { shading_attributes = {
"SurfaceColour": { "SurfaceColour": {"Red": 1.0, "Green": 1.0, "Blue": 1.0},
"Red": 1.0,
"Green": 1.0,
"Blue": 1.0,
},
"Transparency": 0.0, "Transparency": 0.0,
"ReflectanceMethod": "NOTDEFINED", "ReflectanceMethod": "NOTDEFINED",
} }
ifcopenshell.api.style.add_surface_style( ifcopenshell.api.style.add_surface_style(
tool.Ifc.get(), tool.Ifc.get(), style=style, ifc_class="IfcSurfaceStyleRendering", attributes=shading_attributes
style=style,
ifc_class="IfcSurfaceStyleRendering",
attributes=shading_attributes,
) )
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
if tool.Ifc.get_schema() == "IFC2X3":
texture = ifc_file.create_entity(
"IfcImageTexture",
RepeatS=True,
RepeatT=True,
TextureType="TEXTURE",
UrlReference=image_filepath.as_posix(),
)
else:
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD")
textures = [texture] textures = [texture]
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map
ifcopenshell.api.style.add_surface_style( ifcopenshell.api.style.add_surface_style(
ifc_file, ifc_file, style=style, ifc_class="IfcSurfaceStyleWithTextures", attributes={"Textures": textures}
style=style,
ifc_class="IfcSurfaceStyleWithTextures",
attributes={"Textures": textures},
) )
tool.Style.reload_material_from_ifc(material)
tool.Geometry.record_object_materials(obj) logger = logging.getLogger("ImportIFC")
ifc_import_settings = bonsai.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
ifc_importer = bonsai.bim.import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = tool.Ifc.get()
ifc_importer.create_style(style)
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
class ConvertSVGToDXF(bpy.types.Operator): class ConvertSVGToDXF(bpy.types.Operator):
@@ -3248,29 +3248,9 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
bmesh.ops.scale(bm, vec=(scale_factor, scale_factor, 1.0), verts=bm.verts) bmesh.ops.scale(bm, vec=(scale_factor, scale_factor, 1.0), verts=bm.verts)
if bm.loops.layers.uv:
uv_layer = bm.loops.layers.uv.active
min_x = min(v.co.x for v in bm.verts)
max_x = max(v.co.x for v in bm.verts)
min_y = min(v.co.y for v in bm.verts)
max_y = max(v.co.y for v in bm.verts)
width = max_x - min_x
height = max_y - min_y
for face in bm.faces:
for loop in face.loops:
vert = loop.vert
u = (vert.co.x - min_x) / width if width > 0 else 0.5
v = (vert.co.y - min_y) / height if height > 0 else 0.5
u = max(0.0, min(1.0, u))
v = max(0.0, min(1.0, v))
loop[uv_layer].uv = (u, v)
bm.to_mesh(mesh) bm.to_mesh(mesh)
bm.free() bm.free()
tool.Loader.load_generated_uv_map(mesh)
mesh.update() mesh.update()
element = tool.Ifc.get_entity(self.target_object) element = tool.Ifc.get_entity(self.target_object)
@@ -87,6 +87,7 @@ class RemoveStyle(bpy.types.Operator, tool.Ifc.Operator):
core.remove_style(tool.Ifc, tool.Style, style=tool.Ifc.get().by_id(self.style), reload_styles_ui=True) core.remove_style(tool.Ifc, tool.Style, style=tool.Ifc.get().by_id(self.style), reload_styles_ui=True)
# TODO: remove completely
class AddStyle(bpy.types.Operator, tool.Ifc.Operator): class AddStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_style" bl_idname = "bim.add_style"
bl_label = "Add Style" bl_label = "Add Style"
+35 -3
View File
@@ -179,7 +179,8 @@ class Loader(bonsai.core.tool.Loader):
def surface_texture_to_dict(cls, surface_texture): def surface_texture_to_dict(cls, surface_texture):
if isinstance(surface_texture, dict): if isinstance(surface_texture, dict):
return surface_texture return surface_texture
mappings = surface_texture.IsMappedBy or [] # IsMappedBy is an IFC4+ inverse attribute, not available in IFC2X3.
mappings = getattr(surface_texture, "IsMappedBy", None) or []
surface_texture = surface_texture.get_info() surface_texture = surface_texture.get_info()
uv_mode = None uv_mode = None
if mappings: if mappings:
@@ -188,7 +189,7 @@ class Loader(bonsai.core.tool.Loader):
uv_mode = "Generated" uv_mode = "Generated"
elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE": elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE":
uv_mode = "Camera" uv_mode = "Camera"
surface_texture["uv_mode"] = uv_mode or "UV" surface_texture["uv_mode"] = uv_mode or "Generated"
return surface_texture return surface_texture
@classmethod @classmethod
@@ -286,6 +287,9 @@ class Loader(bonsai.core.tool.Loader):
for texture in textures: for texture in textures:
mode = texture.get("Mode", None) mode = texture.get("Mode", None)
# IFC2X3 IfcImageTexture has no Mode attribute; default to DIFFUSE.
if mode is None and texture["type"] == "IfcImageTexture":
mode = "DIFFUSE"
node = None node = None
image_url = None image_url = None
@@ -293,7 +297,8 @@ class Loader(bonsai.core.tool.Loader):
def get_image() -> Union[bpy.types.Image, None]: def get_image() -> Union[bpy.types.Image, None]:
# TODO: orphaned textures after shader recreated? # TODO: orphaned textures after shader recreated?
if texture["type"] == "IfcImageTexture": if texture["type"] == "IfcImageTexture":
original_image_url = texture["URLReference"] # IFC2X3 uses UrlReference, IFC4+ uses URLReference.
original_image_url = texture.get("URLReference") or texture.get("UrlReference", "")
is_relative = not os.path.isabs(original_image_url) is_relative = not os.path.isabs(original_image_url)
nonlocal image_url nonlocal image_url
image_url = Path(original_image_url) image_url = Path(original_image_url)
@@ -539,6 +544,33 @@ class Loader(bonsai.core.tool.Loader):
for colour in colours: for colour in colours:
cls.load_indexed_map(colour, mesh) cls.load_indexed_map(colour, mesh)
@classmethod
def load_generated_uv_map(cls, mesh: bpy.types.Mesh) -> None:
bm = bmesh.new()
bm.from_mesh(mesh)
uv_layer = bm.loops.layers.uv.active or bm.loops.layers.uv.new("UVMap")
all_verts = [v.co for v in bm.verts]
if not all_verts:
bm.free()
return
min_x = min(v.x for v in all_verts)
max_x = max(v.x for v in all_verts)
min_y = min(v.y for v in all_verts)
max_y = max(v.y for v in all_verts)
width = max_x - min_x
height = max_y - min_y
for face in bm.faces:
for loop in face.loops:
u = (loop.vert.co.x - min_x) / width if width > 0 else 0.5
v = (loop.vert.co.y - min_y) / height if height > 0 else 0.5
loop[uv_layer].uv = (max(0.0, min(1.0, u)), max(0.0, min(1.0, v)))
bm.to_mesh(mesh)
bm.free()
@classmethod @classmethod
def load_indexed_map(cls, index_map: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> None: def load_indexed_map(cls, index_map: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> None:
"""Add data from index map as blender mesh attribute. """Add data from index map as blender mesh attribute.
+3 -3
View File
@@ -934,11 +934,11 @@ class TestAddReferenceImage(NewFile):
bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True)
filepath = Path("test/files/image.jpg").absolute() filepath = Path("test/files/image.jpg").absolute()
bpy.ops.bim.add_reference_image(filepath=str(filepath)) bpy.ops.bim.add_reference_image(filepath=str(filepath), x_length=3.53982, y_length=2.0)
obj = bpy.data.objects["IfcAnnotation/image"] obj = bpy.data.objects["IfcAnnotation/image"]
assert obj is not None assert obj is not None
assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((1.0, 0.565, 0.0))) assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0)))
material = obj.active_material material = obj.active_material
assert material assert material
@@ -957,4 +957,4 @@ class TestAddReferenceImage(NewFile):
assert texture_filepath == filepath assert texture_filepath == filepath
uv_node = material_nodes["Texture Coordinate"] uv_node = material_nodes["Texture Coordinate"]
assert len(uv_node.outputs["Generated"].links[:]) == 1 assert len(uv_node.outputs["UV"].links[:]) == 1