2024-08-13 15:47:27 +10:00
|
|
|
# Bonsai - OpenBIM Blender Add-on
|
2022-02-03 18:25:03 +11:00
|
|
|
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
|
|
|
|
#
|
2024-08-13 15:47:27 +10:00
|
|
|
# This file is part of Bonsai.
|
2022-02-03 18:25:03 +11:00
|
|
|
#
|
2024-08-13 15:47:27 +10:00
|
|
|
# Bonsai is free software: you can redistribute it and/or modify
|
2022-02-03 18:25:03 +11:00
|
|
|
# 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,
|
2022-02-03 18:25:03 +11:00
|
|
|
# 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/>.
|
2022-02-03 18:25:03 +11:00
|
|
|
|
2025-09-05 12:36:03 +05:00
|
|
|
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
|
2026-02-15 09:28:43 +01:00
|
|
|
from tempfile import NamedTemporaryFile
|
2026-03-06 19:04:54 +05:00
|
|
|
from typing import cast
|
2026-01-26 17:11:12 +05:00
|
|
|
|
2022-02-03 18:25:03 +11:00
|
|
|
import bpy
|
|
|
|
|
import ifcopenshell
|
2025-06-09 10:59:22 +05:00
|
|
|
import ifcopenshell.api.context
|
2024-12-26 16:37:49 +05:00
|
|
|
import ifcopenshell.api.document
|
|
|
|
|
import ifcopenshell.api.root
|
2025-06-09 10:59:22 +05:00
|
|
|
import ifcopenshell.api.unit
|
2025-09-05 12:36:03 +05:00
|
|
|
import ifcpatch
|
2026-03-04 15:41:39 +05:00
|
|
|
import numpy as np
|
2025-09-05 12:36:03 +05:00
|
|
|
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
|
2022-02-03 18:25:03 +11:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestImplementsTool(NewFile):
|
|
|
|
|
def test_run(self):
|
2024-08-14 13:56:39 +05:00
|
|
|
assert isinstance(subject(), bonsai.core.tool.Project)
|
2022-02-03 18:25:03 +11:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2022-10-15 11:06:09 +11:00
|
|
|
class TestLoadDefaultThumbnails(NewFile):
|
|
|
|
|
def test_nothing(self):
|
2023-06-20 16:44:57 +05:00
|
|
|
pass # Not possible to test this headlessly
|
2022-10-15 11:06:09 +11:00
|
|
|
|
|
|
|
|
|
2022-02-03 18:25:03 +11:00
|
|
|
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())
|
2022-10-15 11:06:09 +11:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
)
|
2022-10-15 11:06:09 +11:00
|
|
|
subject.set_default_context()
|
2025-02-24 11:37:19 +05:00
|
|
|
rprops = tool.Root.get_root_props()
|
|
|
|
|
assert rprops.contexts == str(body.id())
|
2022-10-15 11:06:09 +11:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2022-10-15 11:06:09 +11:00
|
|
|
subject.set_default_modeling_dimensions()
|
2025-02-10 13:31:50 +05:00
|
|
|
props = tool.Model.get_model_props()
|
2023-05-01 13:46:24 +05:00
|
|
|
assert props.extrusion_depth == 3
|
|
|
|
|
assert props.length == 1
|
2022-10-19 17:54:25 +11:00
|
|
|
assert props.rl1 == 0
|
2023-07-20 19:33:13 +05:00
|
|
|
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]
|
2024-09-13 11:34:39 +05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2024-12-26 16:37:49 +05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
2024-12-26 16:37:49 +05:00
|
|
|
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
|
2024-12-26 16:37:49 +05:00
|
|
|
|
|
|
|
|
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()
|
2024-12-26 16:37:49 +05:00
|
|
|
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
|
|
|
|
document = ifcopenshell.api.document.add_information(ifc)
|
2026-02-15 09:28:43 +01:00
|
|
|
document.Name = "X"
|
2024-12-26 16:37:49 +05:00
|
|
|
tool.Ifc.set(ifc)
|
|
|
|
|
subject.load_linked_models_from_ifc()
|
2025-02-19 11:06:03 +05:00
|
|
|
assert len(props.links) == 0
|
2024-12-26 16:37:49 +05:00
|
|
|
|
|
|
|
|
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()
|
2024-12-26 16:37:49 +05:00
|
|
|
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
|
|
|
|
document = ifcopenshell.api.document.add_information(ifc)
|
2026-02-15 09:28:43 +01:00
|
|
|
document.Scope = "LINKED_MODEL"
|
2024-12-26 16:37:49 +05:00
|
|
|
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
2026-02-15 09:28:43 +01:00
|
|
|
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"
|
2024-12-26 16:37:49 +05:00
|
|
|
tool.Ifc.set(ifc)
|
|
|
|
|
subject.load_linked_models_from_ifc()
|
2026-02-15 09:28:43 +01:00
|
|
|
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
|
2024-12-26 16:37:49 +05:00
|
|
|
|
2026-07-02 12:25:32 +02:00
|
|
|
def test_load_linked_models_restores_query_from_cache_json(self):
|
|
|
|
|
"""The selector query used at link time is persisted only in the
|
|
|
|
|
sidecar cache JSON. Reopening the host IFC must restore it onto the
|
|
|
|
|
Link PropertyGroup so subsequent Reload/Load replay the same filter."""
|
|
|
|
|
ifc = ifcopenshell.file()
|
|
|
|
|
props = tool.Project.get_project_props()
|
|
|
|
|
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
|
|
|
|
document = ifcopenshell.api.document.add_information(ifc)
|
|
|
|
|
document.Scope = "LINKED_MODEL"
|
|
|
|
|
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=False) as tmp:
|
|
|
|
|
json.dump({"query": "IfcElement, ! IfcOpeningElement"}, tmp)
|
|
|
|
|
json_path = Path(tmp.name)
|
|
|
|
|
try:
|
|
|
|
|
ifc_filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
|
|
|
|
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
|
|
|
|
reference.Location = Path(ifc_filepath).as_posix()
|
|
|
|
|
reference.Identification = ""
|
|
|
|
|
tool.Ifc.set(ifc)
|
|
|
|
|
subject.load_linked_models_from_ifc()
|
|
|
|
|
assert len(props.links) == 1
|
|
|
|
|
assert props.links[0].query == "IfcElement, ! IfcOpeningElement"
|
|
|
|
|
finally:
|
|
|
|
|
json_path.unlink(missing_ok=True)
|
|
|
|
|
|
|
|
|
|
def test_load_linked_models_query_defaults_empty_without_cache_json(self):
|
|
|
|
|
"""When no sidecar cache JSON exists, the restored Link's query field
|
|
|
|
|
must default to the empty string. Empty query is the documented signal
|
|
|
|
|
for the load path to apply no selector filter."""
|
|
|
|
|
ifc = ifcopenshell.file()
|
|
|
|
|
props = tool.Project.get_project_props()
|
|
|
|
|
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
|
|
|
|
document = ifcopenshell.api.document.add_information(ifc)
|
|
|
|
|
document.Scope = "LINKED_MODEL"
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
|
ifc_path = Path(tmpdir) / "no-cache.ifc"
|
|
|
|
|
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
|
|
|
|
reference.Location = ifc_path.as_posix()
|
|
|
|
|
reference.Identification = ""
|
|
|
|
|
tool.Ifc.set(ifc)
|
|
|
|
|
subject.load_linked_models_from_ifc()
|
|
|
|
|
assert len(props.links) == 1
|
|
|
|
|
assert props.links[0].query == ""
|
|
|
|
|
|
2024-12-26 16:37:49 +05:00
|
|
|
|
2026-02-15 09:28:43 +01:00
|
|
|
class TestCalculateLinkMatrix(NewFile):
|
2026-07-02 12:25:32 +02:00
|
|
|
def _write_cache_json(self, payload: dict) -> Path:
|
|
|
|
|
"""Write ``payload`` to a fresh sidecar cache JSON path and return it.
|
|
|
|
|
|
|
|
|
|
On Windows, ``NamedTemporaryFile(delete=True)`` holds an exclusive
|
|
|
|
|
handle for the ``with`` block's duration, so the code-under-test
|
|
|
|
|
cannot open the same path — hence the manual write + unlink pattern.
|
|
|
|
|
"""
|
|
|
|
|
tmp = NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=False)
|
|
|
|
|
try:
|
|
|
|
|
json.dump(payload, tmp)
|
|
|
|
|
finally:
|
|
|
|
|
tmp.close()
|
|
|
|
|
return Path(tmp.name)
|
|
|
|
|
|
2026-02-15 09:28:43 +01:00
|
|
|
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()
|
2026-02-15 09:28:43 +01:00
|
|
|
gprops = tool.Georeference.get_georeference_props()
|
2026-07-02 12:25:32 +02:00
|
|
|
json_path = self._write_cache_json({"model_project_north": "0", "model_origin_si": "0,0,0"})
|
|
|
|
|
try:
|
2026-02-15 09:28:43 +01:00
|
|
|
link = props.links.add()
|
2026-07-02 12:25:32 +02:00
|
|
|
link.filepath = str(json_path).replace(".ifc.cache.json", ".ifc")
|
2026-02-15 09:28:43 +01:00
|
|
|
gprops.model_project_north = "0"
|
|
|
|
|
gprops.model_origin_si = "0,0,0"
|
|
|
|
|
assert np.allclose(subject.calculate_link_matrix(link), np.eye(4))
|
2026-07-02 12:25:32 +02:00
|
|
|
finally:
|
|
|
|
|
json_path.unlink(missing_ok=True)
|
2026-02-15 09:28:43 +01:00
|
|
|
|
|
|
|
|
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()
|
2026-07-02 12:25:32 +02:00
|
|
|
json_path = self._write_cache_json({"model_project_north": "0", "model_origin_si": "5,0,0"})
|
|
|
|
|
try:
|
2026-02-15 09:28:43 +01:00
|
|
|
link = props.links.add()
|
2026-07-02 12:25:32 +02:00
|
|
|
link.filepath = str(json_path).replace(".ifc.cache.json", ".ifc")
|
2026-02-15 09:28:43 +01:00
|
|
|
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)
|
2026-07-02 12:25:32 +02:00
|
|
|
finally:
|
|
|
|
|
json_path.unlink(missing_ok=True)
|
2026-02-15 09:28:43 +01:00
|
|
|
|
|
|
|
|
def test_linking_an_offset_model_to_our_session_with_offset(self):
|
|
|
|
|
props = tool.Project.get_project_props()
|
|
|
|
|
gprops = tool.Georeference.get_georeference_props()
|
2026-07-02 12:25:32 +02:00
|
|
|
json_path = self._write_cache_json({"model_project_north": "0", "model_origin_si": "5,0,0"})
|
|
|
|
|
try:
|
2026-02-15 09:28:43 +01:00
|
|
|
link = props.links.add()
|
2026-07-02 12:25:32 +02:00
|
|
|
link.filepath = str(json_path).replace(".ifc.cache.json", ".ifc")
|
2026-02-15 09:28:43 +01:00
|
|
|
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)
|
2026-07-02 12:25:32 +02:00
|
|
|
finally:
|
|
|
|
|
json_path.unlink(missing_ok=True)
|
2026-02-15 09:28:43 +01:00
|
|
|
|
|
|
|
|
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()
|
2026-07-02 12:25:32 +02:00
|
|
|
json_path = self._write_cache_json({"model_project_north": "0", "model_origin_si": "5,0,0"})
|
|
|
|
|
try:
|
2026-02-15 09:28:43 +01:00
|
|
|
link = props.links.add()
|
2026-07-02 12:25:32 +02:00
|
|
|
link.filepath = str(json_path).replace(".ifc.cache.json", ".ifc")
|
2026-02-15 09:28:43 +01:00
|
|
|
transformation = np.eye(4)
|
|
|
|
|
transformation[0][3] = 4
|
|
|
|
|
link.transformation = ",".join(map(str, transformation.reshape(-1)))
|
|
|
|
|
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)
|
2026-07-02 12:25:32 +02:00
|
|
|
finally:
|
|
|
|
|
json_path.unlink(missing_ok=True)
|
2025-09-05 12:36:03 +05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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])
|
2025-09-05 12:36:03 +05:00
|
|
|
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)
|