From a4566797fed1f6e58e353faf6780e9758bb19b9d Mon Sep 17 00:00:00 2001 From: Andrea Ghensi Date: Sat, 14 Jan 2023 15:49:23 +0100 Subject: [PATCH] fix: ifcclash bcfxml export --- src/bcf/requirements-dev.txt | 2 +- src/bcf/src/bcf/geometry.py | 44 ++++++++++++----- src/bcf/src/bcf/v2/bcfxml.py | 14 +++--- src/bcf/src/bcf/v2/topic.py | 30 +++++++++--- src/bcf/src/bcf/v2/visinfo.py | 57 ++++++++++++++++++++-- src/bcf/src/bcf/v3/bcfxml.py | 8 ++-- src/bcf/src/bcf/v3/topic.py | 34 +++++++++---- src/bcf/src/bcf/v3/visinfo.py | 57 ++++++++++++++++++++-- src/bcf/tests/v2/test_bcf_xml.py | 40 +++++++++------- src/bcf/tests/v2/test_ifcclash_api.py | 36 ++++++++++++++ src/bcf/tests/v3/test_bcf_xml.py | 40 +++++++--------- src/bcf/tests/v3/test_ifcclash_api.py | 36 ++++++++++++++ src/ifcclash/ifcclash/ifcclash.py | 69 ++++----------------------- 13 files changed, 316 insertions(+), 151 deletions(-) create mode 100644 src/bcf/tests/v2/test_ifcclash_api.py create mode 100644 src/bcf/tests/v3/test_ifcclash_api.py diff --git a/src/bcf/requirements-dev.txt b/src/bcf/requirements-dev.txt index 12033556c7..b895025a91 100644 --- a/src/bcf/requirements-dev.txt +++ b/src/bcf/requirements-dev.txt @@ -3,5 +3,5 @@ mypy pylint isort xsdata -tox +tox==3.27.1 tox-conda diff --git a/src/bcf/src/bcf/geometry.py b/src/bcf/src/bcf/geometry.py index 130befcdb7..92759f9b99 100644 --- a/src/bcf/src/bcf/geometry.py +++ b/src/bcf/src/bcf/geometry.py @@ -1,8 +1,10 @@ +from typing import Optional + import numpy as np from numpy.typing import NDArray -def calc_camera_vectors( +def camera_vectors_from_element_placement( elem_placement: NDArray[np.float_], ) -> tuple[NDArray[np.float_], NDArray[np.float_], NDArray[np.float_]]: """ @@ -15,19 +17,37 @@ def calc_camera_vectors( Camera position, direction and up vectors """ target_position = elem_placement[:3, 3] - camera_position = target_position + np.array((5, 5, 5)) - camera_direction = unit_vector(camera_position - target_position) + return camera_vectors_from_target_position(target_position) + + +def camera_vectors_from_target_position( + target_position: NDArray[np.float_], offset: Optional[NDArray[np.float_]] = None +) -> tuple[NDArray[np.float_], NDArray[np.float_], NDArray[np.float_]]: + """ + Calculate the vectors of a camera pointing to a target point. + + Args: + target_position: point the camera is pointing to. + camera_offset: offset of the camera from the target point. + + Returns: + Camera position, direction and up vectors + """ + camera_offset = np.array((5, 5, 5)) if offset is None else offset + camera_position = target_position + camera_offset + camera_direction = unit_vector(-camera_offset) # pylint: disable=invalid-unary-operand-type camera_right = unit_vector(np.cross(np.array([0.0, 0.0, 1.0]), camera_direction)) camera_up = unit_vector(np.cross(camera_direction, camera_right)) - rotation_transform = np.eye(4) - rotation_transform[0, :3] = camera_right - rotation_transform[1, :3] = camera_up - rotation_transform[2, :3] = camera_direction - translation_transform = np.eye(4) - translation_transform[:3, -1] = -camera_position - look_at_transform = np.matmul(rotation_transform, translation_transform) - mat = np.linalg.inv(look_at_transform) - return camera_position, -mat[:3, 2], mat[:3, 1] + return camera_position, camera_direction, camera_up + # rotation_transform = np.eye(4) + # rotation_transform[0, :3] = camera_right + # rotation_transform[1, :3] = camera_up + # rotation_transform[2, :3] = camera_direction + # translation_transform = np.eye(4) + # translation_transform[:3, -1] = -camera_position + # look_at_transform = np.matmul(rotation_transform, translation_transform) + # mat = np.linalg.inv(look_at_transform) + # return camera_position, -mat[:3, 2], mat[:3, 1] def unit_vector(v: NDArray[np.float_]) -> NDArray[np.float_]: diff --git a/src/bcf/src/bcf/v2/bcfxml.py b/src/bcf/src/bcf/v2/bcfxml.py index 4702526379..de71f65937 100644 --- a/src/bcf/src/bcf/v2/bcfxml.py +++ b/src/bcf/src/bcf/v2/bcfxml.py @@ -3,7 +3,7 @@ import uuid import warnings import zipfile from pathlib import Path -from typing import Any, Optional, TypeVar +from typing import Any, NoReturn, Optional, TypeVar import bcf.v2.model as mdl from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface @@ -50,7 +50,7 @@ class BcfXml: self._version = ( self._xml_handler.parse(self._zip_file.read("bcf.version"), mdl.Version) if self._zip_file - else mdl.Version(version_id="3.0") + else mdl.Version(version_id="2.1") ) return self._version @@ -210,10 +210,12 @@ class BcfXml: self.topics[topic_handler.guid] = topic_handler return topic_handler - def __eq__(self, other: object) -> bool: - if not isinstance(other, BcfXml): - raise TypeError("Equality needs a BcfXml object.") - return self.version == other.version and self.project_info == other.project_info + def __eq__(self, other: object) -> bool | NoReturn: + return ( + self.version == other.version and self.project_info == other.project_info + if isinstance(other, BcfXml) + else NotImplemented + ) # region Deprecated methods def new_project(self) -> "BcfXml": diff --git a/src/bcf/src/bcf/v2/topic.py b/src/bcf/src/bcf/v2/topic.py index 0180427350..8587522ec0 100644 --- a/src/bcf/src/bcf/v2/topic.py +++ b/src/bcf/src/bcf/v2/topic.py @@ -3,9 +3,11 @@ import datetime import uuid import zipfile from pathlib import Path -from typing import Any, Optional +from typing import Any, NoReturn, Optional +import numpy as np from ifcopenshell import entity_instance +from numpy.typing import NDArray from xsdata.models.datatype import XmlDateTime import bcf.v2.model as mdl @@ -226,15 +228,29 @@ class TopicHandler: new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler) self.add_visinfo_handler(new_viewpoint) + def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float_], *guids: str) -> None: + """ + Add a viewpoint tergeting an IFC element to the topic. + + Args: + element: The IFC element. + """ + vi_handler = VisualizationInfoHandler.create_from_point_and_guids( + position, *guids, xml_handler=self._xml_handler + ) + self.add_visinfo_handler(vi_handler) + def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None: self.viewpoints[new_viewpoint.guid] = new_viewpoint self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid)) - def __eq__(self, other: object) -> bool: - if not isinstance(other, TopicHandler): - raise TypeError("Equality needs a BcfXml object.") + def __eq__(self, other: object) -> bool | NoReturn: return ( - self.markup == other.markup - and self.viewpoints == other.viewpoints - and self.bim_snippet == other.bim_snippet + ( + self.markup == other.markup + and self.viewpoints == other.viewpoints + and self.bim_snippet == other.bim_snippet + ) + if isinstance(other, TopicHandler) + else NotImplemented ) diff --git a/src/bcf/src/bcf/v2/visinfo.py b/src/bcf/src/bcf/v2/visinfo.py index 1a15086b99..65754e9497 100644 --- a/src/bcf/src/bcf/v2/visinfo.py +++ b/src/bcf/src/bcf/v2/visinfo.py @@ -9,7 +9,10 @@ from ifcopenshell.util import placement from numpy.typing import NDArray import bcf.v2.model as mdl -from bcf.geometry import calc_camera_vectors +from bcf.geometry import ( + camera_vectors_from_element_placement, + camera_vectors_from_target_position, +) from bcf.inmemory_zipfile import ZipFileInterface from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer @@ -169,6 +172,29 @@ class VisualizationInfoHandler: xml_handler = xml_handler or XmlParserSerializer() return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler) + @classmethod + def create_from_point_and_guids( + cls, + position: NDArray[np.float_], + *guids: str, + xml_handler: Optional[AbstractXmlParserSerializer] = None, + ) -> "VisualizationInfoHandler": + """ + Create a new VisualizationInfoHandler object from an IFC element. + + Args: + position: target point coordinates. + *guids: One or more IFC element GUID. + xml_handler: The XML handler to use. + + Returns: + The VisualizationInfoHandler object. + """ + xml_handler = xml_handler or XmlParserSerializer() + return cls( + visualization_info=build_viewpoint_from_position_and_guids(position, *guids), xml_handler=xml_handler + ) + @lru_cache(maxsize=None) def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: @@ -192,18 +218,39 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: ) -def build_components(guid: str) -> mdl.Components: +def build_viewpoint_from_position_and_guids(position: NDArray[np.float_], *guids: str) -> mdl.VisualizationInfo: + """ + Return a BCF viewpoint of an IFC element. + + This function is cached to speedudp the creation of multiple BCF topics regarding the same element. + + Args: + position: target point coordinates. + *guids: One or more IFC element GUID. + + Returns: + The BCF viewpoint definition. + """ + return mdl.VisualizationInfo( + guid=str(uuid.uuid4()), + components=build_components(*guids), + perspective_camera=build_camera_from_vectors(*camera_vectors_from_target_position(position)), + ) + + +def build_components(*guids: str) -> mdl.Components: """ Return the BCF components from an IFC element GUID. Args: - guid: The IFC element GUID. + *guids: One or more IFC element GUID. Returns: The BCF components definition. """ + components = [mdl.Component(ifc_guid=guid) for guid in guids] return mdl.Components( - selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]), + selection=mdl.ComponentSelection(component=components), visibility=mdl.ComponentVisibility(default_visibility=True), ) @@ -218,7 +265,7 @@ def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera: Returns: The BCF camera definition. """ - return build_camera_from_vectors(*calc_camera_vectors(elem_placement)) + return build_camera_from_vectors(*camera_vectors_from_element_placement(elem_placement)) def build_camera_from_vectors( diff --git a/src/bcf/src/bcf/v3/bcfxml.py b/src/bcf/src/bcf/v3/bcfxml.py index a2a81199ec..481a7557fa 100644 --- a/src/bcf/src/bcf/v3/bcfxml.py +++ b/src/bcf/src/bcf/v3/bcfxml.py @@ -3,7 +3,7 @@ import uuid import warnings import zipfile from pathlib import Path -from typing import Any, Optional, TypeVar +from typing import Any, NoReturn, Optional, TypeVar import bcf.v3.model as mdl from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface @@ -224,13 +224,13 @@ class BcfXml: self.topics[topic_handler.guid] = topic_handler return topic_handler - def __eq__(self, other: object) -> bool: - if not isinstance(other, BcfXml): - raise TypeError("Equality needs a BcfXml object.") + def __eq__(self, other: object) -> bool | NoReturn: return ( self.version == other.version and self.project_info == other.project_info and self.extensions == other.extensions + if isinstance(other, BcfXml) + else NotImplemented ) # region Deprecated methods diff --git a/src/bcf/src/bcf/v3/topic.py b/src/bcf/src/bcf/v3/topic.py index c3a90f7a1b..bc77fd448e 100644 --- a/src/bcf/src/bcf/v3/topic.py +++ b/src/bcf/src/bcf/v3/topic.py @@ -3,9 +3,11 @@ import datetime import uuid import zipfile from pathlib import Path -from typing import Any, Optional +from typing import Any, NoReturn, Optional +import numpy as np from ifcopenshell import entity_instance +from numpy.typing import NDArray from xsdata.models.datatype import XmlDateTime import bcf.v3.model as mdl @@ -73,7 +75,7 @@ class TopicHandler: self._bim_snippet = value @property - def viewpoints(self) -> Optional[VisualizationInfoHandler]: + def viewpoints(self) -> dict[str, "VisualizationInfoHandler"]: if ( not self._viewpoints and self._topic_dir @@ -143,7 +145,7 @@ class TopicHandler: self._save_viewpoints(destination_zip, topic_dir) self._save_bim_snippet(destination_zip) - def _save_viewpoints(self, destination_zip, topic_dir) -> None: + def _save_viewpoints(self, destination_zip: ZipFileInterface, topic_dir: str) -> None: if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point): return for vpt in viewpoints: @@ -172,17 +174,31 @@ class TopicHandler: new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler) self.add_visinfo_handler(new_viewpoint) + def add_viewpoint_from_point_and_guids(self, position: NDArray[np.float_], *guids: str) -> None: + """ + Add a viewpoint tergeting an IFC element to the topic. + + Args: + element: The IFC element. + """ + vi_handler = VisualizationInfoHandler.create_from_point_and_guids( + position, *guids, xml_handler=self._xml_handler + ) + self.add_visinfo_handler(vi_handler) + def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None: self.viewpoints[new_viewpoint.guid] = new_viewpoint if self.topic.viewpoints is None: self.topic.viewpoints = mdl.TopicViewpoints() self.topic.viewpoints.view_point.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid)) - def __eq__(self, other: object) -> bool: - if not isinstance(other, TopicHandler): - raise TypeError("Equality needs a BcfXml object.") + def __eq__(self, other: object) -> bool | NoReturn: return ( - self.markup == other.markup - and self.viewpoints == other.viewpoints - and self.bim_snippet == other.bim_snippet + ( + self.markup == other.markup + and self.viewpoints == other.viewpoints + and self.bim_snippet == other.bim_snippet + ) + if isinstance(other, TopicHandler) + else NotImplemented ) diff --git a/src/bcf/src/bcf/v3/visinfo.py b/src/bcf/src/bcf/v3/visinfo.py index 6c1f79de27..56c1692206 100644 --- a/src/bcf/src/bcf/v3/visinfo.py +++ b/src/bcf/src/bcf/v3/visinfo.py @@ -9,7 +9,10 @@ from ifcopenshell.util import placement from numpy.typing import NDArray import bcf.v3.model as mdl -from bcf.geometry import calc_camera_vectors +from bcf.geometry import ( + camera_vectors_from_element_placement, + camera_vectors_from_target_position, +) from bcf.inmemory_zipfile import ZipFileInterface from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer @@ -169,6 +172,29 @@ class VisualizationInfoHandler: xml_handler = xml_handler or XmlParserSerializer() return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler) + @classmethod + def create_from_point_and_guids( + cls, + position: NDArray[np.float_], + *guids: str, + xml_handler: Optional[AbstractXmlParserSerializer] = None, + ) -> "VisualizationInfoHandler": + """ + Create a new VisualizationInfoHandler object from an IFC element. + + Args: + position: target point coordinates. + *guids: One or more IFC element GUID. + xml_handler: The XML handler to use. + + Returns: + The VisualizationInfoHandler object. + """ + xml_handler = xml_handler or XmlParserSerializer() + return cls( + visualization_info=build_viewpoint_from_position_and_guids(position, *guids), xml_handler=xml_handler + ) + @lru_cache(maxsize=None) def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: @@ -192,18 +218,39 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: ) -def build_components(guid: str) -> mdl.Components: +def build_viewpoint_from_position_and_guids(position: NDArray[np.float_], *guids: str) -> mdl.VisualizationInfo: + """ + Return a BCF viewpoint of an IFC element. + + This function is cached to speedudp the creation of multiple BCF topics regarding the same element. + + Args: + position: target point coordinates. + *guids: One or more IFC element GUID. + + Returns: + The BCF viewpoint definition. + """ + return mdl.VisualizationInfo( + guid=str(uuid.uuid4()), + components=build_components(*guids), + perspective_camera=build_camera_from_vectors(*camera_vectors_from_target_position(position)), + ) + + +def build_components(*guids: str) -> mdl.Components: """ Return the BCF components from an IFC element GUID. Args: - guid: The IFC element GUID. + *guids: One or more IFC element GUID. Returns: The BCF components definition. """ + components = [mdl.Component(ifc_guid=guid) for guid in guids] return mdl.Components( - selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]), + selection=mdl.ComponentSelection(component=components), visibility=mdl.ComponentVisibility(default_visibility=True), ) @@ -218,7 +265,7 @@ def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera: Returns: The BCF camera definition. """ - return build_camera_from_vectors(*calc_camera_vectors(elem_placement)) + return build_camera_from_vectors(*camera_vectors_from_element_placement(elem_placement)) def build_camera_from_vectors( diff --git a/src/bcf/tests/v2/test_bcf_xml.py b/src/bcf/tests/v2/test_bcf_xml.py index fa99ebda8b..26446ee0c3 100644 --- a/src/bcf/tests/v2/test_bcf_xml.py +++ b/src/bcf/tests/v2/test_bcf_xml.py @@ -47,12 +47,16 @@ def test_bcf_edit_saveas(xml_handler, build_sample) -> None: modified_path = Path(tmp_dir) / "edited.bcf" parsed.save(modified_path) with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed: - assert modified_parsed == bcf - parsed_th = modified_parsed.topics[orig_th.guid] - assert parsed_th.markup != orig_th.markup - assert parsed_th.markup == parsed.topics[orig_th.guid].markup - assert parsed_th.viewpoints == orig_th.viewpoints - assert parsed_th.bim_snippet == orig_th.bim_snippet + _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed) + + +def _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed): + assert modified_parsed == bcf + parsed_th = modified_parsed.topics[orig_th.guid] + assert parsed_th.markup != orig_th.markup + assert parsed_th.markup == parsed.topics[orig_th.guid].markup + assert parsed_th.viewpoints == orig_th.viewpoints + assert parsed_th.bim_snippet == orig_th.bim_snippet def test_bcf_edit(xml_handler, build_sample) -> None: @@ -66,13 +70,8 @@ def test_bcf_edit(xml_handler, build_sample) -> None: th.topic.title = "New Topic Title" parsed.save() with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed: - assert modified_parsed == bcf + _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed) assert len(modified_parsed.topics) == 1 - parsed_th = modified_parsed.topics[orig_th.guid] - assert parsed_th.markup != orig_th.markup - assert parsed_th.markup == parsed.topics[orig_th.guid].markup - assert parsed_th.viewpoints == orig_th.viewpoints - assert parsed_th.bim_snippet == orig_th.bim_snippet def test_save_no_filename(build_sample) -> None: @@ -112,11 +111,16 @@ def test_massive_bcf(xml_handler) -> None: bcf.save(file_path) -def test_equality_with_wrong_object() -> None: - with pytest.raises(TypeError): - build_sample[0] == "Wrong object" +def test_equality_with_wrong_object(build_sample) -> None: + assert build_sample[0] != "Wrong object" -def test_topic_equality_with_wrong_object() -> None: - with pytest.raises(TypeError): - build_sample[1] == "Wrong object" +def test_topic_equality_with_wrong_object(build_sample) -> None: + assert build_sample[1] != "Wrong object" + + +def test_bcf_get_set_version(build_sample) -> None: + bcf = build_sample[0] + assert bcf.version.version_id == "2.1" + bcf.version.version_id = "2.0" + assert bcf.version.version_id == "2.0" diff --git a/src/bcf/tests/v2/test_ifcclash_api.py b/src/bcf/tests/v2/test_ifcclash_api.py new file mode 100644 index 0000000000..d6adf29bce --- /dev/null +++ b/src/bcf/tests/v2/test_ifcclash_api.py @@ -0,0 +1,36 @@ +import numpy as np +import pytest + +from bcf.v2.bcfxml import BcfXml + + +def test_create_clash_set_bcf() -> None: + bcfxml = BcfXml.create_new("Clash Test") + topic = bcfxml.add_topic("Test", "Test topic", "IfcClash") + topic.add_viewpoint_from_point_and_guids( + np.array([10, 10, 10]), + "firstId", + "secondId", + ) + assert len(topic.viewpoints) == 1 + guid, vi_handler = next((k, v) for k, v in topic.viewpoints.items()) + v_info = vi_handler.visualization_info + assert v_info.guid == guid + components = v_info.components.selection.component + assert {c.ifc_guid for c in components} == {"firstId", "secondId"} + camera = v_info.perspective_camera + viewpoint = camera.camera_view_point + assert viewpoint.x == 15 + assert viewpoint.y == 15 + assert viewpoint.z == 15 + # default direction is the unit vector of -1, -1, -1 + direction = camera.camera_direction + assert direction.x == pytest.approx(-1 / 3**0.5) + assert direction.y == pytest.approx(-1 / 3**0.5) + assert direction.z == pytest.approx(-1 / 3**0.5) + # default + up_vector = camera.camera_up_vector + assert up_vector.x == pytest.approx(-1 / 6**0.5) + assert up_vector.y == pytest.approx(-1 / 6**0.5) + assert up_vector.z == pytest.approx(1 / 1.5**0.5) + assert camera.field_of_view == 60 diff --git a/src/bcf/tests/v3/test_bcf_xml.py b/src/bcf/tests/v3/test_bcf_xml.py index 63d0bd82b5..133dd66419 100644 --- a/src/bcf/tests/v3/test_bcf_xml.py +++ b/src/bcf/tests/v3/test_bcf_xml.py @@ -48,12 +48,16 @@ def test_bcf_edit_saveas(xml_handler, build_sample) -> None: modified_path = Path(tmp_dir) / "edited.bcf" parsed.save(modified_path) with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed: - assert modified_parsed == bcf - parsed_th = modified_parsed.topics[orig_th.guid] - assert parsed_th.markup != orig_th.markup - assert parsed_th.markup == parsed.topics[orig_th.guid].markup - assert parsed_th.viewpoints == orig_th.viewpoints - assert parsed_th.bim_snippet == orig_th.bim_snippet + _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed) + + +def _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed): + assert modified_parsed == bcf + parsed_th = modified_parsed.topics[orig_th.guid] + assert parsed_th.markup != orig_th.markup + assert parsed_th.markup == parsed.topics[orig_th.guid].markup + assert parsed_th.viewpoints == orig_th.viewpoints + assert parsed_th.bim_snippet == orig_th.bim_snippet def test_bcf_edit(xml_handler, build_sample) -> None: @@ -67,13 +71,8 @@ def test_bcf_edit(xml_handler, build_sample) -> None: th.topic.title = "New Topic Title" parsed.save() with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed: - assert modified_parsed == bcf + _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed) assert len(modified_parsed.topics) == 1 - parsed_th = modified_parsed.topics[orig_th.guid] - assert parsed_th.markup != orig_th.markup - assert parsed_th.markup == parsed.topics[orig_th.guid].markup - assert parsed_th.viewpoints == orig_th.viewpoints - assert parsed_th.bim_snippet == orig_th.bim_snippet def test_save_no_filename(build_sample) -> None: @@ -88,7 +87,7 @@ def test_load_no_filename() -> None: def test_save_keep_open(build_sample) -> None: - bcf, orig_th = build_sample + bcf, _ = build_sample with TemporaryDirectory() as tmp_dir: file_path = Path(tmp_dir) / "test.bcf" bcf.save(file_path, keep_open=True) @@ -96,11 +95,6 @@ def test_save_keep_open(build_sample) -> None: bcf._zip_file.close() -# image = PIL.Image.new('RGB', size=(100, 100)) -# file = BinaryIO() -# image.save(file) - - def test_massive_bcf(xml_handler) -> None: ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"])) bcf = BcfXml.create_new("Test project", extensions=ext, xml_handler=xml_handler) @@ -119,11 +113,9 @@ def test_massive_bcf(xml_handler) -> None: bcf.save(file_path) -def test_equality_with_wrong_object() -> None: - with pytest.raises(TypeError): - build_sample[0] == "Wrong object" +def test_equality_with_wrong_object(build_sample) -> None: + assert build_sample[0] != "Wrong object" -def test_topic_equality_with_wrong_object() -> None: - with pytest.raises(TypeError): - build_sample[1] == "Wrong object" +def test_topic_equality_with_wrong_object(build_sample) -> None: + assert build_sample[1] != "Wrong object" diff --git a/src/bcf/tests/v3/test_ifcclash_api.py b/src/bcf/tests/v3/test_ifcclash_api.py new file mode 100644 index 0000000000..f5c2658208 --- /dev/null +++ b/src/bcf/tests/v3/test_ifcclash_api.py @@ -0,0 +1,36 @@ +import numpy as np +import pytest + +from bcf.v3.bcfxml import BcfXml + + +def test_create_clash_set_bcf() -> None: + bcfxml = BcfXml.create_new("Clash Test") + topic = bcfxml.add_topic("Test", "Test topic", "IfcClash") + topic.add_viewpoint_from_point_and_guids( + np.array([10, 10, 10]), + "firstId", + "secondId", + ) + assert len(topic.viewpoints) == 1 + guid, vi_handler = next((k, v) for k, v in topic.viewpoints.items()) + v_info = vi_handler.visualization_info + assert v_info.guid == guid + components = v_info.components.selection.component + assert {c.ifc_guid for c in components} == {"firstId", "secondId"} + camera = v_info.perspective_camera + viewpoint = camera.camera_view_point + assert viewpoint.x == 15 + assert viewpoint.y == 15 + assert viewpoint.z == 15 + # default direction is the unit vector of -1, -1, -1 + direction = camera.camera_direction + assert direction.x == pytest.approx(-1 / 3**0.5) + assert direction.y == pytest.approx(-1 / 3**0.5) + assert direction.z == pytest.approx(-1 / 3**0.5) + # default + up_vector = camera.camera_up_vector + assert up_vector.x == pytest.approx(-1 / 6**0.5) + assert up_vector.y == pytest.approx(-1 / 6**0.5) + assert up_vector.z == pytest.approx(1 / 1.5**0.5) + assert camera.field_of_view == 60 diff --git a/src/ifcclash/ifcclash/ifcclash.py b/src/ifcclash/ifcclash/ifcclash.py index 3531820626..1513e89c36 100644 --- a/src/ifcclash/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash/ifcclash.py @@ -22,8 +22,6 @@ import numpy as np import json import sys -import argparse -import logging import multiprocessing import ifcopenshell import ifcopenshell.geom @@ -117,71 +115,22 @@ class Clasher: self.export_json() def export_bcfxml(self): - import bcf - import bcf.v2.bcfxml + from bcf.v2.bcfxml import BcfXml for i, clash_set in enumerate(self.clash_sets): - bcfxml = bcf.v2.bcfxml.BcfXml() - bcfxml.new_project() - bcfxml.project.name = clash_set["name"] - bcfxml.edit_project() - for key, clash in clash_set["clashes"].items(): - topic = bcf.v2.data.Topic() - topic.title = "{}/{} and {}/{}".format( - clash["a_ifc_class"], clash["a_name"], clash["b_ifc_class"], clash["b_name"] + bcfxml = BcfXml.create_new(clash_set["name"]) + for clash in clash_set["clashes"].values(): + title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}' + topic = bcfxml.add_topic(title, title, "IfcClash") + topic.add_viewpoint_from_point_and_guids( + np.array(clash["position"]), clash["a_global_id"], clash["b_global_id"], ) - topic = bcfxml.add_topic(topic) - viewpoint = bcf.v2.data.Viewpoint() - viewpoint.perspective_camera = bcf.v2.data.PerspectiveCamera() - position = np.array(clash["position"]) - point = position + np.array((5, 5, 5)) # Dumb, but works (for now)! - viewpoint.perspective_camera.camera_view_point.x = point[0] - viewpoint.perspective_camera.camera_view_point.y = point[1] - viewpoint.perspective_camera.camera_view_point.z = point[2] - mat = self.get_track_to_matrix(point, position) - viewpoint.perspective_camera.camera_direction.x = mat[0][2] * -1 - viewpoint.perspective_camera.camera_direction.y = mat[1][2] * -1 - viewpoint.perspective_camera.camera_direction.z = mat[2][2] * -1 - viewpoint.perspective_camera.camera_up_vector.x = mat[0][1] - viewpoint.perspective_camera.camera_up_vector.y = mat[1][1] - viewpoint.perspective_camera.camera_up_vector.z = mat[2][1] - viewpoint.components = bcf.v2.data.Components() - c1 = bcf.v2.data.Component() - c1.ifc_guid = clash["a_global_id"] - c2 = bcf.v2.data.Component() - c2.ifc_guid = clash["b_global_id"] - viewpoint.components.selection.append(c1) - viewpoint.components.selection.append(c2) - viewpoint.components.visibility = bcf.v2.data.ComponentVisibility() - viewpoint.components.visibility.default_visibility = True - viewpoint.snapshot = self.get_viewpoint_snapshot(viewpoint, mat) - bcfxml.add_viewpoint(topic, viewpoint) - if i == 0: - bcfxml.save_project(self.settings.output) - else: - bcfxml.save_project(self.settings.output + f".{i}") + suffix = f".{i}" if i else "" + bcfxml.save_project(f"{self.settings.output}{suffix}") def get_viewpoint_snapshot(self, viewpoint, mat): return None # Possible to overload this function in a GUI application if used as a library - # https://blender.stackexchange.com/questions/68834/recreate-to-track-quat-with-two-vectors-using-python/141706#141706 - def get_track_to_matrix(self, camera_position, target_position): - camera_direction = camera_position - target_position - camera_direction = camera_direction / np.linalg.norm(camera_direction) - camera_right = np.cross(np.array([0.0, 0.0, 1.0]), camera_direction) - camera_right = camera_right / np.linalg.norm(camera_right) - camera_up = np.cross(camera_direction, camera_right) - camera_up = camera_up / np.linalg.norm(camera_up) - rotation_transform = np.zeros((4, 4)) - rotation_transform[0, :3] = camera_right - rotation_transform[1, :3] = camera_up - rotation_transform[2, :3] = camera_direction - rotation_transform[-1, -1] = 1 - translation_transform = np.eye(4) - translation_transform[:3, -1] = -camera_position - look_at_transform = np.matmul(rotation_transform, translation_transform) - return np.linalg.inv(look_at_transform) - def export_json(self): clash_sets = self.clash_sets.copy() for clash_set in clash_sets: