"""Pre-built complex assemblies — one chat command builds a multi-part scene. Each helper composes existing primitives (turbine, compressor_stage, bolt, gear, etc.) into a coherent named assembly. Sub-parts are stored under '_' so you can right-click any one of them individually. Visual mockups, not engineering-grade. For real turbomachinery design you want a CFD package, not chat_cad. """ from __future__ import annotations import math import cadquery as cq from library import ( bolt_m, nut_m, washer_m, spur_gear, turbine_wheel, propeller, compressor_stage, rocket_nozzle, combustor_can, ) def _store(eng, name: str, wp: cq.Workplane) -> None: eng._snapshot() eng.parts[name] = wp class RecipesEngine: """Recipes that build multiple named parts at once. Each method takes a `prefix` (becomes the part-name root) plus a small set of dimensions, and creates a coherent assembly in the scene. """ def __init__(self, cad_engine): self.cad = cad_engine # ============================================================ # # Turbojet — realistic axial-flow gas turbine cross-section # # Components in axial order, matching real engine diagrams: # # spinner -> inlet -> nacelle -> 8-stage compressor # # (alternating rotor/stator) -> annular combustor -> # # 2-stage turbine (HP+LP) -> afterburner -> conv-div nozzle # # + central shaft + rear tail cone # # ============================================================ # def turbojet(self, prefix: str, fan_d: float = 100, length: float = 500, afterburner: bool = True) -> str: L = float(length) d_out = float(fan_d) # nacelle OD d_core = d_out * 0.62 # gas-path inner casing d_hub = d_out * 0.32 # spinner / hub d_hot = d_out * 0.50 # hot-gas-path diameter d_shaft= d_out * 0.12 # central drive shaft # axial proportions (sum = 1.0) spin_L = L * 0.06 inlt_L = L * 0.08 comp_L = L * 0.32 # 8 stages comb_L = L * 0.14 turb_L = L * 0.10 # 2 stages ab_L = L * 0.15 if afterburner else 0.0 nozz_L = L - (spin_L + inlt_L + comp_L + comb_L + turb_L + ab_L) z = 0.0 # 1. Front spinner (paraboloid bullet) spin_pts = [(0, 0)] nseg = 10 for i in range(1, nseg + 1): f = i / nseg spin_pts.append((d_hub / 2 * math.sqrt(f), spin_L * f)) spin_pts.append((0, spin_L)) spinner = (cq.Workplane("XZ").moveTo(*spin_pts[0]) .polyline(spin_pts[1:]).close().revolve(360)) _store(self.cad, f"{prefix}_spinner", spinner) z += spin_L # 2. Inlet bell (sharp lip, expanding to nacelle OD) ipts = [(d_hub / 2, z), (d_out / 2 * 1.02, z + inlt_L * 0.3), (d_out / 2, z + inlt_L), (d_core / 2 + 0.5, z + inlt_L), (d_core / 2 + 0.5, z + inlt_L * 0.5), (d_hub / 2, z + inlt_L * 0.5)] inlet = (cq.Workplane("XZ").moveTo(*ipts[0]) .polyline(ipts[1:]).close().revolve(360)) _store(self.cad, f"{prefix}_inlet", inlet) z += inlt_L # 3. Outer nacelle — full-length cylindrical case (thin shell) nac_L = comp_L + comb_L + turb_L + ab_L + nozz_L * 0.6 nacelle = (cq.Workplane("XY") .circle(d_out / 2) .circle(d_out / 2 - 1.5) .extrude(nac_L) .translate((0, 0, z))) _store(self.cad, f"{prefix}_nacelle", nacelle) # 4. Compressor — 8 axial stages alternating rotor / stator n_stages = 8 stage_w = comp_L / n_stages for i in range(n_stages): is_rotor = (i % 2 == 0) twist = 10 + i * 1.5 if is_rotor else -(8 + i * 1.0) # stators counter-twisted cs = compressor_stage( blade_count=18 if is_rotor else 16, hub_d=d_core * 0.55, od=d_core, blade_height=stage_w * 0.7, twist_deg=twist, ) cs = cs.translate((0, 0, z + i * stage_w + stage_w * 0.15)) tag = "rotor" if is_rotor else "stator" _store(self.cad, f"{prefix}_comp_{tag}_{i+1}", cs) z += comp_L # 5. Annular combustor — outer can plus inner liner can_outer = combustor_can(diameter=d_core * 1.20, length=comb_L, wall_thickness=2.0, hole_diameter=2.5, hole_rings=6, holes_per_ring=28) can_outer = can_outer.translate((0, 0, z)) _store(self.cad, f"{prefix}_combustor_outer", can_outer) can_inner = (cq.Workplane("XY").circle(d_core * 0.42) .circle(d_core * 0.35) .extrude(comb_L) .translate((0, 0, z))) _store(self.cad, f"{prefix}_combustor_inner", can_inner) z += comb_L # 6. HP turbine (driven by hot gas, drives the compressor) hp = turbine_wheel(blade_count=22, od=d_core * 1.05, hub_d=d_core * 0.42, hub_thickness=turb_L * 0.45, blade_twist_deg=26) hp = hp.translate((0, 0, z)) _store(self.cad, f"{prefix}_turbine_HP", hp) # 7. LP turbine (smaller blade height, downstream) lp = turbine_wheel(blade_count=24, od=d_core * 1.0, hub_d=d_core * 0.40, hub_thickness=turb_L * 0.45, blade_twist_deg=20) lp = lp.translate((0, 0, z + turb_L * 0.55)) _store(self.cad, f"{prefix}_turbine_LP", lp) z += turb_L # 8. Afterburner (optional) — long cylindrical section with internal # flameholders represented by 6 radial fins if afterburner: ab_case = (cq.Workplane("XY").circle(d_core * 1.15) .circle(d_core * 1.07) .extrude(ab_L) .translate((0, 0, z))) _store(self.cad, f"{prefix}_afterburner_case", ab_case) # Flameholder ring (simplified): an annular plate with cutouts fh = (cq.Workplane("XY").circle(d_core * 0.95) .circle(d_core * 0.40) .extrude(ab_L * 0.08) .translate((0, 0, z + ab_L * 0.25))) _store(self.cad, f"{prefix}_flameholder", fh) z += ab_L # 9. Exhaust nozzle (convergent-divergent if afterburning) if afterburner: noz = rocket_nozzle(throat_d=d_hot * 0.62, exit_d=d_hot * 1.1, inlet_d=d_core * 1.1, length=nozz_L) else: # Simpler convergent nozzle noz = rocket_nozzle(throat_d=d_hot * 0.75, exit_d=d_hot * 0.75, inlet_d=d_core * 0.95, length=nozz_L) noz = noz.translate((0, 0, z)) _store(self.cad, f"{prefix}_nozzle", noz) # 10. Central shaft — connects compressor rotors to turbines shaft_L = comp_L + comb_L + turb_L + ab_L * 0.3 shaft = (cq.Workplane("XY").circle(d_shaft / 2) .extrude(shaft_L) .translate((0, 0, spin_L + inlt_L))) _store(self.cad, f"{prefix}_shaft", shaft) ab_str = "with afterburner" if afterburner else "(no afterburner)" n_parts = 13 + (3 if afterburner else 0) + n_stages return (f"built realistic turbojet '{prefix}' {ab_str}: " f"spinner, inlet bell, nacelle, {n_stages}-stage compressor " f"(rotor/stator), annular combustor (outer + inner liner), " f"HP+LP turbine, nozzle, central shaft — {n_parts} sub-parts, " f"total length {L} mm") # ============================================================ # # Turbofan — high-bypass with front fan and bypass duct # # ============================================================ # def turbofan(self, prefix: str, fan_d: float = 180, length: float = 500) -> str: L = float(length) d_fan = float(fan_d) d_core = d_fan * 0.45 d_hot = d_fan * 0.42 fan_L = L * 0.10 case_L = L * 0.45 # bypass duct extends over compressor section comp_L = L * 0.25 comb_L = L * 0.15 turb_L = L * 0.08 nozz_L = L * 0.22 # 1. Front fan fan = propeller(blade_count=22, diameter=d_fan, hub_d=d_core * 1.4, root_chord=d_fan * 0.10, tip_chord=d_fan * 0.05, twist_deg=32) fan = fan.translate((0, 0, fan_L * 0.2)) _store(self.cad, f"{prefix}_fan", fan) # 2. Bypass duct (hollow cylindrical case) case = (cq.Workplane("XY").circle(d_fan / 2 * 1.05) .circle(d_fan / 2 * 0.92) .extrude(case_L)) case = case.translate((0, 0, fan_L * 0.6)) _store(self.cad, f"{prefix}_bypass_case", case) # 3. Core: 3-stage compressor cstart = fan_L * 0.6 + (case_L - comp_L) * 0.3 for i in range(3): cs = compressor_stage(blade_count=18, hub_d=d_core * 0.55, od=d_core, blade_height=(comp_L / 3) * 0.7, twist_deg=8 + i * 2) cs = cs.translate((0, 0, cstart + i * (comp_L / 3))) _store(self.cad, f"{prefix}_compressor_{i+1}", cs) # 4. Combustor cz = cstart + comp_L can = combustor_can(diameter=d_core * 1.1, length=comb_L, wall_thickness=2.5, hole_diameter=3, hole_rings=4, holes_per_ring=24) can = can.translate((0, 0, cz)) _store(self.cad, f"{prefix}_combustor", can) # 5. HP turbine tz = cz + comb_L hp = turbine_wheel(blade_count=22, od=d_core * 1.05, hub_d=d_core * 0.4, hub_thickness=turb_L * 0.55, blade_twist_deg=24) hp = hp.translate((0, 0, tz)) _store(self.cad, f"{prefix}_turbine_hp", hp) # 6. Exhaust nozzle (mixed) nz = tz + turb_L noz = rocket_nozzle(throat_d=d_hot * 0.75, exit_d=d_hot, inlet_d=d_core * 0.9, length=nozz_L) noz = noz.translate((0, 0, nz)) _store(self.cad, f"{prefix}_nozzle", noz) return (f"built turbofan '{prefix}' (fan, bypass case, compressor x3, " f"combustor, HP turbine, nozzle)") # ============================================================ # # Bolted stack — bolt + washer + plate + washer + nut # # ============================================================ # def bolt_stack(self, prefix: str, spec: str = "M6", plate_thickness: float = 10, plate_size: float = 40) -> str: from library import _m d, (af, hh, nh, wo, wt) = _m(spec) # plate plate = (cq.Workplane("XY").box(plate_size, plate_size, plate_thickness) .faces("+Z").workplane().hole(d * 1.1)) _store(self.cad, f"{prefix}_plate", plate) # washer under head z_bot = plate_thickness / 2 + wt w_top = washer_m(spec).translate((0, 0, z_bot)) _store(self.cad, f"{prefix}_washer_top", w_top) # bolt sitting on top washer (head up) bolt = bolt_m(spec, length=plate_thickness + wt * 2 + nh + 5, threaded=True) # rotate so head is at +Z, shank goes down through plate bolt = bolt.rotate((0, 0, 0), (1, 0, 0), 180) bolt = bolt.translate((0, 0, plate_thickness / 2 + wt + hh + (plate_thickness + wt * 2 + nh + 5))) _store(self.cad, f"{prefix}_bolt", bolt) # washer under nut (bottom side) w_bot = washer_m(spec).translate((0, 0, -plate_thickness / 2 - wt)) _store(self.cad, f"{prefix}_washer_bot", w_bot) # nut at the bottom nut = nut_m(spec).translate((0, 0, -plate_thickness / 2 - wt - nh)) _store(self.cad, f"{prefix}_nut", nut) return (f"built {spec} bolted stack '{prefix}' (plate + 2 washers + " f"threaded bolt + nut, 5 sub-parts)") # ============================================================ # # Gear train — N gears in a line, meshing # # ============================================================ # def gear_train(self, prefix: str, n: int = 4, module: float = 1.5, teeth: int = 20, width: float = 6) -> str: n = int(n) if n < 2 or n > 8: raise ValueError("gear train needs 2-8 gears") # pitch diameter pd = module * teeth # gears are tangent, so center-distance = pd for i in range(n): g = spur_gear(module, teeth, width, bore=pd * 0.18) g = g.translate((i * pd, 0, 0)) _store(self.cad, f"{prefix}_g{i+1}", g) return (f"built gear train '{prefix}' ({n} meshing gears, " f"module {module}, {teeth} teeth each)") # ============================================================ # # Piston + connecting rod — single-cylinder engine snapshot # # ============================================================ # def piston_engine(self, prefix: str, bore: float = 50, stroke: float = 60) -> str: # cylinder block (open-bottom) block_h = stroke * 2.4 block = (cq.Workplane("XY").box(bore * 2.4, bore * 2.4, block_h, centered=(True, True, False)) .faces(">Z").workplane() .hole(bore + 0.4, block_h * 0.95)) _store(self.cad, f"{prefix}_block", block) # piston (cylinder with skirt) piston = (cq.Workplane("XY").circle(bore / 2) .extrude(stroke * 0.55) .faces(">Z").workplane().hole(bore * 0.5, stroke * 0.3)) piston = piston.translate((0, 0, block_h * 0.55)) _store(self.cad, f"{prefix}_piston", piston) # piston pin pin = (cq.Workplane("YZ").circle(bore * 0.16) .extrude(bore * 1.05) .translate((-bore * 0.55, 0, block_h * 0.55 + stroke * 0.30))) _store(self.cad, f"{prefix}_pin", pin) # connecting rod (simplified I-shape) rod_L = stroke * 1.6 rod_pts = [(-bore * 0.18, 0), (bore * 0.18, 0), (bore * 0.12, rod_L * 0.85), (bore * 0.30, rod_L), (-bore * 0.30, rod_L), (-bore * 0.12, rod_L * 0.85)] rod = (cq.Workplane("XY").polyline(rod_pts).close() .extrude(bore * 0.18) .rotate((0, 0, 0), (1, 0, 0), 90) .translate((0, -bore * 0.09, block_h * 0.55 + stroke * 0.30 - rod_L))) _store(self.cad, f"{prefix}_rod", rod) # crankpin (offset cylinder) crank_z = block_h * 0.55 + stroke * 0.30 - rod_L crank = (cq.Workplane("YZ").circle(bore * 0.20) .extrude(bore * 0.5) .translate((-bore * 0.25, 0, crank_z))) _store(self.cad, f"{prefix}_crankpin", crank) return (f"built piston engine '{prefix}' (block, piston, pin, rod, " f"crankpin — bore {bore} mm, stroke {stroke} mm)")