This commit is contained in:
Andrej730
2025-03-04 11:58:59 +05:00
parent 33d3984990
commit 53a3afaded
25 changed files with 282 additions and 167 deletions
+2 -2
View File
@@ -55,10 +55,10 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
return
if isinstance(obj, bpy.types.Material):
props = obj.BIMStyleProperties
props = tool.Style.get_material_style_props(obj)
if ifc_definition_id := props.ifc_definition_id:
if props.is_renaming:
props.is_renmaing = False
props.is_renaming = False
return
tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
+4 -2
View File
@@ -265,7 +265,8 @@ class IfcStore:
IfcStore.guid_map[global_id] = obj
if element.is_a("IfcSurfaceStyle"):
obj.BIMStyleProperties.ifc_definition_id = element.id()
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = element.id()
else:
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = element.id()
@@ -406,7 +407,8 @@ class IfcStore:
@staticmethod
def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None:
if isinstance(obj, bpy.types.Material):
obj.BIMStyleProperties.ifc_definition_id = 0
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = 0
else: # bpy.types.Object
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
+4 -3
View File
@@ -86,7 +86,7 @@ class MaterialCreator:
def load_existing_materials(self) -> None:
for material in bpy.data.materials:
if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id:
if ifc_definition_id := tool.Blender.get_ifc_definition_id(material):
self.styles[ifc_definition_id] = material
def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None:
@@ -964,10 +964,11 @@ class IfcImporter:
self.material_creator.styles[style.id()] = blender_material
style_elements = tool.Style.get_style_elements(blender_material)
props = tool.Style.get_material_style_props(blender_material)
if tool.Style.has_blender_external_style(style_elements):
blender_material.BIMStyleProperties.active_style_type = "External"
props.active_style_type = "External"
else:
blender_material.BIMStyleProperties.active_style_type = "Shading"
props.active_style_type = "Shading"
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
@@ -3353,7 +3353,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator):
obj.material_slots[0].material = material
bpy.ops.bim.add_style()
style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id)
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
@@ -1019,7 +1019,7 @@ class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
position = Matrix()
direction = Vector(extrusion.ExtrudedDirection.DirectionRatios).normalized()
tool.Model.import_axis([Vector((0, 0, 0)), direction * extrusion.Depth], obj=obj, position=position)
tool.Model.import_axis((Vector((0, 0, 0)), direction * extrusion.Depth), obj=obj, position=position)
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_axis(context))
+2 -3
View File
@@ -97,9 +97,8 @@ class BlenderMaterialStyleData:
material = obj.active_material
if not material:
return False
props = material.BIMStyleProperties
style_id = props.ifc_definition_id
style = tool.Ifc.get_entity_by_id(style_id)
style = tool.Ifc.get_entity(material)
if not style:
return False
+21 -13
View File
@@ -118,8 +118,7 @@ class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator):
# Don't check blender_material and style_id as this operator is only called from UI.
assert isinstance(self.blender_material, str) # Type checker.
material = bpy.data.materials[self.blender_material]
style_id = material.BIMStyleProperties.ifc_definition_id
style = tool.Ifc.get_entity_by_id(style_id)
style = tool.Ifc.get_entity(material)
# Material is linked to a style from a different project.
if not style or tool.Ifc.get_object(style) != material:
@@ -221,18 +220,25 @@ class UpdateCurrentStyle(bpy.types.Operator):
def execute(self, context):
style = tool.Ifc.get().by_id(self.style_id)
material = tool.Ifc.get_object(style)
current_style_type = material.BIMStyleProperties.active_style_type
msprops = tool.Style.get_material_style_props(material)
current_style_type = msprops.active_style_type
if self.update_all:
sprops = tool.Style.get_style_props()
sprops.active_style_type = current_style_type
return {"FINISHED"}
updated_materials = set()
updated_materials: set[bpy.types.Material] = set()
for obj in context.selected_objects:
if not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve)):
continue
for mat in obj.data.materials:
if mat and mat not in updated_materials and mat.BIMStyleProperties.ifc_definition_id != 0:
mat.BIMStyleProperties.active_style_type = current_style_type
if (
mat
and mat not in updated_materials
and (msprops_ := tool.Style.get_material_style_props(mat)).ifc_definition_id != 0
):
msprops_.active_style_type = current_style_type
updated_materials.add(mat)
return {"FINISHED"}
@@ -755,7 +761,8 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
bonsai.bim.helper.import_attributes2(surface_style or self.ifc_class, attributes, callback)
material = tool.Ifc.get_object(style)
active_style_type = material.BIMStyleProperties.active_style_type
msprops = tool.Style.get_material_style_props(material)
active_style_type = msprops.active_style_type
if self.ifc_class == "IfcExternallyDefinedSurfaceStyle" and active_style_type != "External":
if tool.Style.has_blender_external_style(style_elements):
tool.Style.switch_shading(material, "External")
@@ -803,9 +810,10 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
# restore selected style type
material = tool.Ifc.get_object(self.style)
material.BIMStyleProperties.active_style_type = material.BIMStyleProperties.active_style_type
msprops = tool.Style.get_material_style_props(material)
msprops.active_style_type = msprops.active_style_type
def edit_existing_style(self):
def edit_existing_style(self) -> None:
ifc_file = tool.Ifc.get()
material = tool.Ifc.get_object(self.style)
assert self.surface_style
@@ -867,7 +875,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
attributes=bonsai.bim.helper.export_attributes(attributes),
)
def add_new_style(self):
def add_new_style(self) -> None:
material = tool.Ifc.get_object(self.style)
if self.props.is_editing_class == "IfcSurfaceStyleShading":
surface_style = ifcopenshell.api.run(
@@ -911,13 +919,13 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
attributes=bonsai.bim.helper.export_attributes(attributes),
)
def get_shading_attributes(self):
def get_shading_attributes(self) -> dict[str, Any]:
return {
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
"Transparency": self.props.transparency or None,
}
def get_rendering_attributes(self):
def get_rendering_attributes(self) -> dict[str, Any]:
if self.props.is_diffuse_colour_null:
diffuse_colour = None
elif self.props.diffuse_colour_class == "IfcColourRgb":
@@ -961,7 +969,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
textures.append(texture_data)
return textures
def color_to_dict(self, x):
def color_to_dict(self, x: tuple[float, float, float]) -> dict[str, Any]:
return {"Red": x[0], "Green": x[1], "Blue": x[2]}
+24 -4
View File
@@ -39,13 +39,13 @@ from typing import Literal, Union, TYPE_CHECKING, get_args
_ = gettext.gettext
def get_style_types(self, context):
def get_style_types(self: "BIMStylesProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not StylesData.is_loaded:
StylesData.load()
return StylesData.data["style_types"]
def get_reflectance_methods(self, context):
def get_reflectance_methods(self: "BIMStylesProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not StylesData.is_loaded:
StylesData.load()
return StylesData.data["reflectance_methods"]
@@ -77,6 +77,16 @@ class Style(PropertyGroup):
type=bpy.types.Material,
)
if TYPE_CHECKING:
ifc_definition_id: int
total_elements: int
style_classes: bpy.types.bpy_prop_collection_idprop[StrProperty]
has_surface_colour: bool
surface_colour: tuple[float, float, float]
has_diffuse_colour: bool
diffuse_colour: tuple[float, float, float]
blender_material: Union[bpy.types.Material, None]
STYLE_TYPES = [
("Shading", "Shading", ""),
@@ -86,7 +96,7 @@ STYLE_TYPES = [
def update_shading_styles(self: "BIMStylesProperties", context: bpy.types.Context) -> None:
for mat in bpy.data.materials:
if mat.BIMStyleProperties.ifc_definition_id == 0:
if tool.Blender.get_ifc_definition_id(mat) == 0:
continue
tool.Style.change_current_style_type(mat, self.active_style_type)
@@ -111,7 +121,9 @@ UV_MODES = [
("Camera", "Camera", _("UV from position coordinate in camera space")),
]
TextureMapMode = Literal[
"DIFFUSE", "NORMAL", "METALLICROUGHNESS", "SPECULAR", "SHININESS", "EMISSIVE", "OCCLUSION", "AMBIENT"
]
TEXTURE_MAPS_MODS = (
("DIFFUSE", "DIFFUSE", ""),
("NORMAL", "NORMAL", ""),
@@ -129,6 +141,10 @@ class Texture(PropertyGroup):
# NOTE: subtype `FILE_PATH` is not used to avoid .blend relative paths
path: StringProperty(name="Texture Path", update=update_shader_graph)
if TYPE_CHECKING:
mode: TextureMapMode
path: str
class ColourRgb(PropertyGroup):
name: StringProperty()
@@ -136,6 +152,10 @@ class ColourRgb(PropertyGroup):
# not exposed in the UI, here just to preserve the data
color_name: StringProperty(name="Color Name")
if TYPE_CHECKING:
color_value: tuple[float, float, float]
color_name: str
# to fit blender.bim.helper.export_attributes
def get_value(self):
return {
+22 -7
View File
@@ -16,11 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.bim.helper
import bonsai.tool as tool
from bpy.types import Panel, UIList
from bonsai.bim.module.style.data import StylesData, BlenderMaterialStyleData
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.style.prop import BIMStylesProperties, Style
class BIM_PT_styles(Panel):
@@ -94,7 +99,8 @@ class BIM_PT_styles(Panel):
if active_style:
row = self.layout.row(align=True)
if material := style.blender_material:
row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="")
msprops = tool.Style.get_material_style_props(material)
row.prop(msprops, "active_style_type", icon="SHADING_RENDERED", text="")
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
op.style_id = style.ifc_definition_id
@@ -255,11 +261,19 @@ class BIM_PT_styles(Panel):
class BIM_UL_styles(UIList):
def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_property):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMStylesProperties,
item: Style,
icon,
active_data,
active_property,
):
if item:
row = layout.row(align=True)
props = tool.Style.get_style_props()
if item.ifc_definition_id == props.is_editing_style:
if item.ifc_definition_id == data.is_editing_style:
row.label(text="", icon="GREASEPENCIL")
row.prop(item, "name", text="", emboss=False)
if item.has_surface_colour:
@@ -295,7 +309,7 @@ class BIM_PT_style(Panel):
@classmethod
def poll(cls, context):
return bool(tool.Ifc.get() and (material := context.material) and material.BIMStyleProperties.ifc_definition_id)
return bool(tool.Ifc.get() and (material := context.material) and tool.Blender.get_ifc_definition_id(material))
def draw(self, context):
# NOTE: this UI is needed only to indicate whether blender material is linked to IFC
@@ -305,10 +319,11 @@ class BIM_PT_style(Panel):
BlenderMaterialStyleData.load()
material = context.material
style_id = material.BIMStyleProperties.ifc_definition_id
assert material
style_id = tool.Blender.get_ifc_definition_id(material)
row = self.layout.row(align=True)
if style_id and not BlenderMaterialStyleData.data["is_linked_to_style"]:
if not BlenderMaterialStyleData.data["is_linked_to_style"]:
row.label(text="Material has linked IFC from a different project.")
op = row.operator("bim.unlink_style", icon="UNLINKED", text="")
op.blender_material = material.name
+4 -2
View File
@@ -1601,5 +1601,7 @@ class Blender(bonsai.core.tool.Blender):
return obj.BIMObjectProperties
@classmethod
def get_ifc_definition_id(cls, obj: bpy.types.Object) -> int:
return tool.Blender.get_object_bim_props(obj).ifc_definition_id
def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
if isinstance(obj, bpy.types.Object):
return tool.Blender.get_object_bim_props(obj).ifc_definition_id
return tool.Style.get_material_style_props(obj).ifc_definition_id
+1 -1
View File
@@ -639,7 +639,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def get_object_materials_without_styles(cls, obj: bpy.types.Object) -> list[bpy.types.Material]:
return [
s.material for s in obj.material_slots if s.material and not s.material.BIMStyleProperties.ifc_definition_id
s.material for s in obj.material_slots if s.material and not tool.Blender.get_ifc_definition_id(s.material)
]
@classmethod
+1 -1
View File
@@ -110,7 +110,7 @@ class Ifc(bonsai.core.tool.Ifc):
if isinstance(obj, bpy.types.Object):
props = tool.Blender.get_object_bim_props(obj)
elif isinstance(obj, bpy.types.Material):
props = obj.BIMStyleProperties
props = tool.Style.get_material_style_props(obj)
else:
props = tool.Geometry.get_mesh_props(obj)
+20 -9
View File
@@ -293,8 +293,19 @@ class Model(bonsai.core.tool.Model):
else:
break
unit_scale: float
vertices: list[Vector]
edges: list[Sequence[int]]
arcs: list[Sequence[int]]
circles: list[Sequence[int]]
@classmethod
def import_axis(cls, axis, obj=None, position=None):
def import_axis(
cls,
axis: Union[ifcopenshell.entity_instance, tuple[Vector, Vector]],
obj=None,
position: Optional[Matrix] = None,
) -> bpy.types.Object:
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if position is None:
@@ -305,7 +316,7 @@ class Model(bonsai.core.tool.Model):
cls.arcs = []
cls.circles = []
if isinstance(axis, list):
if isinstance(axis, tuple):
cls.vertices.extend(
[
position @ Vector(cls.convert_unit_to_si(axis[0])).to_3d(),
@@ -478,7 +489,7 @@ class Model(bonsai.core.tool.Model):
@classmethod
def convert_curve_to_mesh(
cls,
obj: bpy.types.Object,
obj: Union[bpy.types.Object, None], # Unused argument.
position: Matrix,
curve: ifcopenshell.entity_instance,
x_angle: Optional[float] = None,
@@ -1624,7 +1635,7 @@ class Model(bonsai.core.tool.Model):
loop_edges = list(bm.edges)
# Create loops from edges
loops = []
loops: list[list[bmesh.types.BMEdge]] = []
while loop_edges:
edge = loop_edges.pop()
loop = [edge]
@@ -1645,19 +1656,19 @@ class Model(bonsai.core.tool.Model):
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
def is_in_group(v, group_name):
def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool:
for group_index in groups[group_name]:
if group_index in v[deform_layer]:
return True
return False
def get_group_index(v, group_name):
def get_group_index(v: bmesh.types.BMVert, group_name: str) -> Union[int, None]:
for group_index in groups[group_name]:
if group_index in v[deform_layer]:
return group_index
# Convert all loops into IFC curves
curves = []
curves: list[ifcopenshell.entity_instance] = []
for loop in loops:
if len(loop) == 1 and all([is_in_group(v, "IFCCIRCLE") for v in loop[0].verts]):
@@ -1670,7 +1681,7 @@ class Model(bonsai.core.tool.Model):
tmp.createIfcCircle(tmp.createIfcAxis2Placement2D(tmp.createIfcCartesianPoint(list(mid))), radius)
)
else:
loop_verts = []
loop_verts: list[bmesh.types.BMVert] = []
for i, edge in enumerate(loop):
if i == 0 and len(loop) == 1:
loop_verts.append(edge.verts[0])
@@ -1746,7 +1757,7 @@ class Model(bonsai.core.tool.Model):
curves.append(tmp.createIfcIndexedPolyCurve(points))
# Sort IFC curves into either closed, or closed with void profile defs
profile_defs = []
profile_defs: list[ifcopenshell.entity_instance] = []
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
+2 -2
View File
@@ -418,8 +418,8 @@ class Root(bonsai.core.tool.Root):
"""Rename material without triggerring name callback and unnecessary writing to IFC."""
if material.name == name:
return
props = material.BIMStyleProperties
props.is_renaming = True
msprops = tool.Style.get_material_style_props(material)
msprops.is_renaming = True
material.name = name # The handler will trigger, and reset is_renaming to False.
@classmethod
+11 -11
View File
@@ -162,14 +162,14 @@ class Style(bonsai.core.tool.Style):
cls, blender_material_or_style: Union[bpy.types.Material, ifcopenshell.entity_instance]
) -> dict[str, ifcopenshell.entity_instance]:
if isinstance(blender_material_or_style, bpy.types.Material):
if not (ifc_definition_id := blender_material_or_style.BIMStyleProperties.ifc_definition_id):
style = tool.Ifc.get_entity(blender_material_or_style)
if not style:
return {}
style = tool.Ifc.get().by_id(ifc_definition_id)
else:
style = blender_material_or_style
style_elements = {}
for style in style.Styles:
style_elements[style.is_a()] = style
for style_ in style.Styles:
style_elements[style_.is_a()] = style_
return style_elements
@classmethod
@@ -305,7 +305,7 @@ class Style(bonsai.core.tool.Style):
return next((l.from_node for l in input_pin.links if l.from_node.type == of_type), None)
return next((l.from_node for l in input_pin.links), None)
props = obj.BIMStyleProperties
props = tool.Style.get_material_style_props(obj)
transparency = 1 - obj.diffuse_color[3]
diffuse_color = obj.diffuse_color
viewport_color = color_to_ifc_format(obj.diffuse_color)
@@ -487,16 +487,14 @@ class Style(bonsai.core.tool.Style):
@classmethod
def get_surface_shading_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id:
style = tool.Ifc.get().by_id(ifc_definition_id)
if style := tool.Ifc.get_entity(obj):
items = [s for s in style.Styles if s.is_a() == "IfcSurfaceStyleShading"]
if items:
return items[0]
@classmethod
def get_surface_texture_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id:
style = tool.Ifc.get().by_id(ifc_definition_id)
if style := tool.Ifc.get_entity(obj):
items = [s for s in style.Styles if s.is_a("IfcSurfaceStyleWithTextures")]
if items:
return items[0]
@@ -581,7 +579,8 @@ class Style(bonsai.core.tool.Style):
@classmethod
def change_current_style_type(cls, blender_material: bpy.types.Material, style_type: str) -> None:
blender_material.BIMStyleProperties.active_style_type = style_type
props = cls.get_material_style_props(blender_material)
props.active_style_type = style_type
@classmethod
def get_styled_items(cls, style: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
@@ -632,7 +631,8 @@ class Style(bonsai.core.tool.Style):
@classmethod
def reload_material_from_ifc(cls, blender_material: bpy.types.Material) -> None:
blender_material.BIMStyleProperties.active_style_type = blender_material.BIMStyleProperties.active_style_type
props = cls.get_material_style_props(blender_material)
props.active_style_type = props.active_style_type
@classmethod
def switch_shading(cls, blender_material: bpy.types.Material, style_type: StyleType) -> None:
+2 -2
View File
@@ -911,14 +911,14 @@ def the_material_name_is_not_an_ifc_material(name):
@then(parsers.parse('the material "{name}" is an IFC style'))
def the_material_name_is_an_ifc_style(name):
obj = the_material_name_exists(name)
ifc_definition_id = obj.BIMStyleProperties.ifc_definition_id
ifc_definition_id = tool.Blender.get_ifc_definition_id(obj)
assert ifc_definition_id != 0, f"The material {obj} has a style ID of {ifc_definition_id}"
@then(parsers.parse('the material "{name}" is not an IFC style'))
def the_material_name_is_not_an_ifc_style(name):
obj = the_material_name_exists(name)
ifc_definition_id = obj.BIMStyleProperties.ifc_definition_id
ifc_definition_id = tool.Blender.get_ifc_definition_id(obj)
assert ifc_definition_id == 0, f"The material {obj} has a style ID of {ifc_definition_id}"
+4 -3
View File
@@ -891,11 +891,12 @@ class TestAddReferenceImage(NewFile):
assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0)))
material = obj.active_material
assert material
assert material.name == "image"
assert material.BIMStyleProperties.ifc_definition_id != 0
assert tool.Blender.get_ifc_definition_id(material) != 0
ifc_file = tool.Ifc.get()
style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id)
style = tool.Ifc.get_entity(material)
assert style
styled_items = set(tool.Style.get_styled_items(style))
representation_items = set(tool.Geometry.get_active_representation(obj).Items)
assert styled_items == representation_items
+3 -2
View File
@@ -112,7 +112,8 @@ class TestGetObjectMaterialsWithoutStyles(NewFile):
material1 = bpy.data.materials.new("Material")
material2 = bpy.data.materials.new("Material")
material3 = bpy.data.materials.new("Material")
material3.BIMStyleProperties.ifc_definition_id = 1
props = tool.Style.get_material_style_props(material3)
props.ifc_definition_id = 1
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
obj.data.materials.append(material1)
obj.data.materials.append(material2)
@@ -307,7 +308,7 @@ class TestRecordObjectMaterials(NewFile):
tool.Ifc.set(ifc)
style = ifc.createIfcSurfaceStyle()
material = bpy.data.materials.new("Material")
material.BIMStyleProperties.ifc_definition_id = style.id()
tool.Ifc.link(style, material)
obj.data.materials.append(material)
subject.record_object_materials(obj)
assert tool.Geometry.get_mesh_props(obj.data).material_checksum == str([style.id()])
+1 -1
View File
@@ -462,7 +462,7 @@ class TestApplyIfcMaterialChanges(NewFile):
def get_used_styles(self, obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]:
ifc_file = tool.Ifc.get()
return {
ifc_file.by_id(s.material.BIMStyleProperties.ifc_definition_id) for s in obj.material_slots if s.material
ifc_file.by_id(tool.Blender.get_ifc_definition_id(s.material)) for s in obj.material_slots if s.material
}
def get_mesh(self, obj: bpy.types.Object) -> bpy.types.Mesh:
+3
View File
@@ -18,6 +18,9 @@
import bpy
import ifcopenshell
import ifcopenshell.api.project
import ifcopenshell.api.root
import ifcopenshell.api.unit
import bonsai.core.tool
import bonsai.tool as tool
from test.bim.bootstrap import NewFile
+5 -4
View File
@@ -308,7 +308,8 @@ class TestGetSurfaceRenderingStyle(NewFile):
style_item = tool.Ifc.get().createIfcSurfaceStyleRendering()
style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item])
obj = bpy.data.materials.new("Material")
obj.BIMStyleProperties.ifc_definition_id = style.id()
props = tool.Style.get_material_style_props(obj)
props.ifc_definition_id = style.id()
assert subject.get_surface_rendering_style(obj) == style_item
@@ -347,7 +348,7 @@ class TestGetSurfaceShadingStyle(NewFile):
style_item = tool.Ifc.get().createIfcSurfaceStyleShading()
style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item])
obj = bpy.data.materials.new("Material")
obj.BIMStyleProperties.ifc_definition_id = style.id()
tool.Ifc.link(style, obj)
assert subject.get_surface_shading_style(obj) == style_item
def test_do_not_get_rendering_styles(self):
@@ -355,7 +356,7 @@ class TestGetSurfaceShadingStyle(NewFile):
style_item = tool.Ifc.get().createIfcSurfaceStyleRendering()
style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item])
obj = bpy.data.materials.new("Material")
obj.BIMStyleProperties.ifc_definition_id = style.id()
tool.Ifc.link(style, obj)
assert subject.get_surface_shading_style(obj) is None
@@ -365,7 +366,7 @@ class TestGetSurfaceTextureStyle(NewFile):
style_item = tool.Ifc.get().createIfcSurfaceStyleWithTextures()
style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item])
obj = bpy.data.materials.new("Material")
obj.BIMStyleProperties.ifc_definition_id = style.id()
tool.Ifc.link(style, obj)
assert subject.get_surface_texture_style(obj) == style_item
@@ -21,159 +21,203 @@ import ifcopenshell.util.element
import ifcopenshell.util.shape
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
from typing import Any, Union, Optional, Literal
from typing import Any, Union, Optional, Literal, get_args
VECTOR_3D = tuple[float, float, float]
CardinalPointNumeric = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
CardinalPointString = Literal[
"bottom left",
"bottom centre",
"bottom right",
"mid-depth left",
"mid-depth centre",
"mid-depth right",
"top left",
"top centre",
"top right",
"geometric centroid",
"bottom in line with the geometric centroid",
"left in line with the geometric centroid",
"right in line with the geometric centroid",
"top in line with the geometric centroid",
"shear centre",
"bottom in line with the shear centre",
"left in line with the shear centre",
"right in line with the shear centre",
"top in line with the shear centre",
]
CARDINAL_POINT_VALUES: tuple[CardinalPointString, ...] = get_args(CardinalPointString)
CardinalPoint = Union[CardinalPointNumeric, CardinalPointString]
def add_profile_representation(
file: ifcopenshell.file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
profile: ifcopenshell.entity_instance,
# in meters
depth: float = 1.0,
cardinal_point: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 5,
# A list of planes that define clipping half space solids
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
# TODO: None makes more sense as default value?
cardinal_point: Union[CardinalPoint, None] = 5,
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None),
) -> ifcopenshell.entity_instance:
"""Add profile representation.
:param context: The IfcGeometricRepresentationContext for the representation,
only Model/Body/MODEL_VIEW type of representations are currently supported.
:param profile: The IfcProfileDef to extrude.
:param depth: The depth of the extrusion in meters.
:param cardinal_point: The cardinal point of the profile.
:param clippings: A list of planes that define clipping half space solids.
Planes are defined either by Clipping objects
or by dictionaries of arguments for `Clipping.parse`.
:param placement_zx_axes: A tuple of two vectors that define the placement of the profile.
The first vector is the Z axis, the second vector is the X axis.
:return: IfcShapeRepresentation.
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": context,
"profile": profile,
"depth": depth,
"cardinal_point": cardinal_point,
"clippings": clippings if clippings is not None else [],
"placement_zx_axes": placement_zx_axes,
}
return usecase.execute()
clippings = clippings if clippings is not None else []
return usecase.execute(context, profile, depth, cardinal_point, clippings, placement_zx_axes)
class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
clippings: list[Clipping]
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]]
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"Clipping" if self.settings["clippings"] else "SweptSolid",
def execute(
self,
context: ifcopenshell.entity_instance,
profile: ifcopenshell.entity_instance,
depth: float,
cardinal_point: Union[CardinalPoint, None],
clippings: list[Union[Clipping, dict[str, Any]]],
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]],
) -> ifcopenshell.entity_instance:
if isinstance(cardinal_point, int):
cardinal_point = CARDINAL_POINT_VALUES[cardinal_point - 1]
self.cardinal_point = cardinal_point
self.profile = profile
self.clippings = [Clipping.parse(c) for c in clippings]
self.depth = depth
self.placement_zx_axes = placement_zx_axes
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
return self.file.create_entity(
"IfcShapeRepresentation",
context,
context.ContextIdentifier,
"Clipping" if self.clippings else "SweptSolid",
[self.create_item()],
)
def create_item(self):
def create_item(self) -> ifcopenshell.entity_instance:
point = self.get_point()
placement = self.file.createIfcAxis2Placement3D(
point,
self.file.createIfcDirection(self.settings["placement_zx_axes"][0] or (0.0, 0.0, 1.0)),
self.file.createIfcDirection(self.settings["placement_zx_axes"][1] or (1.0, 0.0, 0.0)),
self.file.create_entity("IfcDirection", self.placement_zx_axes[0] or (0.0, 0.0, 1.0)),
self.file.create_entity("IfcDirection", self.placement_zx_axes[1] or (1.0, 0.0, 0.0)),
)
extrusion = self.file.createIfcExtrudedAreaSolid(
self.settings["profile"],
extrusion = self.file.create_entity(
"IfcExtrudedAreaSolid",
self.profile,
placement,
self.file.createIfcDirection((0.0, 0.0, 1.0)),
self.convert_si_to_unit(self.settings["depth"]),
self.convert_si_to_unit(self.depth),
)
if self.settings["clippings"]:
if self.clippings:
return self.apply_clippings(extrusion)
return extrusion
def apply_clippings(self, first_operand):
while self.settings["clippings"]:
clipping = self.settings["clippings"].pop()
def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
while self.clippings:
clipping = self.clippings.pop()
if isinstance(clipping, ifcopenshell.entity_instance):
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
else: # Clipping
first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"])
first_operand = clipping.apply(self.file, first_operand, self.unit_scale)
return first_operand
def convert_si_to_unit(self, co):
return co / self.settings["unit_scale"]
def convert_si_to_unit(self, co: float) -> float:
return co / self.unit_scale
def get_point(self):
if not self.settings["cardinal_point"]:
def get_point(self) -> ifcopenshell.entity_instance:
if not self.cardinal_point:
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
elif self.settings["cardinal_point"] == 1:
elif self.cardinal_point == "bottom left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 2:
elif self.cardinal_point == "bottom centre":
return self.file.createIfcCartesianPoint((0.0, self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 3:
elif self.cardinal_point == "bottom right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 4:
elif self.cardinal_point == "mid-depth left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, 0.0, 0.0))
elif self.settings["cardinal_point"] == 5:
elif self.cardinal_point == "mid-depth centre":
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
elif self.settings["cardinal_point"] == 6:
elif self.cardinal_point == "mid-depth right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, 0.0, 0.0))
elif self.settings["cardinal_point"] == 7:
elif self.cardinal_point == "top left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, -self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 8:
elif self.cardinal_point == "top centre":
return self.file.createIfcCartesianPoint((0.0, -self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 9:
elif self.cardinal_point == "top right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, -self.get_y() / 2, 0.0))
# TODO other cardinal points
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
def get_x(self):
if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"):
return self.settings["profile"].OverallWidth
elif self.settings["profile"].is_a("IfcCShapeProfileDef"):
return self.settings["profile"].Width
elif self.settings["profile"].is_a("IfcCircleProfileDef"):
return self.settings["profile"].Radius * 2
elif self.settings["profile"].is_a("IfcEllipseProfileDef"):
return self.settings["profile"].SemiAxis1 * 2
elif self.settings["profile"].is_a("IfcIShapeProfileDef"):
return self.settings["profile"].OverallWidth
elif self.settings["profile"].is_a("IfcLShapeProfileDef"):
return self.settings["profile"].Width
elif self.settings["profile"].is_a("IfcRectangleProfileDef"):
return self.settings["profile"].XDim
elif self.settings["profile"].is_a("IfcTShapeProfileDef"):
return self.settings["profile"].FlangeWidth
elif self.settings["profile"].is_a("IfcUShapeProfileDef"):
return self.settings["profile"].FlangeWidth
elif self.settings["profile"].is_a("IfcZShapeProfileDef"):
return (self.settings["profile"].FlangeWidth * 2) - self.settings["profile"].WebThickness
def get_x(self) -> float:
if self.profile.is_a("IfcAsymmetricIShapeProfileDef"):
return self.profile.OverallWidth
elif self.profile.is_a("IfcCShapeProfileDef"):
return self.profile.Width
elif self.profile.is_a("IfcCircleProfileDef"):
return self.profile.Radius * 2
elif self.profile.is_a("IfcEllipseProfileDef"):
return self.profile.SemiAxis1 * 2
elif self.profile.is_a("IfcIShapeProfileDef"):
return self.profile.OverallWidth
elif self.profile.is_a("IfcLShapeProfileDef"):
return self.profile.Width
elif self.profile.is_a("IfcRectangleProfileDef"):
return self.profile.XDim
elif self.profile.is_a("IfcTShapeProfileDef"):
return self.profile.FlangeWidth
elif self.profile.is_a("IfcUShapeProfileDef"):
return self.profile.FlangeWidth
elif self.profile.is_a("IfcZShapeProfileDef"):
return (self.profile.FlangeWidth * 2) - self.profile.WebThickness
else:
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"])
shape = ifcopenshell.geom.create_shape(settings, self.profile)
return self.convert_si_to_unit(ifcopenshell.util.shape.get_x(shape))
return 0.0
def get_y(self):
if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"):
return self.settings["profile"].OverallDepth
elif self.settings["profile"].is_a("IfcCShapeProfileDef"):
return self.settings["profile"].Depth
elif self.settings["profile"].is_a("IfcCircleProfileDef"):
return self.settings["profile"].Radius * 2
elif self.settings["profile"].is_a("IfcEllipseProfileDef"):
return self.settings["profile"].SemiAxis2 * 2
elif self.settings["profile"].is_a("IfcIShapeProfileDef"):
return self.settings["profile"].OverallDepth
elif self.settings["profile"].is_a("IfcLShapeProfileDef"):
return self.settings["profile"].Depth
elif self.settings["profile"].is_a("IfcRectangleProfileDef"):
return self.settings["profile"].YDim
elif self.settings["profile"].is_a("IfcTShapeProfileDef"):
return self.settings["profile"].Depth
elif self.settings["profile"].is_a("IfcUShapeProfileDef"):
return self.settings["profile"].Depth
elif self.settings["profile"].is_a("IfcZShapeProfileDef"):
return self.settings["profile"].Depth
def get_y(self) -> float:
if self.profile.is_a("IfcAsymmetricIShapeProfileDef"):
return self.profile.OverallDepth
elif self.profile.is_a("IfcCShapeProfileDef"):
return self.profile.Depth
elif self.profile.is_a("IfcCircleProfileDef"):
return self.profile.Radius * 2
elif self.profile.is_a("IfcEllipseProfileDef"):
return self.profile.SemiAxis2 * 2
elif self.profile.is_a("IfcIShapeProfileDef"):
return self.profile.OverallDepth
elif self.profile.is_a("IfcLShapeProfileDef"):
return self.profile.Depth
elif self.profile.is_a("IfcRectangleProfileDef"):
return self.profile.YDim
elif self.profile.is_a("IfcTShapeProfileDef"):
return self.profile.Depth
elif self.profile.is_a("IfcUShapeProfileDef"):
return self.profile.Depth
elif self.profile.is_a("IfcZShapeProfileDef"):
return self.profile.Depth
else:
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"])
shape = ifcopenshell.geom.create_shape(settings, self.profile)
return self.convert_si_to_unit(ifcopenshell.util.shape.get_y(shape))
return 0.0
@@ -149,6 +149,8 @@ class Usecase:
self.settings_2d.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(self.settings_2d, dummy_solid)
# NOTE: points do not need unit conversion
# as dummy file is inherently using project units.
if self.cardinal_point == 1:
return self.get_bottom_left(shape)
elif self.cardinal_point == 2:
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import numpy as np
import ifcopenshell
from typing import Any, Union
@@ -30,7 +31,9 @@ class Clipping:
operand_type: str = "IfcHalfSpaceSolid"
@classmethod
def parse(cls, raw_data: Any) -> Union[ifcopenshell.entity_instance, "Clipping", None]:
def parse(
cls, raw_data: Union[ifcopenshell.entity_instance, Clipping, dict[str, Any]]
) -> Union[ifcopenshell.entity_instance, Clipping]:
"""Parse various formats into a clipping object
`raw_data` can be either:
@@ -20,6 +20,7 @@ import test.bootstrap
import ifcopenshell.api.root
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.util.shape_builder
class TestAddShapeAspect(test.bootstrap.IFC4):