This commit is contained in:
Thomas Krijnen
2024-06-29 14:14:59 +02:00
7 changed files with 138 additions and 27 deletions
@@ -514,6 +514,7 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None: def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
return # TODO ensure this now works with the new approach of styles
elements = settings["products"] elements = settings["products"]
material = settings.get("material") material = settings.get("material")
if material: if material:
@@ -544,6 +545,7 @@ def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, set
def ensure_material_unassigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None: def ensure_material_unassigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
return # TODO ensure this now works with the new approach of styles
elements = settings["products"] elements = settings["products"]
# unassign_material could be called when product is about to get removed # unassign_material could be called when product is about to get removed
@@ -264,7 +264,6 @@ class BIM_PT_project(Panel):
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.prop(props, "ifc_file", text="") row.prop(props, "ifc_file", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="") row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
row.operator("bim.unload_project", text="", icon="CANCEL")
class BIM_PT_new_project_wizard(Panel): class BIM_PT_new_project_wizard(Panel):
@@ -310,7 +310,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
if materials: if materials:
material = materials[0] # Arbitrarily pick a material material = materials[0] # Arbitrarily pick a material
else: else:
material = self.add_default_material() material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown")
rel = ifcopenshell.api.run( rel = ifcopenshell.api.run(
"material.assign_material", ifc_file, products=[element], type="IfcMaterialLayerSet" "material.assign_material", ifc_file, products=[element], type="IfcMaterialLayerSet"
) )
@@ -344,7 +344,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
if materials: if materials:
material = materials[0] # Arbitrarily pick a material material = materials[0] # Arbitrarily pick a material
else: else:
material = self.add_default_material() material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown")
if template == "PROFILESET": if template == "PROFILESET":
named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName] named_profiles = [p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName]
if named_profiles: if named_profiles:
@@ -495,13 +495,6 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
props.type_class = props.type_class props.type_class = props.type_class
return {"FINISHED"} return {"FINISHED"}
def add_default_material(self):
material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown")
blender_material = bpy.data.materials.new(material.Name)
tool.Ifc.link(material, blender_material)
blender_material.use_fake_user = True
return material
class RemoveType(bpy.types.Operator, tool.Ifc.Operator): class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_type" bl_idname = "bim.remove_type"
@@ -749,10 +749,23 @@ def format_length(
def is_attr_type( def is_attr_type(
content_type: ifcopenshell_wrapper.parameter_type, content_type: ifcopenshell_wrapper.parameter_type,
ifc_unit_type_name: str, ifc_unit_type_name: str,
include_select_types: bool = True
) -> Union[ifcopenshell_wrapper.type_declaration, None]: ) -> Union[ifcopenshell_wrapper.type_declaration, None]:
cur_decl = content_type cur_decl = content_type
if include_select_types:
if hasattr(cur_decl, "select_list"):
for select_item in cur_decl.select_list():
if is_attr_type(select_item, ifc_unit_type_name):
return select_item
while hasattr(cur_decl, "declared_type") is True: while hasattr(cur_decl, "declared_type") is True:
cur_decl = cur_decl.declared_type() cur_decl = cur_decl.declared_type()
if include_select_types:
if hasattr(cur_decl, "select_list"):
for select_item in cur_decl.select_list():
if is_attr_type(select_item, ifc_unit_type_name):
return select_item
if hasattr(cur_decl, "name") is False: if hasattr(cur_decl, "name") is False:
continue continue
if cur_decl.name() == ifc_unit_type_name: if cur_decl.name() == ifc_unit_type_name:
@@ -800,6 +813,9 @@ def iter_element_and_attributes_per_type(
if val is None: if val is None:
continue continue
if isinstance(val, ifcopenshell.entity_instance) and not val.is_a(attr_type_name):
continue
yield element, attr, val yield element, attr, val
@@ -833,8 +849,12 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "
# Traverse all elements and their nested attributes in the file and convert them # Traverse all elements and their nested attributes in the file and convert them
for element, attr, val in iter_element_and_attributes_per_type(file_patched, "IfcLengthMeasure"): for element, attr, val in iter_element_and_attributes_per_type(file_patched, "IfcLengthMeasure"):
new_value = convert_value(val) if isinstance(val, ifcopenshell.entity_instance):
setattr(element, attr.name(), new_value) new_value = convert_value(val.wrappedValue)
getattr(element, attr.name()).wrappedValue = new_value
else:
new_value = convert_value(val)
setattr(element, attr.name(), new_value)
file_patched.remove(old_length) file_patched.remove(old_length)
unit_assignment.Units = tuple([new_length, *unit_assignment.Units]) unit_assignment.Units = tuple([new_length, *unit_assignment.Units])
@@ -82,3 +82,13 @@ class TestFormatLength(test.bootstrap.IFC4):
assert ( assert (
subject.format_length(25.23, 4, unit_system="imperial", input_unit="inch", output_unit="inch") == '25 1/4"' subject.format_length(25.23, 4, unit_system="imperial", input_unit="inch", output_unit="inch") == '25 1/4"'
) )
class TestIsAttrType(test.bootstrap.IFC4):
def test_run(self):
schema = ifcopenshell.schema_by_name("IFC4")
declaration = schema.declaration_by_name("IfcPropertySingleValue")
nominal_value = declaration.attribute_by_index(2).type_of_attribute()
assert subject.is_attr_type(nominal_value, "IfcValue")
assert subject.is_attr_type(nominal_value, "IfcLengthMeasure")
assert not subject.is_attr_type(nominal_value, "IfcLengthMeasure", include_select_types=False)
+38 -13
View File
@@ -16,15 +16,18 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>. # along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
from ifcpatch.recipes.SetFalseOrigin import Patcher as SetFalseOrigin
from typing import Union from typing import Union
from logging import Logger from logging import Logger
class Patcher: class Patcher:
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, filepath: str): def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, filepath: Union[str, ifcopenshell.file]):
"""Merge two IFC models into one """Merge two IFC models into one
Note that other than combining the two IfcProject elements into one, no Note that other than combining the two IfcProject elements into one, no
@@ -37,7 +40,6 @@ class Patcher:
:param filepath: The filepath of the second IFC model to merge into the :param filepath: The filepath of the second IFC model to merge into the
first. The first model is already specified as the input to first. The first model is already specified as the input to
IfcPatch. IfcPatch.
:type filepath: str
Example: Example:
@@ -51,10 +53,26 @@ class Patcher:
self.filepath = filepath self.filepath = filepath
def patch(self): def patch(self):
source = ifcopenshell.open(self.filepath) if isinstance(self.filepath, ifcopenshell.file):
# make sure models units will match other = self.filepath
if (main_unit := self.get_unit_name(self.file)) != self.get_unit_name(source): else:
source = ifcopenshell.util.unit.convert_file_length_units(source, main_unit) other = ifcopenshell.open(self.filepath)
if (main_unit := self.get_unit_name(self.file)) != self.get_unit_name(other):
other = ifcopenshell.util.unit.convert_file_length_units(other, main_unit)
existing_enh = np.array(
ifcopenshell.util.geolocation.auto_xyz2enh(self.file, 0, 0, 0, should_return_in_map_units=False)
)
other_enh = np.array(
ifcopenshell.util.geolocation.auto_xyz2enh(other, 0, 0, 0, should_return_in_map_units=False)
)
if not np.allclose(existing_enh, other_enh):
x, y, z = ifcopenshell.util.geolocation.auto_enh2xyz(other, *existing_enh, is_specified_in_map_units=False)
e, n, h = existing_enh
# For now don't handle rotation because my brain is going to explode
SetFalseOrigin("", other, self.logger, name="", x=x, y=y, z=z, e=e, n=n, h=h).patch()
self.existing_contexts: list[ifcopenshell.entity_instance] = self.file.by_type( self.existing_contexts: list[ifcopenshell.entity_instance] = self.file.by_type(
"IfcGeometricRepresentationContext" "IfcGeometricRepresentationContext"
@@ -62,13 +80,13 @@ class Patcher:
self.added_contexts: set[ifcopenshell.entity_instance] = set() self.added_contexts: set[ifcopenshell.entity_instance] = set()
original_project = self.file.by_type("IfcProject")[0] original_project = self.file.by_type("IfcProject")[0]
merged_project = self.file.add(source.by_type("IfcProject")[0]) merged_project = self.file.add(other.by_type("IfcProject")[0])
for element in source.by_type("IfcGeometricRepresentationContext"): for element in other.by_type("IfcGeometricRepresentationContext"):
new = self.file.add(element) new = self.file.add(element)
self.added_contexts.add(new) self.added_contexts.add(new)
for element in source: for element in other:
self.file.add(element) self.file.add(element)
for inverse in self.file.get_inverse(merged_project): for inverse in self.file.get_inverse(merged_project):
@@ -88,11 +106,18 @@ class Patcher:
equivalent_existing_context = self.get_equivalent_existing_context(added_context) equivalent_existing_context = self.get_equivalent_existing_context(added_context)
if equivalent_existing_context: if equivalent_existing_context:
for inverse in self.file.get_inverse(added_context): for inverse in self.file.get_inverse(added_context):
if self.file.schema != "IFC2X3":
if inverse.is_a("IfcCoordinateOperation"):
to_delete.add(inverse.id())
continue
ifcopenshell.util.element.replace_attribute(inverse, added_context, equivalent_existing_context) ifcopenshell.util.element.replace_attribute(inverse, added_context, equivalent_existing_context)
to_delete.add(added_context) to_delete.add(added_context.id())
for added_context in to_delete: for element_id in to_delete:
ifcopenshell.util.element.remove_deep2(self.file, added_context) try:
ifcopenshell.util.element.remove_deep2(self.file, self.file.by_id(element_id))
except:
pass
def get_equivalent_existing_context( def get_equivalent_existing_context(
self, added_context: ifcopenshell.entity_instance self, added_context: ifcopenshell.entity_instance
+64 -2
View File
@@ -18,7 +18,7 @@
import ifcpatch import ifcpatch
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api.georeference
import ifcopenshell.util.placement import ifcopenshell.util.placement
import test.bootstrap import test.bootstrap
import tempfile import tempfile
@@ -33,9 +33,11 @@ class TestMergeProject(test.bootstrap.IFC4):
if ifc_file is None: if ifc_file is None:
ifc_file = ifcopenshell.file(schema=self.file.schema) ifc_file = ifcopenshell.file(schema=self.file.schema)
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject") ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix=prefix) unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix=prefix)
ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit]) ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit])
model = ifcopenshell.api.context.add_context(ifc_file, "Model")
ifcopenshell.api.context.add_context(ifc_file, "Model", "Body", "MODEL_VIEW", parent=model)
matrix = np.eye(4) matrix = np.eye(4)
matrix[:, 3] = (1, 2, 3, 1) matrix[:, 3] = (1, 2, 3, 1)
@@ -50,6 +52,7 @@ class TestMergeProject(test.bootstrap.IFC4):
second_file.write(temp_path) second_file.write(temp_path)
output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [str(temp_path)]}) output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [str(temp_path)]})
assert self.file == output
assert len(output.by_type("IfcWall")) == 2 assert len(output.by_type("IfcWall")) == 2
wall1, wall2 = output.by_type("IfcWall") wall1, wall2 = output.by_type("IfcWall")
@@ -61,6 +64,65 @@ class TestMergeProject(test.bootstrap.IFC4):
matrix[:, 3] = (1, 2, 3, 1) matrix[:, 3] = (1, 2, 3, 1)
assert to_tuple(placement1) == to_tuple(placement2) == to_tuple(matrix) assert to_tuple(placement1) == to_tuple(placement2) == to_tuple(matrix)
def test_reusing_geometric_contexts(self):
self.file = self.setup_project(self.file)
second_file = self.setup_project()
output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [second_file]})
assert len(output.by_type("IfcGeometricRepresentationContext")) == 2
def test_using_the_georeferencing_of_the_original_project(self):
if self.file.schema == "IFC2X3":
return
self.file = self.setup_project(self.file)
second_file = self.setup_project()
ifcopenshell.api.georeference.add_georeferencing(self.file)
ifcopenshell.api.georeference.add_georeferencing(second_file)
output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [second_file]})
assert len(output.by_type("IfcProjectedCRS")) == 1
assert len(output.by_type("IfcMapConversion")) == 1
def test_shifting_the_source_project_to_match_the_original_project_origin(self):
self.file = self.setup_project(self.file)
second_file = self.setup_project()
ifcopenshell.api.georeference.add_georeferencing(self.file)
ifcopenshell.api.georeference.edit_georeferencing(
self.file, coordinate_operation={"Eastings": 10, "Northings": 20}, projected_crs={"Name": "EPSG:1234"}
)
ifcopenshell.api.georeference.add_georeferencing(second_file)
ifcopenshell.api.georeference.edit_georeferencing(
second_file, coordinate_operation={"Eastings": 30000, "Northings": 40000}, projected_crs={"Name": "EPSG:0"}
)
# Original file is in meters
wall1 = self.file.by_type("IfcWall")[0]
m1 = ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)
assert np.allclose(m1[:, 3], (1, 2, 3, 1))
global_m1 = ifcopenshell.util.geolocation.auto_local2global(self.file, m1, should_return_in_map_units=False)
assert np.allclose(global_m1[:, 3], (11, 22, 3, 1))
# Second file is in millimeters with a different false origin
wall1 = second_file.by_type("IfcWall")[0]
m1 = ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)
assert np.allclose(m1[:, 3], (1000, 2000, 3000, 1))
global_m1 = ifcopenshell.util.geolocation.auto_local2global(second_file, m1, should_return_in_map_units=False)
assert np.allclose(global_m1[:, 3], (31000, 42000, 3000, 1))
output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [second_file]})
# In the future we may use proj to support reprojection from different CRSes. For now... nope!
if self.file.schema != "IFC2X3":
assert output.by_type("IfcProjectedCRS")[0].Name == "EPSG:1234"
# The results should be in meters with the false origin of the original file
params = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output)
assert params.e == 10
assert params.n == 20
wall1, wall2 = output.by_type("IfcWall")
m1 = ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)
m2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement)
assert np.allclose(m1[:, 3], (1, 2, 3, 1))
assert np.allclose(m2[:, 3], (21, 22, 3, 1))
class TestMergeProjectIFC2X3(test.bootstrap.IFC2X3, TestMergeProject): class TestMergeProjectIFC2X3(test.bootstrap.IFC2X3, TestMergeProject):
pass pass