mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
Meshes can now be automagically converted into circular and arbitrary profile extrusions (swept solids)
This commit is contained in:
@@ -21,7 +21,11 @@ class Usecase:
|
||||
"is_wireframe": False, # If the geometry is a wireframe
|
||||
"is_curve": False, # If the geometry is a Blender curve
|
||||
"is_point_cloud": False, # If the geometry is a point cloud
|
||||
"is_rectangular_extrusion": False,
|
||||
# Possible IFC representation classes:
|
||||
# IfcExtrudedAreaSolid/IfcRectangleProfileDef
|
||||
# IfcExtrudedAreaSolid/IfcCircleProfileDef
|
||||
# IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef
|
||||
"ifc_representation_class": None, # Whether to cast a mesh into a particular class
|
||||
}
|
||||
self.ifc_vertices = []
|
||||
for key, value in settings.items():
|
||||
@@ -126,8 +130,12 @@ class Usecase:
|
||||
return self.create_curve_representation()
|
||||
elif self.settings["is_point_cloud"]:
|
||||
return self.create_point_cloud_representation()
|
||||
elif self.settings["is_rectangular_extrusion"]:
|
||||
return self.create_rectangular_extrusion_representation()
|
||||
elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcRectangleProfileDef":
|
||||
return self.create_rectangle_extrusion_representation()
|
||||
elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcCircleProfileDef":
|
||||
return self.create_circle_extrusion_representation()
|
||||
elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef":
|
||||
return self.create_arbitrary_extrusion_representation()
|
||||
return self.create_mesh_representation()
|
||||
|
||||
def create_curve3d_representation(self):
|
||||
@@ -219,11 +227,35 @@ class Usecase:
|
||||
results.append(self.file.createIfcPolyline(points))
|
||||
return results
|
||||
|
||||
def create_rectangular_extrusion_representation(self):
|
||||
def create_rectangle_extrusion_representation(self):
|
||||
helper = Helper(self.file)
|
||||
indices = helper.auto_detect_rectangle_profile_extruded_area_solid(self.settings["geometry"])
|
||||
profile_def = helper.create_rectangle_profile_def(self.settings["blender_object"], indices["profile"])
|
||||
item = helper.create_extruded_area_solid(self.settings["blender_object"], indices["extrusion"], profile_def)
|
||||
profile_def = helper.create_rectangle_profile_def(self.settings["geometry"], indices["profile"])
|
||||
item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def)
|
||||
return self.file.createIfcShapeRepresentation(
|
||||
self.settings["context"],
|
||||
self.settings["context"].ContextIdentifier,
|
||||
"SweptSolid",
|
||||
[item],
|
||||
)
|
||||
|
||||
def create_circle_extrusion_representation(self):
|
||||
helper = Helper(self.file)
|
||||
indices = helper.auto_detect_circle_profile_extruded_area_solid(self.settings["geometry"])
|
||||
profile_def = helper.create_circle_profile_def(self.settings["geometry"], indices["profile"])
|
||||
item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def)
|
||||
return self.file.createIfcShapeRepresentation(
|
||||
self.settings["context"],
|
||||
self.settings["context"].ContextIdentifier,
|
||||
"SweptSolid",
|
||||
[item],
|
||||
)
|
||||
|
||||
def create_arbitrary_extrusion_representation(self):
|
||||
helper = Helper(self.file)
|
||||
indices = helper.auto_detect_arbitrary_closed_profile_extruded_area_solid(self.settings["geometry"])
|
||||
profile_def = helper.create_arbitrary_closed_profile_def(self.settings["geometry"], indices["profile"])
|
||||
item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def)
|
||||
return self.file.createIfcShapeRepresentation(
|
||||
self.settings["context"],
|
||||
self.settings["context"].ContextIdentifier,
|
||||
|
||||
@@ -18,7 +18,7 @@ class Helper:
|
||||
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180, verts=bm.verts, edges=bm.edges)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
face = None
|
||||
@@ -26,47 +26,89 @@ class Helper:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
profile = [l.vert.index for l in face.loops]
|
||||
face_verts_set = set(face.verts)
|
||||
|
||||
bm.edges.ensure_lookup_table()
|
||||
extrusion = None
|
||||
for edge in bm.edges:
|
||||
unshared_verts = set(edge.verts) - face_verts_set
|
||||
if len(unshared_verts) == 1:
|
||||
if unshared_verts.pop() == edge.verts[1]:
|
||||
extrusion = [edge.verts[0].index, edge.verts[1].index]
|
||||
else:
|
||||
extrusion = [edge.verts[1].index, edge.verts[0].index]
|
||||
break
|
||||
extrusion = self.detect_extrusion_edge(bm, face)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
|
||||
return {"profile": profile, "extrusion": extrusion}
|
||||
|
||||
# After a limited dissolve, we detect the circle profile as it should be the
|
||||
# only ngon. The extrusion direction is any edge that only shares a single
|
||||
# vertex with the profile. We prioritise the profile that has a downwards
|
||||
# normal.
|
||||
def auto_detect_circle_profile_extruded_area_solid(self, obj):
|
||||
# TODO
|
||||
# only ngon (this assumes the circle has at least a facetation of > 4 edges.
|
||||
# The extrusion direction is any edge that only shares a single vertex with
|
||||
# the profile. We prioritise the profile that has a downwards normal.
|
||||
def auto_detect_circle_profile_extruded_area_solid(self, mesh):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
for face in bm.faces:
|
||||
if len(face.verts) > 4:
|
||||
potential_faces.append(face)
|
||||
for face in potential_faces:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
|
||||
profile = [l.vert.index for l in face.loops]
|
||||
extrusion = self.detect_extrusion_edge(bm, face)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
|
||||
return {"profile": profile, "extrusion": extrusion}
|
||||
|
||||
# After a limited dissolve, the arbitrary profile is any ngon or tri.
|
||||
# Failing that, it is equivalent to a rectangular profile. The extrusion
|
||||
# direction is any edge that only shares a single vertex with the profile.
|
||||
# We prioritise the profile that has a downwards normal.
|
||||
def auto_detect_arbitrary_closed_profile_extruded_area_solid(self, obj):
|
||||
# TODO
|
||||
def auto_detect_arbitrary_closed_profile_extruded_area_solid(self, mesh):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
|
||||
|
||||
def create_extruded_area_solid(self, obj, extrusion_vertex_indices, profile_def):
|
||||
extrusion_edge = self.get_edges_in_v_indices(obj, extrusion_vertex_indices)[0]
|
||||
bm.faces.ensure_lookup_table()
|
||||
potential_faces = []
|
||||
for face in bm.faces:
|
||||
total_verts = len(face.verts)
|
||||
if total_verts > 4 or total_verts == 3:
|
||||
potential_faces.append(face)
|
||||
|
||||
if not potential_faces:
|
||||
potential_faces = bm.faces
|
||||
|
||||
for face in potential_faces:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
|
||||
profile = [l.vert.index for l in face.loops]
|
||||
extrusion = self.detect_extrusion_edge(bm, face)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
|
||||
return {"profile": profile, "extrusion": extrusion}
|
||||
|
||||
def detect_extrusion_edge(self, bm, profile_face):
|
||||
bm.edges.ensure_lookup_table()
|
||||
extrusion = None
|
||||
face_verts_set = set(profile_face.verts)
|
||||
for edge in bm.edges:
|
||||
unshared_verts = set(edge.verts) - face_verts_set
|
||||
if len(unshared_verts) == 1:
|
||||
if unshared_verts.pop() == edge.verts[1]:
|
||||
return [edge.verts[0].index, edge.verts[1].index]
|
||||
return [edge.verts[1].index, edge.verts[0].index]
|
||||
|
||||
def create_extruded_area_solid(self, mesh, extrusion_indices, profile_def):
|
||||
position = self.create_ifc_axis_2_placement_3d(
|
||||
profile_def["curve_ucs"]["center"], profile_def["curve_ucs"]["z_axis"], profile_def["curve_ucs"]["x_axis"]
|
||||
)
|
||||
direction = self.get_extrusion_direction(
|
||||
obj, profile_def["outer_curve_loop"], extrusion_edge, profile_def["curve_ucs"]
|
||||
)
|
||||
direction = self.get_extrusion_direction(mesh, extrusion_indices, profile_def["curve_ucs"])
|
||||
unit_direction = direction.normalized()
|
||||
return self.file.createIfcExtrudedAreaSolid(
|
||||
profile_def["curve"],
|
||||
@@ -75,37 +117,34 @@ class Helper:
|
||||
self.convert_si_to_unit(direction.length),
|
||||
)
|
||||
|
||||
def create_arbitrary_closed_profile_def(self, obj, profile_vertex_indices):
|
||||
outer_curve_loop = self.get_loop_from_v_indices(obj, profile_vertex_indices)
|
||||
curve_ucs = self.get_curve_profile_coordinate_system(obj, outer_curve_loop)
|
||||
outer_curve = self.create_polyline_from_loop(obj, outer_curve_loop, curve_ucs)
|
||||
def create_arbitrary_closed_profile_def(self, mesh, profile_indices):
|
||||
curve_ucs = self.get_curve_profile_coordinate_system(mesh, profile_indices)
|
||||
outer_curve = self.create_polyline_from_loop(mesh, profile_indices, curve_ucs)
|
||||
curve = self.file.createIfcArbitraryClosedProfileDef("AREA", None, outer_curve)
|
||||
return {"outer_curve_loop": outer_curve_loop, "curve_ucs": curve_ucs, "curve": curve}
|
||||
return {"curve_ucs": curve_ucs, "curve": curve}
|
||||
|
||||
def create_rectangle_profile_def(self, obj, profile_vertex_indices):
|
||||
outer_curve_loop = self.get_loop_from_v_indices(obj, profile_vertex_indices)
|
||||
curve_ucs = self.get_curve_profile_coordinate_system(obj, outer_curve_loop)
|
||||
def create_rectangle_profile_def(self, mesh, profile_indices):
|
||||
curve_ucs = self.get_curve_profile_coordinate_system(mesh, profile_indices)
|
||||
xdim = self.convert_si_to_unit(
|
||||
(obj.data.vertices[outer_curve_loop[0]].co - obj.data.vertices[outer_curve_loop[1]].co).length
|
||||
(mesh.vertices[profile_indices[0]].co - mesh.vertices[profile_indices[1]].co).length
|
||||
)
|
||||
ydim = self.convert_si_to_unit(
|
||||
(obj.data.vertices[outer_curve_loop[1]].co - obj.data.vertices[outer_curve_loop[2]].co).length
|
||||
(mesh.vertices[profile_indices[1]].co - mesh.vertices[profile_indices[2]].co).length
|
||||
)
|
||||
curve = self.file.createIfcRectangleProfileDef("AREA", None, None, xdim, ydim)
|
||||
return {"outer_curve_loop": outer_curve_loop, "curve_ucs": curve_ucs, "curve": curve}
|
||||
return {"curve_ucs": curve_ucs, "curve": curve}
|
||||
|
||||
def create_circle_profile_def(self, obj, profile_vertex_indices):
|
||||
indices = profile_vertex_indices
|
||||
outer_curve_loop = self.get_loop_from_v_indices(obj, indices)
|
||||
curve_ucs = self.get_curve_profile_coordinate_system(obj, outer_curve_loop)
|
||||
def create_circle_profile_def(self, mesh, profile_indices):
|
||||
curve_ucs = self.get_curve_profile_coordinate_system(mesh, profile_indices)
|
||||
radius = self.convert_si_to_unit(
|
||||
abs((obj.data.vertices[indices[0]].co - obj.data.vertices[indices[int(len(indices) / 2)]].co).length) / 2
|
||||
abs((mesh.vertices[profile_indices[0]].co - mesh.vertices[profile_indices[int(len(profile_indices) / 2)]].co).length) / 2
|
||||
)
|
||||
center = Vector((0, 0))
|
||||
position = self.create_ifc_axis_2_placement_2d(center, Vector((1, 0)))
|
||||
curve = self.file.createIfcCircleProfileDef("AREA", None, position, radius)
|
||||
return {"outer_curve_loop": outer_curve_loop, "curve_ucs": curve_ucs, "curve": curve}
|
||||
return {"curve_ucs": curve_ucs, "curve": curve}
|
||||
|
||||
# Not used anywhere, but probably useful in the future
|
||||
def get_loop_from_v_indices(self, obj, indices):
|
||||
edges = self.get_edges_in_v_indices(obj, indices)
|
||||
loop = self.get_loop_from_edges(edges)
|
||||
@@ -150,18 +189,16 @@ class Helper:
|
||||
def get_edges_in_v_indices(self, obj, indices):
|
||||
return [e for e in obj.data.edges if (e.vertices[0] in indices and e.vertices[1] in indices)]
|
||||
|
||||
def get_curve_profile_coordinate_system(self, obj, loop):
|
||||
def get_curve_profile_coordinate_system(self, mesh, loop):
|
||||
profile_face = bpy.data.meshes.new("profile_face")
|
||||
profile_verts = [
|
||||
(obj.data.vertices[p].co.x, obj.data.vertices[p].co.y, obj.data.vertices[p].co.z) for p in loop
|
||||
]
|
||||
profile_verts = [(mesh.vertices[p].co.x, mesh.vertices[p].co.y, mesh.vertices[p].co.z) for p in loop]
|
||||
profile_faces = [tuple(range(0, len(profile_verts)))]
|
||||
profile_face.from_pydata(profile_verts, [], profile_faces)
|
||||
center = profile_face.polygons[0].center
|
||||
if (obj.data.vertices[loop[1]].co - obj.data.vertices[loop[0]].co).length < 0.01:
|
||||
x_axis = (obj.data.vertices[loop[0]].co - center).normalized()
|
||||
if (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).length < 0.01:
|
||||
x_axis = (mesh.vertices[loop[0]].co - center).normalized()
|
||||
else:
|
||||
x_axis = (obj.data.vertices[loop[1]].co - obj.data.vertices[loop[0]].co).normalized()
|
||||
x_axis = (mesh.vertices[loop[1]].co - mesh.vertices[loop[0]].co).normalized()
|
||||
z_axis = profile_face.polygons[0].normal.normalized()
|
||||
y_axis = z_axis.cross(x_axis).normalized()
|
||||
matrix = Matrix((x_axis, y_axis, z_axis))
|
||||
@@ -177,10 +214,10 @@ class Helper:
|
||||
def convert_si_to_unit(self, co):
|
||||
return co / self.unit_scale
|
||||
|
||||
def create_polyline_from_loop(self, obj, loop, curve_ucs):
|
||||
def create_polyline_from_loop(self, mesh, loop, curve_ucs):
|
||||
points = []
|
||||
for point in loop:
|
||||
transformed_point = curve_ucs["matrix"] @ obj.data.vertices[point].co
|
||||
transformed_point = curve_ucs["matrix"] @ mesh.vertices[point].co
|
||||
points.append(self.create_cartesian_point(transformed_point.x, transformed_point.y))
|
||||
points.append(points[0])
|
||||
return self.file.createIfcPolyline(points)
|
||||
@@ -193,15 +230,21 @@ class Helper:
|
||||
z = self.convert_si_to_unit(z)
|
||||
return self.file.createIfcCartesianPoint((x, y, z))
|
||||
|
||||
def get_extrusion_direction(self, obj, outer_curve_loop, extrusion_edge, curve_ucs):
|
||||
start, end = self.get_start_and_end_of_extrusion(outer_curve_loop, extrusion_edge)
|
||||
return curve_ucs["matrix"] @ (curve_ucs["center"] + (obj.data.vertices[end].co - obj.data.vertices[start].co))
|
||||
def get_extrusion_direction(self, mesh, extrusion_indices, curve_ucs):
|
||||
return curve_ucs["matrix"] @ (
|
||||
curve_ucs["center"] + (mesh.vertices[extrusion_indices[1]].co - mesh.vertices[extrusion_indices[0]].co)
|
||||
)
|
||||
|
||||
def get_start_and_end_of_extrusion(self, profile_points, extrusion_edge):
|
||||
if extrusion_edge.vertices[0] in profile_points:
|
||||
return (extrusion_edge.vertices[0], extrusion_edge.vertices[1])
|
||||
return (extrusion_edge.vertices[1], extrusion_edge.vertices[0])
|
||||
|
||||
def create_ifc_axis_2_placement_2d(self, point, forward):
|
||||
return self.file.createIfcAxis2Placement2D(
|
||||
self.create_cartesian_point(point.x, point.y), self.file.createIfcDirection((forward.x, forward.y))
|
||||
)
|
||||
|
||||
def create_ifc_axis_2_placement_3d(self, point, up, forward):
|
||||
return self.file.createIfcAxis2Placement3D(
|
||||
self.create_cartesian_point(point.x, point.y, point.z),
|
||||
|
||||
@@ -306,7 +306,7 @@ class UpdateMeshRepresentation(bpy.types.Operator):
|
||||
bl_idname = "bim.update_mesh_representation"
|
||||
bl_label = "Update Mesh Representation"
|
||||
obj: bpy.props.StringProperty()
|
||||
ifc_representation_type: bpy.props.StringProperty()
|
||||
ifc_representation_class: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
if not ContextData.is_loaded:
|
||||
@@ -350,11 +350,28 @@ class UpdateMeshRepresentation(bpy.types.Operator):
|
||||
"total_items": max(1, len(obj.material_slots)),
|
||||
"should_force_faceted_brep": context.scene.BIMGeometryProperties.should_force_faceted_brep,
|
||||
"should_force_triangulation": context.scene.BIMGeometryProperties.should_force_triangulation,
|
||||
"is_rectangular_extrusion": self.ifc_representation_type == "IfcExtrudedAreaSolid/IfcRectangleProfileDef"
|
||||
"ifc_representation_class": self.ifc_representation_class
|
||||
}
|
||||
|
||||
new_representation = add_representation.Usecase(self.file, representation_data).execute()
|
||||
|
||||
#if product.is_a("IfcWall"):
|
||||
# # Generate axis representation
|
||||
# axis_context_id = get_context_id("Model", "Axis", "MODEL_VIEW")
|
||||
# old_axis = ifcopenshell.util.element.get_representation(product, "Model", "Axis", "MODEL_VIEW")
|
||||
# if (
|
||||
# axis_context_id
|
||||
# and old_axis
|
||||
# and context_of_items.ContextType == "Model"
|
||||
# and context_of_items.ContextIdentifier
|
||||
# and context_of_items.ContextIdentifier == "Body"
|
||||
# ):
|
||||
# has_axis_generator = False
|
||||
# if has_axis_generator:
|
||||
# # TODO, just pseudocode for now
|
||||
# representation_data["geometry"] = axis_generator_function_call
|
||||
# pass
|
||||
|
||||
box_context_id = get_context_id("Model", "Box", "MODEL_VIEW")
|
||||
old_box = ifcopenshell.util.element.get_representation(product, "Model", "Box", "MODEL_VIEW")
|
||||
if (
|
||||
|
||||
@@ -82,8 +82,16 @@ class BIM_PT_mesh(Panel):
|
||||
row.operator("bim.update_mesh_representation")
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.update_mesh_representation", text="Convert Mesh to Rectangular Extrusion")
|
||||
op.ifc_representation_type = "IfcExtrudedAreaSolid/IfcRectangleProfileDef"
|
||||
op = row.operator("bim.update_mesh_representation", text="Update Mesh As Rectangle Extrusion")
|
||||
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcRectangleProfileDef"
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.update_mesh_representation", text="Update Mesh As Circle Extrusion")
|
||||
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcCircleProfileDef"
|
||||
|
||||
row = layout.row()
|
||||
op = row.operator("bim.update_mesh_representation", text="Update Mesh As Arbitrary Extrusion")
|
||||
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef"
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.get_representation_ifc_parameters")
|
||||
|
||||
Reference in New Issue
Block a user