""" Ambidextrous Recurve Bow — Exploded Parts View Blender Python script. Run from Blender's Scripting workspace (or Text Editor > Run Script). Creates all individual parts as separate objects in the scene. """ import bpy import bmesh import math from mathutils import Vector, Matrix # ============================================================ # UTILITIES # ============================================================ def clear_scene(): bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() for c in bpy.data.collections: if c.name != 'Scene Collection': bpy.data.collections.remove(c) def make_material(name, color, metallic=0.0, roughness=0.5): mat = bpy.data.materials.new(name) mat.use_nodes = True bsdf = mat.node_tree.nodes["Principled BSDF"] bsdf.inputs["Base Color"].default_value = (*color, 1.0) bsdf.inputs["Metallic"].default_value = metallic bsdf.inputs["Roughness"].default_value = roughness return mat def assign_material(obj, mat): obj.data.materials.append(mat) def new_collection(name): col = bpy.data.collections.new(name) bpy.context.scene.collection.children.link(col) return col def link_to_collection(obj, col): col.objects.link(obj) if obj.name in bpy.context.scene.collection.objects: bpy.context.scene.collection.objects.unlink(obj) def set_smooth(obj): for f in obj.data.polygons: f.use_smooth = True # ============================================================ # MATERIALS # ============================================================ WOOD = None STEEL = None BLACK = None # QPQ nitride WIRE = None RUBBER = None def setup_materials(): global WOOD, STEEL, BLACK, WIRE, RUBBER WOOD = make_material("Mulberry", (0.55, 0.35, 0.18), metallic=0.0, roughness=0.7) STEEL = make_material("Stainless", (0.7, 0.72, 0.74), metallic=0.9, roughness=0.3) BLACK = make_material("QPQ_Nitride", (0.08, 0.08, 0.08), metallic=0.8, roughness=0.4) WIRE = make_material("Wire_Rope", (0.6, 0.62, 0.64), metallic=0.9, roughness=0.35) RUBBER = make_material("Rubber", (0.1, 0.1, 0.1), metallic=0.0, roughness=0.9) # ============================================================ # 1. RISER — Mulberry wood, C2 symmetric # ============================================================ def make_riser(col): """ Riser as a lofted shape: define cross-sections at intervals along Y, skin them together. 280mm long, C2 point symmetry. """ # We'll use a curve + bevel for the main body, then boolean the groove. # Simpler approach: mesh with loop cuts for editing. # Create riser as a tapered box with smooth profile bm = bmesh.new() length = 0.280 # 280mm in meters (Blender default unit) sections = 28 dy = length / sections for i in range(sections + 1): y = -length / 2 + i * dy t = abs(y) / (length / 2) # 0 at center, 1 at ends # Width: 76mm at center, 31mm at ends — smooth bell curve half_w = (0.038 - (0.038 - 0.0155) * t**1.5) # Depth: 28mm at center, ~20mm at ends depth = 0.028 - 0.008 * t**1.5 # S-curve grip cross-section: offset the front/back faces # to create the tessellating S-grip feel grip_offset = 0.003 * math.sin(math.pi * (y / (length / 2))) if abs(y) < 0.05 else 0 # Four corners of cross-section verts = [ bm.verts.new((-half_w, y, 0)), bm.verts.new(( half_w, y, 0)), bm.verts.new(( half_w, y, depth)), bm.verts.new((-half_w, y, depth)), ] # Create faces between consecutive sections bm.verts.ensure_lookup_table() for i in range(sections): base = i * 4 for j in range(4): v0 = bm.verts[base + j] v1 = bm.verts[base + (j + 1) % 4] v2 = bm.verts[base + 4 + (j + 1) % 4] v3 = bm.verts[base + 4 + j] bm.faces.new([v0, v1, v2, v3]) # Cap ends bm.faces.new([bm.verts[0], bm.verts[1], bm.verts[2], bm.verts[3]]) last = sections * 4 bm.faces.new([bm.verts[last+3], bm.verts[last+2], bm.verts[last+1], bm.verts[last]]) mesh = bpy.data.meshes.new("Riser") bm.to_mesh(mesh) bm.free() obj = bpy.data.objects.new("Riser", mesh) link_to_collection(obj, col) assign_material(obj, WOOD) set_smooth(obj) # Add subdivision surface for smoothness mod = obj.modifiers.new("Subsurf", 'SUBSURF') mod.levels = 2 mod.render_levels = 3 return obj # ============================================================ # 2. LIMB STRIP — 316L SS, tapered, recurved # ============================================================ def limb_strip_profile(dist_from_center): """Return (half_width, thickness) at given distance from center in meters.""" d = abs(dist_from_center) * 1000 # to mm # Taper points from doc: # 0mm: 44mm × 3.5mm # 140mm: 40mm × 3.0mm # 250mm: 32mm × 2.2mm # 350mm: 24mm × 1.8mm # 500mm: 20mm × 1.4mm (tip) points = [ (0, 44, 3.5), (140, 40, 3.0), (250, 32, 2.2), (350, 24, 1.8), (500, 20, 1.4), ] # Linear interpolation for j in range(len(points) - 1): d0, w0, t0 = points[j] d1, w1, t1 = points[j + 1] if d0 <= d <= d1: frac = (d - d0) / (d1 - d0) w = w0 + frac * (w1 - w0) t = t0 + frac * (t1 - t0) return (w / 2 / 1000, t / 1000) # Beyond last point return (points[-1][1] / 2 / 1000, points[-1][2] / 1000) def make_limb_strip(col): """ Single continuous limb strip with smooth taper and recurved tips. Uses a curve with bevel for smooth result. """ total_half = 0.500 # 500mm from center to tip segments = 60 bm = bmesh.new() for i in range(segments + 1): # Distance from center dist = i * total_half / (segments / 2) - total_half abs_dist = abs(dist) hw, th = limb_strip_profile(abs_dist) # Recurve: last 100mm of each limb arm bends forward recurve_onset = 0.340 # 340mm from center x_off = 0 if abs_dist > recurve_onset: arc_param = (abs_dist - recurve_onset) / (total_half - recurve_onset) angle = arc_param * math.radians(70) x_off = 0.030 * (1 - math.cos(angle)) # ~30mm bend radius effect y = dist # Adjust y for recurve (arc shortens the projected length) if abs_dist > recurve_onset: arc_param = (abs_dist - recurve_onset) / (total_half - recurve_onset) angle = arc_param * math.radians(70) y_reduction = 0.030 * math.sin(angle) - 0.030 * arc_param * math.sin(math.radians(70)) # Keep it simple — just offset x, keep y linear sign = 1 if dist >= 0 else -1 x_off_signed = x_off * (1 if True else -1) # recurve bends same direction z_base = 0.028 # sits at top of riser groove area verts = [ bm.verts.new((-hw + x_off_signed, y, z_base)), bm.verts.new(( hw + x_off_signed, y, z_base)), bm.verts.new(( hw + x_off_signed, y, z_base + th)), bm.verts.new((-hw + x_off_signed, y, z_base + th)), ] bm.verts.ensure_lookup_table() for i in range(segments): base = i * 4 for j in range(4): v0 = bm.verts[base + j] v1 = bm.verts[base + (j + 1) % 4] v2 = bm.verts[base + 4 + (j + 1) % 4] v3 = bm.verts[base + 4 + j] bm.faces.new([v0, v1, v2, v3]) # Cap ends bm.faces.new([bm.verts[0], bm.verts[1], bm.verts[2], bm.verts[3]]) last = segments * 4 bm.faces.new([bm.verts[last+3], bm.verts[last+2], bm.verts[last+1], bm.verts[last]]) mesh = bpy.data.meshes.new("Limb_Strip") bm.to_mesh(mesh) bm.free() obj = bpy.data.objects.new("Limb_Strip", mesh) link_to_collection(obj, col) assign_material(obj, BLACK) set_smooth(obj) mod = obj.modifiers.new("Subsurf", 'SUBSURF') mod.levels = 2 return obj # ============================================================ # 3. M6 COUNTERSUNK BOLT # ============================================================ def make_bolt(col, name="M6_Bolt"): bpy.ops.mesh.primitive_cone_add( vertices=6, radius1=0.006, radius2=0.003, depth=0.0033, location=(0, 0, 0)) head = bpy.context.active_object head.name = name + "_head" bpy.ops.mesh.primitive_cylinder_add( radius=0.003, depth=0.020, location=(0, 0, -0.0033/2 - 0.010)) shaft = bpy.context.active_object shaft.name = name + "_shaft" # Join head.select_set(True) shaft.select_set(True) bpy.context.view_layer.objects.active = head bpy.ops.object.join() obj = bpy.context.active_object obj.name = name assign_material(obj, STEEL) link_to_collection(obj, col) return obj # ============================================================ # 4. HELI-COIL INSERT # ============================================================ def make_helicoil(col, name="Helicoil"): bpy.ops.mesh.primitive_cylinder_add(radius=0.005, depth=0.010) obj = bpy.context.active_object obj.name = name assign_material(obj, STEEL) # Boolean subtract inner bore bpy.ops.mesh.primitive_cylinder_add(radius=0.0025, depth=0.012) bore = bpy.context.active_object bore.name = name + "_bore" mod = obj.modifiers.new("Bool", 'BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = bore bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier="Bool") bpy.data.objects.remove(bore) link_to_collection(obj, col) return obj # ============================================================ # 5. BELLEVILLE WASHER STACK — Recoil dampener # ============================================================ def make_belleville_stack(col, name="Belleville_Stack"): objs = [] for i in range(4): bpy.ops.mesh.primitive_cone_add( vertices=32, radius1=0.007, radius2=0.006, depth=0.0012, location=(0, 0, i * 0.001)) w = bpy.context.active_object w.name = f"{name}_washer{i}" if i % 2 == 1: w.rotation_euler[0] = math.pi # flip alternate assign_material(w, STEEL) objs.append(w) # Join all bpy.ops.object.select_all(action='DESELECT') for o in objs: o.select_set(True) bpy.context.view_layer.objects.active = objs[0] bpy.ops.object.join() obj = bpy.context.active_object obj.name = name link_to_collection(obj, col) return obj # ============================================================ # 6. BOWSTRING — 1.5mm SS wire rope # ============================================================ def make_bowstring(col): # Bezier curve for the string curve_data = bpy.data.curves.new("Bowstring_Curve", 'CURVE') curve_data.dimensions = '3D' curve_data.bevel_depth = 0.00075 # 1.5mm diameter / 2 curve_data.bevel_resolution = 4 spline = curve_data.splines.new('BEZIER') spline.bezier_points.add(1) # 2 points total # String endpoints — approximate positions at limb tips p0 = spline.bezier_points[0] p0.co = Vector((0.019, -0.400, 0.030)) p0.handle_left = p0.co + Vector((0, 0.05, 0)) p0.handle_right = p0.co + Vector((0, -0.05, 0)) p1 = spline.bezier_points[1] p1.co = Vector((0.019, 0.400, 0.030)) p1.handle_left = p1.co + Vector((0, -0.05, 0)) p1.handle_right = p1.co + Vector((0, 0.05, 0)) obj = bpy.data.objects.new("Bowstring", curve_data) link_to_collection(obj, col) assign_material(obj, WIRE) return obj # ============================================================ # 7. STRING DAMPER # ============================================================ def make_string_damper(col, name="String_Damper"): bpy.ops.mesh.primitive_torus_add( major_radius=0.005, minor_radius=0.002, major_segments=24, minor_segments=12) obj = bpy.context.active_object obj.name = name assign_material(obj, RUBBER) set_smooth(obj) link_to_collection(obj, col) return obj # ============================================================ # 8. NOCKING POINT # ============================================================ def make_nocking_point(col): bpy.ops.mesh.primitive_cylinder_add(radius=0.002, depth=0.003) obj = bpy.context.active_object obj.name = "Nocking_Point" assign_material(obj, STEEL) link_to_collection(obj, col) return obj # ============================================================ # 9. ARROW SHAFT # ============================================================ def make_arrow_shaft(col): bpy.ops.mesh.primitive_cylinder_add( radius=0.00425, depth=0.700) # 8.5mm dia, 700mm long obj = bpy.context.active_object obj.name = "Arrow_Shaft" obj.rotation_euler[0] = math.pi / 2 # align along Y assign_material(obj, WOOD) set_smooth(obj) link_to_collection(obj, col) return obj # ============================================================ # 10. ARROW POINT — Machined from M12 bolt # ============================================================ def make_arrow_point(col): bm = bmesh.new() # Build as a lathe profile (revolution solid) # Profile: socket bore -> body cylinder -> tip cone # We'll just use primitives joined together mesh = bpy.data.meshes.new("Arrow_Point") bm.free() # Socket cylinder bpy.ops.mesh.primitive_cylinder_add( radius=0.0055, depth=0.025, location=(0, 0, 0)) socket = bpy.context.active_object # Body cylinder bpy.ops.mesh.primitive_cylinder_add( radius=0.0055, depth=0.035, location=(0, 0, 0.030)) body = bpy.context.active_object # Tip cone bpy.ops.mesh.primitive_cone_add( radius1=0.0055, radius2=0.0005, depth=0.030, location=(0, 0, 0.0625)) tip = bpy.context.active_object # Join all bpy.ops.object.select_all(action='DESELECT') socket.select_set(True) body.select_set(True) tip.select_set(True) bpy.context.view_layer.objects.active = socket bpy.ops.object.join() obj = bpy.context.active_object obj.name = "Arrow_Point" assign_material(obj, BLACK) set_smooth(obj) link_to_collection(obj, col) return obj # ============================================================ # 11. CAM-FLIGHT NOCK # ============================================================ def make_cam_nock(col): # Cylinder base bpy.ops.mesh.primitive_cylinder_add( radius=0.006, depth=0.014, location=(0, 0, 0)) base = bpy.context.active_object base.name = "Cam_Nock_base" # Wing 1 bpy.ops.mesh.primitive_cube_add(size=1, location=(0.009, 0, -0.010)) w1 = bpy.context.active_object w1.scale = (0.010, 0.00045, 0.0125) w1.name = "Cam_Nock_wing1" bpy.ops.object.transform_apply(scale=True) # Wing 2 (180 degrees opposite) bpy.ops.mesh.primitive_cube_add(size=1, location=(-0.009, 0, -0.010)) w2 = bpy.context.active_object w2.scale = (0.010, 0.00045, 0.0125) w2.name = "Cam_Nock_wing2" bpy.ops.object.transform_apply(scale=True) # Join bpy.ops.object.select_all(action='DESELECT') base.select_set(True) w1.select_set(True) w2.select_set(True) bpy.context.view_layer.objects.active = base bpy.ops.object.join() obj = bpy.context.active_object obj.name = "Cam_Flight_Nock" assign_material(obj, BLACK) link_to_collection(obj, col) return obj # ============================================================ # MAIN — Build all parts in exploded layout # ============================================================ def main(): clear_scene() setup_materials() # Collections for organization bow_col = new_collection("Bow") hw_col = new_collection("Hardware") str_col = new_collection("String") arrow_col = new_collection("Arrow") E = 0.08 # explode spacing in meters # --- BOW --- riser = make_riser(bow_col) limb = make_limb_strip(bow_col) limb.location.z -= E # offset below riser for exploded view # --- HARDWARE (3 sets) --- bolt_spacing = 0.030 for i, offset in enumerate([-bolt_spacing, 0, bolt_spacing]): bolt = make_bolt(hw_col, f"Bolt_{i}") bolt.location = Vector((-E, offset, 0.028 + 0.01)) bolt.rotation_euler[1] = -math.pi / 2 insert = make_helicoil(hw_col, f"Helicoil_{i}") insert.location = Vector((E, offset, 0.014)) insert.rotation_euler[1] = math.pi / 2 bstack = make_belleville_stack(hw_col, f"Belleville_{i}") bstack.location = Vector((-E * 0.5, offset, 0.028 + 0.005)) # --- STRING --- string = make_bowstring(str_col) string.location.x += E * 2 d1 = make_string_damper(str_col, "Damper_Upper") d1.location = Vector((E * 2 + 0.019, 0.160, 0.030)) d2 = make_string_damper(str_col, "Damper_Lower") d2.location = Vector((E * 2 + 0.019, -0.160, 0.030)) nock_pt = make_nocking_point(str_col) nock_pt.location = Vector((E * 2 + 0.019, 0, 0.030)) # --- ARROW (1 representative) --- shaft = make_arrow_shaft(arrow_col) shaft.location = Vector((0, -E * 5, 0.06)) point = make_arrow_point(arrow_col) point.location = Vector((0, -E * 5 - 0.380, 0.06)) point.rotation_euler[0] = math.pi / 2 nock = make_cam_nock(arrow_col) nock.location = Vector((0, -E * 5 + 0.360, 0.06)) nock.rotation_euler[0] = -math.pi / 2 # Deselect all bpy.ops.object.select_all(action='DESELECT') print("Ambi-recurve bow: all parts created.") print("Parts are organized in collections: Bow, Hardware, String, Arrow") print("Select parts individually to inspect or edit meshes.") if __name__ == "__main__": main()