Compare commits

..

7 Commits

Author SHA1 Message Date
Dion Moult 8f64ef4465 Fix bug where incorrect units were calculated in the geometry offset UI 2024-06-07 10:40:23 +10:00
Bernd Hahnebach a16c159b7f BBIM, bcf, ignore problematic topics in case of markup 2024-06-06 22:06:42 +02:00
Bernd Hahnebach 04b641c49c BBIM, bcf, fix colors for hex with alpha channel 2024-06-06 22:06:42 +02:00
Bernd Hahnebach 84d475cfd3 BBIM, bcf, fix setting the viewpoint components 2024-06-06 22:06:42 +02:00
Bernd Hahnebach afe7d526a5 BBIM, bcf, fix if either project or project name is missing in bcf 2024-06-06 22:06:42 +02:00
Dion Moult 98ec3e2301 Fix #4677. Fix #4809. Major improvement to local session offset during project loading (see description)
This improves four things:

 1. Previously, we either used OBJECT_PLACEMENT or CARTESIAN_POINT, but couldn't handle scenarios where simultaneously both the placement and the coords were rubbish for a single object. Now we offset all far cartesian points, so it consistently works and we keep track of a per-object offset.
 2. Previously, we applied the georeferencing conversion on every cartesian point which was very slow. The new method uses a simple XYZ translation which is super fast.
 3. We now use numpy which should be much faster too.
 4. Previously, objects were selectively offset based on whether they fell outside the distance limit. Now, we uniformly treat all non-geometric elements at 0,0,0 as insignificant positionally. This fixes the issue where half the model is offset and the other half isn't, but maintains the fix for situations where the site (typically) is at 0,0,0 and everything else is map coords.
2024-06-06 11:54:07 +10:00
Dion Moult cfd0ed60cf Reduce (arbitrary) element checking threshold down to 3 for georef guessing. 2024-06-06 09:34:32 +10:00
5 changed files with 158 additions and 147 deletions
+55 -75
View File
@@ -689,7 +689,7 @@ class IfcImporter:
def get_offset_point(self) -> Union[npt.NDArray[np.float64], None]:
elements_checked = 0
# If more than these elements aren't far away, the file probably isn't absolutely positioned
element_checking_threshold = 10
element_checking_threshold = 3
for element in self.file.by_type("IfcElement"):
if not element.Representation:
continue
@@ -743,18 +743,30 @@ class IfcImporter:
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
if obj.data and obj.data.get("has_cartesian_point_offset", None):
if not obj.data and tool.Cad.is_x(matrix[0][3], 0) and tool.Cad.is_x(matrix[1][3], 0) and tool.Cad.is_x(matrix[2][3], 0):
# We assume any non-geometric matrix at 0,0,0 is not
# positionally significant and is left alone. This handles
# scenarios where often spatial elements are left at 0,0,0 and
# everything else is at map coordinates.
return mathutils.Matrix(matrix.tolist())
elif obj.data and obj.data.get("has_cartesian_point_offset", None):
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
elif self.is_point_far_away((matrix[:3, 3])):
if cartesian_point_offset := obj.data.get("cartesian_point_offset", None):
obj.BIMObjectProperties.cartesian_point_offset = cartesian_point_offset
offset_x, offset_y, offset_z = map(float, cartesian_point_offset.split(","))
matrix[0][3] += offset_x
matrix[1][3] += offset_y
matrix[2][3] += offset_z
else:
obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
float(props.blender_eastings) * self.unit_scale,
float(props.blender_northings) * self.unit_scale,
float(props.blender_orthogonal_height) * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
)
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
float(props.blender_eastings) * self.unit_scale,
float(props.blender_northings) * self.unit_scale,
float(props.blender_orthogonal_height) * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
)
return mathutils.Matrix(matrix.tolist())
@@ -1201,7 +1213,6 @@ class IfcImporter:
styles.extend(style.Styles)
def create_native_faceted_brep(self, element, mesh_name, native_data):
# TODO: georeferencing?
# co [x y z x y z x y z ...]
# vertex_index [i i i i i ...]
# loop_start [0 3 6 9 ...] (for tris)
@@ -1226,45 +1237,27 @@ class IfcImporter:
mesh = bpy.data.meshes.new("Native")
props = bpy.context.scene.BIMGeoreferenceProperties
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
if props.has_blender_offset and self.is_point_far_away(self.mesh_data["co"][0:3], is_meters=False):
offset_point = np.linalg.inv(mat) @ np.array(
(
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
0.0,
)
)
verts = [None] * len(self.mesh_data["co"])
for i in range(0, len(self.mesh_data["co"]), 3):
verts[i], verts[i + 1], verts[i + 2], _ = native_data["matrix"] @ mathutils.Vector(
(
*ifcopenshell.util.geolocation.enh2xyz(
self.mesh_data["co"][i] * self.unit_scale,
self.mesh_data["co"][i + 1] * self.unit_scale,
self.mesh_data["co"][i + 2] * self.unit_scale,
offset_point[0] * self.unit_scale,
offset_point[1] * self.unit_scale,
offset_point[2] * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
),
1,
)
)
verts_array = np.array(self.mesh_data["co"])
verts_array *= self.unit_scale
offset_x, offset_y, offset_z = verts_array[0:3]
offset = np.array([-offset_x, -offset_y, -offset_z])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
if np.allclose(native_data["matrix"], np.identity(4), atol=1e-8):
verts = offset_verts.tolist()
else:
verts = self.apply_matrix_to_flat_coords(offset_verts, native_data["matrix"])
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = f"{offset_x},{offset_y},{offset_z}"
else:
verts = [None] * len(self.mesh_data["co"])
for i in range(0, len(self.mesh_data["co"]), 3):
verts[i], verts[i + 1], verts[i + 2], _ = native_data["matrix"] @ mathutils.Vector(
(
self.mesh_data["co"][i] * self.unit_scale,
self.mesh_data["co"][i + 1] * self.unit_scale,
self.mesh_data["co"][i + 2] * self.unit_scale,
1,
)
)
verts_array = np.array(self.mesh_data["co"])
verts_array *= self.unit_scale
if np.allclose(native_data["matrix"], np.identity(4), atol=1e-8):
verts = verts_array.tolist()
else:
verts = self.apply_matrix_to_flat_coords(verts_array, native_data["matrix"])
mesh["has_cartesian_point_offset"] = False
mesh.vertices.add(self.mesh_data["total_verts"])
@@ -1281,6 +1274,13 @@ class IfcImporter:
mesh["ios_material_ids"] = self.mesh_data["material_ids"]
return mesh
def apply_matrix_to_flat_coords(self, coords, matrix):
coords_array = np.array(coords).reshape(-1, 3)
ones = np.ones((coords_array.shape[0], 1))
homogeneous_coords = np.hstack([coords_array, ones])
transformed_coords = homogeneous_coords @ matrix.T
return transformed_coords[:, :3].flatten().tolist()
def convert_representation_item_face_based_surface_model(self, item):
mesh = item.get_info_2(recursive=True)
for face_set in mesh["FbsmFaces"]:
@@ -1924,34 +1924,14 @@ class IfcImporter:
and geometry.verts
and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2]))
):
offset_point = np.array(
(
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
0.0,
)
)
if geometry != shape:
m = shape.transformation.matrix.data
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
offset_point = np.linalg.inv(mat) @ offset_point
verts = [None] * len(geometry.verts)
for i in range(0, len(geometry.verts), 3):
# Note: this enh2xyz call is crazy slow.
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
geometry.verts[i],
geometry.verts[i + 1],
geometry.verts[i + 2],
offset_point[0] * self.unit_scale,
offset_point[1] * self.unit_scale,
offset_point[2] * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
)
# Shift geometry close to the origin based off that first vert it found
verts_array = np.array(geometry.verts)
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = f"{geometry.verts[0]},{geometry.verts[1]},{geometry.verts[2]}"
else:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
@@ -59,6 +59,20 @@ class LoadBcfProject(bpy.types.Operator):
if self.filepath:
bcfstore.BcfStore.set_by_filepath(self.filepath)
bcfxml = bcfstore.BcfStore.get_bcfxml()
# a BCFv2.1 does not need to have a project, but BBIM likes to have one
# https://github.com/buildingSMART/BCF-XML/tree/release_2_1/Documentation#bcf-file-structure
nameless = "Unknown"
if bcfxml.project is None:
if bcfxml.version.version_id.startswith("2"):
print("No project, we will create one for BBIM.")
bcfxml.project_info = bcf.v2.model.ProjectExtension(
project=bcf.v2.model.Project(
name=nameless,
project_id=str(uuid.uuid4())
), extension_schema=""
)
if bcfxml.project.name is None:
bcfxml.project.name = nameless
context.scene.BCFProperties.name = bcfxml.project.name
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -86,7 +100,19 @@ class LoadBcfTopics(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
context.scene.BCFProperties.topics.clear()
for index, topic_guid in enumerate(bcfxml.topics.keys()):
# workaround, one non standard topic would break reading entire bcf
# ignored these topics ATM
# happens on non standard nodes or on missing nodes in markup
topics2use = []
for topic_guid in bcfxml.topics.keys():
# print("topic guid: {}".format(topic_guid))
try:
topic_titel = bcfxml.topics[topic_guid].topic.title
topics2use.append(topic_guid)
except:
print("Problems on reading topic, thus ignored: {}".format(topic_guid))
continue
for index, topic_guid in enumerate(topics2use):
new = context.scene.BCFProperties.topics.add()
bpy.ops.bim.load_bcf_topic(topic_guid=topic_guid, topic_index=index)
return {"FINISHED"}
@@ -938,22 +964,42 @@ class ActivateBcfViewpoint(bpy.types.Operator):
# Operators with context overrides are used because they are
# significantly faster than looping through all objects
self.set_exceptions(viewpoint, context)
self.set_view_setup_hints(viewpoint, context)
# set selection at the end not to conflict with .hide_spaces
self.set_selection(viewpoint)
self.set_colours(viewpoint)
def set_exceptions(self, viewpoint, context):
if (
not hasattr(viewpoint.visualization_info.components, "visibility")
or not hasattr(viewpoint.visualization_info.components.visibility.exceptions, "component")
):
return
exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions.component or []}
# print("default_visibility: {}".format(viewpoint.visualization_info.components.visibility.default_visibility))
if viewpoint.visualization_info.components.visibility.default_visibility:
# default_visibility is True: show all objs, hide the exceptions
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.hide_view_clear()
context.area.type = old
for global_id in exception_global_ids:
# print("{}: hide".format(global_id))
obj = IfcStore.get_element(global_id)
if obj and bpy.context.view_layer.objects.get(obj.name):
# print(" obj found")
obj.hide_set(True)
else:
# default_visibility is False: hide all objs, show the exceptions
objs = []
for global_id in exception_global_ids:
# print("{}: show".format(global_id))
obj = IfcStore.get_element(global_id)
if obj:
# print(" obj found")
objs.append(obj)
if objs:
old = context.area.type
@@ -967,6 +1013,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
bpy.data.objects["Viewpoint"].hide_set(False)
context.area.type = old
def set_view_setup_hints(self, viewpoint, context):
if viewpoint.visualization_info.components.view_setup_hints:
if not viewpoint.visualization_info.components.view_setup_hints.spaces_visible:
self.hide_spaces(context)
@@ -978,10 +1025,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
self.hide_spaces(context)
self.set_openings_visibility(False, context)
# set selection at the end not to conflict with .hide_spaces
self.set_selection(viewpoint)
self.set_colours(viewpoint)
def hide_spaces(self, context):
old = context.area.type
context.area.type = "VIEW_3D"
@@ -999,19 +1042,25 @@ class ActivateBcfViewpoint(bpy.types.Operator):
selected_global_ids = [s.ifc_guid for s in viewpoint.visualization_info.components.selection.component or []]
bpy.ops.object.select_all(action="DESELECT")
for global_id in selected_global_ids:
# print("{}: selected".format(global_id))
obj = IfcStore.get_element(global_id)
if obj:
# print(" obj found")
obj.select_set(True)
obj.hide_set(False)
def set_colours(self, viewpoint):
if not viewpoint.visualization_info.components or not viewpoint.visualization_info.components.coloring:
return
global_id_colours = {}
for coloring in viewpoint.visualization_info.components.coloring or []:
for component in coloring.components:
global_id_colours.setdefault(component.ifc_guid, coloring.color)
for acoloring in viewpoint.visualization_info.components.coloring.color:
for acomponent in acoloring.component:
global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color)
for global_id, color in global_id_colours.items():
# print("{}: color: {}: {}".format(global_id, color, self.hex_to_rgb(color)))
obj = IfcStore.get_element(global_id)
if obj:
# print(" obj found ")
obj.color = self.hex_to_rgb(color)
def draw_lines(self, viewpoint, context):
@@ -1095,8 +1144,14 @@ class ActivateBcfViewpoint(bpy.types.Operator):
def hex_to_rgb(self, value):
value = value.lstrip("#")
lv = len(value)
t = tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3))
return [t[0] / 255.0, t[1] / 255.0, t[2] / 255.0, 1]
# https://github.com/buildingSMART/BCF-XML/tree/release_3_0/Documentation#coloring
if lv == 8:
t = tuple(int(value[i : i + lv // 4], 16) for i in range(0, lv, lv // 4))
col = [t[1] / 255.0, t[2] / 255.0, t[3] / 255.0, t[0] / 255.0]
else:
t = tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3))
col = [t[0] / 255.0, t[1] / 255.0, t[2] / 255.0, 1]
return col
class OpenBcfReferenceLink(bpy.types.Operator):
@@ -1519,48 +1519,40 @@ class ActivateDrawing(bpy.types.Operator):
bl_idname = "bim.activate_drawing"
bl_label = "Activate Drawing"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Activates the selected drawing view.\n\n" + "ALT+CLICK to keep the viewport position.\n\n" + "SHIFT+CLICK activiate drawing without turning objects on/off."
bl_description = "Activates the selected drawing view.\n\n" + "ALT+CLICK to keep the viewport position"
drawing: bpy.props.IntProperty()
camera_view_point: bpy.props.BoolProperty(name="Camera View Point", default=True, options={"SKIP_SAVE"})
switch_camera_only: bpy.props.BoolProperty(name="Only Changes Camera View", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# keep the viewport position on alt+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.alt:
self.camera_view_point = False
# Only activates the camera view on alt+shift. Does not turn on/off objects in scene
if event.type == "LEFTMOUSE" and event.shift:
self.switch_camera_only = True
return self.execute(context)
def execute(self, context):
drawing = tool.Ifc.get().by_id(self.drawing)
dprops = bpy.context.scene.DocProperties
camera = tool.Drawing.import_drawing(drawing)
if self.switch_camera_only:
tool.Blender.activate_camera(camera)
else:
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
dprops.active_drawing_id = self.drawing
# reset DrawingsData to reload_drawing_styles work correctly
DrawingsData.is_loaded = False
dprops.drawing_styles.clear()
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
tool.Drawing.show_decorations()
dprops.active_drawing_id = self.drawing
# reset DrawingsData to reload_drawing_styles work correctly
DrawingsData.is_loaded = False
dprops.drawing_styles.clear()
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
tool.Drawing.show_decorations()
return {"FINISHED"}
@@ -328,12 +328,17 @@ class PlacementData:
@classmethod
def load(cls):
cls.data = {
"has_placement": cls.has_placement(),
"original_x": cls.original_x(),
"original_y": cls.original_y(),
"original_z": cls.original_z(),
}
cls.data = {"has_placement": cls.has_placement()}
props = bpy.context.scene.BIMGeoreferenceProperties
obj = bpy.context.active_object
if obj and props.has_blender_offset:
xyz = cls.original_xyz(obj)
cls.data.update({
"original_x": str(xyz[0]),
"original_y": str(xyz[1]),
"original_z": str(xyz[2]),
})
cls.is_loaded = True
@classmethod
@@ -344,40 +349,18 @@ class PlacementData:
return False
@classmethod
def original_x(cls):
def original_xyz(cls, obj):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props = bpy.context.scene.BIMGeoreferenceProperties
obj = bpy.context.active_object
if not obj or not props.has_blender_offset:
return
return str(round(cls.original_xyz(obj.location)[0], 3))
@classmethod
def original_y(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
obj = bpy.context.active_object
if not obj or not props.has_blender_offset:
return
return str(round(cls.original_xyz(obj.location)[1], 3))
@classmethod
def original_z(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
obj = bpy.context.active_object
if not obj or not props.has_blender_offset:
return
return str(round(cls.original_xyz(obj.location)[2], 3))
@classmethod
def original_xyz(cls, location):
props = bpy.context.scene.BIMGeoreferenceProperties
return ifcopenshell.util.geolocation.xyz2enh(
location[0],
location[1],
location[2],
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
xyz = ifcopenshell.util.geolocation.xyz2enh(
obj.matrix_world[0][3],
obj.matrix_world[1][3],
obj.matrix_world[2][3],
float(props.blender_eastings) * unit_scale,
float(props.blender_northings) * unit_scale,
float(props.blender_orthogonal_height) / unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
1.0,
)
return [round(o, 3) / unit_scale for o in xyz] # To nearest mm of precision
+1
View File
@@ -445,6 +445,7 @@ class BIMObjectProperties(PropertyGroup):
name="Blender Offset",
default="NONE",
)
cartesian_point_offset: StringProperty(name="Cartesian Point Offset")
is_reassigning_class: BoolProperty(name="Is Reassigning Class")
is_renaming: BoolProperty(name="Is Renaming", default=False)
location_checksum: StringProperty(name="Location Checksum")