Metadata-Version: 2.3
Name: legolization
Version: 0.2.0
Summary: Python CLI for converting a 3D model to LDraw.
Keywords: ldraw,lego,cad,3d,modeling,parts,bricks
Author: Harold Martin
Author-email: Harold Martin <harold.martin@gmail.com>
License: GPL-3.0-or-later
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Artistic Software
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Dist: clarabel>=0.11.1
Requires-Dist: cvxpy>=1.9.2
Requires-Dist: highspy>=1.15.1
Requires-Dist: numpy>=2.5.1
Requires-Dist: osqp>=1.1.3
Requires-Dist: pyldraw3>=1.0.0
Requires-Dist: scipy>=1.18.0
Requires-Dist: scs>=3.2.11
Requires-Dist: trimesh>=4.12.2
Requires-Python: >=3.12
Project-URL: Homepage, https://github.com/hbmartin/legolization
Project-URL: Repository, https://github.com/hbmartin/legolization
Project-URL: Documentation, https://github.com/hbmartin/legolization/blob/main/README.md
Project-URL: Changelog, https://github.com/hbmartin/legolization/releases
Description-Content-Type: text/markdown

# legolization

Turn a colored voxel model into a **physically buildable LEGO model** in LDraw
format, with step-by-step build instructions and a bill of materials.

This is the classic "LEGO construction problem" from the research literature
(see `references/`): voxelize → hollow → place bricks → check structural
stability → repair → export. The stability check is a full
**Rigid-Block-Equilibrium (RBE)** model (StableLego formulation, cross-validated
against its released test fixtures): per-brick force *and* torque balance with
knob-friction capacities, solved as a provably exact linear program on an open
solver stack — no Gurobi required.

## What it does

- **Input**: a MagicaVoxel `.vox` file or a numpy `.npy` array (LDraw colour
  codes or RGB(A) voxels — colours are quantized to the nearest solid LDraw
  colour, with optional Floyd-Steinberg dithering for gradients).
- **Placement**: covers every voxel with bricks and plates at true heights
  (plate = 8 LDU, brick = 24 LDU) using one of six strategies; tiles and 45°
  slopes are opt-in finishing passes (`--tiles`, `--slopes` — the catalogued
  33° slope and 2x2 slope are not yet placed by any pass):
  - `greedy` (default): largest-first bottom-up fill with Kollsker's
    remainder-lookahead h(r) and distance-decayed stretcher-bond scoring,
    then delete-and-rebuild reinforcement around the weakest bricks.
  - `luo`: Luo et al. (2015) maximal random merge with split-and-remerge
    refinement, accepted by Luo's maximin friction capacity C_M; supports
    soft colour constraints (`--colour soft`).
  - `bond`: Kollsker & Malaguti's constructive brick-bonding heuristic —
    remainder lookahead + staggering reward + per-layer repair.
  - `fast`: Bao et al.'s greedy per-layer merge with a dominant big-brick
    weight, perpendicularity term, and connectivity retries.
  - `smga`: Lee et al.'s split-and-merge genetic algorithm per layer
    (`--ga-generations`, `--time-budget`).
  - `beauty`: Min et al.'s objective-driven tiling with symmetry/balance,
    stability-priority, and big-brick terms (`--beauty-preset
    {balanced,stability,aesthetics,efficiency}`).
- **Physics**: every layout is scored by the RBE — gravity, support, press,
  drag/pull friction (capacity T = 0.98 N per contact point), knob presses,
  and torque-capable side presses at shared-face extremes (side-supported
  structures shed load like Luo's bridges). Equilibrium residuals sit *in the
  objective*, so even collapsing structures solve and failures localize to
  specific bricks.
- **Repair**: unstable layouts go through an ALNS destroy-and-repair pass
  (Kollsker's artificial-link QP pinpoints the deficit; the freed region is
  refilled by the merge engine or an exact-cover MILP) before any material is
  added back by the stability-aware hollow-restore loop.
- **Auto-hollow**: interiors are hollowed to a shell (~1 brick thick, tunable
  with `--shell-plates`); interior cells are colour-free so merges never
  fragment on invisible boundaries.
- **Instructions**: smart step sequencing (default) chunks each layer into
  ~7-brick spatially coherent steps, keeps mirror-symmetric halves together,
  guarantees every prefix is stable and vertically insertable (or warns), and
  adds `0 ROTSTEP` view hints. `--bom out.json` writes a bill of materials
  with per-step callouts.
- **Output**: a valid `.ldr` or `.mpd` written through
  [pyldraw3](https://pypi.org/project/pyldraw3/). Open it in
  [LDView](https://tcobbs.github.io/ldview/) or
  [BrickLink Studio](https://www.bricklink.com/v3/studio/download.page).

## Setup

```sh
uv sync
uv run ldraw download   # once: fetch the LDraw parts library
uv run ldraw generate   # once: generate ldraw.library.* part/colour modules
```

## Usage

```sh
uv run legolization data/examples/heart.vox -o heart.ldr
uv run legolization model.npy --strategy beauty --beauty-preset aesthetics
uv run legolization model.vox --strategy bond --bom parts.json
uv run legolization model.npy --strategy luo --solid --seed 7
uv run legolization model.vox --slopes --tiles      # surface finishing passes
uv run legolization model.vox --aspect-correct      # keep cubic voxel aspect
uv run legolization model.vox --milp                # cross-check the exact LP
```

The CLI reports brick count, mass, step count, and the physics verdict:

```text
wrote heart.ldr
  bricks: 12   mass: 17.7 g   steps: 7   slopes: 0   tiles: 0
  stability: STABLE (worst score 0.001, min capacity 0.979 N)
```

Exit code 0 means the model is stable, one stud-connected component, and
ground-connected. Exit 2 means it is not buildable as-is (try another
`--strategy`, `--solid`, or a different `--seed`) — note that an input made of
several disconnected voxel islands is reported as multiple components even
when every island stands on the ground.

Python API:

```python
from pathlib import Path
from legolization import PipelineConfig, VoxelGrid, run, run_file

result = run_file(Path("model.vox"), Path("model.ldr"), PipelineConfig(seed=1))
print(result.buildable, result.step_count, result.stability.max_score)
```

## How the stability model works

Each mated stud contributes 3 or 4 contact points (per StableLego's measured
geometry) carrying a shared normal force and a friction (drag/pull) force, so
Newton's third law holds by construction; each knob adds four horizontal
knob-press forces, and laterally touching bricks exchange two side presses at
the shared face's vertical extremes so lateral load transfer carries torque.
Per brick, five equilibrium residuals (3 forces, 2 torques about the mass
centroid) are minimized rather than constrained. A brick scores `1` when it
cannot reach equilibrium or its friction demand exceeds T; otherwise
`drag_max / T` — so the score doubles as a stress heatmap. The default solver
is a hand-assembled LP on scipy/HiGHS, and the relaxation is provably exact
(each contact's press and pull columns are exact negatives, so no optimum ever
uses both); `--milp` re-verifies with explicit big-M complementarity via
cvxpy. The whole stack reproduces all nine verdicts of the StableLego
release's test fixtures (vendored under `tests/data/stablelego/`).

## Benchmark

`uv run python scripts/benchmark.py` compares all six strategies across the
example models (brick count, stability margin, seam/perpendicularity/symmetry
metrics, runtime). Highlights at seed 0: `bond` and `beauty` cover the arch in
13 bricks where largest-first greedy needs 32, and `beauty --beauty-preset
aesthetics` produces perfectly mirror-symmetric layers on symmetric models.

## Development

```sh
uv run pytest          # analytic physics, placement invariants, golden pins
uv run ruff format --check . && uv run ruff check .
uv run ty check src tests
uv run pyrefly check src tests
```

## License

GPL-3.0-or-later (inherited from pyldraw3).
