Fully Customizable Drone System with Selectable Components : Blender Script.
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()