Launcher core that reuses services/update_service: seed default config into
~/.cmbot on first run, check the manifest, download the release zip, verify
SHA-256, extract, validate version, swap into app/ (keeping app.old/ for
rollback), then launch app/CMBot.exe. Degrades to the local version on any
failure; checks install-root writability before updating.
- update_service: UpdateInfo gains sha256/size/min_supported; release URL
resolved to absolute; extract make_auth_header + add download() helper
- tests/test_launcher.py: 11 tests (seed, find-root, swap, sha mismatch,
no-update); also verified end-to-end against a local HTTP server
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packaged builds now resolve the data root to ~/.cmbot (%USERPROFILE%\.cmbot)
instead of the program dir, so user config/templates/output stay writable and
per-user regardless of where the program is installed or whether it is launched
via Launcher.exe (docs/10-lan-update.md §5). CMBOT_DATA_DIR still overrides.
- tests/test_file_service.py: 5 tests covering env override, frozen->~/.cmbot,
dev->project root, blank-env handling
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Launcher is a compiled Python/PyInstaller Launcher.exe (retires update.ps1
and install_local.ps1)
- portable: extract to any writable dir; InstallRoot = Launcher.exe's folder,
no longer tied to %LOCALAPPDATA%
- user data moves to ~/.cmbot (%USERPROFILE%\.cmbot): always writable, per-user,
survives program updates; get_data_dir() three-tier (env -> ~/.cmbot -> dev root)
- writability check now applies to the program dir only; first-run seeds default
templates from app\config into ~/.cmbot
- docs/10 §3/§4/§5/§8/§9/§11/§16 updated; tasks 17.19 added (docs done, code TODO)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#1 BOM: PowerShell Set-Content -Encoding UTF8 writes a BOM that made the in-app
json.loads silently fail (banner never showed even with an update available).
- update_service: decode manifest with utf-8-sig (HTTP + local)
- build.ps1: write manifest.json without BOM (UTF8Encoding $false)
- tests: +2 covering BOM manifests (HTTP + local)
#2 docs: docs/10 §16 status synced to the implemented app/app.old/version.txt +
SHA-256 model (stages 2/3 done + e2e verified); tasks 17.18 updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/update.ps1: check manifest, stage+verify the new version, swap it in
alongside the running one (versions\<ver> + current.txt pointer), then launch
with CMBOT_DATA_DIR. Degrades to the local version on any failure; never
overwrites the running version. Rollback = edit current.txt.
Verified against a fake versioned layout: update, idempotent re-run, unreachable
source, no source, corrupt download (marker missing), malformed manifest.
Not yet wired into a real install layout (build.ps1 still ships flat onedir).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read manifest.json from the configured update_source and show a dismissable
banner when a newer version is available. Notify-only — no install yet.
- services/update_service.py: version compare + check_for_update (pure, tested)
- config: add update_source key (empty = no check)
- main_window: top banner, background-thread check; open folder via os.startfile
(QDesktopServices.openUrl mishandles file:// folder URLs — ShellExecute err 2)
- tests: +15 covering version compare and check_for_update branches
- docs 02/05/10 + tasks 17.16
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- Output filename now includes the print: <garment>_<print>.<ext>, e.g.
1_TY030.png (folder grouping unchanged) (§17.12).
- Hide the single-export button (export via the queue) and remove the
orphaned static export hint label (§17.13).
- Fix: selecting a non-fine-tuned queue item now lays the print out per the
selected template (apply_current) instead of load_print's centred default;
still guarded by _loading_queue_item so it isn't marked 已微调 (§17.14).
Docs: PRD 6.6/6.8/9, UI design 7.6/7.7/10/12.4, tasks.md 17.12–17.14.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Auto-preview the first image after a folder loads (both panels), so the
canvas isn't empty (§17.8).
- Group exports as <output_dir>/<timestamp>/<print_stem>/<garment_stem>.<ext>;
one timestamp per export run, shared across the batch (§17.9).
- Remember the last garment/print folders and reopen the picker there;
persisted via config, wired through main_window (§17.10).
- Shrink the 3 box templates further and lower them (chest placement).
- Fix: batch export ignored the chosen format/quality/output dir because the
queue never received ExportOptions. Export panel now emits
export_options_changed → queue.set_export_options (+ initial sync) (§17.11).
Docs: PRD 6.1/6.8/9, architecture 4.12, UI design 5.1, tasks.md 17.8–17.11.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The in-content header strip showed the app name + version, duplicating the
OS window title (setWindowTitle). With the 设置 button already gone it had no
other purpose, so remove it (and its styles / unused _HEADER_HEIGHT). Name and
version stay visible via the OS title bar; the work area gains ~40px.
Docs: UI design 3 (layout) and 4.1 (title bar), tasks.md 17.7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Template delete: add a 删除 button (reuses template_service.delete_template)
with a confirm dialog; hidden for built-ins like 保存; reloads and re-selects
the first built-in after deletion, persisting the new selection.
- Whole reset: move the template-wide reset out of the template area to the
bottom of the transform panel and rename it to 位置尺寸旋转重置为当前模板, so it
sits with the params it resets, beside the three per-section resets.
- Compact layout: pair X/Y and 宽/高 onto single rows (short labels), and put
the export 格式/质量 selectors on one row. Sync logic unchanged.
Docs: PRD 6.6, UI design 7.1/7.2/7.3/7.4/7.5/10, tasks.md 17.4/17.5/17.6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§17.1 Preferences persistence:
- Wire the previously-unused config_service; remember and restore the
last-selected template and last batch mode. main_window loads config,
distributes initial values, then persists on change (save-on-change).
- template_panel: template_changed signal, select_template,
current_template_state. queue_panel: set_batch_mode + batch_mode_changed.
§17.2 Independent resets:
- Replace the combined reset and 归零 with per-section 位置/尺寸/旋转 reset
buttons (quiet ghost styling) that restore only their own aspect to the
selected template. Size reset changes width/height only, leaving X/Y.
Button de-dup:
- Remove the export panel's 应用为模板 / 重置为模板 — literal duplicates of
the template panel's 另存为 / 重置为模板. Template actions now live only
in the template area; the export panel handles output only.
Docs: PRD (6.6/6.9/9/10), architecture (4.12), UI design (7.x/10),
development-rules (6, single-home actions + consistent naming), tasks.md (17).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TransformPanel provides the right-panel parameter editing section:
- Position: X/Y QDoubleSpinBox (range ±99999 px, 0.1 step)
- Size: Width/Height QDoubleSpinBox (min 10 px) + 锁定宽高比例 checkbox
- When locked and width changes, height is auto-updated to maintain ratio
- When locked and height changes, width is auto-updated to maintain ratio
- Locking captures the current ratio at that moment
- Rotation: angle QDoubleSpinBox + 左旋 90° / 右旋 90° buttons
Signal-loop prevention:
- set_transform() sets self._updating = True and blockSignals on all inputs
before updating values, preventing valueChanged from re-firing _emit()
- _on_width_changed/_on_height_changed also blockSignals on the companion
spinbox when adjusting for aspect ratio to avoid double emission
Public API:
- set_transform(TransformState): programmatic update, no signal emitted
- get_transform() -> TransformState: read current inputs
- transform_changed signal: emitted whenever user edits any input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>