Just a short public service announcement: I tested the basket_arch.py add-on on Blender 4.4 and it just works 😀
Basket arch add-on tested with Blender 4.4
Edgemap select add-on
I find it occasionally useful to have the option to manage collections of selected edges, so I basically copied the functionality of the Facemap select add-on and changed it to work on edges instead of faces.
The add-on can be downloaded from Github. Simply click on the "download raw file" button (near the top right) and then install the file as an add-on in the usual manner. The panel with the edge map functionality can then be found in the Mesh properties (the operators themselves are only active in edit mode).
Weightlifter add-on for Blender is now free
Weightlifter |
In a previous article I mentioned that I stopped publishing add-ons on blender market because basically I was done with it it: I didn't earn enough to warrant the amount of effort that goes into maintaining and supporting add-ons.
However, there is still quite some interest in some of the add-ons, and so I decided to open source some of the add-ons and make them available for free. The first one was Snap! but now it is Weightlifters turn, my very first add-on published on BlenderMarket (now Superhive), back in 2014 (!)
Weightlifter is the Swiss army knife of recording mesh attributes to vertex weights or -colors. Anything from whether a vertex is visible from the camera to its slope or curvature can be converted to weight maps or vertex colors, and much more.
Technically Weightlifter already was open source, at least the source code was, but now I'm also open sourcing the manual, and that means that you can download both the source and the manual from this GitHub repository.
This repository is still a work in progress and the manual hasn't been updated for Blender 4.x yet, but the code is and will run on 4.4. For now, simple download weightlifter.py and use the install from filesystem in the add-ons section. Then read the manual!
To give you some idea of its capabilities, there is a video playlist on YouTube (but they show old versions of Blender).
Select colinear edges - vibe coding a Blender add-on
![]() |
| Warning! Long read! (≈ 10 min if you just skim the code blocks) |
The goal
The first attempt
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: 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
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
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!
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 also have a bl_info dict however, otherwise Blender will complain.
Don't forget bl_info!
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!
# ...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.There is still an issue here: the colinearity test cannot deal with zero length edges which do occur in practice, but I'll fix that myself. The code in the GitHub repo does guard against it.
Conclusion
Snap! add-on for Blender is now free
In a previous article I mentioned that I stopped publishing add-ons on blender market because basically I was done with it it: I didn't earn enough to warrant the amount of effort that goes into maintaining and supporting add-ons.
However, there is still quite some interest in some of the add-ons, especially Snap!, the add-on that makes it really easy to work with modular assets, and so I decided to open source this add-on and make it available for free.
Technically it already was open source, at least the source code was, but now I'm also open sourcing the manual, and that means that you can download both the source and the manual from this GitHub repository.
A .zip file that can be installed as-is is also provided in the releases section. Just make sure to check the readme (and the documentation) before you start using it.
MicroTiles - uniform subdivision of selected faces
Introduction
I often use Blender to model railway scenery in H0 scale for 3D printing. While Blender is not a CAD program, it proves to be a very useful tool in cases where artistic considerations are just as important as exact tolerances.
Problem with Tunnel Portals
Recently, I was working on some tunnel portals and wanted to create a rough surface on certain faces. Using fuzzing settings in a slicer did not yield the results I was looking for, so I decided to use a displacement modifier with Musgrave noise to achieve this effect.
Vertex Groups and Subdivision Modifier
By defining a vertex group, the displacement modifier can be restricted to just the faces that need the rough texture. However, there must be enough geometry to see any details.MicroTile Add-on

Availability
Notes
Minor tweak to Facemap Select
In a previous article I wrote about some new functionality in the Facemap Select add-on.
I just added a minor tweak to it: now all Facemap actions, like select and unselect can be undone, just like any other selection in Blender.
The updated version can be downloaded from my github repository.









