See #6190. Polyline tool support for XZ and YZ planes for profiles.

This commit is contained in:
Bruno Perdigão
2025-02-20 18:46:23 -03:00
parent d37baa4e0c
commit 9f0d408e79
2 changed files with 94 additions and 78 deletions
+72 -65
View File
@@ -429,71 +429,78 @@ def get_horizontal_profile_preview_data(context, relating_type):
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
# Create profile curve
scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0))
grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts]
profile_curve = bpy.data.curves.new("Profile", type="CURVE")
profile_curve.dimensions = "2D"
profile_curve.splines.new("POLY")
profile_curve.splines[0].points.add(len(grouped_verts))
for i, point in enumerate(profile_curve.splines[0].points):
if i == len(grouped_verts): # Close curve
point.co = Vector((*grouped_verts[0], 0))
continue
point.co = Vector((*grouped_verts[i], 0))
profile_obj = bpy.data.objects.new("Profile", profile_curve)
# Create path curve with profile object as bevel
path_curve = bpy.data.curves.new("Polyline", type="CURVE")
path_curve.dimensions = "2D"
path_curve.splines.new("POLY")
path_curve.splines[0].points.add(len(polyline_verts) - 1)
for i, point in enumerate(path_curve.splines[0].points):
point.co = Vector((*polyline_verts[i], 0))
path_curve.splines[0].use_smooth = False
path_curve.bevel_mode = "OBJECT"
path_curve.bevel_object = profile_obj
# Convert path curve to mesh
# This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here
path_obj = bpy.data.objects.new("Preview", path_curve)
context.scene.collection.objects.link(path_obj)
bpy.context.view_layer.objects.active = path_obj
dg = context.evaluated_depsgraph_get()
path_obj = path_obj.evaluated_get(dg)
me = path_obj.to_mesh()
# Create bmesh from path mesh
bm = bmesh.new()
new_verts = [bm.verts.new(v.co) for v in me.vertices]
index = [[v for v in edge.vertices] for edge in me.edges]
new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index]
for face in me.polygons:
verts = [new_verts[i] for i in face.vertices]
bm.faces.new(verts)
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True)
bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True)
try:
bpy.data.curves.remove(profile_obj.data, do_unlink=True)
except:
pass
try:
bpy.data.curves.remove(path_obj.data, do_unlink=True)
except:
pass
data = {}
data["verts"] = [tuple(v.co) for v in bm.verts]
data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
data["verts"] = []
data["edges"] = []
data["tris"] = []
grouped_verts = [(v) for v in grouped_verts]
all_bm = bmesh.new()
for i in range(len(polyline_verts) - 1):
mesh = bpy.data.meshes.new("TempMesh")
# Create the initial mesh from the profile verts
bm = create_bmesh_from_vertices(grouped_verts, is_closed=True)
bm.verts.ensure_lookup_table()
# Creates the clipping plane formed by two segments.
# The first one is for the profile start, based on the current and previous segment of the polyline.
# The second is for the profile end, based on the current and the next segment.
if i == 0:
d = (polyline_verts[i+1] - polyline_verts[i]).normalized()
clip_start = d
else:
d1 = (polyline_verts[i] - polyline_verts[i-1]).normalized()
d2 = (polyline_verts[i] - polyline_verts[i+1]).normalized()
clip_start = (d1-d2).normalized()
if i == len(polyline_verts) - 2:
d = (polyline_verts[i+1] - polyline_verts[i]).normalized()
clip_end = d
else:
d1 = (polyline_verts[i+1] - polyline_verts[i]).normalized()
d2 = (polyline_verts[i+1] - polyline_verts[i+2]).normalized()
clip_end = (d1-d2).normalized()
# Rotates the profile face to the right direction
direction = polyline_verts[i+1] - polyline_verts[i]
position = polyline_verts[i]
rotation_matrix = direction.to_track_quat('Z', 'Y').to_matrix().to_4x4()
bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix)
bmesh.ops.translate(bm, verts=bm.verts, vec=position)
bmesh.ops.translate(bm, verts=bm.verts, vec=-direction)
# Extrude and move the new face
last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)]
bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3)
# Apply the cutting planes
cut = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=polyline_verts[i], plane_no=clip_start, clear_inner=True)
bm.verts.index_update()
bm.edges.index_update()
cut = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=polyline_verts[i+1], plane_no=clip_end, clear_outer=True)
bm.to_mesh(mesh)
bm.free()
mesh.update()
all_bm.from_mesh(mesh)
bpy.data.meshes.remove(bpy.data.meshes["TempMesh"])
# It's necessary to add the mesh to an object to get the expected result.
mesh = bpy.data.meshes.new("TempMesh2")
all_bm.to_mesh(mesh)
all_bm.free()
obj = bpy.data.objects.new('TempObj', mesh)
bm = bmesh.new()
bm.from_mesh(obj.data)
bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"])
verts = [tuple(v.co) for v in bm.verts]
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
data["verts"] = verts
data["edges"] = edges
data["tris"] = tris
bm.free()
return data
@@ -632,21 +639,21 @@ class PolylineOperator:
if x:
if event.shift and event.value == "PRESS" and event.type == "X":
self.tool_state.use_default_container = False
self.tool_state.plane_method = "YZ"
self.tool_state.plane_method = "YZ" if self.tool_state.plane_method !="YZ" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
if y:
if event.shift and event.value == "PRESS" and event.type == "Y":
self.tool_state.use_default_container = False
self.tool_state.plane_method = "XZ"
self.tool_state.plane_method = "XZ" if self.tool_state.plane_method !="XZ" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
if z:
if event.shift and event.value == "PRESS" and event.type == "Z":
self.tool_state.use_default_container = False
self.tool_state.plane_method = "XY"
self.tool_state.plane_method = "XY" if self.tool_state.plane_method !="XY" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
+22 -13
View File
@@ -48,6 +48,7 @@ class DumbProfileGenerator:
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self, insertion_type="CURSOR"):
self.insertion_type = insertion_type
self.file = tool.Ifc.get()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
material = ifcopenshell.util.element.get_material(self.relating_type)
@@ -70,9 +71,9 @@ class DumbProfileGenerator:
self.rotation = 0
self.location = Vector((0, 0, 0))
self.cardinal_point = int(props.cardinal_point)
if insertion_type == "POLYLINE":
if self.insertion_type == "POLYLINE":
return self.derive_from_polyline()
elif insertion_type == "CURSOR":
elif self.insertion_type == "CURSOR":
return self.derive_from_cursor()
def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]:
@@ -107,13 +108,17 @@ class DumbProfileGenerator:
matrix_world = Matrix()
if self.relating_type.is_a() not in ("IfcColumnType", "IfcPileType"):
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
if self.insertion_type not in {"POLYLINE"}:
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
else:
rotation_matrix = self.direction.to_track_quat('Z', 'Y')
matrix_world = rotation_matrix.to_matrix().to_4x4() @ matrix_world
matrix_world.translation = self.location
if self.container_obj:
if self.insertion_type not in {"POLYLINE"} and self.container_obj:
matrix_world.translation.z = self.container_obj.location.z
element = bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
@@ -171,19 +176,19 @@ class DumbProfileGenerator:
return obj
def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]:
direction = coords[1] - coords[0]
length = direction.length
self.direction = coords[1] - coords[0]
length = self.direction.length
if round(length, 4) < 0.1:
return
data = {"coords": coords}
self.depth = length
self.rotation = atan2(direction[1], direction[0])
self.rotation = atan2(self.direction[1], self.direction[0])
if should_round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
nearest_degree = (pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_profile()
@@ -1122,6 +1127,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
def __init__(self):
super().__init__()
self.input_ui = tool.Polyline.create_input_ui(init_z=True)
self.input_options = ["D", "A", "X", "Y", "Z"]
self.relating_type = None
props = tool.Model.get_model_props()
relating_type_id = props.relating_type_id
@@ -1166,7 +1173,9 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
self.handle_mouse_move(context, event, should_round=True)
self.choose_axis(event)
self.choose_axis(event, z=True)
self.choose_plane(event)
self.handle_snap_selection(context, event)
@@ -1202,6 +1211,6 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
def _invoke(self, context, event):
super().invoke(context, event)
ProductDecorator.install(context)
self.tool_state.use_default_container = True
self.tool_state.plane_method = "XY"
self.tool_state.use_default_container = False
self.tool_state.plane_method = None
return {"RUNNING_MODAL"}