First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1,57 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Manage visual styles of geometry (colours, transparency, rendering, etc)
Geometry may have visual styles associated with it, including surface styles,
2D curve styles, text styles, and more. Surface styles are most commonly used
for simple colouring.
"""
from .. import wrap_usecases
from .add_style import add_style
from .add_surface_style import add_surface_style
from .add_surface_textures import add_surface_textures
from .assign_item_style import assign_item_style
from .assign_material_style import assign_material_style
from .assign_representation_styles import assign_representation_styles
from .edit_presentation_style import edit_presentation_style
from .edit_surface_style import edit_surface_style
from .remove_style import remove_style
from .remove_styled_representation import remove_styled_representation
from .remove_surface_style import remove_surface_style
from .unassign_material_style import unassign_material_style
from .unassign_representation_styles import unassign_representation_styles
wrap_usecases(__path__, __name__)
__all__ = [
"add_style",
"add_surface_style",
"add_surface_textures",
"assign_item_style",
"assign_material_style",
"assign_representation_styles",
"edit_presentation_style",
"edit_surface_style",
"remove_style",
"remove_styled_representation",
"remove_surface_style",
"unassign_material_style",
"unassign_representation_styles",
]
@@ -0,0 +1,65 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 typing import Optional
import ifcopenshell
def add_style(
file: ifcopenshell.file, name: Optional[str] = None, ifc_class="IfcSurfaceStyle"
) -> ifcopenshell.entity_instance:
"""Add a new presentation style
A presentation style is a container of visual settings (called
presentation items) that affect the appearance of objects. There are
four types of style:
- Surface styles, which give 3D objects (which have surfaces / faces)
their colours and textures. This is the most common type of style.
- Curve styles, which give 2D and 3D curves, lines, polylines, their
stroke thickness and colour.
- Fill area styles, which gives 2D polygons and flat 3D planes their
colours, hatch patterns, tiled patterns, and pattern scales.
- Text styles, which gives text their font family, weight, variant,
size, indentation, alignment, decoration, spacing, and transformation.
Once you have created a presentation style object, you can further
define the properties of your style using other API functions by adding
presentation items, such as ifcopenshell.api.style.add_surface_style.
:param name: The name of the style. Used to easily identify it using a
style library.
:param ifc_class: Choose from IfcSurfaceStyle, IfcCurveStyle,
IfcFillAreaStyle, or IfcTextStyle.
:return: The newly created style element, based on the provided
ifc_class.
Example:
.. code:: python
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
"""
kwargs = {"Name": name}
if ifc_class == "IfcSurfaceStyle":
# Name is filled out because Revit treats this incorrectly as the material name
kwargs["Side"] = "BOTH"
return file.create_entity(ifc_class, **kwargs)
@@ -0,0 +1,143 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 typing import Any, Literal, Optional
import ifcopenshell
import ifcopenshell.api.style
SURFACE_STYLE_TYPES = Literal[
"IfcSurfaceStyleShading",
"IfcSurfaceStyleRendering",
"IfcSurfaceStyleWithTextures",
"IfcSurfaceStyleLighting",
"IfcSurfaceStyleRefraction",
"IfcExternallyDefinedSurfaceStyle",
]
def add_surface_style(
file: ifcopenshell.file,
style: ifcopenshell.entity_instance,
ifc_class: SURFACE_STYLE_TYPES = "IfcSurfaceStyleShading",
attributes: Optional[dict[str, Any]] = None,
) -> ifcopenshell.entity_instance:
"""Adds a new presentation item to a surface style
A surface style can have multiple different types of presentation items
assigned to it:
- Shading, this is the simplest item, which defines a single basic
colour and transparency that can be used to display the object on a
screen. It is an indicative colour of what the object would be in real
life. It is commonly incorrectly abused to colour code systems for MEP
equipment or object types for structural steel. If you just want to
give something a colour, this is what you need.
- Rendering, this is an advanced extension of shading, which includes
the definition of a shader for a rendering engine. You may select the
reflectance / lighting model such as PHYSICAL, for PBR style
rendering, or FLAT, for flat shading, or PHONG for older biased
rendering workflows. Based on the chosen lighting model, you may then
specify the appropriate colour maps, such as diffuse colours,
specularity, emissive component, etc. These lighting models are fully
compatible with glTF and X3D. This should be used if your model is
prepared to be rendered by a rendering engine which is compatible with
glTF / X3D shader descriptions. If you are doing archviz or 3D
rendering, this is what you need.
- Textures, this is a special type of Rendering presentation item that
uses image textures instead of single colours. Textures may be either
mapped using a bounding box stretch mapping, or with UV coordinates
for mesh-like geometry.
- Lighting, this is used to define photometrically accurate colour
parameters used in lighting simulation. If you are a simulationist,
this is what you need.
- Reflectance, this is a special type of Lighting presentation item
which includes some lesser used photometric properties, typically
required for advanced materials like glazing.
- External, this is for any other surface style defined using an
external URI. This is relevant if you are using a third-party non-glTF
compatible shader definition such as for Cycles, Renderman, V-Ray,
etc, or a complex lighting simulation definition, such as for
Radiance.
Shading is sufficient for the majority of basic models.
The attributes you specify will depend on the type of presentation item
you are adding. An example is shown below, but for full details please
refer to the IFC documentation.
:param style: The IfcSurfaceStyle you want to add to presentation item
to. See ifcopenshell.api.style.add_style.
:param ifc_class: Choose from IfcSurfaceStyleShading,
IfcSurfaceStyleRendering, IfcSurfaceStyleWithTextures,
IfcSurfaceStyleLighting, IfcSurfaceStyleReflectance, or
IfcExternallyDefinedSurfaceStyle.
:param attributes: a dictionary of attribute names and values.
:return: The newly created presentation item based on the provided
ifc_class.
Example:
.. code:: python
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
# Create a simple shading colour and transparency.
ifcopenshell.api.style.add_surface_style(model,
style=style, ifc_class="IfcSurfaceStyleShading", attributes={
"SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 },
"Transparency": 0., # 0 is opaque, 1 is transparent
})
# Alternatively, create a rendering style.
ifcopenshell.api.style.add_surface_style(model,
style=style, ifc_class="IfcSurfaceStyleRendering", attributes={
# A surface colour and transparency is still supplied for
# viewport display only. This will supersede the shading
# presentation item.
"SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 },
"Transparency": 0., # 0 is opaque, 1 is transparent
# NOTDEFINED is assumed to be a PHYSICAL (PBR) lighting
# model. In IFC4X3, you may choose PHYSICAL directly.
"ReflectanceMethod": "NOTDEFINED",
# For PBR shading, you may specify these parameters:
"DiffuseColour": { "Name": None, "Red": 0.9, "Green": 0.8, "Blue": 0.8 },
"SpecularColour": 0.1, # Metallic factor
"SpecularHighlight": {"SpecularRoughness": 0.5}, # Roughness factor
})
"""
attributes = attributes or {}
style_item = file.create_entity(ifc_class)
ifcopenshell.api.style.edit_surface_style(file, style=style_item, attributes=attributes)
styles: list[ifcopenshell.entity_instance]
styles = list(style.Styles or [])
select_class = ifc_class
if select_class == "IfcSurfaceStyleRendering":
select_class = "IfcSurfaceStyleShading"
duplicate_items = [s for s in styles if s.is_a(select_class)]
for duplicate_item in duplicate_items:
ifcopenshell.api.style.remove_surface_style(file, style=duplicate_item)
styles = list(style.Styles or [])
styles.append(style_item)
style.Styles = styles
return style_item
@@ -0,0 +1,204 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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
from typing import TYPE_CHECKING, Any, Optional
import ifcopenshell
if TYPE_CHECKING:
import bpy # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
def add_surface_textures(
file: ifcopenshell.file,
material: Optional[bpy.types.Material] = None,
textures: Optional[list[dict]] = None,
uv_maps: Optional[list[ifcopenshell.entity_instance]] = None,
) -> list[ifcopenshell.entity_instance]:
"""Add surface texture based on a Blender material definition or texture data.
Either `material` or `textures` should be provided.
:param material: The Blender material definition with a node tree that
is compatible with glTF. See one of the valid combinations here:
https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html
:param uv_maps: A list of IfcIndexedTextureMap for any
IfcTessellatedFaceSets that the representation has, obtained from
the HasTextures attribute.
:param textures: A list of dictionaries containing:
1. Attributes to create IfcImageTexture.
2. One additional parameter `uv_mode` to map IfcImageTexture to correct
IfcTextureCoordinate type.
Possible `uv_mode` values:
* `UV` - use IfcTextureCoordinate from `uv_maps` parameter;
* `Generated` - IfcTextureCoordinateGenerator with mode COORD (autogenerated UV
based on geometry);
* `Camera` - IfcTextureCoordinateGenerator with mode COORD_EYE (autogenerated UV
based on camera position)
:return: A list of IfcImageTexture
"""
usecase = Usecase()
# TODO: This usecase currently depends on Blender's data model
usecase.file = file
usecase.settings = {"material": material, "uv_maps": uv_maps or [], "textures": textures or []}
return usecase.execute()
class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
if self.file.schema == "IFC2X3":
# TODO: research how compatible IFC2X3 and IFC4 textures are
return []
# We optimistically assume the user has specified one of these valid combinations
# https://docs.blender.org/manual/en/dev/addons/import_export/scene_gltf2.html
# glTF, X3D, and IFC are compatible. As long as they have something that
# loosely resembles the node tree, we treat it as valid.
self.textures = []
for texture in self.settings["textures"]:
uv_mode = texture.get("uv_mode", None)
texture_data = texture.copy()
texture_data.pop("uv_mode", None)
texture = self.file.create_entity("IfcImageTexture", **texture_data)
if uv_mode == "Generated":
self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD")
elif uv_mode == "Camera":
self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD-EYE")
elif uv_mode == "UV":
self.apply_uv_map_to_texture(texture)
self.textures.append(texture)
if self.settings["material"] is None:
return self.textures
output = {n.type: n for n in self.settings["material"].node_tree.nodes}.get("OUTPUT_MATERIAL", None)
if not output:
return self.textures
bsdf = output.inputs["Surface"].links[0].from_node
if bsdf.type == "ADD_SHADER":
for socket in bsdf.inputs:
if socket.links and socket.links[0].from_node.type == "BSDF_PRINCIPLED":
bsdf = socket.links[0].from_node
break
if bsdf.type == "MIX_SHADER":
self.detect_unlit_emissive_map(bsdf)
elif bsdf.type == "BSDF_PRINCIPLED":
self.detect_normal_map(bsdf)
self.detect_emissive_map(bsdf)
self.detect_metallicroughness_map(bsdf)
self.detect_occlusion_map()
self.detect_diffuse_map(bsdf)
# We do not support Phong shading. What year is this, 1995?
return self.textures
def detect_unlit_emissive_map(self, bsdf):
for socket in bsdf.inputs:
if socket.links and socket.links[0].from_node.type == "TEX_IMAGE":
return self.create_surface_texture(socket.links[0].from_node, "EMISSIVE")
def detect_normal_map(self, bsdf):
if bsdf.inputs["Normal"].links and bsdf.inputs["Normal"].links[0].from_node.type == "NORMAL_MAP":
normal = bsdf.inputs["Normal"].links[0].from_node
if normal.inputs["Color"].links and normal.inputs["Color"].links[0].from_node.type == "TEX_IMAGE":
return self.create_surface_texture(normal.inputs["Color"].links[0].from_node, "NORMAL")
def detect_emissive_map(self, bsdf):
if bsdf.outputs[0].links[0].to_node.type != "ADD_SHADER":
return
bsdf = bsdf.outputs[0].links[0].to_node
for socket in bsdf.inputs:
if socket.links and socket.links[0].from_node.type == "EMISSION":
bsdf = socket.links[0].from_node
if bsdf.inputs["Color"].links and bsdf.inputs["Color"].links[0].from_node.type == "TEX_IMAGE":
return self.create_surface_texture(bsdf.inputs["Color"].links[0].from_node, "EMISSIVE")
def detect_metallicroughness_map(self, bsdf):
if bsdf.inputs["Metallic"].links and bsdf.inputs["Metallic"].links[0].from_node.type == "SEPRGB":
seprgb = bsdf.inputs["Metallic"].links[0].from_node
if seprgb.inputs["Image"].links and seprgb.inputs["Image"].links[0].from_node.type == "TEX_IMAGE":
return self.create_surface_texture(seprgb.inputs["Image"].links[0].from_node, "METALLICROUGHNESS")
if bsdf.inputs["Roughness"].links and bsdf.inputs["Roughness"].links[0].from_node.type == "SEPRGB":
seprgb = bsdf.inputs["Roughness"].links[0].from_node
if seprgb.inputs["Image"].links and seprgb.inputs["Image"].links[0].from_node.type == "TEX_IMAGE":
return self.create_surface_texture(seprgb.inputs["Image"].links[0].from_node, "METALLICROUGHNESS")
def detect_occlusion_map(self):
for node in self.settings["material"].node_tree.nodes:
if (
node.type != "GROUP"
or not node.node_tree
or node.node_tree.name != "glTF Material Output"
or not node.inputs
or not node.inputs[0].links
):
continue
from_node = node.inputs[0].links[0].from_node
if from_node.type == "SEPRGB":
sep = from_node
if sep.inputs["Image"].links and sep.inputs["Image"].links[0].from_node.type == "TEX_IMAGE":
return self.create_surface_texture(sep.inputs["Image"].links[0].from_node, "OCCLUSION")
elif from_node.type == "TEX_IMAGE":
return self.create_surface_texture(from_node, "OCCLUSION")
def detect_diffuse_map(self, bsdf):
links = bsdf.inputs["Base Color"].links
if links and links[0].from_node.type == "TEX_IMAGE":
return self.create_surface_texture(links[0].from_node, "DIFFUSE")
def create_surface_texture(self, node, mode):
import bonsai.tool as tool
texture = self.file.create_entity(
"IfcImageTexture",
RepeatS=node.extension == "REPEAT",
RepeatT=node.extension == "REPEAT",
Mode=mode,
URLReference=tool.Blender.blender_path_to_posix(node.image.filepath),
)
self.textures.append(texture)
self.process_texture_coordinates(node, texture)
def process_texture_coordinates(self, node, texture):
if node.inputs["Vector"].links and node.inputs["Vector"].links[0].from_node.type == "TEX_COORD":
if node.inputs["Vector"].links[0].from_socket.name == "UV":
self.apply_uv_map_to_texture(texture)
elif node.inputs["Vector"].links[0].from_socket.name == "Generated":
self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD")
elif node.inputs["Vector"].links[0].from_socket.name == "Camera":
self.file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD-EYE")
elif node.inputs["Vector"].links and node.inputs["Vector"].links[0].from_node.type == "UVMAP":
self.apply_uv_map_to_texture(texture)
def apply_uv_map_to_texture(self, texture):
for uv_map in self.settings["uv_maps"]:
maps = set(uv_map.Maps or [])
maps.add(texture)
uv_map.Maps = list(maps)
@@ -0,0 +1,133 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 typing import Optional, Union
import ifcopenshell
def assign_item_style(
file: ifcopenshell.file,
item: ifcopenshell.entity_instance,
style: Optional[ifcopenshell.entity_instance],
should_use_presentation_style_assignment: bool = False,
) -> Union[ifcopenshell.entity_instance, None]:
"""Assigns a style directly to a representation item
A style may either be assigned directly to an object's representation
items, or to a material which is then associated with the object. If both
exist, then the style assigned directly to the object's representation
takes precedence. It is recommended to use materials and assign styles to
materials. However, sometimes you may want to assign colours directly to
the object representation as an override. This API function provides that
capability.
If you want to assign styles to a material instead (recommended), then
please see ifcopenshell.api.style.assign_material_style.
:param item: The IfcRepresentationItem of the object
that you want to assign styles to.
:param style: A presentation style, typically IfcSurfaceStyle. None if you
want to remove an existing style from the item.
:return: Created or existing IfcStyledItem, or None if the style was removed.
Example:
.. code:: python
# A model context is needed to store 3D geometry
model3d = ifcopenshell.api.context.add_context(model, context_type="Model")
# Specifically, we want to store body geometry
body = ifcopenshell.api.context.add_context(model,
context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d)
# Let's create a new block shaped furniture. The furniture does not have any geometry yet.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# Let's use the "3D Body" representation we created earlier to add a
# new wall-like body geometry, 5 meters long, 3 meters high, and
# 200mm thick
representation = ifcopenshell.api.geometry.add_wall_representation(model,
context=body, length=5, height=3, thickness=0.2)
# Assign our new body geometry back to our wall
ifcopenshell.api.geometry.assign_representation(model,
product=wall, representation=representation)
# Place our wall at the origin
ifcopenshell.api.geometry.edit_object_placement(model, product=wall)
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
# Create a simple grey shading colour and transparency.
ifcopenshell.api.style.add_surface_style(model,
style=style, ifc_class="IfcSurfaceStyleShading", attributes={
"SurfaceColour": { "Name": None, "Red": 0.5, "Green": 0.5, "Blue": 0.5 },
"Transparency": 0., # 0 is opaque, 1 is transparent
})
# Now specifically our wall's only item only will be coloured grey.
ifcopenshell.api.style.assign_item_style(model,
style=style, item=representation.Items[0])
"""
if not (styled_item := next(iter(item.StyledByItem), None)):
if style is None:
return
if file.schema == "IFC2X3" or should_use_presentation_style_assignment:
style = file.create_entity("IfcPresentationStyleAssignment", (style,))
return file.create_entity("IfcStyledItem", item, (style,))
styled_item_styles = styled_item.Styles
if style and styled_item_styles == (style,):
return styled_item
if file.schema == "IFC4X3":
if style is None:
file.remove(styled_item)
return
styled_item.Styles = (style,)
return styled_item
# < IFC4X3
# Can't just remove a styled item or assign a style
# since we need to remove/change the possible style assignments.
assignment = None
for style_ in styled_item_styles:
if not style_.is_a("IfcPresentationStyleAssignment"):
continue
# Remove second assignment.
if style is None or assignment:
file.remove(style_)
else:
assignment = style_
if assignment.Styles != (style,):
assignment.Styles = (style,)
if style is None:
file.remove(styled_item)
return
if assignment:
if styled_item_styles == (assignment,):
return styled_item
styled_item.Styles = (assignment,)
return styled_item
styled_item.Styles = (style,)
return styled_item
@@ -0,0 +1,247 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 typing import Any
import ifcopenshell
import ifcopenshell.api.style
import ifcopenshell.util.element
def assign_material_style(
file: ifcopenshell.file,
material: ifcopenshell.entity_instance,
style: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
should_use_presentation_style_assignment: bool = False,
) -> None:
"""Assigns a style to a material
A style may either be assigned directly to an object's representation,
or to a material which is then associated with the object. If both
exist, then the style assigned directly to the object's representation
takes precedence. It is recommended to use materials and assign styles
to materials. This API function provides that capability.
:param material: The IfcMaterial which you want to assign the style to.
:param style: The IfcPresentationStyle (typically IfcSurfaceStyle) that
you want to assign to the material. This will then be applied to all
objects that have that material.
:param context: The IfcGeometricRepresentationSubContext at which this
style should be used. Typically this is the Model BODY context.
:param should_use_presentation_style_assignment: This is a technical
detail to accomodate a bug in Revit. This should always be left as
the default of False, unless you are finding that colours aren't
showing up in Revit. In that case, set it to True, but keep in mind
that this is no longer a valid IFC. Blame Autodesk.
:return: None
Example:
.. code:: python
# A model context is needed to store 3D geometry
model3d = ifcopenshell.api.context.add_context(model, context_type="Model")
# Specifically, we want to store body geometry
body = ifcopenshell.api.context.add_context(model,
context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d)
# Let's create a new wall. The wall does not have any geometry yet.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# Let's use the "3D Body" representation we created earlier to add a
# new wall-like body geometry, 5 meters long, 3 meters high, and
# 200mm thick
representation = ifcopenshell.api.geometry.add_wall_representation(model,
context=body, length=5, height=3, thickness=0.2)
# Assign our new body geometry back to our wall
ifcopenshell.api.geometry.assign_representation(model,
product=wall, representation=representation)
# Place our wall at the origin
ifcopenshell.api.geometry.edit_object_placement(model, product=wall)
# Let's prepare a concrete material. Note that our concrete material
# does not have any colours (styles) at this point.
concrete = ifcopenshell.api.material.add_material(model, name="CON01", category="concrete")
# Assign our concrete material to our wall
ifcopenshell.api.material.assign_material(model,
products=[wall], type="IfcMaterial", material=concrete)
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
# Create a simple grey shading colour and transparency.
ifcopenshell.api.style.add_surface_style(model,
style=style, ifc_class="IfcSurfaceStyleShading", attributes={
"SurfaceColour": { "Name": None, "Red": 0.5, "Green": 0.5, "Blue": 0.5 },
"Transparency": 0., # 0 is opaque, 1 is transparent
})
# Now any element (like our wall) with a concrete material will have
# a grey colour applied.
ifcopenshell.api.style.assign_material_style(model, material=concrete, style=style, context=body)
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"material": material,
"style": style,
"context": context,
"should_use_presentation_style_assignment": should_use_presentation_style_assignment,
}
return usecase.execute()
class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
self.style = self.settings["style"]
if self.file.schema == "IFC2X3" or self.settings["should_use_presentation_style_assignment"]:
self.style = self.file.createIfcPresentationStyleAssignment([self.settings["style"]])
if self.settings["material"].HasRepresentation:
self.modify_existing_definition_representation()
else:
self.create_new_definition_representation()
# handle material constituents and shape aspects
material_constituents_names = []
for inverse in self.file.get_inverse(self.settings["material"]):
if inverse.is_a("IfcMaterialConstituent") and inverse.Name:
material_constituents_names.append(inverse.Name)
if not material_constituents_names:
return
elements = ifcopenshell.util.element.get_elements_by_material(self.file, self.settings["material"])
shape_aspects = []
for element in elements:
shape_aspects += ifcopenshell.util.element.get_shape_aspects(element)
for shape_aspect in shape_aspects:
if shape_aspect.Name not in material_constituents_names:
continue
for rep in shape_aspect.ShapeRepresentations:
ifcopenshell.api.style.assign_representation_styles(
self.file, shape_representation=rep, styles=[self.style]
)
def modify_existing_definition_representation(self):
# NOTE: while it's theoritically possible to have multiple styles per 1 material
# (either with multiple styled items or multiple styles in 1 item)
# we use an implicit convention that there is only 1 style per material.
definition_representation = self.settings["material"].HasRepresentation[0]
representation = self.get_styled_representation(definition_representation)
if representation:
items = list(representation.Items)
new_items = []
same_style_items = []
for item in items:
if not item.is_a("IfcStyledItem"):
continue
if self.has_proposed_style(item):
return
if self.has_same_style_type(item):
same_style_items.append(item)
else:
new_items.append(item)
item_to_reuse = same_style_items.pop(0) if same_style_items else None
new_items.append(self.create_styled_item(item_to_reuse))
representation.Items = new_items
for item in same_style_items:
if self.file.get_total_inverses(item) == 0:
self.file.remove(item)
else:
representations = list(definition_representation.Representations)
representations.append(self.create_styled_representation())
definition_representation.Representations = representations
def has_proposed_style(self, styled_item: ifcopenshell.entity_instance) -> bool:
style = self.settings["style"]
styles = styled_item.Styles
if style in styles:
return True
if self.file.schema != "IFC4X3":
# IfcPresentationStyleAssignment is removed in IFC4X3
for s in styles:
if s.is_a("IfcPresentationStyleAssignment"):
if style in s.Styles:
return True
return False
def has_same_style_type(self, styled_item: ifcopenshell.entity_instance) -> bool:
style = self.settings["style"]
style_class = style.is_a()
for s in styled_item.Styles:
s_class = s.is_a()
if s_class == style_class:
return True
elif s_class == "IfcPresentationStyleAssignment":
for ss in s.Styles:
if ss.is_a() == style_class:
return True
return False
def create_new_definition_representation(self):
representation = self.create_styled_representation()
definition_representation = self.file.create_entity(
"IfcMaterialDefinitionRepresentation",
**{"Representations": [representation], "RepresentedMaterial": self.settings["material"]},
)
def get_styled_representation(self, definition_representation):
representations = [
r
for r in definition_representation.Representations
if r.is_a("IfcStyledRepresentation") and r.ContextOfItems == self.settings["context"]
]
if representations:
return representations[0]
def create_styled_representation(self):
return self.file.create_entity(
"IfcStyledRepresentation",
**{
"ContextOfItems": self.settings["context"],
"RepresentationIdentifier": self.settings["context"].ContextIdentifier,
"Items": [self.create_styled_item()],
},
)
def create_styled_item(self, reuse_item=None):
if reuse_item is None:
return self.file.create_entity(
"IfcStyledItem", **{"Styles": [self.style], "Name": self.settings["style"].Name}
)
# IfcPresentationStyleAssignment we created end up not being used
# TODO: do not create IfcPresentationStyleAssignment in the first place
# as it might get removed
if reuse_item.is_a("IfcPresentationStyleAssignment") and self.style.is_a("IfcPresentationStyleAssignment"):
self.file.remove(self.style)
self.style = reuse_item
reuse_item.Styles = (self.settings["style"],)
reuse_item.Name = self.settings["style"].Name
return reuse_item
@@ -0,0 +1,204 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 typing import Any
import ifcopenshell
def assign_representation_styles(
file: ifcopenshell.file,
shape_representation: ifcopenshell.entity_instance,
styles: list[ifcopenshell.entity_instance],
replace_previous_same_type_style: bool = True,
should_use_presentation_style_assignment: bool = False,
) -> list[ifcopenshell.entity_instance]:
"""Assigns a style directly to an object representation
A style may either be assigned directly to an object's representation,
or to a material which is then associated with the object. If both
exist, then the style assigned directly to the object's representation
takes precedence. It is recommended to use materials and assign styles
to materials. However, sometimes you may want to assign colours directly
to the object representation as an override. This API function provides
that capability.
This function assigns styles in bulk in an ordered manner to every item in
the representation, so the order and total styles provided is significant.
If you want more granular control, use
:func:`ifcopenshell.api.style.assign_item_style`.
If you want to assign styles to a material instead (recommended), then
please see ifcopenshell.api.style.assign_material_style.
:param shape_representation: The IfcShapeRepresentation of the object
that you want to assign styles to. This implicitly defines the
context at which the styles should be used.
:param styles: A list of presentation styles, typically IfcSurfaceStyle.
The number of items in the list should correlate with the number of
items in the shape_representation's Items attribute. If you have
more items than styles, the last style is used.
:param replace_previous_same_type_style: Remove previously assigned styles
of the same type as currently assign style`. Defaults to `True`.
:param should_use_presentation_style_assignment: This is a technical
detail to accomodate a bug in Revit. This should always be left as
the default of False, unless you are finding that colours aren't
showing up in Revit. In that case, set it to True, but keep in mind
that this is no longer a valid IFC. Blame Autodesk.
:return: List of created IfcStyledItems
Example:
.. code:: python
# A model context is needed to store 3D geometry
model3d = ifcopenshell.api.context.add_context(model, context_type="Model")
# Specifically, we want to store body geometry
body = ifcopenshell.api.context.add_context(model,
context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d)
# Let's create a new wall. The wall does not have any geometry yet.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# Let's use the "3D Body" representation we created earlier to add a
# new wall-like body geometry, 5 meters long, 3 meters high, and
# 200mm thick
representation = ifcopenshell.api.geometry.add_wall_representation(model,
context=body, length=5, height=3, thickness=0.2)
# Assign our new body geometry back to our wall
ifcopenshell.api.geometry.assign_representation(model,
product=wall, representation=representation)
# Place our wall at the origin
ifcopenshell.api.geometry.edit_object_placement(model, product=wall)
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
# Create a simple grey shading colour and transparency.
ifcopenshell.api.style.add_surface_style(model,
style=style, ifc_class="IfcSurfaceStyleShading", attributes={
"SurfaceColour": { "Name": None, "Red": 0.5, "Green": 0.5, "Blue": 0.5 },
"Transparency": 0., # 0 is opaque, 1 is transparent
})
# Now specifically our wall only will be coloured grey.
ifcopenshell.api.style.assign_representation_styles(model,
shape_representation=representation, styles=[style])
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"shape_representation": shape_representation,
"styles": styles or [],
"replace_previous_same_type_style": replace_previous_same_type_style,
"should_use_presentation_style_assignment": should_use_presentation_style_assignment,
}
return usecase.execute()
class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
results: list[ifcopenshell.entity_instance]
def execute(self):
if not self.settings["styles"]:
return []
self.settings["styles"] = self.settings["styles"].copy()
self.results = []
use_style_assignment = self.file.schema == "IFC2X3" or self.settings["should_use_presentation_style_assignment"]
replace_previous_same_type_style = self.settings["replace_previous_same_type_style"]
for element in self.file.traverse(self.settings["shape_representation"]):
if not element.is_a("IfcShapeModel"):
continue
for item in element.Items:
if not item.is_a("IfcGeometricRepresentationItem") and not item.is_a(
"IfcTopologicalRepresentationItem"
):
continue
if self.settings["styles"]:
# If there are more items than styles, fallback to using the last style
style = self.settings["styles"].pop(0)
name = style.Name
current_style_type = style.is_a()
# item may had previous styled item
prev_styled_item = next((i for i in item.StyledByItem), None)
style_assignment = None # try to find some style assignment to reuse
if prev_styled_item is None:
if use_style_assignment:
style_assignment = self.file.createIfcPresentationStyleAssignment([style])
self.results.append(self.file.createIfcStyledItem(item, [style_assignment], name))
else:
self.results.append(self.file.createIfcStyledItem(item, [style], name))
continue
if replace_previous_same_type_style:
self.remove_same_type_styles(prev_styled_item, current_style_type, remove_item=False)
for style_ in prev_styled_item.Styles:
if style_.is_a("IfcPresentationStyleAssignment"):
if use_style_assignment and style_assignment is None:
style_assignment = style_
self.remove_same_type_styles(style_assignment, current_style_type, remove_item=False)
else:
self.remove_same_type_styles(style_assignment, current_style_type, remove_item=True)
if use_style_assignment:
if style_assignment:
style_assignment.Styles = style_assignment.Styles + (style,)
else:
style_assignment = self.file.createIfcPresentationStyleAssignment([style])
prev_styled_item.Styles = prev_styled_item.Styles + (style_assignment,)
else:
prev_styled_item.Styles = prev_styled_item.Styles + (style,)
continue
# collect previously assigned styles
assigned_styles = []
for style_ in prev_styled_item.Styles:
if style_.is_a("IfcPresentationStyleAssignment"):
if style_assignment is None:
style_assignment = style_
assigned_styles.extend(style_.Styles)
else: # IfcPresentationStyle
assigned_styles.append(style_)
if style in assigned_styles:
continue
if use_style_assignment:
if style_assignment is not None:
style_assignment.Styles = style_assignment.Styles + (style,)
else:
style_assignment = self.file.createIfcPresentationStyleAssignment([style])
prev_styled_item.Styles = prev_styled_item.Styles + (style_assignment,)
else:
prev_styled_item.Styles = prev_styled_item.Styles + (style,)
return self.results
def remove_same_type_styles(self, style_item, current_style_type: str, remove_item: bool) -> None:
styles = [s for s in style_item.Styles if s.is_a() != current_style_type]
if remove_item and not styles:
self.file.remove(style_item)
else:
style_item.Styles = styles
@@ -0,0 +1,46 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 typing import Any
import ifcopenshell
def edit_presentation_style(
file: ifcopenshell.file, style: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcPresentationStyle
For more information about the attributes and data types of an
IfcPresentationStyle, consult the IFC documentation.
:param style: The IfcPresentationStyle entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
# Change the name of the style to "Foo"
ifcopenshell.api.style.edit_presentation_style(model, style=style, attributes={"Name": "Foo"})
"""
for name, value in attributes.items():
setattr(style, name, value)
@@ -0,0 +1,138 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 typing import Any, Union
import ifcopenshell
def edit_surface_style(
file: ifcopenshell.file, style: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcPresentationItem
For more information about the attributes and data types of an
IfcPresentationItem, consult the IFC documentation.
The IfcPresentationItem is expected to be one of IfcSurfaceStyleShading,
IfcSurfaceStyleRendering, IfcSurfaceStyleWithTextures,
IfcSurfaceStyleLighting, IfcSurfaceStyleReflectance, or
IfcExternallyDefinedSurfaceStyle.
To represent a colour, a nested dictionary should be used. See the
example below.
:param style: The IfcPresentationStyle entity you want to edit
:param attributes: a dictionary of attribute names and values.
:return: None
Example:
.. code:: python
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
# Create a blank rendering style.
rendering = ifcopenshell.api.style.add_surface_style(model,
style=style, ifc_class="IfcSurfaceStyleRendering")
# Edit the attributes of the rendering style.
ifcopenshell.api.style.edit_surface_style(model,
style=rendering, attributes={
# A surface colour and transparency is still supplied for
# viewport display only. This will supersede the shading
# presentation item.
"SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 },
"Transparency": 0., # 0 is opaque, 1 is transparent
# NOTDEFINED is assumed to be a PHYSICAL (PBR) lighting
# model. In IFC4X3, you may choose PHYSICAL directly.
"ReflectanceMethod": "NOTDEFINED",
# For PBR shading, you may specify these parameters:
"DiffuseColour": { "Name": None, "Red": 0.9, "Green": 0.8, "Blue": 0.8 },
"SpecularColour": 0.1, # Metallic factor
"SpecularHighlight": {"SpecularRoughness": 0.5}, # Roughness factor
})
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(style, attributes)
class Usecase:
file: ifcopenshell.file
def execute(self, style: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
self.style = style
attribute_types: dict[str, str] = {}
for attribute in style.wrapped_data.declaration().as_entity().all_attributes():
attribute_type = attribute.type_of_attribute()
if attribute_type.as_aggregation_type() is None:
attribute_type = attribute_type.declared_type().name()
else:
# doesn't have .declared_type()
attribute_type = attribute_type.type_of_element()
attribute_types[attribute.name()] = attribute_type
for key, value in attributes.items():
attribute_class = attribute_types.get(key)
if attribute_class == "IfcColourRgb":
self.edit_colour_rgb(key, value)
elif key == "SpecularHighlight":
self.edit_specular_highlight(value)
elif attribute_class == "IfcColourOrFactor":
self.edit_colour_or_factor(key, value)
else:
setattr(style, key, value)
def edit_colour_rgb(self, name: str, value: dict[str, Any]):
if (attribute := getattr(self.style, name)) is None:
attribute = self.file.createIfcColourRgb()
setattr(self.style, name, attribute)
attribute.Name = value.get("Name", None)
attribute.Red = value["Red"]
attribute.Green = value["Green"]
attribute.Blue = value["Blue"]
def edit_colour_or_factor(self, name: str, value: Union[dict[str, Any], ifcopenshell.entity_instance, None]):
if isinstance(value, dict):
attribute = getattr(self.style, name)
if not attribute or not attribute.is_a("IfcColourRgb"):
colour = self.file.createIfcColourRgb(None, 0, 0, 0)
setattr(self.style, name, colour)
attribute = getattr(self.style, name)
attribute[1] = value["Red"]
attribute[2] = value["Green"]
attribute[3] = value["Blue"]
else: # assume it's float value for IfcNormalisedRatioMeasure or None
existing_value = getattr(self.style, name)
if existing_value and existing_value.id():
self.file.remove(existing_value)
if value is not None:
value = self.file.create_entity("IfcNormalisedRatioMeasure", value)
setattr(self.style, name, value)
def edit_specular_highlight(self, value: Union[dict[str, Any], None]) -> None:
if value is None:
self.style.SpecularHighlight = None
elif value.get("IfcSpecularExponent", None):
self.style.SpecularHighlight = self.file.createIfcSpecularExponent(value["IfcSpecularExponent"])
elif value.get("IfcSpecularRoughness", None):
self.style.SpecularHighlight = self.file.createIfcSpecularRoughness(value["IfcSpecularRoughness"])
@@ -0,0 +1,93 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 collections.abc import Iterable
import ifcopenshell.api.style
import ifcopenshell.util.element
def remove_style(file: ifcopenshell.file, style: ifcopenshell.entity_instance) -> None:
"""Removes a presentation style
All of the presentation items of the style will also be removed.
:param style: The IfcPresentationStyle to remove.
:return: None
Example:
.. code:: python
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
# Not anymore!
ifcopenshell.api.style.remove_style(model, style=style)
"""
usecase = Usecase()
usecase.file = file
return usecase.execute(style)
class Usecase:
file: ifcopenshell.file
def execute(
self, style: ifcopenshell.entity_instance, do_not_delete: Iterable[ifcopenshell.entity_instance] = ()
) -> None:
self.purge_inverses(style)
ifc_class = style.is_a()
if ifc_class == "IfcSurfaceStyle":
for style_ in style.Styles:
ifcopenshell.api.style.remove_surface_style(self.file, style=style_)
elif ifc_class == "IfcFillAreaStyle":
for style_ in style.FillStyles:
ifcopenshell.util.element.remove_deep2(
self.file, style_, also_consider=[style], do_not_delete=list(do_not_delete)
)
self.file.remove(style)
def purge_inverses(self, style: ifcopenshell.entity_instance) -> None:
for inverse in self.file.get_inverse(style):
if inverse.is_a("IfcStyledItem"):
if len(inverse.Styles) == 1:
self.purge_styled_representations(inverse)
self.file.remove(inverse)
# IfcCurveStyle -> IfcFillAreaStyleHatching.
elif inverse.is_a("IfcFillAreaStyleHatching"):
self.purge_fill_area_style_hatching(inverse, style)
def purge_styled_representations(self, styled_item: ifcopenshell.entity_instance) -> None:
for inverse in self.file.get_inverse(styled_item):
if inverse.is_a("IfcStyledRepresentation") and len(inverse.Items) == 1:
self.purge_material_definition_representations(inverse)
self.file.remove(inverse)
def purge_material_definition_representations(self, styled_representation: ifcopenshell.entity_instance) -> None:
for inverse in self.file.get_inverse(styled_representation):
if inverse.is_a("IfcMaterialDefinitionRepresentation") and len(inverse.Representations) == 1:
self.file.remove(inverse)
def purge_fill_area_style_hatching(
self, fill_area_style_hatching: ifcopenshell.entity_instance, style: ifcopenshell.entity_instance
) -> None:
for inverse in self.file.get_inverse(fill_area_style_hatching):
if inverse.is_a("IfcFillAreaStyle"):
self.execute(inverse, do_not_delete=(fill_area_style_hatching, style))
ifcopenshell.util.element.remove_deep2(self.file, fill_area_style_hatching, do_not_delete=[style])
@@ -0,0 +1,48 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def remove_styled_representation(file: ifcopenshell.file, representation: ifcopenshell.entity_instance) -> None:
"""Removes a styled representation
Styled representations are typically associated with materials. This
removes the representation but not the underlying styles.
:param representation: The IfcStyledRepresentation to remove.
:return: None
Example:
.. code:: python
# Remove a styled representation
ifcopenshell.api.style.remove_styled_representation(model, representation=representation)
"""
for inverse in file.get_inverse(representation):
if inverse.is_a("IfcMaterialDefinitionRepresentation") and len(inverse.Representations) == 1:
file.remove(inverse)
for item in representation.Items:
if item.is_a("IfcStyledItem") and file.get_total_inverses(item) == 1:
for style in item.Styles:
if style.is_a("IfcPresentationStyleAssignment"):
file.remove(style)
file.remove(item)
file.remove(representation)
@@ -0,0 +1,67 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.util.element
def remove_surface_style(file: ifcopenshell.file, style: ifcopenshell.entity_instance) -> None:
"""Removes a presentation item from a presentation style
:param style: The IfcPresentationItem to remove.
:return: None
Example:
.. code:: python
# Create a new surface style
style = ifcopenshell.api.style.add_style(model)
# Create a simple shading colour and transparency.
shading = ifcopenshell.api.style.add_surface_style(model,
style=style, ifc_class="IfcSurfaceStyleShading", attributes={
"SurfaceColour": { "Name": None, "Red": 1.0, "Green": 0.8, "Blue": 0.8 },
"Transparency": 0., # 0 is opaque, 1 is transparent
})
# Remove the shading item
ifcopenshell.api.style.remove_surface_style(model, style=shading)
"""
to_delete = set()
if style.is_a("IfcSurfaceStyleWithTextures"):
textures = style.Textures
if file.schema == "IFC2X3":
to_delete.update(textures)
else:
for texture in textures:
if coords := texture.IsMappedBy:
for coordinate in coords:
to_delete.add(coordinate)
else:
to_delete.add(texture)
for attribute in style:
if isinstance(attribute, ifcopenshell.entity_instance) and attribute.id():
to_delete.add(attribute)
file.remove(style)
for element in to_delete:
ifcopenshell.util.element.remove_deep2(file, element)
@@ -0,0 +1,101 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.style
import ifcopenshell.util.element
def unassign_material_style(
file: ifcopenshell.file,
material: ifcopenshell.entity_instance,
style: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> None:
"""Unassigns a style to a material
This does the inverse of assign_material_style.
:param material: The IfcMaterial which you want to unassign the style from.
:param style: The IfcPresentationStyle (typically IfcSurfaceStyle) that
you want to unassign from material. This will then be applied to all
objects that have that material.
:param context: The IfcGeometricRepresentationSubContext at which this
style should be unassigned. Typically this is the Model BODY context.
:return: None
Example:
.. code:: python
ifcopenshell.api.style.unassign_material_style(model, material=concrete, style=style, context=body)
"""
settings = {
"material": material,
"style": style,
"context": context,
}
for definition in settings["material"].HasRepresentation:
for representation in definition.Representations:
if not representation.is_a("IfcStyledRepresentation"):
continue
if representation.ContextOfItems != settings["context"]:
continue
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
continue
styles = []
for s in item.Styles:
if s == settings["style"]:
continue
if s.is_a("IfcPresentationStyleAssignment"):
if s.Styles == (settings["style"],):
continue
styles.append(s)
if not styles:
file.remove(item)
elif len(styles) != len(item.Styles):
item.Styles = styles
if not representation.Items:
file.remove(representation)
if not definition.Representations:
file.remove(definition)
# handle material constituents and shape aspects
material_constituents_names = []
for inverse in file.get_inverse(settings["material"]):
if inverse.is_a("IfcMaterialConstituent") and inverse.Name:
material_constituents_names.append(inverse.Name)
if not material_constituents_names:
return
elements = ifcopenshell.util.element.get_elements_by_material(file, settings["material"])
shape_aspects = []
for element in elements:
shape_aspects += ifcopenshell.util.element.get_shape_aspects(element)
for shape_aspect in shape_aspects:
if shape_aspect.Name not in material_constituents_names:
continue
for rep in shape_aspect.ShapeRepresentations:
ifcopenshell.api.style.unassign_representation_styles(
file, shape_representation=rep, styles=[settings["style"]]
)
@@ -0,0 +1,97 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# 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 typing import Any
import ifcopenshell
def unassign_representation_styles(
file: ifcopenshell.file,
shape_representation: ifcopenshell.entity_instance,
styles: list[ifcopenshell.entity_instance],
should_use_presentation_style_assignment: bool = False,
) -> None:
"""Unassigns styles directly assigned to an object representation
This does the inverse of assign_representation_styles.
:param shape_representation: The IfcShapeRepresentation of the object
that you want to unassign styles from.
:param styles: A list of presentation styles, typically IfcSurfaceStyle.
The number of items in the list should correlate with the number of
items in the shape_representation's Items attribute. If you have
more items than styles, the last style is used.
:param should_use_presentation_style_assignment: This is a technical
detail to accomodate a bug in Revit. This should always be left as
the default of False, unless you are finding that colours aren't
showing up in Revit. In that case, set it to True, but keep in mind
that this is no longer a valid IFC. Blame Autodesk.
:return: None
Example:
.. code:: python
ifcopenshell.api.style.unassign_representation_styles(model,
shape_representation=representation, styles=[style])
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"shape_representation": shape_representation,
"styles": styles or [],
"should_use_presentation_style_assignment": should_use_presentation_style_assignment,
}
return usecase.execute()
class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
if not self.settings["styles"]:
return []
self.results = []
use_style_assignment = self.file.schema == "IFC2X3" or self.settings["should_use_presentation_style_assignment"]
for element in self.file.traverse(self.settings["shape_representation"]):
if not element.is_a("IfcShapeRepresentation"):
continue
for item in element.Items:
if not item.is_a("IfcGeometricRepresentationItem"):
continue
if not item.StyledByItem:
continue
item = item.StyledByItem[0]
if use_style_assignment:
for style_ in item.Styles:
if style_.is_a("IfcPresentationStyleAssignment"):
self.remove_styles(style_)
self.remove_styles(item)
def remove_styles(self, item):
"""Removes styles from a styled item or a style assignment
and purges item if doesn't have any styles after"""
styles = [s for s in item.Styles if s not in self.settings["styles"]]
if not styles:
self.file.remove(item)
elif len(styles) != len(item.Styles):
item.Styles = styles