mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user