fix: ifcclash bcfxml export

This commit is contained in:
Andrea Ghensi
2023-01-14 15:49:23 +01:00
committed by Dion Moult
parent 4ee3458ff9
commit a4566797fe
13 changed files with 316 additions and 151 deletions
+1 -1
View File
@@ -3,5 +3,5 @@ mypy
pylint pylint
isort isort
xsdata xsdata
tox tox==3.27.1
tox-conda tox-conda
+32 -12
View File
@@ -1,8 +1,10 @@
from typing import Optional
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
def calc_camera_vectors( def camera_vectors_from_element_placement(
elem_placement: NDArray[np.float_], elem_placement: NDArray[np.float_],
) -> tuple[NDArray[np.float_], NDArray[np.float_], 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 Camera position, direction and up vectors
""" """
target_position = elem_placement[:3, 3] target_position = elem_placement[:3, 3]
camera_position = target_position + np.array((5, 5, 5)) return camera_vectors_from_target_position(target_position)
camera_direction = unit_vector(camera_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_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)) camera_up = unit_vector(np.cross(camera_direction, camera_right))
rotation_transform = np.eye(4) return camera_position, camera_direction, camera_up
rotation_transform[0, :3] = camera_right # rotation_transform = np.eye(4)
rotation_transform[1, :3] = camera_up # rotation_transform[0, :3] = camera_right
rotation_transform[2, :3] = camera_direction # rotation_transform[1, :3] = camera_up
translation_transform = np.eye(4) # rotation_transform[2, :3] = camera_direction
translation_transform[:3, -1] = -camera_position # translation_transform = np.eye(4)
look_at_transform = np.matmul(rotation_transform, translation_transform) # translation_transform[:3, -1] = -camera_position
mat = np.linalg.inv(look_at_transform) # look_at_transform = np.matmul(rotation_transform, translation_transform)
return camera_position, -mat[:3, 2], mat[:3, 1] # 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_]: def unit_vector(v: NDArray[np.float_]) -> NDArray[np.float_]:
+8 -6
View File
@@ -3,7 +3,7 @@ import uuid
import warnings import warnings
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from typing import Any, Optional, TypeVar from typing import Any, NoReturn, Optional, TypeVar
import bcf.v2.model as mdl import bcf.v2.model as mdl
from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface
@@ -50,7 +50,7 @@ class BcfXml:
self._version = ( self._version = (
self._xml_handler.parse(self._zip_file.read("bcf.version"), mdl.Version) self._xml_handler.parse(self._zip_file.read("bcf.version"), mdl.Version)
if self._zip_file if self._zip_file
else mdl.Version(version_id="3.0") else mdl.Version(version_id="2.1")
) )
return self._version return self._version
@@ -210,10 +210,12 @@ class BcfXml:
self.topics[topic_handler.guid] = topic_handler self.topics[topic_handler.guid] = topic_handler
return topic_handler return topic_handler
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool | NoReturn:
if not isinstance(other, BcfXml): return (
raise TypeError("Equality needs a BcfXml object.") self.version == other.version and self.project_info == other.project_info
return self.version == other.version and self.project_info == other.project_info if isinstance(other, BcfXml)
else NotImplemented
)
# region Deprecated methods # region Deprecated methods
def new_project(self) -> "BcfXml": def new_project(self) -> "BcfXml":
+23 -7
View File
@@ -3,9 +3,11 @@ import datetime
import uuid import uuid
import zipfile import zipfile
from pathlib import Path 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 ifcopenshell import entity_instance
from numpy.typing import NDArray
from xsdata.models.datatype import XmlDateTime from xsdata.models.datatype import XmlDateTime
import bcf.v2.model as mdl import bcf.v2.model as mdl
@@ -226,15 +228,29 @@ class TopicHandler:
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler) new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
self.add_visinfo_handler(new_viewpoint) 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: def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
self.viewpoints[new_viewpoint.guid] = new_viewpoint self.viewpoints[new_viewpoint.guid] = new_viewpoint
self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid)) self.markup.viewpoints.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool | NoReturn:
if not isinstance(other, TopicHandler):
raise TypeError("Equality needs a BcfXml object.")
return ( return (
self.markup == other.markup (
and self.viewpoints == other.viewpoints self.markup == other.markup
and self.bim_snippet == other.bim_snippet and self.viewpoints == other.viewpoints
and self.bim_snippet == other.bim_snippet
)
if isinstance(other, TopicHandler)
else NotImplemented
) )
+52 -5
View File
@@ -9,7 +9,10 @@ from ifcopenshell.util import placement
from numpy.typing import NDArray from numpy.typing import NDArray
import bcf.v2.model as mdl 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.inmemory_zipfile import ZipFileInterface
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
@@ -169,6 +172,29 @@ class VisualizationInfoHandler:
xml_handler = xml_handler or XmlParserSerializer() xml_handler = xml_handler or XmlParserSerializer()
return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler) 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) @lru_cache(maxsize=None)
def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: 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. Return the BCF components from an IFC element GUID.
Args: Args:
guid: The IFC element GUID. *guids: One or more IFC element GUID.
Returns: Returns:
The BCF components definition. The BCF components definition.
""" """
components = [mdl.Component(ifc_guid=guid) for guid in guids]
return mdl.Components( return mdl.Components(
selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]), selection=mdl.ComponentSelection(component=components),
visibility=mdl.ComponentVisibility(default_visibility=True), visibility=mdl.ComponentVisibility(default_visibility=True),
) )
@@ -218,7 +265,7 @@ def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera:
Returns: Returns:
The BCF camera definition. 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( def build_camera_from_vectors(
+4 -4
View File
@@ -3,7 +3,7 @@ import uuid
import warnings import warnings
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from typing import Any, Optional, TypeVar from typing import Any, NoReturn, Optional, TypeVar
import bcf.v3.model as mdl import bcf.v3.model as mdl
from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface
@@ -224,13 +224,13 @@ class BcfXml:
self.topics[topic_handler.guid] = topic_handler self.topics[topic_handler.guid] = topic_handler
return topic_handler return topic_handler
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool | NoReturn:
if not isinstance(other, BcfXml):
raise TypeError("Equality needs a BcfXml object.")
return ( return (
self.version == other.version self.version == other.version
and self.project_info == other.project_info and self.project_info == other.project_info
and self.extensions == other.extensions and self.extensions == other.extensions
if isinstance(other, BcfXml)
else NotImplemented
) )
# region Deprecated methods # region Deprecated methods
+25 -9
View File
@@ -3,9 +3,11 @@ import datetime
import uuid import uuid
import zipfile import zipfile
from pathlib import Path 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 ifcopenshell import entity_instance
from numpy.typing import NDArray
from xsdata.models.datatype import XmlDateTime from xsdata.models.datatype import XmlDateTime
import bcf.v3.model as mdl import bcf.v3.model as mdl
@@ -73,7 +75,7 @@ class TopicHandler:
self._bim_snippet = value self._bim_snippet = value
@property @property
def viewpoints(self) -> Optional[VisualizationInfoHandler]: def viewpoints(self) -> dict[str, "VisualizationInfoHandler"]:
if ( if (
not self._viewpoints not self._viewpoints
and self._topic_dir and self._topic_dir
@@ -143,7 +145,7 @@ class TopicHandler:
self._save_viewpoints(destination_zip, topic_dir) self._save_viewpoints(destination_zip, topic_dir)
self._save_bim_snippet(destination_zip) 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): if not self.topic.viewpoints or not (viewpoints := self.topic.viewpoints.view_point):
return return
for vpt in viewpoints: for vpt in viewpoints:
@@ -172,17 +174,31 @@ class TopicHandler:
new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler) new_viewpoint = VisualizationInfoHandler.create_new(element, self._xml_handler)
self.add_visinfo_handler(new_viewpoint) 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: def add_visinfo_handler(self, new_viewpoint: VisualizationInfoHandler) -> None:
self.viewpoints[new_viewpoint.guid] = new_viewpoint self.viewpoints[new_viewpoint.guid] = new_viewpoint
if self.topic.viewpoints is None: if self.topic.viewpoints is None:
self.topic.viewpoints = mdl.TopicViewpoints() self.topic.viewpoints = mdl.TopicViewpoints()
self.topic.viewpoints.view_point.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid)) self.topic.viewpoints.view_point.append(mdl.ViewPoint(viewpoint=new_viewpoint.guid, guid=new_viewpoint.guid))
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool | NoReturn:
if not isinstance(other, TopicHandler):
raise TypeError("Equality needs a BcfXml object.")
return ( return (
self.markup == other.markup (
and self.viewpoints == other.viewpoints self.markup == other.markup
and self.bim_snippet == other.bim_snippet and self.viewpoints == other.viewpoints
and self.bim_snippet == other.bim_snippet
)
if isinstance(other, TopicHandler)
else NotImplemented
) )
+52 -5
View File
@@ -9,7 +9,10 @@ from ifcopenshell.util import placement
from numpy.typing import NDArray from numpy.typing import NDArray
import bcf.v3.model as mdl 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.inmemory_zipfile import ZipFileInterface
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
@@ -169,6 +172,29 @@ class VisualizationInfoHandler:
xml_handler = xml_handler or XmlParserSerializer() xml_handler = xml_handler or XmlParserSerializer()
return cls(visualization_info=build_viewpoint(element), xml_handler=xml_handler) 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) @lru_cache(maxsize=None)
def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo: 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. Return the BCF components from an IFC element GUID.
Args: Args:
guid: The IFC element GUID. *guids: One or more IFC element GUID.
Returns: Returns:
The BCF components definition. The BCF components definition.
""" """
components = [mdl.Component(ifc_guid=guid) for guid in guids]
return mdl.Components( return mdl.Components(
selection=mdl.ComponentSelection(component=[mdl.Component(ifc_guid=guid)]), selection=mdl.ComponentSelection(component=components),
visibility=mdl.ComponentVisibility(default_visibility=True), visibility=mdl.ComponentVisibility(default_visibility=True),
) )
@@ -218,7 +265,7 @@ def build_camera(elem_placement: NDArray[np.float_]) -> mdl.PerspectiveCamera:
Returns: Returns:
The BCF camera definition. 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( def build_camera_from_vectors(
+22 -18
View File
@@ -47,12 +47,16 @@ def test_bcf_edit_saveas(xml_handler, build_sample) -> None:
modified_path = Path(tmp_dir) / "edited.bcf" modified_path = Path(tmp_dir) / "edited.bcf"
parsed.save(modified_path) parsed.save(modified_path)
with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed: with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed:
assert modified_parsed == bcf _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed)
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 def _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed):
assert parsed_th.viewpoints == orig_th.viewpoints assert modified_parsed == bcf
assert parsed_th.bim_snippet == orig_th.bim_snippet 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: 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" th.topic.title = "New Topic Title"
parsed.save() parsed.save()
with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed: 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 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: def test_save_no_filename(build_sample) -> None:
@@ -112,11 +111,16 @@ def test_massive_bcf(xml_handler) -> None:
bcf.save(file_path) bcf.save(file_path)
def test_equality_with_wrong_object() -> None: def test_equality_with_wrong_object(build_sample) -> None:
with pytest.raises(TypeError): assert build_sample[0] != "Wrong object"
build_sample[0] == "Wrong object"
def test_topic_equality_with_wrong_object() -> None: def test_topic_equality_with_wrong_object(build_sample) -> None:
with pytest.raises(TypeError): assert build_sample[1] != "Wrong object"
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"
+36
View File
@@ -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
+16 -24
View File
@@ -48,12 +48,16 @@ def test_bcf_edit_saveas(xml_handler, build_sample) -> None:
modified_path = Path(tmp_dir) / "edited.bcf" modified_path = Path(tmp_dir) / "edited.bcf"
parsed.save(modified_path) parsed.save(modified_path)
with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed: with BcfXml.load(modified_path, xml_handler=xml_handler) as modified_parsed:
assert modified_parsed == bcf _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed)
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 def _assert_modified_parsed(modified_parsed, bcf, orig_th, parsed):
assert parsed_th.viewpoints == orig_th.viewpoints assert modified_parsed == bcf
assert parsed_th.bim_snippet == orig_th.bim_snippet 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: 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" th.topic.title = "New Topic Title"
parsed.save() parsed.save()
with BcfXml.load(file_path, xml_handler=xml_handler) as modified_parsed: 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 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: 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: def test_save_keep_open(build_sample) -> None:
bcf, orig_th = build_sample bcf, _ = build_sample
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
file_path = Path(tmp_dir) / "test.bcf" file_path = Path(tmp_dir) / "test.bcf"
bcf.save(file_path, keep_open=True) bcf.save(file_path, keep_open=True)
@@ -96,11 +95,6 @@ def test_save_keep_open(build_sample) -> None:
bcf._zip_file.close() bcf._zip_file.close()
# image = PIL.Image.new('RGB', size=(100, 100))
# file = BinaryIO()
# image.save(file)
def test_massive_bcf(xml_handler) -> None: def test_massive_bcf(xml_handler) -> None:
ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"])) ext = mdl.Extensions(topic_types=mdl.ExtensionsTopicTypes(topic_type=["Test type"]))
bcf = BcfXml.create_new("Test project", extensions=ext, xml_handler=xml_handler) 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) bcf.save(file_path)
def test_equality_with_wrong_object() -> None: def test_equality_with_wrong_object(build_sample) -> None:
with pytest.raises(TypeError): assert build_sample[0] != "Wrong object"
build_sample[0] == "Wrong object"
def test_topic_equality_with_wrong_object() -> None: def test_topic_equality_with_wrong_object(build_sample) -> None:
with pytest.raises(TypeError): assert build_sample[1] != "Wrong object"
build_sample[1] == "Wrong object"
+36
View File
@@ -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
+9 -60
View File
@@ -22,8 +22,6 @@
import numpy as np import numpy as np
import json import json
import sys import sys
import argparse
import logging
import multiprocessing import multiprocessing
import ifcopenshell import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
@@ -117,71 +115,22 @@ class Clasher:
self.export_json() self.export_json()
def export_bcfxml(self): def export_bcfxml(self):
import bcf from bcf.v2.bcfxml import BcfXml
import bcf.v2.bcfxml
for i, clash_set in enumerate(self.clash_sets): for i, clash_set in enumerate(self.clash_sets):
bcfxml = bcf.v2.bcfxml.BcfXml() bcfxml = BcfXml.create_new(clash_set["name"])
bcfxml.new_project() for clash in clash_set["clashes"].values():
bcfxml.project.name = clash_set["name"] title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}'
bcfxml.edit_project() topic = bcfxml.add_topic(title, title, "IfcClash")
for key, clash in clash_set["clashes"].items(): topic.add_viewpoint_from_point_and_guids(
topic = bcf.v2.data.Topic() np.array(clash["position"]), clash["a_global_id"], clash["b_global_id"],
topic.title = "{}/{} and {}/{}".format(
clash["a_ifc_class"], clash["a_name"], clash["b_ifc_class"], clash["b_name"]
) )
topic = bcfxml.add_topic(topic) suffix = f".{i}" if i else ""
viewpoint = bcf.v2.data.Viewpoint() bcfxml.save_project(f"{self.settings.output}{suffix}")
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}")
def get_viewpoint_snapshot(self, viewpoint, mat): def get_viewpoint_snapshot(self, viewpoint, mat):
return None # Possible to overload this function in a GUI application if used as a library 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): def export_json(self):
clash_sets = self.clash_sets.copy() clash_sets = self.clash_sets.copy()
for clash_set in clash_sets: for clash_set in clash_sets: