fix: per-item transform in batch export (correct placement across sizes)

QueuePanel reused one pixel TransformState for all non-fine-tuned items,
mis-placing garments/prints of other sizes. Store the ratio-based template
instead and recompute each item from its own garment/print size.

- add core.composer.image_size() and resolve_transform() (pure, tested)
- queue: set_template() replaces set_transform(); per-item resolve + size cache
- template_panel: expose current_template(); main_window pushes it to the queue
- tests: +4 covering fine-tune precedence, per-size recompute, empty inputs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 16:42:20 +08:00
co-authored by Claude Opus 4.8
parent 76f2c6dd5b
commit f2b08fecb3
6 changed files with 155 additions and 11 deletions
+42 -2
View File
@@ -9,8 +9,8 @@ from PIL import Image
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from core.composer import compose
from core.models import ExportOptions, TransformState
from core.composer import compose, resolve_transform
from core.models import ExportOptions, Template, TransformState
# ---------------------------------------------------------------------------
# helpers
@@ -207,5 +207,45 @@ class TestJpgOutput(_TmpDir, unittest.TestCase):
self.assertTrue(result.success, result.error)
class TestResolveTransform(unittest.TestCase):
"""resolve_transform: per-item placement for batch export."""
def _square_template(self):
# 25% box centred at (0.40,0.40)-(0.65,0.65); square print fills the box.
return Template(name="sq", x_ratio=0.40, y_ratio=0.40,
width_ratio=0.25, height_ratio=0.25)
def test_fine_tuned_transform_wins(self):
own = TransformState(x=1, y=2, width=3, height=4)
out = resolve_transform(self._square_template(), own, (1000, 1000), (50, 50))
self.assertIs(out, own)
def test_differently_sized_garments_get_different_pixels(self):
"""The core bug: one shared pixel transform mis-places other sizes.
Each garment must be laid out from its own dimensions."""
tpl = self._square_template()
small = resolve_transform(tpl, None, (1000, 1000), (100, 100))
large = resolve_transform(tpl, None, (2000, 3000), (100, 100))
# 1000px garment: 25% box = 250px, square print fills it, centred at 0.525.
self.assertAlmostEqual(small.width, 250.0)
self.assertAlmostEqual(small.x, 400.0)
# 2000x3000 garment: box is 500x750, square print contain-fits to 500,
# and x stays proportional — proving per-item recomputation.
self.assertAlmostEqual(large.width, 500.0)
self.assertAlmostEqual(large.x, 800.0)
self.assertNotAlmostEqual(small.x, large.x)
def test_none_template_returns_none(self):
self.assertIsNone(resolve_transform(None, None, (100, 100), (10, 10)))
def test_missing_sizes_return_none(self):
tpl = self._square_template()
self.assertIsNone(resolve_transform(tpl, None, None, (10, 10)))
self.assertIsNone(resolve_transform(tpl, None, (100, 100), None))
self.assertIsNone(resolve_transform(tpl, None, (0, 0), (10, 10)))
if __name__ == "__main__":
unittest.main()