My model folder got out of hand. If you print much you know the shape of it: STL, 3MF, STEP, OBJ, PLY and ZIP files piling up faster than you can name anything sensibly, until finding one thing means opening six.
So I wrote something. It opens a folder, previews any model in it, and files everything into category folders on its own. All of it runs locally.
Now, "categorize this automatically" usually means "call an LLM" these days. I didn't want to. An API key means a per-user cost, a network round trip, and a tool that quits working the moment you're on a plane. So the classifier inside the MakerX 3D File Sorter is a few hundred lines of ordinary JavaScript instead. It's on GitHub if you'd rather just read the code.
What's in it
Electron 28 for the shell, packaged to a Windows NSIS installer with electron-builder. Three.js for the preview. occt-import-js, which is OpenCascade compiled to WebAssembly, for STEP. adm-zip and JSZip for archives.
The renderer is plain HTML, CSS and JavaScript. No React, no build step. Three files carry it: app.js does the viewer and the file list, auto-sort.js is the classifier, and style.css is everything else.
All the filesystem work happens in the main process. The renderer reaches it through a contextIsolation preload that exposes a deliberately narrow window.electronAPI: scanFolder, peekZip, createDir, move, and not much more. It never touches fs itself.
That last bit is worth a sentence on its own. There's no server anywhere in this thing, so "your files never leave your machine" isn't a promise you have to take my word on. There's nowhere for them to go.
Five formats, one geometry
Everything gets normalized into a single THREE.BufferGeometry. After that, the bounding box, the volume, the print estimate and the color picker don't have to care what the file started life as.
STL I parse by hand, binary and ASCII both, straight into interleaved position and normal buffers. The format is simple enough that pulling in a dependency felt like overkill.
3MF is where the time went. It's a ZIP full of XML, and what slicers actually export doesn't always match the spec, because Bambu Studio and OrcaSlicer both have opinions. I unzip in process, parse the mesh XML, then apply each object's build transform.
Watch that transform matrix. It's row-major. Load it column-major and your model turns up mirrored or rotated, and it looks plausible enough that the matrix won't be your first suspect.
OBJ and PLY landed in v2.2, so Blender exports and scanner output preview and sort like everything else.
STEP you can't hand-parse. It's real B-rep CAD. I lazy-init occt-import-js and tessellate from there, so the WASM engine only loads the first time somebody actually opens a STEP file. Startup stays quick for all the sessions where nobody does.
The viewer reads GCODE into a toolpath preview too. That one wasn't planned and turned out to be handy.
From the mesh volume I work out a print estimate. Filament length is volume / (π · r²) with r = 0.875 mm for 1.75 mm filament, and weight and cost fall out of that once you set density, infill and price. Don't mistake it for a slicer. It's a gut check before you commit to an overnight print.
The classifier
autoClassify(fileName) is boring on purpose. Deterministic, offline, no training.
First it normalizes the name. Drop the extension, lowercase, split on camelCase and on letter/number boundaries, then accent-fold through NFD decomposition so dragón becomes dragon and küche becomes kuche. That folds Cyrillic combining marks as well, so the keyword tables are stored pre-folded to match.
Then it scores. Every category owns a list of keyword phrases. A whole-word regex hit is worth keyword.length × 2. A substring hit counts for keyword.length, but only at six characters or more, because otherwise "carro" matches "carrot" and you get a Spanish car filed with the vegetables. Longer phrases win naturally, so "baby yoda" beats whatever generic token is sitting next to it in the same filename.
The tables are multilingual, and that mattered more than I expected. Downloaded models are named in every language there is. Spanish, Portuguese, French, German, Italian, Dutch, Polish, Czech, Russian, Ukrainian and Chinese terms all merge into the same categories, so gato, chat, katze, gatto and 猫 all land in Pets.
Here's the rule that actually made the results usable, though: form-factor beats theme.
Categories split two ways. Form-factor is what the object is or does, like Keychains, Storage, Lamps. Theme is what it looks like: Halloween, Dragons, Star Wars. When a filename hits both, form-factor takes it. "Halloween Skull Keychain" goes to Keychains. But "Easter Egg" still goes to Easter, because nothing functional is competing for it.
Brand names get ignored on purpose. Marketplace filenames are stuffed with [Creality Cloud], Bambu, Prusa and MakerWorld, and matching any of those would dump half your library into printer parts. They're stopwords. The printer-parts category only fires on real part words: nozzle, hotend, spool holder.
Whatever doesn't match goes to an Unsorted bucket, and the app mines that pile afterward. If a meaningful token turns up across five or more files, and it isn't a stopword and isn't already a keyword, you get offered it as a new category. Approve it and it's saved to localStorage and reused next run. Mute it and it stays quiet for good. No training involved, just counting what's in front of you.
The upshot is a classifier that runs instantly over thousands of files, costs nothing because there's no inference bill, and works with the wifi off. The catch is real though. It's keyword-based, so a cryptically named file will defeat it every time. That's exactly what Unsorted and the learn-a-group loop are for. You close the gap, and the app remembers.
Sort Check
A one-shot sorter doesn't help much if you already have years of hand-filed folders, which I did.
Sort Check walks a sorted tree recursively, runs the same classifier over every file, and flags anything whose predicted category doesn't match the folder it's actually sitting in. You review the flags full screen with a live 3D preview and either move the file or mark it correctly placed. Every decision goes into IndexedDB keyed on a name|size signature, so a later audit never asks you about it twice.
It also prefers moving a file into a folder that already exists, matched case-insensitively, rather than spawning a new one. Otherwise you end up with Keychains and KEYCHAINS sitting next to each other, which is its own kind of mess.
About that unsigned installer
One honest note for anyone else shipping a free Electron app.
A Microsoft-trusted code-signing certificate costs money every year, and that's hard to justify for something you're giving away. So the installer is unsigned, and Windows SmartScreen greets every single user with "Windows protected your PC."
I went the other direction on it. The source is public, the download page spells out the More info, then Run anyway step instead of pretending it doesn't happen, and anyone wary can read exactly what the app does before running it. Transparency in place of a certificate. Not ideal, but it's the honest version.
If you're building something similar
Normalizing wildly different formats into one geometry type early pays for itself over and over. Assume row-major on slicer-exported 3MF. Lazy-load your WASM engine when most files won't need it.
And on the classifier: automatic categorization doesn't have to mean an LLM. A deterministic multilingual keyword scorer, one good tie-break rule, and a way to learn from what it missed got me most of the way there for none of the cost. Better yet, I can debug it when it's wrong.
The app is free and Windows only: makerxdesigns.com/utilities/3d-file-sorter.html. Source is at github.com/jrf1969-creator/makerx-3d-file-sorter.
Top comments (0)