feat: thumbnail grid, aspect-aware templates, and UI hierarchy polish

Material list:
- Switch asset list from one-per-row to a wrapping thumbnail grid
  (IconMode cards: thumbnail + corner checkbox + elided name, subfolder
  moved to tooltip).

Templates:
- Template.to_transform_state now contains-fits the print inside the
  target box by the print's native aspect ratio (no stretch); falls back
  to the box when print size is unknown.
- Feed print native size into template_panel for aspect-aware fitting.
- Shrink built-in template boxes to realistic print proportions.

UI hierarchy:
- Promote 打开文件夹 buttons to primary (filled accent).
- Add inline 归零 (reset angle) and a 重置位置/尺寸/角度 button to the
  transform panel; wire it to re-apply the selected template.
- Hide 保存 for built-in templates instead of leaving it greyed.

Docs: record template box/contain behavior (PRD) and the grid layout,
button hierarchy, and reset/save-visibility rules (UI design).
Tests: add 3 aspect-fit cases for to_transform_state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 15:11:56 +08:00
co-authored by Claude Opus 4.8
parent df9145abdb
commit 8a34fe93ff
9 changed files with 273 additions and 78 deletions
+42
View File
@@ -192,6 +192,48 @@ class TestTemplateToTransformState(unittest.TestCase):
self.assertAlmostEqual(state.width, 100.0)
self.assertAlmostEqual(state.height, 200.0)
def test_aspect_fit_square_box_wide_print(self):
"""A wide print in a square box fits by width and stays undistorted."""
from core.models import Template
# Box: 200x200 at (100,100) on a 400x400 garment
t = Template(name="t", x_ratio=0.25, y_ratio=0.25,
width_ratio=0.5, height_ratio=0.5)
state = t.to_transform_state(400, 400, print_width=100, print_height=50)
# Wide print (2:1) → limited by width: 200 wide, 100 tall
self.assertAlmostEqual(state.width, 200.0)
self.assertAlmostEqual(state.height, 100.0)
# Aspect ratio preserved
self.assertAlmostEqual(state.width / state.height, 100 / 50)
# Centered in the box (centre at 200,200)
self.assertAlmostEqual(state.x + state.width / 2, 200.0)
self.assertAlmostEqual(state.y + state.height / 2, 200.0)
def test_aspect_fit_square_box_tall_print(self):
"""A tall print in a square box fits by height and stays undistorted."""
from core.models import Template
t = Template(name="t", x_ratio=0.25, y_ratio=0.25,
width_ratio=0.5, height_ratio=0.5)
state = t.to_transform_state(400, 400, print_width=50, print_height=100)
# Tall print (1:2) → limited by height: 100 wide, 200 tall
self.assertAlmostEqual(state.width, 100.0)
self.assertAlmostEqual(state.height, 200.0)
self.assertAlmostEqual(state.x + state.width / 2, 200.0)
self.assertAlmostEqual(state.y + state.height / 2, 200.0)
def test_aspect_fit_never_exceeds_box(self):
"""Fitted print must never exceed the template box bounds."""
from core.models import Template
t = Template(name="t", x_ratio=0.34, y_ratio=0.22,
width_ratio=0.32, height_ratio=0.32)
# Portrait garment, square print
state = t.to_transform_state(800, 1200, print_width=300, print_height=300)
box_w = 0.32 * 800
box_h = 0.32 * 1200
self.assertLessEqual(state.width, box_w + 1e-6)
self.assertLessEqual(state.height, box_h + 1e-6)
# Square print stays square (no stretch despite portrait garment)
self.assertAlmostEqual(state.width, state.height)
# ---------------------------------------------------------------------------
# File scanning