An intelligent 3D packing optimization service with interactive visualization.
- Physics-aware heuristic packing algorithm considering:
- Weight limits and distribution
- Stability and support (60% minimum support ratio)
- Center of mass balance
- Layering (heavy objects at the bottom)
- Edge-anchored placement that packs objects flush against their neighbours instead of snapping only to the search grid, for tighter results
- Automatic multi-container management
- Optional object rotations (enabled via request flag or environment variable)
- Per-container and aggregate utilization metrics (volume and weight) reported in every response and stream
- Packaging-material estimation: the void (empty) volume of every finished container that must be filled with cushioning material, reported per container and aggregated per shipment
- Configurable request guardrails (max objects / container types per request) returning clear
422errors - Bounded position search: the per-axis placement grid is capped (512 positions), so even adversarially huge container dimensions cannot stall the service
- Cooperative cancellation: live-stream packing stops immediately when the SSE client disconnects
- Background GitHub release updater with checksum verification and configurable rate-limit handling
- Native release installers for Linux (
.deb), macOS (.pkg), and Windows (.msix) - Reusable library crate plus an offline CLI (
packsubcommand) sharing the same engine as the HTTP API - Comprehensive unit, integration, and doc tests
- REST API with JSON communication, plus
GET /health,GET /version, andGET /configfor monitoring and introspection - OpenAPI & Swagger UI with live documentation at
/docs - OOP principles with DRY architecture
- Fully documented code (Rust docstrings)
- Interactive 3D visualization
- OrbitControls for camera control
- Container navigation (Previous/Next buttons)
- Step-by-step animation of the packing process
- Highlighted live/animation focus for the current placement step
- Live statistics:
- Object count
- Total weight and weight load
- Volume utilization (authoritative backend value)
- Packaging material (void volume) needed per container and in total
- Center of mass position and balance
- Aggregate fill (volume / weight) across all containers
- Packing status panel with progress and configuration readiness
- Unplaced object panel with rejection reasons
- JSON result export of the current batch or live run (button or
Eshortcut) - Configuration modal with object rotation toggle
- Persistent configuration via browser local storage
- Keyboard shortcuts for batch/live runs, animation, navigation, export, and configuration
- Inline validation and toast notifications for faster feedback
- Responsive design
- Rust (1.70+)
- Cargo
- Modern web browser
- Python 3 (only needed for the Unix one-command installer)
cargo runThe server runs on http://localhost:8080
π‘ Configuration note: Copy
.env.exampleto.envif needed to customize the API port, host, or update parameters. Unset values automatically fall back to their defaults.
The same binary can optimize a request without starting the server, which is handy for scripting and pipelines. It shares the exact validation and packing logic as the /pack endpoint.
# Read a PackRequest from a file and print the PackResponse as JSON
cargo run -- pack request.json
# Read from stdin
cat request.json | cargo run -- pack
echo '{"containers":[{"dims":[10,10,10],"max_weight":100}],"objects":[{"id":1,"dims":[10,10,5],"weight":40}]}' \
| cargo run -- pack -
# Help and version
cargo run -- --help
cargo run -- --versionThe pack subcommand exits non-zero with a descriptive message on invalid input.
The web client is automatically served by the Rust backend. After startup, simply open http://localhost:8080/ in your browser.
π Same-origin note: The frontend intentionally calls
/packand/pack_streamon the same origin that serves the UI. This matches the default local setup (cargo run) and the production deployment model where the Rust backend serves both API and web assets.
In the browser:
- Button "π Pack (Batch)" performs a one-time optimization and displays the result.
- Button "π‘ Pack (Live)" starts the live stream of optimization steps via SSE and renders them continuously.
- Saved configurations are restored automatically after a page reload.
- The status and unplaced-object panels provide immediate feedback without blocking dialogs.
- Keyboard shortcuts:
B= batch packingL= live packingC= open configurationE= export the current result as JSONβ/β= switch containersSpace= start/stop animation
A GitHub Actions workflow (.github/workflows/release.yml) exists for releases that generates platform packages when tags in the format v* are created (or manually via workflow_dispatch):
- Linux (x86_64):
sort-it-now-<version>-linux-x86_64.tar.gz - Linux native installer:
sort-it-now-<version>-linux-x86_64.deb - macOS (ARM64/Apple Silicon):
sort-it-now-<version>-macos-arm64.tar.gz - macOS (x86_64/Intel):
sort-it-now-<version>-macos-x86_64.tar.gz - macOS native installer:
sort-it-now-<version>-macos-<arch>.pkg - Windows (x86_64):
sort-it-now-<version>-windows-x86_64.zip - Windows native installer:
sort-it-now-<version>-windows-x86_64.msix
Each archive package contains the pre-compiled binary, the current README.md, and installation/uninstallation scripts.
The artifacts are uploaded both as workflow artifacts and automatically added to the GitHub release for the corresponding tag version.
The install scripts detect supported platforms and download the latest matching release. Currently supported targets are Linux (x86_64), macOS (arm64 and x86_64), and Windows (x86_64). No version entry or other modifications are required. The commands below stream the script directly into the shell / PowerShell, so no temporary script download is needed.
-
Linux / macOS install:
curl -fsSL https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/install.sh | bashRequires
bash,curl, andpython3. The Unix install script usespython3to parse GitHub release metadata, so on minimal systems you may need to installpython3first. -
Linux / macOS uninstall:
curl -fsSL https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/uninstall.sh | bash -
Windows install (PowerShell, run as Administrator for the default destination under
%ProgramFiles%):irm "https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/install.ps1" | iex
-
Windows uninstall (PowerShell):
irm "https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/uninstall.ps1" | iex
To install a specific version instead of the latest release, set the environment variable SORT_IT_NOW_VERSION to a release tag (for example v1.5.0):
# Linux / macOS (when installing with sudo, preserve the pinned version variable)
curl -fsSL https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/install.sh | sudo env SORT_IT_NOW_VERSION=v1.5.0 bash# Windows
$env:SORT_IT_NOW_VERSION="v1.5.0"; irm "https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/install.ps1" | iexIf you prefer to review the script before execution, you can still download it manually first.
Both installer scripts also continue to work locally from an extracted release bundle. Set INSTALL_DIR (Unix) or -Destination (PowerShell, for example "$env:LOCALAPPDATA\Programs\sort-it-now" for a per-user install) to override the default target.
- Linux/macOS: Run
./install.shin the extracted folder (optionally withsudo) to copysort_it_nowto/usr/local/bin. - Linux/macOS: Run
./uninstall.shin the extracted folder to remove a prior archive-based installation again. - Windows: Run
install.ps1(PowerShell). By default, it installs to%ProgramFiles%\sort-it-nowand adds the path to the user environment variable. - Windows: Run
uninstall.ps1to remove the installed binary and clean the user PATH entry again.
- Linux (
.deb): Install withsudo dpkg -i sort-it-now-<version>-linux-x86_64.deb, uninstall withsudo dpkg -r sort-it-now. - macOS (
.pkg): Install withsudo installer -pkg sort-it-now-<version>-macos-<arch>.pkg -target /. Use the uninstall shell script afterwards if you want to remove the binary from/usr/local/bin. - Windows (
.msix): Each release workflow run produces a signed MSIX together with a matching.cercertificate for that specific release. Import the certificate for the version you want to install into the trusted people store, then install the package withAdd-AppxPackage .\sort-it-now-<version>-windows-x86_64.msix. Because the workflow currently signs with a repository-generated self-signed certificate, you may need to repeat the import step for a different release, and you should only trust a certificate when the release came from the official repository and the published checksums were verified.
For each release, a Docker image is automatically published to Docker Hub. Images are provided for multiple architectures (linux/amd64, linux/arm64).
π Setup guide: See DOCKER_SETUP.md for a detailed guide on setting up the Docker Hub deployment pipeline.
Run Docker image:
Note: Replace
<username>withjosunlp(or the corresponding Docker Hub username of the project maintainer).
docker run -p 8080:8080 -e SORT_IT_NOW_SKIP_UPDATE_CHECK=1 <username>/sort-it-now:latestWith environment variables:
docker run -p 8080:8080 \
-e SORT_IT_NOW_API_HOST=0.0.0.0 \
-e SORT_IT_NOW_API_PORT=8080 \
-e SORT_IT_NOW_SKIP_UPDATE_CHECK=1 \
<username>/sort-it-now:latestBuild your own image:
docker build -t sort-it-now .
docker run -p 8080:8080 -e SORT_IT_NOW_SKIP_UPDATE_CHECK=1 sort-it-nowThe server is then available at http://localhost:8080.
On startup, the service checks for the latest GitHub releases (JosunLP/sort-it-now) in the background. If a newer version is found, the updater downloads the archive package matching the current platform and replaces the running binary in place (whatever it is named). Native installers (.deb, .pkg, .msix) are published alongside the archive assets for manual installation flows. On Windows, if the running executable is locked, a <name>.new file is placed next to it instead.
SORT_IT_NOW_UPDATE_MODEselects the behaviour:install(default) downloads and installs automatically,notifyonly reports that a new release exists, andoffskips the check entirely.- The check can also be disabled via the environment variable
SORT_IT_NOW_SKIP_UPDATE_CHECK=1(e.g., for offline installations or CI). - GitHub limits unauthenticated API calls to 60 per hour. If the limit is reached, the check is skipped and info is displayed. Optionally set
SORT_IT_NOW_GITHUB_TOKEN(orGITHUB_TOKEN) to a Personal Access Token to get higher limits; the updater also uses the token when downloading release artifacts. - To avoid unexpectedly large downloads, the updater limits release artifacts to 200 MB by default. Adjust the limit via
SORT_IT_NOW_MAX_DOWNLOAD_MB(value0disables the limit). - Repo/owner and timeout can be configured via
SORT_IT_NOW_GITHUB_OWNER,SORT_IT_NOW_GITHUB_REPO, andSORT_IT_NOW_HTTP_TIMEOUT_SECSβ defaults apply automatically if no.envis present.
GET /docsdelivers an interactive Swagger UI with Subresource Integrity-protected assets.GET /docs/openapi.jsonprovides the OpenAPI schema (v3) and can be used for code generators.
GET /healthreturns{ "status": "ok" }and is suitable as a liveness/readiness probe.GET /versionreturns the running build'sname,version, anddescription.GET /configreturns the active packing configuration (grid step, support ratio, tolerances, rotation default) and the per-request guardrails (max_objects,max_containers).
Packs objects into containers.
Request:
{
"containers": [
{ "name": "Standard", "dims": [100.0, 100.0, 70.0], "max_weight": 500.0 },
{ "name": "Compact", "dims": [60.0, 80.0, 50.0], "max_weight": 320.0 }
],
"objects": [
{ "id": 1, "dims": [30.0, 30.0, 10.0], "weight": 50.0 },
{ "id": 2, "dims": [20.0, 50.0, 15.0], "weight": 30.0 }
],
"allow_rotations": true
}The optional field allow_rotations enables 90Β° rotations per request. If omitted, the default setting from the environment variable SORT_IT_NOW_PACKING_ALLOW_ROTATIONS (default: false) applies.
Response:
{
"results": [
{
"id": 1,
"template_id": 0,
"label": "Standard",
"dims": [100.0, 100.0, 70.0],
"max_weight": 500.0,
"total_weight": 80.0,
"placed": [
{
"id": 1,
"pos": [0.0, 0.0, 0.0],
"weight": 50.0,
"dims": [30.0, 30.0, 10.0]
}
],
"diagnostics": {
"center_of_mass_offset": 0.0,
"balance_limit": 63.6,
"imbalance_ratio": 0.0,
"average_support_percent": 100.0,
"minimum_support_percent": 100.0,
"volume_utilization_percent": 1.93,
"weight_utilization_percent": 16.0,
"packaging": {
"container_volume": 700000.0,
"used_volume": 13510.0,
"void_volume": 686490.0,
"void_volume_percent": 98.07
},
"support_samples": []
}
}
],
"unplaced": [],
"is_complete": true,
"diagnostics_summary": {
"max_imbalance_ratio": 0.0,
"worst_support_percent": 100.0,
"average_support_percent": 100.0,
"average_volume_utilization_percent": 1.93,
"average_weight_utilization_percent": 16.0,
"packaging": {
"total_container_volume": 700000.0,
"total_used_volume": 13510.0,
"total_void_volume": 686490.0,
"average_void_volume_percent": 98.07
}
}
}The packaging object reports the void volume β the empty space inside each finished container that has to be filled with cushioning material (air pillows, foam, packing paper, β¦) to immobilise the load during transport. void_volume is given in cubic units (cmΒ³ when dimensions are in cm); void_volume_percent is the complement of volume_utilization_percent. The diagnostics_summary.packaging block aggregates this across every opened container, so total_void_volume is the total amount of packaging material a shipment needs.
Streams progress events in real-time as text/event-stream. Each event is a JSON object with a type field:
ContainerStarted{ id, dims, max_weight, label, template_id }ObjectPlaced{ container_id, id, pos, weight, dims, total_weight }Finished
Note: In the frontend, you can start live mode with the "π‘ Pack (Live)" button.
cargo testAll tests should pass successfully:
- β heavy_boxes_stay_below_lighter
- β single_box_snaps_to_corner
- β creates_additional_containers_when_weight_exceeded
- β reject_heavier_on_light_support
- β sample_pack_respects_weight_order
- Application entry point
- Starts the Tokio runtime and API server
Box3D: Represents a 3D object with ID, dimensions, and weightPlacedBox: Object with position in the containerContainer: Packaging container with capacity limits- Methods:
volume(),base_area(),total_weight(),remaining_weight(),utilization_percent(),free_volume(),packaging_fill()
intersects(): AABB collision detection between two objectsoverlap_1d(): Calculates 1D overlapoverlap_area_xy(): Calculates XY overlap areapoint_inside(): Point-in-box test
PackagingFill: Void-fill / packaging-material requirement for a single containerPackagingSummary: Aggregated packaging-material requirement across all containersPackagingAccumulator: Folds per-container fills into a summary (DRY aggregation)
PackingConfig: Configurable parameters (grid, support ratio, tolerances)pack_objects(): Main packing algorithmpack_objects_with_config(): Version with customizable parametersfind_stable_position(): Finds stable position for an objectsupports_weight_correctly(): Checks weight hierarchyhas_sufficient_support(): Checks minimum support ratiocalculate_balance_after(): Calculates center of mass deviationcompute_container_diagnostics(): Per-container metrics including packaging-material volume
- REST API with Axum framework
- CORS support for frontend communication
- JSON serialization/deserialization
- Three.js scene setup
- OrbitControls for camera
- Functions:
clearScene(): Clears scenedrawContainerFrame(): Draws container wireframedrawBox(): Renders individual objectvisualizeContainer(): Shows complete containeranimateContainer(): Step-by-step animationupdateStats(): Updates statistics panelfetchPacking(): API communication
PackingConfigstructure instead of scattered constants- Reusable functions for geometry calculations
- Centralized error handling
- Clear separation of data models and logic
- Encapsulation in modules
- Trait implementation for common behavior
- Rust docstrings for all public functions
- JSDoc comments in frontend
- Inline comments for complex algorithms
The application optionally loads a .env file on startup (using dotenvy). Unset variables retain their defaults, so the service runs normally even without .env. Relevant variables:
| Variable | Default | Description |
|---|---|---|
SORT_IT_NOW_API_HOST |
0.0.0.0 |
IP address the HTTP server binds to. Set e.g. 127.0.0.1 for local access. |
SORT_IT_NOW_API_PORT |
8080 |
API server port. Values of 0 are rejected. |
SORT_IT_NOW_MAX_OBJECTS |
10000 |
Maximum objects accepted per request (0 = unlimited). Exceeding it returns 422. |
SORT_IT_NOW_MAX_CONTAINERS |
1000 |
Maximum container types accepted per request (0 = unlimited). Exceeding it returns 422. |
SORT_IT_NOW_GITHUB_OWNER |
JosunLP |
GitHub owner/organization whose releases are queried for updates. |
SORT_IT_NOW_GITHUB_REPO |
sort-it-now |
Repository name for the updater. |
SORT_IT_NOW_HTTP_TIMEOUT_SECS |
30 |
Connect/read timeout in seconds for the updater's GitHub requests (downloads are not cut off). |
SORT_IT_NOW_MAX_DOWNLOAD_MB |
200 |
Maximum size of a release asset (0 = unlimited). |
SORT_IT_NOW_GITHUB_TOKEN / GITHUB_TOKEN |
β | Optional PAT for higher GitHub rate limits and private releases. |
SORT_IT_NOW_SKIP_UPDATE_CHECK |
β | If set (any value), disables automatic update check. |
SORT_IT_NOW_UPDATE_MODE |
install |
Update behaviour: install (download + install), notify (report only), or off (skip). |
SORT_IT_NOW_PACKING_GRID_STEP |
5.0 |
|
SORT_IT_NOW_PACKING_SUPPORT_RATIO |
0.6 |
|
SORT_IT_NOW_PACKING_HEIGHT_EPSILON |
1e-3 |
|
SORT_IT_NOW_PACKING_GENERAL_EPSILON |
1e-6 |
|
SORT_IT_NOW_PACKING_BALANCE_LIMIT_RATIO |
0.45 |
|
SORT_IT_NOW_PACKING_ALLOW_ROTATIONS |
false |
Enables all 90Β° object rotations. Can also be set per request via allow_rotations. |
An example file can be found in .env.example.
PackingConfig {
grid_step: 5.0, // Position grid in units
support_ratio: 0.6, // 60% minimum support
height_epsilon: 1e-3, // Height tolerance
general_epsilon: 1e-6, // General tolerance
balance_limit_ratio: 0.45, // Max center of mass deviation
allow_item_rotation: false, // Enable object rotations (disabled by default)
}const CONTAINER_SIZE = [100, 100, 70]; // Container dimensions
const COLOR_PALETTE = [...]; // Object colors- Throughput: ~100 objects/second
- Memory: O(n) for n objects
- Complexity: O(n Γ p Γ z) where:
- n = number of objects
- p = grid positions
- z = Z-levels
- Rotation: Only 90Β° rotations; complex freeform rotations are not covered
- Dynamic stability: No physical simulation
- Optimal packing: Heuristic, no guaranteed optimum
- Browser support: Requires WebGL support
Project-specific - See license file.
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Open a pull request
For questions or issues, please open an issue.
Developed with β€οΈ in Rust & Three.js