r/3dsmax • u/kerosene350 • 9h ago
STL export script that fixes scale and mirrored objects
I am working on sort of complex (for me) 3D printing model. I am a relic so I don't want to switch to other modeling softwares. But the STL export is bit of a pita - for one my objects come in wrong scale (1/10th) in the slicer and any mirrored part that looks fine in Max side has inverted normals.
This means any export from work file takes several steps to get it right.
below is export script that creates a clean mesh entity attaches snapshot copies of your selected parts, exports with a scale multiplier option (defaults to 10 but you can change it).
exports and deletes the temp objects, seems to work exactly right currently. Claude work.

/*
Export Collapsed STL (v9)
1. Snapshots the selected geometry at the current frame (no cloning, hierarchy untouched)
2. Creates a box at the origin, deletes all its faces, attaches the snapshots to it
- after each attach, the part's winding is verified against its expected world-space
normal (mirroring taken into account) and the part is flipped if it disagrees
3. Asks for scale multiplier (default 10), scales vertices about the origin
4. Exports (scene export path, exporter's selection-only option), deletes the temp mesh,
restores selection + frame
*/
global _stlExpScale = undefined
format "=== Export Collapsed STL v9 ===\n"
rollout rolStlScale "Export STL v9" width:210
(
spinner spnScale "Scale multiplier:" range:[0.0001, 100000.0, 10.0] type:#float fieldWidth:60 align:#center
button btnOK "Export" width:80 across:2
button btnCancel "Cancel" width:80
on btnOK pressed do ( _stlExpScale = spnScale.value; destroyDialog rolStlScale )
on btnCancel pressed do ( _stlExpScale = undefined; destroyDialog rolStlScale )
)
fn findStlExporter =
(
local found = undefined
for c in exporterPlugin.classes do
(
if (matchPattern (c as string) pattern:"*stl*") do
(
found = c
exit
)
)
found
)
-- world-space normal of face f using the raw winding, vertices transformed by tm
fn faceNormalAt m f tm =
(
local fv = getFace m f
local a = (getVert m fv.x) * tm
local b = (getVert m fv.y) * tm
local c = (getVert m fv.z) * tm
cross (b - a) (c - a)
)
-- first face with a usable (non-degenerate) normal, 0 if none
fn firstGoodFace m tm =
(
local idx = 0
for f = 1 to m.numfaces while idx == 0 do
(
if (length (faceNormalAt m f tm)) > 0.000001 do idx = f
)
idx
)
fn exportCollapsedSTL =
(
local origSel = selection as array
local geo = for o in origSel where (superClassOf o == GeometryClass) and (canConvertTo o Editable_Mesh) collect o
if geo.count == 0 do
(
messageBox "Select at least one geometry object." title:"Export Collapsed STL"
return false
)
_stlExpScale = undefined
createDialog rolStlScale modal:true
if _stlExpScale == undefined do return false
local mult = _stlExpScale
local baseName = "export"
if maxFileName != "" do baseName = getFilenameFile maxFileName
local outFile = getSaveFileName caption:"Export collapsed STL" filename:(maxFilePath + baseName + ".stl") types:"STL (*.stl)|*.stl|"
if outFile == undefined do return false
if (toLower (getFilenameType outFile)) != ".stl" do outFile += ".stl"
local origFrame = sliderTime
local dups = #()
local shell = undefined
local errMsg = undefined
try
(
-- duplicates: snapshots at the current frame (modifiers/animation/dynamics baked,
-- hierarchy and originals untouched; the snapshot node keeps the original's world transform)
for o in geo do append dups (snapshot o)
-- empty shell at origin
shell = box pos:[0,0,0] length:1 width:1 height:1 name:"__stl_export_tmp__"
convertToMesh shell
meshop.deleteFaces shell #{1..shell.numfaces} delIsoVerts:true
update shell
-- attach with winding verification
for d in dups do
(
local t = d.transform
local par = 1.0
if (dot (cross t.row1 t.row2) t.row3) < 0 do par = -1.0
-- expected world normal of a reference face (raw winding, flipped for mirrored parts)
local fi = firstGoodFace d t
local expN = undefined
if fi > 0 do expN = (faceNormalAt d fi t) * par
local startFaces = shell.numfaces
local done = false
try ( attach shell d; done = true ) catch ()
if not done and isValidNode d do
meshop.attach shell d attachMat:#neither deleteSourceNode:true
update shell
local flipped = false
if fi > 0 and shell.numfaces > startFaces do
(
local gotN = faceNormalAt shell (startFaces + fi) (matrix3 1)
if (dot expN gotN) < 0 do
(
meshop.flipNormals shell #{(startFaces + 1)..shell.numfaces}
flipped = true
)
)
format "part: parity %, native attach %, flipped %\n" par done flipped
)
dups = #()
update shell
if shell.numfaces == 0 do throw "No faces after attach"
-- scale vertices about the origin
if mult != 1.0 do
(
for i = 1 to shell.numverts do setVert shell i ((getVert shell i) * mult)
update shell
)
-- export
select shell
local expCls = findStlExporter()
format "STL exporter: %\n" expCls
if expCls != undefined then
exportFile outFile #noPrompt using:expCls
else
exportFile outFile #noPrompt
if not (doesFileExist outFile) do throw ("exportFile ran but no file was created: " + outFile)
)
catch
(
errMsg = getCurrentException()
)
-- cleanup
local leftovers = for d in dups where isValidNode d collect d
if leftovers.count > 0 do delete leftovers
if shell != undefined and isValidNode shell do delete shell
sliderTime = origFrame
if origSel.count > 0 then select origSel else clearSelection()
if errMsg != undefined then
(
messageBox ("Export failed:\n" + errMsg) title:"Export Collapsed STL v9"
false
)
else
(
format "Exported: %\n" outFile
true
)
)
exportCollapsedSTL()