Web-based SLA slicing application powered by PrusaSlicer CLI (headless). Features a React frontend with 3D preview, configurable slicing parameters, and support mesh visualization.
- SLA Slicing: Slice STL models into layer images using PrusaSlicer engine
- 3D Preview: Interactive Three.js viewer with orbit controls (Z-up coordinate system)
- Support Generation: Auto-generate supports with configurable parameters
- Support Mesh Export: Download generated supports as STL for external use
- Hollow Interior Generation: Generate hollow interior mesh for visualization (NEW)
- Configurable Parameters: Layer height, exposure times, support settings, pad options, hollowing
- Layer Navigation: Browse through sliced layers with slider control
- macOS (tested on macOS 15.x)
- Python 3.9+
- Node.js 18+
- PrusaSlicer Fork (with
--export-support-stlfeature)
The project uses a custom PrusaSlicer fork with support mesh STL export capability.
# Build the fork (includes --export-support-stl feature)
./scripts/build_prusaslicer_fork_macos.shThis builds the binary at third_party/prusaslicer_build/src/prusa-slicer.
# Set the path to the forked binary
export PRUSA_SLICER_BIN=$(pwd)/third_party/prusaslicer_build/src/prusa-slicer
# Start the agent
./scripts/run_agent.shBackend runs at http://127.0.0.1:5179
cd web
npm install
npm run devFrontend runs at http://localhost:5174
| Interface | URL | Description |
|---|---|---|
| React UI | http://localhost:5174 | Main frontend - slicing, preview, supports, hollow |
| Boolean Test | http://localhost:5179/test/boolean | Experimental boolean operations test page |
| API Docs | http://localhost:5179/docs | Swagger UI for API exploration |
- Select STL File: Click file input to select a model - 3D preview appears immediately
- Configure Settings (optional): Expand "Slicing Config" to adjust parameters
- Layer height: 0.025mm, 0.05mm, or 0.1mm
- Exposure time: 1-30 seconds
- Initial exposure: 5-60 seconds
- Enable/disable supports with detailed settings
- Enable/disable pad
- Slice: Click "Slice" button to start processing
- View Results:
- 3D Preview: Model (blue) with support mesh overlay (red) if supports enabled
- Layer View: Navigate through individual slice images
- Download Support STL: When supports are generated, download button appears
| Parameter | Description | Range | Default |
|---|---|---|---|
layer_height |
Height of each slice layer | 0.025, 0.05, 0.1 mm | 0.05 mm |
exposure_time |
UV exposure per layer | 1-30 s | 10 s |
initial_exposure_time |
First layers exposure | 5-60 s | 15 s |
supports_enable |
Generate support structures | on/off | off |
support_head_front_diameter |
Support tip diameter | 0.2-1.0 mm | 0.4 mm |
support_head_penetration |
Tip penetration depth | 0.1-0.5 mm | 0.2 mm |
support_pillar_diameter |
Support pillar width | 0.5-2.0 mm | 1.0 mm |
support_points_density_relative |
Support density | 50-200% | 100% |
pad_enable |
Generate base pad | on/off | off |
hollowing_enable |
Enable hollowing | on/off | off |
hollowing_min_thickness |
Wall thickness | 0.5-10 mm | 3.0 mm |
hollowing_quality |
Voxel quality (higher = finer) | 0.1-1.0 | 0.5 |
hollowing_closing_distance |
Smoothing distance | 0-10 mm | 2.0 mm |
curl http://127.0.0.1:5179/# Basic (default config)
curl -X POST http://127.0.0.1:5179/api/jobs \
-F "file=@model.stl"
# With custom config
curl -X POST http://127.0.0.1:5179/api/jobs \
-F "file=@model.stl" \
-F 'config={"layer_height":0.05,"supports_enable":true,"exposure_time":12}'Response:
{"job_id": "a1b2c3d4", "status": "pending"}curl http://127.0.0.1:5179/api/jobs/{job_id}Response:
{
"job_id": "a1b2c3d4",
"status": "completed",
"layer_count": 750,
"error": null,
"has_support_mesh": true
}curl http://127.0.0.1:5179/api/jobs/{job_id}/layers/50.png --output layer50.pngcurl http://127.0.0.1:5179/api/jobs/{job_id}/model.stl --output model.stlcurl http://127.0.0.1:5179/api/jobs/{job_id}/support.stl --output support.stlReturns combined mesh of supports and pad (if enabled). Only available when has_support_mesh: true in job status.
# 1. Create job with hollow config
curl -X POST http://127.0.0.1:5179/api/v2/slices \
-H "Content-Type: application/json" \
-d '{"config": {"hollowing_enable": true, "hollowing_min_thickness": 2.0}}'
# 2. Upload model
curl -X POST http://127.0.0.1:5179/api/v2/slices/{job_id}/upload \
-F "file=@model.stl"
# 3. Generate hollow interior
curl -X POST http://127.0.0.1:5179/api/v2/slices/{job_id}/generate-hollow
# 4. Poll status until completed
curl http://127.0.0.1:5179/api/v2/slices/{job_id}
# Response: {"data": {"status": "completed", "hasHollowMesh": true}}
# 5. Download hollow mesh
curl http://127.0.0.1:5179/api/jobs/{job_id}/hollow.stl --output hollow.stlcurl http://127.0.0.1:5179/api/jobs/{job_id}/hollow.stl --output hollow.stlReturns the hollow interior mesh. Only available when has_hollow_mesh: true in job status.
The backend supports multiple frontends through versioned API endpoints:
┌─────────────────────┐ ┌─────────────────────┐
│ DS-Online (Vue) │ │ web_slicer_core │
│ Dental Slicer UI │ │ (React) Basic UI │
│ :5173 │ │ :5174 │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
│ /api/v2/slices │ /api/jobs
│ │
▼ ▼
┌─────────────────────────────────────────────────┐
│ FastAPI Backend (:5179) │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ /api/v2/slices │ │ /api/jobs (v1) │ │
│ │ DS-Online API │ │ Original API │ │
│ └────────┬────────┘ └──────────┬──────────┘ │
│ └────────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Job Manager │ │
│ │ (shared service) │ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ PrusaSlicer CLI │ │
│ │ (Fork + support │ │
│ │ mesh export) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ Job Storage │
│ agent/jobs/ │
└─────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ UI Layer │
│ - DS-Online (Vue + Three.js + PrimeVue) │
│ - web_slicer_core (React + Three.js) │
├─────────────────────────────────────────────────────────────┤
│ API Layer (FastAPI) │
│ - /api/jobs/* (v1 - original) │
│ - /api/v2/slices/* (v2 - DS-Online compatible) │
├─────────────────────────────────────────────────────────────┤
│ Service Layer │
│ - Job Manager (create, status, polling) │
│ - Config Manager (INI generation, validation) │
├─────────────────────────────────────────────────────────────┤
│ Engine Layer │
│ - PrusaSlicer CLI Adapter │
│ - Support mesh export (--export-support-stl) │
│ - Hollow interior export (--export-hollow-stl) │
└─────────────────────────────────────────────────────────────┘
web_slicer_core/
├── agent/
│ ├── main.py # FastAPI application & endpoints
│ ├── config.py # Configuration constants
│ ├── models.py # Pydantic models (SLAConfig, JobStatus)
│ ├── jobs.py # Job management & slicing logic
│ └── jobs/ # Job data storage (gitignored)
│ └── {job_id}/
│ ├── input/model.stl
│ ├── output/
│ │ ├── model.sl1
│ │ └── model_support.stl # If supports enabled
│ ├── layers/{0..N}.png
│ ├── config.ini
│ └── status.json
├── web/
│ ├── src/
│ │ ├── App.tsx # Main application component
│ │ ├── App.css # Styles
│ │ ├── STLViewer.tsx # Three.js 3D viewer component
│ │ └── main.tsx # Entry point
│ ├── package.json
│ └── vite.config.ts
├── third_party/
│ ├── prusaslicer_fork/ # PrusaSlicer fork (submodule)
│ └── prusaslicer_build/ # Build output (gitignored)
├── scripts/
│ ├── run_agent.sh
│ └── build_prusaslicer_fork_macos.sh
├── requirements.txt
└── README.md
This project uses a custom fork of PrusaSlicer (github.com:MaxShih147/PrusaSlicer.git) with additional CLI options:
Exports the generated support and pad meshes as a combined STL file after SLA slicing.
prusa-slicer --export-sla --export-support-stl -o output.sl1 model.stl
# Creates: output.sl1 and model_support.stlThe exported STL includes:
- Support structures (if
supports_enable = true) - Pad/raft (if
pad_enable = true)
Implementation details:
- Added in
src/libslic3r/PrintConfig.cpp(CLI option definition) - Export logic in
src/CLI/ProcessActions.cpp - Uses
SLAPrintObject::support_mesh()andSLAPrintObject::pad_mesh()
Generates and exports the hollow interior mesh as STL. This is a standalone operation that doesn't require full slicing.
prusa-slicer --export-hollow-stl \
--hollowing-min-thickness 2 \
--hollowing-quality 0.5 \
--hollowing-closing-distance 1 \
-o interior.stl model.stl
# Creates: interior.stl (hollow interior mesh only)Parameters:
--hollowing-min-thickness: Wall thickness in mm (default: 2.0)--hollowing-quality: Voxel quality 0.1-1.0 (default: 0.5, higher = finer detail)--hollowing-closing-distance: Morphological closing distance in mm (default: 0.5)
Important notes:
- Wall thickness must be appropriate for model size (small models need thinner walls)
- The interior mesh has flipped normals for proper visualization
- Uses OpenVDB for voxelization and interior generation
Implementation details:
- CLI option defined in
src/libslic3r/PrintConfig.cpp - Handler in
src/CLI/ProcessActions.cpp - Uses
sla::generate_interior()fromlibslic3r/SLA/Hollowing.hpp - Normals flipped via
sla::swap_normals()for visualization
The DS-Online frontend (paramsStore.bedSize) and the PrusaSlicer backend use separate, unconnected parameter sets. When slicing via backend CLI, generate_config_ini() in agent/sla_operations.py writes only slicing parameters (layer_height, exposure, supports, hollowing) to the config INI — no printer/display parameters are included. PrusaSlicer therefore falls back to its hardcoded defaults.
This matters for any feature that needs to map between PNG pixel coordinates and real-world mm coordinates (e.g., island detection 3D overlay, cross-section alignment).
Source: third_party/prusaslicer_fork/src/libslic3r/PrintConfig.cpp (lines ~4272-4295)
| Parameter | Default Value | Description |
|---|---|---|
display_width |
120.0 mm | Physical display width |
display_height |
68.0 mm | Physical display height |
display_pixels_x |
2560 | Horizontal pixel count |
display_pixels_y |
1440 | Vertical pixel count |
display_orientation |
portrait | Display rotation |
These defaults closely match the Original Prusa SL1 printer profile.
Profiles are stored in third_party/prusaslicer_build/resources/profiles/:
| Printer | display_width | display_height | pixels_x | pixels_y | orientation |
|---|---|---|---|---|---|
| Prusa SL1 | 120.96 mm | 68.04 mm | 2560 | 1440 | portrait |
| Prusa SL1S SPEED | 128.00 mm | 81.00 mm | 2560 | 1620 | portrait |
| Anycubic Photon Mono | (see AnycubicSLA.ini) | ||||
| Anycubic Photon Mono X | (see AnycubicSLA.ini) | ||||
| Anycubic Photon Mono X 6K | (see AnycubicSLA.ini) |
Output PNGs are 1440 x 2560 pixels (width x height). With display_orientation = portrait, the pixel layout is rotated 90 degrees from the physical display:
PNG width (1440 px) → physical short axis → 68.0 mm (display_height)
PNG height (2560 px) → physical long axis → 120.0 mm (display_width)
Pixel-to-mm conversion:
world_x = (px / png_width) * display_height // 68.0 mm
world_y = (py / png_height) * display_width // 120.0 mm
Note: PrusaSlicer auto-centers the model on the bed and auto-drops it to Z=0. The model's position in the PNG is relative to the bed center.
Frontend config generate_config_ini() PrusaSlicer CLI
(backendSlicer.js) --> (sla_operations.py) --> (--load config.ini)
Writes: Reads config.ini +
- layer_height Falls back to defaults for:
- exposure_time - display_width (120.0)
- supports_enable - display_height (68.0)
- hollowing_* - display_pixels_x (2560)
- etc. - display_pixels_y (1440)
Does NOT write:
- display_width
- display_height
- display_pixels_*
- Add printer/display config to
generate_config_ini()— Writedisplay_width,display_height,display_pixels_x,display_pixels_y,display_orientationinto the INI file so PrusaSlicer uses the same bed as the frontend - Add printer profile selection to frontend — Let users choose a printer profile (SL1, SL1S, Anycubic, custom) or enter custom display dimensions
- Return display config from backend API — Include
display_width,display_height,display_pixels_x,display_pixels_yin slice job status response so the frontend knows the actual bed size used - Sync
paramsStore.bedSizewith PrusaSlicer display — Frontend'sparamsStore.bedSize(currently [195.84, 122.4] for LS Plus) should match the PrusaSlicer printer profile, or be overridden by it - Read actual PNG dimensions — Instead of assuming 1440x2560, read the IHDR header from the first layer PNG to get actual pixel dimensions (supports different printer profiles)
- Handle display_orientation — Account for portrait vs landscape orientation when mapping pixel coordinates to world coordinates
- Add layer_height to job status response — Backend currently returns
layerCountbut notlayer_height; needed for accurate Z coordinate mapping
# Install dependencies
pip install -r requirements.txt
# Run with auto-reload
cd agent && uvicorn main:app --reload --port 5179cd web
npm install
npm run dev # Vite dev server with HMRIf you need to modify the PrusaSlicer fork:
- Edit source in
third_party/prusaslicer_fork/ - Rebuild:
cd third_party/prusaslicer_build make -j8 - Test the binary:
./src/prusa-slicer --help | grep export-support
Ensure PRUSA_SLICER_BIN is set correctly:
export PRUSA_SLICER_BIN=$(pwd)/third_party/prusaslicer_build/src/prusa-slicer
$PRUSA_SLICER_BIN --versionThe backend includes CORS middleware for common localhost dev origins and https://dentalslice.onrender.com.
To add more origins without editing code, set:
export CORS_ALLOWED_ORIGINS="https://your-ui.example.com,https://another.example.com"- Ensure "Enable Supports" is checked in config panel
- Check backend logs for "Support mesh exported" message
- Verify
has_support_mesh: truein job status response
| Feature | CLI | Backend API | Frontend | Notes |
|---|---|---|---|---|
| SLA Slicing | ✅ --export-sla |
✅ /execute |
✅ | Full layer export |
| Support Generation | ✅ --export-support-stl |
✅ /generate-supports |
✅ | Includes pad mesh |
| Hollow Interior | ✅ --export-hollow-stl |
✅ /generate-hollow |
✅ | Interior mesh only |
| Layer Preview | ✅ | ✅ /layers/{idx}.png |
✅ | PNG extraction from SL1 |
| 3D Visualization | - | - | ✅ | Three.js with Z-up |
- Drain Holes: Add
--export-drill-stlfor drain hole generation- PrusaSlicer has
DrainHoleinHollowing.hpp - Needs position input (click-to-place in UI)
- PrusaSlicer has
- Combined Hollow + Supports: Single operation for hollowed model with internal supports
- Hollow Preview Before Apply: Show preview without generating full mesh
- Auto-Orient: Expose PrusaSlicer's auto-orient via CLI
- Support Editing: Manual support point placement/removal
- Infill Patterns: Support for partial hollowing with infill
- Multi-Model Support: Handle multiple models in single job
- WebAssembly Port: Run hollowing in browser (OpenVDB is complex)
- Streaming Layers: WebSocket for real-time layer streaming during slice
- Diff Slicing: Only re-slice changed regions
-
Wall Thickness vs Model Size
- Small models need thinner walls (0.5-1mm)
- Large models can use thicker walls (2-3mm)
- Error "interior mesh is empty" means wall is too thick for model
-
Hollow Mesh Positioning
- Frontend must apply same transform as original model
- Currently copies position/rotation/scale from selected model
- If model is transformed after hollow generation, mesh will be misaligned
-
Memory Usage
- Hollowing uses OpenVDB which can be memory-intensive
- High quality setting (1.0) on large models may use several GB RAM
-
No Incremental Updates
- Changing hollow parameters requires full regeneration
- No caching of intermediate voxel grids
-
Single Model Per Job
- v2 API currently only processes first uploaded model
- Multi-model support requires job structure changes
Decision: Export only the hollow interior mesh, not a combined hollowed model.
Rationale:
- Frontend already has the original mesh
- Smaller data transfer (interior only vs full hollowed model)
- Can toggle hollow preview on/off without re-fetching
- Allows different materials/transparency for interior visualization
- PrusaSlicer stores interior separately in
sla::Interior
Trade-off: Frontend must combine meshes; can't directly print the exported hollow mesh.
Decision: Flip normals in PrusaSlicer CLI before export (sla::swap_normals()).
Rationale:
- Interior mesh faces inward by default (for boolean subtraction)
- Visualization requires outward-facing normals
- Better to flip once at export than in every frontend
- Consistent with how support mesh is exported
Decision: Hollow generation is a separate endpoint, not part of /execute.
Rationale:
- Hollow preview doesn't need full slicing
- Faster feedback loop for parameter tuning
- Can hollow without committing to slice
- Matches
/generate-supportspattern - Future: could cache hollow result for final slice
┌─────────────┐
│ created │ (in-memory, _pending_jobs)
└──────┬──────┘
│ upload model
▼
┌─────────────┐
┌─────────│ pending │─────────┐
│ └─────────────┘ │
│ generate-supports generate-hollow
▼ ▼
┌───────────┐ ┌───────────┐
│ processing│ │ processing│
└─────┬─────┘ └─────┬─────┘
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ completed │ │ completed │
│ +supports │ │ +hollow │
└───────────┘ └───────────┘
│
│ execute (full slice)
▼
┌───────────┐
│ completed │
│ +layers │
└───────────┘
This section is a self-reminder for future product and business evolution. Not all items are meant to be pursued at once.
Idea: Expose the slicer as a headless, scalable service rather than a desktop tool.
Potential value:
- Remove slicer maintenance burden for customers
- Enable cloud / automation / AI pipelines
- Natural fit for batch processing and scale
Target users:
- Manufacturing platforms
- Dental labs
- Cloud manufacturing services
- AI-generated model pipelines
Indicators to revisit:
- Stable job-based API
- Clear cost metrics (per job / per GB / per minute)
- Demand for non-interactive slicing
Idea: Sell decision-making instead of slicing itself.
Examples:
- Support quality evaluation
- Failure risk estimation
- Auto parameter / support suggestions
- Comparative analysis between slicing strategies
Why this matters:
- Support = process know-how, not just geometry
- Enables AI-driven optimization
- Hard to copy, high long-term value
Indicators to revisit:
- Support data is structured and comparable
- Repeated slicing failures observed in users
- Need for "why did this fail?" answers
Idea: Provide the slicer as an embedded or white-label component for hardware vendors.
Potential value:
- Recurring licensing revenue
- Strong fit with device-centric workflows
- Avoids consumer software competition
Target customers:
- 3D printer manufacturers
- Specialized hardware startups
- Non-general-purpose printing systems
Indicators to revisit:
- Requests for custom workflow / UI
- Need for tight hardware-software integration
- Vendor reluctance to maintain slicer teams
Idea: Turn slicing outputs and parameters into auditable, traceable production records.
Examples:
- Layer-level archives
- Parameter history
- Reproducibility reports
- Compliance-ready logs
Why it's valuable:
- Required in medical / dental / industrial contexts
- Seen as a cost of doing business, not a feature
- High willingness to pay
Indicators to revisit:
- Regulated customers (medical, dental, ISO)
- Need for print reproducibility
- QA / audit requirements
- This project is not just a slicer; it is a platform around slicing
- Engine choice is a means, not the product
- Supports, layers, and parameters are data assets, not UI details
- Monetization should prioritize process value, not feature count
PrusaSlicer is licensed under AGPLv3. See the fork repository for details.