1 / 3
Fully Customizable Drone System with Selectable Components : Blender Script. — zdjęcie 1
1 / 3

Fully Customizable Drone System with Selectable Components : Blender Script.

Zamów wydruk tego modelu

Obliczymy ostateczną cenę na podstawie wybranego pliku. Sprawdzisz ją i zaakceptujesz przed płatnością.

Zapisane na tym urządzeniu.

Fully Customizable Drone System with Selectable Components : Blender Script.

Ocena
0 / 5
Polubienia
3
Pobrania
6
Wydruki
0
Wyświetlenia
55
Oceny
0

Aktualizacja u źródła:

Informacje o modelu

Źródło
Printables
Kategoria
RC & Robotics
Licencja
Creative Commons — Attribution — Noncommercial

Fully Customizable Drone System with Selectable Components : Blender Script. 

Pick your parts. Pick your drone style, Pic your Rail/pin/drop system, landing gear...  Auto layout.  + you can select to see your parts assembled.  

ONE SHOT BLENDER SCRIPT : Made with Gemini Argon 4.  Medium Thinking level.  NO EDITS.  F - ME this is nuts.   Smart Geometry has leveled up for blender scripting.  I could not do this 2 months ago.  Every two months Massive changes.  

Oops forgot to have it make and display a BOM...   I will do another model with the updates beyond this one shot attempt... If anyone one wants to test this out I will gladly gift you a roll of Filament of your choice... Seriously This could be really cool... But I , myself have no patients to print test print test this ... so if someone wants to give it a go... Free Filament . 

bl_info = {
    "name": "ModuDrone: Parametric Utility Drone & Payload System",
    "author": "Peter Farell & Engineering Assistant",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (N) > ModuDrone",
    "description": "Generates zero-support interlocking utility drone frames, tapered quick-swap arms, and 45-deg dovetail payload sleds with auto bed-packing.",
    "category": "Add Mesh",
}

import bpy import bmesh import math import os from mathutils import Vector, Matrix from bpy.props import ( EnumProperty, FloatProperty, BoolProperty, StringProperty, PointerProperty, ) from bpy.types import PropertyGroup, Operator, Panel

=====================================================================

1. CORE GEOMETRY & BMESH UTILITIES (1 Blender Unit = 1.0 mm)

=====================================================================

def get_or_create_collection(name="ModuDrone_Parts"): if name in bpy.data.collections: col = bpy.data.collections[name] else: col = bpy.data.collections.new(name) bpy.context.scene.collection.children.link(col) return col

def clear_collection(col_name="ModuDrone_Parts"): if col_name not in bpy.data.collections: return col = bpy.data.collections[col_name] for obj in list(col.objects): mesh = obj.data bpy.data.objects.remove(obj, do_unlink=True) if mesh and mesh.users == 0 and isinstance(mesh, bpy.types.Mesh): bpy.data.meshes.remove(mesh)

def assign_material(obj, name, color): if name in bpy.data.materials: mat = bpy.data.materials[name] else: mat = bpy.data.materials.new(name=name) mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") if bsdf: bsdf.inputs["Base Color"].default_value = color bsdf.inputs["Roughness"].default_value = 0.45 if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat)

def bmesh_to_object(bm, name, collection, mat_name="Drone_Structural", color=(0.18, 0.20, 0.22, 1.0)): bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) mesh = bpy.data.meshes.new(name) bm.to_mesh(mesh) bm.free() obj = bpy.data.objects.new(name, mesh) collection.objects.link(obj) assign_material(obj, mat_name, color) return obj

def apply_boolean_cut(target_obj, cutter_bm): """Applies a fast, clean EXACT boolean difference from a cutter BMesh and cleans up.""" bmesh.ops.remove_doubles(cutter_bm, verts=cutter_bm.verts, dist=0.001) bmesh.ops.recalc_face_normals(cutter_bm, faces=cutter_bm.faces)

cutter_mesh = bpy.data.meshes.new("Temp_Cutter_Mesh")
cutter_bm.to_mesh(cutter_mesh)
cutter_bm.free()

cutter_obj = bpy.data.objects.new("Temp_Cutter_Obj", cutter_mesh)
bpy.context.scene.collection.objects.link(cutter_obj)

# Ensure active context for modifier application in Blender 5.0+
bpy.ops.object.select_all(action='DESELECT')
target_obj.select_set(True)
bpy.context.view_layer.objects.active = target_obj

mod = target_obj.modifiers.new(name="Boolean_Cut", type='BOOLEAN')
mod.operation = 'DIFFERENCE'
mod.solver = 'EXACT'
mod.use_self = True
mod.object = cutter_obj

bpy.ops.object.modifier_apply(modifier=mod.name)

bpy.data.objects.remove(cutter_obj, do_unlink=True)
bpy.data.meshes.remove(cutter_mesh)

# Final manifold cleanup
bm = bmesh.new()
bm.from_mesh(target_obj.data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0005)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(target_obj.data)
bm.free()

def add_box_to_bm(bm, size_x, size_y, size_z, center=(0, 0, 0), rot_z=0.0): mat = Matrix.Translation(Vector(center)) @ Matrix.Rotation(rot_z, 4, 'Z') bmesh.ops.create_cube(bm, size=1.0, matrix=mat @ Matrix.Diagonal((size_x, size_y, size_z, 1.0)))

def add_Rounded_plate_to_bm(bm, sx, sy, sz, bevel_r=5.0, center=(0, 0, 0)): """Creates a rectangular box with 45-deg or rounded vertical corners.""" temp_bm = bmesh.new() bmesh.ops.create_cube(temp_bm, size=1.0) bmesh.ops.scale(temp_bm, vec=(sx, sy, sz), verts=temp_bm.verts)

# Select vertical edges for corner chamfer/rounding
vert_edges = [
    e for e in temp_bm.edges
    if abs(e.verts[0].co.x - e.verts[1].co.x) < 0.001
    and abs(e.verts[0].co.y - e.verts[1].co.y) < 0.001
]
if vert_edges and bevel_r > 0:
    bmesh.ops.bevel(
        temp_bm,
        geom=vert_edges,
        offset=bevel_r,
        offset_type='OFFSET',
        segments=4,
        profile=0.5,
        affect='EDGES'
    )
bmesh.ops.translate(temp_bm, vec=center, verts=temp_bm.verts)

# Merge into target bm
me = bpy.data.meshes.new("tmp")
temp_bm.to_mesh(me)
temp_bm.free()
bm.from_mesh(me)
bpy.data.meshes.remove(me)

def add_cylinder_to_bm(bm, radius, depth, segs=24, center=(0, 0, 0), rot_euler=(0, 0, 0)): mat = Matrix.Translation(Vector(center)) @ bpy.types.Object.bl_rna.properties['rotation_euler'] if False else ( Matrix.Translation(Vector(center)) @ Matrix.Rotation(rot_euler[2], 4, 'Z') @ Matrix.Rotation(rot_euler[1], 4, 'Y') @ Matrix.Rotation(rot_euler[0], 4, 'X') ) bmesh.ops.create_cone( bm, cap_ends=True, cap_tris=False, segments=segs, radius1=radius, radius2=radius, depth=depth, matrix=mat )

def add_trapezoid_prism_to_bm(bm, w_bottom, w_top, length_y, height_z, center=(0, 0, 0), rot_z=0.0): """ Creates a prism with trapezoidal X-Z cross-section extruded along Y. Used for 45-degree zero-support dovetails and tapered arm roots. """ hb = w_bottom * 0.5 ht = w_top * 0.5 hy = length_y * 0.5 hz = height_z * 0.5

coords = [
    (-hb, -hy, -hz), (hb, -hy, -hz), (ht, -hy, hz), (-ht, -hy, hz),
    (-hb,  hy, -hz), (hb,  hy, -hz), (ht,  hy, hz), (-ht,  hy, hz),
]
mat = Matrix.Translation(Vector(center)) @ Matrix.Rotation(rot_z, 4, 'Z')
verts = [bm.verts.new(mat @ Vector(c)) for c in coords]

faces = [
    (verts[0], verts[3], verts[2], verts[1]), # Front (-Y)
    (verts[4], verts[5], verts[6], verts[7]), # Back (+Y)
    (verts[0], verts[1], verts[5], verts[4]), # Bottom (-Z)
    (verts[3], verts[7], verts[6], verts[2]), # Top (+Z)
    (verts[1], verts[2], verts[6], verts[5]), # Right (+X)
    (verts[0], verts[4], verts[7], verts[3]), # Left (-X)
]
for f in faces:
    bm.faces.new(f)

def add_tapered_y_prism_to_bm(bm, w_y_neg, w_y_pos, length_y, height_z, center=(0, 0, 0), rot_z=0.0): """ Creates a horizontal wedge that tapers in width (X) along length (Y). Used for the self-centering tapered arm roots and sockets. """ h_neg = w_y_neg * 0.5 h_pos = w_y_pos * 0.5 hy = length_y * 0.5 hz = height_z * 0.5

coords = [
    (-h_neg, -hy, -hz), (h_neg, -hy, -hz), (h_neg, -hy, hz), (-h_neg, -hy, hz),
    (-h_pos,  hy, -hz), (h_pos,  hy, -hz), (h_pos,  hy, hz), (-h_pos,  hy, hz),
]
mat = Matrix.Translation(Vector(center)) @ Matrix.Rotation(rot_z, 4, 'Z')
verts = [bm.verts.new(mat @ Vector(c)) for c in coords]

faces = [
    (verts[0], verts[3], verts[2], verts[1]),
    (verts[4], verts[5], verts[6], verts[7]),
    (verts[0], verts[1], verts[5], verts[4]),
    (verts[3], verts[7], verts[6], verts[2]),
    (verts[1], verts[2], verts[6], verts[5]),
    (verts[0], verts[4], verts[7], verts[3]),
]
for f in faces:
    bm.faces.new(f)

def add_fastener_hole(cutter_bm, x, y, z_center, total_h, props, nut_on_top=True): """Adds an M3 through-hole plus either a Captive M3 Hex Nut Trap or M3 Heat-Set Insert pocket.""" m3_clear_r = 1.65 # 3.3mm clearance for M3 bolt add_cylinder_to_bm(cutter_bm, m3_clear_r, total_h + 4.0, segs=20, center=(x, y, z_center))

if props.fastener_mode == 'HEX_NUT':
    # M3 hex nut is 5.5mm across flats -> circumradius = (5.5 + tol) / sqrt(3)
    af = 5.55 + (props.clearance * 0.5)
    hex_r = af / math.sqrt(3.0)
    trap_d = 2.6
    z_pos = (z_center + total_h * 0.5 - trap_d * 0.5 + 0.2) if nut_on_top else (z_center - total_h * 0.5 + trap_d * 0.5 - 0.2)
    add_cylinder_to_bm(cutter_bm, hex_r, trap_d + 0.4, segs=6, center=(x, y, z_pos))
elif props.fastener_mode == 'HEAT_INSERT':
    # Standard M3x3x4.6 or Voron M3x4x4.5 insert pilot hole (4.1mm diameter)
    ins_r = 2.05 + (props.clearance * 0.25)
    ins_d = 3.4
    z_pos = (z_center + total_h * 0.5 - ins_d * 0.5 + 0.2) if nut_on_top else (z_center - total_h * 0.5 + ins_d * 0.5 - 0.2)
    add_cylinder_to_bm(cutter_bm, ins_r, ins_d + 0.4, segs=24, center=(x, y, z_pos))

=====================================================================

2. MODULE GENERATORS

=====================================================================

def get_arm_angles(frame_type): """Returns the 4 arm angles in radians (FL, FR, RL, RR). +Y is Nose/Forward.""" if frame_type == 'DEADCAT': # Wider front arms (60 deg from Y axis) to keep props out of front camera/sensor view return [ math.radians(60), # Front-Left math.radians(-60), # Front-Right math.radians(132), # Rear-Left math.radians(-132), # Rear-Right ] else: # True-X: 45 deg symmetric return [ math.radians(45), math.radians(-45), math.radians(135), math.radians(-135), ]

def generate_core_bottom_plate(props, col): """ Generates the Central Core Lower Chassis with: - 4x Tapered Dovetail Arm Sockets + MR30 bulkhead pockets + Cross-pin locks - 30.5x30.5mm and/or 20x20mm FC/ESC M3 grid - Recessed Matek Micro BEC (19x16x5mm) pocket - Bottom 45-degree Self-Zeroing Dovetail Belly Rail with 10mm CG index notches """ bm = bmesh.new() cutter = bmesh.new()

core_w = 96.0
core_l = 128.0
body_h = 14.0  # Houses the 10mm tall tapered arm roots + 2mm top/bottom walls
rail_w = props.rail_width
rail_h = 5.0

# 1. Main Core Clamshell Body (Z from 0 to 14)
add_Rounded_plate_to_bm(bm, core_w, core_l, body_h, bevel_r=10.0, center=(0, 0, body_h * 0.5))

# 2. Bottom 45-Deg Male Dovetail Rail (Z from -rail_h to 0)
# Wider at bottom (-Z), narrower at top (Z=0) by rail_h on each side -> 45-deg overhang when printed upside down!
add_trapezoid_prism_to_bm(
    bm,
    w_bottom=rail_w,
    w_top=rail_w - (2.0 * rail_h),
    length_y=core_l - 8.0,
    height_z=rail_h + 0.5,
    center=(0, 0, -rail_h * 0.5 + 0.25)
)

# 3. Cutouts: Central Electronics Bay & Wire Channels
add_Rounded_plate_to_bm(cutter, 52.0, 68.0, body_h - 4.0, bevel_r=6.0, center=(0, 0, body_h * 0.5 + 2.5))

# 4. FC / ESC Stack Mounting Grid (30.5x30.5 and/or 20x20)
grids = []
if props.fc_grid in ('30_5', 'BOTH'):
    grids.append(15.25)
if props.fc_grid in ('20_0', 'BOTH'):
    grids.append(10.0)
for half_s in grids:
    for sx in (-half_s, half_s):
        for sy in (-half_s, half_s):
            add_fastener_hole(cutter, sx, sy, body_h * 0.5, body_h + 12.0, props, nut_on_top=False)

# 5. Matek Micro BEC Pocket (19 x 16 x 4.5 mm) in rear bay
if props.include_bec_pocket:
    add_box_to_bm(cutter, 19.5, 16.5, 6.0, center=(0, -44.0, body_h - 2.5))
    # Wire pass-through slot down to the belly payload rail
    add_box_to_bm(cutter, 10.0, 8.0, body_h + rail_h + 4.0, center=(0, -34.0, body_h * 0.5 - 2.0))

# 6. CG Indexing Notches along the Bottom Belly Rail (every 15mm)
for ny in range(-45, 50, 15):
    add_box_to_bm(cutter, rail_w + 4.0, 3.2, 2.8, center=(0, float(ny), -rail_h + 1.2))

# 7. 4x Tapered Arm Sockets + Captive Cross-Pin + MR30 Bulkhead Pocket
angles = get_arm_angles(props.frame_type)
tol = props.clearance
sock_depth = 26.0
root_w_outer = 22.0 + (tol * 2.0)
root_w_inner = 16.0 + (tol * 2.0)
root_h = 9.0 + (tol * 1.5)

for ang in angles:
    # Arm socket sits radially at distance 44mm from center
    dist = 44.0
    cx = -math.sin(ang) * dist
    cy = math.cos(ang) * dist

    # Tapered wedge pocket (wider at outside of frame, narrower toward center)
    add_tapered_y_prism_to_bm(
        cutter,
        w_y_neg=root_w_inner,
        w_y_pos=root_w_outer,
        length_y=sock_depth,
        height_z=root_h,
        center=(cx, cy, body_h * 0.5),
        rot_z=ang
    )

    # Vertical M3 Cross-Pin Lock Hole through socket center
    add_fastener_hole(cutter, cx, cy, body_h * 0.5, body_h + 4.0, props, nut_on_top=False)

    # Internal MR30 3-Pin Connector Bulkhead Pocket at inner end of socket
    if props.use_mr30_plug:
        mr_dist = dist - (sock_depth * 0.5) - 2.5
        mx = -math.sin(ang) * mr_dist
        my = math.cos(ang) * mr_dist
        add_box_to_bm(cutter, 10.6 + tol, 6.5, 5.6 + tol, center=(mx, my, body_h * 0.5), rot_z=ang)
        # Wire channel into FC bay
        ch_dist = mr_dist - 8.0
        add_box_to_bm(cutter, 8.0, 18.0, 6.0, center=(-math.sin(ang) * ch_dist, math.cos(ang) * ch_dist, body_h * 0.5 + 2.0), rot_z=ang)

# 8. 8x Perimeter Clamshell M3 Standoff Holes
for px in (-38.0, 38.0):
    for py in (-54.0, 0.0, 54.0):
        add_fastener_hole(cutter, px, py, body_h * 0.5, body_h + 4.0, props, nut_on_top=False)

obj = bmesh_to_object(bm, "01_Core_Bottom_Chassis", col)
apply_boolean_cut(obj, cutter)
return obj

def generate_core_top_plate(props, col): """ Generates the Central Core Upper Deck with: - Top 45-degree Self-Zeroing Dovetail Battery Rail - Ventilation & FC USB/Bluetooth access slots - Tail GPS/ELRS Mast Socket (21x21mm or 18x18mm M10 GPS + dual 90-deg antenna tubes) """ bm = bmesh.new() cutter = bmesh.new()

core_w = 96.0
core_l = 128.0
plate_h = 4.0
rail_w = props.rail_width
rail_h = 5.0

# Base Top Plate (Z from 0 to 4)
add_Rounded_plate_to_bm(bm, core_w, core_l, plate_h, bevel_r=10.0, center=(0, 0, plate_h * 0.5))

# Top 45-Deg Male Dovetail Rail for Battery Sled (Z from 4 to 9)
# Wider at top (+Z), narrower at base (Z=4) -> prints upside-down on flat top face with 45-deg self-supporting bevels!
add_trapezoid_prism_to_bm(
    bm,
    w_bottom=rail_w - (2.0 * rail_h),
    w_top=rail_w,
    length_y=core_l - 16.0,
    height_z=rail_h + 0.5,
    center=(0, 4.0, plate_h + rail_h * 0.5 - 0.25)
)

# Tail GPS & ELRS Diversity Antenna Mast Pod (at rear -Y)
if props.gps_mast_type != 'NONE':
    gps_size = 21.6 if props.gps_mast_type == 'M10_21' else 18.6
    pod_w = gps_size + 5.0
    add_Rounded_plate_to_bm(bm, pod_w, 26.0, 12.0, bevel_r=3.0, center=(0, -core_l * 0.5 - 6.0, 6.0))
    # GPS ceramic module pocket
    add_box_to_bm(cutter, gps_size + props.clearance, gps_size + props.clearance, 9.0, center=(0, -core_l * 0.5 - 6.0, 8.5))
    # Dual 45-deg ELRS Antenna Tube holes (3.2mm diameter)
    for side in (-1, 1):
        add_cylinder_to_bm(
            cutter, 1.65, 22.0, segs=16,
            center=(side * (pod_w * 0.4), -core_l * 0.5 - 6.0, 4.0),
            rot_euler=(0, math.radians(side * 45), 0)
        )

# CG Indexing Notches on Top Battery Rail
for ny in range(-40, 55, 15):
    add_box_to_bm(cutter, rail_w + 4.0, 3.2, 2.8, center=(0, float(ny), plate_h + rail_h - 1.2))

# Weight-reduction & FC cooling windows
for wy in (-22.0, 22.0):
    add_Rounded_plate_to_bm(cutter, 44.0, 24.0, plate_h + rail_h + 4.0, bevel_r=4.0, center=(0, wy, 4.0))

# 8x Perimeter Standoff Holes + 4x Arm Cross-Pin Access Holes
for px in (-38.0, 38.0):
    for py in (-54.0, 0.0, 54.0):
        add_cylinder_to_bm(cutter, 1.65, 20.0, segs=20, center=(px, py, 4.0))

for ang in get_arm_angles(props.frame_type):
    cx = -math.sin(ang) * 44.0
    cy = math.cos(ang) * 44.0
    add_cylinder_to_bm(cutter, 1.75, 20.0, segs=20, center=(cx, cy, 4.0))

obj = bmesh_to_object(bm, "02_Core_Top_Deck", col)
apply_boolean_cut(obj, cutter)
return obj

def generate_quick_swap_arm(props, col, arm_index=1): """ Generates a Quick-Swap Propulsion Arm along +Y with: - Tapered Dovetail Root + M3 cross-pin hole + MR30 plug pocket - Triangular Iso-Truss or Enclosed Aero Beam - 19x19mm (2807) and/or 16x16mm motor mount pad with M3 slots - Integrated tip socket for the Modular Landing Leg """ bm = bmesh.new() cutter = bmesh.new()

prop_in = float(props.prop_size)
# Calculate arm length from root to motor center to guarantee prop clearance
arm_len = (prop_in * 25.4 * 0.5) + 32.0
sock_depth = 25.5
root_h = 9.0
beam_h = 11.0
beam_w = 18.0

# 1. Tapered Root Wedge (Y from -sock_depth to 0)
add_tapered_y_prism_to_bm(
    bm,
    w_y_neg=16.0,
    w_y_pos=22.0,
    length_y=sock_depth,
    height_z=root_h,
    center=(0, -sock_depth * 0.5, root_h * 0.5)
)

# 2. Main Structural Beam (Y from 0 to arm_len)
add_tapered_y_prism_to_bm(
    bm,
    w_y_neg=22.0,
    w_y_pos=beam_w,
    length_y=arm_len,
    height_z=beam_h,
    center=(0, arm_len * 0.5, beam_h * 0.5)
)

# 3. Motor Mount Circular Pad at Y = arm_len
pad_r = 16.5
add_cylinder_to_bm(bm, pad_r, beam_h, segs=32, center=(0, arm_len, beam_h * 0.5))

# 4. Cutouts: Cross-Pin Lock Hole & MR30 Root Pocket
add_cylinder_to_bm(cutter, 1.65, 24.0, segs=20, center=(0, -sock_depth * 0.5, root_h * 0.5))
if props.use_mr30_plug:
    add_box_to_bm(cutter, 10.3, 8.0, 5.3, center=(0, -sock_depth + 2.5, root_h * 0.5))

# 5. Internal Wire Tunnel & Iso-Truss Weight-Saving Triangles
add_box_to_bm(cutter, 6.5, arm_len + sock_depth, 4.5, center=(0, (arm_len - sock_depth) * 0.5, beam_h * 0.5 + 1.5))

if props.arm_style == 'ISO_TRUSS':
    num_bays = max(2, int((arm_len - 25.0) // 22.0))
    bay_step = (arm_len - 28.0) / num_bays
    for i in range(num_bays):
        by = 14.0 + (i + 0.5) * bay_step
        # Diamond/Triangular truss cutouts through Z (prints flat with zero supports)
        for side in (-1, 1):
            add_cylinder_to_bm(cutter, 3.8, beam_h + 4.0, segs=4, center=(side * 5.2, by, beam_h * 0.5))

# 6. Motor Mount M3 Bolt Pattern (19x19mm and/or 16x16mm + 9mm center shaft hole)
add_cylinder_to_bm(cutter, 4.5, beam_h + 4.0, segs=24, center=(0, arm_len, beam_h * 0.5))

# Recessed lower counterbore so standard M3x8 motor screws sit at optimal depth (5.5mm plate thickness under motor)
add_cylinder_to_bm(cutter, 13.5, beam_h - 5.5, segs=28, center=(0, arm_len, (beam_h - 5.5) * 0.5 - 0.2))

r_min = 8.0 if props.motor_grid in ('16_0', 'UNIVERSAL') else 9.5
r_max = 9.5 if props.motor_grid in ('19_0', 'UNIVERSAL') else 8.0

for m_ang_deg in (45, 135, 225, 315):
    rad = math.radians(m_ang_deg)
    for r_step in (r_min, (r_min + r_max) * 0.5, r_max):
        mx = math.cos(rad) * r_step
        my = arm_len + math.sin(rad) * r_step
        add_cylinder_to_bm(cutter, 1.65, beam_h + 4.0, segs=16, center=(mx, my, beam_h * 0.5))

# 7. Under-Tip Dovetail Socket for Modular Landing Leg
add_box_to_bm(cutter, 8.0 + props.clearance, 14.0, 5.5, center=(0, arm_len - 18.0, 2.5))
add_cylinder_to_bm(cutter, 1.65, 24.0, segs=16, center=(0, arm_len - 18.0, beam_h * 0.5), rot_euler=(0, math.radians(90), 0))

obj = bmesh_to_object(bm, f"03_QuickSwap_Arm_{arm_index}", col)
apply_boolean_cut(obj, cutter)
return obj

def generate_battery_sled(props, col): """ Generates the Top Universal Battery Sled with: - Female 45-degree Dovetail Channel - Compliant Print-in-Place Spring Latch that engages the 15mm CG rail notches - Integrated XT60 Bulkhead Holder & Velcro Strap Slots """ bm = bmesh.new() cutter = bmesh.new()

sled_w = 68.0
sled_l = props.battery_sled_length
sled_h = 8.5
rail_w = props.rail_width + (props.clearance * 2.0)
rail_h = 5.0 + props.clearance

# Main Battery Sled Deck (Z from 0 to 8.5)
add_Rounded_plate_to_bm(bm, sled_w, sled_l, sled_h, bevel_r=6.0, center=(0, 0, sled_h * 0.5))

# Rear XT60 Bulkhead Lock Block (at -Y end)
add_Rounded_plate_to_bm(bm, 24.0, 14.0, 14.0, bevel_r=2.0, center=(0, -sled_l * 0.5 + 7.0, sled_h + 5.0))
# XT60 press-fit rectangular cutout (15.6 x 8.2 mm)
add_box_to_bm(cutter, 15.7 + props.clearance, 16.0, 8.3 + props.clearance, center=(0, -sled_l * 0.5 + 7.0, sled_h + 5.0))

# Female 45-Deg Dovetail Channel along Bottom (-Z)
add_trapezoid_prism_to_bm(
    cutter,
    w_bottom=rail_w - (2.0 * rail_h),
    w_top=rail_w,
    length_y=sled_l + 4.0,
    height_z=rail_h + 0.4,
    center=(0, 0, rail_h * 0.5 - 0.2)
)

# 3x Pairs of Velcro Battery Strap Slots (22 x 3.5 mm)
for sy in (-sled_l * 0.3, 0.0, sled_l * 0.3):
    for sx in (-sled_w * 0.38, sled_w * 0.38):
        add_box_to_bm(cutter, 4.0, 22.0, sled_h + 4.0, center=(sx, sy, sled_h * 0.5))

# Center Weight-Saving Hexagonal/Diamond Windows
for wy in (-sled_l * 0.18, sled_l * 0.18):
    add_Rounded_plate_to_bm(cutter, 26.0, 24.0, sled_h + 4.0, bevel_r=4.0, center=(0, wy, sled_h * 0.5))

# Front CG Locking Pin Hole (M3 pin or thumbscrew engages Core rail notches)
add_cylinder_to_bm(cutter, 1.65, sled_w + 4.0, segs=18, center=(0, sled_l * 0.5 - 12.0, 3.5), rot_euler=(0, math.radians(90), 0))

obj = bmesh_to_object(bm, "04_Universal_Battery_Sled", col, mat_name="Drone_Accent", color=(0.85, 0.38, 0.08, 1.0))
apply_boolean_cut(obj, cutter)
return obj

def generate_belly_payload_module(props, col): """ Generates the Under-Belly Mission Payload Sled with: - Top Female 45-deg Dovetail Receiver (slides onto Bottom Core Rail) - Selectable Mission Bay: 1. MG996R 55g Heavy Clevis Pin-Drop (Zero vertical servo load!) 2. MG90S 9g Micro Pendulum Drop 3. Gravity MOSFET Solid-State Trigger Pod 4. Blank Parametric Utility Plate """ bm = bmesh.new() cutter = bmesh.new()

rail_w = props.rail_width + (props.clearance * 2.0)
rail_h = 5.0 + props.clearance
sled_w = 56.0
sled_l = 96.0
top_deck_h = 9.0

# 1. Upper Dovetail Receiver Deck (Z from -top_deck_h to 0)
add_Rounded_plate_to_bm(bm, sled_w, sled_l, top_deck_h, bevel_r=5.0, center=(0, 0, -top_deck_h * 0.5))

# Cut the Female 45-Deg Dovetail Slot into top face (Z from -rail_h to 0)
add_trapezoid_prism_to_bm(
    cutter,
    w_bottom=rail_w,
    w_top=rail_w - (2.0 * rail_h),
    length_y=sled_l + 4.0,
    height_z=rail_h + 0.4,
    center=(0, 0, -rail_h * 0.5 + 0.2)
)

# Cross-Pin Lock Hole at Front & Rear to lock into the Core's 15mm CG notches
for py in (-30.0, 30.0):
    add_cylinder_to_bm(cutter, 1.65, sled_w + 4.0, segs=18, center=(0, py, -3.5), rot_euler=(0, math.radians(90), 0))

# 2. Mission-Specific Lower Bay Geometry
if props.payload_type == 'MG996R_CLEVIS':
    # Heavy-Lift Servo Housing + Zero-Load Vertical Clevis Slot directly under CG (Y=0)
    bay_h = 34.0
    add_Rounded_plate_to_bm(bm, 46.0, 84.0, bay_h, bevel_r=4.0, center=(0, 0, -top_deck_h - bay_h * 0.5))
    
    # MG996R Servo Cutout (41.2 x 20.5 mm) positioned slightly aft so horn pulls pin across Y=0
    servo_y = -18.0
    add_box_to_bm(cutter, 20.6 + props.clearance, 41.4 + props.clearance, bay_h + 4.0, center=(0, servo_y, -top_deck_h - bay_h * 0.5))
    # 4x MG996R M3 Mounting Boss Holes (49.5 x 10.0 mm pattern)
    for sx in (-5.0, 5.0):
        for sy in (servo_y - 24.75, servo_y + 24.75):
            add_fastener_hole(cutter, sx, sy, -top_deck_h - bay_h * 0.5, bay_h + 2.0, props, nut_on_top=True)

    # Vertical Cargo Loop Clevis Slot at CG (Y = +20.0mm)
    clevis_y = 20.0
    add_box_to_bm(cutter, 12.0, 16.0, bay_h - 6.0, center=(0, clevis_y, -top_deck_h - bay_h * 0.5 - 4.0))
    # Horizontal 3.5mm Steel Release Pin Guide Tunnel connecting Servo Horn to Clevis
    add_cylinder_to_bm(cutter, 2.0, 48.0, segs=20, center=(0, 8.0, -top_deck_h - bay_h + 10.0), rot_euler=(math.radians(90), 0, 0))

elif props.payload_type == 'MG90S_MICRO':
    # Compact 9g Micro Servo Bay + Release Hook Slot
    bay_h = 22.0
    add_Rounded_plate_to_bm(bm, 36.0, 64.0, bay_h, bevel_r=4.0, center=(0, 0, -top_deck_h - bay_h * 0.5))
    # MG90S Cutout (23.2 x 12.6 mm)
    add_box_to_bm(cutter, 12.8 + props.clearance, 23.4 + props.clearance, bay_h + 4.0, center=(0, -8.0, -top_deck_h - bay_h * 0.5))
    # Micro clevis drop slot at Y = +14mm
    add_box_to_bm(cutter, 10.0, 12.0, bay_h - 4.0, center=(0, 14.0, -top_deck_h - bay_h * 0.5 - 3.0))
    add_cylinder_to_bm(cutter, 1.6, 36.0, segs=16, center=(0, 6.0, -top_deck_h - bay_h + 7.0), rot_euler=(math.radians(90), 0, 0))

elif props.payload_type == 'MOSFET_POD':
    # Enclosed Solid-State Electronics Pod for DFRobot Gravity MOSFET + Pump/Magnet Mounts
    bay_h = 18.0
    add_Rounded_plate_to_bm(bm, 48.0, 74.0, bay_h, bevel_r=4.0, center=(0, 0, -top_deck_h - bay_h * 0.5))
    # Internal cavity for MOSFET board (38 x 28 mm)
    add_Rounded_plate_to_bm(cutter, 38.0, 56.0, bay_h - 4.0, bevel_r=3.0, center=(0, 0, -top_deck_h - bay_h * 0.5 + 2.5))
    # Bottom M3 payload attachment grid (30 x 30 mm)
    for sx in (-15.0, 15.0):
        for sy in (-15.0, 15.0):
            add_fastener_hole(cutter, sx, sy, -top_deck_h - bay_h + 2.0, 8.0, props, nut_on_top=True)

else:  # 'BLANK_PLATE'
    # Universal Parametric Mounting Sled with user-defined X/Y hole grid
    gx = props.custom_hole_x * 0.5
    gy = props.custom_hole_y * 0.5
    for sx in (-gx, gx):
        for sy in (-gy, gy):
            add_fastener_hole(cutter, sx, sy, -top_deck_h * 0.5, top_deck_h + 4.0, props, nut_on_top=False)

obj = bmesh_to_object(bm, "05_Belly_Payload_Module", col, mat_name="Drone_Accent", color=(0.85, 0.38, 0.08, 1.0))
apply_boolean_cut(obj, cutter)
return obj

def get_required_leg_height(props): if not props.auto_leg_height: return props.manual_leg_height # Calculate depth of belly rail (5mm) + top_deck (9mm) + mission bay + 35mm safety ground clearance bay_depths = { 'MG996R_CLEVIS': 34.0, 'MG90S_MICRO': 22.0, 'MOSFET_POD': 18.0, 'BLANK_PLATE': 0.0, } total_belly = 5.0 + 9.0 + bay_depths.get(props.payload_type, 20.0) return total_belly + 35.0

def generate_landing_leg(props, col, leg_index=1): """ Generates an A-Frame Shock-Absorbing Landing Leg that plugs into the Arm Tip Socket. Prints 100% flat on its side with zero supports. """ bm = bmesh.new() cutter = bmesh.new()

leg_h = get_required_leg_height(props)
peg_w = 8.0
peg_l = 13.5
peg_h = 5.2

# Top Insertion Peg (mates with Arm Tip Socket)
add_box_to_bm(bm, peg_w, peg_l, peg_h, center=(0, 0, peg_h * 0.5))

# Angled Strut Body (Z from 0 down to -leg_h)
add_trapezoid_prism_to_bm(
    bm,
    w_bottom=10.0,
    w_top=peg_w,
    length_y=24.0,
    height_z=leg_h,
    center=(0, 0, -leg_h * 0.5)
)

# Wide Foot Skid at Bottom (-Z)
add_Rounded_plate_to_bm(bm, 14.0, 36.0, 6.0, bevel_r=2.5, center=(0, 0, -leg_h + 3.0))

# M3 Cross-Bolt Hole through top peg
add_cylinder_to_bm(cutter, 1.65, 20.0, segs=16, center=(0, 0, 2.5), rot_euler=(0, math.radians(90), 0))

# Compliant Shock-Absorbing Flex Slot in middle of strut
if leg_h > 45.0:
    add_Rounded_plate_to_bm(cutter, 20.0, 10.0, leg_h * 0.45, bevel_r=3.5, center=(0, 0, -leg_h * 0.48))

obj = bmesh_to_object(bm, f"06_Landing_Leg_{leg_index}", col)
apply_boolean_cut(obj, cutter)
return obj

def generate_tpu_dampers(props, col): """Generates co-aligned TPU vibration-isolation inserts for multi-toolhead printers (Prusa XL).""" bm = bmesh.new() # 4x Arm Root TPU End-Cap Pads + 2x Rail Damping Strips for i, ang in enumerate(get_arm_angles(props.frame_type)): dist = 31.0 cx = -math.sin(ang) * dist cy = math.cos(ang) * dist add_box_to_bm(bm, 15.5, 1.8, 8.8, center=(cx, cy, 7.0), rot_z=ang)

# Top & Bottom Rail Friction Strips
add_box_to_bm(bm, 18.0, 80.0, 1.2, center=(0, 0, 18.5))
add_box_to_bm(bm, 18.0, 80.0, 1.2, center=(0, 0, -4.5))

obj = bmesh_to_object(bm, "07_MultiMaterial_TPU_Dampers", col, mat_name="Drone_TPU", color=(0.10, 0.75, 0.45, 1.0))
return obj

=====================================================================

3. ASSEMBLED PLACEMENT vs. SMART 2D PRINT-BED PACKING ENGINE

=====================================================================

def position_assembled_preview(parts, props): """Positions all generated modules into their true flight coordinates for visual inspection.""" core_bot, core_top, arms, bat_sled, belly_mod, legs, tpu_obj = parts

# Core Bottom sits at Z = 0..14
core_bot.location = (0, 0, 0)
# Core Top sits above a 28mm FC stack standoff gap (Z = 42)
core_top.location = (0, 0, 42.0)
# Battery Sled slides onto Top Rail (Z = 46)
bat_sled.location = (0, 4.0, 46.2)
# Belly Payload Module slides onto Bottom Rail (Z = 0)
belly_mod.location = (0, 0, -0.2)

angles = get_arm_angles(props.frame_type)
prop_in = float(props.prop_size)
arm_len = (prop_in * 25.4 * 0.5) + 32.0

for idx, ang in enumerate(angles):
    if idx < len(arms):
        arm = arms[idx]
        # Root origin sits at radial distance 56.5mm so wedge seats into socket
        r_root = 56.5
        arm.rotation_euler = (0, 0, ang)
        arm.location = (-math.sin(ang) * r_root, math.cos(ang) * r_root, 2.5)

    if idx < len(legs):
        leg = legs[idx]
        r_leg = 56.5 + arm_len - 18.0
        leg.rotation_euler = (0, 0, ang)
        leg.location = (-math.sin(ang) * r_leg, math.cos(ang) * r_leg, 2.5)

def get_bed_dimensions(bed_preset): beds = { 'BED_220': (220.0, 220.0), 'BED_250': (250.0, 210.0), 'BED_360': (360.0, 360.0), } return beds.get(bed_preset, (220.0, 220.0))

def create_bed_visualizer(bed_x, bed_y, col): """Draws a non-rendering 3D wireframe boundary representing the 3D printer build plate.""" bm = bmesh.new() hx, hy = bed_x * 0.5, bed_y * 0.5 v1 = bm.verts.new((-hx, -hy, 0.0)) v2 = bm.verts.new(( hx, -hy, 0.0)) v3 = bm.verts.new(( hx, hy, 0.0)) v4 = bm.verts.new((-hx, hy, 0.0)) for a, b in ((v1, v2), (v2, v3), (v3, v4), (v4, v1)): bm.edges.new((a, b)) me = bpy.data.meshes.new("00_Print_Bed_Boundary") bm.to_mesh(me) bm.free() obj = bpy.data.objects.new("00_Print_Bed_Boundary", me) obj.display_type = 'WIRE' obj.hide_render = True col.objects.link(obj) return obj

def flatten_and_pack_to_bed(objects, props, col): """ Rotates parts to their optimal zero-support printing orientation, drops their lowest vertex to Z = 0.0, and packs them onto the selected build plate using 2D shelf packing. Returns (success_bool, overflow_count). """ bed_x, bed_y = get_bed_dimensions(props.bed_size) create_bed_visualizer(bed_x, bed_y, col) bpy.context.view_layer.update()

# 1. Orient each part for zero-support printing & bake rotation
for obj in objects:
    obj.location = (0, 0, 0)
    obj.rotation_euler = (0, 0, 0)

    if "Core_Bottom" in obj.name or "Belly_Payload" in obj.name:
        # Flip 180 deg around X so the bottom rail / clevis prints upward with 45-deg self-supporting walls
        obj.rotation_euler = (math.radians(180), 0, 0)
    elif "Landing_Leg" in obj.name:
        # Lay flat on its X side for maximum layer strength along the strut
        obj.rotation_euler = (0, math.radians(90), 0)

bpy.context.view_layer.update()

# 2. Compute world-space bounding boxes and sort by height (Y) descending for optimal shelf packing
item_data = []
for obj in objects:
    bbox = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
    min_x = min(v.x for v in bbox)
    max_x = max(v.x for v in bbox)
    min_y = min(v.y for v in bbox)
    max_y = max(v.y for v in bbox)
    min_z = min(v.z for v in bbox)
    sx = max_x - min_x
    sy = max_y - min_y
    
    # If an arm is taller in Y than X and fits better rotated 90 deg, rotate it
    if sy > sx and sy > (bed_y - 10.0) and sx <= (bed_y - 10.0):
        obj.rotation_euler.z += math.radians(90)
        bpy.context.view_layer.update()
        bbox = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
        min_x, max_x = min(v.x for v in bbox), max(v.x for v in bbox)
        min_y, max_y = min(v.y for v in bbox), max(v.y for v in bbox)
        min_z = min(v.z for v in bbox)
        sx, sy = max_x - min_x, max_y - min_y

    item_data.append({
        "obj": obj,
        "sx": sx,
        "sy": sy,
        "cx_offset": (min_x + max_x) * 0.5,
        "cy_offset": (min_y + max_y) * 0.5,
        "min_z": min_z,
    })

item_data.sort(key=lambda d: d["sy"], reverse=True)

# 3. 2D Shelf Bin-Packing within [-bed_x/2, +bed_x/2] x [-bed_y/2, +bed_y/2]
pad = 5.0
cur_x = -bed_x * 0.5 + pad
cur_y = -bed_y * 0.5 + pad
row_h = 0.0
overflow_count = 0

for d in item_data:
    obj = d["obj"]
    sx, sy = d["sx"], d["sy"]

    if cur_x + sx + pad > (bed_x * 0.5):
        # Move to next Y shelf
        cur_x = -bed_x * 0.5 + pad
        cur_y += row_h + pad
        row_h = 0.0

    if cur_y + sy + pad > (bed_y * 0.5) or sx > (bed_x - pad * 2):
        overflow_count += 1

    target_cx = cur_x + sx * 0.5
    target_cy = cur_y + sy * 0.5

    # Place object so its bounding box center hits (target_cx, target_cy) and lowest vertex hits Z = 0.0
    obj.location.x = target_cx - d["cx_offset"]
    obj.location.y = target_cy - d["cy_offset"]
    obj.location.z = -d["min_z"]

    cur_x += sx + pad
    row_h = max(row_h, sy)

bpy.context.view_layer.update()
return (overflow_count == 0), overflow_count

=====================================================================

4. BLENDER PROPERTIES, OPERATORS & SIDEBAR UI

=====================================================================

class MODUDRONE_PG_Settings(PropertyGroup): bed_size: EnumProperty( name="Print Bed", description="Target 3D Printer Build Plate Size for capacity checks and auto-packing", items=[ ('BED_220', "220 x 220 mm (Standard)", "Standard 220x220mm build plate"), ('BED_250', "250 x 210 mm (MK3/MK4/Core1)", "Standard Prusa MK3S+/MK4S/Core One build plate"), ('BED_360', "360 x 360 mm (Prusa XL)", "Large-format 360x360mm multi-toolhead build plate"), ], default='BED_250', ) clearance: FloatProperty( name="Interlock Clearance (mm)", description="Global tolerance gap for 45-deg dovetail rails and tapered arm sockets", default=0.20, min=0.05, max=0.60, step=1, precision=2, ) fastener_mode: EnumProperty( name="M3 Fasteners", description="Choose between captive hex nut pockets or brass heat-set insert bosses", items=[ ('HEAT_INSERT', "Brass Heat-Set Inserts (4.1mm)", "Generates 4.1mm pilot pockets for M3x3x4.6 or M3x4x4.5 brass inserts"), ('HEX_NUT', "Captive M3 Hex Nuts (5.5mm)", "Generates 6-sided captive M3 nut traps—no soldering iron required"), ], default='HEAT_INSERT', ) include_tpu: BoolProperty( name="Generate TPU Damper Sub-Meshes", description="Spawns co-aligned TPU vibration-damping inserts for multi-material slicing", default=False, ) frame_type: EnumProperty( name="Frame Layout", items=[ ('DEADCAT', "Deadcat (Clear Front Sensor View)", "Sweeps front arms to 60 deg so propellers stay out of forward camera/sensor view"), ('TRUE_X', "True-X (Symmetric Lifter)", "Balanced 45-deg X geometry for pure vertical cargo lifting"), ], default='DEADCAT', ) prop_size: EnumProperty( name="Propeller Class", items=[ ('5', "5-Inch (Compact Utility)", "5-inch propeller clearance"), ('7', "7-Inch (Standard Heavy Lifter)", "Optimal 7-inch efficiency & payload lifter (2807 motor class)"), ('8', "8-Inch (Extended Endurance)", "8-inch propeller clearance"), ('10', "10-Inch (Max Cargo Lifter)", "10-inch heavy lifter"), ], default='7', ) fc_grid: EnumProperty( name="FC / ESC Stack Grid", items=[ ('30_5', "30.5 x 30.5 mm (SpeedyBee F405)", "Standard 30.5x30.5mm M3 stack mounting pattern"), ('20_0', "20.0 x 20.0 mm (Mini Stack)", "Compact 20x20mm M3 stack pattern"), ('BOTH', "Dual Grid (30.5 & 20.0 mm)", "Drills both mounting patterns into the core chassis"), ], default='30_5', ) motor_grid: EnumProperty( name="Motor Mount Grid", items=[ ('19_0', "19 x 19 mm (2807 Lifter Class)", "Standard M3x19mm bolt circle for 2807/2808 motors"), ('16_0', "16 x 16 mm (2207/2306 Class)", "Standard M3x16mm bolt circle"), ('UNIVERSAL', "Universal Slotted (16 to 19 mm)", "Slotted motor holes supporting both 16mm and 19mm motors"), ], default='UNIVERSAL', ) arm_style: EnumProperty( name="Arm Structure", items=[ ('ISO_TRUSS', "Triangular Iso-Truss (Lightweight)", "Internal diamond/truss cutouts for high stiffness-to-weight"), ('SOLID_TUBE', "Enclosed Aero Beam", "Solid walled beam with internal wire tunnel"), ], default='ISO_TRUSS', ) use_mr30_plug: BoolProperty( name="MR30 Blind-Mate Root Pockets", description="Carves internal Amass MR30 3-pin connector pockets into arm roots and chassis sockets", default=True, ) rail_width: FloatProperty( name="Dovetail Rail Width (mm)", description="Width of the 45-degree self-zeroing top and belly dovetail rails (38mm = Arca-Swiss standard)", default=38.0, min=28.0, max=50.0, ) payload_type: EnumProperty( name="Belly Payload Module", items=[ ('MG996R_CLEVIS', "Heavy Clevis Pin-Drop (MG996R 55g)", "Zero-load vertical clevis slot + MG996R standard servo bay"), ('MG90S_MICRO', "Micro Pendulum Hook (MG90S 9g)", "Lightweight 9g micro servo drop bay"), ('MOSFET_POD', "Solid-State MOSFET Trigger Pod", "Enclosed bay for Gravity MOSFET switch + 30x30mm bottom payload grid"), ('BLANK_PLATE', "Blank Parametric Utility Sled", "Flat dovetail sled with customizable X/Y M3 mounting grid"), ], default='MG996R_CLEVIS', ) include_bec_pocket: BoolProperty( name="Matek Micro BEC Pocket (19x16mm)", description="Carves a dedicated voltage regulator pocket into the lower core plate to prevent FC brownouts", default=True, ) custom_hole_x: FloatProperty( name="Custom Grid X (mm)", default=30.0, min=10.0, max=45.0, ) custom_hole_y: FloatProperty( name="Custom Grid Y (mm)", default=40.0, min=10.0, max=75.0, ) battery_sled_length: FloatProperty( name="Battery Sled Length (mm)", description="Length of top slide-in battery tray (135mm fits 6S 21700 Li-Ion packs)", default=135.0, min=90.0, max=180.0, ) auto_leg_height: BoolProperty( name="Auto-Calculate Leg Clearance", description="Automatically sizes landing gear height to Payload Module Depth + 35mm safety clearance", default=True, ) manual_leg_height: FloatProperty( name="Manual Leg Height (mm)", default=75.0, min=40.0, max=150.0, ) gps_mast_type: EnumProperty( name="Tail GPS & ELRS Mast", items=[ ('M10_21', "21 x 21 mm M10 GPS + ELRS Tubes", "Fits HGLRC M100-5883 & standard 21mm compass GPS modules"), ('MINI_18', "18 x 18 mm Mini GPS + ELRS Tubes", "Fits compact 18mm GPS modules"), ('NONE', "No Tail Mast", "Omit rear GPS pocket"), ], default='M10_21', ) spawn_mode: EnumProperty( name="Layout Mode", items=[ ('ASSEMBLED', "3D Assembled Flight Preview", "Places all modules in their assembled flight coordinates"), ('PLATED', "Print-Bed Plated (Z=0 & Packed)", "Rotates for zero-support printing, flattens to Z=0, and packs onto bed"), ], default='ASSEMBLED', ) spawn_all_four_arms: BoolProperty( name="Generate All 4 Arms & Legs", description="Uncheck to generate only 1 Arm and 1 Leg for single-part test printing", default=True, ) export_dir: StringProperty( name="Export Folder", subtype='DIR_PATH', default="//ModuDrone_STLs/", ) last_pack_status: StringProperty( name="Pack Status", default="Ready to generate.", )

class MODUDRONE_OT_GenerateSystem(Operator): bl_idname = "modudrone.generate_system" bl_label = "Generate ModuDrone System" bl_description = "Builds the complete parametric interlocking utility drone and payload system" bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
    props = context.scene.modudrone_props
    col = get_or_create_collection("ModuDrone_Parts")
    clear_collection("ModuDrone_Parts")

    core_bot = generate_core_bottom_plate(props, col)
    core_top = generate_core_top_plate(props, col)
    bat_sled = generate_battery_sled(props, col)
    belly_mod = generate_belly_payload_module(props, col)

    count = 4 if props.spawn_all_four_arms else 1
    arms = [generate_quick_swap_arm(props, col, i + 1) for i in range(count)]
    legs = [generate_landing_leg(props, col, i + 1) for i in range(count)]

    tpu_obj = generate_tpu_dampers(props, col) if props.include_tpu else None

    all_mesh_objs = [core_bot, core_top, bat_sled, belly_mod] + arms + legs
    if tpu_obj:
        all_mesh_objs.append(tpu_obj)

    if props.spawn_mode == 'ASSEMBLED':
        position_assembled_preview((core_bot, core_top, arms, bat_sled, belly_mod, legs, tpu_obj), props)
        props.last_pack_status = f"Assembled Preview Ready (Leg Clearance: {get_required_leg_height(props):.0f} mm)"
    else:
        ok, overflow = flatten_and_pack_to_bed(all_mesh_objs, props, col)
        if ok:
            props.last_pack_status = f"All {len(all_mesh_objs)} parts flattened to Z=0 & packed inside bed!"
        else:
            props.last_pack_status = f"Note: {overflow} part(s) exceeded single-plate capacity (placed in overflow row)."
            self.report({'WARNING'}, props.last_pack_status)

    return {'FINISHED'}

class MODUDRONE_OT_TogglePlatedLayout(Operator): bl_idname = "modudrone.toggle_layout" bl_label = "Switch Assembled / Print-Bed Plated" bl_description = "Toggles between 3D flight assembly and Z=0 flattened print-bed layout" bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
    props = context.scene.modudrone_props
    props.spawn_mode = 'PLATED' if props.spawn_mode == 'ASSEMBLED' else 'ASSEMBLED'
    bpy.ops.modudrone.generate_system()
    return {'FINISHED'}

class MODUDRONE_OT_BatchExportSTL(Operator): bl_idname = "modudrone.batch_export_stl" bl_label = "Flatten to Z=0 & Batch Export STLs" bl_description = "Automatically orients each part for zero-support printing, drops lowest vertex to Z=0, and exports individual STLs"

def execute(self, context):
    props = context.scene.modudrone_props
    if "ModuDrone_Parts" not in bpy.data.collections:
        bpy.ops.modudrone.generate_system()

    col = bpy.data.collections["ModuDrone_Parts"]
    mesh_objs = [o for o in col.objects if o.type == 'MESH' and not o.name.startswith("00_")]

    # Ensure Plated Z=0 orientation before exporting
    was_assembled = (props.spawn_mode == 'ASSEMBLED')
    if was_assembled:
        props.spawn_mode = 'PLATED'
        bpy.ops.modudrone.generate_system()
        mesh_objs = [o for o in col.objects if o.type == 'MESH' and not o.name.startswith("00_")]

    out_dir = bpy.path.abspath(props.export_dir)
    os.makedirs(out_dir, exist_ok=True)

    exported = 0
    for obj in mesh_objs:
        # Deduplicate identical arms/legs during export if all 4 are spawned
        if obj.name.endswith(("_2", "_3", "_4")):
            continue
        bpy.ops.object.select_all(action='DESELECT')
        obj.select_set(True)
        context.view_layer.objects.active = obj

        filepath = os.path.join(out_dir, f"{obj.name}.stl")
        # Blender 4.2 / 5.0+ uses bpy.ops.wm.stl_export
        if hasattr(bpy.ops.wm, "stl_export"):
            bpy.ops.wm.stl_export(filepath=filepath, export_selected_objects=True)
        else:
            bpy.ops.export_mesh.stl(filepath=filepath, use_selection=True)
        exported += 1

    self.report({'INFO'}, f"Exported {exported} watertight STL modules to {out_dir}")
    return {'FINISHED'}

class MODUDRONE_PT_MainPanel(Panel): bl_label = "ModuDrone Utility System" bl_idname = "MODUDRONE_PT_main_panel" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'ModuDrone'

def draw(self, context):
    layout = self.layout
    props = context.scene.modudrone_props

    # 1. Print Bed & Tolerances
    box = layout.box()
    box.label(text="1. Print Bed & Tolerances", icon='MOD_BUILD')
    box.prop(props, "bed_size")
    box.prop(props, "clearance", slider=True)
    box.prop(props, "fastener_mode")
    box.prop(props, "include_tpu")

    # 2. Core Chassis & Propulsion
    box = layout.box()
    box.label(text="2. Core & Quick-Swap Arms", icon='PROP_ON')
    box.prop(props, "frame_type")
    box.prop(props, "prop_size")
    box.prop(props, "fc_grid")
    box.prop(props, "motor_grid")
    box.prop(props, "arm_style")
    box.prop(props, "use_mr30_plug")

    # 3. Belly Payload & Battery Rail
    box = layout.box()
    box.label(text="3. 45° Belly Payload & Battery Rail", icon='MOD_ARRAY')
    box.prop(props, "rail_width")
    box.prop(props, "payload_type")
    if props.payload_type == 'BLANK_PLATE':
        row = box.row(align=True)
        row.prop(props, "custom_hole_x")
        row.prop(props, "custom_hole_y")
    box.prop(props, "include_bec_pocket")
    box.prop(props, "battery_sled_length")

    # 4. Auto-Clearance Landing Gear & GPS
    box = layout.box()
    box.label(text="4. Landing Gear & Navigation", icon='CON_FLOOR')
    box.prop(props, "auto_leg_height")
    if not props.auto_leg_height:
        box.prop(props, "manual_leg_height")
    else:
        box.label(text=f"Computed Leg Height: {get_required_leg_height(props):.1f} mm", icon='INFO')
    box.prop(props, "gps_mast_type")

    # 5. Generation & Batch Export
    box = layout.box()
    box.label(text="5. Build, Pack & Export", icon='EXPORT')
    box.prop(props, "spawn_mode")
    box.prop(props, "spawn_all_four_arms")

    col = box.column(align=True)
    col.scale_y = 1.4
    col.operator("modudrone.generate_system", icon='MESH_ICOSPHERE')
    col.operator("modudrone.toggle_layout", icon='UV_SYNC_SELECT')

    box.separator()
    box.prop(props, "export_dir")
    box.operator("modudrone.batch_export_stl", icon='FILE_TICK')
    box.label(text=props.last_pack_status, icon='CHECKMARK')

=====================================================================

5. REGISTRATION

=====================================================================

classes = ( MODUDRONE_PG_Settings, MODUDRONE_OT_GenerateSystem, MODUDRONE_OT_TogglePlatedLayout, MODUDRONE_OT_BatchExportSTL, MODUDRONE_PT_MainPanel, )

def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.modudrone_props = PointerProperty(type=MODUDRONE_PG_Settings)

def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) del bpy.types.Scene.modudrone_props

if name == "main": register()

Tagi