Category: Coding

Blender Scripting Lesson of the Week: Beveling

We were playing around with bevels this week – it’s pretty straight forward, the API lets you set the parameters you set through the GUI in a bevel modifier.

import bpy

# Clear all existing objects
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)

# Set Units
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 0.001  # 1 BU = 1 mm

# Create rectangular cube
bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))
block = bpy.context.active_object
block.name = "Block"

# cube default size is 2x2x2, so set absolute dimensions
block.dimensions = (2.0, 20.0, 0.25)
bpy.context.view_layer.objects.active = block
block.select_set(True)

# Apply scale so booleans/bevel behave predictably
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

# Create cylinder cutter
hole_diameter = 1.0
hole_radius = hole_diameter / 2.0

# Make it longer than the block thickness so it fully cuts through
cutter_depth = 5.0

bpy.ops.mesh.primitive_cylinder_add(
    vertices=64,
    radius=hole_radius,
    depth=cutter_depth,
    location=(0.0, 0.0, 0.0),   # center of the block
    rotation=(0.0, 0.0, 0.0)
)
cutter = bpy.context.active_object
cutter.name = "HoleCutter"

bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

# Boolean: cut hole
bpy.context.view_layer.objects.active = block
bool_mod = block.modifiers.new(name="Hole", type='BOOLEAN')
bool_mod.operation = 'DIFFERENCE'
bool_mod.solver = 'EXACT'
bool_mod.object = cutter

# Apply boolean
bpy.ops.object.modifier_apply(modifier=bool_mod.name)

# Hide cutter in viewport + renders
cutter.hide_set(True)
cutter.hide_render = True

# Bevel the block
bevel_width = 0.08 
bevel_segments = 5

bevel_mod = block.modifiers.new(name="Bevel", type='BEVEL')
bevel_mod.width = bevel_width
bevel_mod.segments = bevel_segments
bevel_mod.limit_method = 'ANGLE'
bevel_mod.angle_limit = 0.523599  # 30 degrees in radians

# Apply bevel
bpy.ops.object.modifier_apply(modifier=bevel_mod.name)

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

# Clear all existing objects
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)

# -----------------------------
# 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)
# -----------------------------
bracket_thickness = inch(0.25)   # sheet thickness
bracket_width = inch(3)   # bracket width (across the post)

# Leg lengths (side profile)
bracket_top_length = inch(1)        # bracket segment 1 length
bracket_middle_length = inch(2)     # bracket segment 2 length
bracket_bottom_length = inch(4.5)   # bracket segment 3 length

# Bend included angles
bend1_angle_included = 105.0   # top flange
bend2_angle_included = 255.0   # web -> long leg

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

# -----------------------------
# Punch hole
# -----------------------------
do_punch = True

# T-post size references
tpost_horizontal_hole_height = inch(0.25)
tpost_horizontal_hole_width = inch(1.5)
tpost_vertical_hole_height = inch(2)
tpost_vertical_hole_width = inch(0.25)
punch_clearance = 1.0 # clearance added around each rectangle (mm)

# Position of t-post before rotation (Z from p0 end, and X across width)
punch_center_z = inch(1)
punch_center_x = bracket_width / 2

# Vertical placement on top flange (Y=0 plane)
punch_center_y = -inch(0.5)

# -----------------------------
# Optional bevel to make edges25ook more formed
# -----------------------------
do_bevel = True

bevel_width = inch(0.05)
bevel_segments = 25

# -----------------------------
# Cleanup
# -----------------------------
#for n in ["BracketShape", "PunchBar", "PunchStem", "HoleRight1", "HoleRight2", "HoleRight3", "HoleRight4", "HoleLeft1", "HoleRLeft2", "HoleLeft3", "HoleLeft4"]:
#    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, rotation_xyz):
    bpy.ops.mesh.primitive_cube_add(size=1, location=location_xyz, rotation=rotation_xyz)
    obj = bpy.context.active_object
    obj.name = name
    obj.scale = (size_xyz[0], size_xyz[1], size_xyz[2])
    bpy.ops.object.transform_apply()
    return obj

def add_cylinder(name, radius, length, location_xyz, rotation_xyz):
    bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=length, location=location_xyz, rotation=rotation_xyz)
    obj = bpy.context.active_object
    obj.name = name
    bpy.ops.object.transform_apply()
    return obj

# Convert included bend angles to turn angles
angle_top = 180.0 - bend1_angle_included
angle_bottom = 180.0 - bend2_angle_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 - angle_top
d1 = unit_from_angle(theta1)

# After bend2, go toward +Z again (or flip if needed)
theta2 = theta1 + (angle_bottom if not flip_second_bend else - angle_bottom)
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 * bracket_top_length       # bend1 line
p2 = p1 + d1 * bracket_middle_length    # bend2 line
p3 = p2 + d2 * bracket_bottom_length    # 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, bracket_width

# 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 = bracket_thickness
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:
    tpost_length_y = bracket_thickness * 5  # ensure it fully cuts through

    # Crossbar rectangle
    horizontal_hole = add_cube(
        "PunchBar",
        size_xyz=(tpost_horizontal_hole_width + 2 * punch_clearance, tpost_length_y, tpost_horizontal_hole_height + 2 * punch_clearance),
        location_xyz=(punch_center_x, 13 + punch_center_y, punch_center_z),
        rotation_xyz=(math.radians(90 - bend1_angle_included / 2), math.radians(0), math.radians(0))
    )

    # Stem rectangle (placed under the bar like a lowercase "t")
    vertical_hole = add_cube(
        "PunchStem",
        size_xyz=(tpost_vertical_hole_width + 2 * punch_clearance, tpost_length_y, tpost_vertical_hole_height + 2 * punch_clearance),
        location_xyz=(punch_center_x, punch_center_y, punch_center_z),
        rotation_xyz=(math.radians(90), math.radians(0), math.radians(0))
        #rotation_xyz=(math.radians(90 - bend1_angle_included / 2), math.radians(0), math.radians(0))
    )
    boolean_diff(bracket, vertical_hole)
    boolean_diff(bracket, horizontal_hole)


for hole in range(4):
    right_hole = add_cylinder(
        "HoleRight{}".format(hole),
        radius=inch(0.125),
        length=100,
        location_xyz=(inch(0.5), -inch(2), inch(2) + inch(1.175) * hole),
        rotation_xyz=(math.radians(90), 0, 0)
    )
    left_hole = add_cylinder(
        "HoleLeft{}".format(hole),
        radius=inch(0.125),
        length=100,
        location_xyz=(inch(2.5), -inch(2), inch(2) + inch(1.175) * hole),
        rotation_xyz=(math.radians(90), 0, 0)
    )
    boolean_diff(bracket, right_hole)
    boolean_diff(bracket, left_hole)

# -----------------------------
# 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)
    bev.use_clamp_overlap = False
    bpy.context.view_layer.objects.active = bracket
    bpy.ops.object.modifier_apply(modifier=bev.name)

API Documentation Links:

https://docs.blender.org/api/current/bpy.ops.mesh.html
https://docs.blender.org/api/current/bmesh.ops.html

Blender Scripting Lesson of the Week: Cylinders

Quick script for creating a cylinder using bpy

import bpy

# Clear all existing objects
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)

# Set Units
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 0.001  # 1 BU = 1 mm

# Create cylinder
bpy.ops.mesh.primitive_cylinder_add(
    vertices=32, radius=10.0, depth=20.0,
    end_fill_type='NGON', calc_uvs=True,
    enter_editmode=False, align='WORLD',
    location=(0.0, 0.0, -2.0), rotation=(0.0, 0.0, 0.0),
    scale=(1, 1, 1)
)

# Name cylinder
obj = bpy.context.active_object
obj.name = "MyCylinder"

# Frame Selected 
for area in bpy.context.window.screen.areas:
    if area.type == 'VIEW_3D':
        for region in area.regions:
            if region.type == 'WINDOW':
                with bpy.context.temp_override(area=area, region=region):
                    bpy.ops.view3d.view_selected(use_all_regions=False)
                break
        break

Python: Partition and RPartition

Found a neat pair of methods that were added in Python 2.5 — it’s like split/index except it handles breaking the string into two elements for you. A tuple is returned with the part before the separator, the separator, and the part after the separator. If the separator is not found, element 0 and 1 are empty strings.

 

C:\Users\lisa> python
Python 3.13.3
Type “help”, “copyright”, “credits” or “license” for more information.
>>> test = “This is a string | with pipe characters as | delimiters in the string”
>>> print(test.rpartition(“|”)[0])
This is a string | with pipe characters as
>>> print(test.partition(“|”)[0])
This is a string
>>>

Chocolate Chip Cookies with Dark Cherries and Almond Flour

Anya made me birthday cookies! I wanted to save the recipe because they turned out really well.

 
Ingredients:
  • 1 3/4 cups all-purpose flour
  • 1/2 cup almond flour
  • 1 teaspoon baking soda
  • 1/2 teaspoon salt
  • 1 cup unsalted butter, softened
  • 1 cup maple syrup
  • 1/2 cup plain Greek yogurt
  • 2 large eggs
  • 2 cups semi-sweet chocolate chips
  • 1 cup frozen dark cherries, roughly chopped
Instructions:
  1. Preheat your oven to 375°F. Line baking sheets with parchment paper.
  2. In a medium bowl, whisk together the all-purpose flour, almond flour, baking soda, and salt. Set aside.
  3. In a large mixing bowl, beat the softened butter with the maple syrup until well mixed. The mixture will be looser than a typical creamed butter-sugar mixture due to the syrup.
  4. Mix in the Greek yogurt until smooth. Beat in the eggs one at a time, mixing well after each addition.
  5. Gradually add the dry ingredients to the wet ingredients, mixing until just combined. The dough will be a bit softer due to the syrup and yogurt.
  6. Gently fold in the chocolate chips and frozen dark cherries until evenly distributed throughout the dough.
  7. Drop rounded tablespoons of dough onto the prepared baking sheets, leaving about 2 inches between each cookie to allow for spreading.
  8. Bake the cookies in the preheated oven for 10-12 minutes, or until the edges are golden brown and the centers are set but still soft. If you made really big cookies, this may be more like 20 minutes!
  9. Allow the cookies to cool on the baking sheet for about 5 minutes before transferring them to a wire rack to cool completely.

QR Code Generation

I put together a quick program that creates a “fancy” QR code to a specified URL with the specified color and drops the desired “logo” file into the center of the code.

import qrcode
from PIL import Image

def generate_qr_code_with_custom_color_and_logo():
    url = input("Please enter the URL for which you want to generate a QR code: ")

    rgb_input = input("Please enter the RGB values for the QR code color (e.g. 0,0,0 for black): ")
    
    try:
        rgb_color = tuple(map(int, rgb_input.split(',')))
        if len(rgb_color) != 3 or not all(0 <= n <= 255 for n in rgb_color):
            raise ValueError("Invalid RGB color value.")
    except Exception:
        print("Error parsing RGB values. Please make sure to enter three integers separated by commas.")
        return

    qr = qrcode.QRCode(
        version=1,  # controls the size of the QR Code
        error_correction=qrcode.constants.ERROR_CORRECT_H,  # high error correction for image insertion
        box_size=10,
        border=4,
    )
    qr.add_data(url)
    qr.make(fit=True)

    # Generate the QR code with the specified RGB color
    img = qr.make_image(fill_color=rgb_color, back_color="white")

    # Load the logo image
    logo_image_path = input("Please enter the logo for the center of this QR code: ")

    try:
        logo = Image.open(logo_image_path)
    except FileNotFoundError:
        print(f"Logo image file '{logo_image_path}' not found. Proceeding without a logo.")
        img.save("qr_code_with_custom_color.png")
        print("QR code has been generated and saved as 'qr_code_with_custom_color.png'.")
        return

    # Resize the logo image to fit in the QR code
    img_width, img_height = img.size
    logo_size = int(img_width * 0.2)  # The logo will take up 20% of the QR code width
    logo = logo.resize((logo_size, logo_size), Image.ANTIALIAS)

    position = ((img_width - logo_size) // 2, (img_height - logo_size) // 2)

    img.paste(logo, position, mask=logo.convert("RGBA"))

    img.save("qr_code_with_custom_color_and_logo.png")

    print("QR code with a custom color and a logo image has been generated and saved as 'qr_code_with_custom_color_and_logo.png'.")

if __name__ == "__main__":
    generate_qr_code_with_custom_color_and_logo()

Voila!

JavaScript: Extracting Web Content You Cannot Copy

There are many times I need to copy “stuff” from a website that is structured in such a way that simply copy/pasting the table data is impossible. Screen prints work, but I usually want the table of data in Excel so I can add notations and such. In these cases, running JavaScript from the browser’s developers console lets you access the underlying text elements.

Right click on one of the text elements and select “Inspect”

Now copy the element’s XPath

Read the value — we don’t generally want just this one element … but the path down to the “tbody” tag looks like a reasonable place to find the values within the table.

/html/body/div[1]/div/div/div[2]/div[2]/div[2]/div/div[3]/div/div/div[3]/div/div/div/table/tbody/a[4]/td[2]/div/span[2]

Use JavaScript to grab all of the TD elements under the tbody:

// Define the XPath expression to select all <td> elements within the specific <tbody>
const xpathExpression = "/html/body/div[1]/div/div/div[2]/div[2]/div[2]/div/div[3]/div/div/div[3]/div/div/div/table/tbody//td";

// Use document.evaluate to get all matching <td> nodes
const nodesSnapshot = document.evaluate(xpathExpression, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

// Log the number of nodes found (for debugging purposes)
console.log("Total <td> elements found:", nodesSnapshot.snapshotLength);

// Iterate over the nodes and log their text content
for (let i = 0; i < nodesSnapshot.snapshotLength; i++) {
    let node = nodesSnapshot.snapshotItem(i);
    if (node) {
        const textContent = node.textContent.trim();
        if (textContent) { // Only log non-empty content
            console.log(textContent);
        }
    }
}

Voila! I redacted some data below, but it’s just a list of values, one per line.

Python: Getting Active Directory Subnets

Like my script that pulls the AD site information – this lets me see what subnets are defined and which sites are assigned to those subnets. I was able to quickly confirm that the devices that had problems communicating with Active Directory don’t have a site defined. Way back in 2000, we created a “catch all” 10.0.0.0/8 subnet and assigned it to the user authentication site. New networks on a whole different addressing scheme don’t have a site assignment. It should still work, but the application in question has historically had issues with going the “Ok, list ’em all” route.

from ldap3 import Server, Connection, ALL, SUBTREE, Tls
import ssl
import getpass

# Attempt to import USERNAME and PASSWORD from config.py
try:
    from config import USERNAME, PASSWORD
except ImportError:
    USERNAME, PASSWORD = None, None

# Define constants
LDAP_SERVER = 'ad.example.com'
LDAP_PORT = 636

def get_subnets_and_sites(username, password):
    # Set up TLS configuration
    tls_configuration = Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1_2)

    # Connect to the LDAP server
    server = Server(LDAP_SERVER, port=LDAP_PORT, use_ssl=True, tls=tls_configuration, get_info=ALL)
    connection = Connection(server, user=username, password=password, authentication='SIMPLE', auto_bind=True)

    # Define the search base for subnets
    search_base = 'CN=Subnets,CN=Sites,CN=Configuration,DC=example,DC=com'  # Change this to match your domain's DN
    search_filter = '(objectClass=subnet)'  # Filter to find all subnet objects
    search_attributes = ['cn', 'siteObject']  # Retrieve the common name and site object references

    # Perform the search
    connection.search(search_base, search_filter, SUBTREE, attributes=search_attributes)

    # Extract and return subnets and their site assignments
    subnets_sites = []
    for entry in connection.entries:
        subnet_name = entry.cn.value
        site_dn = entry.siteObject.value if entry.siteObject else "No site assigned"
        subnets_sites.append((subnet_name, site_dn))

    return subnets_sites

def print_subnets_and_sites(subnets_sites):
    if subnets_sites:
        print("\nSubnets and their Site Assignments:")
        for subnet, site in subnets_sites:
            print(f"Subnet: {subnet}, Site: {site}")
    else:
        print("No subnets found in the domain.")

def main():
    # Prompt for username and password if not available in config.py
    username = USERNAME if USERNAME else input("Enter your LDAP username: ")
    password = PASSWORD if PASSWORD else getpass.getpass("Enter your LDAP password: ")

    subnets_sites = get_subnets_and_sites(username, password)
    print_subnets_and_sites(subnets_sites)

if __name__ == "__main__":
    main()