Tag: bmesh

Blender Scripting – T-Post Sign Holder

We spent a lot of the day trying to modify 3D models that we found online to work as a sign holder. Something like the bent metal plates you can buy at the tractor store. Since these are simple polygons, I thought it might be easier to script the build (plus making changes to the dimensions would just require tweaking variables).

Voila – hopefully it’s a T-post sign holder! It at least looks like one.

import bpy
import bmesh
import math
from mathutils import Vector

# -----------------------------
# Scene units (mm)
# -----------------------------
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 0.001  # 1 Blender unit = 1 mm

INCH = 25.4
def inch(x): return x * INCH

# -----------------------------
# PARAMETERS (mm)
# -----------------------------
thk = 10.0            # sheet thickness
width_x = 50.0       # bracket width (across the post)

# Leg lengths (side profile)
L_top = 15.0         # top flange length
L_web = 48.0         # drop/web height between bends
L_leg = 50.0        # long leg length (down the post)

# Bend included angles
bend1_included = 200.0   # top flange
bend2_included = 90.0    # web -> long leg

# If the long leg goes the wrong direction, flip this
flip_second_bend = False

# -----------------------------
# Punch hole on TOP FLANGE
# -----------------------------
do_punch = True

# T-post references
t_bar_w  = inch(1.375)     # crossbar width
t_bar_h  = 12.0            # crossbar height (mm)  <-- tune
t_stem_w = 12.0            # stem width (mm)       <-- tune
t_stem_h = inch(1.625)     # stem height

punch_clear = 1.0          # clearance added around each rectangle (mm)

# Position on top flange (Z from p0 end, and X across width)
punch_center_z = L_top * 0.55
punch_center_x = width_x * 0.50

# Vertical placement on top flange (Y=0 plane)
punch_center_y = 0.0

# -----------------------------
# Optional bevel to make edges look more formed
# -----------------------------
do_bevel = True
bevel_width = 0.6
bevel_segments = 2

# -----------------------------
# Cleanup
# -----------------------------
for n in ["BracketShape", "PunchBar", "PunchStem"]:
    o = bpy.data.objects.get(n)
    if o:
        bpy.data.objects.remove(o, do_unlink=True)

# -----------------------------
# Helpers (YZ plane directions)
# Define 0° as +Z. +90° is +Y. -90° is -Y.
# -----------------------------
def unit_from_angle(deg_from_posZ):
    a = math.radians(deg_from_posZ)
    return Vector((0.0, math.sin(a), math.cos(a)))

def boolean_diff(target, cutter):
    mod = target.modifiers.new(name=f"BOOL_{cutter.name}", type="BOOLEAN")
    mod.operation = 'DIFFERENCE'
    mod.solver = 'EXACT'
    mod.object = cutter
    bpy.context.view_layer.objects.active = target
    bpy.ops.object.modifier_apply(modifier=mod.name)
    cutter.hide_set(True)

def add_cube(name, size_xyz, location_xyz):
    bpy.ops.mesh.primitive_cube_add(size=1, location=location_xyz)
    obj = bpy.context.active_object
    obj.name = name
    obj.scale = (size_xyz[0]/2, size_xyz[1]/2, size_xyz[2]/2)
    bpy.ops.object.transform_apply(scale=True)
    return obj

# Convert included bend angles to turn angles
turn1 = 180.0 - bend1_included
turn2 = 180.0 - bend2_included

# Start along +Z (top flange)
theta0 = 0.0
d0 = unit_from_angle(theta0)

# After bend1, go "down" (toward -Y) by turning negative
theta1 = theta0 - turn1
d1 = unit_from_angle(theta1)

# After bend2, go toward +Z again (or flip if needed)
theta2 = theta1 + (turn2 if not flip_second_bend else -turn2)
d2 = unit_from_angle(theta2)

# Profile points (center surface)
p0 = Vector((0.0, 0.0, 0.0))      # free end of top flange
p1 = p0 + d0 * L_top              # bend1 line
p2 = p1 + d1 * L_web              # bend2 line
p3 = p2 + d2 * L_leg              # end of long leg

# -----------------------------
# Build a single connected sheet surface:
# Create two polylines separated in X, then make quads between them.
# -----------------------------
mesh = bpy.data.meshes.new("BracketShapeMesh")
bracket = bpy.data.objects.new("BracketShape", mesh)
bpy.context.collection.objects.link(bracket)
bpy.context.view_layer.objects.active = bracket
bracket.select_set(True)

bm = bmesh.new()

x0, x1 = 0.0, width_x

# Left side (x0)
v0a = bm.verts.new((x0, p0.y, p0.z))
v1a = bm.verts.new((x0, p1.y, p1.z))
v2a = bm.verts.new((x0, p2.y, p2.z))
v3a = bm.verts.new((x0, p3.y, p3.z))

# Right side (x1)
v0b = bm.verts.new((x1, p0.y, p0.z))
v1b = bm.verts.new((x1, p1.y, p1.z))
v2b = bm.verts.new((x1, p2.y, p2.z))
v3b = bm.verts.new((x1, p3.y, p3.z))

# Faces (one per segment)
bm.faces.new((v0a, v0b, v1b, v1a))  # top flange
bm.faces.new((v1a, v1b, v2b, v2a))  # web
bm.faces.new((v2a, v2b, v3b, v3a))  # long leg

bm.normal_update()
bm.to_mesh(mesh)
bm.free()

# -----------------------------
# Solidify to thickness (sheet metal look)
# -----------------------------
solid = bracket.modifiers.new("Solidify", type="SOLIDIFY")
solid.thickness = thk
solid.offset = 0.0
bpy.ops.object.modifier_apply(modifier=solid.name)

# -----------------------------
# Punch the lowercase "t" on the top flange
# (Top flange is flat at Y=0; punch straight through Y)
# -----------------------------
if do_punch:
    cutter_depth_y = thk * 6.0  # ensure it fully cuts through

    # Crossbar rectangle
    bar = add_cube(
        "PunchBar",
        size_xyz=(t_bar_w + 2*punch_clear, cutter_depth_y, t_bar_h + 2*punch_clear),
        location_xyz=(punch_center_x, punch_center_y, punch_center_z)
    )

    # Stem rectangle (placed under the bar like a lowercase "t")
    stem_center_z = punch_center_z - (t_bar_h*0.35) - (t_stem_h*0.5)
    stem = add_cube(
        "PunchStem",
        size_xyz=(t_stem_w + 2*punch_clear, cutter_depth_y, t_stem_h + 2*punch_clear),
        location_xyz=(punch_center_x, punch_center_y, stem_center_z)
    )

    #boolean_diff(bracket, bar)
    #boolean_diff(bracket, stem)

# -----------------------------
# Optional bevel
# -----------------------------
if do_bevel:
    bev = bracket.modifiers.new("Bevel", type="BEVEL")
    bev.width = bevel_width
    bev.segments = bevel_segments
    bev.limit_method = 'ANGLE'
    bev.angle_limit = math.radians(35)
    bpy.context.view_layer.objects.active = bracket
    bpy.ops.object.modifier_apply(modifier=bev.name)