mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
See #6190. Polyline tool support for XZ and YZ planes for profiles.
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user