1 / 3
Fully Customizable Drone System with Selectable Components : Blender Script. — image 1
1 / 3

Fully Customizable Drone System with Selectable Components : Blender Script.

Order a print of this model

We calculate the final price from your selected file. You review and accept it before payment.

Saved on this device.

Fully Customizable Drone System with Selectable Components : Blender Script.

Rating
0 / 5
Likes
3
Downloads
6
Makes
0
Views
55
Reviews
0

Updated at source:

Model information

License
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()

Tags