Replace placeholder ImageCanvas with full QGraphicsView/QGraphicsScene editor:
Layout: ImageCanvas(QWidget) = top bar + _CanvasView(QGraphicsView) + hint bar
Top bar: garment/print filenames, mode buttons (移动/缩放/旋转, context-aware),
zoom controls (−/% label/+, scroll wheel).
_CanvasView:
- drawBackground: checkerboard in garment area, gray outside
- wheelEvent: zoom in/out anchored at cursor
- mousePressEvent/mouseMoveEvent/mouseReleaseEvent: delegate to ImageCanvas
Scene (1:1 with garment pixel coords):
- garment QGraphicsPixmapItem at (0,0), not interactive
- print QGraphicsPixmapItem positioned/rotated from TransformState
- cosmetic selection rect (blue, 1.5px)
- 4 corner handles (white circle, blue border) for scaling
- rotation arm (dashed) + handle (blue circle) above top-center
Interaction:
- Hit test: rotation handle > corner handles > print body (priority order)
- Move: delta in scene coords added to TransformState.x/y
- Scale: _scene_to_local_xy() maps mouse to item local coords, computes
new size keeping opposite corner fixed (_do_scale); supports aspect
ratio lock via Shift or keep_aspect_ratio flag; correct with rotation
- Rotate: atan2(mouse - center) + angle offset at press
Coordinate helpers (static):
- _local_to_scene_xy(lx, ly, state): item-local → scene
- _scene_to_local_xy(sx, sy, state): scene → item-local (inverse rotation)
Both use TransformState snapshot, not live item state, to avoid stale-pixmap issues.
transform_changed signal fires on mouse release with final TransformState.
Preview zoom changes only QGraphicsView transform; TransformState is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expand MainWindow from placeholder to full structural layout:
- Header bar: app name, version label, settings button
- Flow tab bar (QTabBar): tabs 2/3 disabled, click redirects to status bar message
- Work area (QSplitter): ImageListPanel (left 374px) | ImageCanvas (center, stretch) | right scroll with TemplatePanel/TransformPanel/ExportPanel (318px)
- Queue panel (188px, collapsible ▲/▼ toggle)
- QStatusBar with Windows-style styling
Applies color/font spec from docs/07-ui-design.md §10.
No business logic in main_window.py; panel stubs remain for tasks 8–13.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- compose(): scales print to TransformState.width/height, rotates
clockwise by TransformState.rotation (negated for Pillow), pastes
onto a transparent layer via alpha_composite, saves PNG or JPG
- Rotation centre fixed at print bounding-box centre; paste_x/y
computed from centre - rotated_size/2 per design doc §13.3
- JPG: flattens RGBA onto white RGB background before saving
- Out-of-bounds print clips silently via PIL paste; width/height<=0
returns ComposeResult(success=False); file errors caught, never raise
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BUILTIN_TEMPLATES: 4 presets (正方形/纵向/横向长方形/左胸小号)
- load_custom_templates(): reads templates.json, skips corrupt entries
individually so one bad record never blocks the rest
- get_all_templates(): builtin first, then custom
- add_template(): upsert by name, forces type="custom"
- rename_template() / delete_template(): raise ValueError if not found
- _save_custom_templates(): auto-creates config/ dir, logs on failure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Template, ExportOptions, BatchMode, BatchOptions, ComposeResult,
BatchResult to models.py. Keep existing ImageAsset and TransformState,
adding center() helper to TransformState.
Template uses ratio-based fields with to_transform_state() to convert
to pixel coordinates for any garment size.
BatchMode enum covers all four batch modes from PRD.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DEFAULT_CONFIG defines output_format, output_quality, output_dir,
last_garment_dir, last_print_dir
- load_config(): merges file values with defaults; never raises on
missing, damaged, or non-object JSON root
- save_config(): creates config/ dir as needed; logs error on failure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_app_dir(): project root in dev, exe dir in PyInstaller bundle
- get_resource_path/get_config_path: paths under app dir
- get_log_dir/get_output_dir: auto-create and return logs/ and output/
- Refactor log_service to use get_log_dir() instead of own __file__ calc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- setup_logging() creates logs/ dir and writes timestamped log file
- format: asctime name levelname message
- falls back to console-only if log dir is not writable
- main.py now calls setup_logging() at startup and logs app name/version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>