Hadlock

Mandalin-body.py

May 31st, 2026
71
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.48 KB | Music | 0 0
  1. """
  2. Parametric Fender-style solid mandolin body for the matching bolt-on neck.
  3.  
  4. Units: millimeters.
  5. Coordinate system matches mandolin_fender_style_neck.py:
  6. - Nut is at X = 0
  7. - Neck centerline is Y = 0
  8. - Nominal high-course bridge/saddle line is at X = scale
  9. - Neck heel/pocket end is at X = nut_to_heel_end
  10.  
  11. Default design intent:
  12. - 13.875 in / 352.425 mm scale
  13. - Body top at Z = 0, body extends downward to Z = -body_thickness
  14. - Recessed Fender-style neck pocket
  15. - 4 neck screw clearance holes through the body, aligned to the neck pilot holes
  16. - 2 bridge mounting holes near the scale line
  17. - 2 tailpiece mounting holes behind the bridge
  18.  
  19. Run:
  20.    python mandolin_fender_style_body.py
  21.  
  22. Exports:
  23.    mandolin_fender_style_body.step
  24.    mandolin_fender_style_body.stl
  25.    mandolin_body_and_neck_reference.step  # if mandolin_fender_style_neck.py is importable
  26.  
  27. CadQuery install example:
  28.    conda install -c conda-forge cadquery
  29. """
  30.  
  31. from __future__ import annotations
  32.  
  33. from dataclasses import dataclass
  34. import cadquery as cq
  35.  
  36. MM_PER_INCH = 25.4
  37.  
  38.  
  39. @dataclass(frozen=True)
  40. class BodyParams:
  41.     # Match the neck script defaults
  42.     scale: float = 13.875 * MM_PER_INCH           # 352.425 mm
  43.     nut_to_heel_end: float = 10.25 * MM_PER_INCH  # 260.35 mm
  44.     heel_length: float = 55.0
  45.     heel_width: float = 39.0
  46.     heel_pocket_clearance: float = 0.35
  47.  
  48.     # Body proportions: roughly mandolin-sized solidbody, not a full ES/archtop mandolin
  49.     body_thickness: float = 34.0
  50.     pocket_depth: float = 16.0
  51.     body_start_extra: float = 18.0       # body extends this far forward of pocket start
  52.     body_length: float = 285.0           # from body_start_x to strap-end/tail end
  53.     upper_bout_width: float = 175.0
  54.     waist_width: float = 150.0
  55.     lower_bout_width: float = 255.0
  56.  
  57.     # Mounting holes
  58.     neck_screw_clearance_dia: float = 4.2
  59.     neck_screw_counterbore_dia: float = 9.0
  60.     neck_screw_counterbore_depth: float = 4.0
  61.  
  62.     # Bridge / tailpiece mounting holes. These are just sensible pilots/clearance holes;
  63.     # adjust to match your actual bridge and tailpiece hardware.
  64.     bridge_hole_dia: float = 3.2
  65.     bridge_post_spacing: float = 42.0
  66.     bridge_x_offset_from_scale: float = 2.0       # slightly behind nominal scale line
  67.  
  68.     tailpiece_hole_dia: float = 4.0
  69.     tailpiece_hole_spacing: float = 38.0
  70.     tailpiece_x_offset_from_scale: float = 76.0
  71.  
  72.     # Optional wire/string-through reference holes, disabled by default
  73.     stl_tolerance: float = 0.08
  74.     stl_angular_tolerance: float = 0.2
  75.  
  76.  
  77. P = BodyParams()
  78.  
  79.  
  80. def make_body_blank(p: BodyParams) -> cq.Workplane:
  81.     """Make a small offset-waist Fender-ish mandolin solidbody."""
  82.     pocket_start_x = p.nut_to_heel_end - p.heel_length
  83.     x0 = pocket_start_x - p.body_start_extra
  84.     x1 = x0 + p.body_length
  85.  
  86.     # Key longitudinal stations
  87.     xn = x0 + 8.0                         # neck-side upper shoulder
  88.     xu = x0 + 60.0                        # upper bout
  89.     xw = x0 + 132.0                       # waist
  90.     xl = x0 + 215.0                       # lower bout
  91.     xt = x1 - 6.0                         # rounded tail
  92.  
  93.     # Outline points, clockwise, using splines for a smooth guitar/mandolin body.
  94.     # Widths are total widths, so y uses half width.
  95.     pts = [
  96.         (xn, -p.heel_width * 0.78),
  97.         (xu, -p.upper_bout_width * 0.50),
  98.         (xw, -p.waist_width * 0.50),
  99.         (xl, -p.lower_bout_width * 0.50),
  100.         (xt, -p.lower_bout_width * 0.34),
  101.         (x1, 0.0),
  102.         (xt, p.lower_bout_width * 0.34),
  103.         (xl, p.lower_bout_width * 0.50),
  104.         (xw, p.waist_width * 0.50),
  105.         (xu, p.upper_bout_width * 0.50),
  106.         (xn, p.heel_width * 0.78),
  107.     ]
  108.  
  109.     body = (
  110.         cq.Workplane("XY")
  111.         .moveTo(*pts[0])
  112.         .spline(pts[1:])
  113.         .close()
  114.         .extrude(-p.body_thickness)
  115.     )
  116.  
  117.     # Soften the front/back perimeter a bit. If your CadQuery install complains about
  118.     # the selector, comment these two lines out; the flat functional features remain.
  119.     body = body.edges("|Z").fillet(4.0)
  120.     body = body.edges("<Z or >Z").fillet(1.5)
  121.     return body
  122.  
  123.  
  124. def make_neck_pocket_cutter(p: BodyParams) -> cq.Workplane:
  125.     pocket_start_x = p.nut_to_heel_end - p.heel_length
  126.     width = p.heel_width + 2.0 * p.heel_pocket_clearance
  127.     length = p.heel_length + 0.8
  128.  
  129.     # Cuts from body top Z=0 downward to pocket floor at -pocket_depth.
  130.     return (
  131.         cq.Workplane("XY")
  132.         .box(length, width, p.pocket_depth + 0.2, centered=(False, True, False))
  133.         .translate((pocket_start_x - 0.4, 0, -p.pocket_depth - 0.1))
  134.     )
  135.  
  136.  
  137. def cut_neck_mounting_holes(body: cq.Workplane, p: BodyParams) -> cq.Workplane:
  138.     pocket_start_x = p.nut_to_heel_end - p.heel_length
  139.     screw_xs = [pocket_start_x + p.heel_length * 0.34, pocket_start_x + p.heel_length * 0.74]
  140.     screw_ys = [-p.heel_width * 0.27, p.heel_width * 0.27]
  141.  
  142.     for x in screw_xs:
  143.         for y in screw_ys:
  144.             through = (
  145.                 cq.Workplane("XY")
  146.                 .center(x, y)
  147.                 .circle(p.neck_screw_clearance_dia / 2.0)
  148.                 .extrude(-p.body_thickness - 1.0)
  149.                 .translate((0, 0, 0.5))
  150.             )
  151.             body = body.cut(through)
  152.  
  153.             # Counterbore from the back face. Because the body extends negative Z,
  154.             # this is placed at the bottom/back side.
  155.             counterbore = (
  156.                 cq.Workplane("XY")
  157.                 .center(x, y)
  158.                 .circle(p.neck_screw_counterbore_dia / 2.0)
  159.                 .extrude(p.neck_screw_counterbore_depth)
  160.                 .translate((0, 0, -p.body_thickness - 0.05))
  161.             )
  162.             body = body.cut(counterbore)
  163.     return body
  164.  
  165.  
  166. def cut_bridge_and_tailpiece_holes(body: cq.Workplane, p: BodyParams) -> cq.Workplane:
  167.     bridge_x = p.scale + p.bridge_x_offset_from_scale
  168.     tailpiece_x = p.scale + p.tailpiece_x_offset_from_scale
  169.  
  170.     for y in [-p.bridge_post_spacing / 2.0, p.bridge_post_spacing / 2.0]:
  171.         hole = (
  172.             cq.Workplane("XY")
  173.             .center(bridge_x, y)
  174.             .circle(p.bridge_hole_dia / 2.0)
  175.             .extrude(-p.body_thickness - 1.0)
  176.             .translate((0, 0, 0.5))
  177.         )
  178.         body = body.cut(hole)
  179.  
  180.     for y in [-p.tailpiece_hole_spacing / 2.0, p.tailpiece_hole_spacing / 2.0]:
  181.         hole = (
  182.             cq.Workplane("XY")
  183.             .center(tailpiece_x, y)
  184.             .circle(p.tailpiece_hole_dia / 2.0)
  185.             .extrude(-p.body_thickness - 1.0)
  186.             .translate((0, 0, 0.5))
  187.         )
  188.         body = body.cut(hole)
  189.  
  190.     return body
  191.  
  192.  
  193. def add_reference_marks(body: cq.Workplane, p: BodyParams) -> cq.Workplane:
  194.     """Add shallow engraved centerline and scale-line marks for layout sanity checks."""
  195.     pocket_start_x = p.nut_to_heel_end - p.heel_length
  196.     x0 = pocket_start_x - p.body_start_extra + 10.0
  197.     x1 = x0 + p.body_length - 20.0
  198.  
  199.     centerline = (
  200.         cq.Workplane("XY")
  201.         .box(x1 - x0, 0.7, 0.6, centered=(False, True, False))
  202.         .translate((x0, 0, -0.35))
  203.     )
  204.     scale_mark = (
  205.         cq.Workplane("XY")
  206.         .box(1.0, 70.0, 0.6, centered=(True, True, False))
  207.         .translate((p.scale, 0, -0.35))
  208.     )
  209.     pocket_end_mark = (
  210.         cq.Workplane("XY")
  211.         .box(1.0, 50.0, 0.6, centered=(True, True, False))
  212.         .translate((p.nut_to_heel_end, 0, -0.35))
  213.     )
  214.     return body.cut(centerline).cut(scale_mark).cut(pocket_end_mark)
  215.  
  216.  
  217. def build_body(p: BodyParams = P, reference_marks: bool = True) -> cq.Workplane:
  218.     body = make_body_blank(p)
  219.     body = body.cut(make_neck_pocket_cutter(p))
  220.     body = cut_neck_mounting_holes(body, p)
  221.     body = cut_bridge_and_tailpiece_holes(body, p)
  222.     if reference_marks:
  223.         body = add_reference_marks(body, p)
  224.     return body
  225.  
  226.  
  227. def try_build_body_and_neck_reference(body: cq.Workplane, p: BodyParams) -> cq.Workplane | None:
  228.     """Optional assembly reference if the neck script is next to this file."""
  229.     try:
  230.         import mandolin_fender_style_neck as neck_script
  231.     except Exception:
  232.         return None
  233.  
  234.     neck = neck_script.build_model(neck_script.P)
  235.  
  236.     # Drop the heel bottom onto the pocket floor. The neck script's heel bottom is at Z=0.
  237.     neck = neck.translate((0, 0, -p.pocket_depth))
  238.     return body.union(neck)
  239.  
  240.  
  241. if __name__ == "__main__":
  242.     body = build_body(P)
  243.     cq.exporters.export(body, "mandolin_fender_style_body.step")
  244.     cq.exporters.export(body, "mandolin_fender_style_body.stl", tolerance=P.stl_tolerance, angularTolerance=P.stl_angular_tolerance)
  245.  
  246.     ref = try_build_body_and_neck_reference(body, P)
  247.     if ref is not None:
  248.         cq.exporters.export(ref, "mandolin_body_and_neck_reference.step")
  249.         print("Exported mandolin_body_and_neck_reference.step")
  250.  
  251.     bridge_x = P.scale + P.bridge_x_offset_from_scale
  252.     tailpiece_x = P.scale + P.tailpiece_x_offset_from_scale
  253.     print("Exported mandolin_fender_style_body.step")
  254.     print("Exported mandolin_fender_style_body.stl")
  255.     print(f"Body top: Z = 0; body back: Z = {-P.body_thickness:.3f} mm")
  256.     print(f"Neck pocket floor: Z = {-P.pocket_depth:.3f} mm")
  257.     print(f"Pocket start X: {P.nut_to_heel_end - P.heel_length:.3f} mm")
  258.     print(f"Pocket end X: {P.nut_to_heel_end:.3f} mm")
  259.     print(f"Nominal scale line X: {P.scale:.3f} mm")
  260.     print(f"Bridge holes X: {bridge_x:.3f} mm, Y: +/-{P.bridge_post_spacing / 2.0:.3f} mm")
  261.     print(f"Tailpiece holes X: {tailpiece_x:.3f} mm, Y: +/-{P.tailpiece_hole_spacing / 2.0:.3f} mm")
Tags: Mandalin
Advertisement
Add Comment
Please, Sign In to add comment