mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 06:39:13 +00:00
Merge branch 'v0.8.0' of https://github.com/IfcOpenShell/IfcOpenShell into v0.8.0
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user