Fair warning, this is going to be a loooong article! 😁
The question is: How easy is it to create a Blender addon from scratch with the help of ChatGPT and is the code any good?
TLDR; It is fairly easy and while the code is mostly ok, details in the algorithm require close scrutiny and once it has an ok idea, it tends to suddenly forget things once tasked with an enhancement. In short: It acts like a junior software dev. It may save considerable time, but you have to keep a close eye on it (and it is annoyingly cheery an polite too! 😏)
The goal
The idea is to create a simple addon with an operator that will select all edges that are colinear with any of the selected edges. We use Vscode CoPilot with ChatGPT 4.1 and see how far we get.
The first attempt
Me: create a Blender Operator that operates on an active Mesh object, and that selects all edges that are colinear with any of the currently selected edges
GitHub Copilot: Certainly! Here’s a Blender Operator in Python that selects all edges colinear with any currently selected edge in the active mesh object. This script uses Blender’s bmesh module for mesh manipulation.
import bpy import bmesh from mathutils import Vector class MESH_OT_select_colinear_edges(bpy.types.Operator): """Select all edges colinear with any currently selected edge""" bl_idname = "mesh.select_colinear_edges" bl_label = "Select Colinear Edges" bl_options = {'REGISTER', 'UNDO'} angle_threshold: bpy.props.FloatProperty( name="Angle Threshold", description="Maximum angle (degrees) to consider edges colinear", default=1.0, min=0.0, max=10.0 ) def execute(self, context): obj = context.active_object if not obj or obj.type != 'MESH': self.report({'ERROR'}, "Active object is not a mesh") return {'CANCELLED'} bm = bmesh.from_edit_mesh(obj.data) bm.edges.ensure_lookup_table() # Find all selected edges and their directions selected_dirs = [] for e in bm.edges: if e.select: v1, v2 = e.verts dir_vec = (v2.co - v1.co).normalized() selected_dirs.append(dir_vec) if not selected_dirs: self.report({'WARNING'}, "No edges selected") return {'CANCELLED'} # Deselect all edges first for e in bm.edges: e.select = False # Select all edges colinear with any selected edge threshold_rad = self.angle_threshold * 3.14159265 / 180.0 cos_threshold = abs(Vector((1, 0, 0)).angle(Vector((1, 0, 0)))) # Always 0, but for clarity for e in bm.edges: v1, v2 = e.verts dir_vec = (v2.co - v1.co).normalized() for sel_dir in selected_dirs: angle = dir_vec.angle(sel_dir) if angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad: e.select = True break bmesh.update_edit_mesh(obj.data) return {'FINISHED'} def menu_func(self, context): self.layout.operator(MESH_OT_select_colinear_edges.bl_idname) def register(): bpy.utils.register_class(MESH_OT_select_colinear_edges) bpy.types.VIEW3D_MT_edit_mesh_edges.append(menu_func) def unregister(): bpy.types.VIEW3D_MT_edit_mesh_edges.remove(menu_func) bpy.utils.unregister_class(MESH_OT_select_colinear_edges) if __name__ == "__main__": register()Usage:
1. Switch to Edit Mode and select one or more edges.
2. Run this script in Blender’s Text Editor.
3. Press `F3` and search for "Select Colinear Edges" or find it in the Edge menu.
You can adjust the `Angle Threshold` to control how strict the colinearity check is.
Now this is pretty solid for a first attempt and it even takes care of registering the operator. It would have been nice to have a poll() function but that is not essential (and I didn't ask for it.)
Adding an extra option
Now let's see how it deals with a request to add an extra option.
Me: add an option to only select edges that are directly or indirectly connected to the originally selected edges
import bpy import bmesh from mathutils import Vector class MESH_OT_select_colinear_edges(bpy.types.Operator): """Select all edges colinear with any currently selected edge""" bl_idname = "mesh.select_colinear_edges" bl_label = "Select Colinear Edges" bl_options = {'REGISTER', 'UNDO'} angle_threshold: bpy.props.FloatProperty( name="Angle Threshold", description="Maximum angle (degrees) to consider edges colinear", default=1.0, min=0.0, max=10.0 ) only_connected: bpy.props.BoolProperty( name="Only Connected", description="Only select edges directly or indirectly connected to the originally selected edges", default=False ) def execute(self, context): obj = context.active_object if not obj or obj.type != 'MESH': self.report({'ERROR'}, "Active object is not a mesh") return {'CANCELLED'} bm = bmesh.from_edit_mesh(obj.data) bm.edges.ensure_lookup_table() # Find all originally selected edges and their directions original_selected_edges = [e for e in bm.edges if e.select] if not original_selected_edges: self.report({'WARNING'}, "No edges selected") return {'CANCELLED'} selected_dirs = [] for e in original_selected_edges: v1, v2 = e.verts dir_vec = (v2.co - v1.co).normalized() selected_dirs.append(dir_vec) # Optionally, build a set of connected edges connected_edges = set() if self.only_connected: # BFS to find all edges connected to the originally selected edges visited_verts = set() edge_queue = list(original_selected_edges) for e in original_selected_edges: connected_edges.add(e) visited_verts.update(e.verts) while edge_queue: current_edge = edge_queue.pop(0) for v in current_edge.verts: for e2 in v.link_edges: if e2 not in connected_edges: connected_edges.add(e2) edge_queue.append(e2) visited_verts.update(e2.verts) # Deselect all edges first for e in bm.edges: e.select = False # Select all edges colinear with any selected edge (and optionally connected) threshold_rad = self.angle_threshold * 3.14159265 / 180.0 for e in bm.edges: if self.only_connected and e not in connected_edges: continue v1, v2 = e.verts dir_vec = (v2.co - v1.co).normalized() for sel_dir in selected_dirs: angle = dir_vec.angle(sel_dir) if angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad: e.select = True break bmesh.update_edit_mesh(obj.data) return {'FINISHED'} def menu_func(self, context): self.layout.operator(MESH_OT_select_colinear_edges.bl_idname) def register(): bpy.utils.register_class(MESH_OT_select_colinear_edges) bpy.types.VIEW3D_MT_edit_mesh_edges.append(menu_func) def unregister(): bpy.types.VIEW3D_MT_edit_mesh_edges.remove(menu_func) bpy.utils.unregister_class(MESH_OT_select_colinear_edges) if __name__ == "__main__": register()
This time it is my fault: I guess I wasn't clear enough with my specification.Me: that is not exactly what I meant: I want only those edges to be selected that are connected to the originally selected edges along a path that would only contain colinear edges
GitHub Copilot: Thank you for clarifying!
You want to select all edges that are reachable from the originally selected edges by traversing only colinear edges (within the angle threshold).
Here’s an updated operator that performs a breadth-first search (BFS) from each originally selected edge, only traversing to neighboring edges if they are colinear with the current edge.
import bpy import bmesh from mathutils import Vector class MESH_OT_select_colinear_edges(bpy.types.Operator): """Select all edges colinear and connected via colinear paths to any currently selected edge""" bl_idname = "mesh.select_colinear_edges" bl_label = "Select Colinear Edges (Colinear Paths)" bl_options = {'REGISTER', 'UNDO'} angle_threshold: bpy.props.FloatProperty( name="Angle Threshold", description="Maximum angle (degrees) to consider edges colinear", default=1.0, min=0.0, max=10.0 ) only_colinear_paths: bpy.props.BoolProperty( name="Only Colinear Paths", description="Only select edges connected via colinear paths from the originally selected edges", default=True ) def execute(self, context): obj = context.active_object if not obj or obj.type != 'MESH': self.report({'ERROR'}, "Active object is not a mesh") return {'CANCELLED'} bm = bmesh.from_edit_mesh(obj.data) bm.edges.ensure_lookup_table() # Find all originally selected edges original_selected_edges = [e for e in bm.edges if e.select] if not original_selected_edges: self.report({'WARNING'}, "No edges selected") return {'CANCELLED'} # Deselect all edges first for e in bm.edges: e.select = False threshold_rad = self.angle_threshold * 3.14159265 / 180.0 # Helper to get edge direction as a normalized vector def edge_dir(edge): v1, v2 = edge.verts return (v2.co - v1.co).normalized() # BFS for each originally selected edge visited = set() queue = [] for e in original_selected_edges: queue.append((e, edge_dir(e))) visited.add(e) while queue: current_edge, current_dir = queue.pop(0) current_edge.select = True # For each vertex of the current edge for v in current_edge.verts: for neighbor in v.link_edges: if neighbor is current_edge or neighbor in visited: continue neighbor_dir = edge_dir(neighbor) angle = current_dir.angle(neighbor_dir) # Check colinearity (both directions) if angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad: queue.append((neighbor, neighbor_dir)) visited.add(neighbor) bmesh.update_edit_mesh(obj.data) return {'FINISHED'} def menu_func(self, context): self.layout.operator(MESH_OT_select_colinear_edges.bl_idname) def register(): bpy.utils.register_class(MESH_OT_select_colinear_edges) bpy.types.VIEW3D_MT_edit_mesh_edges.append(menu_func) def unregister(): bpy.types.VIEW3D_MT_edit_mesh_edges.remove(menu_func) bpy.utils.unregister_class(MESH_OT_select_colinear_edges) if __name__ == "__main__": register()How it works: - For each originally selected edge, it traverses to neighboring edges only if they are colinear (within the angle threshold). - Only edges reachable via such colinear paths are selected. Let me know if you need further adjustments!
It added the option alright but didn't bother to act on it in the code. Is it getting passive aggressive? Also, that "thanks for clarifying" is really annoying.
You forgot to use your option bro!
Me: there is no code that takes into account the only_colinear_paths option. The code as show acts as if that option is always true. It should check and if false, select all edges that are colinear with the originally selected edges
GitHub Copilot: You're absolutely right!
Here’s the corrected operator. Now, if `only_colinear_paths` is `False`, it simply selects all edges colinear (within the threshold) with any originally selected edge, regardless of connectivity. If `only_colinear_paths` is `True`, it performs the BFS as before.
import bpy import bmesh from mathutils import Vector class MESH_OT_select_colinear_edges(bpy.types.Operator): """Select all edges colinear with any currently selected edge, optionally only along colinear paths""" bl_idname = "mesh.select_colinear_edges" bl_label = "Select Colinear Edges (Colinear Paths Option)" bl_options = {'REGISTER', 'UNDO'} angle_threshold: bpy.props.FloatProperty( name="Angle Threshold", description="Maximum angle (degrees) to consider edges colinear", default=1.0, min=0.0, max=10.0 ) only_colinear_paths: bpy.props.BoolProperty( name="Only Colinear Paths", description="Only select edges connected via colinear paths from the originally selected edges", default=True ) def execute(self, context): obj = context.active_object if not obj or obj.type != 'MESH': self.report({'ERROR'}, "Active object is not a mesh") return {'CANCELLED'} bm = bmesh.from_edit_mesh(obj.data) bm.edges.ensure_lookup_table() # Find all originally selected edges original_selected_edges = [e for e in bm.edges if e.select] if not original_selected_edges: self.report({'WARNING'}, "No edges selected") return {'CANCELLED'} # Deselect all edges first for e in bm.edges: e.select = False threshold_rad = self.angle_threshold * 3.14159265 / 180.0 # Helper to get edge direction as a normalized vector def edge_dir(edge): v1, v2 = edge.verts return (v2.co - v1.co).normalized() if self.only_colinear_paths: # BFS for each originally selected edge, traversing only colinear edges visited = set() queue = [] for e in original_selected_edges: queue.append((e, edge_dir(e))) visited.add(e) while queue: current_edge, current_dir = queue.pop(0) current_edge.select = True for v in current_edge.verts: for neighbor in v.link_edges: if neighbor is current_edge or neighbor in visited: continue neighbor_dir = edge_dir(neighbor) angle = current_dir.angle(neighbor_dir) if angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad: queue.append((neighbor, neighbor_dir)) visited.add(neighbor) else: # Select all edges colinear with any originally selected edge, regardless of connectivity selected_dirs = [edge_dir(e) for e in original_selected_edges] for e in bm.edges: dir_vec = edge_dir(e) for sel_dir in selected_dirs: angle = dir_vec.angle(sel_dir) if angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad: e.select = True break bmesh.update_edit_mesh(obj.data) return {'FINISHED'} def menu_func(self, context): self.layout.operator(MESH_OT_select_colinear_edges.bl_idname) def register(): bpy.utils.register_class(MESH_OT_select_colinear_edges) bpy.types.VIEW3D_MT_edit_mesh_edges.append(menu_func) def unregister(): bpy.types.VIEW3D_MT_edit_mesh_edges.remove(menu_func) bpy.utils.unregister_class(MESH_OT_select_colinear_edges) if __name__ == "__main__": register()
Not too bad for ten minutes work. Let's do some polishing.
A change of menu
Me: ok, looks good, but could you add it to the Select -> Select similar menu instead?import bpy import bmesh from mathutils import Vector class MESH_OT_select_colinear_edges(bpy.types.Operator): """Select all edges colinear with any currently selected edge, optionally only along colinear paths""" bl_idname = "mesh.select_colinear_edges" bl_label = "Colinear Edges" bl_options = {'REGISTER', 'UNDO'} angle_threshold: bpy.props.FloatProperty( name="Angle Threshold", description="Maximum angle (degrees) to consider edges colinear", default=1.0, min=0.0, max=10.0 ) only_colinear_paths: bpy.props.BoolProperty( name="Only Colinear Paths", description="Only select edges connected via colinear paths from the originally selected edges", default=True ) @classmethod def poll(cls, context): obj = context.active_object return obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH' def execute(self, context): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) bm.edges.ensure_lookup_table() # Find all originally selected edges original_selected_edges = [e for e in bm.edges if e.select] if not original_selected_edges: self.report({'WARNING'}, "No edges selected") return {'CANCELLED'} # Deselect all edges first for e in bm.edges: e.select = False threshold_rad = self.angle_threshold * 3.14159265 / 180.0 def edge_dir(edge): v1, v2 = edge.verts return (v2.co - v1.co).normalized() if self.only_colinear_paths: visited = set() queue = [] for e in original_selected_edges: queue.append((e, edge_dir(e))) visited.add(e) while queue: current_edge, current_dir = queue.pop(0) current_edge.select = True for v in current_edge.verts: for neighbor in v.link_edges: if neighbor is current_edge or neighbor in visited: continue neighbor_dir = edge_dir(neighbor) angle = current_dir.angle(neighbor_dir) if angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad: queue.append((neighbor, neighbor_dir)) visited.add(neighbor) else: selected_dirs = [edge_dir(e) for e in original_selected_edges] for e in bm.edges: dir_vec = edge_dir(e) for sel_dir in selected_dirs: angle = dir_vec.angle(sel_dir) if angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad: e.select = True break bmesh.update_edit_mesh(obj.data) return {'FINISHED'} def menu_func(self, context): self.layout.operator( MESH_OT_select_colinear_edges.bl_idname, text="Colinear Edges" ) def register(): bpy.utils.register_class(MESH_OT_select_colinear_edges) bpy.types.VIEW3D_MT_edit_mesh_select_similar.append(menu_func) def unregister(): bpy.types.VIEW3D_MT_edit_mesh_select_similar.remove(menu_func) bpy.utils.unregister_class(MESH_OT_select_colinear_edges) if __name__ == "__main__": register()
Now you’ll find "Colinear Edges" in the Select → Select Similar menu in Edit Mode.
A proper addon should have a bl_info dict, otherwise Blender will complain.
Don't forget bl_info!
Me: also add a proper bl_info please
Me: the colinearity tests are wrong: currenly the check if edges are parallel, but parallel is not always colinear. Could you make these test explicitely test for colinearity?
bl_info = { "name": "Select Colinear Edges", "author": "michel.anders, GitHub Copilot", "version": (1, 1, 0), "blender": (2, 80, 0), "location": "View3D > Select > Select Similar > Colinear Edges", "description": "Select all edges colinear with any currently selected edge, optionally only along colinear paths", "category": "Mesh", } import bpy import bmesh from mathutils import Vector class MESH_OT_select_colinear_edges(bpy.types.Operator): """Select all edges colinear with any currently selected edge, optionally only along colinear paths""" bl_idname = "mesh.select_colinear_edges" bl_label = "Colinear Edges" bl_options = {"REGISTER", "UNDO"} angle_threshold: bpy.props.FloatProperty( name="Angle Threshold", description="Maximum angle (degrees) to consider edges colinear", default=1.0, min=0.0, max=10.0, ) only_colinear_paths: bpy.props.BoolProperty( name="Only Colinear Paths", description="Only select edges connected via colinear paths from the originally selected edges", default=True, ) @classmethod def poll(cls, context): obj = context.active_object return obj and obj.type == "MESH" and context.mode == "EDIT_MESH" def execute(self, context): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) bm.edges.ensure_lookup_table() # Find all originally selected edges original_selected_edges = [e for e in bm.edges if e.select] if not original_selected_edges: self.report({"WARNING"}, "No edges selected") return {"CANCELLED"} # Deselect all edges first for e in bm.edges: e.select = False threshold_rad = self.angle_threshold * 3.14159265 / 180.0 def edge_dir(edge): v1, v2 = edge.verts return (v2.co - v1.co).normalized() if self.only_colinear_paths: visited = set() queue = [] for e in original_selected_edges: queue.append((e, edge_dir(e))) visited.add(e) while queue: current_edge, current_dir = queue.pop(0) current_edge.select = True for v in current_edge.verts: for neighbor in v.link_edges: if neighbor is current_edge or neighbor in visited: continue neighbor_dir = edge_dir(neighbor) angle = current_dir.angle(neighbor_dir) if ( angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad ): queue.append((neighbor, neighbor_dir)) visited.add(neighbor) else: selected_dirs = [edge_dir(e) for e in original_selected_edges] for e in bm.edges: dir_vec = edge_dir(e) for sel_dir in selected_dirs: angle = dir_vec.angle(sel_dir) if angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad: e.select = True break bmesh.update_edit_mesh(obj.data) return {"FINISHED"} def menu_func(self, context): self.layout.operator(MESH_OT_select_colinear_edges.bl_idname, text="Colinear Edges") def register(): bpy.utils.register_class(MESH_OT_select_colinear_edges) bpy.types.VIEW3D_MT_edit_mesh_select_similar.append(menu_func) def unregister(): bpy.types.VIEW3D_MT_edit_mesh_select_similar.remove(menu_func) bpy.utils.unregister_class(MESH_OT_select_colinear_edges) if __name__ == "__main__": register()
The code wasn't properly tested of course...
And here working with an LLM shows it's major weakness: it has no idea what it is doing or even if the code compiled, let alone whether the algorithm is correct at all. It doesn't test its own work!
GitHub Copilot: Certainly!
Currently, the code checks if two edge direction vectors are parallel (angle close to 0 or 180 degrees), but this does not guarantee that the edges are colinear (i.e., lie on the same infinite line).
To be colinear, two edges must be parallel and their points must be aligned (the vector between any point on one edge and any point on the other edge is also parallel to the direction vector).
Here's how you can update the colinearity test:
- Two edges (A: v1-v2, B: w1-w2) are colinear if:
1. Their direction vectors are parallel (angle close to 0 or 180).
2. The vector from v1 to w1 is also parallel to the direction vector.
Below is the improved code for the colinearity check, replacing the angle checks in both BFS and the "all edges" loop:
# ...existing code... threshold_rad = self.angle_threshold * 3.14159265 / 180.0 def edge_dir(edge): v1, v2 = edge.verts return (v2.co - v1.co).normalized() def are_colinear(e1, e2): # Check if direction vectors are parallel dir1 = edge_dir(e1) dir2 = edge_dir(e2) angle = dir1.angle(dir2) if not (angle < threshold_rad or abs(angle - 3.14159265) < threshold_rad): return False # Check if the vector between their start points is also parallel to the direction v1 = e1.verts[0].co w1 = e2.verts[0].co between = (w1 - v1) # If between is zero vector, they share a vertex, so colinear if between.length < 1e-6: return True between_dir = between.normalized() angle2 = dir1.angle(between_dir) return angle2 < threshold_rad or abs(angle2 - 3.14159265) < threshold_rad if self.only_colinear_paths: visited = set() queue = [] for e in original_selected_edges: queue.append(e) visited.add(e) while queue: current_edge = queue.pop(0) current_edge.select = True for v in current_edge.verts: for neighbor in v.link_edges: if neighbor is current_edge or neighbor in visited: continue if are_colinear(current_edge, neighbor): queue.append(neighbor) visited.add(neighbor) else: for e in bm.edges: for sel_edge in original_selected_edges: if are_colinear(sel_edge, e): e.select = True break # ...existing code...Summary of changes: - Added a function `are_colinear(e1, e2)` that checks both parallelism and alignment. - Used this function in both BFS and the "all edges" selection. This will ensure only truly colinear edges are selected.
Conclusion
Even though it has its limitations, working with Vscode CoPilot works quite well, provided you have some experience in writing add-ons already and make sure to check its output. But if that checks out, it can really save you quite some time.
Code can be found in my Blenderaddons Repository.
No comments:
Post a Comment