Hi,
I often find myself exporting a STL file for 3D printing, importing it into the slicer, discovering something that has to change in the file, re-exporting, importing and so on.
To make this a bit faster I made a python-script that someone might find useful as well:
You can set your export settings and a output directory inside the script. When you export then your file, the script will try to figure out if the object has been exported before and if so it will overwrite the previous file. Then you only have to select reload STL
in the slicer to update it. If it’s a file that hasn’t been exported before it will make a new file based on the filename of the rhino file and the location of the object in the workspace.
When naming ang “tracking” exported objects, the script uses the projected bounding box in centimeters. If a newly exported object’s center point falls within the boundaries of an existing box, the script treats it as the same object. While this approach generally works, it can create confusion when you’re exporting both large assemblies and their individual parts, since their bounding boxes may overlap with each other.
You can run this script form a button or with a shortcut like so:
!_-RunPythonScript "C:\Filepath\export_STL.py"
cheers
export_STL.py (5.9 KB)
import rhinoscriptsyntax as rs
import System.Guid
import math
import os
# Configuration
EXPORT_DIR = r"C:\Filepath"
# STL Mesh export settings from Rhino dialog
MESH_DENSITY = 1 # Mesh density
MESH_ANGLE = 5.0 # Maximum angle between mesh faces
MESH_ASPECT = 4.0 # Maximum aspect ratio
MESH_MIN_EDGE = 0.0001 # Minimum edge length
MESH_MAX_EDGE = 0 # Maximum edge length
MESH_EDGE_DISTANCE = 0.008 # Maximum edge to surface distance
MESH_MIN_GRID = 0 # Minimum initial grid quads
MESH_FLAGS = "_RefineGrid=Yes _SimplePlanes=Yes _PackTextures=Yes"
MESH_SETTINGS = "_MeshQuality {} _Density {} _MaxAngle {} _AspectRatio {} _MinEdgeLength {} _MaxEdgeLength {} _RefineGrid _SimplePlanes _PackTextures _Enter".format(
"Custom",
MESH_DENSITY,
MESH_ANGLE,
MESH_ASPECT,
MESH_MIN_EDGE,
MESH_MAX_EDGE)
def ensure_export_dir():
"""Ensure export directory exists"""
if not os.path.exists(EXPORT_DIR):
os.makedirs(EXPORT_DIR)
def get_projected_bounds():
"""Get the projected bounds of all selected objects"""
selected_objects = rs.SelectedObjects()
if not selected_objects:
return None
bbox = rs.BoundingBox(selected_objects)
if not bbox:
return None
#convert to cm
min_x = min(pt.X for pt in bbox)/10
max_x = max(pt.X for pt in bbox)/10
min_y = min(pt.Y for pt in bbox)/10
max_y = max(pt.Y for pt in bbox)/10
# outward rounding
min_x = int(math.ceil(min_x) if min_x < 0 else math.floor(min_x))
max_x = int(math.floor(max_x) if max_x < 0 else math.ceil(max_x))
min_y = int(math.ceil(min_y) if min_y < 0 else math.floor(min_y))
max_y = int(math.floor(max_y) if max_y < 0 else math.ceil(max_y))
return min_x, min_y, max_x, max_y
def format_coordinate(value):
"""Format coordinate with P/N prefix, treating 0 as positive"""
value = int(value) # Ensure integer value
if value >= 0:
return "P{}".format(value)
return "N{}".format(abs(value))
def construct_filename(base_name, bounds):
"""Construct filename with coded coordinates"""
min_x, min_y, max_x, max_y = bounds
coords = [format_coordinate(c) for c in [min_x, min_y, max_x, max_y]]
return "{}_{}.stl".format(base_name, ''.join(coords))
def export_stl(filepath):
"""Export STL with predefined mesh settings without prompts"""
cmd = '-_NoEcho -_Export "{}" _STL _UseSimpleDialog=_No _Enter _DetailedOptions _Density={} _GridAngle={} _AspectRatio={} _MinEdgeLen={} _MaxEdgeLen={} _MaxEdgeSrf={} _PackTextures _SimplePlanes _RefineGrid _Enter'.format(
filepath,
MESH_DENSITY,
MESH_ANGLE,
MESH_ASPECT,
MESH_MIN_EDGE,
MESH_MAX_EDGE,
MESH_EDGE_DISTANCE
)
rs.Command(cmd, False) # False to suppress command line history
def point_in_bounds(point, bounds):
"""Check if point lies within bounds"""
if not bounds or len(bounds) != 4:
return False
min_x, min_y, max_x, max_y = bounds
return (min_x <= point[0] <= max_x and
min_y <= point[1] <= max_y)
def get_center_point(bounds):
"""Get center point of bounds"""
min_x, min_y, max_x, max_y = bounds
return ((min_x + max_x) / 2, (min_y + max_y) / 2)
def parse_filename_bounds(filename):
"""Parse bounds from filename format like 'rhinofile_P3P10N3N5.stl'
Returns (min_x, min_y, max_x, max_y)"""
try:
# Get just the filename without path
filename = os.path.basename(filename)
# Split filename and get the coordinate part
coords_part = filename.rsplit('.', 1)[0].split('_')[-1]
# Initialize variables
coords = []
current_num = ""
sign = 1
# Parse the coordinate string
for char in coords_part:
if char == 'P':
if current_num:
coords.append(sign * int(current_num))
sign = 1
current_num = ""
elif char == 'N':
if current_num:
coords.append(sign * int(current_num))
sign = -1
current_num = ""
else: # must be a digit
current_num += char
# Add the last number
if current_num:
coords.append(sign * int(current_num))
# Verify we got exactly 4 coordinates
if len(coords) != 4:
raise ValueError("Invalid coordinate format")
return tuple(coords)
except Exception as e:
print("Error parsing filename: {}".format(e))
return None
def main():
ensure_export_dir()
bounds = get_projected_bounds()
if not bounds:
print("No objects selected")
return
# Get base filename from current Rhino file
doc_name = rs.DocumentName()
if doc_name:
base_name = doc_name.rsplit('.', 1)[0]
else:
base_name = "export" # Default name if document name not available
print("Warning: Could not get document name, using 'export' as base name")
# Construct new filename
filename = construct_filename(base_name, bounds)
filepath = os.path.join(EXPORT_DIR, filename)
# Get center point of current bounds
center = get_center_point(bounds)
# Check for existing files and handle export
if os.path.exists(filepath):
old_bounds = parse_filename_bounds(filename)
if old_bounds and point_in_bounds(center, old_bounds):
export_stl(filepath) # Overwrite
print("Overwritten {}".format(filename))
else:
export_stl(filepath) # New file
print("Exported {}".format(filename))
else:
export_stl(filepath) # New file
print("Exported {}".format(filename))
if __name__ == '__main__':
main()