Merge branch 'v0.7.0' into covering_tool

This commit is contained in:
Massimo Fabbro
2023-10-05 21:23:11 +02:00
committed by GitHub
49 changed files with 1307 additions and 327 deletions
+12
View File
@@ -58,6 +58,7 @@ option(BUILD_PACKAGE "" OFF)
option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
@@ -934,6 +935,17 @@ if(BUILD_CONVERT OR BUILD_IFCPYTHON)
add_library(Serializers ${SERIALIZERS_FILES})
set_target_properties(Serializers PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS" VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
message(STATUS "WITH_PROJ ${WITH_PROJ}")
if (WITH_PROJ)
target_compile_definitions(Serializers PRIVATE "WITH_PROJ")
if (PROJ_STATIC)
target_compile_definitions(Serializers PRIVATE "PROJ_DLL=")
endif()
target_include_directories(Serializers PRIVATE ${PROJ_INCLUDE_DIR} ${SQLITE_INCLUDE_DIR})
target_link_libraries(Serializers ${PROJ_LIBRARIES})
endif()
target_link_libraries(Serializers ${SERIALIZER_SCHEMA_LIBRARIES} ${OPENCOLLADA_LIBRARIES} ${USD_LIBRARIES})
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
+1 -1
View File
@@ -169,7 +169,7 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9cc1f5f-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f0e03c7-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -32,9 +32,16 @@ class SelectFMIfcFile(bpy.types.Operator):
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement)
def execute(self, context):
context.scene.BIMFMProperties.ifc_file = self.filepath
props = context.scene.BIMFMProperties
props.ifc_files.clear()
dirname = os.path.dirname(self.filepath)
for f in self.files:
new = props.ifc_files.add()
new.name = os.path.join(dirname, f.name)
props.ifc_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -42,6 +49,7 @@ class SelectFMIfcFile(bpy.types.Operator):
return {"RUNNING_MODAL"}
class ExecuteIfcFM(bpy.types.Operator):
bl_idname = "bim.execute_ifcfm"
bl_label = "Execute IfcFM"
@@ -66,17 +74,32 @@ class ExecuteIfcFM(bpy.types.Operator):
props = context.scene.BIMFMProperties
ifc_file = tool.Ifc.get()
filepaths = []
if not (ifc_file and props.should_load_from_memory):
ifc_file = ifcopenshell.open(props.ifc_file)
if len(props.ifc_files):
ifc_files = [ifcopenshell.open(f.name) for f in props.ifc_files]
filepaths = [f.name for f in props.ifc_files]
else:
ifc_files = [ifcopenshell.open(props.ifc_file)]
else:
ifc_files = [ifc_file]
parser = ifcfm.Parser(preset=props.engine)
parser.parse(ifc_file)
writer = ifcfm.Writer(parser)
writer.write()
if props.format == "csv":
writer.write_csv('tmp/')
elif props.format == "ods":
writer.write_ods(self.filepath)
elif props.format == "xlsx":
writer.write_xlsx(self.filepath)
for i, ifc_file in enumerate(ifc_files):
if filepaths:
dirname = os.path.dirname(self.filepath)
prefix, _ = os.path.splitext(os.path.basename(filepaths[i]))
basename = os.path.basename(self.filepath)
filepath = os.path.join(dirname, f"{prefix}-{basename}")
else:
filepath = self.filepath
parser = ifcfm.Parser(preset=props.engine)
parser.parse(ifc_file)
writer = ifcfm.Writer(parser)
writer.write()
if props.format == "csv":
writer.write_csv('tmp/')
elif props.format == "ods":
writer.write_ods(filepath)
elif props.format == "xlsx":
writer.write_xlsx(filepath)
return {"FINISHED"}
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -32,6 +33,7 @@ from bpy.props import (
class BIMFMProperties(PropertyGroup):
ifc_file: StringProperty(default="", name="IFC File")
ifc_files: CollectionProperty(name="IFC Files", type=StrProperty)
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
engine: EnumProperty(
items=[
@@ -42,7 +42,10 @@ class BIM_PT_fm(Panel):
if not IfcStore.get_file() or not props.should_load_from_memory:
row = layout.row(align=True)
row.prop(props, "ifc_file")
if props.ifc_files:
row.label(text=f"{len(props.ifc_files)} Files Selected")
else:
row.prop(props, "ifc_file")
row.operator("bim.select_fm_ifc_file", icon="FILE_FOLDER", text="")
row = layout.row()
@@ -23,6 +23,7 @@ classes = (
operator.AddRepresentation,
operator.CopyRepresentation,
operator.EditObjectPlacement,
operator.FlipObject,
operator.GetRepresentationIfcParameters,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
@@ -1203,9 +1203,64 @@ class RefreshAggregate(bpy.types.Operator):
else:
parts = ifcopenshell.util.element.get_parts(instance_entity)
for part in parts:
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
if part.is_a("IfcElementAssembly"):
if not pset:
data_children = {
"children": [],
"instance_of": [part.GlobalId],
}
data = [data_children]
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data"
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Parent": instance_entity.GlobalId, "Data": json.dumps(data)},
)
part_obj = tool.Ifc.get_object(part)
new_part = duplicate_objects(part_obj)
blenderbim.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(entity),
related_obj=tool.Ifc.get_object(new_part),
)
duplicate_children(new_part)
for part in parts:
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
if part.is_a("IfcElementAssembly"):
pass
else:
if not pset:
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=part, name="BBIM_Aggregate_Data"
)
else:
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Aggregate_Data")
pset = tool.Ifc.get().by_id(pset["id"])
data_children = {
"children": [],
"instance_of": [],
}
data = [data_children]
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Parent": instance_entity.GlobalId, "Data": json.dumps(data)},
)
part_obj = tool.Ifc.get_object(part)
new_part = duplicate_objects(part_obj)
blenderbim.core.aggregate.assign_object(
@@ -1226,6 +1281,10 @@ class RefreshAggregate(bpy.types.Operator):
collection.objects.link(new_obj)
obj.select_set(False)
new_obj.select_set(True)
# This is needed to make sure the new object gets unlink from
# the old object assembly collection
new_obj.BIMObjectProperties.collection = None
# Copy the actual class
new_entity = blenderbim.core.root.copy_class(
@@ -1234,7 +1293,9 @@ class RefreshAggregate(bpy.types.Operator):
if new_entity:
tool.Model.handle_array_on_copied_element(new_entity)
blenderbim.core.aggregate.unassign_object(
if not new_entity.is_a("IfcElementAssembly"):
blenderbim.core.aggregate.unassign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
@@ -1662,3 +1723,19 @@ class OverrideModeSetObject(bpy.types.Operator):
if self.edited_objs:
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
class FlipObject(bpy.types.Operator):
bl_idname = "bim.flip_object"
bl_label = "Flip Object"
bl_description = "Flip object's local axes, keep the position"
bl_options = {"REGISTER", "UNDO"}
flip_local_axes: bpy.props.EnumProperty(
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
)
def execute(self, context):
for obj in context.selected_objects:
tool.Geometry.flip_object(obj, self.flip_local_axes)
return {"FINISHED"}
@@ -56,7 +56,12 @@ class GeoreferenceData:
@classmethod
def map_conversion(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return {}
project = tool.Ifc.get().by_type("IfcProject")[0]
map_conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")
if not map_conversion:
return {}
del map_conversion["id"]
return map_conversion
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.HasCoordinateOperation:
@@ -72,8 +77,8 @@ class GeoreferenceData:
def map_derived_angle(cls):
if (
cls.data["map_conversion"]
and cls.data["map_conversion"]["XAxisAbscissa"] is not None
and cls.data["map_conversion"]["XAxisOrdinate"] is not None
and cls.data["map_conversion"].get("XAxisAbscissa", None) is not None
and cls.data["map_conversion"].get("XAxisOrdinate", None) is not None
):
return str(
round(
@@ -88,7 +93,12 @@ class GeoreferenceData:
@classmethod
def projected_crs(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return {}
project = tool.Ifc.get().by_type("IfcProject")[0]
projected_crs = ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS")
if not projected_crs:
return {}
del projected_crs["id"]
return projected_crs
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.HasCoordinateOperation:
@@ -78,10 +78,14 @@ class BIM_PT_gis(Panel):
def draw_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
if tool.Ifc.get_schema() == "IFC2X3":
row = self.layout.row()
row.label(text="IFC2X3 Fallback In Use", icon="ERROR")
if not GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Not Georeferenced")
if tool.Ifc.get_schema != "IFC2X3":
if tool.Ifc.get_schema() != "IFC2X3":
row.operator("bim.add_georeferencing", icon="ADD", text="")
if props.has_blender_offset:
@@ -110,8 +114,9 @@ class BIM_PT_gis(Panel):
if GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
row.operator("bim.remove_georeferencing", icon="X", text="")
if tool.Ifc.get_schema() != "IFC2X3":
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
row.operator("bim.remove_georeferencing", icon="X", text="")
for key, value in GeoreferenceData.data["projected_crs"].items():
if not value:
@@ -184,4 +189,4 @@ class BIM_PT_gis_utilities(Panel):
row = self.layout.row(align=True)
row.prop(props, "y_axis_abscissa_output", text="North Abscissa")
row = self.layout.row(align=True)
row.prop(props, "y_axis_ordinate_output", text="North Ordinate")
row.prop(props, "y_axis_ordinate_output", text="North Ordinate")
@@ -169,7 +169,6 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
data = json.loads(pset["Data"])
data[self.item]["count"] = 1
if (self.keep_objs) & (self.item < (len(data) - 1)):
self.report(
@@ -188,6 +187,8 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.Modifier.Array.bake_children_transform(element, self.item)
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, False)
if not self.keep_objs:
data[self.item]["count"] = 1
tool.Model.regenerate_array(parent, data, self.keep_objs)
pset = tool.Ifc.get().by_id(pset["id"])
@@ -393,18 +393,7 @@ class FlipFill(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
if not element or not element.FillsVoids:
continue
flip_matrix = Matrix.Rotation(pi, 4, "Z")
bottom_left = obj.matrix_world @ Vector(obj.bound_box[0])
top_right = obj.matrix_world @ Vector(obj.bound_box[6])
center = obj.matrix_world.translation.copy()
center_offset = center - bottom_left
flipped_center = top_right - center_offset
obj.matrix_world = obj.matrix_world @ flip_matrix
obj.matrix_world.translation.xy = flipped_center.xy
bpy.context.view_layer.update()
tool.Geometry.flip_object(obj, "XY")
return {"FINISHED"}
@@ -99,7 +99,7 @@ class DeleteSverchokGraph(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def draw(self, context):
self.layout.label(text="WARNING. The graph will be removed permamently.")
self.layout.label(text="WARNING. The graph will be removed permanently.")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -1262,8 +1262,13 @@ class DumbWallJoiner:
results["is_sloped"] = True
results["height"] = (item.Depth * self.unit_scale) / (1 / cos(results["x_angle"]))
break
elif item.is_a("IfcBooleanClippingResult"):
elif item.is_a("IfcBooleanClippingResult"): # should be before IfcBooleanResult check
item = item.FirstOperand
elif item.is_a("IfcBooleanResult"):
if item.FirstOperand.is_a("IfcExtrudedAreaSolid") or item.FirstOperand.is_a("IfcBooleanResult"):
item = item.FirstOperand
else:
item = item.SecondOperand
else:
break
return results
@@ -358,6 +358,7 @@ class BimToolUI:
cls.layout.operator("bim.mep_connect_elements")
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
@@ -668,6 +669,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.flip_wall()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.flip_fill()
elif self.active_class in ("IfcBeam", "IfcColumn"):
bpy.ops.bim.flip_object(flip_local_axes="XZ")
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
@@ -822,10 +822,13 @@ class SelectSimilar(Operator, tool.Ifc.Operator):
props = context.scene.BIMSearchProperties
obj = context.active_object
element = tool.Ifc.get_entity(obj)
value = ifcopenshell.util.selector.get_element_value(element, props.element_key)
key = props.element_key
if props.element_key == "PredefinedType":
key = "predefined_type"
value = ifcopenshell.util.selector.get_element_value(element, key)
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if ifcopenshell.util.selector.get_element_value(element, props.element_key) == value:
if ifcopenshell.util.selector.get_element_value(element, key) == value:
obj.select_set(True)
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
import os
import bpy
import time
import tempfile
@@ -72,8 +72,10 @@ class ExecuteIfcTester(bpy.types.Operator, tool.Ifc.Operator):
tool.Tester.report = report
props.specifications.clear()
for spec in report:
print(spec)
new_spec = props.specifications.add()
new_spec.name = spec["name"]
new_spec.description = spec["description"]
new_spec.status = spec["status"]
blenderbim.bim.handler.refresh_ui_data()
@@ -37,6 +37,7 @@ def update_active_specification_index(self, context):
class Specification(PropertyGroup):
name: StringProperty(name="Name")
description: StringProperty(name="Description")
status: BoolProperty(default=False, name="Status")
@@ -107,8 +107,9 @@ class BIM_PT_tester(Panel):
class BIM_UL_tester_specifications(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row = layout.split(factor=0.3, align=True)
row.label(text=item.name, icon="CHECKMARK" if item.status else "CANCEL")
row.label(text=item.description)
class BIM_UL_tester_failed_entities(UIList):
+1 -1
View File
@@ -237,7 +237,7 @@ class FileAssociate(bpy.types.Operator):
# tried to do the regitsry change from powershell/cmd - but even admin rights are not enough
# this is why we're using .reg
reg_change_path = os.path.join(src_dir, "windows_bbim_association.reg")
subprocess.run(["cmd", "/c", reg_change_path])
subprocess.run(["cmd", "/c", "call", reg_change_path])
ps_script_path = os.path.join(src_dir, "windows_bbim_association.ps1")
# NOTE: call powershell with RunAs to get admin rights from user
@@ -71,13 +71,13 @@ def set_cursor_location(georeference):
def convert_local_to_global(georeference):
coordinates = georeference.xyz2enh(georeference.get_coordinates("input"), georeference.get_map_conversion())
coordinates = georeference.xyz2enh(georeference.get_coordinates("input"))
georeference.set_coordinates("output", coordinates)
georeference.set_cursor_location(coordinates)
def convert_global_to_local(georeference):
coordinates = georeference.enh2xyz(georeference.get_coordinates("input"), georeference.get_map_conversion())
coordinates = georeference.enh2xyz(georeference.get_coordinates("input"))
georeference.set_coordinates("output", coordinates)
georeference.set_cursor_location(coordinates)
@@ -86,4 +86,4 @@ def convert_angle_to_coord(georeference, type):
georeference.set_vector_coordinates(vector_coordinates,type)
def import_plot(georeference, filepath):
georeference.import_plot(filepath, georeference.get_map_conversion())
georeference.import_plot(filepath)
+9 -1
View File
@@ -95,30 +95,37 @@ def select_similar_container(ifc, spatial, obj=None):
def select_product(spatial, product):
spatial.select_products([product])
def load_container_manager(spatial):
spatial.load_container_manager()
def edit_container_attributes(spatial, entity=None):
spatial.edit_container_attributes(entity)
spatial.load_container_manager()
def contract_container(spatial, container=None):
spatial.contract_container(container)
spatial.load_container_manager()
def expand_container(spatial, container=None):
spatial.expand_container(container)
spatial.load_container_manager()
def delete_container(ifc, spatial, geometry, container=None):
geometry.delete_ifc_object(ifc.get_object(container))
spatial.load_container_manager()
def select_decomposed_elements(spatial):
container = spatial.get_active_container()
if container:
spatial.select_products(spatial.get_decomposed_elements(container))
#HERE STARTS SPATIAL TOOL
def generate_space(ifc, spatial, model, Type):
active_obj = spatial.get_active_obj()
@@ -139,6 +146,7 @@ def generate_space(ifc, spatial, model, Type):
else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() ##mat
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
if not space_polygon:
@@ -188,6 +196,7 @@ def generate_spaces_from_walls(ifc, spatial, collector):
spatial.assign_ifcspace_class_to_obj(obj)
spatial.assign_container_to_obj(obj)
def toggle_space_visibility(ifc, spatial):
model = ifc.get()
@@ -195,4 +204,3 @@ def toggle_space_visibility(ifc, spatial):
if not spaces:
return
spatial.toggle_spaces_visibility_wired_and_textured(spaces)
+2 -3
View File
@@ -384,12 +384,11 @@ class Georeference:
def angle2coords(cls, angle, type): pass
def disable_editing(cls): pass
def enable_editing(cls): pass
def enh2xyz(cls, map_conversion, coordinates): pass
def enh2xyz(cls, coordinates): pass
def get_angle(cls, type): pass
def get_coordinates(cls, io): pass
def get_cursor_location(cls): pass
def get_map_conversion_attributes(cls): pass
def get_map_conversion(cls): pass
def get_projected_crs_attributes(cls): pass
def get_true_north_attributes(cls): pass
def import_map_conversion(cls): pass
@@ -402,7 +401,7 @@ class Georeference:
def set_ifc_grid_north(cls): pass
def set_ifc_true_north(cls): pass
def set_vector_coordinates(cls, vector_coordinates, type): pass
def xyz2enh(cls, map_conversion, coordinates): pass
def xyz2enh(cls, coordinates): pass
@interface
@@ -0,0 +1,4 @@
Windows Registry Editor Version 5.00
[-HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ifc]
[-]
@@ -343,6 +343,8 @@ class Blender:
"max_y": bound_box[6][1],
"min_z": bound_box[0][2],
"max_z": bound_box[6][2],
"min_point": Vector(bound_box[0]),
"max_point": Vector(bound_box[6]),
"center": (Vector(bound_box[6]) + Vector(bound_box[0])) / 2,
}
return bbox_dict
+23 -1
View File
@@ -28,7 +28,7 @@ import blenderbim.core.style
import blenderbim.core.spatial
import blenderbim.tool as tool
import blenderbim.bim.import_ifc
from math import radians
from math import radians, pi
from mathutils import Vector, Matrix
from blenderbim.bim.ifc import IfcStore
@@ -641,3 +641,25 @@ class Geometry(blenderbim.core.tool.Geometry):
@classmethod
def get_model_representations(cls):
return tool.Ifc.get().by_type("IfcShapeRepresentation")
@classmethod
def flip_object(cls, obj, flip_local_axes):
assert len(flip_local_axes) == 2, "flip_local_axes must be two axes to flip"
rotation_axis = next(i for i in "XYZ" if i not in flip_local_axes)
rotation_axis_i = "XYZ".index(rotation_axis)
bb_data = tool.Blender.get_object_bounding_box(obj)
# min max points of rotated plane of origin based bounding box
min_point = Vector([min(i, 0) for i in bb_data["min_point"]])
max_point = Vector([max(i, 0) for i in bb_data["max_point"]])
# keep it in rotated plane only
max_point[rotation_axis_i] = min_point[rotation_axis_i]
# to compensate for flipped two axes
# we adjust new max point to match previous min point (or vice versa)
original_min_point = obj.matrix_world @ min_point
obj.matrix_world = obj.matrix_world @ Matrix.Rotation(pi, 4, rotation_axis)
new_max_point = obj.matrix_world @ max_point
obj.matrix_world.translation += original_min_point - new_max_point
bpy.context.view_layer.update()
+5 -54
View File
@@ -174,15 +174,7 @@ class Georeference(blenderbim.core.tool.Georeference):
)
@classmethod
def get_map_conversion(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.HasCoordinateOperation:
return context.HasCoordinateOperation[0]
@classmethod
def xyz2enh(cls, coordinates, map_conversion):
def xyz2enh(cls, coordinates):
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
coordinates = ifcopenshell.util.geolocation.xyz2enh(
@@ -196,53 +188,12 @@ class Georeference(blenderbim.core.tool.Georeference):
float(props.blender_x_axis_ordinate),
1.0,
)
if map_conversion:
unit = map_conversion.TargetCRS.MapUnit
e = map_conversion.Eastings
n = map_conversion.Northings
h = map_conversion.OrthogonalHeight
if unit:
scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
e = ifcopenshell.util.unit.convert(e, getattr(unit, "Prefix", None), unit.Name, None, None) / scale
n = ifcopenshell.util.unit.convert(n, getattr(unit, "Prefix", None), unit.Name, None, None) / scale
h = ifcopenshell.util.unit.convert(h, getattr(unit, "Prefix", None), unit.Name, None, None) / scale
coordinates = ifcopenshell.util.geolocation.xyz2enh(
coordinates[0],
coordinates[1],
coordinates[2],
e,
n,
h,
map_conversion.XAxisAbscissa or 1.0,
map_conversion.XAxisOrdinate or 0.0,
map_conversion.Scale or 1.0,
)
return coordinates
return ifcopenshell.util.geolocation.auto_xyz2enh(tool.Ifc.get(), *coordinates)
@classmethod
def enh2xyz(cls, coordinates, map_conversion):
def enh2xyz(cls, coordinates):
coordinates = ifcopenshell.util.geolocation.auto_enh2xyz(tool.Ifc.get(), *coordinates)
props = bpy.context.scene.BIMGeoreferenceProperties
if map_conversion:
unit = map_conversion.TargetCRS.MapUnit
e = map_conversion.Eastings
n = map_conversion.Northings
h = map_conversion.OrthogonalHeight
if unit:
scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
e = ifcopenshell.util.unit.convert(e, getattr(unit, "Prefix", None), unit.Name, None, None) / scale
n = ifcopenshell.util.unit.convert(n, getattr(unit, "Prefix", None), unit.Name, None, None) / scale
h = ifcopenshell.util.unit.convert(h, getattr(unit, "Prefix", None), unit.Name, None, None) / scale
coordinates = ifcopenshell.util.geolocation.enh2xyz(
coordinates[0],
coordinates[1],
coordinates[2],
e,
n,
h,
map_conversion.XAxisAbscissa or 1.0,
map_conversion.XAxisOrdinate or 0.0,
map_conversion.Scale or 1.0,
)
if props.has_blender_offset:
coordinates = ifcopenshell.util.geolocation.enh2xyz(
coordinates[0],
@@ -316,7 +267,7 @@ class Georeference(blenderbim.core.tool.Georeference):
rows = parse_csv(filepath)
vertices = []
for row in rows:
coordinates = cls.enh2xyz([float(row[0]), float(row[1]), float(row[2])], map_conversion)
coordinates = cls.enh2xyz([float(row[0]), float(row[1]), float(row[2])])
vertices.append(coordinates)
mesh = bpy.data.meshes.new("mesh")
@@ -97,8 +97,7 @@ class TestSetCursorLocation:
class TestConvertLocalToGlobal:
def test_run(self, georeference):
georeference.get_coordinates("input").should_be_called().will_return("coordinates")
georeference.get_map_conversion().should_be_called().will_return("map_conversion")
georeference.xyz2enh("coordinates", "map_conversion").should_be_called().will_return("enh")
georeference.xyz2enh("coordinates").should_be_called().will_return("enh")
georeference.set_coordinates("output", "enh").should_be_called()
georeference.set_cursor_location("enh").should_be_called()
subject.convert_local_to_global(georeference)
@@ -107,8 +106,7 @@ class TestConvertLocalToGlobal:
class TestConvertGlobalToLocal:
def test_run(self, georeference):
georeference.get_coordinates("input").should_be_called().will_return("coordinates")
georeference.get_map_conversion().should_be_called().will_return("map_conversion")
georeference.enh2xyz("coordinates", "map_conversion").should_be_called().will_return("xyz")
georeference.enh2xyz("coordinates").should_be_called().will_return("xyz")
georeference.set_coordinates("output", "xyz").should_be_called()
georeference.set_cursor_location("xyz").should_be_called()
subject.convert_global_to_local(georeference)
+20 -18
View File
@@ -247,25 +247,21 @@ class TestSetBlenderTrueNorth(NewFile):
assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == 45
class TestGetMapConversion(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
assert subject.get_map_conversion().is_a("IfcMapConversion")
class TestXyz2Enh(NewFile):
def test_run(self):
assert subject.xyz2enh([0.0, 0.0, 0.0], None) == [0.0, 0.0, 0.0]
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (0.0, 0.0, 0.0)
def test_using_the_blender_offset(self):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
props = bpy.context.scene.BIMGeoreferenceProperties
props.has_blender_offset = True
props.blender_eastings = "1.0"
assert subject.xyz2enh([0.0, 0.0, 0.0], None) == (1.0, 0.0, 0.0)
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0)
def test_using_the_map_conversion(self):
ifc = ifcopenshell.file()
@@ -275,7 +271,7 @@ class TestXyz2Enh(NewFile):
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
map_conversion = ifc.by_type("IfcMapConversion")[0]
map_conversion.Eastings = 1.0
assert subject.xyz2enh([0.0, 0.0, 0.0], map_conversion) == (1.0, 0.0, 0.0)
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0)
def test_applying_both_blender_offset_and_map_conversion(self):
props = bpy.context.scene.BIMGeoreferenceProperties
@@ -288,18 +284,24 @@ class TestXyz2Enh(NewFile):
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
map_conversion = ifc.by_type("IfcMapConversion")[0]
map_conversion.Northings = 1.0
assert subject.xyz2enh([0.0, 0.0, 0.0], map_conversion) == (1.0, 1.0, 0.0)
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 1.0, 0.0)
class TestEnh2Xyz(NewFile):
def test_run(self):
assert subject.enh2xyz([0.0, 0.0, 0.0], None) == [0.0, 0.0, 0.0]
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (0.0, 0.0, 0.0)
def test_using_the_blender_offset(self):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
props = bpy.context.scene.BIMGeoreferenceProperties
props.has_blender_offset = True
props.blender_eastings = "1.0"
assert subject.enh2xyz([0.0, 0.0, 0.0], None) == (-1.0, 0.0, 0.0)
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0)
def test_using_the_map_conversion(self):
ifc = ifcopenshell.file()
@@ -309,7 +311,7 @@ class TestEnh2Xyz(NewFile):
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
map_conversion = ifc.by_type("IfcMapConversion")[0]
map_conversion.Eastings = 1.0
assert subject.enh2xyz([0.0, 0.0, 0.0], map_conversion) == (-1.0, 0.0, 0.0)
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0)
def test_applying_both_blender_offset_and_map_conversion(self):
props = bpy.context.scene.BIMGeoreferenceProperties
@@ -322,7 +324,7 @@ class TestEnh2Xyz(NewFile):
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
map_conversion = ifc.by_type("IfcMapConversion")[0]
map_conversion.Northings = 1.0
assert subject.enh2xyz([0.0, 0.0, 0.0], map_conversion) == (-1.0, -1.0, 0.0)
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, -1.0, 0.0)
class TestSetIfcGridNorth(NewFile):
+37 -9
View File
@@ -52,12 +52,14 @@ class TestGenerateOccurrenceName(NewFile):
bpy.context.scene.BIMModelProperties.occurrence_name_function = '"Foobar"'
assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar"
class TestGetManualBooleans(NewFile):
def test_run(self):
assert isinstance(subject(), blenderbim.core.tool.Model)
def test_len_returned_boolean(self):
def setup_profile_represntation(self, clippings=[]):
ifc = ifcopenshell.file()
self.ifc = ifc
tool.Ifc.set(ifc)
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
length = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT")
@@ -65,17 +67,43 @@ class TestGetManualBooleans(NewFile):
ifcopenshell.api.run("unit.assign_unit", ifc)
element = ifc.createIfcColumn()
hea100 = ifc.create_entity(
"IfcIShapeProfileDef", ProfileName="HEA100", ProfileType="AREA",
OverallWidth=100, OverallDepth=96, WebThickness=5, FlangeThickness=8, FilletRadius=12,
"IfcIShapeProfileDef",
ProfileName="HEA100",
ProfileType="AREA",
OverallWidth=100,
OverallDepth=96,
WebThickness=5,
FlangeThickness=8,
FilletRadius=12,
)
model3d = ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
body = ifcopenshell.api.run("context.add_context", ifc,context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d)
representation = ifcopenshell.api.run("geometry.add_profile_representation", ifc, context=body, profile=hea100, depth=5)
body = ifcopenshell.api.run(
"context.add_context",
ifc,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model3d,
)
representation = ifcopenshell.api.run(
"geometry.add_profile_representation", ifc, context=body, profile=hea100, depth=5, clippings=clippings
)
ifcopenshell.api.run("geometry.assign_representation", ifc, product=element, representation=representation)
return element, representation
def test_manual_booleans(self):
element, representation = self.setup_profile_represntation()
matrix = np.eye(4)
matrix = ifcopenshell.util.placement.rotation(45,"X") @ matrix
matrix[:,3][0:3] = (0, 0, 3)
matrix = matrix.tolist()
ifcopenshell.api.run("geometry.add_boolean", ifc, representation = representation, type = "IfcHalfSpaceSolid", matrix = matrix)
ifcopenshell.api.run(
"geometry.add_boolean", self.ifc, representation=representation, type="IfcHalfSpaceSolid", matrix=matrix
)
assert len(subject.get_manual_booleans(element)) == 1
def test_automatic_booleans_ignored(self):
clipping = {
"type": "IfcBooleanClippingResult",
"operand_type": "IfcHalfSpaceSolid",
"matrix": np.eye(4).tolist(),
}
element, representation = self.setup_profile_represntation(clippings=[clipping])
assert len(subject.get_manual_booleans(element)) == 0
+4 -1
View File
@@ -461,6 +461,7 @@ int main(int argc, char** argv) {
("space-name-transform", po::value<std::string>(),
"Additional transform to the space labels in SVG")
("edge-arrows", "Adds arrow heads to edge segments to signify edge direction")
("ecef", "Write glTF in Earth-Centered Earth-Fixed coordinates. Requires PROJ")
;
po::options_description cmdline_options;
@@ -527,6 +528,7 @@ int main(int argc, char** argv) {
const bool generate_uvs = vmap.count("generate-uvs") != 0;
const bool validate = vmap.count("validate") != 0;
const bool edge_arrows = vmap.count("edge-arrows") != 0;
const bool write_gltf_ecef = vmap.count("ecef") != 0;
const bool no_wire_intersection_check = vmap.count("no-wire-intersection-check") != 0;
const bool no_wire_intersection_tolerance = vmap.count("no-wire-intersection-tolerance") != 0;
const bool strict_tolerance = vmap.count("strict-tolerance") != 0;
@@ -840,7 +842,8 @@ int main(int argc, char** argv) {
settings.set(SerializerSettings::USE_MATERIAL_NAMES, use_material_names);
settings.set(SerializerSettings::USE_ELEMENT_TYPES, use_element_types);
settings.set(SerializerSettings::USE_ELEMENT_HIERARCHY, use_element_hierarchy);
settings.set_deflection_tolerance(deflection_tolerance);
settings.set(SerializerSettings::WRITE_GLTF_ECEF, write_gltf_ecef);
settings.set_deflection_tolerance(deflection_tolerance);
settings.set_angular_tolerance(angular_tolerance);
settings.precision = precision;
+56 -40
View File
@@ -64,9 +64,10 @@ def get_facility_data(ifc_file, element):
"Name": element.Name,
"ProjectName": ifc_file.by_type("IfcProject")[0].Name,
"SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None),
"Category": get_classification(element),
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"ClassificationIdentification": get_classification_identification(element),
"ClassificationName": get_classification_name(element),
"OrganizationName": get_owner_name(element),
"CreationDate": get_owner_creation_date(element),
"ModelSoftware": get_owner_application(element),
"ModelProjectID": ifc_file.by_type("IfcProject")[0].GlobalId,
"ModelSiteID": getattr(get_facility_parent(element, "IfcSite"), "GlobalId", None),
@@ -80,9 +81,10 @@ def get_facility_data(ifc_file, element):
def get_storey_data(ifc_file, element):
return {
"Name": element.Name,
"Category": "Level",
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"ClassificationIdentification": "Level",
"ClassificationName": get_classification_name(element),
"OrganizationName": get_owner_name(element),
"CreationDate": get_owner_creation_date(element),
"ModelSoftware": get_owner_application(element),
"ModelObject": element.is_a(),
"ModelID": element.GlobalId,
@@ -95,10 +97,11 @@ def get_space_data(ifc_file, element):
return {
"Name": element.Name,
"Description": element.LongName,
"Category": get_classification(element),
"ClassificationIdentification": get_classification_identification(element),
"ClassificationName": get_classification_name(element),
"LevelName": getattr(get_facility_parent(element, "IfcBuildingStorey"), "Name", None),
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"OrganizationName": get_owner_name(element),
"CreationDate": get_owner_creation_date(element),
"ModelSoftware": get_owner_application(element),
"ModelID": element.GlobalId,
"AreaGross": get_property(psets, "Qto_SpaceBaseQuantities", "GrossFloorArea", decimals=2),
@@ -111,8 +114,8 @@ def get_zone_data(ifc_file, element):
return {
"Name": zone.Name,
"SpaceName": space.Name,
"AuthorOrganizationName": get_owner_name(zone),
"AuthorDate": get_owner_creation_date(zone),
"OrganizationName": get_owner_name(zone),
"CreationDate": get_owner_creation_date(zone),
"ModelSoftware": get_owner_application(zone),
"ModelID": zone.GlobalId,
}
@@ -123,9 +126,10 @@ def get_element_type_data(ifc_file, element):
return {
"Name": element.Name,
"Description": element.Description,
"Category": get_classification(element),
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"ClassificationIdentification": get_classification_identification(element),
"ClassificationName": get_classification_name(element),
"OrganizationName": get_owner_name(element),
"CreationDate": get_owner_creation_date(element),
"ModelSoftware": get_owner_application(element),
"ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
"ModelID": element.GlobalId,
@@ -149,8 +153,8 @@ def get_element_data(ifc_file, element):
"TypeName": ifcopenshell.util.element.get_type(element).Name,
"SpaceName": space_name,
"SystemName": system,
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"OrganizationName": get_owner_name(element),
"CreationDate": get_owner_creation_date(element),
"ModelSoftware": get_owner_application(element),
"ModelObject": "{}[{}]".format(element.is_a(), ifcopenshell.util.element.get_predefined_type(element)),
"ModelID": element.GlobalId,
@@ -169,9 +173,10 @@ def get_system_data(ifc_file, element):
return {
"Name": element.Name,
"Description": element.Description,
"Category": get_classification(element),
"AuthorOrganizationName": get_owner_name(element),
"AuthorDate": get_owner_creation_date(element),
"ClassificationIdentification": get_classification_identification(element),
"ClassificationName": get_classification_name(element),
"OrganizationName": get_owner_name(element),
"CreationDate": get_owner_creation_date(element),
"ModelSoftware": get_owner_application(element),
"ModelID": element.GlobalId,
}
@@ -205,12 +210,18 @@ def get_facility_parent(element, ifc_class):
parent = ifcopenshell.util.element.get_aggregate(parent)
def get_classification(element):
def get_classification_identification(element):
references = list(ifcopenshell.util.classification.get_references(element))
if references:
if hasattr(references[0], "Identification"):
return "{}:{}".format(references[0].Identification, references[0].Name)
return "{}:{}".format(references[0].ItemReference, references[0].Name)
return references[0].Identification
return references[0].ItemReference
def get_classification_name(element):
references = list(ifcopenshell.util.classification.get_references(element))
if references:
return references[0].Name
def get_property(psets, pset_name, prop_name, decimals=None):
@@ -239,9 +250,10 @@ config = {
"Name",
"ProjectName",
"SiteName",
"Category",
"AuthorOrganizationName",
"AuthorDate",
"ClassificationIdentification",
"ClassificationName",
"OrganizationName",
"CreationDate",
"ModelSoftware",
"ModelProjectID",
"ModelSiteID",
@@ -259,9 +271,10 @@ config = {
"keys": ["Name"],
"headers": [
"Name",
"Category",
"AuthorOrganizationName",
"AuthorDate",
"ClassificationIdentification",
"ClassificationName",
"OrganizationName",
"CreationDate",
"ModelSoftware",
"ModelObject",
"ModelID",
@@ -277,10 +290,11 @@ config = {
"headers": [
"Name",
"Description",
"Category",
"ClassificationIdentification",
"ClassificationName",
"LevelName",
"AuthorOrganizationName",
"AuthorDate",
"OrganizationName",
"CreationDate",
"ModelSoftware",
"ModelID",
"AreaGross",
@@ -293,7 +307,7 @@ config = {
},
"Zones": {
"keys": ["Name", "SpaceName"],
"headers": ["Name", "SpaceName", "AuthorOrganizationName", "AuthorDate", "ModelSoftware", "ModelID"],
"headers": ["Name", "SpaceName", "OrganizationName", "CreationDate", "ModelSoftware", "ModelID"],
"colours": "prreee",
"sort": [{"name": "Name", "order": "ASC"}],
"get_category_elements": get_zones,
@@ -304,9 +318,10 @@ config = {
"headers": [
"Name",
"Description",
"Category",
"AuthorOrganizationName",
"AuthorDate",
"ClassificationIdentification",
"ClassificationName",
"OrganizationName",
"CreationDate",
"ModelSoftware",
"ModelObject",
"ModelID",
@@ -329,8 +344,8 @@ config = {
"TypeName",
"SpaceName",
"SystemName",
"AuthorOrganizationName",
"AuthorDate",
"OrganizationName",
"CreationDate",
"ModelSoftware",
"ModelObject",
"ModelID",
@@ -353,9 +368,10 @@ config = {
"headers": [
"Name",
"Description",
"Category",
"AuthorOrganizationName",
"AuthorDate",
"ClassificationIdentification",
"ClassificationName",
"OrganizationName",
"CreationDate",
"ModelSoftware",
"ModelID",
],
+20 -53
View File
@@ -320,27 +320,21 @@ def get_facility_data(ifc_file, element):
site_name = None
site_description = None
if site:
site_name = val(site.Name) or val(site.LongName) or site.GlobalId
site_description = val(site.Description) or val(site.LongName) or val(site.Name)
site_name = val(site.Name)
site_description = val(site.Description)
project = None
project_name = None
project_description = None
try:
project = ifc_file.by_type("IfcProject")[0]
project_name = val(project.Name) or val(project.LongName) or project.GlobalId
project_description = val(project.Description) or val(project.LongName) or val(project.Name)
project_name = val(project.Name)
project_description = val(project.Description)
except:
pass
name = val(element.Name) or val(element.LongName)
if not name:
name = val(project.Name) or val(project.LongName)
if not name and site:
name = val(site.Name) or val(site.LongName)
return {
"Name": name,
"Name": val(element.Name),
"CreatedBy": get_created_by(element),
"CreatedOn": get_created_on(element),
"Category": get_category(element),
@@ -353,12 +347,12 @@ def get_facility_data(ifc_file, element):
"AreaMeasurement": get_area_measurement(element),
"ExternalSystem": get_external_system(element),
"ExternalProjectObject": "IfcProject",
"ExternalProjectIdentifier": project.GlobalId if project else ifcopenshell.guid.new(),
"ExternalProjectIdentifier": project.GlobalId if project else None,
"ExternalSiteObject": "IfcSite",
"ExternalSiteIdentifier": site.GlobalId if site else ifcopenshell.guid.new(),
"ExternalSiteIdentifier": site.GlobalId if site else None,
"ExternalFacilityObject": "IfcBuilding",
"ExternalFacilityIdentifier": element.GlobalId,
"Description": val(element.Description) or val(element.LongName) or val(element.Name),
"Description": val(element.Description),
"ProjectDescription": project_description,
"SiteDescription": site_description,
"Phase": val(project.Phase) if project else None,
@@ -366,10 +360,6 @@ def get_facility_data(ifc_file, element):
def get_floor_data(ifc_file, element):
external_object = element.is_a()
if element.ObjectType and element.ObjectType.lower() in ("site", "ifcsite"):
external_object = "IfcSite"
height_names = {
"Height",
"NetHeight",
@@ -400,9 +390,9 @@ def get_floor_data(ifc_file, element):
"CreatedOn": get_created_on(element),
"Category": get_category(element),
"ExternalSystem": get_external_system(element),
"ExternalObject": external_object,
"ExternalObject": element.is_a(),
"ExternalIdentifier": element.GlobalId,
"Description": val(element.Description) or val(element.LongName) or val(element.Name),
"Description": val(element.Description),
"Elevation": val(elevation),
"Height": height,
}
@@ -439,7 +429,7 @@ def get_space_data(ifc_file, element):
"CreatedOn": get_created_on(element),
"Category": get_category(element),
"FloorName": floor_name,
"Description": val(element.Description) or val(element.LongName) or val(element.Name),
"Description": val(element.Description),
"ExternalSystem": get_external_system(element),
"ExternalObject": element.is_a(),
"ExternalIdentifier": element.GlobalId,
@@ -976,6 +966,7 @@ def get_owner_name(element):
def get_created_on(element):
if getattr(element, "OwnerHistory", None):
return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat()
return "1900-12-31T23:59:59" # Yes, really
def get_external_system(element):
@@ -1009,13 +1000,20 @@ def get_area_measurement(element):
if result:
return result
psets = ifcopenshell.util.element.get_psets(element)
if psets:
for _, props in psets.items():
for name, value in props.items():
if name == "MethodOfMeasurement":
return value
def get_category(element):
references = list(ifcopenshell.util.classification.get_references(element))
results = []
for reference in references:
if reference.is_a("IfcClassification"):
results.append(reference.Name)
continue
elif reference.is_a("IfcClassificationReference"):
identification = val(getattr(reference, "Identification", getattr(reference, "ItemReference", None)))
if val(reference.Name) and identification and val(reference.Name) != identification:
@@ -1024,40 +1022,9 @@ def get_category(element):
results.append(reference.Name)
elif identification:
results.append(identification)
elif reference.ReferencedSource and val(reference.ReferencedSource.Name):
results.append(reference.ReferencedSource.Name)
elif val(reference.Location):
results.append(reference.Location)
if results:
return ",".join(results)
category_props = [
("Assembly Code", "Assembly Description"),
("Category Code", "Category Description"),
("Classification Code", "Classification Description"),
("OmniClass Number", "OmniClass Title"),
("Uniclass Code", "Uniclass Description"),
]
psets = ifcopenshell.util.element.get_psets(element)
properties = {}
if psets:
for _, props in psets.items():
properties.update(props)
for code, description in category_props:
code = val(properties.get(code, None))
if code:
description = val(properties.get(description, None))
if code and description:
results.append(code + " : " + description)
else:
results.append(code)
if results:
return ",".join(results)
return val(getattr(element, "ObjectType", None))
def get_pao_address(person, organization, name):
for actor in [organization, person]:
+1
View File
@@ -993,6 +993,7 @@ def get_owner_name(element):
def get_created_on(element):
if getattr(element, "OwnerHistory", None):
return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat()
return "1900-12-31T23:59:59" # Yes, really
def get_external_system(element):
@@ -49,8 +49,11 @@ public:
/// Use Y UP .
/// Applicable for OBJ output.
USE_Y_UP = 1ULL << (IfcGeom::IteratorSettings::NUM_SETTINGS + 7ULL),
/// Write in ECEF coordinates.
/// Applicable to glTF output
WRITE_GLTF_ECEF = 1ULL << (IfcGeom::IteratorSettings::NUM_SETTINGS + 8ULL),
/// Number of different setting flags.
NUM_SETTINGS = 7
NUM_SETTINGS = 8
};
SerializerSettings()
+1 -1
View File
@@ -103,7 +103,7 @@ endif
mkdir -p dist/ifcopenshell
cp -r ifcopenshell/* dist/ifcopenshell/
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-9cc1f5f-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f0e03c7-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell/ifcopenshell_wrapper.py dist/ifcopenshell/
ifeq ($(PLATFORM), win)
@@ -20,11 +20,11 @@ Pre-built packages
| build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ |
+----------------+----------------+----------------+----------------+------------------+
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-linux64.zip
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-win32.zip
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-win64.zip
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-macos64.zip
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-9cc1f5f-macosm164.zip
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-linux64.zip
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-win32.zip
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-win64.zip
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-macos64.zip
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f0e03c7-macosm164.zip
2. Unzip the downloaded file and run IfcConvert using the command line.
@@ -41,34 +41,34 @@ changes in the IfcOpenShell C++ core.
| Python 3.11 | py311-linux64_ | py311-win32_ | py311-win64_ | N/A | py311-macosm164_ |
+-------------+----------------+----------------+----------------+----------------+------------------+
.. _py36-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-9cc1f5f-linux64.zip
.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-linux64.zip
.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-linux64.zip
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-linux64.zip
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-linux64.zip
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9cc1f5f-linux64.zip
.. _py36-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-9cc1f5f-win32.zip
.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-win32.zip
.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-win32.zip
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-win32.zip
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-win32.zip
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9cc1f5f-win32.zip
.. _py36-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-9cc1f5f-win64.zip
.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-win64.zip
.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-win64.zip
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-win64.zip
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-win64.zip
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9cc1f5f-win64.zip
.. _py36-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-9cc1f5f-macos64.zip
.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-macos64.zip
.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-macos64.zip
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-macos64.zip
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-macos64.zip
.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-9cc1f5f-macosm164.zip
.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-9cc1f5f-macosm164.zip
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-9cc1f5f-macosm164.zip
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-9cc1f5f-macosm164.zip
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-9cc1f5f-macosm164.zip
.. _py36-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-f0e03c7-linux64.zip
.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-linux64.zip
.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-linux64.zip
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-linux64.zip
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-linux64.zip
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f0e03c7-linux64.zip
.. _py36-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-f0e03c7-win32.zip
.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-win32.zip
.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-win32.zip
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-win32.zip
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-win32.zip
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f0e03c7-win32.zip
.. _py36-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-f0e03c7-win64.zip
.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-win64.zip
.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-win64.zip
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-win64.zip
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-win64.zip
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f0e03c7-win64.zip
.. _py36-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-36-v0.7.0-f0e03c7-macos64.zip
.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-macos64.zip
.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-macos64.zip
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-macos64.zip
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-macos64.zip
.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-f0e03c7-macosm164.zip
.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-f0e03c7-macosm164.zip
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f0e03c7-macosm164.zip
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f0e03c7-macosm164.zip
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f0e03c7-macosm164.zip
2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your
Python path. If you're not sure where your Python path is, run the following
@@ -11,15 +11,15 @@ Packaged installation
---------------------
IfcSverchok is packaged like a regular Blender add-on, so installation is the
same as any other Blender add-on. You can download the package for installation
at the `Get BlenderBIM <https://blenderbim.org/download.html>`__ website.
same as any other Blender add-on. `Download IfcSverchok here
<https://blenderbim.org/builds/ifcsverchok-230823.zip>`__.
Like all Blender add-ons, they can be installed using ``Edit > Preferences >
Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox``. You can
enable add-ons permanently by using ``Save User Settings`` from the Addons menu.
Before installing, you will also need to `install the BlenderBIM Add-on
<../blenderbim/installation>`__ and `install Sverchok
<https://blenderbim.org/download.html>`__ and `install Sverchok
<https://github.com/nortikin/sverchok#installation>`__.
If you downloaded Blender as a ``.zip`` file without running an installer, you
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
import numpy as np
class Usecase:
@@ -44,7 +45,6 @@ class Usecase:
elif self.settings["type"] == "Mesh":
if self.settings["blender_obj"]:
result = self.create_blender_mesh()
representation_type = "Clipping"
items = []
for item in self.settings["representation"].Items:
# For now, we don't use IfcBooleanClippingResult.
@@ -56,23 +56,13 @@ class Usecase:
self.settings["representation"].Items = items
def create_half_space_solid(self):
clipping = self.settings["matrix"]
return self.file.createIfcHalfSpaceSolid(
self.file.createIfcPlane(
self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint(
(
self.convert_si_to_unit(clipping[0][3]),
self.convert_si_to_unit(clipping[1][3]),
self.convert_si_to_unit(clipping[2][3]),
)
),
self.file.createIfcDirection((clipping[0][2], clipping[1][2], clipping[2][2])),
self.file.createIfcDirection((clipping[0][0], clipping[1][0], clipping[2][0])),
)
),
False,
)
clipping = np.array(self.settings["matrix"])[:3]
local_z = self.file.createIfcDirection(clipping[:, 2].tolist())
local_x = self.file.createIfcDirection(clipping[:, 0].tolist())
point = self.file.createIfcCartesianPoint(self.convert_si_to_unit(clipping[:, 3]).tolist())
placement = self.file.createIfcAxis2Placement3D(point, local_z, local_x)
plane = self.file.createIfcPlane(placement)
return self.file.createIfcHalfSpaceSolid(plane, AgreementFlag=False)
def create_blender_mesh(self):
self.ifc_vertices = []
@@ -61,7 +61,7 @@ class Usecase:
wall = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcWall")
# Let's construct that wall!
ifcopenshell.api.run("sequence.assign_product", relating_product=wall, related_object=task)
ifcopenshell.api.run("sequence.assign_product", model, relating_product=wall, related_object=task)
"""
self.file = file
self.settings = {
@@ -23,11 +23,34 @@ import ifcopenshell.util.unit
def dms2dd(degrees, minutes, seconds, ms=0):
"""Convert degrees, minutes, and (milli)seconds to decimal degrees
:param degrees: The degrees component
:type degrees: int
:param minutes: The minutes component
:type minutes: int
:param seconds: The seconds component
:type seconds: int
:param ms: The milliseconds component
:type ms: int
:return: The angle in decimal degrees.
:rtype: float
"""
dd = float(degrees) + float(minutes) / 60.0 + float(seconds) / (3600.0) + float(ms / 3600000000.0)
return dd
def dd2dms(dd, use_ms=False):
"""Convert decimal degrees to degrees, minutes, and (milli)seconds format
:param dd: The decimal degrees
:type dd: float
:param use_ms: True if to include milliseconds and false otherwise. Defaults to false.
:type use_ms: bool
:return: The angle in a tuple of either 3 or 4 values, being degrees,
minutes, seconds, and optionally milliseconds.
:rtype: tuple[float]
"""
dd = float(dd)
sign = 1 if dd >= 0 else -1
dd = abs(dd)
@@ -43,6 +66,43 @@ def dd2dms(dd, use_ms=False):
def xyz2enh(x, y, z, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None):
"""Manually convert local XYZ coordinates to map eastings, northings, and height
This function is for advanced users as it allows you to specify your own
helmert transformation parameters (i.e. those typically stored in
IfcMapConversion). This manual approach is useful for tests or in case your
are setting your helmert transformations in non-standard locations, or if
you are applying your own temporary false origin (such as when federating
models for digital twins of large cities).
No unit conversion is performed.
For most scenarios you should use ``auto_xyz2enh`` instead.
:param x: The X local engineering coordinate.
:type x: float
:param y: The Y local engineering coordinate.
:type y: float
:param z: The Z local engineering coordinate.
:type z: float
:param eastings: The eastings offset to apply.
:type eastings: float
:param northings: The northings offset to apply.
:type northings: float
:param orthogonal_height: The orthogonal height offset to apply.
:type orthogonal_height: float
:param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:type x_axis_abscissa: float
:param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:type x_axis_ordinate: float
:param scale: The combined scale factor to convert from local coordinates
to map coordinates.
:type scale: float
:return: A tuple of three ordinates representing the easting, northing and height.
:rtype: tuple[float]
"""
if scale is None:
scale = 1.0
rotation = math.atan2(x_axis_ordinate, x_axis_abscissa)
@@ -76,20 +136,60 @@ def xyz2enh_ifc4x3(
def auto_xyz2enh(ifc_file, x, y, z):
"""Convert from local XYZ coordinates to global map coordinate eastings, northings, and heights
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the Z coordinate is returned unchanged.
For IFC2X3, the map conversion is detected from the IfcProject's
ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC":
https://www.buildingsmart.org/standards/bsi-standards/standards-library/
:param ifc_file: The IFC file
:type ifc_file: ifcopenshell.file.file
:param x: The X local engineering coordinate provided in project length units.
:type x: float
:param y: The Y local engineering coordinate provided in project length units.
:type y: float
:param z: The Z local engineering coordinate provided in project length units.
:type z: float
:return: The global map coordinate eastings, northings, and height in map units.
:rtype: tuple[float]
"""
conversion = None
try:
conversion = ifc_file.by_type("IfcMapConversion")
except:
return (x, y, z)
if not conversion:
return (x, y, z)
conversion = conversion[0]
e = conversion.Eastings or 0
n = conversion.Northings or 0
h = conversion.OrthogonalHeight or 0
xaa = conversion.XAxisAbscissa or 0
xao = conversion.XAxisOrdinate or 0
scale = conversion.Scale or 0
map_unit = conversion.TargetCRS.MapUnit
pass
if conversion:
conversion = conversion[0]
e = conversion.Eastings or 0
n = conversion.Northings or 0
h = conversion.OrthogonalHeight or 0
xaa = conversion.XAxisAbscissa or 0
xao = conversion.XAxisOrdinate or 0
scale = conversion.Scale or 1
map_unit = conversion.TargetCRS.MapUnit
else:
project = ifc_file.by_type("IfcProject")[0]
conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")
if not conversion:
return (x, y, z)
e = conversion.get("Eastings", None) or 0
n = conversion.get("Northings", None) or 0
h = conversion.get("OrthogonalHeight", None) or 0
xaa = conversion.get("XAxisAbscissa", None) or 0
xao = conversion.get("XAxisOrdinate", None) or 0
scale = conversion.get("Scale", None) or 1
map_unit = None
if not xaa and not xao:
xaa = 1.0
xao = 0.0
if map_unit:
# Warning! This definition has changed in IFC4X3 such that map_unit no
# longer affects unit conversion, only the Scale attribute affects unit
@@ -103,6 +203,74 @@ def auto_xyz2enh(ifc_file, x, y, z):
return xyz2enh(x, y, z, e, n, h, xaa, xao, scale)
def auto_enh2xyz(ifc_file, easting, northing, height):
"""Convert from global map coordinate eastings, northings, and heights to local XYZ coordinates
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the Z coordinate is returned unchanged.
For IFC2X3, the map conversion is detected from the IfcProject's
ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC":
https://www.buildingsmart.org/standards/bsi-standards/standards-library/
:param ifc_file: The IFC file
:type ifc_file: ifcopenshell.file.file
:param easting: The global easting map coordinate provided in map units.
:type easting: float
:param northing: The global northing map coordinate provided in map units.
:type northing: float
:param height: The global height map coordinate provided in map units.
:type height: float
:return: The local engineering XYZ coordinates in project length units.
:rtype: tuple[float]
"""
conversion = None
try:
conversion = ifc_file.by_type("IfcMapConversion")
except:
pass
if conversion:
conversion = conversion[0]
e = conversion.Eastings or 0
n = conversion.Northings or 0
h = conversion.OrthogonalHeight or 0
xaa = conversion.XAxisAbscissa or 0
xao = conversion.XAxisOrdinate or 0
scale = conversion.Scale or 1
map_unit = conversion.TargetCRS.MapUnit
else:
project = ifc_file.by_type("IfcProject")[0]
conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")
if not conversion:
return (easting, northing, height)
e = conversion.get("Eastings", None) or 0
n = conversion.get("Northings", None) or 0
h = conversion.get("OrthogonalHeight", None) or 0
xaa = conversion.get("XAxisAbscissa", None) or 0
xao = conversion.get("XAxisOrdinate", None) or 0
scale = conversion.get("Scale", None) or 1
map_unit = None
if not xaa and not xao:
xaa = 1.0
xao = 0.0
if map_unit:
# Warning! This definition has changed in IFC4X3 such that map_unit no
# longer affects unit conversion, only the Scale attribute affects unit
# conversion. TODO: consolidate once IFC4X3 confirmed.
project_unit = ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT")
map_prefix = getattr(map_unit, "Prefix", None)
project_prefix = getattr(project_unit, "Prefix", None)
e = ifcopenshell.util.unit.convert(e, map_prefix, map_unit.Name, project_prefix, project_unit.Name)
n = ifcopenshell.util.unit.convert(n, map_prefix, map_unit.Name, project_prefix, project_unit.Name)
h = ifcopenshell.util.unit.convert(h, map_prefix, map_unit.Name, project_prefix, project_unit.Name)
return enh2xyz(easting, northing, height, e, n, h, xaa, xao, scale)
def auto_z2e(ifc_file, z):
"""Convert a Z coordinate to an elevation using model georeferencing data
@@ -110,6 +278,10 @@ def auto_z2e(ifc_file, z):
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the Z coordinate is returned unchanged.
For IFC2X3, the map conversion is detected from the IfcProject's
ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC":
https://www.buildingsmart.org/standards/bsi-standards/standards-library/
:param ifc_file: The IFC file
:type ifc_file: ifcopenshell.file.file
:param z: The Z local engineering coordinate provided in project length units.
@@ -117,15 +289,25 @@ def auto_z2e(ifc_file, z):
:return: The elevation in project length units.
:rtype: float
"""
conversion = None
try:
conversion = ifc_file.by_type("IfcMapConversion")
except:
return z
if not conversion or not conversion[0].OrthogonalHeight:
return z
conversion = conversion[0]
h = conversion.OrthogonalHeight
map_unit = conversion.TargetCRS.MapUnit
pass
if conversion and not conversion[0].OrthogonalHeight:
conversion = conversion[0]
h = conversion.OrthogonalHeight
map_unit = conversion.TargetCRS.MapUnit
else:
project = ifc_file.by_type("IfcProject")[0]
conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")
if not conversion:
return z
h = conversion.get("OrthogonalHeight", None) or 0
map_unit = None
if map_unit:
# Warning! This definition has changed in IFC4X3 such that map_unit no
# longer affects unit conversion, only the Scale attribute affects unit
@@ -142,10 +324,61 @@ def auto_z2e(ifc_file, z):
def z2e(z, h):
"""Manually convert a Z coordinate to an elevation
This function is for advanced users as it allows you to specify your own
orthogonal height offset.
For most scenarios you should use ``auto_z2e`` instead.
:param z: The Z local engineering coordinate provided in project length units.
:type z: float
:param h: The orthogonal height offset in project length units.
:type h: float
:return: The elevation in project length units.
:rtype: float
"""
return z + h
def enh2xyz(e, n, h, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None):
"""Manually convert map eastings, northings, and height to local XYZ coordinates
This function is for advanced users as it allows you to specify your own
helmert transformation parameters (i.e. those typically stored in
IfcMapConversion). This manual approach is useful for tests or in case your
are setting your helmert transformations in non-standard locations, or if
you are applying your own temporary false origin (such as when federating
models for digital twins of large cities).
No unit conversion is performed.
For most scenarios you should use ``auto_enh2xyz`` instead.
:param e: The global easting map coordinate.
:type e: float
:param n: The global northing map coordinate.
:type n: float
:param h: The global height map coordinate.
:type h: float
:param eastings: The eastings offset to apply.
:type eastings: float
:param northings: The northings offset to apply.
:type northings: float
:param orthogonal_height: The orthogonal height offset to apply.
:type orthogonal_height: float
:param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:type x_axis_abscissa: float
:param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:type x_axis_ordinate: float
:param scale: The combined scale factor to convert from local coordinates
to map coordinates.
:type scale: float
:return: A tuple of three ordinates representing XYZ.
:rtype: tuple[float]
"""
if scale is None:
scale = 1.0
rotation = math.atan2(x_axis_ordinate, x_axis_abscissa)
@@ -158,6 +391,37 @@ def enh2xyz(e, n, h, eastings, northings, orthogonal_height, x_axis_abscissa, x_
def local2global(matrix, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None):
"""Manually convert a 4x4 matrix from local to global coordinates
This function is for advanced users as it allows you to specify your own
helmert transformation parameters (i.e. those typically stored in
IfcMapConversion). This manual approach is useful for tests or in case your
are setting your helmert transformations in non-standard locations, or if
you are applying your own temporary false origin (such as when federating
models for digital twins of large cities).
No unit conversion is performed.
:param matrix: A 4x4 numpy matrix representing local coordinates.
:type matrix: np.array
:param eastings: The eastings offset to apply.
:type eastings: float
:param northings: The northings offset to apply.
:type northings: float
:param orthogonal_height: The orthogonal height offset to apply.
:type orthogonal_height: float
:param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:type x_axis_abscissa: float
:param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:type x_axis_ordinate: float
:param scale: The combined scale factor to convert from local coordinates
to map coordinates.
:type scale: float
:return: A numpy 4x4 array matrix representing global coordinates.
:rtype: np.array
"""
if scale is None:
scale = 1.0
x = np.array([x_axis_abscissa, x_axis_ordinate, 0])
@@ -221,6 +485,37 @@ def local2global_ifc4x3(
def global2local(matrix, eastings, northings, orthogonal_height, x_axis_abscissa, x_axis_ordinate, scale=None):
"""Manually convert a 4x4 matrix from global to local coordinates
This function is for advanced users as it allows you to specify your own
helmert transformation parameters (i.e. those typically stored in
IfcMapConversion). This manual approach is useful for tests or in case your
are setting your helmert transformations in non-standard locations, or if
you are applying your own temporary false origin (such as when federating
models for digital twins of large cities).
No unit conversion is performed.
:param matrix: A 4x4 numpy matrix representing global coordinates.
:type matrix: np.array
:param eastings: The eastings offset to apply.
:type eastings: float
:param northings: The northings offset to apply.
:type northings: float
:param orthogonal_height: The orthogonal height offset to apply.
:type orthogonal_height: float
:param x_axis_abscissa: The X axis abscissa (i.e. first coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:type x_axis_abscissa: float
:param x_axis_ordinate: The X axis ordinate (i.e. second coordinate) of the
2D vector that points to the local X axis when in map coordinates.
:type x_axis_ordinate: float
:param scale: The combined scale factor to convert from local coordinates
to map coordinates.
:type scale: float
:return: A numpy 4x4 array matrix representing local coordinates.
:rtype: np.array
"""
if scale is None:
scale = 1.0
x = np.array([x_axis_abscissa, x_axis_ordinate, 0])
@@ -245,13 +540,31 @@ def global2local(matrix, eastings, northings, orthogonal_height, x_axis_abscissa
)
# Used for converting the X and Y vectors of the X Axis in IFC grid north geolocation
def xaxis2angle(x, y):
"""Converts X axis abscissa and ordinates to an angle in decimal degrees
:param x: The X axis abscissa
:type x: float
:param y: The X axis ordinate
:type y: float
:return: The equivalent angle in decimal degrees from the X axis
:rtype: float
"""
return math.degrees(math.atan2(y, x)) * -1
# Used for converting the X and Y vectors of the Y Axis in IFC true north geolocation
def yaxis2angle(x, y):
"""Converts Y axis abscissa and ordinates to an angle in decimal degrees
The Y axis abscissa and ordinate is how IFC stores true north.
:param x: The Y axis abscissa
:type x: float
:param y: The Y axis ordinate
:type y: float
:return: The equivalent angle in decimal degrees from the Y axis
:rtype: float
"""
angle = math.degrees(math.atan2(y, x)) - 90
if angle < -180:
angle += 360
@@ -261,36 +574,94 @@ def yaxis2angle(x, y):
def get_grid_north(ifc_file):
"""Get an angle pointing to map grid north
Anticlockwise is positive.
The necessary georeferencing map conversion is automatically detected from
the IFC map conversion parameters present in the IFC model. If no map
conversion is present, then the Z coordinate is returned unchanged.
For IFC2X3, the map conversion is detected from the IfcProject's
ePSet_MapConversion. See the "User Guide for Geo-referencing in IFC":
https://www.buildingsmart.org/standards/bsi-standards/standards-library/
:param ifc_file: The IFC file
:type ifc_file: ifcopenshell.file.file
:return: An angle to grid north in decimal degrees
:rtype: float
"""
conversion = None
try:
conversion = ifc_file.by_type("IfcMapConversion")[0]
except:
return 0
if not conversion.XAxisAbscissa or not conversion.XAxisOrdinate:
return 0
return xaxis2angle(conversion.XAxisAbscissa, conversion.XAxisOrdinate)
pass
if conversion:
if not conversion.XAxisAbscissa or not conversion.XAxisOrdinate:
return 0
xaa = conversion.XAxisAbscissa
xao = conversion.XAxisOrdinate
else:
project = ifc_file.by_type("IfcProject")[0]
conversion = ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion")
if not conversion:
return 0
xaa = conversion.get("XAxisAbscissa", None) or 0
xao = conversion.get("XAxisOrdinate", None) or 0
return xaxis2angle(xaa, xao)
def get_true_north(ifc_file):
"""Get an angle pointing to global true north
Anticlockwise is positive.
Always remember that true north is not a constant! (Unless you are working
in polar coordinates) This true north is only a reference value useful for
things like solar analysis on small sites (<1km). If you're after the north
that your surveyor is using, you're probably after ``get_grid_north``
instead.
:param ifc_file: The IFC file
:type ifc_file: ifcopenshell.file.file
:return: An angle to true north in decimal degrees
:rtype: float
"""
try:
for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if not context.TrueNorth:
continue
return yaxis2angle(*context.TrueNorth.DirectionRatios[0:2])
if context.TrueNorth:
return yaxis2angle(*context.TrueNorth.DirectionRatios[0:2])
except:
return 0
return 0
# Used for converting an angle in degrees to return the X and Y vectors of the X Axis in IFC grid north geolocation:
def angle2xaxis(angle):
"""Converts an angle into an X axis abscissa and ordinate
The inverse of ``xaxis2angle``.
:param angle: The angle in decimal degrees where anticlockwise is positive.
:type angle: float
:return: A tuple of X axis abscissa and ordinate
:rtype: tuple[float]
"""
angle_rad = math.radians(angle)
x = math.cos(angle_rad)
y = -math.sin(angle_rad)
return x, y
# Used for converting True North angle as seen in CAD (relative to +Y)
def angle2yaxis(angle):
"""Converts an angle into an Y axis abscissa and ordinate
The inverse of ``yaxis2angle``.
:param angle: The angle in decimal degrees where anticlockwise is positive.
:type angle: float
:return: A tuple of Y axis abscissa and ordinate
:rtype: tuple[float]
"""
angle_rad = math.radians(angle)
x = -math.sin(angle_rad)
y = math.cos(angle_rad)
@@ -0,0 +1,46 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api
import numpy as np
from ifcopenshell.util.shape_builder import ShapeBuilder
class TestAddBoolean(test.bootstrap.IFC4):
def test_returning_ifc_boolean_result(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
body = ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model,
)
wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
builder = ShapeBuilder(self.file)
extrusion = builder.extrude(builder.rectangle())
rep = builder.get_representation(body, extrusion)
ifcopenshell.api.run("geometry.assign_representation", self.file, product=wall, representation=rep)
ifcopenshell.api.run("geometry.add_boolean", self.file, representation=rep, matrix=np.eye(4))
assert rep.Items[0].is_a() == "IfcBooleanResult"
assert rep.RepresentationType == "CSG"
@@ -0,0 +1,51 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcPatch.
#
# IfcPatch is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcPatch is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.util.element
class Patcher:
def __init__(self, src, file, logger):
"""Reassigns occurrence classifications to types
Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/691)
where it assigns classification codes to occurrences instead of types.
Almost always, this is not what you want. This patch reassigns all
classifications to their respective types.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "FixRevitClassificationCodeTypes"})
"""
self.src = src
self.file = file
self.logger = logger
def patch(self):
for rel in self.file.by_type("IfcRelAssociatesClassification"):
related_types = set()
for related_object in rel.RelatedObjects:
relating_type = ifcopenshell.util.element.get_type(related_object)
if relating_type:
related_types.add(relating_type)
else:
related_types.add(related_object)
rel.RelatedObjects = list(related_types)
@@ -0,0 +1,60 @@
# IfcPatch - IFC patching utiliy
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcPatch.
#
# IfcPatch is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcPatch is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
class Patcher:
def __init__(self, src, file, logger):
"""Removes the built-in Revit Uniformat classification.
Revit has a bug (see https://github.com/Autodesk/revit-ifc/issues/486)
where it always inserts a Uniformat classification regardless if your
project needs it or not. This patch removes it.
Example:
.. code:: python
ifcpatch.execute({"input": model, "recipe": "RemoveRevitUniformatClassification"})
"""
self.src = src
self.file = file
self.logger = logger
def patch(self):
for classification in self.file.by_type("IfcClassification"):
if classification.Name != "Uniformat":
continue
references = self.get_references(classification)
for reference in references:
self.file.remove(reference)
self.file.remove(classification)
for rel in self.file.by_type("IfcRelAssociatesClassification"):
if not rel.RelatingClassification:
self.file.remove(rel)
for rel in self.file.by_type("IfcExternalReferenceRelationship"):
if not rel.RelatingReference:
self.file.remove(rel)
def get_references(self, classification):
results = []
if not classification.HasReferences:
return results
for reference in classification.HasReferences:
results.append(reference)
results.extend(self.get_references(reference))
return results
+1 -1
View File
@@ -48,7 +48,7 @@ def get_pset(element, pset):
@lru_cache
def get_psets(element, pset):
def get_psets(element):
return ifcopenshell.util.element.get_psets(element)
+1 -1
View File
@@ -24,4 +24,4 @@ dependencies = [
include = ["ifctester"]
exclude = ["test*"]
[tool.setuptools.package-data]
ifctester = ["*.xsd"]
ifctester = ["*.xsd", "templates/*.html"]
+293 -8
View File
@@ -23,6 +23,10 @@
#include "../ifcparse/utils.h"
#ifdef WITH_PROJ
#include <proj.h>
#endif
#include <iterator>
static const uint32_t GLTF = 0x46546C67U;
@@ -177,13 +181,23 @@ void GltfSerializer::write(const IfcGeom::TriangulationElement* o) {
node_array_.push_back(json_["nodes"].size());
const std::vector<double>& m = o->transformation().matrix().data();
// nb: note that this contains the Y-UP transform as well.
const std::array<double, 16> matrix_flat = {
m[0], m[ 2], -m[ 1], 0,
m[3], m[ 5], -m[ 4], 0,
m[6], m[ 8], -m[ 7], 0,
m[9], m[11], -m[10], 1
};
std::array<double, 16> matrix_flat;
if (settings_.get(SerializerSettings::WRITE_GLTF_ECEF)) {
matrix_flat = {
m[0], m[1], m[2], 0,
m[3], m[4], m[5], 0,
m[6], m[7], m[8], 0,
m[9], m[10], m[11], 1
};
} else {
// nb: note that this contains the Y-UP transform as well.
matrix_flat = {
m[0], m[2], -m[1], 0,
m[3], m[5], -m[4], 0,
m[6], m[8], -m[7], 0,
m[9], m[11], -m[10], 1
};
}
static const std::array<double, 16> identity_matrix = {1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1};
json node;
@@ -317,6 +331,22 @@ void write_block(std::ostream& fs, It begin, It end) {
}
void GltfSerializer::finalize() {
if (north_rotation_) {
(*north_rotation_)["children"] = json::array();
for (int i = 0; i < json_["nodes"].size(); ++i) {
(*north_rotation_)["children"].push_back(i);
}
json_["nodes"].push_back(*north_rotation_);
}
if (ecef_transform_) {
(*ecef_transform_)["children"] = json::array();
for (int i = 0; i < json_["nodes"].size(); ++i) {
(*ecef_transform_)["children"].push_back(i);
}
json_["nodes"].push_back(*ecef_transform_);
}
tmp_fstream1_.close();
tmp_fstream2_.close();
@@ -335,7 +365,11 @@ void GltfSerializer::finalize() {
}
json scene_0;
scene_0["nodes"] = node_array_;
if (north_rotation_ || ecef_transform_) {
scene_0["nodes"] = std::array<size_t, 1>{json_["nodes"].size() - 1};
} else {
scene_0["nodes"] = node_array_;
}
json_["scenes"].push_back(scene_0);
//The generated glb file will contain the indices buffer followed by the vertices buffer.
@@ -375,4 +409,255 @@ void GltfSerializer::finalize() {
write_padding<BIN>(fstream_, binary_length);
}
namespace {
void normalize(std::array<double, 3>& v) {
auto l = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
v[0] /= l;
v[1] /= l;
v[2] /= l;
}
void cross(const std::array<double, 3>& v1, const std::array<double, 3>& v2, std::array<double, 3>& result) {
result[0] = v1[1] * v2[2] - v1[2] * v2[1];
result[1] = v1[2] * v2[0] - v1[0] * v2[2];
result[2] = v1[0] * v2[1] - v1[1] * v2[0];
}
void proj_log(void *, int, const char* c) {
Logger::Error("PROJ: " + std::string(c));
}
}
void GltfSerializer::setFile(IfcParse::IfcFile* f) {
if (!settings_.get(SerializerSettings::WRITE_GLTF_ECEF)) {
return;
}
boost::optional<std::string> crs_epsg;
boost::optional<std::array<double, 3>> crs_x_axis;
boost::optional<std::array<double, 3>> eastings_northings_elevation;
aggregate_of_instance::ptr coordops;
try {
coordops = f->instances_by_type("IfcCoordinateOperation");
} catch (IfcParse::IfcException&) {
// Ignored. Schema likely doesn't support IfcCoordinateOperation.
}
if (coordops) {
for (auto& coordop : *coordops) {
IfcUtil::IfcBaseClass* source_crs = *coordop->as<IfcUtil::IfcBaseEntity>()->get("SourceCRS");
if (source_crs->declaration().is("IfcGeometricRepresentationContext")) {
IfcUtil::IfcBaseClass* target_crs = *coordop->as<IfcUtil::IfcBaseEntity>()->get("TargetCRS");
auto name_attr = target_crs->as<IfcUtil::IfcBaseEntity>()->get("Name");
if (coordop->declaration().is("IfcMapConversion")) {
if (!name_attr->isNull()) {
std::string epsg_code = *name_attr;
crs_epsg = epsg_code;
// @todo in which unit are these?
double eastings = *coordop->as<IfcUtil::IfcBaseEntity>()->get("Eastings");
double northings = *coordop->as<IfcUtil::IfcBaseEntity>()->get("Northings");
double height = *coordop->as<IfcUtil::IfcBaseEntity>()->get("OrthogonalHeight");
height = 0.;
eastings_northings_elevation = { { eastings, northings, height} };
auto xaxis_attr = coordop->as<IfcUtil::IfcBaseEntity>()->get("XAxisAbscissa");
auto yaxis_attr = coordop->as<IfcUtil::IfcBaseEntity>()->get("XAxisOrdinate");
if (!xaxis_attr->isNull() && !yaxis_attr->isNull()) {
double xaxis = *xaxis_attr;
double yaxis = *yaxis_attr;
crs_x_axis = { { xaxis, yaxis, 0. } };
}
}
}
}
}
}
if (!crs_epsg) {
auto sites = f->instances_by_type("IfcSite");
if (sites && sites->size() == 1) {
auto lat_attr = (*sites->begin())->as<IfcUtil::IfcBaseEntity>()->get("RefLatitude");
auto lon_attr = (*sites->begin())->as<IfcUtil::IfcBaseEntity>()->get("RefLongitude");
if (!lat_attr->isNull() && !lon_attr->isNull()) {
std::vector<int> lat_dms = *lat_attr;
std::vector<int> lon_dms = *lon_attr;
auto to_decimal = [](const std::vector<int>& dms) {
double val = dms[0] + dms[1] / 60. + dms[2] / 3600.;
if (dms.size() == 4) {
val += dms[3] / 3600.e6;
}
return val;
};
auto lat = to_decimal(lat_dms);
auto lon = to_decimal(lon_dms);
double elev = 0.;
/*
auto elev_attr = (*sites->begin())->as<IfcUtil::IfcBaseEntity>()->get("RefElevation");
if (!elev_attr->isNull()) {
elev = *elev_attr;
}
*/
crs_epsg.reset("EPSG:4326");
eastings_northings_elevation = { { lat, lon, elev } };
}
}
}
auto contexts = f->instances_by_type_excl_subtypes("IfcGeometricRepresentationContext");
if (contexts && contexts->size() > 0) {
auto context = (*contexts->begin())->as<IfcUtil::IfcBaseEntity>();
auto north_attr = context->get("TrueNorth");
if (!north_attr->isNull()) {
IfcUtil::IfcBaseClass* north = *north_attr;
if (north->declaration().is("IfcDirection")) {
std::vector<double> ratios = *north->as<IfcUtil::IfcBaseEntity>()->get("DirectionRatios");
crs_x_axis = { { ratios[1], -ratios[0], 0. } };
}
}
}
#ifdef WITH_PROJ
if (crs_epsg) {
PJ_COORD wgs84_point;
auto C = proj_context_create();
proj_log_func(C, nullptr, proj_log);
// @todo a bit ugly we assume a proj.db in current working directory.
// a very simplistic but at least portable solution.
proj_context_set_database_path(C, "proj.db", nullptr, nullptr);
if (*crs_epsg == "EPSG:4326") {
wgs84_point = proj_coord(
(*eastings_northings_elevation)[0],
(*eastings_northings_elevation)[1],
(*eastings_northings_elevation)[2],
0);
} else {
// @todo a bit ugly we assume a proj.db in current working directory.
// a very simplistic but at least portable solution.
proj_context_set_database_path(C, "proj.db", nullptr, nullptr);
auto P = proj_create_crs_to_crs(
C, crs_epsg->c_str(), "EPSG:4326",
NULL);
if (!P) {
Logger::Error("Failed to create PROJ transformation object");
return;
}
auto a = proj_coord(
(*eastings_northings_elevation)[0],
(*eastings_northings_elevation)[1],
(*eastings_northings_elevation)[2],
0);
wgs84_point = proj_trans(P, PJ_FWD, a);
Logger::Notice("Calculated latitude: " + std::to_string(wgs84_point.lp.lam) + " longitude: " + std::to_string(wgs84_point.lp.phi));
}
std::swap(wgs84_point.lp.phi, wgs84_point.lp.lam);
const char *input_crs = "+proj=latlong +datum=WGS84";
const char *output_crs = "+proj=geocent +datum=WGS84 +units=m";
// Create a transformation object
PJ *transform = proj_create_crs_to_crs(C, input_crs, output_crs, NULL);
// Perform the transformation
PJ_COORD output_point = proj_trans(transform, PJ_FWD, wgs84_point);
// Extract the ECEF coordinates
double x = output_point.xyz.x;
double y = output_point.xyz.y;
double z = output_point.xyz.z;
const char *ellipsoid_def = "WGS84";
// Create a CRS object representing the ellipsoid
PJ *ellipsoid_crs = proj_create(C, ellipsoid_def);
if (!ellipsoid_crs) {
Logger::Error("Failed to create ellipsoid CRS");
return;
}
auto ellipse = proj_get_ellipsoid(C, ellipsoid_crs);
int _;
double semi_major, semi_minor, __;
proj_ellipsoid_get_parameters(C, ellipse, &semi_major, &semi_minor, &_, &__);
std::array<double, 3> dxyz = { {
x * (1. / (semi_major * semi_major)),
y * (1. / (semi_major * semi_major)),
z * (1. / (semi_minor * semi_minor))
} };
normalize(dxyz);
// Oblate spheroid, so X and Y axis are equal, so rotation around Z yields east axis.
std::array<double, 3> east_xyz = { {
-y,
x,
0.
} };
normalize(east_xyz);
std::array<double, 3> north;
cross(dxyz, east_xyz, north);
std::array<double, 16> matrix = {
east_xyz[0], east_xyz[1], east_xyz[2], 0,
north[0], north[1], north[2], 0.,
dxyz[0], dxyz[1], dxyz[2], 0,
0,0,0,1
};
ecef_transform_ = json::object({
{"matrix", matrix }
});
json_["extensions"]["CESIUM_RTC"]["center"] = std::array<double, 3>{ {x, y, z} };
json_["extensionsUsed"].push_back("CESIUM_RTC");
// Clean up
proj_destroy(ellipsoid_crs);
proj_destroy(transform);
proj_context_destroy(C);
}
if (crs_x_axis) {
normalize(*crs_x_axis);
auto phi = std::atan2((*crs_x_axis)[1], (*crs_x_axis)[0]);
north_rotation_ = json::object({
{"matrix", std::array<double, 16>{
+std::cos(-phi), -std::sin(-phi), 0., 0.,
+std::sin(-phi), +std::cos(-phi), 0., 0.,
0., 0., 1., 0.,
0., 0., 0., 1.
}}
});
}
#endif
}
#endif
+2 -2
View File
@@ -36,9 +36,9 @@ private:
std::ofstream fstream_, tmp_fstream1_, tmp_fstream2_;
std::map<std::string, int> materials_, meshes_;
json json_, node_array_;
boost::optional<json> ecef_transform_, north_rotation_;
int bufferViewId;
int writeMaterial(const IfcGeom::Material& style);
public:
GltfSerializer(const std::string& filename, const SerializerSettings& settings);
@@ -50,7 +50,7 @@ public:
void finalize();
bool isTesselated() const { return true; }
void setUnitNameAndMagnitude(const std::string& /*name*/, float /*magnitude*/) {}
void setFile(IfcParse::IfcFile*) {}
void setFile(IfcParse::IfcFile*);
};
#endif
+35
View File
@@ -193,6 +193,41 @@ IF "%IFCOS_INSTALL_PYTHON%"=="TRUE" (
echo PYTHONHOME=%PYTHONHOME%>>"%~dp0\%BUILD_DEPS_CACHE_PATH%"
)
:proj
set DEPENDENCY_NAME=sqlite3
md %INSTALL_DIR%\sqlite3\lib %INSTALL_DIR%\sqlite3\bin %INSTALL_DIR%\sqlite3\include
call :DownloadFile https://www.sqlite.org/2023/sqlite-amalgamation-3430100.zip "%DEPS_DIR%" sqlite-amalgamation-3430100.zip
IF NOT %ERRORLEVEL%==0 GOTO :Error
call :ExtractArchive sqlite-amalgamation-3430100.zip "%DEPS_DIR%" "%DEPS_DIR%\sqlite-amalgamation-3430100"
IF NOT %ERRORLEVEL%==0 GOTO :Error
pushd "%DEPS_DIR%\sqlite-amalgamation-3430100"
cl /c sqlite3.c
lib /OUT:%INSTALL_DIR%\sqlite3\lib\sqlite3.lib sqlite3.obj
cl sqlite3.c shell.c /link /out:%INSTALL_DIR%\sqlite3\bin\sqlite3.exe
copy sqlite3.h %INSTALL_DIR%\sqlite3\include
popd
set DEPENDENCY_NAME=proj
set DEPENDENCY_DIR=%DEPS_DIR%\proj-9.2.1
call :DownloadFile https://download.osgeo.org/proj/proj-9.2.1.zip "%DEPS_DIR%" proj-9.2.1.zip
IF NOT %ERRORLEVEL%==0 GOTO :Error
call :ExtractArchive proj-9.2.1.zip "%DEPS_DIR%" "%DEPS_DIR%\proj-9.2.1"
IF NOT %ERRORLEVEL%==0 GOTO :Error
cd "%DEPENDENCY_DIR%"
call :RunCMake -DCMAKE_INSTALL_PREFIX="%INSTALL_DIR%\proj-9.2.1" ^
-DSQLITE3_INCLUDE_DIR=%INSTALL_DIR%\sqlite3\include -DSQLITE3_LIBRARY=%INSTALL_DIR%\sqlite3\lib\sqlite3.lib ^
-DENABLE_TIFF=Off -DENABLE_CURL=Off -DBUILD_PROJSYNC=Off ^
-DBUILD_SHARED_LIBS=Off
IF NOT %ERRORLEVEL%==0 GOTO :Error
call :BuildSolution "%DEPENDENCY_DIR%\%BUILD_DIR%\PROJ.sln" %BUILD_CFG%
IF NOT %ERRORLEVEL%==0 GOTO :Error
call :InstallCMakeProject "%DEPENDENCY_DIR%\%BUILD_DIR%" %BUILD_CFG%
IF NOT %ERRORLEVEL%==0 GOTO :Error
goto :Successful
:mpir
set DEPENDENCY_NAME=mpir
set DEPENDENCY_DIR=%DEPS_DIR%\mpir