gears/bevel/bevel_gear.py → OBJECT_OT_bevel_gear (object.bevel_gear, "Bevel Gear")
Straight-tooth bevel gear — a toothed frustum for shafts meeting at 90°, built via Tredgold's approximation (each axial slice is the standard flat involute profile, uniformly scaled toward the cone apex — not a true spherical involute). See README.md for family-wide conventions.
Bevel gears in this library have no hand property (straight teeth only, no spiral-bevel support) and no bore feature — the two biggest differences from every other single-gear primitive.
Properties
| Property | Type | Default | Range | Notes |
|---|---|---|---|---|
target | Object pointer | — | gears with bmech_module | Match Target; runs sync_bevel — copies module/PA, and sets mate_teeth to the target's own tooth_count |
tooth_count | Int | 16 | 8–120 (soft) | This gear's own tooth count |
mate_teeth | Int | 16 | 8–120 (soft) | Tooth count of the mating gear — sets this gear's cone angle |
module | Float (mm) | 2.0 | 0.1–20.0 (soft) | Module at the large end (back face) |
pressure_angle_deg | Float (°) | 20.0 | 10–45 | |
face_width_mm | Float (mm) | 8.0 | 1–60 (soft) | Tooth length along the cone slant — keep ≤ cone distance / 3 |
n_slices | Int | 12 | 2–64 (soft) | Axial divisions — more = smoother taper |
Note the minimum tooth count here is 8, not the 5 used by spur/helical/herringbone — keeps the cone-angle math and small-end tooth size sane at the apex.
[BUG, fixed] mate_teeth/module/pressure_angle_deg weren't frozen in draw() when a target was set — sync_bevel itself always updated them correctly on pick, but the panel never grayed them out the way every other gear generator does (see spur_gear.md's own module_row.enabled = not has_target), so a target pick looked like it silently did nothing, and the driven values could be immediately overwritten by hand with no visual cue that they were supposed to be locked. tooth_count stays outside the frozen column deliberately — it's this gear's own choice, not something sync_bevel touches (the target's tooth_count becomes this gear's mate_teeth, not its own tooth_count).
Making a mating pair
Two bevel gears mesh at 90° with coincident apices when their cone angles are complementary: delta_1 + delta_2 = 90°. This library derives cone angle from atan(tooth_count / mate_teeth), so to build the mating gear, create a second bevel gear with tooth_count and mate_teeth swapped — or just point its target at the first gear and let sync_bevel do the swap for you automatically.
delta = atan(tooth_count / mate_teeth) # this gear's cone half-angle
pitch_r = module * tooth_count / 2 # at the large end
cone_dist L = pitch_r / sin(delta) # apex-to-back-face slant distance
z_apex = L * cos(delta) = pitch_r * mate_teeth / tooth_count
z_top = face_width_mm * cos(delta) # axial height of the small end
scale_top = (L - face_width_mm) / L # uniform XY scale at the small end
Build method
One 2D involute profile is built once at full size (the large end). For each of n_slices axial layers from z=0 to z=z_top, every vertex of that profile is uniformly scaled by (z_apex - z) / z_apex and placed at height z — this uniform per-slice scaling is Tredgold's approximation. No twist, no hand.
End caps at both z=0 and z=z_top are each a single n-gon face — no center vertex, no boolean. This is explicitly why there's no bore feature: the cap already covers the hub area under the root circle, so there's no boolean step to hook a bore into without also cutting into tooth material.
Cap fix
This used to fan each end cap from an added center vertex (like cluster_gear.py's shoulders still do). That degenerates: the shared _build_gear_profile inserts a dedendum-circle point at the same angle as the adjacent involute departure point wherever base_r > ded_r — a real undercut-flank segment, not a bug, but it puts that profile point and the newly-added center vertex on the same ray through the origin, making the fan triangle there exactly zero area. Confirmed before the fix: 28 zero-area cap faces at the default tooth_count=16, across both caps at every slice count and pressure angle tried — non_manifold was always 0 even with the defect present (a zero-area triangle still shares its short edge with exactly one real neighboring face, so edge-manifoldness was never actually broken), which is why this was cosmetic rather than a hard build failure and was safely deferred while higher-severity issues in other generators were fixed first.
Unlike the annulus family's caps (which bridge an inner AND outer loop and need bmesh.ops.triangle_fill to do that correctly — see README.md), a bevel gear's cap is a single closed loop with nothing to bridge, so the fix is simpler: a single n-gon face per cap, matching how helical_gear.py/ herringbone_gear.py already cap their own extrusions. No per-point triangles means this class of degenerate face can't occur regardless of tooth count or pressure angle. Verified clean (0 zero-area faces, 0 non-manifold edges) across tooth counts {8, 12, 16, 20, 30, 45, 60} × mate-tooth ratios {8, 16, 24, 45} × pressure angles {14.5°, 20°, 25°, 30°} — 112 combinations, all clean.
Panel warnings
face_width_mm >= cone_dist→ "Face width ≥ cone distance — gear vanishes" (ERROR, blocks —execute()reportsself.report({'ERROR'}, ...)and returnsCANCELLED. This is the only bevel-gear check that produces an explicit error report rather than a silent cancel).face_width_mm > cone_dist / 3(and not the above) → "Face width > L/3 — small-end teeth very small" (ERROR-icon label, but not blocking —execute()callsself.report({'WARNING'}, ...)and still builds the gear).cone_dist / 3is called out in the source as "the standard engineering limit" for bevel gears.
The info box also shows: cone angle, mate cone angle (90° - delta), pitch/tip/root diameter at the large end, cone distance, height, tip scale, pitch diameter at the small end, and the N:Nm ratio.
Output
One object, stamped:
gear_matching.stamp_gear(obj, "bevel", module, pressure_angle_deg,
tooth_count=tooth_count)
Note mate_teeth and hand are not stamped — unlike helical/ herringbone gears, the bevel-matching system relies entirely on sync_bevel reading the target object's live bmech_tooth_count at pick-time, not on any stamped mate/cone data on the gear itself.
On success: self.report({'INFO'}, "Bevel gear: %d/%d teeth, δ=%.1f°, module %.1f, face %.1f mm").