Files
IfcOpenShell/src/bonsai/test/tool/test_project.py
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

443 lines
16 KiB
Python
Raw Normal View History

2024-08-13 15:47:27 +10:00
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
2024-08-13 15:47:27 +10:00
# This file is part of Bonsai.
#
2024-08-13 15:47:27 +10:00
# 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.
#
2024-08-13 15:47:27 +10:00
# 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
2024-08-13 15:47:27 +10:00
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import contextlib
2026-03-04 15:41:39 +05:00
import json
2026-01-26 17:11:12 +05:00
import tempfile
from pathlib import Path
from tempfile import NamedTemporaryFile
2026-03-06 19:04:54 +05:00
from typing import cast
2026-01-26 17:11:12 +05:00
import bpy
import ifcopenshell
2025-06-09 10:59:22 +05:00
import ifcopenshell.api.context
import ifcopenshell.api.document
import ifcopenshell.api.root
2025-06-09 10:59:22 +05:00
import ifcopenshell.api.unit
import ifcpatch
2026-03-04 15:41:39 +05:00
import numpy as np
from ifcpatch.recipes import Ifc2Sql
2026-01-26 17:11:12 +05:00
import bonsai.core.tool
import bonsai.tool as tool
2024-08-14 13:56:39 +05:00
from bonsai.tool.project import Project as subject
2026-01-26 17:11:12 +05:00
from test.bim.bootstrap import NewFile
class TestImplementsTool(NewFile):
def test_run(self):
2024-08-14 13:56:39 +05:00
assert isinstance(subject(), bonsai.core.tool.Project)
class TestAppendAllTypesFromTemplate(NewFile):
def test_nothing(self):
# TODO refactor
pass
class TestCreateEmpty(NewFile):
def test_run(self):
subject.create_empty("Foobar")
assert bpy.data.objects.get("Foobar")
assert not bpy.data.objects.get("Foobar").data
class TestLoadDefaultThumbnails(NewFile):
def test_nothing(self):
2023-06-20 16:44:57 +05:00
pass # Not possible to test this headlessly
class TestRunAggregateAssignObject(NewFile):
def test_nothing(self):
pass
class TestRunContextAddContext(NewFile):
def test_nothing(self):
pass
class TestRunOwnerAddOrganisation(NewFile):
def test_nothing(self):
pass
class TestRunOwnerAddPerson(NewFile):
def test_nothing(self):
pass
class TestRunOwnerAddPersonAndOrganisation(NewFile):
def test_nothing(self):
pass
class TestRunOwnerSetUser(NewFile):
def test_nothing(self):
pass
class TestRunAssignClass(NewFile):
def test_nothing(self):
pass
class TestRunUnitAssignSceneUnits(NewFile):
def test_nothing(self):
pass
class TestSetContext(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
subject.set_context(context)
2025-02-24 11:37:19 +05:00
rprops = tool.Root.get_root_props()
assert rprops.contexts == str(context.id())
class TestSetDefaultContext(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifc.createIfcProject()
2025-06-09 10:59:22 +05:00
model = ifcopenshell.api.context.add_context(ifc, context_type="Model")
body = ifcopenshell.api.context.add_context(
2024-07-26 12:19:21 +10:00
ifc,
parent=model,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
)
subject.set_default_context()
2025-02-24 11:37:19 +05:00
rprops = tool.Root.get_root_props()
assert rprops.contexts == str(body.id())
class TestSetDefaultModelingDimensions(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifc.createIfcProject()
2025-06-09 10:59:22 +05:00
ifcopenshell.api.unit.assign_unit(ifc)
subject.set_default_modeling_dimensions()
2025-02-10 13:31:50 +05:00
props = tool.Model.get_model_props()
assert props.extrusion_depth == 3
assert props.length == 1
2022-10-19 17:54:25 +11:00
assert props.rl1 == 0
assert props.rl2 == 1
assert props.x == 0.5
assert props.y == 0.5
assert props.z == 0.5
2024-07-01 14:37:16 +05:00
class PreserveFileContents:
original_content: str
filepath: Path
def __init__(self, filepath: Path):
self.filepath = filepath
def __enter__(self):
if not self.filepath.exists():
self.filepath.parent.mkdir(parents=True, exist_ok=True)
open(self.filepath, "w").close() # touch.
with open(self.filepath, "r") as file:
self.original_content = file.read()
return self
def __exit__(self, exc_type, exc_value, traceback):
with open(self.filepath, "w") as file:
file.write(self.original_content)
class TestRecentIFCProjects(NewFile):
def test_get_recent_ifc_projects_path(self):
assert subject.get_recent_ifc_projects_path().name == "recent-ifc-projects.txt"
def test_clear_recent_ifc_projects(self):
filepath = subject.get_recent_ifc_projects_path()
with PreserveFileContents(filepath):
with open(filepath, "w") as fo:
fo.write(tempfile.NamedTemporaryFile(suffix=".ifc").name)
assert filepath.stat().st_size != 0
subject.clear_recent_ifc_projects()
assert filepath.stat().st_size == 0
def test_get_write_recent_ifc_projects(self):
filepath = subject.get_recent_ifc_projects_path()
with PreserveFileContents(filepath):
subject.clear_recent_ifc_projects()
assert filepath.stat().st_size == 0
projects: list[Path] = []
for _ in range(3):
ifc_file = Path(tempfile.NamedTemporaryFile(suffix=".ifc").name)
open(ifc_file, "w").close()
projects.append(ifc_file)
subject.write_recent_ifc_projects(projects)
assert filepath.stat().st_size != 0
assert subject.get_recent_ifc_projects() == projects
with open(filepath) as fi:
contents = fi.read()
assert contents == "\n".join(str(p) for p in projects)
def test_add_recent_ifc_project(self):
filepath = subject.get_recent_ifc_projects_path()
with PreserveFileContents(filepath):
subject.clear_recent_ifc_projects()
assert filepath.stat().st_size == 0
ifc_file = Path(tempfile.NamedTemporaryFile(suffix=".ifc").name)
subject.add_recent_ifc_project(ifc_file)
assert filepath.stat().st_size != 0
assert subject.get_recent_ifc_projects() == [ifc_file]
class TestLoadProject(NewFile):
def test_load_project_and_start_fresh_sesion(self):
filepath = Path("test/files/basic.ifc")
bpy.ops.mesh.primitive_monkey_add()
monkey = bpy.context.object
assert monkey
bpy.ops.bim.load_project(filepath=filepath.as_posix())
assert tool.Ifc.get()
assert not tool.Blender.is_valid_data_block(monkey)
def test_load_project_without_starting_fresh_sesion(self):
filepath = Path("test/files/basic.ifc")
bpy.ops.mesh.primitive_monkey_add()
monkey = bpy.context.object
assert monkey
bpy.ops.bim.load_project(filepath=filepath.as_posix(), should_start_fresh_session=False)
assert tool.Ifc.get()
assert tool.Blender.is_valid_data_block(monkey)
def test_load_project_without_ifc_data(self):
filepath = Path("test/files/basic.ifc")
bpy.ops.mesh.primitive_monkey_add()
monkey = bpy.context.object
assert monkey
bpy.ops.bim.load_project(filepath=filepath.as_posix(), import_without_ifc_data=True)
assert not tool.Ifc.get()
assert bpy.data.objects["IfcWall/Wall"]
assert not tool.Blender.is_valid_data_block(monkey)
def test_load_project_without_ifc_data_and_restarting_session(self):
filepath = Path("test/files/basic.ifc")
bpy.ops.mesh.primitive_monkey_add()
monkey = bpy.context.object
assert monkey
bpy.ops.bim.load_project(
filepath=filepath.as_posix(), import_without_ifc_data=True, should_start_fresh_session=False
)
assert not tool.Ifc.get()
assert bpy.data.objects["IfcWall/Wall"]
assert tool.Blender.is_valid_data_block(monkey)
class TestLoadLinkedModels(NewFile):
def test_load_linked_models_no_document(self):
2025-02-19 11:06:03 +05:00
props = tool.Project.get_project_props()
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
subject.load_linked_models_from_ifc()
2025-02-19 11:06:03 +05:00
assert len(props.links) == 0
def test_load_linked_models_document_no_references(self):
ifc = ifcopenshell.file()
2025-02-19 11:06:03 +05:00
props = tool.Project.get_project_props()
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
document = ifcopenshell.api.document.add_information(ifc)
document.Name = "X"
tool.Ifc.set(ifc)
subject.load_linked_models_from_ifc()
2025-02-19 11:06:03 +05:00
assert len(props.links) == 0
def test_load_linked_models_document_with_references(self):
ifc = ifcopenshell.file()
2025-02-19 11:06:03 +05:00
props = tool.Project.get_project_props()
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
document = ifcopenshell.api.document.add_information(ifc)
document.Scope = "LINKED_MODEL"
reference = ifcopenshell.api.document.add_reference(ifc, document)
reference.Location = "test.ifc"
reference.Identification = ""
reference2 = ifcopenshell.api.document.add_reference(ifc, document)
reference2.Location = "test2.ifc"
reference2.Identification = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16"
tool.Ifc.set(ifc)
subject.load_linked_models_from_ifc()
assert len(props.links) == 2
assert props.links[0].name == "test.ifc"
assert props.links[0].ifc_definition_id == reference.id()
assert props.links[0].has_transformation is False
assert props.links[1].name == "test2.ifc"
assert props.links[1].ifc_definition_id == reference2.id()
assert props.links[1].has_transformation is True
class TestCalculateLinkMatrix(NewFile):
def test_linking_a_model_without_an_offset_to_our_session_with_no_offset(self):
2025-02-19 11:06:03 +05:00
props = tool.Project.get_project_props()
gprops = tool.Georeference.get_georeference_props()
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
link = props.links.add()
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
json.dump({"model_project_north": "0", "model_origin_si": "0,0,0"}, tmp)
tmp.flush()
gprops.model_project_north = "0"
gprops.model_origin_si = "0,0,0"
assert np.allclose(subject.calculate_link_matrix(link), np.eye(4))
def test_linking_an_offset_model_to_our_session_with_no_offset(self):
props = tool.Project.get_project_props()
gprops = tool.Georeference.get_georeference_props()
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
link = props.links.add()
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
tmp.flush()
gprops.model_project_north = "0"
gprops.model_origin_si = "0,0,0"
m = np.eye(4)
m[0][3] = 5
assert np.allclose(subject.calculate_link_matrix(link), m)
def test_linking_an_offset_model_to_our_session_with_offset(self):
props = tool.Project.get_project_props()
gprops = tool.Georeference.get_georeference_props()
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
link = props.links.add()
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
tmp.flush()
gprops.model_project_north = "0"
gprops.model_origin_si = "2,0,0"
m = np.eye(4)
m[0][3] = 3
assert np.allclose(subject.calculate_link_matrix(link), m)
def test_linking_an_offset_model_to_our_session_with_offset_and_transformation(self):
props = tool.Project.get_project_props()
gprops = tool.Georeference.get_georeference_props()
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
link = props.links.add()
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
transformation = np.eye(4)
transformation[0][3] = 4
link.transformation = ",".join(map(str, transformation.reshape(-1)))
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
tmp.flush()
gprops.model_project_north = "0"
gprops.model_origin_si = "2,0,0"
m = np.eye(4)
m[0][3] = 7
assert np.allclose(subject.calculate_link_matrix(link), m)
class TestLoadingIfcSqlite(NewFile):
def test_run(self):
filepath = Path("test/files/basic.ifc")
ifc_file: ifcopenshell.file
ifc_file = ifcopenshell.open(filepath)
patcher = Ifc2Sql.Patcher(
ifc_file,
sql_type="SQLite",
)
patcher.patch()
2026-03-17 13:28:11 +05:00
tmp_file = Path(tempfile.mkstemp(suffix=".ifcsqlite")[1])
ifcpatch.write(patcher.get_output(), tmp_file)
elements_with_meshes = [
# Types.
"IfcSlabType/Slab",
"IfcWallType/Wall",
# Occurrences.
"IfcBeam/Beam",
"IfcWall/Wall",
]
elements_without_meshes = (
"IfcProject/My Project",
"IfcSite/My Site",
"IfcBuilding/My Building",
"IfcBuildingStorey/Ground Floor",
)
def clean_up() -> None:
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
ifc_file.db.close()
tmp_file.unlink(missing_ok=True)
with contextlib.ExitStack() as stack:
stack.callback(clean_up)
bpy.ops.bim.load_project(filepath=tmp_file.as_posix())
assert isinstance(tool.Ifc.get(), ifcopenshell.sqlite)
for element_name in elements_with_meshes:
assert element_name in bpy.data.objects
assert bpy.data.objects[element_name].data
for element_name in elements_without_meshes:
assert element_name in bpy.data.objects
assert not bpy.data.objects[element_name].data
2026-03-06 19:04:54 +05:00
class TestGettingLinkedElementGeomSlice:
TEST_OBJ = {
"guids": ["aaa", "bbb", "ccc"],
"guid_ids": [5, 10, 15],
}
def test_get_first_element(self):
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
obj = cast(bpy.types.Object, obj)
slice_ = subject.Link.get_linked_element_geom_slice(obj, "aaa")
assert range(15)[slice_] == range(5)
def test_get_middle_element(self):
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
obj = cast(bpy.types.Object, obj)
slice_ = subject.Link.get_linked_element_geom_slice(obj, "bbb")
assert range(15)[slice_] == range(5, 10)
def test_skip_hidden_first_element(self):
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
obj = obj | {"hidden_indices": [0]}
obj = cast(bpy.types.Object, obj)
slice_ = subject.Link.get_linked_element_geom_slice(obj, "bbb")
assert range(15)[slice_] == range(5)
def test_skip_hidden_middle_element(self):
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
obj = obj | {"hidden_indices": [1]}
obj = cast(bpy.types.Object, obj)
slice_ = subject.Link.get_linked_element_geom_slice(obj, "ccc")
assert range(15)[slice_] == range(5, 10)
def test_handle_hidden_non_first_element(self):
obj = TestGettingLinkedElementGeomSlice.TEST_OBJ
obj = obj | {"hidden_indices": [1]}
obj = cast(bpy.types.Object, obj)
slice_ = subject.Link.get_linked_element_geom_slice(obj, "aaa")
assert range(15)[slice_] == range(5)