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