Hadlock

mandolin-neck-fender.py

Jun 9th, 2026
146
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.72 KB | Music | 0 0
  1. """
  2. Parametric Fender-style bolt-on mandolin neck in CadQuery.
  3.  
  4. Units: millimeters.
  5. Default design intent:
  6. - 13.875 in / 352.425 mm mandolin scale
  7. - 22 frets
  8. - ~10.25 in / 260.35 mm nut-to-pocket-end neck length
  9. - Fender-style flat bolt-on heel
  10. - Radiused fretboard, default 12 in / 304.8 mm radius
  11. - Simple non-angled solid headstock with 8 tuner holes
  12.  
  13. Run:
  14.    python mandolin_fender_style_neck.py
  15.  
  16. Exports:
  17.    mandolin_bolt_on_neck.step
  18.    mandolin_bolt_on_neck.stl
  19.    mandolin_neck_pocket_cutter.step
  20.  
  21. CadQuery install example:
  22.    conda install -c conda-forge cadquery
  23. """
  24.  
  25. from __future__ import annotations
  26.  
  27. import math
  28. from dataclasses import dataclass
  29.  
  30. import cadquery as cq
  31. from cadquery import Vector, Edge, Wire, Solid
  32.  
  33. MM_PER_INCH = 25.4
  34.  
  35.  
  36. @dataclass(frozen=True)
  37. class Params:
  38.     # Core mandolin layout
  39.     scale: float = 13.875 * MM_PER_INCH          # 352.425 mm
  40.     frets: int = 22
  41.     nut_to_heel_end: float = 10.25 * MM_PER_INCH # 260.35 mm
  42.  
  43.     # Fretboard
  44.     fretboard_radius: float = 12.0 * MM_PER_INCH # 12 in radius
  45.     fretboard_thickness_center: float = 5.5
  46.     fretboard_overhang_after_last_fret: float = 6.0
  47.     nut_width: float = 29.0
  48.     fretboard_end_width: float = 38.0
  49.     fret_slot_width: float = 0.55
  50.     fret_slot_depth: float = 1.25
  51.  
  52.     # Neck shaft and heel
  53.     neck_top_z: float = 16.0                    # underside to board bottom at nut/shaft
  54.     nut_neck_width: float = 26.0
  55.     shaft_end_width: float = 35.0
  56.     heel_width: float = 39.0                    # pocket should be slightly bigger than this
  57.     heel_length: float = 55.0
  58.     heel_thickness: float = 19.0                # flat heel thickness before fretboard
  59.     heel_pilot_dia: float = 2.4
  60.     heel_pilot_depth: float = 14.0
  61.  
  62.     # Headstock, deliberately simple so you can reshape later
  63.     headstock_length: float = 88.0
  64.     headstock_width: float = 48.0
  65.     headstock_thickness: float = 12.0
  66.     tuner_hole_dia: float = 6.2
  67.  
  68.     # Nut
  69.     nut_thickness_x: float = 3.0
  70.     nut_height: float = 6.5
  71.     nut_depth_y: float = 31.0
  72.  
  73.     # Export quality
  74.     stl_tolerance: float = 0.05
  75.     stl_angular_tolerance: float = 0.15
  76.  
  77.  
  78. P = Params()
  79.  
  80.  
  81. def fret_position(scale: float, fret_no: int) -> float:
  82.     """Distance from nut to fret, equal temperament."""
  83.     return scale - (scale / (2 ** (fret_no / 12.0)))
  84.  
  85.  
  86. def lerp(a: float, b: float, t: float) -> float:
  87.     return a + (b - a) * t
  88.  
  89.  
  90. def width_at_x(x: float, x0: float, x1: float, w0: float, w1: float) -> float:
  91.     t = max(0.0, min(1.0, (x - x0) / (x1 - x0)))
  92.     return lerp(w0, w1, t)
  93.  
  94.  
  95. def fretboard_section_wire(x: float, width: float, thickness_center: float, radius: float) -> Wire:
  96.     """Closed Y/Z wire at length position X. Flat bottom, radiused top."""
  97.     half = width / 2.0
  98.     sag = radius - math.sqrt(radius * radius - half * half)
  99.     edge_z = thickness_center - sag
  100.  
  101.     p1 = Vector(x, -half, 0.0)
  102.     p2 = Vector(x, half, 0.0)
  103.     p3 = Vector(x, half, edge_z)
  104.     p4 = Vector(x, -half, edge_z)
  105.     crown = Vector(x, 0.0, thickness_center)
  106.  
  107.     edges = [
  108.         Edge.makeLine(p1, p2),
  109.         Edge.makeLine(p2, p3),
  110.         Edge.makeThreePointArc(p3, crown, p4),
  111.         Edge.makeLine(p4, p1),
  112.     ]
  113.     return Wire.assembleEdges(edges)
  114.  
  115.  
  116. def neck_back_section_wire(x: float, width: float, top_z: float, center_back_z: float = 0.0) -> Wire:
  117.     """Closed Y/Z wire for a simple C-ish neck back with flat top."""
  118.     half = width / 2.0
  119.     shoulder_z = top_z * 0.35
  120.     shoulder_y = half * 0.92
  121.  
  122.     p1 = Vector(x, -half, top_z)
  123.     p2 = Vector(x, half, top_z)
  124.     p3 = Vector(x, shoulder_y, shoulder_z)
  125.     p4 = Vector(x, -shoulder_y, shoulder_z)
  126.     back = Vector(x, 0.0, center_back_z)
  127.  
  128.     edges = [
  129.         Edge.makeLine(p1, p2),
  130.         Edge.makeLine(p2, p3),
  131.         Edge.makeThreePointArc(p3, back, p4),
  132.         Edge.makeLine(p4, p1),
  133.     ]
  134.     return Wire.assembleEdges(edges)
  135.  
  136.  
  137. def make_fretboard(p: Params) -> cq.Workplane:
  138.     last_fret_x = fret_position(p.scale, p.frets)
  139.     fb_len = min(p.nut_to_heel_end, last_fret_x + p.fretboard_overhang_after_last_fret)
  140.  
  141.     sections = []
  142.     for x in [0, fb_len * 0.25, fb_len * 0.5, fb_len * 0.75, fb_len]:
  143.         width = width_at_x(x, 0, fb_len, p.nut_width, p.fretboard_end_width)
  144.         sections.append(fretboard_section_wire(x, width, p.fretboard_thickness_center, p.fretboard_radius))
  145.  
  146.     solid = Solid.makeLoft(sections)
  147.     board = cq.Workplane("XY").add(solid).translate((0, 0, p.neck_top_z))
  148.  
  149.     # Cut fret slots down through the radius. These are shallow visual/locator grooves,
  150.     # not prescriptive luthier fret tang dimensions.
  151.     for n in range(1, p.frets + 1):
  152.         x = fret_position(p.scale, n)
  153.         if x > fb_len:
  154.             continue
  155.         w = width_at_x(x, 0, fb_len, p.nut_width, p.fretboard_end_width) + 4.0
  156.         cutter = (
  157.             cq.Workplane("XY")
  158.             .box(p.fret_slot_width, w, p.fret_slot_depth, centered=(True, True, True))
  159.             .translate((x, 0, p.neck_top_z + p.fretboard_thickness_center - p.fret_slot_depth / 2.0 + 0.2))
  160.         )
  161.         board = board.cut(cutter)
  162.  
  163.     # Dot markers as small shallow inlay discs. These intersect the curved top slightly.
  164.     marker_frets = [5, 7, 10, 12, 15, 17, 19]
  165.     for n in marker_frets:
  166.         if n >= p.frets:
  167.             continue
  168.         x1 = fret_position(p.scale, n)
  169.         x2 = fret_position(p.scale, n + 1)
  170.         xm = (x1 + x2) / 2.0
  171.         dots_y = [-5.0, 5.0] if n == 12 else [0.0]
  172.         for y in dots_y:
  173.             dot = (
  174.                 cq.Workplane("XY")
  175.                 .center(xm, y)
  176.                 .circle(1.6)
  177.                 .extrude(0.7)
  178.                 .translate((0, 0, p.neck_top_z + p.fretboard_thickness_center - 0.25))
  179.             )
  180.             board = board.union(dot)
  181.  
  182.     return board
  183.  
  184.  
  185. def make_neck_core(p: Params) -> cq.Workplane:
  186.     shaft_end_x = p.nut_to_heel_end - p.heel_length
  187.  
  188.     sections = []
  189.     for x in [0, shaft_end_x * 0.33, shaft_end_x * 0.66, shaft_end_x]:
  190.         width = width_at_x(x, 0, shaft_end_x, p.nut_neck_width, p.shaft_end_width)
  191.         sections.append(neck_back_section_wire(x, width, p.neck_top_z))
  192.  
  193.     shaft = cq.Workplane("XY").add(Solid.makeLoft(sections))
  194.  
  195.     heel = (
  196.         cq.Workplane("XY")
  197.         .box(p.heel_length, p.heel_width, p.heel_thickness, centered=(False, True, False))
  198.         .translate((shaft_end_x, 0, 0))
  199.     )
  200.  
  201.     neck = shaft.union(heel)
  202.  
  203.     # Fender-like 4 pilot holes in the heel bottom. These are pilot holes in the neck,
  204.     # not body clearance holes.
  205.     screw_xs = [shaft_end_x + p.heel_length * 0.34, shaft_end_x + p.heel_length * 0.74]
  206.     screw_ys = [-p.heel_width * 0.27, p.heel_width * 0.27]
  207.     for x in screw_xs:
  208.         for y in screw_ys:
  209.             pilot = (
  210.                 cq.Workplane("XY")
  211.                 .center(x, y)
  212.                 .circle(p.heel_pilot_dia / 2.0)
  213.                 .extrude(p.heel_pilot_depth)
  214.             )
  215.             neck = neck.cut(pilot)
  216.  
  217.     return neck
  218.  
  219.  
  220. def make_headstock(p: Params) -> cq.Workplane:
  221.     L = p.headstock_length
  222.     W = p.headstock_width
  223.     t = p.headstock_thickness
  224.  
  225.     # Simple Fender-ish paddle outline, non-angled. Nut is at x=0, headstock extends negative.
  226.     outline = [
  227.         (-L, -W * 0.30),
  228.         (-L * 0.82, -W * 0.48),
  229.         (-L * 0.25, -W * 0.44),
  230.         (0, -15.0),
  231.         (0, 15.0),
  232.         (-L * 0.18, W * 0.32),
  233.         (-L * 0.78, W * 0.45),
  234.         (-L, W * 0.30),
  235.     ]
  236.     hs = cq.Workplane("XY").polyline(outline).close().extrude(t)
  237.  
  238.     # Blend transition into nut end with a small rectangular tongue.
  239.     tongue = cq.Workplane("XY").box(10.0, 28.0, t, centered=(False, True, False)).translate((-5.0, 0, 0))
  240.     hs = hs.union(tongue)
  241.  
  242.     # Eight inline tuner holes. Change to 4+4 if you prefer a Gibson/mandolin look.
  243.     start_x = -L + 14.0
  244.     spacing = 8.8
  245.     y = W * 0.20
  246.     for i in range(8):
  247.         x = start_x + i * spacing
  248.         hole = cq.Workplane("XY").center(x, y).circle(p.tuner_hole_dia / 2.0).extrude(t + 1.0)
  249.         hs = hs.cut(hole)
  250.  
  251.     return hs
  252.  
  253.  
  254. def make_nut(p: Params) -> cq.Workplane:
  255.     return (
  256.         cq.Workplane("XY")
  257.         .box(p.nut_thickness_x, p.nut_depth_y, p.nut_height, centered=(False, True, False))
  258.         .translate((-p.nut_thickness_x, 0, p.neck_top_z))
  259.     )
  260.  
  261.  
  262. def make_pocket_cutter(p: Params) -> cq.Workplane:
  263.     """Body-side helper: neck pocket cutter/block, with a small fit clearance."""
  264.     clearance = 0.25
  265.     depth = p.heel_thickness
  266.     shaft_end_x = p.nut_to_heel_end - p.heel_length
  267.     return (
  268.         cq.Workplane("XY")
  269.         .box(p.heel_length + 0.5, p.heel_width + clearance * 2.0, depth, centered=(False, True, False))
  270.         .translate((shaft_end_x - 0.25, 0, 0))
  271.     )
  272.  
  273.  
  274. def build_model(p: Params = P) -> cq.Workplane:
  275.     neck = make_neck_core(p)
  276.     fretboard = make_fretboard(p)
  277.     headstock = make_headstock(p)
  278.     nut = make_nut(p)
  279.     return neck.union(fretboard).union(headstock).union(nut)
  280.  
  281.  
  282. if __name__ == "__main__":
  283.     model = build_model(P)
  284.     pocket = make_pocket_cutter(P)
  285.  
  286.     cq.exporters.export(model, "mandolin_bolt_on_neck.step")
  287.     cq.exporters.export(model, "mandolin_bolt_on_neck.stl", tolerance=P.stl_tolerance, angularTolerance=P.stl_angular_tolerance)
  288.     cq.exporters.export(pocket, "mandolin_neck_pocket_cutter.step")
  289.  
  290.     print("Exported mandolin_bolt_on_neck.step")
  291.     print("Exported mandolin_bolt_on_neck.stl")
  292.     print("Exported mandolin_neck_pocket_cutter.step")
  293.     print(f"Scale: {P.scale:.3f} mm")
  294.     print(f"Nut to heel/pocket end: {P.nut_to_heel_end:.3f} mm")
  295.     print(f"Pocket end to nominal bridge: {P.scale - P.nut_to_heel_end:.3f} mm")
  296.     print(f"Fretboard radius: {P.fretboard_radius:.3f} mm")
  297.  
Tags: python Cad
Add Comment
Please, Sign In to add comment