ambi-recurve-limb-formed.py raw

   1  """
   2  Formed limb strip — after press brake operations on the flat blank.
   3  
   4  Three forming stages:
   5    1. Belly curve: gentle reflex along the full working limb
   6    2. Siyah recurve: 70 deg forward bend over last ~120mm at each tip
   7    3. String retention: conical taper with rolled edges.
   8       The tip narrows to a cone in plan view.
   9       The belly-side edges roll upward and inward toward the centerline,
  10       forming retention lips that cradle the string loop.
  11       String drops in from above, self-centers on the cone taper
  12       under tension.
  13  
  14  Center section (inside riser, +/-140mm) stays flat.
  15  
  16  Run: blender --background --python ambi-recurve-limb-formed.py
  17  """
  18  
  19  import bpy
  20  import bmesh
  21  import math
  22  
  23  
  24  def clear_scene():
  25      bpy.ops.object.select_all(action='SELECT')
  26      bpy.ops.object.delete()
  27  
  28  
  29  def make_material(name, color, metallic=0.0, roughness=0.5):
  30      mat = bpy.data.materials.new(name)
  31      mat.use_nodes = True
  32      bsdf = mat.node_tree.nodes["Principled BSDF"]
  33      bsdf.inputs["Base Color"].default_value = (*color, 1.0)
  34      bsdf.inputs["Metallic"].default_value = metallic
  35      bsdf.inputs["Roughness"].default_value = roughness
  36      return mat
  37  
  38  
  39  # ---------------------------------------------------------------
  40  # Taper — Gaussian bell curve
  41  # ---------------------------------------------------------------
  42  
  43  def limb_taper(d_mm):
  44      """(half_width_mm, thickness_mm) at distance d_mm from center."""
  45      d = abs(d_mm)
  46      w_peak, w_floor, w_sigma = 22.0, 10.0, 450.0
  47      t_peak, t_floor, t_sigma = 3.5, 1.4, 420.0
  48      hw = w_floor + (w_peak - w_floor) * math.exp(-d*d / (2*w_sigma*w_sigma))
  49      th = t_floor + (t_peak - t_floor) * math.exp(-d*d / (2*t_sigma*t_sigma))
  50      return (hw, th)
  51  
  52  
  53  # ---------------------------------------------------------------
  54  # Path (belly + siyah)
  55  # ---------------------------------------------------------------
  56  
  57  BELLY_START  = 140.0
  58  BELLY_END    = 360.0
  59  BELLY_RADIUS = 800.0
  60  SIYAH_START  = BELLY_END
  61  SIYAH_END    = 480.0
  62  SIYAH_ANGLE  = math.radians(70)
  63  SIYAH_ARC    = SIYAH_END - SIYAH_START
  64  SIYAH_RADIUS = SIYAH_ARC / SIYAH_ANGLE
  65  
  66  
  67  def limb_path(d_mm):
  68      """(y_mm, z_mm, angle_rad) along the neutral axis."""
  69      if d_mm <= BELLY_START:
  70          return (d_mm, 0.0, 0.0)
  71  
  72      belly_arc = BELLY_END - BELLY_START
  73      belly_theta_end = belly_arc / BELLY_RADIUS
  74      y_belly_end = BELLY_START + BELLY_RADIUS * math.sin(belly_theta_end)
  75      z_belly_end = BELLY_RADIUS * (1 - math.cos(belly_theta_end))
  76  
  77      if d_mm <= BELLY_END:
  78          a = (d_mm - BELLY_START) / BELLY_RADIUS
  79          return (BELLY_START + BELLY_RADIUS * math.sin(a),
  80                  BELLY_RADIUS * (1 - math.cos(a)), a)
  81  
  82      base_a = belly_theta_end
  83      if d_mm <= SIYAH_END:
  84          a = (d_mm - SIYAH_START) / SIYAH_RADIUS
  85          total_a = base_a + a
  86          dy = SIYAH_RADIUS * (math.sin(total_a) - math.sin(base_a))
  87          dz = SIYAH_RADIUS * (math.cos(base_a) - math.cos(total_a))
  88          return (y_belly_end + dy, z_belly_end + dz, total_a)
  89  
  90      return limb_path(SIYAH_END)
  91  
  92  
  93  def tip_state():
  94      return limb_path(SIYAH_END)
  95  
  96  
  97  # ---------------------------------------------------------------
  98  # Build main strip body (one arm)
  99  # ---------------------------------------------------------------
 100  
 101  def build_strip_body(bm, sign, steps=100):
 102      """Build one arm. Returns list of (v0,v1,v2,v3) rings."""
 103      dd = SIYAH_END / steps
 104      rings = []
 105      for i in range(steps + 1):
 106          d = i * dd
 107          hw_mm, th_mm = limb_taper(d)
 108          y_mm, z_mm, angle = limb_path(d)
 109  
 110          y = sign * y_mm / 1000.0
 111          z = z_mm / 1000.0
 112          hw = hw_mm / 1000.0
 113          th = th_mm / 1000.0
 114  
 115          ny = -math.sin(angle) * sign
 116          nz = math.cos(angle)
 117  
 118          v0 = bm.verts.new((-hw, y,           z))
 119          v1 = bm.verts.new(( hw, y,           z))
 120          v2 = bm.verts.new(( hw, y + ny * th, z + nz * th))
 121          v3 = bm.verts.new((-hw, y + ny * th, z + nz * th))
 122          rings.append((v0, v1, v2, v3))
 123  
 124      for i in range(len(rings) - 1):
 125          for j in range(4):
 126              jn = (j + 1) % 4
 127              bm.faces.new([rings[i][j], rings[i][jn],
 128                            rings[i+1][jn], rings[i+1][j]])
 129      return rings
 130  
 131  
 132  # ---------------------------------------------------------------
 133  # Build cone tip with rolled edges for one arm
 134  # ---------------------------------------------------------------
 135  
 136  def build_cone_tip(bm, sign):
 137      """
 138      Conical taper with S-curve edge profile for string retention.
 139  
 140      Edge profile (looking end-on at one edge):
 141  
 142          head (small, tight)
 143           __
 144          /  \    <-- string sits HERE, under the head
 145         |    |
 146          \   |
 147           \  |   <-- belly (big, sweeping arc) — clearance zone
 148            \ |       wire only touches the head, not the limb
 149             \|
 150              |   <-- strip body
 151  
 152      The S has:
 153        - Big belly: large radius arc sweeping forward, creating
 154          clearance between wire and limb surface
 155        - Small head: tight reverse curve hooking back over the belly,
 156          forming the spring lip that holds the string
 157        - The string wire touches ONLY the head
 158        - The head acts as a leaf spring: flexes under load
 159  
 160      The area between belly and limb body is the narrow gap that
 161      keeps the wire clear. Cone taper self-centers the loop.
 162      """
 163      ty_mm, tz_mm, t_angle = tip_state()
 164      hw_mm, th_mm = limb_taper(SIYAH_END)
 165  
 166      cos_a = math.cos(t_angle)
 167      sin_a = math.sin(t_angle)
 168      tg_y = cos_a * sign
 169      tg_z = sin_a
 170      nm_y = -sin_a * sign
 171      nm_z = cos_a
 172      fw_y = sin_a * sign
 173      fw_z = -cos_a
 174  
 175      by = sign * ty_mm / 1000.0
 176      bz = tz_mm / 1000.0
 177  
 178      def world_pt(lat_mm, along_mm, up_mm, fwd_mm=0):
 179          x = lat_mm / 1000.0
 180          y = (by + (along_mm / 1000.0) * tg_y
 181                  + (up_mm / 1000.0) * nm_y
 182                  + (fwd_mm / 1000.0) * fw_y)
 183          z = (bz + (along_mm / 1000.0) * tg_z
 184                  + (up_mm / 1000.0) * nm_z
 185                  + (fwd_mm / 1000.0) * fw_z)
 186          return bm.verts.new((x, y, z))
 187  
 188      cone_length = 25.0
 189      steps_along = 24
 190      tip_hw = 2.0
 191  
 192      # S-curve parameters
 193      belly_r = 3.0     # big belly radius (mm) — the large sweeping arc
 194      head_r = 1.0      # small head radius (mm) — the tight hook-back
 195  
 196      # S-curve profile for one edge (in the fwd/up plane):
 197      #
 198      # Start at strip corner: (fwd=0, up=0)
 199      # Belly arc: large radius, sweeps forward and up
 200      #   center at (0, belly_r), sweeps from -90 deg to ~+90 deg
 201      #   -> endpoint at (belly_r, belly_r) roughly
 202      #   but we only go partway: about 160 deg of arc
 203      # Head arc: small radius, reverses back
 204      #   tangent-continuous with belly, hooks back over
 205      #   -> creates the lip overhang
 206  
 207      # Points per side of the S-curve
 208      belly_pts = 8    # points along the big belly arc
 209      head_pts = 5     # points along the small head arc
 210      pts_per_side = belly_pts + head_pts
 211      # Total ring: left S + bottom center + right S + 3 back spine
 212      pts_per_ring = pts_per_side * 2 + 1 + 3
 213  
 214      all_rings = []
 215  
 216      for i in range(steps_along + 1):
 217          frac = i / steps_along
 218          d = frac * cone_length
 219          frac_s = frac * frac * (3 - 2 * frac)
 220  
 221          local_hw = hw_mm + (tip_hw - hw_mm) * frac_s
 222  
 223          # How developed the S-curve is: 0 at base (flat), 1 at tip (full S)
 224          s_dev = frac_s
 225  
 226          # Belly arc: sweep angle increases from 0 to 160 deg
 227          belly_sweep = math.radians(160) * s_dev
 228          # Head arc: sweep angle increases from 0 to 140 deg
 229          head_sweep = math.radians(140) * s_dev
 230  
 231          ring_verts = []
 232  
 233          def s_curve_points(side):
 234              """
 235              Generate S-curve profile points for one edge.
 236              side: -1 for left, +1 for right.
 237              Returns list of (fwd_mm, up_mm) tuples.
 238              """
 239              pts = []
 240  
 241              # --- Belly arc ---
 242              # Center of belly arc: at the edge corner, offset inward by belly_r
 243              # Arc starts at (fwd=0, up=0) and sweeps forward and up
 244              # Starting angle: -90 deg (pointing down = at the corner)
 245              # Sweep: belly_sweep degrees counterclockwise
 246              belly_cx = 0.0  # fwd center
 247              belly_cz = belly_r  # up center (above the corner)
 248  
 249              for k in range(belly_pts):
 250                  t = k / (belly_pts - 1)  # 0 to 1
 251                  a = -math.pi / 2 + belly_sweep * t
 252                  fwd = belly_cx + belly_r * math.cos(a)
 253                  up = belly_cz + belly_r * math.sin(a)
 254                  pts.append((fwd, up))
 255  
 256              if belly_sweep > 0.01 and head_sweep > 0.01:
 257                  # --- Head arc ---
 258                  # Tangent-continuous with belly end.
 259                  # At the belly endpoint, the tangent direction is
 260                  # perpendicular to the radius at that point.
 261                  belly_end_angle = -math.pi / 2 + belly_sweep
 262                  belly_end_fwd = belly_cx + belly_r * math.cos(belly_end_angle)
 263                  belly_end_up = belly_cz + belly_r * math.sin(belly_end_angle)
 264  
 265                  # The head arc center is offset from the belly endpoint
 266                  # in the opposite direction of the belly radius
 267                  # (to create the S reversal)
 268                  # Belly radius direction at endpoint:
 269                  br_fwd = math.cos(belly_end_angle)
 270                  br_up = math.sin(belly_end_angle)
 271                  # Head center: step INWARD from belly end by head_r
 272                  # in the radius direction
 273                  head_cx = belly_end_fwd + head_r * br_fwd
 274                  head_cz = belly_end_up + head_r * br_up
 275  
 276                  # Head arc starts at the belly endpoint
 277                  # and sweeps in the OPPOSITE rotational direction
 278                  # Starting angle for head: belly_end_angle + pi
 279                  # (pointing back toward the belly endpoint)
 280                  head_start = belly_end_angle + math.pi
 281  
 282                  for k in range(head_pts):
 283                      t = k / (head_pts - 1)
 284                      a = head_start - head_sweep * t
 285                      fwd = head_cx + head_r * math.cos(a)
 286                      up = head_cz + head_r * math.sin(a)
 287                      pts.append((fwd, up))
 288  
 289              return pts
 290  
 291          def s_curve_points_with_lip(side, local_hw_val):
 292              """
 293              Generate S-curve + lip. The lip is a lateral flare on the
 294              INSIDE face of the head — the metal is stretched sideways
 295              (toward strip center) to create a smooth bearing shelf
 296              where the wire sits. Prevents the edge from cutting
 297              into the wire rope.
 298              """
 299              base_pts = s_curve_points(side)
 300              result = []
 301  
 302              n_belly = belly_pts
 303              for idx, (fwd, up) in enumerate(base_pts):
 304                  lat = side * local_hw_val  # default lateral position
 305  
 306                  # For the head section (after belly points),
 307                  # the inner face gets a lateral lip/shelf
 308                  if idx >= n_belly:
 309                      head_idx = idx - n_belly
 310                      head_t = head_idx / max(head_pts - 1, 1)
 311                      # Lip grows from 0 to ~0.8mm inward on the
 312                      # inner face of the head. Eased.
 313                      lip_t = head_t * head_t * (3 - 2 * head_t)
 314                      lip_inward = 0.8 * lip_t  # mm toward center
 315                      lat = side * (local_hw_val - lip_inward)
 316  
 317                  result.append((lat, fwd, up))
 318  
 319              return result
 320  
 321              return pts
 322  
 323          # --- Left S-curve with lip ---
 324          left_pts = s_curve_points_with_lip(-1, local_hw)
 325          for lat, fwd, up in left_pts:
 326              ring_verts.append(world_pt(lat, d, up, fwd))
 327  
 328          # --- Bottom center ---
 329          ring_verts.append(world_pt(0, d, 0, 0))
 330  
 331          # --- Right S-curve with lip (reversed for continuous ring) ---
 332          right_pts = s_curve_points_with_lip(+1, local_hw)
 333          for lat, fwd, up in reversed(right_pts):
 334              ring_verts.append(world_pt(lat, d, up, fwd))
 335  
 336          # --- Back spine ---
 337          ring_verts.append(world_pt(local_hw, d, th_mm, 0))
 338          ring_verts.append(world_pt(0, d, th_mm, 0))
 339          ring_verts.append(world_pt(-local_hw, d, th_mm, 0))
 340  
 341          all_rings.append(ring_verts)
 342  
 343      # Faces between consecutive rings
 344      for i in range(len(all_rings) - 1):
 345          n = len(all_rings[i])
 346          if len(all_rings[i+1]) != n:
 347              continue
 348          for j in range(n):
 349              jn = (j + 1) % n
 350              bm.faces.new([all_rings[i][j], all_rings[i][jn],
 351                            all_rings[i+1][jn], all_rings[i+1][j]])
 352  
 353      # Cap the tip
 354      tip_ring = all_rings[-1]
 355      center_v = world_pt(0, cone_length, th_mm / 2, 0)
 356      n = len(tip_ring)
 357      for j in range(n):
 358          jn = (j + 1) % n
 359          bm.faces.new([tip_ring[j], tip_ring[jn], center_v])
 360  
 361  
 362  # ---------------------------------------------------------------
 363  # Main
 364  # ---------------------------------------------------------------
 365  
 366  def main():
 367      clear_scene()
 368      steel = make_material("316L_QPQ", (0.08, 0.08, 0.08), metallic=0.8, roughness=0.4)
 369  
 370      bm = bmesh.new()
 371  
 372      upper = build_strip_body(bm, +1)
 373      lower = build_strip_body(bm, -1)
 374  
 375      # bridge center
 376      u, l = upper[0], lower[0]
 377      for j in range(4):
 378          jn = (j + 1) % 4
 379          bm.faces.new([u[j], u[jn], l[jn], l[j]])
 380  
 381      # cone tips with rolled edges
 382      build_cone_tip(bm, +1)
 383      build_cone_tip(bm, -1)
 384  
 385      mesh = bpy.data.meshes.new("Limb_Formed")
 386      bm.to_mesh(mesh)
 387      bm.free()
 388  
 389      obj = bpy.data.objects.new("Limb_Strip_Formed", mesh)
 390      bpy.context.scene.collection.objects.link(obj)
 391      obj.data.materials.append(steel)
 392  
 393      for f in obj.data.polygons:
 394          f.use_smooth = True
 395  
 396      mod = obj.modifiers.new("Subsurf", 'SUBSURF')
 397      mod.levels = 2
 398      mod.render_levels = 3
 399  
 400      # bolt holes
 401      for offset_mm in [-30, 0, 30]:
 402          bpy.ops.mesh.primitive_cylinder_add(
 403              radius=0.00325, depth=0.008,
 404              location=(0, offset_mm / 1000.0, 0.00175))
 405          hole = bpy.context.active_object
 406          hole.name = f"BoltHole_{offset_mm}"
 407          bmod = obj.modifiers.new(f"Hole_{offset_mm}", 'BOOLEAN')
 408          bmod.operation = 'DIFFERENCE'
 409          bmod.object = hole
 410          bpy.context.view_layer.objects.active = obj
 411          bpy.ops.object.modifier_apply(modifier=f"Hole_{offset_mm}")
 412          bpy.data.objects.remove(hole)
 413  
 414      bpy.ops.object.select_all(action='DESELECT')
 415      obj.select_set(True)
 416      bpy.context.view_layer.objects.active = obj
 417  
 418      print("Done: formed limb strip with S-curve tip retention.")
 419      print("  Taper: Gaussian bell curve")
 420      print("  1. Belly curve: R800mm, 140-360mm")
 421      print("  2. Siyah recurve: 70 deg, 360-480mm")
 422      print("  3. Cone tip with S-curve edges: 480-505mm")
 423      print("     - Plan view: narrows 20mm -> 4mm (cone)")
 424      print("     - Edge profile: S-curve (big belly + small head)")
 425      print("     - Belly (R3mm): sweeps forward, clearance zone")
 426      print("     - Head (R1mm): hooks back, spring lip holds string")
 427      print("     - Wire touches ONLY the head, clear of limb body")
 428  
 429  
 430  if __name__ == "__main__":
 431      main()
 432