diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index 05b9c9cfb5..32f3debde2 100644
--- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt
@@ -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)
diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile
index 08f291afa1..f8ea60a639 100644
--- a/src/blenderbim/Makefile
+++ b/src/blenderbim/Makefile
@@ -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/
diff --git a/src/blenderbim/blenderbim/bim/module/fm/operator.py b/src/blenderbim/blenderbim/bim/module/fm/operator.py
index d48502790a..a59fe1dc9a 100644
--- a/src/blenderbim/blenderbim/bim/module/fm/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/fm/operator.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/fm/prop.py b/src/blenderbim/blenderbim/bim/module/fm/prop.py
index f6a3074da3..c75ac52518 100644
--- a/src/blenderbim/blenderbim/bim/module/fm/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/fm/prop.py
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see .
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=[
diff --git a/src/blenderbim/blenderbim/bim/module/fm/ui.py b/src/blenderbim/blenderbim/bim/module/fm/ui.py
index 8ef8a4dd43..e838d72a0f 100644
--- a/src/blenderbim/blenderbim/bim/module/fm/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/fm/ui.py
@@ -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()
diff --git a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py
index 29503856bc..1b64ef180e 100644
--- a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py
@@ -23,6 +23,7 @@ classes = (
operator.AddRepresentation,
operator.CopyRepresentation,
operator.EditObjectPlacement,
+ operator.FlipObject,
operator.GetRepresentationIfcParameters,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py
index af1db13bd6..ef8c3e865c 100644
--- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py
index 66df12b93b..d362027939 100644
--- a/src/blenderbim/blenderbim/bim/module/georeference/data.py
+++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py
@@ -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:
diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py
index bb75f06f5e..f401376bbe 100644
--- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py
@@ -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")
\ No newline at end of file
+ row.prop(props, "y_axis_ordinate_output", text="North Ordinate")
diff --git a/src/blenderbim/blenderbim/bim/module/model/array.py b/src/blenderbim/blenderbim/bim/module/model/array.py
index 087bf1e436..f8a40c2713 100644
--- a/src/blenderbim/blenderbim/bim/module/model/array.py
+++ b/src/blenderbim/blenderbim/bim/module/model/array.py
@@ -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"])
diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py
index 9b841cfe2b..32f42340e9 100644
--- a/src/blenderbim/blenderbim/bim/module/model/opening.py
+++ b/src/blenderbim/blenderbim/bim/module/model/opening.py
@@ -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"}
diff --git a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py
index 7c6428a4eb..ab54ef8d24 100644
--- a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py
+++ b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py
index 54b81214a0..f4cd1e346f 100644
--- a/src/blenderbim/blenderbim/bim/module/model/wall.py
+++ b/src/blenderbim/blenderbim/bim/module/model/wall.py
@@ -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
diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py
index 6af2600dee..0f8d59b683 100644
--- a/src/blenderbim/blenderbim/bim/module/model/workspace.py
+++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py
@@ -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()
diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py
index ee018ccef8..78b5887751 100644
--- a/src/blenderbim/blenderbim/bim/module/search/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/search/operator.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/bim/module/tester/operator.py b/src/blenderbim/blenderbim/bim/module/tester/operator.py
index 57ed89429d..010314e325 100644
--- a/src/blenderbim/blenderbim/bim/module/tester/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/tester/operator.py
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see .
-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()
diff --git a/src/blenderbim/blenderbim/bim/module/tester/prop.py b/src/blenderbim/blenderbim/bim/module/tester/prop.py
index 7496d4ba44..ab4c175196 100644
--- a/src/blenderbim/blenderbim/bim/module/tester/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/tester/prop.py
@@ -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")
diff --git a/src/blenderbim/blenderbim/bim/module/tester/ui.py b/src/blenderbim/blenderbim/bim/module/tester/ui.py
index 31c45bc888..b33cdd5aff 100644
--- a/src/blenderbim/blenderbim/bim/module/tester/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/tester/ui.py
@@ -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):
diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py
index 7b491e58be..e65ad34b49 100644
--- a/src/blenderbim/blenderbim/bim/operator.py
+++ b/src/blenderbim/blenderbim/bim/operator.py
@@ -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
diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py
index dcc9823904..9c6e6de118 100644
--- a/src/blenderbim/blenderbim/core/georeference.py
+++ b/src/blenderbim/blenderbim/core/georeference.py
@@ -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)
diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py
index 14478ea651..d7c2fc6496 100644
--- a/src/blenderbim/blenderbim/core/spatial.py
+++ b/src/blenderbim/blenderbim/core/spatial.py
@@ -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)
-
diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py
index 5bd6c431cd..f0010b3ab8 100644
--- a/src/blenderbim/blenderbim/core/tool.py
+++ b/src/blenderbim/blenderbim/core/tool.py
@@ -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
diff --git a/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.reg b/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.reg
new file mode 100644
index 0000000000..33451e8dc7
--- /dev/null
+++ b/src/blenderbim/blenderbim/libs/desktop/windows_bbim_association.reg
@@ -0,0 +1,4 @@
+Windows Registry Editor Version 5.00
+
+[-HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ifc]
+[-]
\ No newline at end of file
diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py
index d544a67787..73e245bdc9 100644
--- a/src/blenderbim/blenderbim/tool/blender.py
+++ b/src/blenderbim/blenderbim/tool/blender.py
@@ -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
diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py
index 8e624d61f0..ce33fbcd7d 100644
--- a/src/blenderbim/blenderbim/tool/geometry.py
+++ b/src/blenderbim/blenderbim/tool/geometry.py
@@ -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()
diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py
index 52c31de1a2..037b5c6645 100644
--- a/src/blenderbim/blenderbim/tool/georeference.py
+++ b/src/blenderbim/blenderbim/tool/georeference.py
@@ -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")
diff --git a/src/blenderbim/test/core/test_georeference.py b/src/blenderbim/test/core/test_georeference.py
index 957e43b232..13aee6060c 100644
--- a/src/blenderbim/test/core/test_georeference.py
+++ b/src/blenderbim/test/core/test_georeference.py
@@ -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)
diff --git a/src/blenderbim/test/tool/test_georeference.py b/src/blenderbim/test/tool/test_georeference.py
index 8540fed870..177d9aeb6b 100644
--- a/src/blenderbim/test/tool/test_georeference.py
+++ b/src/blenderbim/test/tool/test_georeference.py
@@ -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):
diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py
index 90546c0a16..20329a3cad 100644
--- a/src/blenderbim/test/tool/test_model.py
+++ b/src/blenderbim/test/tool/test_model.py
@@ -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
diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp
index 059aebd45c..f73b137921 100644
--- a/src/ifcconvert/IfcConvert.cpp
+++ b/src/ifcconvert/IfcConvert.cpp
@@ -461,6 +461,7 @@ int main(int argc, char** argv) {
("space-name-transform", po::value(),
"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;
diff --git a/src/ifcfm/ifcfm/basic.py b/src/ifcfm/ifcfm/basic.py
index 697f9c2bbe..76e1a8e856 100644
--- a/src/ifcfm/ifcfm/basic.py
+++ b/src/ifcfm/ifcfm/basic.py
@@ -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",
],
diff --git a/src/ifcfm/ifcfm/cobie24.py b/src/ifcfm/ifcfm/cobie24.py
index 29d13cc3c2..7134d8b72e 100644
--- a/src/ifcfm/ifcfm/cobie24.py
+++ b/src/ifcfm/ifcfm/cobie24.py
@@ -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]:
diff --git a/src/ifcfm/ifcfm/cobie24legacy.py b/src/ifcfm/ifcfm/cobie24legacy.py
index 0de1a783ad..83c9f85d9c 100644
--- a/src/ifcfm/ifcfm/cobie24legacy.py
+++ b/src/ifcfm/ifcfm/cobie24legacy.py
@@ -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):
diff --git a/src/ifcgeom_schema_agnostic/GeometrySerializer.h b/src/ifcgeom_schema_agnostic/GeometrySerializer.h
index 58fb7c4d64..63b1507d97 100644
--- a/src/ifcgeom_schema_agnostic/GeometrySerializer.h
+++ b/src/ifcgeom_schema_agnostic/GeometrySerializer.h
@@ -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()
diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile
index 05442dacbb..105f0faf28 100644
--- a/src/ifcopenshell-python/Makefile
+++ b/src/ifcopenshell-python/Makefile
@@ -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)
diff --git a/src/ifcopenshell-python/docs/ifcconvert/installation.rst b/src/ifcopenshell-python/docs/ifcconvert/installation.rst
index da8fecf14d..3ff4cc9f0f 100644
--- a/src/ifcopenshell-python/docs/ifcconvert/installation.rst
+++ b/src/ifcopenshell-python/docs/ifcconvert/installation.rst
@@ -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.
diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
index 8dc7811f1d..f7ea78d3c6 100644
--- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
+++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
@@ -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
diff --git a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst
index c1db44bdf3..775ab2bcb0 100644
--- a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst
+++ b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst
@@ -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 `__ website.
+same as any other Blender add-on. `Download IfcSverchok here
+`__.
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
+`__ and `install Sverchok
`__.
If you downloaded Blender as a ``.zip`` file without running an installer, you
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py
index 7569f58bbc..c477d0cfe6 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see .
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 = []
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py
index 5e4393457f..d8003227d1 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py
@@ -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 = {
diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py
index d4737ac016..7128630c2f 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py
@@ -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)
diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py
new file mode 100644
index 0000000000..041e6c1980
--- /dev/null
+++ b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py
@@ -0,0 +1,46 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+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"
diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitClassificationCodeTypes.py b/src/ifcpatch/ifcpatch/recipes/FixRevitClassificationCodeTypes.py
new file mode 100644
index 0000000000..d6bdde4b5a
--- /dev/null
+++ b/src/ifcpatch/ifcpatch/recipes/FixRevitClassificationCodeTypes.py
@@ -0,0 +1,51 @@
+# IfcPatch - IFC patching utiliy
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+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)
diff --git a/src/ifcpatch/ifcpatch/recipes/RemoveRevitUniformatClassification.py b/src/ifcpatch/ifcpatch/recipes/RemoveRevitUniformatClassification.py
new file mode 100644
index 0000000000..22cad79bba
--- /dev/null
+++ b/src/ifcpatch/ifcpatch/recipes/RemoveRevitUniformatClassification.py
@@ -0,0 +1,60 @@
+# IfcPatch - IFC patching utiliy
+# Copyright (C) 2023 Dion Moult
+#
+# 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 .
+
+
+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
diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py
index d10664fee2..78e718a264 100644
--- a/src/ifctester/ifctester/facet.py
+++ b/src/ifctester/ifctester/facet.py
@@ -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)
diff --git a/src/ifctester/pyproject.toml b/src/ifctester/pyproject.toml
index be2f338522..1936495312 100644
--- a/src/ifctester/pyproject.toml
+++ b/src/ifctester/pyproject.toml
@@ -24,4 +24,4 @@ dependencies = [
include = ["ifctester"]
exclude = ["test*"]
[tool.setuptools.package-data]
-ifctester = ["*.xsd"]
+ifctester = ["*.xsd", "templates/*.html"]
diff --git a/src/serializers/GltfSerializer.cpp b/src/serializers/GltfSerializer.cpp
index 3c285b7c03..f0a4f62737 100644
--- a/src/serializers/GltfSerializer.cpp
+++ b/src/serializers/GltfSerializer.cpp
@@ -23,6 +23,10 @@
#include "../ifcparse/utils.h"
+#ifdef WITH_PROJ
+#include
+#endif
+
#include
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& m = o->transformation().matrix().data();
- // nb: note that this contains the Y-UP transform as well.
- const std::array 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 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 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{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(fstream_, binary_length);
}
+namespace {
+ void normalize(std::array& 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& v1, const std::array& v2, std::array& 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 crs_epsg;
+ boost::optional> crs_x_axis;
+ boost::optional> 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()->get("SourceCRS");
+ if (source_crs->declaration().is("IfcGeometricRepresentationContext")) {
+ IfcUtil::IfcBaseClass* target_crs = *coordop->as()->get("TargetCRS");
+ auto name_attr = target_crs->as()->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()->get("Eastings");
+ double northings = *coordop->as()->get("Northings");
+ double height = *coordop->as()->get("OrthogonalHeight");
+ height = 0.;
+
+ eastings_northings_elevation = { { eastings, northings, height} };
+
+ auto xaxis_attr = coordop->as()->get("XAxisAbscissa");
+ auto yaxis_attr = coordop->as()->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()->get("RefLatitude");
+ auto lon_attr = (*sites->begin())->as()->get("RefLongitude");
+
+ if (!lat_attr->isNull() && !lon_attr->isNull()) {
+ std::vector lat_dms = *lat_attr;
+ std::vector lon_dms = *lon_attr;
+
+ auto to_decimal = [](const std::vector& 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()->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();
+ auto north_attr = context->get("TrueNorth");
+ if (!north_attr->isNull()) {
+ IfcUtil::IfcBaseClass* north = *north_attr;
+ if (north->declaration().is("IfcDirection")) {
+ std::vector ratios = *north->as()->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 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 east_xyz = { {
+ -y,
+ x,
+ 0.
+ } };
+ normalize(east_xyz);
+
+ std::array north;
+ cross(dxyz, east_xyz, north);
+
+ std::array 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{ {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{
+ +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
diff --git a/src/serializers/GltfSerializer.h b/src/serializers/GltfSerializer.h
index 7450fa2afb..5f7265730f 100644
--- a/src/serializers/GltfSerializer.h
+++ b/src/serializers/GltfSerializer.h
@@ -36,9 +36,9 @@ private:
std::ofstream fstream_, tmp_fstream1_, tmp_fstream2_;
std::map materials_, meshes_;
json json_, node_array_;
+ boost::optional 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
diff --git a/win/build-deps.cmd b/win/build-deps.cmd
index 4c7d1aa591..238d3df0d1 100644
--- a/win/build-deps.cmd
+++ b/win/build-deps.cmd
@@ -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