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
+12 -1
View File
@@ -257,8 +257,14 @@ class MainWindow(QMainWindow):
# Template selection updates canvas, param panel, and queue template
self.template_panel.template_applied.connect(self.image_canvas.set_transform)
self.template_panel.template_applied.connect(self.transform_panel.set_transform)
self.template_panel.template_applied.connect(self.queue_panel.set_transform)
self.template_panel.template_applied.connect(self.export_panel.set_transform)
# Give the queue the template itself (ratio-based) so each item is laid
# out from its own garment/print size, not one shared pixel transform.
# template_changed fires on selection even before a garment is loaded;
# template_applied covers re-applies once sizes are known.
self.template_panel.template_changed.connect(self._sync_queue_template)
self.template_panel.template_applied.connect(self._sync_queue_template)
self.queue_panel.set_template(self.template_panel.current_template()) # initial sync
# Output settings (dir/format/quality) flow to the queue for batch export
self.export_panel.export_options_changed.connect(self.queue_panel.set_export_options)
@@ -346,6 +352,11 @@ class MainWindow(QMainWindow):
self._loading_queue_item = False
self._update_reset_availability()
def _sync_queue_template(self, _state=None):
"""Push the currently selected template (ratio-based) to the queue so
batch export recomputes each item from its own garment/print size."""
self.queue_panel.set_template(self.template_panel.current_template())
def _on_transform_changed(self, state):
"""Route transform changes to panels and mark the active queue item fine-tuned."""
# Keep template panel and export panel up to date for save/export actions
+37 -8
View File
@@ -21,8 +21,8 @@ from PySide6.QtWidgets import (
QWidget,
)
from core.composer import compose
from core.models import BatchMode, ExportOptions, ImageAsset, TransformState
from core.composer import compose, image_size, resolve_transform
from core.models import BatchMode, ExportOptions, ImageAsset, Template, TransformState
from services.file_service import (
get_output_dir,
make_safe_output_path,
@@ -182,7 +182,8 @@ class QueuePanel(QWidget):
super().__init__(parent)
self._garments: List[ImageAsset] = []
self._prints: List[ImageAsset] = []
self._template_transform: Optional[TransformState] = None
self._template: Optional[Template] = None
self._size_cache: dict = {} # path -> (w, h), reused within a batch run
self._export_options: ExportOptions = ExportOptions()
self._running = False
self._setup_ui()
@@ -277,9 +278,14 @@ class QueuePanel(QWidget):
self._prints = prints
self._rebuild_queue()
def set_transform(self, state: TransformState):
"""Store the template transform applied to items that have not been fine-tuned."""
self._template_transform = state
def set_template(self, template: Optional[Template]):
"""Store the selected template (ratio-based) for items not fine-tuned.
Each item's pixel transform is recomputed from its own garment and
print sizes at export time, so differently sized images in one batch
are placed correctly (see _resolve_transform).
"""
self._template = template
def set_export_options(self, options: ExportOptions):
self._export_options = options
@@ -366,6 +372,7 @@ class QueuePanel(QWidget):
def _new_run_dir(self):
"""Timestamped folder shared by all images in one export run."""
self._size_cache.clear() # fresh sizes per run
base = self._export_options.output_dir or str(get_output_dir())
return timestamped_run_dir(base)
@@ -387,11 +394,33 @@ class QueuePanel(QWidget):
self._running = False
self._batch_btn.setText("开始批量导出")
def _image_size(self, path):
"""Return (w, h) for path, caching within a run. None if unreadable."""
key = str(path)
if key not in self._size_cache:
try:
self._size_cache[key] = image_size(path)
except (OSError, ValueError) as exc:
logger.error("Cannot read image size %s: %s", key, exc)
self._size_cache[key] = None
return self._size_cache[key]
def _resolve_transform(self, item: QueueItem) -> Optional[TransformState]:
"""Per-item transform: the item's own fine-tune, else the selected
template recomputed for this item's garment and print sizes."""
if item.transform:
return item.transform
return resolve_transform(
self._template, None,
self._image_size(item.garment.path),
self._image_size(item.print_asset.path),
)
def _export_item(self, row: int, item: QueueItem, run_dir):
transform = item.transform or self._template_transform
transform = self._resolve_transform(item)
if not transform:
item.status = "失败"
item.error = "无印花参数(请先在画布中调整印花位置)"
item.error = "无印花参数(请先选择模板或在画布中调整印花位置)"
self._model.update_row(row)
return
+8
View File
@@ -165,6 +165,14 @@ class TemplatePanel(QWidget):
return self._state_for_template(t)
return None
def current_template(self) -> Optional[Template]:
"""Return the currently selected template (ratio-based), or None.
The queue uses this to recompute each item's placement from its own
garment and print sizes, rather than reusing one pixel transform.
"""
return self._current_template()
def set_transform(self, state: TransformState):
"""Store current print transform (used when saving a template)."""
self._current_state = state