Files
IfcOpenShell/src/bonsai/test/tool/test_model.py
T
Gorgious56 4962e3256d Promote idle-row icons into the slot system
The toggle_openings icon lived outside the IconSlot layout — each
host (wall, roof) declared an ad-hoc setup_pen_row_toggle_openings_icon
+ update_pen_row_toggle_openings_icon pair, and GizmoArrayEdition
queried a hardcoded _FEATURE_IDLE_MAX_X dict to position past it.
On an arrayed wall the dict was shadowed: find_for_element returns
"array" before "wall" in EDIT_TYPES order, the wall reservation was
never consulted, and the first per-layer ARRAY icon (local X=0.37)
landed 13cm from the wall's toggle_openings (X=0.50) — visually on
top of each other.

Promote idle-row icons into the slot system instead of patching the
dict:

* IconSlot gains an Optional visible_when predicate for state-driven
  visibility (toggle_openings only when the host carries openings).
* BaseParametricGizmoGroup gains idle_slots: ClassVar[tuple[IconSlot]]
  + _idle_slot_x_positions() + _idle_row_right_edge() helpers; the
  setup + idle-branch positioning loops mirror the existing
  feature_slots path.
* Wall and roof declare toggle_openings as an idle_slot and drop
  their ad-hoc setup/update calls.
* GizmoArrayEdition's _resolve_feature_idle_max_x walks
  BaseParametricGizmoGroup.REGISTRY and takes the max
  _idle_row_right_edge() across peers whose poll passes — no more
  hardcoded dict, no more find_for_element-order shadowing.
* setup_pen_row_toggle_openings_icon + update_pen_row_toggle_openings_icon
  helpers deleted from drawing/gizmos.py.
* 3 forward-compat AST guards pin the new contract.

Also bundles an unrelated array-test fix: TestUsingArrays in
test/tool/test_model.py was asserting against bpy.context.selected_objects
which is a fragile signal after remove_array / apply_array. A new
_array_objects() helper filters bpy.data.objects via the BIM_Array
pset's IfcActuator type instead.

Layout on an arrayed wall after the fix:
  pen        X = 0.00
  toggle     X = 0.50 (idle_slot 0)
  array[0]   X = 0.87 (one ICON_ARRAY_GAP past idle row)
  array[1]   X = 1.27
All separated by the standard inter-icon spacing.

Generated with the assistance of an AI coding tool.
2026-06-06 18:22:27 +02:00

937 lines
35 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import json
from typing import Any
import bpy
import ifcopenshell
import ifcopenshell.api.geometry
import ifcopenshell.api.material
import ifcopenshell.api.root
import ifcopenshell.api.style
import ifcopenshell.api.type
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import numpy as np
from ifcopenshell.util.shape_builder import ShapeBuilder, V
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.tool.model import Model as subject
from test.bim.bootstrap import NewFile
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(subject(), bonsai.core.tool.Model)
class TestGenerateOccurrenceName(NewFile):
def test_generating_based_on_class(self):
ifc = ifcopenshell.file()
element_type = ifc.createIfcWallType(Name="Foobar")
prefs = tool.Blender.get_addon_preferences()
with tool.Blender.preserve_prop_value(prefs, "occurrence_name_style"):
prefs.occurrence_name_style = "CLASS"
assert subject.generate_occurrence_name(element_type, "IfcWall") == "Wall"
def test_generating_based_on_type_name(self):
ifc = ifcopenshell.file()
element_type = ifc.createIfcWallType()
prefs = tool.Blender.get_addon_preferences()
with tool.Blender.preserve_prop_value(prefs, "occurrence_name_style"):
prefs.occurrence_name_style = "TYPE"
assert subject.generate_occurrence_name(element_type, "IfcWall") == "Unnamed"
element_type.Name = "Foobar"
assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar"
def test_generating_based_on_a_custom_function(self):
ifc = ifcopenshell.file()
element_type = ifc.createIfcWallType()
prefs = tool.Blender.get_addon_preferences()
with tool.Blender.preserve_prop_value(prefs, "occurrence_name_style"):
prefs.occurrence_name_style = "CUSTOM"
prefs.occurrence_name_function = '"Foobar"'
assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar"
class TestGetBooleans(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
element = ifc.createIfcWall()
items = [ifc.createIfcExtrudedAreaSolid()]
representation = ifc.createIfcShapeRepresentation(Items=items, ContextOfItems=context)
ifcopenshell.api.geometry.assign_representation(ifc, product=element, representation=representation)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc)
cut1 = builder.half_space_solid(builder.plane())
cut2 = builder.half_space_solid(builder.plane())
bools = ifcopenshell.api.geometry.add_boolean(ifc, first_item=items[0], second_items=[cut1, cut2])
assert set(subject.get_booleans(element, representation)) == set(bools)
class TestGetManualBooleans(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
element = ifc.createIfcWall()
items = [ifc.createIfcExtrudedAreaSolid()]
representation = ifc.createIfcShapeRepresentation(Items=items, ContextOfItems=context)
ifcopenshell.api.geometry.assign_representation(ifc, product=element, representation=representation)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc)
cut1 = builder.half_space_solid(builder.plane())
cut2 = builder.half_space_solid(builder.plane())
bools = ifcopenshell.api.geometry.add_boolean(ifc, first_item=items[0], second_items=[cut1, cut2])
assert set(subject.get_booleans(element, representation)) == set(bools)
assert len(subject.get_manual_booleans(element, representation)) == 0
bool1 = bools[0]
subject.mark_manual_booleans(element, [bool1])
assert set(subject.get_manual_booleans(element, representation)) == {bool1}
class TestMarkManualBooleans(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcWall()
boolean = ifc.createIfcBooleanClippingResult()
subject.mark_manual_booleans(element, [boolean])
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean")
assert pset
value = json.loads(pset["Data"])
assert set(value) == {boolean.id()}
class TestUnmarkManualBooleans(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcWall()
boolean = ifc.createIfcBooleanClippingResult()
boolean2 = ifc.createIfcBooleanClippingResult()
subject.mark_manual_booleans(element, [boolean, boolean2])
subject.unmark_manual_booleans(element, [boolean.id()])
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean")
assert pset
value = json.loads(pset["Data"])
assert set(value) == {boolean2.id()}
class TestStairCalculatedParams(NewFile):
def compare_data(self, pset_data, expected_calculated_data):
calculated_data = subject.get_active_stair_calculated_params(pset_data)
for key, value in expected_calculated_data.items():
assert tool.Cad.is_x(calculated_data[key], value)
def test_run(self):
bpy.ops.bim.create_project()
bpy.ops.mesh.add_stair()
pset_data_base = {
"number_of_treads": 3,
"height": 1.0,
"tread_run": 0.3,
"custom_first_last_tread_run": (None, None),
"nosing_length": 0.0,
}
calculated_data_base = {
"Number of Risers": 4,
"Tread Rise": 0.25,
"Length": 1.2,
}
self.compare_data(pset_data_base, calculated_data_base)
# custom first and last treads run
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.1, 0.4)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] += -0.2 + 0.1
self.compare_data(pset_data, calculated_data)
# zero-width first tread
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.0, None)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
self.compare_data(pset_data, calculated_data)
# zero-width last tread
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (None, 0.0)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
self.compare_data(pset_data, calculated_data)
# both first and last treads zero-width
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
pset_data["custom_tread_lock"] = False
calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
self.compare_data(pset_data, calculated_data)
# overlap affects stair length only by first tread
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["nosing_length"] = 0.1
calculated_data["Length"] += 0.1
self.compare_data(pset_data, calculated_data)
# tread gap
pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy()
pset_data["nosing_length"] = -0.1
calculated_data["Length"] += 0.1 * pset_data["number_of_treads"]
self.compare_data(pset_data, calculated_data)
class TestGenerateStair2DProfile(NewFile):
def compare_data(self, generated_profile, expected_profile):
verts_gen, edges_gen, faces_gen = generated_profile
verts, edges, faces = expected_profile
assert np.all(edges == np.array(edges_gen))
assert faces == tuple(tuple(face) for face in faces_gen)
for vert, vert_gen in zip(verts, verts_gen, strict=True):
assert np.allclose(vert, V(vert_gen), atol=0.01)
CONCRETE_STAIR_KWARGS: dict[str, Any] = {
"base_slab_depth": 0.25,
"has_top_nib": False,
"height": 1.0,
"number_of_treads": 3,
"stair_type": "CONCRETE",
"top_slab_depth": 0.25,
"tread_depth": 0.25,
"tread_run": 0.3,
"width": 1.2,
}
def test_create_concrete_stair(self):
kwargs = self.CONCRETE_STAIR_KWARGS.copy()
verts_data = (
V(0.0, 0, 0.0),
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.6, 0, 0.75),
V(0.9, 0, 0.75),
V(0.9, 0, 1.0),
V(1.2, 0, 1.0),
V(1.2, 0, 0.67457),
V(0.1, 0, -0.25),
V(0.0, 0, -0.25),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
(6, 7),
(7, 8),
(8, 9),
(11, 0),
(10, 11),
(9, 10),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_concrete_stair_nib(self):
kwargs = self.CONCRETE_STAIR_KWARGS.copy()
kwargs["has_top_nib"] = True
verts_data = (
V(0.0, 0, 0.0),
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.6, 0, 0.75),
V(0.9, 0, 0.75),
V(0.9, 0, 1.0),
V(1.2, 0, 1.0),
V(1.2, 0, 0.75),
V(1.3, 0, 0.75),
V(0.1, 0, -0.25),
V(0.0, 0, -0.25),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
(6, 7),
(7, 8),
(8, 9),
(9, 10),
(12, 0),
(11, 12),
(10, 11),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_concrete_stair_zero_width_first_tread(self):
kwargs = self.CONCRETE_STAIR_KWARGS.copy()
kwargs["custom_first_last_tread_run"] = (0.0, None)
verts_data = (
V(0.0, 0, 0.0),
# First tread skipped - goes straight to second tread
V(0.0, 0, 0.5),
V(0.3, 0, 0.5),
V(0.3, 0, 0.75),
V(0.6, 0, 0.75),
V(0.6, 0, 1.0),
V(0.9, 0, 1.0),
V(0.9, 0, 0.6745729),
V(0.0, 0, -0.0754271),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
(6, 7),
(8, 0),
(7, 8),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_concrete_stair_zero_width_last_tread(self):
kwargs = self.CONCRETE_STAIR_KWARGS.copy()
kwargs["custom_first_last_tread_run"] = (None, 0.0)
verts_data = (
V(0.0, 0, 0.0),
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.6, 0, 0.75),
V(0.9, 0, 0.75),
# Last tread skipped
V(0.9, 0, 0.42457),
V(0.1, 0, -0.25),
V(0.0, 0, -0.25),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
(6, 7),
(9, 0),
(8, 9),
(7, 8),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
WOOD_STEEL_STAIR_KWARGS: dict[str, Any] = {
"height": 1.0,
"number_of_treads": 3,
"stair_type": "WOOD/STEEL",
"tread_depth": 0.25,
"tread_run": 0.3,
"width": 1.2,
}
def test_create_wood_steel_stair(self):
kwargs = self.WOOD_STEEL_STAIR_KWARGS.copy()
verts_data = (
V(0.0, 0, 0.0),
V(0.3, 0, 0.0),
V(0.3, 0, 0.25),
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.6, 0, 0.25),
V(0.6, 0, 0.5),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.9, 0, 0.5),
V(0.9, 0, 0.75),
V(0.6, 0, 0.75),
V(0.9, 0, 0.75),
V(1.2, 0, 0.75),
V(1.2, 0, 1.0),
V(0.9, 0, 1.0),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 0),
(4, 5),
(5, 6),
(6, 7),
(7, 4),
(8, 9),
(9, 10),
(10, 11),
(11, 8),
(12, 13),
(13, 14),
(14, 15),
(15, 12),
)
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_wood_steel_stair_zero_width_first_tread(self):
kwargs = self.WOOD_STEEL_STAIR_KWARGS.copy()
kwargs["custom_first_last_tread_run"] = (0.0, None)
verts_data = (
# First tread skipped - start at second tread
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.3, 0, 0.5),
V(0.0, 0, 0.5),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.6, 0, 0.75),
V(0.3, 0, 0.75),
V(0.6, 0, 0.75),
V(0.9, 0, 0.75),
V(0.9, 0, 1.0),
V(0.6, 0, 1.0),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 0),
(4, 5),
(5, 6),
(6, 7),
(7, 4),
(8, 9),
(9, 10),
(10, 11),
(11, 8),
)
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_wood_steel_stair_zero_width_last_tread(self):
"""Test wood/steel stair with zero-width last tread"""
kwargs = self.WOOD_STEEL_STAIR_KWARGS.copy()
kwargs["custom_first_last_tread_run"] = (None, 0.0)
verts_data = (
V(0.0, 0, 0.0),
V(0.3, 0, 0.0),
V(0.3, 0, 0.25),
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.6, 0, 0.25),
V(0.6, 0, 0.5),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.9, 0, 0.5),
V(0.9, 0, 0.75),
V(0.6, 0, 0.75),
# Last tread skipped
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 0),
(4, 5),
(5, 6),
(6, 7),
(7, 4),
(8, 9),
(9, 10),
(10, 11),
(11, 8),
)
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
GENERIC_STAIR_KWARGS: dict[str, Any] = {
"height": 1.0,
"number_of_treads": 3,
"stair_type": "GENERIC",
"tread_run": 0.3,
"width": 1.2,
}
def test_create_generic_stair(self):
kwargs = self.GENERIC_STAIR_KWARGS.copy()
verts_data = (
V(0.0, 0, 0.0),
V(0.0, 0, 0.25),
V(0.3, 0, 0.25),
V(0.3, 0, 0.5),
V(0.6, 0, 0.5),
V(0.6, 0, 0.75),
V(0.9, 0, 0.75),
V(0.9, 0, 1.0),
V(1.2, 0, 1.0),
V(1.2, 0, 0.0),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
(6, 7),
(7, 8),
(8, 9),
(9, 0),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
def test_create_generic_stair_zero_width_treads(self):
kwargs = self.GENERIC_STAIR_KWARGS.copy()
kwargs["custom_first_last_tread_run"] = (0.0, 0.0)
verts_data = (
V(0.0, 0, 0.0),
# First tread skipped
V(0.0, 0, 0.5),
V(0.3, 0, 0.5),
V(0.3, 0, 0.75),
V(0.6, 0, 0.75),
# Last tread skipped
V(0.6, 0, 0.0),
)
edges_data = (
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 0),
)
edges_data = [e[::-1] for e in edges_data]
faces_data = ()
expected_profile = (verts_data, edges_data, faces_data)
generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile)
class TestUsingArrays(NewFile):
@staticmethod
def _array_objects() -> list[bpy.types.Object]:
return [o for o in bpy.data.objects if (e := tool.Ifc.get_entity(o)) and e.is_a("IfcActuator")]
def setup_array(self, add_second_layer=False, sync_children=False):
tool.Project.get_project_props().template_file = "0"
bpy.ops.bim.create_project()
bpy.ops.mesh.primitive_cube_add()
obj = bpy.context.active_object
assert obj
rprops = tool.Root.get_root_props()
rprops.ifc_product = "IfcElement"
bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="")
bpy.ops.bim.add_array()
bpy.ops.bim.enable_editing_array(item=0)
props = tool.Model.get_array_props(obj)
props.count = 4
props.x = 4
props.sync_children = sync_children
bpy.ops.bim.finish_editing_array()
if add_second_layer:
bpy.ops.bim.add_array()
bpy.ops.bim.enable_editing_array(item=1)
props = tool.Model.get_array_props(obj)
props.count = 3
props.y = 4
props.sync_children = sync_children
bpy.ops.bim.finish_editing_array()
def test_remove_array_last_to_first(self):
self.setup_array(add_second_layer=True)
bpy.ops.bim.remove_array(item=1)
assert len(self._array_objects()) == 4
bpy.ops.bim.remove_array(item=0)
assert len(self._array_objects()) == 1
def test_remove_array_first_to_last(self):
self.setup_array(add_second_layer=True)
bpy.ops.bim.remove_array(item=0)
assert len(bpy.context.selected_objects) == 3
bpy.ops.bim.remove_array(item=0)
assert len(bpy.context.selected_objects) == 1
def test_apply_array_1_layer(self):
self.setup_array()
bpy.ops.bim.apply_array()
objs = bpy.context.selected_objects
assert len(objs) == 4
# check BBIM_Array psets are removed
for obj in objs:
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
assert pset is None, (obj, pset)
def test_apply_array_multiple_layers(self):
self.setup_array(add_second_layer=True)
bpy.ops.bim.apply_array() # apply second layer
bpy.ops.bim.apply_array() # apply first layer
objs = self._array_objects()
assert len(objs) == 12
# check BBIM_Array psets are removed
for obj in objs:
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
assert pset is None, (obj, pset)
def test_apply_array_with_sync_children(self):
self.setup_array(sync_children=True)
bpy.ops.bim.apply_array()
objs = bpy.context.selected_objects
assert len(objs) == 4
# check BBIM_Array psets are removed
for obj in objs:
element = tool.Ifc.get_entity(obj)
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
assert pset is None, (obj, pset)
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(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:
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
return mesh
def setup_test(self, and_elements: bool = True) -> None:
props = tool.Project.get_project_props()
props.template_file = "0"
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
bpy.ops.bim.create_project()
ifc_file = tool.Ifc.get()
# Setup materials and styles.
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
assert context # Type checker.
red_material = ifcopenshell.api.material.add_material(ifc_file, "Red Material")
bpy.ops.bim.load_styles(style_type="IfcSurfaceStyle")
bpy.ops.bim.enable_adding_presentation_style()
sprops = tool.Style.get_style_props()
sprops.style_name = "Red"
bpy.ops.bim.add_presentation_style()
red_style = next(i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Red")
ifcopenshell.api.style.assign_material_style(ifc_file, red_material, red_style, context)
blue_material = ifcopenshell.api.material.add_material(ifc_file, "Blue Material")
bpy.ops.bim.enable_adding_presentation_style()
sprops.style_name = "Blue"
bpy.ops.bim.add_presentation_style()
blue_style = next(i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Blue")
ifcopenshell.api.style.assign_material_style(ifc_file, blue_material, blue_style, context)
bpy.ops.bim.enable_adding_presentation_style()
sprops.style_name = "Green"
bpy.ops.bim.add_presentation_style()
if and_elements:
self.setup_elements()
def setup_elements(self) -> None:
ifc_file = tool.Ifc.get()
blue_material = next(i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Blue Material")
blue_style = tool.Material.get_style(blue_material)
# Element type.
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
element_type_obj = bpy.data.objects["Cube"]
bpy.ops.bim.assign_class(ifc_class="IfcActuatorType", predefined_type="ELECTRICACTUATOR", userdefined_type="")
element_type = tool.Ifc.get_entity(element_type_obj)
# Setup occurrences.
relating_type_id = element_type.id()
bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id)
simple = bpy.context.active_object
simple.name = "Simple"
# Occurrence with an opening.
bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id)
with_opening = bpy.context.active_object
with_opening.name = "With Opening"
props = tool.Root.get_root_props()
props.representation_obj = with_opening
bpy.ops.bim.add_element(ifc_product="IfcFeatureElement", ifc_class="IfcOpeningElement")
# Occurrence with a material override.
bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id)
with_material = bpy.context.active_object
with_material.name = "With Material"
tool.Blender.set_objects_selection(bpy.context, active_object=with_material, selected_objects=[with_material])
ifcopenshell.api.material.assign_material(
ifc_file, products=[tool.Ifc.get_entity(with_material)], material=blue_material
)
tool.Material.ensure_material_assigned([tool.Ifc.get_entity(with_material)], material=blue_material)
assert self.get_used_styles(element_type_obj) == set()
for element in ifc_file.by_type("IfcActuator"):
obj = tool.Ifc.get_object(element)
expected = {blue_style} if obj.name == "With Material" else set()
assert self.get_used_styles(obj) == expected
def test_element_type_and_occurrences(self):
self.setup_test()
ifc_file = tool.Ifc.get()
element_type = next(ifc_file.by_type("IfcActuatorType").__iter__())
red_material = next(i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Red Material")
red_style = tool.Material.get_style(red_material)
blue_style = next(i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Blue")
ifcopenshell.api.material.assign_material(ifc_file, material=red_material, products=[element_type])
tool.Material.ensure_material_assigned([element_type], material=red_material)
assert self.get_used_styles(tool.Ifc.get_object(element_type)) == {red_style}
for element in ifc_file.by_type("IfcActuator"):
obj = tool.Ifc.get_object(element)
expected = {blue_style} if obj.name == "With Material" else {red_style}
assert self.get_used_styles(obj) == expected
ifcopenshell.api.material.unassign_material(ifc_file, products=[element_type])
tool.Material.ensure_material_unassigned([element_type])
assert self.get_used_styles(tool.Ifc.get_object(element_type)) == set()
for element in ifc_file.by_type("IfcActuator"):
obj = tool.Ifc.get_object(element)
expected = {blue_style} if obj.name == "With Material" else set()
assert self.get_used_styles(obj) == expected
def test_dont_override_exisiting_styles(self):
self.setup_test()
ifc_file = tool.Ifc.get()
element_type = next(ifc_file.by_type("IfcActuatorType").__iter__())
red_material = next(i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Red Material")
green_style = next(i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Green")
# Occurrence with a style.
element_type_obj = tool.Ifc.get_object(element_type)
with bpy.context.temp_override(selected_objects=[element_type_obj]):
bpy.ops.bim.assign_style_to_selected(style_id=green_style.id())
ifcopenshell.api.material.assign_material(ifc_file, material=red_material, products=[element_type])
tool.Material.ensure_material_assigned([element_type], material=red_material)
assert self.get_used_styles(tool.Ifc.get_object(element_type)) == {green_style}
for element in ifc_file.by_type("IfcActuator"):
obj = tool.Ifc.get_object(element)
assert self.get_used_styles(obj) == {green_style}
ifcopenshell.api.material.unassign_material(ifc_file, products=[element_type])
tool.Material.ensure_material_unassigned([element_type])
assert self.get_used_styles(tool.Ifc.get_object(element_type)) == {green_style}
for element in ifc_file.by_type("IfcActuator"):
obj = tool.Ifc.get_object(element)
assert self.get_used_styles(obj) == {green_style}
def test_assign_material_to_representation_that_has_2_items_and_1_item_has_a_style(self):
self.setup_test(and_elements=False)
ifc_file = tool.Ifc.get()
red_material = next(i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Red Material")
red_style = tool.Material.get_style(red_material)
green_style = next(i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Green")
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
obj = bpy.data.objects["Cube"]
bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="")
element = tool.Ifc.get_entity(obj)
builder = ShapeBuilder(ifc_file)
# Change representation that consists of 2 rep items:
# 1 with style and other without.
rep = tool.Geometry.get_active_representation(obj)
assert rep
cube = rep.Items[0]
cube2 = builder.deep_copy(cube)
rep.Items = [cube, cube2]
tool.Style.assign_style_to_representation_item(cube, green_style)
tool.Geometry._reload_representation(obj)
def get_material_indices(mesh: bpy.types.Mesh) -> np.ndarray:
buffer = np.empty(len(mesh.polygons), dtype="I")
mesh.polygons.foreach_get("material_index", buffer)
return buffer
mesh = self.get_mesh(obj)
assert len(mesh.materials) == 2
assert set(mesh.materials) == {bpy.data.materials["Green"], None}
ifcopenshell.api.material.assign_material(ifc_file, products=[element], material=red_material)
tool.Material.ensure_material_assigned([element], material=red_material)
assert self.get_used_styles(obj) == {green_style, red_style}
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
tool.Material.ensure_material_unassigned([element])
mesh = self.get_mesh(obj)
assert len(mesh.materials) == 2
assert set(mesh.materials) == {bpy.data.materials["Green"], None}
# Test that if style is the same it would just reuse it.
tool.Style.assign_style_to_representation_item(cube, red_style)
tool.Geometry._reload_representation(obj)
mesh = self.get_mesh(obj)
assert len(mesh.materials) == 2
assert set(mesh.materials) == {bpy.data.materials["Red"], None}
ifcopenshell.api.material.assign_material(ifc_file, products=[element], material=red_material)
tool.Material.ensure_material_assigned([element], material=red_material)
mesh = self.get_mesh(obj)
assert mesh.materials[:] == [bpy.data.materials["Red"]]
# All polygons are just reassigned to the existing material.
assert set(get_material_indices(mesh)) == {mesh.materials.find("Red")}
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
tool.Material.ensure_material_unassigned([element])
mesh = self.get_mesh(obj)
assert len(mesh.materials) == 2
assert set(mesh.materials) == {bpy.data.materials["Red"], None}
assert set(get_material_indices(mesh)) == {0, 1}
def test_assign_unassign_overriding_occurrence_material(self):
self.setup_test(and_elements=True)
ifc_file = tool.Ifc.get()
element_type = next(ifc_file.by_type("IfcActuatorType").__iter__())
red_material = next(i for i in ifc_file.by_type("IfcMaterial") if i.Name == "Red Material")
no_style_material = ifcopenshell.api.material.add_material(ifc_file, "No Style")
obj = bpy.data.objects["Simple"]
element = tool.Ifc.get_entity(obj)
ifcopenshell.api.material.assign_material(ifc_file, material=red_material, products=[element_type])
tool.Material.ensure_material_assigned([element_type], material=red_material)
# Override type material.
ifcopenshell.api.material.assign_material(ifc_file, material=no_style_material, products=[element])
tool.Material.ensure_material_assigned([element], material=no_style_material)
assert self.get_mesh(obj).materials[:] == []
ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
tool.Material.ensure_material_unassigned([element])
assert self.get_mesh(obj).materials[:] == [bpy.data.materials["Red"]]
class TestOffsetWall(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall_type = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWallType", name="WAL01")
material_set = ifcopenshell.api.material.add_material_set(ifc, set_type="IfcMaterialLayerSet")
material = ifcopenshell.api.material.add_material(ifc, name="PB01", category="gypsum")
layer = ifcopenshell.api.material.add_layer(ifc, layer_set=material_set, material=material)
ifcopenshell.api.material.edit_layer(ifc, layer=layer, attributes={"LayerThickness": 100})
ifcopenshell.api.material.assign_material(ifc, products=[wall_type], material=material_set)
wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
ifcopenshell.api.type.assign_type(ifc, related_objects=[wall], relating_type=wall_type)
rel = ifcopenshell.api.material.assign_material(ifc, products=[wall], type="IfcMaterialLayerSetUsage")
usage = rel.RelatingMaterial
obj = bpy.data.objects.new("Wall", None)
tool.Ifc.link(wall, obj)
usage.DirectionSense = "POSITIVE"
subject.offset_wall(obj, "CENTER")
assert usage.OffsetFromReferenceLine == -50
usage.DirectionSense = "NEGATIVE"
subject.offset_wall(obj, "CENTER")
assert usage.OffsetFromReferenceLine == 50
usage.DirectionSense = "POSITIVE"
subject.offset_wall(obj, "INTERIOR")
assert usage.OffsetFromReferenceLine == -100
usage.DirectionSense = "NEGATIVE"
subject.offset_wall(obj, "INTERIOR")
assert usage.OffsetFromReferenceLine == 0
usage.DirectionSense = "POSITIVE"
subject.offset_wall(obj, "EXTERIOR")
assert usage.OffsetFromReferenceLine == 0
usage.DirectionSense = "NEGATIVE"
subject.offset_wall(obj, "EXTERIOR")
assert usage.OffsetFromReferenceLine == 100